remove shared global scene and attach it to every instance (#1706)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
54f8d8f820
commit
20500b7822
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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>(
|
||||
|
@ -125,7 +125,6 @@ export const newTextElement = (
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return textElement;
|
||||
};
|
||||
|
||||
|
@ -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
src/scene/Scene.ts
Normal file
121
src/scene/Scene.ts
Normal 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;
|
@ -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();
|
@ -15,4 +15,3 @@ export {
|
||||
hasText,
|
||||
} from "./comparisons";
|
||||
export { getZoomOrigin, getNormalizedZoom } from "./zoom";
|
||||
export { globalSceneState } from "./globalScene";
|
||||
|
Loading…
x
Reference in New Issue
Block a user