refactor: decoupling global Scene state part-1 (#7577)

This commit is contained in:
David Luzar 2024-01-22 00:23:02 +01:00 committed by GitHub
parent 740a165452
commit 0415c616b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 630 additions and 384 deletions

View file

@ -1,7 +1,6 @@
import React, { useState } from "react";
import { useState } from "react";
import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types";
import { t } from "../i18n";
import { useDevice } from "./App";
import {
@ -44,17 +43,14 @@ import { useTunnels } from "../context/tunnels";
export const SelectedShapeActions = ({
appState,
elements,
elementsMap,
renderAction,
}: {
appState: UIAppState;
elements: readonly ExcalidrawElement[];
elementsMap: NonDeletedElementsMap;
renderAction: ActionManager["renderAction"];
}) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
);
const targetElements = getTargetElements(elementsMap, appState);
let isSingleElementBoundContainer = false;
if (
@ -137,12 +133,12 @@ export const SelectedShapeActions = ({
{renderAction("changeFontFamily")}
{(appState.activeTool.type === "text" ||
suppportsHorizontalAlign(targetElements)) &&
suppportsHorizontalAlign(targetElements, elementsMap)) &&
renderAction("changeTextAlign")}
</>
)}
{shouldAllowVerticalAlign(targetElements) &&
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
renderAction("changeVerticalAlign")}
{(canHaveArrowheads(appState.activeTool.type) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && (

View file

@ -1417,7 +1417,7 @@ class App extends React.Component<AppProps, AppState> {
const { renderTopRightUI, renderCustomStats } = this.props;
const versionNonce = this.scene.getVersionNonce();
const { canvasElements, visibleElements } =
const { elementsMap, visibleElements } =
this.renderer.getRenderableElements({
versionNonce,
zoom: this.state.zoom,
@ -1627,7 +1627,7 @@ class App extends React.Component<AppProps, AppState> {
<StaticCanvas
canvas={this.canvas}
rc={this.rc}
elements={canvasElements}
elementsMap={elementsMap}
visibleElements={visibleElements}
versionNonce={versionNonce}
selectionNonce={
@ -1648,7 +1648,7 @@ class App extends React.Component<AppProps, AppState> {
<InteractiveCanvas
containerRef={this.excalidrawContainerRef}
canvas={this.interactiveCanvas}
elements={canvasElements}
elementsMap={elementsMap}
visibleElements={visibleElements}
selectedElements={selectedElements}
versionNonce={versionNonce}
@ -2780,7 +2780,7 @@ class App extends React.Component<AppProps, AppState> {
private renderInteractiveSceneCallback = ({
atLeastOneVisibleElement,
scrollBars,
elements,
elementsMap,
}: RenderInteractiveSceneCallback) => {
if (scrollBars) {
currentScrollBars = scrollBars;
@ -2789,7 +2789,7 @@ class App extends React.Component<AppProps, AppState> {
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && elements.length > 0;
: !atLeastOneVisibleElement && elementsMap.size > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
@ -3119,7 +3119,10 @@ class App extends React.Component<AppProps, AppState> {
newElements.forEach((newElement) => {
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
const container = getContainerElement(newElement);
const container = getContainerElement(
newElement,
this.scene.getElementsMapIncludingDeleted(),
);
redrawTextBoundingBox(newElement, container);
}
});
@ -4183,11 +4186,18 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement(_element, {
text,
isDeleted,
originalText,
});
return updateTextElement(
_element,
getContainerElement(
_element,
this.scene.getElementsMapIncludingDeleted(),
),
{
text,
isDeleted,
originalText,
},
);
}
return _element;
}),
@ -7700,13 +7710,9 @@ class App extends React.Component<AppProps, AppState> {
groupIds: [],
});
this.scene.replaceAllElements(
removeElementsFromFrame(
this.scene.getElementsIncludingDeleted(),
[linearElement],
this.state,
),
);
removeElementsFromFrame([linearElement]);
this.scene.informMutation();
}
}
}
@ -7716,7 +7722,7 @@ class App extends React.Component<AppProps, AppState> {
this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = this.scene.getSelectedElements(this.state);
let nextElements = this.scene.getElementsIncludingDeleted();
let nextElements = this.scene.getElementsMapIncludingDeleted();
const updateGroupIdsAfterEditingGroup = (
elements: ExcalidrawElement[],
@ -7809,7 +7815,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements(
addElementsToFrame(
this.scene.getElementsIncludingDeleted(),
this.scene.getElementsMapIncludingDeleted(),
elementsInsideFrame,
draggingElement,
),
@ -7857,7 +7863,6 @@ class App extends React.Component<AppProps, AppState> {
this.state,
),
frame,
this.state,
);
}
@ -9137,10 +9142,10 @@ class App extends React.Component<AppProps, AppState> {
if (
transformElements(
pointerDownState,
pointerDownState.originalElements,
transformHandleType,
selectedElements,
pointerDownState.resize.arrowDirection,
this.scene.getElementsMapIncludingDeleted(),
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),
selectedElements.length === 1 && isImageElement(selectedElements[0])
@ -9150,7 +9155,6 @@ class App extends React.Component<AppProps, AppState> {
resizeY,
pointerDownState.resize.center.x,
pointerDownState.resize.center.y,
this.state,
)
) {
this.maybeSuggestBindingForAll(selectedElements);

View file

@ -226,7 +226,7 @@ const LayerUI = ({
>
<SelectedShapeActions
appState={appState}
elements={elements}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
/>
</Island>

View file

@ -183,7 +183,7 @@ export const MobileMenu = ({
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elements={elements}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
/>
</Section>

View file

@ -7,6 +7,7 @@ import type { DOMAttributes } from "react";
import type { AppState, InteractiveCanvasAppState } from "../../types";
import type {
InteractiveCanvasRenderConfig,
RenderableElementsMap,
RenderInteractiveSceneCallback,
} from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
@ -15,7 +16,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils";
type InteractiveCanvasProps = {
containerRef: React.RefObject<HTMLDivElement>;
canvas: HTMLCanvasElement | null;
elements: readonly NonDeletedExcalidrawElement[];
elementsMap: RenderableElementsMap;
visibleElements: readonly NonDeletedExcalidrawElement[];
selectedElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
@ -113,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
renderInteractiveScene(
{
canvas: props.canvas,
elements: props.elements,
elementsMap: props.elementsMap,
visibleElements: props.visibleElements,
selectedElements: props.selectedElements,
scale: window.devicePixelRatio,
@ -201,10 +202,10 @@ const areEqual = (
prevProps.selectionNonce !== nextProps.selectionNonce ||
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on element arrays because they may have renewed
// we need to memoize on elementsMap because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elements !== nextProps.elements ||
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.visibleElements !== nextProps.visibleElements ||
prevProps.selectedElements !== nextProps.selectedElements
) {

View file

@ -3,14 +3,17 @@ import { RoughCanvas } from "roughjs/bin/canvas";
import { renderStaticScene } from "../../renderer/renderScene";
import { isShallowEqual } from "../../utils";
import type { AppState, StaticCanvasAppState } from "../../types";
import type { StaticCanvasRenderConfig } from "../../scene/types";
import type {
RenderableElementsMap,
StaticCanvasRenderConfig,
} from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
import { isRenderThrottlingEnabled } from "../../reactUtils";
type StaticCanvasProps = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elements: readonly NonDeletedExcalidrawElement[];
elementsMap: RenderableElementsMap;
visibleElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
selectionNonce: number | undefined;
@ -63,7 +66,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
canvas,
rc: props.rc,
scale: props.scale,
elements: props.elements,
elementsMap: props.elementsMap,
visibleElements: props.visibleElements,
appState: props.appState,
renderConfig: props.renderConfig,
@ -106,10 +109,10 @@ const areEqual = (
if (
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on element arrays because they may have renewed
// we need to memoize on elementsMap because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elements !== nextProps.elements ||
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.visibleElements !== nextProps.visibleElements
) {
return false;