Abstract away or eliminate most of the mutation of the Elements array (#955)

This commit is contained in:
Pete Hunt 2020-03-14 21:48:51 -07:00 committed by GitHub
parent 05af9f04ed
commit e1e2249f57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 293 additions and 272 deletions

View File

@ -6,7 +6,6 @@ import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons"; import { done } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { register } from "./register"; import { register } from "./register";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
export const actionFinalize = register({ export const actionFinalize = register({
@ -29,7 +28,7 @@ export const actionFinalize = register({
if (isInvisiblySmallElement(appState.multiElement)) { if (isInvisiblySmallElement(appState.multiElement)) {
newElements = newElements.slice(0, -1); newElements = newElements.slice(0, -1);
} }
invalidateShapeForElement(appState.multiElement);
if (!appState.elementLocked) { if (!appState.elementLocked) {
appState.selectedElementIds[appState.multiElement.id] = true; appState.selectedElementIds[appState.multiElement.id] = true;
} }

View File

@ -11,7 +11,7 @@ import { AppState } from "../../src/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { DEFAULT_FONT } from "../appState"; import { DEFAULT_FONT } from "../appState";
import { register } from "./register"; import { register } from "./register";
import { newElementWith, newTextElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
const changeProperty = ( const changeProperty = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@ -266,7 +266,7 @@ export const actionChangeFontSize = register({
return { return {
elements: changeProperty(elements, appState, el => { elements: changeProperty(elements, appState, el => {
if (isTextElement(el)) { if (isTextElement(el)) {
const element: ExcalidrawTextElement = newTextElementWith(el, { const element: ExcalidrawTextElement = newElementWith(el, {
font: `${value}px ${el.font.split("px ")[1]}`, font: `${value}px ${el.font.split("px ")[1]}`,
}); });
redrawTextBoundingBox(element); redrawTextBoundingBox(element);
@ -313,7 +313,7 @@ export const actionChangeFontFamily = register({
return { return {
elements: changeProperty(elements, appState, el => { elements: changeProperty(elements, appState, el => {
if (isTextElement(el)) { if (isTextElement(el)) {
const element: ExcalidrawTextElement = newTextElementWith(el, { const element: ExcalidrawTextElement = newElementWith(el, {
font: `${el.font.split("px ")[0]}px ${value}`, font: `${el.font.split("px ")[0]}px ${value}`,
}); });
redrawTextBoundingBox(element); redrawTextBoundingBox(element);

View File

@ -6,7 +6,7 @@ import {
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { DEFAULT_FONT } from "../appState"; import { DEFAULT_FONT } from "../appState";
import { register } from "./register"; import { register } from "./register";
import { mutateTextElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
let copiedStyles: string = "{}"; let copiedStyles: string = "{}";
@ -44,7 +44,7 @@ export const actionPasteStyles = register({
roughness: pastedElement?.roughness, roughness: pastedElement?.roughness,
}); });
if (isTextElement(newElement)) { if (isTextElement(newElement)) {
mutateTextElement(newElement, { mutateElement(newElement, {
font: pastedElement?.font || DEFAULT_FONT, font: pastedElement?.font || DEFAULT_FONT,
}); });
redrawTextBoundingBox(newElement); redrawTextBoundingBox(newElement);

View File

@ -3,7 +3,6 @@ import React from "react";
import socketIOClient from "socket.io-client"; import socketIOClient from "socket.io-client";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import { Point } from "roughjs/bin/geometry";
import { import {
newElement, newElement,
@ -95,9 +94,9 @@ import {
} from "../constants"; } from "../constants";
import { LayerUI } from "./LayerUI"; import { LayerUI } from "./LayerUI";
import { ScrollBars } from "../scene/types"; import { ScrollBars } from "../scene/types";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { generateCollaborationLink, getCollaborationLinkData } from "../data"; import { generateCollaborationLink, getCollaborationLinkData } from "../data";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { invalidateShapeForElement } from "../renderer/renderElement";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEST HOOKS // TEST HOOKS
@ -106,27 +105,24 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
declare global { declare global {
interface Window { interface Window {
__TEST__: { __TEST__: {
elements: typeof elements; elements: readonly ExcalidrawElement[];
appState: AppState; appState: AppState;
}; };
// TEMPORARY until we have a UI to support this
generateCollaborationLink: () => Promise<string>;
} }
} }
if (process.env.NODE_ENV === "test") { if (process.env.NODE_ENV === "test") {
window.__TEST__ = {} as Window["__TEST__"]; window.__TEST__ = {} as Window["__TEST__"];
} }
window.generateCollaborationLink = generateCollaborationLink;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
let { elements } = createScene(); const scene = createScene();
if (process.env.NODE_ENV === "test") { if (process.env.NODE_ENV === "test") {
Object.defineProperty(window.__TEST__, "elements", { Object.defineProperty(window.__TEST__, "elements", {
get() { get() {
return elements; return scene.getAllElements();
}, },
}); });
} }
@ -173,7 +169,7 @@ export class App extends React.Component<any, AppState> {
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
this.syncActionResult, this.syncActionResult,
() => this.state, () => this.state,
() => elements, () => scene.getAllElements(),
); );
this.actionManager.registerAll(actions); this.actionManager.registerAll(actions);
@ -181,6 +177,11 @@ export class App extends React.Component<any, AppState> {
this.actionManager.registerAction(createRedoAction(history)); this.actionManager.registerAction(createRedoAction(history));
} }
private replaceElements = (nextElements: readonly ExcalidrawElement[]) => {
scene.replaceAllElements(nextElements);
this.setState({});
};
private syncActionResult = ( private syncActionResult = (
res: ActionResult, res: ActionResult,
commitToHistory: boolean = true, commitToHistory: boolean = true,
@ -189,11 +190,10 @@ export class App extends React.Component<any, AppState> {
return; return;
} }
if (res.elements) { if (res.elements) {
elements = res.elements; this.replaceElements(res.elements);
if (commitToHistory) { if (commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
this.setState({});
} }
if (res.appState) { if (res.appState) {
@ -212,12 +212,12 @@ export class App extends React.Component<any, AppState> {
if (isWritableElement(event.target)) { if (isWritableElement(event.target)) {
return; return;
} }
copyToAppClipboard(elements, this.state); copyToAppClipboard(scene.getAllElements(), this.state);
const { elements: nextElements, appState } = deleteSelectedElements( const { elements: nextElements, appState } = deleteSelectedElements(
elements, scene.getAllElements(),
this.state, this.state,
); );
elements = nextElements; this.replaceElements(nextElements);
history.resumeRecording(); history.resumeRecording();
this.setState({ ...appState }); this.setState({ ...appState });
event.preventDefault(); event.preventDefault();
@ -226,7 +226,7 @@ export class App extends React.Component<any, AppState> {
if (isWritableElement(event.target)) { if (isWritableElement(event.target)) {
return; return;
} }
copyToAppClipboard(elements, this.state); copyToAppClipboard(scene.getAllElements(), this.state);
event.preventDefault(); event.preventDefault();
}; };
@ -291,15 +291,19 @@ export class App extends React.Component<any, AppState> {
// Perform reconciliation - in collaboration, if we encounter // Perform reconciliation - in collaboration, if we encounter
// elements with more staler versions than ours, ignore them // elements with more staler versions than ours, ignore them
// and keep ours. // and keep ours.
if (elements == null || elements.length === 0) { if (
elements = restoredState.elements; scene.getAllElements() == null ||
scene.getAllElements().length === 0
) {
this.replaceElements(restoredState.elements);
} else { } else {
// create a map of ids so we don't have to iterate // create a map of ids so we don't have to iterate
// over the array more than once. // over the array more than once.
const localElementMap = getElementMap(elements); const localElementMap = getElementMap(scene.getAllElements());
// Reconcile // Reconcile
elements = restoredState.elements this.replaceElements(
restoredState.elements
.reduce((elements, element) => { .reduce((elements, element) => {
// if the remote element references one that's currently // if the remote element references one that's currently
// edited on local, skip it (it'll be added in the next // edited on local, skip it (it'll be added in the next
@ -320,7 +324,8 @@ export class App extends React.Component<any, AppState> {
delete localElementMap[element.id]; delete localElementMap[element.id];
} else if ( } else if (
localElementMap.hasOwnProperty(element.id) && localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version === element.version && localElementMap[element.id].version ===
element.version &&
localElementMap[element.id].versionNonce !== localElementMap[element.id].versionNonce !==
element.versionNonce element.versionNonce
) { ) {
@ -344,17 +349,17 @@ export class App extends React.Component<any, AppState> {
return elements; return elements;
}, [] as any) }, [] as any)
// add local elements that weren't deleted or on remote // add local elements that weren't deleted or on remote
.concat(...Object.values(localElementMap)); .concat(...Object.values(localElementMap)),
);
} }
this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion( this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
elements, scene.getAllElements(),
); );
// We haven't yet implemented multiplayer undo functionality, so we clear the undo stack // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
// when we receive any messages from another peer. This UX can be pretty rough -- if you // when we receive any messages from another peer. This UX can be pretty rough -- if you
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However, // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
// right now we think this is the right tradeoff. // right now we think this is the right tradeoff.
history.clear(); history.clear();
this.setState({});
if (this.socketInitialized === false) { if (this.socketInitialized === false) {
this.socketInitialized = true; this.socketInitialized = true;
} }
@ -423,12 +428,12 @@ export class App extends React.Component<any, AppState> {
const data: SocketUpdateDataSource["SCENE_UPDATE"] = { const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
type: "SCENE_UPDATE", type: "SCENE_UPDATE",
payload: { payload: {
elements: getSyncableElements(elements), elements: getSyncableElements(scene.getAllElements()),
}, },
}; };
this.lastBroadcastedOrReceivedSceneVersion = Math.max( this.lastBroadcastedOrReceivedSceneVersion = Math.max(
this.lastBroadcastedOrReceivedSceneVersion, this.lastBroadcastedOrReceivedSceneVersion,
getDrawingVersion(elements), getDrawingVersion(scene.getAllElements()),
); );
return this._broadcastSocketData( return this._broadcastSocketData(
data as typeof data & { _brand: "socketUpdateData" }, data as typeof data & { _brand: "socketUpdateData" },
@ -553,7 +558,9 @@ export class App extends React.Component<any, AppState> {
public state: AppState = getDefaultAppState(); public state: AppState = getDefaultAppState();
private onResize = () => { private onResize = () => {
elements.forEach(element => invalidateShapeForElement(element)); scene
.getAllElements()
.forEach(element => invalidateShapeForElement(element));
this.setState({}); this.setState({});
}; };
@ -587,7 +594,8 @@ export class App extends React.Component<any, AppState> {
const step = event.shiftKey const step = event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT; : ELEMENT_TRANSLATE_AMOUNT;
elements = elements.map(el => { this.replaceElements(
scene.getAllElements().map(el => {
if (this.state.selectedElementIds[el.id]) { if (this.state.selectedElementIds[el.id]) {
const update: { x?: number; y?: number } = {}; const update: { x?: number; y?: number } = {};
if (event.key === KEYS.ARROW_LEFT) { if (event.key === KEYS.ARROW_LEFT) {
@ -602,8 +610,8 @@ export class App extends React.Component<any, AppState> {
return newElementWith(el, update); return newElementWith(el, update);
} }
return el; return el;
}); }),
this.setState({}); );
event.preventDefault(); event.preventDefault();
} else if ( } else if (
shapesShortcutKeys.includes(event.key.toLowerCase()) && shapesShortcutKeys.includes(event.key.toLowerCase()) &&
@ -635,14 +643,17 @@ export class App extends React.Component<any, AppState> {
}; };
private copyToAppClipboard = () => { private copyToAppClipboard = () => {
copyToAppClipboard(elements, this.state); copyToAppClipboard(scene.getAllElements(), this.state);
}; };
private copyToClipboardAsPng = () => { private copyToClipboardAsPng = () => {
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(
scene.getAllElements(),
this.state,
);
exportCanvas( exportCanvas(
"clipboard", "clipboard",
selectedElements.length ? selectedElements : elements, selectedElements.length ? selectedElements : scene.getAllElements(),
this.state, this.state,
this.canvas!, this.canvas!,
this.state, this.state,
@ -686,7 +697,7 @@ export class App extends React.Component<any, AppState> {
this.state.currentItemFont, this.state.currentItemFont,
); );
elements = [...elements, element]; this.replaceElements([...scene.getAllElements(), element]);
this.setState({ selectedElementIds: { [element.id]: true } }); this.setState({ selectedElementIds: { [element.id]: true } });
history.resumeRecording(); history.resumeRecording();
} }
@ -729,11 +740,6 @@ export class App extends React.Component<any, AppState> {
this.setState(obj); this.setState(obj);
}; };
setElements = (elements_: readonly ExcalidrawElement[]) => {
elements = elements_;
this.setState({});
};
removePointer = (event: React.PointerEvent<HTMLElement>) => { removePointer = (event: React.PointerEvent<HTMLElement>) => {
gesture.pointers.delete(event.pointerId); gesture.pointers.delete(event.pointerId);
}; };
@ -768,8 +774,8 @@ export class App extends React.Component<any, AppState> {
appState={this.state} appState={this.state}
setAppState={this.setAppState} setAppState={this.setAppState}
actionManager={this.actionManager} actionManager={this.actionManager}
elements={elements} elements={scene.getAllElements()}
setElements={this.setElements} setElements={this.replaceElements}
language={getLanguage()} language={getLanguage()}
onRoomCreate={this.createRoom} onRoomCreate={this.createRoom}
onRoomDestroy={this.destroyRoom} onRoomDestroy={this.destroyRoom}
@ -810,7 +816,7 @@ export class App extends React.Component<any, AppState> {
); );
const element = getElementAtPosition( const element = getElementAtPosition(
elements, scene.getAllElements(),
this.state, this.state,
x, x,
y, y,
@ -824,7 +830,7 @@ export class App extends React.Component<any, AppState> {
action: () => this.pasteFromClipboard(null), action: () => this.pasteFromClipboard(null),
}, },
probablySupportsClipboardBlob && probablySupportsClipboardBlob &&
hasNonDeletedElements(elements) && { hasNonDeletedElements(scene.getAllElements()) && {
label: t("labels.copyAsPng"), label: t("labels.copyAsPng"),
action: this.copyToClipboardAsPng, action: this.copyToClipboardAsPng,
}, },
@ -908,7 +914,7 @@ export class App extends React.Component<any, AppState> {
); );
const elementAtPosition = getElementAtPosition( const elementAtPosition = getElementAtPosition(
elements, scene.getAllElements(),
this.state, this.state,
x, x,
y, y,
@ -940,10 +946,11 @@ export class App extends React.Component<any, AppState> {
let textY = event.clientY; let textY = event.clientY;
if (elementAtPosition && isTextElement(elementAtPosition)) { if (elementAtPosition && isTextElement(elementAtPosition)) {
elements = elements.filter( this.replaceElements(
element => element.id !== elementAtPosition.id, scene
.getAllElements()
.filter(element => element.id !== elementAtPosition.id),
); );
this.setState({});
const centerElementX = elementAtPosition.x + elementAtPosition.width / 2; const centerElementX = elementAtPosition.x + elementAtPosition.width / 2;
const centerElementY = elementAtPosition.y + elementAtPosition.height / 2; const centerElementY = elementAtPosition.y + elementAtPosition.height / 2;
@ -998,14 +1005,14 @@ export class App extends React.Component<any, AppState> {
zoom: this.state.zoom, zoom: this.state.zoom,
onSubmit: text => { onSubmit: text => {
if (text) { if (text) {
elements = [ this.replaceElements([
...elements, ...scene.getAllElements(),
{ {
// we need to recreate the element to update dimensions & // we need to recreate the element to update dimensions &
// position // position
...newTextElement(element, text, element.font), ...newTextElement(element, text, element.font),
}, },
]; ]);
} }
this.setState(prevState => ({ this.setState(prevState => ({
selectedElementIds: { selectedElementIds: {
@ -1083,11 +1090,11 @@ export class App extends React.Component<any, AppState> {
const originX = multiElement.x; const originX = multiElement.x;
const originY = multiElement.y; const originY = multiElement.y;
const points = multiElement.points; const points = multiElement.points;
const pnt = points[points.length - 1];
pnt[0] = x - originX; mutateElement(multiElement, {
pnt[1] = y - originY; points: [...points.slice(0, -1), [x - originX, y - originY]],
mutateElement(multiElement); });
invalidateShapeForElement(multiElement);
this.setState({}); this.setState({});
return; return;
} }
@ -1097,10 +1104,13 @@ export class App extends React.Component<any, AppState> {
return; return;
} }
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(
scene.getAllElements(),
this.state,
);
if (selectedElements.length === 1 && !isOverScrollBar) { if (selectedElements.length === 1 && !isOverScrollBar) {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, scene.getAllElements(),
this.state, this.state,
{ x, y }, { x, y },
this.state.zoom, this.state.zoom,
@ -1114,7 +1124,7 @@ export class App extends React.Component<any, AppState> {
} }
} }
const hitElement = getElementAtPosition( const hitElement = getElementAtPosition(
elements, scene.getAllElements(),
this.state, this.state,
x, x,
y, y,
@ -1306,14 +1316,17 @@ export class App extends React.Component<any, AppState> {
let elementIsAddedToSelection = false; let elementIsAddedToSelection = false;
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, scene.getAllElements(),
this.state, this.state,
{ x, y }, { x, y },
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
); );
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(
scene.getAllElements(),
this.state,
);
if (selectedElements.length === 1 && resizeElement) { if (selectedElements.length === 1 && resizeElement) {
this.setState({ this.setState({
resizingElement: resizeElement ? resizeElement.element : null, resizingElement: resizeElement ? resizeElement.element : null,
@ -1326,7 +1339,7 @@ export class App extends React.Component<any, AppState> {
isResizingElements = true; isResizingElements = true;
} else { } else {
hitElement = getElementAtPosition( hitElement = getElementAtPosition(
elements, scene.getAllElements(),
this.state, this.state,
x, x,
y, y,
@ -1353,7 +1366,7 @@ export class App extends React.Component<any, AppState> {
[hitElement!.id]: true, [hitElement!.id]: true,
}, },
})); }));
elements = elements.slice(); this.replaceElements(scene.getAllElements());
elementIsAddedToSelection = true; elementIsAddedToSelection = true;
} }
@ -1363,7 +1376,7 @@ export class App extends React.Component<any, AppState> {
// put the duplicates where the selected elements used to be. // put the duplicates where the selected elements used to be.
const nextElements = []; const nextElements = [];
const elementsToAppend = []; const elementsToAppend = [];
for (const element of elements) { for (const element of scene.getAllElements()) {
if (this.state.selectedElementIds[element.id]) { if (this.state.selectedElementIds[element.id]) {
nextElements.push(duplicateElement(element)); nextElements.push(duplicateElement(element));
elementsToAppend.push(element); elementsToAppend.push(element);
@ -1371,7 +1384,7 @@ export class App extends React.Component<any, AppState> {
nextElements.push(element); nextElements.push(element);
} }
} }
elements = [...nextElements, ...elementsToAppend]; this.replaceElements([...nextElements, ...elementsToAppend]);
} }
} }
} }
@ -1421,12 +1434,12 @@ export class App extends React.Component<any, AppState> {
zoom: this.state.zoom, zoom: this.state.zoom,
onSubmit: text => { onSubmit: text => {
if (text) { if (text) {
elements = [ this.replaceElements([
...elements, ...scene.getAllElements(),
{ {
...newTextElement(element, text, this.state.currentItemFont), ...newTextElement(element, text, this.state.currentItemFont),
}, },
]; ]);
} }
this.setState(prevState => ({ this.setState(prevState => ({
selectedElementIds: { selectedElementIds: {
@ -1469,9 +1482,9 @@ export class App extends React.Component<any, AppState> {
[multiElement.id]: true, [multiElement.id]: true,
}, },
})); }));
multiElement.points.push([x - rx, y - ry]); mutateElement(multiElement, {
mutateElement(multiElement); points: [...multiElement.points, [x - rx, y - ry]],
invalidateShapeForElement(multiElement); });
} else { } else {
this.setState(prevState => ({ this.setState(prevState => ({
selectedElementIds: { selectedElementIds: {
@ -1479,10 +1492,10 @@ export class App extends React.Component<any, AppState> {
[element.id]: false, [element.id]: false,
}, },
})); }));
element.points.push([0, 0]); mutateElement(element, {
mutateElement(element); points: [...element.points, [0, 0]],
invalidateShapeForElement(element); });
elements = [...elements, element]; this.replaceElements([...scene.getAllElements(), element]);
this.setState({ this.setState({
draggingElement: element, draggingElement: element,
editingElement: element, editingElement: element,
@ -1494,7 +1507,7 @@ export class App extends React.Component<any, AppState> {
draggingElement: element, draggingElement: element,
}); });
} else { } else {
elements = [...elements, element]; this.replaceElements([...scene.getAllElements(), element]);
this.setState({ this.setState({
multiElement: null, multiElement: null,
draggingElement: element, draggingElement: element,
@ -1505,7 +1518,7 @@ export class App extends React.Component<any, AppState> {
let resizeArrowFn: let resizeArrowFn:
| (( | ((
element: ExcalidrawElement, element: ExcalidrawElement,
p1: Point, pointIndex: number,
deltaX: number, deltaX: number,
deltaY: number, deltaY: number,
pointerX: number, pointerX: number,
@ -1516,13 +1529,14 @@ export class App extends React.Component<any, AppState> {
const arrowResizeOrigin = ( const arrowResizeOrigin = (
element: ExcalidrawElement, element: ExcalidrawElement,
p1: Point, pointIndex: number,
deltaX: number, deltaX: number,
deltaY: number, deltaY: number,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
perfect: boolean, perfect: boolean,
) => { ) => {
const p1 = element.points[pointIndex];
if (perfect) { if (perfect) {
const absPx = p1[0] + element.x; const absPx = p1[0] + element.x;
const absPy = p1[1] + element.y; const absPy = p1[1] + element.y;
@ -1535,44 +1549,52 @@ export class App extends React.Component<any, AppState> {
const dx = element.x + width + p1[0]; const dx = element.x + width + p1[0];
const dy = element.y + height + p1[1]; const dy = element.y + height + p1[1];
p1[0] = absPx - element.x;
p1[1] = absPy - element.y;
mutateElement(element, { mutateElement(element, {
x: dx, x: dx,
y: dy, y: dy,
points: element.points.map((point, i) =>
i === pointIndex ? [absPx - element.x, absPy - element.y] : point,
),
}); });
} else { } else {
p1[0] -= deltaX;
p1[1] -= deltaY;
mutateElement(element, { mutateElement(element, {
x: element.x + deltaX, x: element.x + deltaX,
y: element.y + deltaY, y: element.y + deltaY,
points: element.points.map((point, i) =>
i === pointIndex ? [p1[0] - deltaX, p1[1] - deltaY] : point,
),
}); });
} }
}; };
const arrowResizeEnd = ( const arrowResizeEnd = (
element: ExcalidrawElement, element: ExcalidrawElement,
p1: Point, pointIndex: number,
deltaX: number, deltaX: number,
deltaY: number, deltaY: number,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
perfect: boolean, perfect: boolean,
) => { ) => {
const p1 = element.points[pointIndex];
if (perfect) { if (perfect) {
const { width, height } = getPerfectElementSize( const { width, height } = getPerfectElementSize(
element.type, element.type,
pointerX - element.x, pointerX - element.x,
pointerY - element.y, pointerY - element.y,
); );
p1[0] = width; mutateElement(element, {
p1[1] = height; points: element.points.map((point, i) =>
i === pointIndex ? [width, height] : point,
),
});
} else { } else {
p1[0] += deltaX; mutateElement(element, {
p1[1] += deltaY; points: element.points.map((point, i) =>
i === pointIndex ? [p1[0] + deltaX, p1[1] + deltaY] : point,
),
});
} }
mutateElement(element);
}; };
const onPointerMove = (event: PointerEvent) => { const onPointerMove = (event: PointerEvent) => {
@ -1623,7 +1645,10 @@ export class App extends React.Component<any, AppState> {
if (isResizingElements && this.state.resizingElement) { if (isResizingElements && this.state.resizingElement) {
this.setState({ isResizing: true }); this.setState({ isResizing: true });
const el = this.state.resizingElement; const el = this.state.resizingElement;
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(
scene.getAllElements(),
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -1646,15 +1671,7 @@ export class App extends React.Component<any, AppState> {
resizeArrowFn = arrowResizeOrigin; resizeArrowFn = arrowResizeOrigin;
} }
} }
resizeArrowFn( resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
element,
p1,
deltaX,
deltaY,
x,
y,
event.shiftKey,
);
} else { } else {
mutateElement(element, { mutateElement(element, {
x: element.x + deltaX, x: element.x + deltaX,
@ -1678,15 +1695,7 @@ export class App extends React.Component<any, AppState> {
resizeArrowFn = arrowResizeOrigin; resizeArrowFn = arrowResizeOrigin;
} }
} }
resizeArrowFn( resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
element,
p1,
deltaX,
deltaY,
x,
y,
event.shiftKey,
);
} else { } else {
const nextWidth = element.width + deltaX; const nextWidth = element.width + deltaX;
mutateElement(element, { mutateElement(element, {
@ -1708,15 +1717,7 @@ export class App extends React.Component<any, AppState> {
resizeArrowFn = arrowResizeOrigin; resizeArrowFn = arrowResizeOrigin;
} }
} }
resizeArrowFn( resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
element,
p1,
deltaX,
deltaY,
x,
y,
event.shiftKey,
);
} else { } else {
mutateElement(element, { mutateElement(element, {
x: element.x + deltaX, x: element.x + deltaX,
@ -1737,15 +1738,7 @@ export class App extends React.Component<any, AppState> {
resizeArrowFn = arrowResizeOrigin; resizeArrowFn = arrowResizeOrigin;
} }
} }
resizeArrowFn( resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
element,
p1,
deltaX,
deltaY,
x,
y,
event.shiftKey,
);
} else { } else {
mutateElement(element, { mutateElement(element, {
width: element.width + deltaX, width: element.width + deltaX,
@ -1756,9 +1749,13 @@ export class App extends React.Component<any, AppState> {
} }
break; break;
case "n": { case "n": {
let points;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort((a, b) => a[1] - b[1]); points = [...element.points].sort((a, b) => a[1] - b[1]) as [
number,
number,
][];
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1769,13 +1766,18 @@ export class App extends React.Component<any, AppState> {
mutateElement(element, { mutateElement(element, {
height: element.height - deltaY, height: element.height - deltaY,
y: element.y + deltaY, y: element.y + deltaY,
points,
}); });
break; break;
} }
case "w": { case "w": {
let points;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort((a, b) => a[0] - b[0]); points = [...element.points].sort((a, b) => a[0] - b[0]) as [
number,
number,
][];
for (let i = 0; i < points.length; ++i) { for (let i = 0; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1786,13 +1788,19 @@ export class App extends React.Component<any, AppState> {
mutateElement(element, { mutateElement(element, {
width: element.width - deltaX, width: element.width - deltaX,
x: element.x + deltaX, x: element.x + deltaX,
points,
}); });
break; break;
} }
case "s": { case "s": {
let points;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort((a, b) => a[1] - b[1]); points = [...element.points].sort((a, b) => a[1] - b[1]) as [
number,
number,
][];
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1802,14 +1810,18 @@ export class App extends React.Component<any, AppState> {
mutateElement(element, { mutateElement(element, {
height: element.height + deltaY, height: element.height + deltaY,
points: element.points, // no-op, but signifies that we mutated points in-place above points,
}); });
break; break;
} }
case "e": { case "e": {
let points;
if (element.points.length > 0) { if (element.points.length > 0) {
const len = element.points.length; const len = element.points.length;
const points = [...element.points].sort((a, b) => a[0] - b[0]); points = [...element.points].sort((a, b) => a[0] - b[0]) as [
number,
number,
][];
for (let i = 1; i < points.length; ++i) { for (let i = 1; i < points.length; ++i) {
const pnt = points[i]; const pnt = points[i];
@ -1819,7 +1831,7 @@ export class App extends React.Component<any, AppState> {
mutateElement(element, { mutateElement(element, {
width: element.width + deltaX, width: element.width + deltaX,
points: element.points, // no-op, but signifies that we mutated points in-place above points,
}); });
break; break;
} }
@ -1838,7 +1850,6 @@ export class App extends React.Component<any, AppState> {
x: element.x, x: element.x,
y: element.y, y: element.y,
}); });
invalidateShapeForElement(el);
lastX = x; lastX = x;
lastY = y; lastY = y;
@ -1851,7 +1862,10 @@ export class App extends React.Component<any, AppState> {
// Marking that click was used for dragging to check // Marking that click was used for dragging to check
// if elements should be deselected on pointerup // if elements should be deselected on pointerup
draggingOccurred = true; draggingOccurred = true;
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(
scene.getAllElements(),
this.state,
);
if (selectedElements.length > 0) { if (selectedElements.length > 0) {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -1906,14 +1920,12 @@ export class App extends React.Component<any, AppState> {
} }
if (points.length === 1) { if (points.length === 1) {
points.push([dx, dy]); mutateElement(draggingElement, { points: [...points, [dx, dy]] });
} else if (points.length > 1) { } else if (points.length > 1) {
const pnt = points[points.length - 1]; mutateElement(draggingElement, {
pnt[0] = dx; points: [...points.slice(0, -1), [dx, dy]],
pnt[1] = dy; });
} }
mutateElement(draggingElement, { points });
} else { } else {
if (event.shiftKey) { if (event.shiftKey) {
({ width, height } = getPerfectElementSize( ({ width, height } = getPerfectElementSize(
@ -1935,14 +1947,15 @@ export class App extends React.Component<any, AppState> {
}); });
} }
invalidateShapeForElement(draggingElement);
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) { if (
!event.shiftKey &&
isSomeElementSelected(scene.getAllElements(), this.state)
) {
this.setState({ selectedElementIds: {} }); this.setState({ selectedElementIds: {} });
} }
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
elements, scene.getAllElements(),
draggingElement, draggingElement,
); );
this.setState(prevState => ({ this.setState(prevState => ({
@ -1990,12 +2003,12 @@ export class App extends React.Component<any, AppState> {
this.state, this.state,
this.canvas, this.canvas,
); );
draggingElement.points.push([ mutateElement(draggingElement, {
x - draggingElement.x, points: [
y - draggingElement.y, ...draggingElement.points,
]); [x - draggingElement.x, y - draggingElement.y],
mutateElement(draggingElement); ],
invalidateShapeForElement(draggingElement); });
this.setState({ this.setState({
multiElement: this.state.draggingElement, multiElement: this.state.draggingElement,
editingElement: this.state.draggingElement, editingElement: this.state.draggingElement,
@ -2030,7 +2043,7 @@ export class App extends React.Component<any, AppState> {
isInvisiblySmallElement(draggingElement) isInvisiblySmallElement(draggingElement)
) { ) {
// remove invisible element which was added in onPointerDown // remove invisible element which was added in onPointerDown
elements = elements.slice(0, -1); this.replaceElements(scene.getAllElements().slice(0, -1));
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
}); });
@ -2047,7 +2060,9 @@ export class App extends React.Component<any, AppState> {
} }
if (resizingElement && isInvisiblySmallElement(resizingElement)) { if (resizingElement && isInvisiblySmallElement(resizingElement)) {
elements = elements.filter(el => el.id !== resizingElement.id); this.replaceElements(
scene.getAllElements().filter(el => el.id !== resizingElement.id),
);
} }
// If click occurred on already selected element // If click occurred on already selected element
@ -2090,7 +2105,7 @@ export class App extends React.Component<any, AppState> {
if ( if (
elementType !== "selection" || elementType !== "selection" ||
isSomeElementSelected(elements, this.state) isSomeElementSelected(scene.getAllElements(), this.state)
) { ) {
history.resumeRecording(); history.resumeRecording();
} }
@ -2163,7 +2178,7 @@ export class App extends React.Component<any, AppState> {
return duplicate; return duplicate;
}); });
elements = [...elements, ...newElements]; this.replaceElements([...scene.getAllElements(), ...newElements]);
history.resumeRecording(); history.resumeRecording();
this.setState({ this.setState({
selectedElementIds: newElements.reduce((map, element) => { selectedElementIds: newElements.reduce((map, element) => {
@ -2174,7 +2189,11 @@ export class App extends React.Component<any, AppState> {
}; };
private getTextWysiwygSnappedToCenterPosition(x: number, y: number) { private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
const elementClickedInside = getElementContainingPosition(elements, x, y); const elementClickedInside = getElementContainingPosition(
scene.getAllElements(),
x,
y,
);
if (elementClickedInside) { if (elementClickedInside) {
const elementCenterX = const elementCenterX =
elementClickedInside.x + elementClickedInside.width / 2; elementClickedInside.x + elementClickedInside.width / 2;
@ -2209,7 +2228,7 @@ export class App extends React.Component<any, AppState> {
}; };
private saveDebounced = debounce(() => { private saveDebounced = debounce(() => {
saveToLocalStorage(elements, this.state); saveToLocalStorage(scene.getAllElements(), this.state);
}, 300); }, 300);
componentDidUpdate() { componentDidUpdate() {
@ -2233,7 +2252,7 @@ export class App extends React.Component<any, AppState> {
); );
}); });
const { atLeastOneVisibleElement, scrollBars } = renderScene( const { atLeastOneVisibleElement, scrollBars } = renderScene(
elements, scene.getAllElements(),
this.state, this.state,
this.state.selectionElement, this.state.selectionElement,
window.devicePixelRatio, window.devicePixelRatio,
@ -2254,20 +2273,22 @@ export class App extends React.Component<any, AppState> {
currentScrollBars = scrollBars; currentScrollBars = scrollBars;
} }
const scrolledOutside = const scrolledOutside =
!atLeastOneVisibleElement && hasNonDeletedElements(elements); !atLeastOneVisibleElement &&
hasNonDeletedElements(scene.getAllElements());
if (this.state.scrolledOutside !== scrolledOutside) { if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside: scrolledOutside }); this.setState({ scrolledOutside: scrolledOutside });
} }
this.saveDebounced(); this.saveDebounced();
if ( if (
getDrawingVersion(elements) > this.lastBroadcastedOrReceivedSceneVersion getDrawingVersion(scene.getAllElements()) >
this.lastBroadcastedOrReceivedSceneVersion
) { ) {
this.broadcastSceneUpdate(); this.broadcastSceneUpdate();
} }
if (history.isRecording()) { if (history.isRecording()) {
history.pushEntry(this.state, elements); history.pushEntry(this.state, scene.getAllElements());
history.skipRecording(); history.skipRecording();
} }
} }

View File

@ -14,7 +14,7 @@ export function serializeAsJSON(
type: "excalidraw", type: "excalidraw",
version: 1, version: 1,
source: window.location.origin, source: window.location.origin,
elements, elements: elements.filter(element => !element.isDeleted),
appState: cleanAppStateForExport(appState), appState: cleanAppStateForExport(appState),
}, },
null, null,

View File

@ -10,7 +10,10 @@ export function saveToLocalStorage(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) { ) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements)); localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify(elements.filter(element => !element.isDeleted)),
);
localStorage.setItem( localStorage.setItem(
LOCAL_STORAGE_KEY_STATE, LOCAL_STORAGE_KEY_STATE,
JSON.stringify(clearAppStateForLocalStorage(appState)), JSON.stringify(clearAppStateForLocalStorage(appState)),

View File

@ -1,4 +1,4 @@
import { Point } from "roughjs/bin/geometry"; import { Point } from "../types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";

View File

@ -1,7 +1,7 @@
import { ExcalidrawElement } from "./types"; import { ExcalidrawElement } from "./types";
import { rotate } from "../math"; import { rotate } from "../math";
import { Drawable } from "roughjs/bin/core"; import { Drawable } from "roughjs/bin/core";
import { Point } from "roughjs/bin/geometry"; import { Point } from "../types";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
// If the element is created from right to left, the width is going to be negative // If the element is created from right to left, the width is going to be negative
@ -68,7 +68,7 @@ export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
// move, bcurveTo, lineTo, and curveTo // move, bcurveTo, lineTo, and curveTo
if (op === "move") { if (op === "move") {
// change starting point // change starting point
currentP = data as Point; currentP = (data as unknown) as Point;
// move operation does not draw anything; so, it always // move operation does not draw anything; so, it always
// returns false // returns false
} else if (op === "bcurveTo") { } else if (op === "bcurveTo") {
@ -133,7 +133,7 @@ export function getArrowPoints(element: ExcalidrawElement, shape: Drawable[]) {
const prevOp = ops[ops.length - 2]; const prevOp = ops[ops.length - 2];
let p0: Point = [0, 0]; let p0: Point = [0, 0];
if (prevOp.op === "move") { if (prevOp.op === "move") {
p0 = prevOp.data as Point; p0 = (prevOp.data as unknown) as Point;
} else if (prevOp.op === "bcurveTo") { } else if (prevOp.op === "bcurveTo") {
p0 = [prevOp.data[4], prevOp.data[5]]; p0 = [prevOp.data[4], prevOp.data[5]];
} }

View File

@ -7,7 +7,7 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getLinearElementAbsoluteBounds, getLinearElementAbsoluteBounds,
} from "./bounds"; } from "./bounds";
import { Point } from "roughjs/bin/geometry"; import { Point } from "../types";
import { Drawable, OpSet } from "roughjs/bin/core"; import { Drawable, OpSet } from "roughjs/bin/core";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
@ -231,7 +231,7 @@ const hitTestRoughShape = (opSet: OpSet[], x: number, y: number) => {
// move, bcurveTo, lineTo, and curveTo // move, bcurveTo, lineTo, and curveTo
if (op === "move") { if (op === "move") {
// change starting point // change starting point
currentP = data as Point; currentP = (data as unknown) as Point;
// move operation does not draw anything; so, it always // move operation does not draw anything; so, it always
// returns false // returns false
} else if (op === "bcurveTo") { } else if (op === "bcurveTo") {

View File

@ -1,5 +1,6 @@
import { ExcalidrawElement, ExcalidrawTextElement } from "./types"; import { ExcalidrawElement } from "./types";
import { randomSeed } from "roughjs/bin/math"; import { randomSeed } from "roughjs/bin/math";
import { invalidateShapeForElement } from "../renderer/renderElement";
type ElementUpdate<TElement extends ExcalidrawElement> = Omit< type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
@ -9,45 +10,35 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
// This function tracks updates of text elements for the purposes for collaboration. // This function tracks updates of text elements for the purposes for collaboration.
// The version is used to compare updates when more than one user is working in // The version is used to compare updates when more than one user is working in
// the same drawing. // the same drawing.
export function mutateElement( export function mutateElement<TElement extends ExcalidrawElement>(
element: ExcalidrawElement, element: TElement,
updates?: ElementUpdate<ExcalidrawElement>, updates: ElementUpdate<TElement>,
) { ) {
if (updates) { const mutableElement = element as any;
Object.assign(element, updates);
for (const key in updates) {
const value = (updates as any)[key];
if (typeof value !== "undefined") {
mutableElement[key] = value;
} }
(element as any).version++; }
(element as any).versionNonce = randomSeed();
if (
typeof updates.height !== "undefined" ||
typeof updates.width !== "undefined" ||
typeof updates.points !== "undefined"
) {
invalidateShapeForElement(element);
}
mutableElement.version++;
mutableElement.versionNonce = randomSeed();
} }
export function newElementWith( export function newElementWith<TElement extends ExcalidrawElement>(
element: ExcalidrawElement, element: TElement,
updates: ElementUpdate<ExcalidrawElement>, updates: ElementUpdate<TElement>,
): ExcalidrawElement { ): TElement {
return {
...element,
...updates,
version: element.version + 1,
versionNonce: randomSeed(),
};
}
// This function tracks updates of text elements for the purposes for collaboration.
// The version is used to compare updates when more than one user is working in
// the same document.
export function mutateTextElement(
element: ExcalidrawTextElement,
updates: ElementUpdate<ExcalidrawTextElement>,
): void {
Object.assign(element, updates);
(element as any).version++;
(element as any).versionNonce = randomSeed();
}
export function newTextElementWith(
element: ExcalidrawTextElement,
updates: ElementUpdate<ExcalidrawTextElement>,
): ExcalidrawTextElement {
return { return {
...element, ...element,
...updates, ...updates,

View File

@ -1,6 +1,6 @@
import { randomSeed } from "roughjs/bin/math"; import { randomSeed } from "roughjs/bin/math";
import nanoid from "nanoid"; import nanoid from "nanoid";
import { Point } from "roughjs/bin/geometry"; import { Point } from "../types";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { measureText } from "../utils"; import { measureText } from "../utils";
@ -32,7 +32,7 @@ export function newElement(
roughness, roughness,
opacity, opacity,
seed: randomSeed(), seed: randomSeed(),
points: [] as Point[], points: [] as readonly Point[],
version: 1, version: 1,
versionNonce: 0, versionNonce: 0,
isDeleted: false, isDeleted: false,

View File

@ -1,5 +1,4 @@
import { ExcalidrawElement } from "./types"; import { ExcalidrawElement } from "./types";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean { export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
@ -101,7 +100,5 @@ export function normalizeDimensions(
}); });
} }
invalidateShapeForElement(element);
return true; return true;
} }

View File

@ -1,10 +1,10 @@
import { measureText } from "../utils"; import { measureText } from "../utils";
import { ExcalidrawTextElement } from "./types"; import { ExcalidrawTextElement } from "./types";
import { mutateTextElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => { export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
const metrics = measureText(element.text, element.font); const metrics = measureText(element.text, element.font);
mutateTextElement(element, { mutateElement(element, {
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline, baseline: metrics.baseline,

View File

@ -1,4 +1,4 @@
import { Point } from "roughjs/bin/geometry"; import { Point } from "./types";
// https://stackoverflow.com/a/6853926/232122 // https://stackoverflow.com/a/6853926/232122
export function distanceBetweenPointAndSegment( export function distanceBetweenPointAndSegment(

View File

@ -7,7 +7,6 @@ import {
} from "../element/bounds"; } from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core"; import { Drawable } from "roughjs/bin/core";
import { Point } from "roughjs/bin/geometry";
import { RoughSVG } from "roughjs/bin/svg"; import { RoughSVG } from "roughjs/bin/svg";
import { RoughGenerator } from "roughjs/bin/generator"; import { RoughGenerator } from "roughjs/bin/generator";
import { SceneState } from "../scene/types"; import { SceneState } from "../scene/types";
@ -214,13 +213,11 @@ function generateElement(
}; };
// points array can be empty in the beginning, so it is important to add // points array can be empty in the beginning, so it is important to add
// initial position to it // initial position to it
const points: Point[] = element.points.length const points = element.points.length ? element.points : [[0, 0]];
? element.points
: [[0, 0]];
// curve is always the first element // curve is always the first element
// this simplifies finding the curve for an element // this simplifies finding the curve for an element
shape = [generator.curve(points, options)]; shape = [generator.curve(points as [number, number][], options)];
// add lines only in arrow // add lines only in arrow
if (element.type === "arrow") { if (element.type === "arrow") {

View File

@ -1,6 +1,17 @@
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
class SceneState {
constructor(private _elements: readonly ExcalidrawElement[] = []) {}
getAllElements() {
return this._elements;
}
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this._elements = nextElements;
}
}
export const createScene = () => { export const createScene = () => {
const elements: readonly ExcalidrawElement[] = []; return new SceneState();
return { elements };
}; };

View File

@ -1,7 +1,9 @@
import { ExcalidrawElement, PointerType } from "./element/types"; import { ExcalidrawElement, PointerType } from "./element/types";
import { SHAPES } from "./shapes"; import { SHAPES } from "./shapes";
import { Point as RoughPoint } from "roughjs/bin/geometry";
export type FlooredNumber = number & { _brand: "FlooredNumber" }; export type FlooredNumber = number & { _brand: "FlooredNumber" };
export type Point = Readonly<RoughPoint>;
export type AppState = { export type AppState = {
draggingElement: ExcalidrawElement | null; draggingElement: ExcalidrawElement | null;