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 { 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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)),
|
||||||
|
@ -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";
|
||||||
|
@ -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]];
|
||||||
}
|
}
|
||||||
|
@ -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") {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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") {
|
||||||
|
@ -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 };
|
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user