From 20500b7822705ba1827bca100393a3a241731290 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 30 Jul 2020 14:50:59 +0530 Subject: [PATCH] remove shared global scene and attach it to every instance (#1706) Co-authored-by: dwelle --- src/actions/manager.tsx | 5 +- src/components/App.tsx | 168 ++++++++++++++--------------- src/element/linearElementEditor.ts | 12 ++- src/element/mutateElement.ts | 5 +- src/element/newElement.ts | 1 - src/element/textWysiwyg.tsx | 10 +- src/scene/Scene.ts | 121 +++++++++++++++++++++ src/scene/globalScene.ts | 80 -------------- src/scene/index.ts | 1 - 9 files changed, 219 insertions(+), 184 deletions(-) create mode 100644 src/scene/Scene.ts delete mode 100644 src/scene/globalScene.ts diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 2c4350ec..865a4201 100644 --- a/src/actions/manager.tsx +++ b/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; diff --git a/src/components/App.tsx b/src/components/App.tsx index 831e5100..12ec4183 100644 --- a/src/components/App.tsx +++ b/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 { portal: Portal = new Portal(this); lastBroadcastedOrReceivedSceneVersion: number = -1; broadcastedElementVersions: Map = new Map(); - removeSceneCallback: SceneStateCallbackRemover | null = null; unmounted: boolean = false; actionManager: ActionManager; private excalidrawRef: any; @@ -252,6 +250,7 @@ class App extends React.Component { width: window.innerWidth, height: window.innerHeight, }; + private scene: Scene; constructor(props: ExcalidrawProps) { super(props); @@ -266,11 +265,12 @@ class App extends React.Component { ...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 { 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 { 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 { if (actionResult.syncHistory) { history.setCurrentState( this.state, - globalSceneState.getElementsIncludingDeleted(), + this.scene.getElementsIncludingDeleted(), ); } }, @@ -421,7 +421,7 @@ class App extends React.Component { }; 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 { }); } - 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 { 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 { ); } 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 { ); 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 { 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 { }); 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 { 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 { 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 { 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 { // 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 { 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 { } let syncableElements = getSyncableElements( - globalSceneState.getElementsIncludingDeleted(), + this.scene.getElementsIncludingDeleted(), ); if (!syncAll) { @@ -1340,7 +1332,7 @@ class App extends React.Component { }; 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 { (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 { 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 { ) { 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 { }); private setElements = (elements: readonly ExcalidrawElement[]) => { - globalSceneState.replaceAllElements(elements); + this.scene.replaceAllElements(elements); }; private handleTextWysiwyg( @@ -1570,8 +1565,8 @@ class App extends React.Component { }, ) { 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 { setCursorForShape(this.state.elementType); } }), + element, }); // deselect all other elements when inserting text this.setState({ @@ -1642,7 +1638,7 @@ class App extends React.Component { y: number, ): NonDeleted | null { const element = getElementAtPosition( - globalSceneState.getElements(), + this.scene.getElements(), this.state, x, y, @@ -1715,8 +1711,8 @@ class App extends React.Component { 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 { } const selectedElements = getSelectedElements( - globalSceneState.getElements(), + this.scene.getElements(), this.state, ); @@ -1763,7 +1759,10 @@ class App extends React.Component { ) { 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 { 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 { selectedElementIds: { [hitElement!.id]: true }, selectedGroupIds: {}, }, - globalSceneState.getElements(), + this.scene.getElements(), ), ); return; @@ -1960,7 +1959,7 @@ class App extends React.Component { 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 { 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 { 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 { [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 { 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 { 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 { 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 { 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 { // 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 { 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 { nextElements.push(element); } } - globalSceneState.replaceAllElements([ + this.scene.replaceAllElements([ ...nextElements, ...elementsToAppend, ]); @@ -2925,7 +2924,7 @@ class App extends React.Component { } 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 { }, {} as any), }, }, - globalSceneState.getElements(), + this.scene.getElements(), ), ); } @@ -3065,8 +3064,8 @@ class App extends React.Component { 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 { } 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 { 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 { 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 { scale: number, ) { const elementClickedInside = getElementContainingPosition( - globalSceneState + this.scene .getElementsIncludingDeleted() .filter((element) => !isTextElement(element)), x, @@ -3467,10 +3466,7 @@ class App extends React.Component { }, 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: { diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index 844d762c..b24ba1b4 100644 --- a/src/element/linearElementEditor.ts +++ b/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) { - LinearElementEditor.normalizePoints(element); - + constructor(element: NonDeleted, 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["elementId"]) { - const element = globalSceneState.getNonDeletedElement(id); + const element = Scene.getScene(id)?.getNonDeletedElement(id); if (element) { return element as NonDeleted; } diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts index 63366765..d04d2391 100644 --- a/src/element/mutateElement.ts +++ b/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 = >( element.version++; element.versionNonce = randomInteger(); - - globalSceneState.informMutation(); + Scene.getScene(element)?.informMutation(); }; export const newElementWith = ( diff --git a/src/element/newElement.ts b/src/element/newElement.ts index c1bd1dae..e9af4f8a 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -125,7 +125,6 @@ export const newTextElement = ( }, {}, ); - return textElement; }; diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 1530baa2..c810405e 100644 --- a/src/element/textWysiwyg.tsx +++ b/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(); }); diff --git a/src/scene/Scene.ts b/src/scene/Scene.ts new file mode 100644 index 00000000..39cfc0c6 --- /dev/null +++ b/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["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(); + private static sceneMapById = new Map(); + + 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 = new Set(); + + private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; + private elements: readonly ExcalidrawElement[] = []; + private elementsMap = new Map(); + + 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 | 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; diff --git a/src/scene/globalScene.ts b/src/scene/globalScene.ts deleted file mode 100644 index 441c0c0a..00000000 --- a/src/scene/globalScene.ts +++ /dev/null @@ -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 = 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 | 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(); diff --git a/src/scene/index.ts b/src/scene/index.ts index 01e7f1b0..9113cfe5 100644 --- a/src/scene/index.ts +++ b/src/scene/index.ts @@ -15,4 +15,3 @@ export { hasText, } from "./comparisons"; export { getZoomOrigin, getNormalizedZoom } from "./zoom"; -export { globalSceneState } from "./globalScene";