remove shared global scene and attach it to every instance (#1706)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2020-07-30 14:50:59 +05:30 committed by GitHub
parent 54f8d8f820
commit 20500b7822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 184 deletions

View File

@ -9,7 +9,6 @@ import {
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { t } from "../i18n"; import { t } from "../i18n";
import { globalSceneState } from "../scene";
export class ActionManager implements ActionsManagerInterface { export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"]; actions = {} as ActionsManagerInterface["actions"];
@ -23,9 +22,7 @@ export class ActionManager implements ActionsManagerInterface {
constructor( constructor(
updater: UpdaterFn, updater: UpdaterFn,
getAppState: () => AppState, getAppState: () => AppState,
getElementsIncludingDeleted: () => ReturnType< getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
typeof globalSceneState["getElementsIncludingDeleted"]
>,
) { ) {
this.updater = updater; this.updater = updater;
this.getAppState = getAppState; this.getAppState = getAppState;

View File

@ -39,7 +39,6 @@ import {
getElementContainingPosition, getElementContainingPosition,
getNormalizedZoom, getNormalizedZoom,
getSelectedElements, getSelectedElements,
globalSceneState,
isSomeElementSelected, isSomeElementSelected,
calculateScrollCenter, calculateScrollCenter,
} from "../scene"; } from "../scene";
@ -137,7 +136,6 @@ import { generateCollaborationLink, getCollaborationLinkData } from "../data";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { unstable_batchedUpdates } from "react-dom"; import { unstable_batchedUpdates } from "react-dom";
import { SceneStateCallbackRemover } from "../scene/globalScene";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { actionFinalize, actionDeleteSelected } from "../actions"; import { actionFinalize, actionDeleteSelected } from "../actions";
import { import {
@ -155,6 +153,7 @@ import {
getSelectedGroupIdForElement, getSelectedGroupIdForElement,
} from "../groups"; } from "../groups";
import { Library } from "../data/library"; import { Library } from "../data/library";
import Scene from "../scene/Scene";
/** /**
* @param func handler taking at most single parameter (event). * @param func handler taking at most single parameter (event).
@ -243,7 +242,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
portal: Portal = new Portal(this); portal: Portal = new Portal(this);
lastBroadcastedOrReceivedSceneVersion: number = -1; lastBroadcastedOrReceivedSceneVersion: number = -1;
broadcastedElementVersions: Map<string, number> = new Map(); broadcastedElementVersions: Map<string, number> = new Map();
removeSceneCallback: SceneStateCallbackRemover | null = null;
unmounted: boolean = false; unmounted: boolean = false;
actionManager: ActionManager; actionManager: ActionManager;
private excalidrawRef: any; private excalidrawRef: any;
@ -252,6 +250,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
}; };
private scene: Scene;
constructor(props: ExcalidrawProps) { constructor(props: ExcalidrawProps) {
super(props); super(props);
@ -266,11 +265,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
...this.getCanvasOffsets(), ...this.getCanvasOffsets(),
}; };
this.scene = new Scene();
this.excalidrawRef = React.createRef(); this.excalidrawRef = React.createRef();
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
this.syncActionResult, this.syncActionResult,
() => this.state, () => this.state,
() => globalSceneState.getElementsIncludingDeleted(), () => this.scene.getElementsIncludingDeleted(),
); );
this.actionManager.registerAll(actions); this.actionManager.registerAll(actions);
@ -308,7 +308,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
appState={this.state} appState={this.state}
setAppState={this.setAppState} setAppState={this.setAppState}
actionManager={this.actionManager} actionManager={this.actionManager}
elements={globalSceneState.getElements()} elements={this.scene.getElements()}
onRoomCreate={this.openPortal} onRoomCreate={this.openPortal}
onRoomDestroy={this.closePortal} onRoomDestroy={this.closePortal}
onUsernameChange={(username) => { onUsernameChange={(username) => {
@ -368,7 +368,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
editingElement = element; editingElement = element;
} }
}); });
globalSceneState.replaceAllElements(actionResult.elements); this.scene.replaceAllElements(actionResult.elements);
if (actionResult.commitToHistory) { if (actionResult.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
@ -394,7 +394,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (actionResult.syncHistory) { if (actionResult.syncHistory) {
history.setCurrentState( history.setCurrentState(
this.state, this.state,
globalSceneState.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
); );
} }
}, },
@ -421,7 +421,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}; };
private onFontLoaded = () => { private onFontLoaded = () => {
globalSceneState.getElementsIncludingDeleted().forEach((element) => { this.scene.getElementsIncludingDeleted().forEach((element) => {
if (isTextElement(element)) { if (isTextElement(element)) {
invalidateShapeForElement(element); invalidateShapeForElement(element);
} }
@ -562,9 +562,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
} }
this.removeSceneCallback = globalSceneState.addCallback( this.scene.addCallback(this.onSceneUpdated);
this.onSceneUpdated,
);
this.addEventListeners(); this.addEventListeners();
this.setState(this.getCanvasOffsets(), () => { this.setState(this.getCanvasOffsets(), () => {
@ -574,14 +572,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
public componentWillUnmount() { public componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
this.removeSceneCallback!();
this.removeEventListeners(); this.removeEventListeners();
this.scene.destroy();
clearTimeout(touchTimeout); clearTimeout(touchTimeout);
} }
private onResize = withBatchedUpdates(() => { private onResize = withBatchedUpdates(() => {
globalSceneState this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.forEach((element) => invalidateShapeForElement(element)); .forEach((element) => invalidateShapeForElement(element));
this.setState({}); this.setState({});
@ -682,10 +679,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
); );
} catch {} } catch {}
} }
if ( if (this.state.isCollaborating && this.scene.getElements().length > 0) {
this.state.isCollaborating &&
globalSceneState.getElements().length > 0
) {
event.preventDefault(); event.preventDefault();
// NOTE: modern browsers no longer allow showing a custom message here // NOTE: modern browsers no longer allow showing a custom message here
event.returnValue = ""; event.returnValue = "";
@ -753,7 +747,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
); );
cursorButton[socketID] = user.button; cursorButton[socketID] = user.button;
}); });
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const { atLeastOneVisibleElement, scrollBars } = renderScene( const { atLeastOneVisibleElement, scrollBars } = renderScene(
elements.filter((element) => { elements.filter((element) => {
// don't render text element that's being currently edited (it's // don't render text element that's being currently edited (it's
@ -798,14 +792,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.saveDebounced(); this.saveDebounced();
if ( if (
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) > getDrawingVersion(this.scene.getElementsIncludingDeleted()) >
this.lastBroadcastedOrReceivedSceneVersion this.lastBroadcastedOrReceivedSceneVersion
) { ) {
this.broadcastScene(SCENE.UPDATE, /* syncAll */ false); this.broadcastScene(SCENE.UPDATE, /* syncAll */ false);
this.queueBroadcastAllElements(); this.queueBroadcastAllElements();
} }
history.record(this.state, globalSceneState.getElementsIncludingDeleted()); history.record(this.state, this.scene.getElementsIncludingDeleted());
} }
// Copy/paste // Copy/paste
@ -828,11 +822,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
private copyAll = () => { private copyAll = () => {
copyToAppClipboard(globalSceneState.getElements(), this.state); copyToAppClipboard(this.scene.getElements(), this.state);
}; };
private copyToClipboardAsPng = () => { private copyToClipboardAsPng = () => {
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
exportCanvas( exportCanvas(
@ -846,14 +840,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private copyToClipboardAsSvg = () => { private copyToClipboardAsSvg = () => {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
exportCanvas( exportCanvas(
"clipboard-svg", "clipboard-svg",
selectedElements.length selectedElements.length ? selectedElements : this.scene.getElements(),
? selectedElements
: globalSceneState.getElements(),
this.state, this.state,
this.canvas!, this.canvas!,
this.state, this.state,
@ -958,15 +950,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const dy = y - elementsCenterY; const dy = y - elementsCenterY;
const groupIdMap = new Map(); const groupIdMap = new Map();
const newElements = clipboardElements.map((element) => const newElements = clipboardElements.map((element) => {
duplicateElement(this.state.editingGroupId, groupIdMap, element, { return duplicateElement(this.state.editingGroupId, groupIdMap, element, {
x: element.x + dx - minX, x: element.x + dx - minX,
y: element.y + dy - minY, y: element.y + dy - minY,
}), });
); });
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
...newElements, ...newElements,
]); ]);
history.resumeRecording(); history.resumeRecording();
@ -1004,8 +996,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
verticalAlign: DEFAULT_VERTICAL_ALIGN, verticalAlign: DEFAULT_VERTICAL_ALIGN,
}); });
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
element, element,
]); ]);
this.setState({ selectedElementIds: { [element.id]: true } }); this.setState({ selectedElementIds: { [element.id]: true } });
@ -1116,15 +1108,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
// 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 ( if (
globalSceneState.getElementsIncludingDeleted() == null || this.scene.getElementsIncludingDeleted() == null ||
globalSceneState.getElementsIncludingDeleted().length === 0 this.scene.getElementsIncludingDeleted().length === 0
) { ) {
globalSceneState.replaceAllElements(remoteElements); this.scene.replaceAllElements(remoteElements);
} 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( const localElementMap = getElementMap(
globalSceneState.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
); );
// Reconcile // Reconcile
@ -1183,7 +1175,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
newElements, newElements,
); );
globalSceneState.replaceAllElements(newElements); this.scene.replaceAllElements(newElements);
} }
// 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
@ -1317,7 +1309,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
let syncableElements = getSyncableElements( let syncableElements = getSyncableElements(
globalSceneState.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
); );
if (!syncAll) { if (!syncAll) {
@ -1340,7 +1332,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}; };
this.lastBroadcastedOrReceivedSceneVersion = Math.max( this.lastBroadcastedOrReceivedSceneVersion = Math.max(
this.lastBroadcastedOrReceivedSceneVersion, this.lastBroadcastedOrReceivedSceneVersion,
getDrawingVersion(globalSceneState.getElementsIncludingDeleted()), getDrawingVersion(this.scene.getElementsIncludingDeleted()),
); );
for (const syncableElement of syncableElements) { for (const syncableElement of syncableElements) {
this.broadcastedElementVersions.set( this.broadcastedElementVersions.set(
@ -1427,8 +1419,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
(event.shiftKey (event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT); : ELEMENT_TRANSLATE_AMOUNT);
globalSceneState.replaceAllElements( this.scene.replaceAllElements(
globalSceneState.getElementsIncludingDeleted().map((el) => { this.scene.getElementsIncludingDeleted().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) {
@ -1448,7 +1440,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ENTER) { } else if (event.key === KEYS.ENTER) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
@ -1462,7 +1454,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) { ) {
history.resumeRecording(); history.resumeRecording();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]), editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene,
),
}); });
} }
} else if ( } else if (
@ -1558,7 +1553,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
private setElements = (elements: readonly ExcalidrawElement[]) => { private setElements = (elements: readonly ExcalidrawElement[]) => {
globalSceneState.replaceAllElements(elements); this.scene.replaceAllElements(elements);
}; };
private handleTextWysiwyg( private handleTextWysiwyg(
@ -1570,8 +1565,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, },
) { ) {
const updateElement = (text: string, isDeleted = false) => { const updateElement = (text: string, isDeleted = false) => {
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement(_element, { return updateTextElement(_element, {
text, text,
@ -1624,6 +1619,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
setCursorForShape(this.state.elementType); setCursorForShape(this.state.elementType);
} }
}), }),
element,
}); });
// deselect all other elements when inserting text // deselect all other elements when inserting text
this.setState({ this.setState({
@ -1642,7 +1638,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
y: number, y: number,
): NonDeleted<ExcalidrawTextElement> | null { ): NonDeleted<ExcalidrawTextElement> | null {
const element = getElementAtPosition( const element = getElementAtPosition(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
x, x,
y, y,
@ -1715,8 +1711,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN }); mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
} }
} else { } else {
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
element, element,
]); ]);
@ -1752,7 +1748,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
@ -1763,7 +1759,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) { ) {
history.resumeRecording(); history.resumeRecording();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]), editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene,
),
}); });
} }
return; return;
@ -1781,7 +1780,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const selectedGroupIds = getSelectedGroupIds(this.state); const selectedGroupIds = getSelectedGroupIds(this.state);
if (selectedGroupIds.length > 0) { if (selectedGroupIds.length > 0) {
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const hitElement = getElementAtPosition( const hitElement = getElementAtPosition(
elements, elements,
this.state, this.state,
@ -1803,7 +1802,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
selectedElementIds: { [hitElement!.id]: true }, selectedElementIds: { [hitElement!.id]: true },
selectedGroupIds: {}, selectedGroupIds: {},
}, },
globalSceneState.getElements(), this.scene.getElements(),
), ),
); );
return; return;
@ -1960,7 +1959,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return; return;
} }
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if ( if (
@ -2262,7 +2261,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
window.devicePixelRatio, window.devicePixelRatio,
); );
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
@ -2377,7 +2376,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
): boolean => { ): boolean => {
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithResizeHandler = getElementWithResizeHandler( const elementWithResizeHandler = getElementWithResizeHandler(
@ -2491,12 +2490,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
[hitElement!.id]: true, [hitElement!.id]: true,
}, },
}, },
globalSceneState.getElements(), this.scene.getElements(),
); );
}); });
// TODO: this is strange... // TODO: this is strange...
globalSceneState.replaceAllElements( this.scene.replaceAllElements(
globalSceneState.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
); );
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
} }
@ -2610,8 +2609,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
mutateElement(element, { mutateElement(element, {
points: [...element.points, [0, 0]], points: [...element.points, [0, 0]],
}); });
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
element, element,
]); ]);
this.setState({ this.setState({
@ -2649,8 +2648,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
draggingElement: element, draggingElement: element,
}); });
} else { } else {
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
element, element,
]); ]);
this.setState({ this.setState({
@ -2672,7 +2671,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (pointerDownState.drag.offset === null) { if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors( pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY( getDragOffsetXY(
getSelectedElements(globalSceneState.getElements(), this.state), getSelectedElements(this.scene.getElements(), this.state),
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@ -2735,7 +2734,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (pointerDownState.resize.isResizing) { if (pointerDownState.resize.isResizing) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
const resizeHandle = pointerDownState.resize.handle; const resizeHandle = pointerDownState.resize.handle;
@ -2796,7 +2795,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
// if elements should be deselected on pointerup // if elements should be deselected on pointerup
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
globalSceneState.getElements(), this.scene.getElements(),
this.state, this.state,
); );
if (selectedElements.length > 0) { if (selectedElements.length > 0) {
@ -2818,7 +2817,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const nextElements = []; const nextElements = [];
const elementsToAppend = []; const elementsToAppend = [];
const groupIdMap = new Map(); const groupIdMap = new Map();
for (const element of globalSceneState.getElementsIncludingDeleted()) { for (const element of this.scene.getElementsIncludingDeleted()) {
if ( if (
this.state.selectedElementIds[element.id] || this.state.selectedElementIds[element.id] ||
// case: the state.selectedElementIds might not have been // case: the state.selectedElementIds might not have been
@ -2846,7 +2845,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
nextElements.push(element); nextElements.push(element);
} }
} }
globalSceneState.replaceAllElements([ this.scene.replaceAllElements([
...nextElements, ...nextElements,
...elementsToAppend, ...elementsToAppend,
]); ]);
@ -2925,7 +2924,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) { if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
this.setState({ this.setState({
selectedElementIds: {}, selectedElementIds: {},
@ -2949,7 +2948,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, {} as any), }, {} as any),
}, },
}, },
globalSceneState.getElements(), this.scene.getElements(),
), ),
); );
} }
@ -3065,8 +3064,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
isInvisiblySmallElement(draggingElement) isInvisiblySmallElement(draggingElement)
) { ) {
// remove invisible element which was added in onPointerDown // remove invisible element which was added in onPointerDown
globalSceneState.replaceAllElements( this.scene.replaceAllElements(
globalSceneState.getElementsIncludingDeleted().slice(0, -1), this.scene.getElementsIncludingDeleted().slice(0, -1),
); );
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
@ -3086,8 +3085,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
if (resizingElement && isInvisiblySmallElement(resizingElement)) { if (resizingElement && isInvisiblySmallElement(resizingElement)) {
globalSceneState.replaceAllElements( this.scene.replaceAllElements(
globalSceneState this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.filter((el) => el.id !== resizingElement.id), .filter((el) => el.id !== resizingElement.id),
); );
@ -3143,7 +3142,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if ( if (
elementType !== "selection" || elementType !== "selection" ||
isSomeElementSelected(globalSceneState.getElements(), this.state) isSomeElementSelected(this.scene.getElements(), this.state)
) { ) {
history.resumeRecording(); history.resumeRecording();
} }
@ -3283,7 +3282,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
window.devicePixelRatio, window.devicePixelRatio,
); );
const elements = globalSceneState.getElements(); const elements = this.scene.getElements();
const element = getElementAtPosition( const element = getElementAtPosition(
elements, elements,
this.state, this.state,
@ -3409,7 +3408,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
scale: number, scale: number,
) { ) {
const elementClickedInside = getElementContainingPosition( const elementClickedInside = getElementContainingPosition(
globalSceneState this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.filter((element) => !isTextElement(element)), .filter((element) => !isTextElement(element)),
x, x,
@ -3467,10 +3466,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, 300); }, 300);
private saveDebounced = debounce(() => { private saveDebounced = debounce(() => {
saveToLocalStorage( saveToLocalStorage(this.scene.getElementsIncludingDeleted(), this.state);
globalSceneState.getElementsIncludingDeleted(),
this.state,
);
}, 300); }, 300);
private getCanvasOffsets() { private getCanvasOffsets() {
@ -3515,10 +3511,10 @@ if (
Object.defineProperties(window.h, { Object.defineProperties(window.h, {
elements: { elements: {
get() { get() {
return globalSceneState.getElementsIncludingDeleted(); return this.app.scene.getElementsIncludingDeleted();
}, },
set(elements: ExcalidrawElement[]) { set(elements: ExcalidrawElement[]) {
return globalSceneState.replaceAllElements(elements); return this.app.scene.replaceAllElements(elements);
}, },
}, },
history: { history: {

View File

@ -9,7 +9,8 @@ import { getElementPointsCoords } from "./bounds";
import { Point, AppState } from "../types"; import { Point, AppState } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { SceneHistory } from "../history"; import { SceneHistory } from "../history";
import { globalSceneState } from "../scene";
import Scene from "../scene/Scene";
export class LinearElementEditor { export class LinearElementEditor {
public elementId: ExcalidrawElement["id"] & { public elementId: ExcalidrawElement["id"] & {
@ -19,12 +20,13 @@ export class LinearElementEditor {
public draggingElementPointIndex: number | null; public draggingElementPointIndex: number | null;
public lastUncommittedPoint: Point | null; public lastUncommittedPoint: Point | null;
constructor(element: NonDeleted<ExcalidrawLinearElement>) { constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
LinearElementEditor.normalizePoints(element);
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
Scene.mapElementToScene(this.elementId, scene);
LinearElementEditor.normalizePoints(element);
this.activePointIndex = null; this.activePointIndex = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
this.draggingElementPointIndex = null; this.draggingElementPointIndex = null;
@ -41,7 +43,7 @@ export class LinearElementEditor {
* statically guarantee this method returns an ExcalidrawLinearElement) * statically guarantee this method returns an ExcalidrawLinearElement)
*/ */
static getElement(id: InstanceType<typeof LinearElementEditor>["elementId"]) { static getElement(id: InstanceType<typeof LinearElementEditor>["elementId"]) {
const element = globalSceneState.getNonDeletedElement(id); const element = Scene.getScene(id)?.getNonDeletedElement(id);
if (element) { if (element) {
return element as NonDeleted<ExcalidrawLinearElement>; return element as NonDeleted<ExcalidrawLinearElement>;
} }

View File

@ -1,6 +1,6 @@
import { ExcalidrawElement } from "./types"; import { ExcalidrawElement } from "./types";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { globalSceneState } from "../scene"; import Scene from "../scene/Scene";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random"; import { randomInteger } from "../random";
import { Point } from "../types"; import { Point } from "../types";
@ -81,8 +81,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element.version++; element.version++;
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
Scene.getScene(element)?.informMutation();
globalSceneState.informMutation();
}; };
export const newElementWith = <TElement extends ExcalidrawElement>( export const newElementWith = <TElement extends ExcalidrawElement>(

View File

@ -125,7 +125,6 @@ export const newTextElement = (
}, },
{}, {},
); );
return textElement; return textElement;
}; };

View File

@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isWritableElement, getFontString } from "../utils"; import { isWritableElement, getFontString } from "../utils";
import { globalSceneState } from "../scene"; import Scene from "../scene/Scene";
import { isTextElement } from "./typeChecks"; import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants"; import { CLASSES } from "../constants";
import { ExcalidrawElement } from "./types"; import { ExcalidrawElement } from "./types";
@ -37,16 +37,18 @@ export const textWysiwyg = ({
onChange, onChange,
onSubmit, onSubmit,
getViewportCoords, getViewportCoords,
element,
}: { }: {
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
appState: AppState; appState: AppState;
onChange?: (text: string) => void; onChange?: (text: string) => void;
onSubmit: (text: string) => void; onSubmit: (text: string) => void;
getViewportCoords: (x: number, y: number) => [number, number]; getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawElement;
}) => { }) => {
function updateWysiwygStyle() { function updateWysiwygStyle() {
const updatedElement = globalSceneState.getElement(id); const updatedElement = Scene.getScene(element)?.getElement(id);
if (isTextElement(updatedElement)) { if (updatedElement && isTextElement(updatedElement)) {
const [viewportX, viewportY] = getViewportCoords( const [viewportX, viewportY] = getViewportCoords(
updatedElement.x, updatedElement.x,
updatedElement.y, updatedElement.y,
@ -183,7 +185,7 @@ export const textWysiwyg = ({
}; };
// handle updates of textElement properties of editing element // handle updates of textElement properties of editing element
const unbindUpdate = globalSceneState.addCallback(() => { const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
updateWysiwygStyle(); updateWysiwygStyle();
editable.focus(); editable.focus();
}); });

121
src/scene/Scene.ts Normal file
View File

@ -0,0 +1,121 @@
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeleted,
} from "../element/types";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
if (typeof elementKey === "string") {
return true;
}
return false;
};
class Scene {
// ---------------------------------------------------------------------------
// static methods/props
// ---------------------------------------------------------------------------
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
private static sceneMapById = new Map<string, Scene>();
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
if (isIdKey(elementKey)) {
this.sceneMapById.set(elementKey, scene);
} else {
this.sceneMapByElement.set(elementKey, scene);
}
}
static getScene(elementKey: ElementKey): Scene | null {
if (isIdKey(elementKey)) {
return this.sceneMapById.get(elementKey) || null;
}
return this.sceneMapByElement.get(elementKey) || null;
}
// ---------------------------------------------------------------------------
// instance methods/props
// ---------------------------------------------------------------------------
private callbacks: Set<SceneStateCallback> = new Set();
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private elements: readonly ExcalidrawElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
getElementsIncludingDeleted() {
return this.elements;
}
getElements(): readonly NonDeletedExcalidrawElement[] {
return this.nonDeletedElements;
}
getElement(id: ExcalidrawElement["id"]): ExcalidrawElement | null {
return this.elementsMap.get(id) || null;
}
getNonDeletedElement(
id: ExcalidrawElement["id"],
): NonDeleted<ExcalidrawElement> | null {
const element = this.getElement(id);
if (element && isNonDeletedElement(element)) {
return element;
}
return null;
}
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this.elements = nextElements;
this.elementsMap.clear();
nextElements.forEach((element) => {
this.elementsMap.set(element.id, element);
Scene.mapElementToScene(element, this);
});
this.nonDeletedElements = getNonDeletedElements(this.elements);
this.informMutation();
}
informMutation() {
for (const callback of Array.from(this.callbacks)) {
callback();
}
}
addCallback(cb: SceneStateCallback): SceneStateCallbackRemover {
if (this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.add(cb);
return () => {
if (!this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.delete(cb);
};
}
destroy() {
Scene.sceneMapById.forEach((scene, elementKey) => {
if (scene === this) {
Scene.sceneMapById.delete(elementKey);
}
});
// done not for memory leaks, but to guard against possible late fires
// (I guess?)
this.callbacks.clear();
}
}
export default Scene;

View File

@ -1,80 +0,0 @@
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeleted,
} from "../element/types";
import {
getNonDeletedElements,
isNonDeletedElement,
getElementMap,
} from "../element";
export interface SceneStateCallback {
(): void;
}
export interface SceneStateCallbackRemover {
(): void;
}
class GlobalScene {
private callbacks: Set<SceneStateCallback> = new Set();
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private elements: readonly ExcalidrawElement[] = [];
private elementsMap: {
[id: string]: ExcalidrawElement;
} = {};
getElementsIncludingDeleted() {
return this.elements;
}
getElements(): readonly NonDeletedExcalidrawElement[] {
return this.nonDeletedElements;
}
getElement(id: ExcalidrawElement["id"]): ExcalidrawElement | null {
return this.elementsMap[id] || null;
}
getNonDeletedElement(
id: ExcalidrawElement["id"],
): NonDeleted<ExcalidrawElement> | null {
const element = this.getElement(id);
if (element && isNonDeletedElement(element)) {
return element;
}
return null;
}
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this.elements = nextElements;
this.elementsMap = getElementMap(nextElements);
this.nonDeletedElements = getNonDeletedElements(this.elements);
this.informMutation();
}
informMutation() {
for (const callback of Array.from(this.callbacks)) {
callback();
}
}
addCallback(cb: SceneStateCallback): SceneStateCallbackRemover {
if (this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.add(cb);
return () => {
if (!this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.delete(cb);
};
}
}
export const globalSceneState = new GlobalScene();

View File

@ -15,4 +15,3 @@ export {
hasText, hasText,
} from "./comparisons"; } from "./comparisons";
export { getZoomOrigin, getNormalizedZoom } from "./zoom"; export { getZoomOrigin, getNormalizedZoom } from "./zoom";
export { globalSceneState } from "./globalScene";