Browse Source

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

Co-authored-by: dwelle <luzar.david@gmail.com>
Aakansha Doshi 4 years ago
parent
commit
20500b7822

+ 1 - 4
src/actions/manager.tsx

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

+ 82 - 86
src/components/App.tsx

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

+ 7 - 5
src/element/linearElementEditor.ts

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

+ 2 - 3
src/element/mutateElement.ts

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

+ 0 - 1
src/element/newElement.ts

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

+ 6 - 4
src/element/textWysiwyg.tsx

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

+ 121 - 0
src/scene/Scene.ts

@@ -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;

+ 0 - 80
src/scene/globalScene.ts

@@ -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();

+ 0 - 1
src/scene/index.ts

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