mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-10 11:35:52 +01:00
Abstract away or eliminate most of the mutation of the Elements array (#955)
This commit is contained in:
parent
05af9f04ed
commit
e1e2249f57
@ -6,7 +6,6 @@ import { ToolButton } from "../components/ToolButton";
|
||||
import { done } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
import { register } from "./register";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
|
||||
export const actionFinalize = register({
|
||||
@ -29,7 +28,7 @@ export const actionFinalize = register({
|
||||
if (isInvisiblySmallElement(appState.multiElement)) {
|
||||
newElements = newElements.slice(0, -1);
|
||||
}
|
||||
invalidateShapeForElement(appState.multiElement);
|
||||
|
||||
if (!appState.elementLocked) {
|
||||
appState.selectedElementIds[appState.multiElement.id] = true;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { AppState } from "../../src/types";
|
||||
import { t } from "../i18n";
|
||||
import { DEFAULT_FONT } from "../appState";
|
||||
import { register } from "./register";
|
||||
import { newElementWith, newTextElementWith } from "../element/mutateElement";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
|
||||
const changeProperty = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -266,7 +266,7 @@ export const actionChangeFontSize = register({
|
||||
return {
|
||||
elements: changeProperty(elements, appState, el => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newTextElementWith(el, {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
font: `${value}px ${el.font.split("px ")[1]}`,
|
||||
});
|
||||
redrawTextBoundingBox(element);
|
||||
@ -313,7 +313,7 @@ export const actionChangeFontFamily = register({
|
||||
return {
|
||||
elements: changeProperty(elements, appState, el => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newTextElementWith(el, {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
font: `${el.font.split("px ")[0]}px ${value}`,
|
||||
});
|
||||
redrawTextBoundingBox(element);
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
import { KEYS } from "../keys";
|
||||
import { DEFAULT_FONT } from "../appState";
|
||||
import { register } from "./register";
|
||||
import { mutateTextElement, newElementWith } from "../element/mutateElement";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
|
||||
let copiedStyles: string = "{}";
|
||||
|
||||
@ -44,7 +44,7 @@ export const actionPasteStyles = register({
|
||||
roughness: pastedElement?.roughness,
|
||||
});
|
||||
if (isTextElement(newElement)) {
|
||||
mutateTextElement(newElement, {
|
||||
mutateElement(newElement, {
|
||||
font: pastedElement?.font || DEFAULT_FONT,
|
||||
});
|
||||
redrawTextBoundingBox(newElement);
|
||||
|
@ -3,7 +3,6 @@ import React from "react";
|
||||
import socketIOClient from "socket.io-client";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
|
||||
import {
|
||||
newElement,
|
||||
@ -95,9 +94,9 @@ import {
|
||||
} from "../constants";
|
||||
import { LayerUI } from "./LayerUI";
|
||||
import { ScrollBars } from "../scene/types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { generateCollaborationLink, getCollaborationLinkData } from "../data";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TEST HOOKS
|
||||
@ -106,27 +105,24 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
declare global {
|
||||
interface Window {
|
||||
__TEST__: {
|
||||
elements: typeof elements;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
};
|
||||
// TEMPORARY until we have a UI to support this
|
||||
generateCollaborationLink: () => Promise<string>;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
window.__TEST__ = {} as Window["__TEST__"];
|
||||
}
|
||||
window.generateCollaborationLink = generateCollaborationLink;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let { elements } = createScene();
|
||||
const scene = createScene();
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
Object.defineProperty(window.__TEST__, "elements", {
|
||||
get() {
|
||||
return elements;
|
||||
return scene.getAllElements();
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -173,7 +169,7 @@ export class App extends React.Component<any, AppState> {
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
() => this.state,
|
||||
() => elements,
|
||||
() => scene.getAllElements(),
|
||||
);
|
||||
this.actionManager.registerAll(actions);
|
||||
|
||||
@ -181,6 +177,11 @@ export class App extends React.Component<any, AppState> {
|
||||
this.actionManager.registerAction(createRedoAction(history));
|
||||
}
|
||||
|
||||
private replaceElements = (nextElements: readonly ExcalidrawElement[]) => {
|
||||
scene.replaceAllElements(nextElements);
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
private syncActionResult = (
|
||||
res: ActionResult,
|
||||
commitToHistory: boolean = true,
|
||||
@ -189,11 +190,10 @@ export class App extends React.Component<any, AppState> {
|
||||
return;
|
||||
}
|
||||
if (res.elements) {
|
||||
elements = res.elements;
|
||||
this.replaceElements(res.elements);
|
||||
if (commitToHistory) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
if (res.appState) {
|
||||
@ -212,12 +212,12 @@ export class App extends React.Component<any, AppState> {
|
||||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements, this.state);
|
||||
copyToAppClipboard(scene.getAllElements(), this.state);
|
||||
const { elements: nextElements, appState } = deleteSelectedElements(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
elements = nextElements;
|
||||
this.replaceElements(nextElements);
|
||||
history.resumeRecording();
|
||||
this.setState({ ...appState });
|
||||
event.preventDefault();
|
||||
@ -226,7 +226,7 @@ export class App extends React.Component<any, AppState> {
|
||||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements, this.state);
|
||||
copyToAppClipboard(scene.getAllElements(), this.state);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
@ -291,70 +291,75 @@ export class App extends React.Component<any, AppState> {
|
||||
// Perform reconciliation - in collaboration, if we encounter
|
||||
// elements with more staler versions than ours, ignore them
|
||||
// and keep ours.
|
||||
if (elements == null || elements.length === 0) {
|
||||
elements = restoredState.elements;
|
||||
if (
|
||||
scene.getAllElements() == null ||
|
||||
scene.getAllElements().length === 0
|
||||
) {
|
||||
this.replaceElements(restoredState.elements);
|
||||
} else {
|
||||
// create a map of ids so we don't have to iterate
|
||||
// over the array more than once.
|
||||
const localElementMap = getElementMap(elements);
|
||||
const localElementMap = getElementMap(scene.getAllElements());
|
||||
|
||||
// Reconcile
|
||||
elements = restoredState.elements
|
||||
.reduce((elements, element) => {
|
||||
// if the remote element references one that's currently
|
||||
// edited on local, skip it (it'll be added in the next
|
||||
// step)
|
||||
if (
|
||||
element.id === this.state.editingElement?.id ||
|
||||
element.id === this.state.resizingElement?.id ||
|
||||
element.id === this.state.draggingElement?.id
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version > element.version
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
delete localElementMap[element.id];
|
||||
} else if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version === element.version &&
|
||||
localElementMap[element.id].versionNonce !==
|
||||
element.versionNonce
|
||||
) {
|
||||
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
||||
this.replaceElements(
|
||||
restoredState.elements
|
||||
.reduce((elements, element) => {
|
||||
// if the remote element references one that's currently
|
||||
// edited on local, skip it (it'll be added in the next
|
||||
// step)
|
||||
if (
|
||||
localElementMap[element.id].versionNonce <
|
||||
element.versionNonce
|
||||
element.id === this.state.editingElement?.id ||
|
||||
element.id === this.state.resizingElement?.id ||
|
||||
element.id === this.state.draggingElement?.id
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version > element.version
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
delete localElementMap[element.id];
|
||||
} else if (
|
||||
localElementMap.hasOwnProperty(element.id) &&
|
||||
localElementMap[element.id].version ===
|
||||
element.version &&
|
||||
localElementMap[element.id].versionNonce !==
|
||||
element.versionNonce
|
||||
) {
|
||||
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
||||
if (
|
||||
localElementMap[element.id].versionNonce <
|
||||
element.versionNonce
|
||||
) {
|
||||
elements.push(localElementMap[element.id]);
|
||||
} else {
|
||||
// it should be highly unlikely that the two versionNonces are the same. if we are
|
||||
// really worried about this, we can replace the versionNonce with the socket id.
|
||||
elements.push(element);
|
||||
}
|
||||
delete localElementMap[element.id];
|
||||
} else {
|
||||
// it should be highly unlikely that the two versionNonces are the same. if we are
|
||||
// really worried about this, we can replace the versionNonce with the socket id.
|
||||
elements.push(element);
|
||||
delete localElementMap[element.id];
|
||||
}
|
||||
delete localElementMap[element.id];
|
||||
} else {
|
||||
elements.push(element);
|
||||
delete localElementMap[element.id];
|
||||
}
|
||||
|
||||
return elements;
|
||||
}, [] as any)
|
||||
// add local elements that weren't deleted or on remote
|
||||
.concat(...Object.values(localElementMap));
|
||||
return elements;
|
||||
}, [] as any)
|
||||
// add local elements that weren't deleted or on remote
|
||||
.concat(...Object.values(localElementMap)),
|
||||
);
|
||||
}
|
||||
this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
);
|
||||
// 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
|
||||
// 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.
|
||||
history.clear();
|
||||
this.setState({});
|
||||
if (this.socketInitialized === false) {
|
||||
this.socketInitialized = true;
|
||||
}
|
||||
@ -423,12 +428,12 @@ export class App extends React.Component<any, AppState> {
|
||||
const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
|
||||
type: "SCENE_UPDATE",
|
||||
payload: {
|
||||
elements: getSyncableElements(elements),
|
||||
elements: getSyncableElements(scene.getAllElements()),
|
||||
},
|
||||
};
|
||||
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
||||
this.lastBroadcastedOrReceivedSceneVersion,
|
||||
getDrawingVersion(elements),
|
||||
getDrawingVersion(scene.getAllElements()),
|
||||
);
|
||||
return this._broadcastSocketData(
|
||||
data as typeof data & { _brand: "socketUpdateData" },
|
||||
@ -553,7 +558,9 @@ export class App extends React.Component<any, AppState> {
|
||||
public state: AppState = getDefaultAppState();
|
||||
|
||||
private onResize = () => {
|
||||
elements.forEach(element => invalidateShapeForElement(element));
|
||||
scene
|
||||
.getAllElements()
|
||||
.forEach(element => invalidateShapeForElement(element));
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
@ -587,23 +594,24 @@ export class App extends React.Component<any, AppState> {
|
||||
const step = event.shiftKey
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT;
|
||||
elements = elements.map(el => {
|
||||
if (this.state.selectedElementIds[el.id]) {
|
||||
const update: { x?: number; y?: number } = {};
|
||||
if (event.key === KEYS.ARROW_LEFT) {
|
||||
update.x = el.x - step;
|
||||
} else if (event.key === KEYS.ARROW_RIGHT) {
|
||||
update.x = el.x + step;
|
||||
} else if (event.key === KEYS.ARROW_UP) {
|
||||
update.y = el.y - step;
|
||||
} else if (event.key === KEYS.ARROW_DOWN) {
|
||||
update.y = el.y + step;
|
||||
this.replaceElements(
|
||||
scene.getAllElements().map(el => {
|
||||
if (this.state.selectedElementIds[el.id]) {
|
||||
const update: { x?: number; y?: number } = {};
|
||||
if (event.key === KEYS.ARROW_LEFT) {
|
||||
update.x = el.x - step;
|
||||
} else if (event.key === KEYS.ARROW_RIGHT) {
|
||||
update.x = el.x + step;
|
||||
} else if (event.key === KEYS.ARROW_UP) {
|
||||
update.y = el.y - step;
|
||||
} else if (event.key === KEYS.ARROW_DOWN) {
|
||||
update.y = el.y + step;
|
||||
}
|
||||
return newElementWith(el, update);
|
||||
}
|
||||
return newElementWith(el, update);
|
||||
}
|
||||
return el;
|
||||
});
|
||||
this.setState({});
|
||||
return el;
|
||||
}),
|
||||
);
|
||||
event.preventDefault();
|
||||
} else if (
|
||||
shapesShortcutKeys.includes(event.key.toLowerCase()) &&
|
||||
@ -635,14 +643,17 @@ export class App extends React.Component<any, AppState> {
|
||||
};
|
||||
|
||||
private copyToAppClipboard = () => {
|
||||
copyToAppClipboard(elements, this.state);
|
||||
copyToAppClipboard(scene.getAllElements(), this.state);
|
||||
};
|
||||
|
||||
private copyToClipboardAsPng = () => {
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
exportCanvas(
|
||||
"clipboard",
|
||||
selectedElements.length ? selectedElements : elements,
|
||||
selectedElements.length ? selectedElements : scene.getAllElements(),
|
||||
this.state,
|
||||
this.canvas!,
|
||||
this.state,
|
||||
@ -686,7 +697,7 @@ export class App extends React.Component<any, AppState> {
|
||||
this.state.currentItemFont,
|
||||
);
|
||||
|
||||
elements = [...elements, element];
|
||||
this.replaceElements([...scene.getAllElements(), element]);
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
history.resumeRecording();
|
||||
}
|
||||
@ -729,11 +740,6 @@ export class App extends React.Component<any, AppState> {
|
||||
this.setState(obj);
|
||||
};
|
||||
|
||||
setElements = (elements_: readonly ExcalidrawElement[]) => {
|
||||
elements = elements_;
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
||||
gesture.pointers.delete(event.pointerId);
|
||||
};
|
||||
@ -768,8 +774,8 @@ export class App extends React.Component<any, AppState> {
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={elements}
|
||||
setElements={this.setElements}
|
||||
elements={scene.getAllElements()}
|
||||
setElements={this.replaceElements}
|
||||
language={getLanguage()}
|
||||
onRoomCreate={this.createRoom}
|
||||
onRoomDestroy={this.destroyRoom}
|
||||
@ -810,7 +816,7 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
|
||||
const element = getElementAtPosition(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -824,7 +830,7 @@ export class App extends React.Component<any, AppState> {
|
||||
action: () => this.pasteFromClipboard(null),
|
||||
},
|
||||
probablySupportsClipboardBlob &&
|
||||
hasNonDeletedElements(elements) && {
|
||||
hasNonDeletedElements(scene.getAllElements()) && {
|
||||
label: t("labels.copyAsPng"),
|
||||
action: this.copyToClipboardAsPng,
|
||||
},
|
||||
@ -908,7 +914,7 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
|
||||
const elementAtPosition = getElementAtPosition(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -940,10 +946,11 @@ export class App extends React.Component<any, AppState> {
|
||||
let textY = event.clientY;
|
||||
|
||||
if (elementAtPosition && isTextElement(elementAtPosition)) {
|
||||
elements = elements.filter(
|
||||
element => element.id !== elementAtPosition.id,
|
||||
this.replaceElements(
|
||||
scene
|
||||
.getAllElements()
|
||||
.filter(element => element.id !== elementAtPosition.id),
|
||||
);
|
||||
this.setState({});
|
||||
|
||||
const centerElementX = elementAtPosition.x + elementAtPosition.width / 2;
|
||||
const centerElementY = elementAtPosition.y + elementAtPosition.height / 2;
|
||||
@ -998,14 +1005,14 @@ export class App extends React.Component<any, AppState> {
|
||||
zoom: this.state.zoom,
|
||||
onSubmit: text => {
|
||||
if (text) {
|
||||
elements = [
|
||||
...elements,
|
||||
this.replaceElements([
|
||||
...scene.getAllElements(),
|
||||
{
|
||||
// we need to recreate the element to update dimensions &
|
||||
// position
|
||||
...newTextElement(element, text, element.font),
|
||||
},
|
||||
];
|
||||
]);
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
@ -1083,11 +1090,11 @@ export class App extends React.Component<any, AppState> {
|
||||
const originX = multiElement.x;
|
||||
const originY = multiElement.y;
|
||||
const points = multiElement.points;
|
||||
const pnt = points[points.length - 1];
|
||||
pnt[0] = x - originX;
|
||||
pnt[1] = y - originY;
|
||||
mutateElement(multiElement);
|
||||
invalidateShapeForElement(multiElement);
|
||||
|
||||
mutateElement(multiElement, {
|
||||
points: [...points.slice(0, -1), [x - originX, y - originY]],
|
||||
});
|
||||
|
||||
this.setState({});
|
||||
return;
|
||||
}
|
||||
@ -1097,10 +1104,13 @@ export class App extends React.Component<any, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
@ -1114,7 +1124,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
}
|
||||
const hitElement = getElementAtPosition(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -1306,14 +1316,17 @@ export class App extends React.Component<any, AppState> {
|
||||
let elementIsAddedToSelection = false;
|
||||
if (this.state.elementType === "selection") {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && resizeElement) {
|
||||
this.setState({
|
||||
resizingElement: resizeElement ? resizeElement.element : null,
|
||||
@ -1326,7 +1339,7 @@ export class App extends React.Component<any, AppState> {
|
||||
isResizingElements = true;
|
||||
} else {
|
||||
hitElement = getElementAtPosition(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
@ -1353,7 +1366,7 @@ export class App extends React.Component<any, AppState> {
|
||||
[hitElement!.id]: true,
|
||||
},
|
||||
}));
|
||||
elements = elements.slice();
|
||||
this.replaceElements(scene.getAllElements());
|
||||
elementIsAddedToSelection = true;
|
||||
}
|
||||
|
||||
@ -1363,7 +1376,7 @@ export class App extends React.Component<any, AppState> {
|
||||
// put the duplicates where the selected elements used to be.
|
||||
const nextElements = [];
|
||||
const elementsToAppend = [];
|
||||
for (const element of elements) {
|
||||
for (const element of scene.getAllElements()) {
|
||||
if (this.state.selectedElementIds[element.id]) {
|
||||
nextElements.push(duplicateElement(element));
|
||||
elementsToAppend.push(element);
|
||||
@ -1371,7 +1384,7 @@ export class App extends React.Component<any, AppState> {
|
||||
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,
|
||||
onSubmit: text => {
|
||||
if (text) {
|
||||
elements = [
|
||||
...elements,
|
||||
this.replaceElements([
|
||||
...scene.getAllElements(),
|
||||
{
|
||||
...newTextElement(element, text, this.state.currentItemFont),
|
||||
},
|
||||
];
|
||||
]);
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
@ -1469,9 +1482,9 @@ export class App extends React.Component<any, AppState> {
|
||||
[multiElement.id]: true,
|
||||
},
|
||||
}));
|
||||
multiElement.points.push([x - rx, y - ry]);
|
||||
mutateElement(multiElement);
|
||||
invalidateShapeForElement(multiElement);
|
||||
mutateElement(multiElement, {
|
||||
points: [...multiElement.points, [x - rx, y - ry]],
|
||||
});
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
@ -1479,10 +1492,10 @@ export class App extends React.Component<any, AppState> {
|
||||
[element.id]: false,
|
||||
},
|
||||
}));
|
||||
element.points.push([0, 0]);
|
||||
mutateElement(element);
|
||||
invalidateShapeForElement(element);
|
||||
elements = [...elements, element];
|
||||
mutateElement(element, {
|
||||
points: [...element.points, [0, 0]],
|
||||
});
|
||||
this.replaceElements([...scene.getAllElements(), element]);
|
||||
this.setState({
|
||||
draggingElement: element,
|
||||
editingElement: element,
|
||||
@ -1494,7 +1507,7 @@ export class App extends React.Component<any, AppState> {
|
||||
draggingElement: element,
|
||||
});
|
||||
} else {
|
||||
elements = [...elements, element];
|
||||
this.replaceElements([...scene.getAllElements(), element]);
|
||||
this.setState({
|
||||
multiElement: null,
|
||||
draggingElement: element,
|
||||
@ -1505,7 +1518,7 @@ export class App extends React.Component<any, AppState> {
|
||||
let resizeArrowFn:
|
||||
| ((
|
||||
element: ExcalidrawElement,
|
||||
p1: Point,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
@ -1516,13 +1529,14 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
const arrowResizeOrigin = (
|
||||
element: ExcalidrawElement,
|
||||
p1: Point,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
) => {
|
||||
const p1 = element.points[pointIndex];
|
||||
if (perfect) {
|
||||
const absPx = p1[0] + element.x;
|
||||
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 dy = element.y + height + p1[1];
|
||||
p1[0] = absPx - element.x;
|
||||
p1[1] = absPy - element.y;
|
||||
mutateElement(element, {
|
||||
x: dx,
|
||||
y: dy,
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? [absPx - element.x, absPy - element.y] : point,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
p1[0] -= deltaX;
|
||||
p1[1] -= deltaY;
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
y: element.y + deltaY,
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? [p1[0] - deltaX, p1[1] - deltaY] : point,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const arrowResizeEnd = (
|
||||
element: ExcalidrawElement,
|
||||
p1: Point,
|
||||
pointIndex: number,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
perfect: boolean,
|
||||
) => {
|
||||
const p1 = element.points[pointIndex];
|
||||
if (perfect) {
|
||||
const { width, height } = getPerfectElementSize(
|
||||
element.type,
|
||||
pointerX - element.x,
|
||||
pointerY - element.y,
|
||||
);
|
||||
p1[0] = width;
|
||||
p1[1] = height;
|
||||
mutateElement(element, {
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? [width, height] : point,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
p1[0] += deltaX;
|
||||
p1[1] += deltaY;
|
||||
mutateElement(element, {
|
||||
points: element.points.map((point, i) =>
|
||||
i === pointIndex ? [p1[0] + deltaX, p1[1] + deltaY] : point,
|
||||
),
|
||||
});
|
||||
}
|
||||
mutateElement(element);
|
||||
};
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
@ -1623,7 +1645,10 @@ export class App extends React.Component<any, AppState> {
|
||||
if (isResizingElements && this.state.resizingElement) {
|
||||
this.setState({ isResizing: true });
|
||||
const el = this.state.resizingElement;
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
@ -1646,15 +1671,7 @@ export class App extends React.Component<any, AppState> {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(
|
||||
element,
|
||||
p1,
|
||||
deltaX,
|
||||
deltaY,
|
||||
x,
|
||||
y,
|
||||
event.shiftKey,
|
||||
);
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
@ -1678,15 +1695,7 @@ export class App extends React.Component<any, AppState> {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(
|
||||
element,
|
||||
p1,
|
||||
deltaX,
|
||||
deltaY,
|
||||
x,
|
||||
y,
|
||||
event.shiftKey,
|
||||
);
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
const nextWidth = element.width + deltaX;
|
||||
mutateElement(element, {
|
||||
@ -1708,15 +1717,7 @@ export class App extends React.Component<any, AppState> {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(
|
||||
element,
|
||||
p1,
|
||||
deltaX,
|
||||
deltaY,
|
||||
x,
|
||||
y,
|
||||
event.shiftKey,
|
||||
);
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
@ -1737,15 +1738,7 @@ export class App extends React.Component<any, AppState> {
|
||||
resizeArrowFn = arrowResizeOrigin;
|
||||
}
|
||||
}
|
||||
resizeArrowFn(
|
||||
element,
|
||||
p1,
|
||||
deltaX,
|
||||
deltaY,
|
||||
x,
|
||||
y,
|
||||
event.shiftKey,
|
||||
);
|
||||
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
|
||||
} else {
|
||||
mutateElement(element, {
|
||||
width: element.width + deltaX,
|
||||
@ -1756,9 +1749,13 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
break;
|
||||
case "n": {
|
||||
let points;
|
||||
if (element.points.length > 0) {
|
||||
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) {
|
||||
const pnt = points[i];
|
||||
@ -1769,13 +1766,18 @@ export class App extends React.Component<any, AppState> {
|
||||
mutateElement(element, {
|
||||
height: element.height - deltaY,
|
||||
y: element.y + deltaY,
|
||||
points,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "w": {
|
||||
let points;
|
||||
if (element.points.length > 0) {
|
||||
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) {
|
||||
const pnt = points[i];
|
||||
@ -1786,13 +1788,19 @@ export class App extends React.Component<any, AppState> {
|
||||
mutateElement(element, {
|
||||
width: element.width - deltaX,
|
||||
x: element.x + deltaX,
|
||||
points,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "s": {
|
||||
let points;
|
||||
|
||||
if (element.points.length > 0) {
|
||||
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) {
|
||||
const pnt = points[i];
|
||||
@ -1802,14 +1810,18 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
mutateElement(element, {
|
||||
height: element.height + deltaY,
|
||||
points: element.points, // no-op, but signifies that we mutated points in-place above
|
||||
points,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "e": {
|
||||
let points;
|
||||
if (element.points.length > 0) {
|
||||
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) {
|
||||
const pnt = points[i];
|
||||
@ -1819,7 +1831,7 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
mutateElement(element, {
|
||||
width: element.width + deltaX,
|
||||
points: element.points, // no-op, but signifies that we mutated points in-place above
|
||||
points,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -1838,7 +1850,6 @@ export class App extends React.Component<any, AppState> {
|
||||
x: element.x,
|
||||
y: element.y,
|
||||
});
|
||||
invalidateShapeForElement(el);
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
@ -1851,7 +1862,10 @@ export class App extends React.Component<any, AppState> {
|
||||
// Marking that click was used for dragging to check
|
||||
// if elements should be deselected on pointerup
|
||||
draggingOccurred = true;
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length > 0) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
@ -1906,14 +1920,12 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
if (points.length === 1) {
|
||||
points.push([dx, dy]);
|
||||
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
||||
} else if (points.length > 1) {
|
||||
const pnt = points[points.length - 1];
|
||||
pnt[0] = dx;
|
||||
pnt[1] = dy;
|
||||
mutateElement(draggingElement, {
|
||||
points: [...points.slice(0, -1), [dx, dy]],
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(draggingElement, { points });
|
||||
} else {
|
||||
if (event.shiftKey) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
@ -1935,14 +1947,15 @@ export class App extends React.Component<any, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
invalidateShapeForElement(draggingElement);
|
||||
|
||||
if (this.state.elementType === "selection") {
|
||||
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
||||
if (
|
||||
!event.shiftKey &&
|
||||
isSomeElementSelected(scene.getAllElements(), this.state)
|
||||
) {
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
const elementsWithinSelection = getElementsWithinSelection(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
draggingElement,
|
||||
);
|
||||
this.setState(prevState => ({
|
||||
@ -1990,12 +2003,12 @@ export class App extends React.Component<any, AppState> {
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
draggingElement.points.push([
|
||||
x - draggingElement.x,
|
||||
y - draggingElement.y,
|
||||
]);
|
||||
mutateElement(draggingElement);
|
||||
invalidateShapeForElement(draggingElement);
|
||||
mutateElement(draggingElement, {
|
||||
points: [
|
||||
...draggingElement.points,
|
||||
[x - draggingElement.x, y - draggingElement.y],
|
||||
],
|
||||
});
|
||||
this.setState({
|
||||
multiElement: this.state.draggingElement,
|
||||
editingElement: this.state.draggingElement,
|
||||
@ -2030,7 +2043,7 @@ export class App extends React.Component<any, AppState> {
|
||||
isInvisiblySmallElement(draggingElement)
|
||||
) {
|
||||
// remove invisible element which was added in onPointerDown
|
||||
elements = elements.slice(0, -1);
|
||||
this.replaceElements(scene.getAllElements().slice(0, -1));
|
||||
this.setState({
|
||||
draggingElement: null,
|
||||
});
|
||||
@ -2047,7 +2060,9 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
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
|
||||
@ -2090,7 +2105,7 @@ export class App extends React.Component<any, AppState> {
|
||||
|
||||
if (
|
||||
elementType !== "selection" ||
|
||||
isSomeElementSelected(elements, this.state)
|
||||
isSomeElementSelected(scene.getAllElements(), this.state)
|
||||
) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
@ -2163,7 +2178,7 @@ export class App extends React.Component<any, AppState> {
|
||||
return duplicate;
|
||||
});
|
||||
|
||||
elements = [...elements, ...newElements];
|
||||
this.replaceElements([...scene.getAllElements(), ...newElements]);
|
||||
history.resumeRecording();
|
||||
this.setState({
|
||||
selectedElementIds: newElements.reduce((map, element) => {
|
||||
@ -2174,7 +2189,11 @@ export class App extends React.Component<any, AppState> {
|
||||
};
|
||||
|
||||
private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
|
||||
const elementClickedInside = getElementContainingPosition(elements, x, y);
|
||||
const elementClickedInside = getElementContainingPosition(
|
||||
scene.getAllElements(),
|
||||
x,
|
||||
y,
|
||||
);
|
||||
if (elementClickedInside) {
|
||||
const elementCenterX =
|
||||
elementClickedInside.x + elementClickedInside.width / 2;
|
||||
@ -2209,7 +2228,7 @@ export class App extends React.Component<any, AppState> {
|
||||
};
|
||||
|
||||
private saveDebounced = debounce(() => {
|
||||
saveToLocalStorage(elements, this.state);
|
||||
saveToLocalStorage(scene.getAllElements(), this.state);
|
||||
}, 300);
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -2233,7 +2252,7 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
});
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
elements,
|
||||
scene.getAllElements(),
|
||||
this.state,
|
||||
this.state.selectionElement,
|
||||
window.devicePixelRatio,
|
||||
@ -2254,20 +2273,22 @@ export class App extends React.Component<any, AppState> {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside =
|
||||
!atLeastOneVisibleElement && hasNonDeletedElements(elements);
|
||||
!atLeastOneVisibleElement &&
|
||||
hasNonDeletedElements(scene.getAllElements());
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside: scrolledOutside });
|
||||
}
|
||||
this.saveDebounced();
|
||||
|
||||
if (
|
||||
getDrawingVersion(elements) > this.lastBroadcastedOrReceivedSceneVersion
|
||||
getDrawingVersion(scene.getAllElements()) >
|
||||
this.lastBroadcastedOrReceivedSceneVersion
|
||||
) {
|
||||
this.broadcastSceneUpdate();
|
||||
}
|
||||
|
||||
if (history.isRecording()) {
|
||||
history.pushEntry(this.state, elements);
|
||||
history.pushEntry(this.state, scene.getAllElements());
|
||||
history.skipRecording();
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export function serializeAsJSON(
|
||||
type: "excalidraw",
|
||||
version: 1,
|
||||
source: window.location.origin,
|
||||
elements,
|
||||
elements: elements.filter(element => !element.isDeleted),
|
||||
appState: cleanAppStateForExport(appState),
|
||||
},
|
||||
null,
|
||||
|
@ -10,7 +10,10 @@ export function saveToLocalStorage(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(elements.filter(element => !element.isDeleted)),
|
||||
);
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY_STATE,
|
||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Point } from "../types";
|
||||
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { rotate } from "../math";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Point } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
|
||||
// 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
|
||||
if (op === "move") {
|
||||
// change starting point
|
||||
currentP = data as Point;
|
||||
currentP = (data as unknown) as Point;
|
||||
// move operation does not draw anything; so, it always
|
||||
// returns false
|
||||
} else if (op === "bcurveTo") {
|
||||
@ -133,7 +133,7 @@ export function getArrowPoints(element: ExcalidrawElement, shape: Drawable[]) {
|
||||
const prevOp = ops[ops.length - 2];
|
||||
let p0: Point = [0, 0];
|
||||
if (prevOp.op === "move") {
|
||||
p0 = prevOp.data as Point;
|
||||
p0 = (prevOp.data as unknown) as Point;
|
||||
} else if (prevOp.op === "bcurveTo") {
|
||||
p0 = [prevOp.data[4], prevOp.data[5]];
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
getElementAbsoluteCoords,
|
||||
getLinearElementAbsoluteBounds,
|
||||
} from "./bounds";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Point } from "../types";
|
||||
import { Drawable, OpSet } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
@ -231,7 +231,7 @@ const hitTestRoughShape = (opSet: OpSet[], x: number, y: number) => {
|
||||
// move, bcurveTo, lineTo, and curveTo
|
||||
if (op === "move") {
|
||||
// change starting point
|
||||
currentP = data as Point;
|
||||
currentP = (data as unknown) as Point;
|
||||
// move operation does not draw anything; so, it always
|
||||
// returns false
|
||||
} else if (op === "bcurveTo") {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { randomSeed } from "roughjs/bin/math";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
@ -9,45 +10,35 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
// 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 drawing.
|
||||
export function mutateElement(
|
||||
element: ExcalidrawElement,
|
||||
updates?: ElementUpdate<ExcalidrawElement>,
|
||||
export function mutateElement<TElement extends ExcalidrawElement>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
) {
|
||||
if (updates) {
|
||||
Object.assign(element, updates);
|
||||
const mutableElement = element as any;
|
||||
|
||||
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(
|
||||
element: ExcalidrawElement,
|
||||
updates: ElementUpdate<ExcalidrawElement>,
|
||||
): ExcalidrawElement {
|
||||
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 {
|
||||
export function newElementWith<TElement extends ExcalidrawElement>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
): TElement {
|
||||
return {
|
||||
...element,
|
||||
...updates,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomSeed } from "roughjs/bin/math";
|
||||
import nanoid from "nanoid";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Point } from "../types";
|
||||
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import { measureText } from "../utils";
|
||||
@ -32,7 +32,7 @@ export function newElement(
|
||||
roughness,
|
||||
opacity,
|
||||
seed: randomSeed(),
|
||||
points: [] as Point[],
|
||||
points: [] as readonly Point[],
|
||||
version: 1,
|
||||
versionNonce: 0,
|
||||
isDeleted: false,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
|
||||
@ -101,7 +100,5 @@ export function normalizeDimensions(
|
||||
});
|
||||
}
|
||||
|
||||
invalidateShapeForElement(element);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { measureText } from "../utils";
|
||||
import { ExcalidrawTextElement } from "./types";
|
||||
import { mutateTextElement } from "./mutateElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
|
||||
const metrics = measureText(element.text, element.font);
|
||||
mutateTextElement(element, {
|
||||
mutateElement(element, {
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Point } from "./types";
|
||||
|
||||
// https://stackoverflow.com/a/6853926/232122
|
||||
export function distanceBetweenPointAndSegment(
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from "../element/bounds";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { RoughSVG } from "roughjs/bin/svg";
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
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
|
||||
// initial position to it
|
||||
const points: Point[] = element.points.length
|
||||
? element.points
|
||||
: [[0, 0]];
|
||||
const points = element.points.length ? element.points : [[0, 0]];
|
||||
|
||||
// curve is always the first 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
|
||||
if (element.type === "arrow") {
|
||||
|
@ -1,6 +1,17 @@
|
||||
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 = () => {
|
||||
const elements: readonly ExcalidrawElement[] = [];
|
||||
return { elements };
|
||||
return new SceneState();
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { ExcalidrawElement, PointerType } from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
||||
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
||||
export type Point = Readonly<RoughPoint>;
|
||||
|
||||
export type AppState = {
|
||||
draggingElement: ExcalidrawElement | null;
|
||||
|
Loading…
Reference in New Issue
Block a user