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

@ -21,7 +21,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
import type { Drawable } from "roughjs/bin/core";
import type { RoughSVG } from "roughjs/bin/svg";
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
import {
SVGRenderConfig,
StaticCanvasRenderConfig,
RenderableElementsMap,
} from "../scene/types";
import {
distance,
getFontString,
@ -611,6 +615,7 @@ export const renderSelectionElement = (
export const renderElement = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,
@ -715,7 +720,7 @@ export const renderElement = (
let shiftX = (x2 - x1) / 2 - (element.x - x1);
let shiftY = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) {
const container = getContainerElement(element);
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
const boundTextCoords =
LinearElementEditor.getBoundTextElementPosition(
@ -900,6 +905,7 @@ const maybeWrapNodesInFrameClipPath = (
export const renderElementToSvg = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
rsvg: RoughSVG,
svgRoot: SVGElement,
files: BinaryFiles,
@ -912,7 +918,7 @@ export const renderElementToSvg = (
let cx = (x2 - x1) / 2 - (element.x - x1);
let cy = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) {
const container = getContainerElement(element);
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
@ -1013,6 +1019,7 @@ export const renderElementToSvg = (
createPlaceholderEmbeddableLabel(element);
renderElementToSvg(
label,
elementsMap,
rsvg,
root,
files,

View file

@ -33,6 +33,7 @@ import {
SVGRenderConfig,
StaticCanvasRenderConfig,
StaticSceneRenderConfig,
RenderableElementsMap,
} from "../scene/types";
import {
getScrollBars,
@ -61,7 +62,7 @@ import {
TransformHandles,
TransformHandleType,
} from "../element/transformHandles";
import { throttleRAF } from "../utils";
import { arrayToMap, throttleRAF } from "../utils";
import { UserIdleState } from "../types";
import { FRAME_STYLE, THEME_FILTER } from "../constants";
import {
@ -75,10 +76,7 @@ import {
isIframeLikeElement,
isLinearElement,
} from "../element/typeChecks";
import {
isIframeLikeOrItsLabel,
createPlaceholderEmbeddableLabel,
} from "../element/embeddable";
import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
import {
elementOverlapsWithFrame,
getTargetFrame,
@ -446,7 +444,7 @@ const bootstrapCanvas = ({
const _renderInteractiveScene = ({
canvas,
elements,
elementsMap,
visibleElements,
selectedElements,
scale,
@ -454,7 +452,7 @@ const _renderInteractiveScene = ({
renderConfig,
}: InteractiveSceneRenderConfig) => {
if (canvas === null) {
return { atLeastOneVisibleElement: false, elements };
return { atLeastOneVisibleElement: false, elementsMap };
}
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
@ -562,75 +560,64 @@ const _renderInteractiveScene = ({
if (showBoundingBox) {
// Optimisation for finding quickly relevant element ids
const locallySelectedIds = selectedElements.reduce(
(acc: Record<string, boolean>, element) => {
acc[element.id] = true;
return acc;
},
{},
);
const locallySelectedIds = arrayToMap(selectedElements);
const selections = elements.reduce(
(
acc: {
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[],
element,
) => {
const selectionColors = [];
// local user
if (
locallySelectedIds[element.id] &&
!isSelectedViaGroup(appState, element)
) {
selectionColors.push(selectionColor);
}
// remote users
if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(socketId: string) => {
const background = getClientColor(socketId);
return background;
},
),
);
}
const selections: {
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[] = [];
if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true);
acc.push({
angle: element.angle,
elementX1,
elementY1,
elementX2,
elementY2,
selectionColors,
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
cx,
cy,
activeEmbeddable:
appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active",
});
}
return acc;
},
[],
);
for (const element of elementsMap.values()) {
const selectionColors = [];
// local user
if (
locallySelectedIds.has(element.id) &&
!isSelectedViaGroup(appState, element)
) {
selectionColors.push(selectionColor);
}
// remote users
if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(socketId: string) => {
const background = getClientColor(socketId);
return background;
},
),
);
}
if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true);
selections.push({
angle: element.angle,
elementX1,
elementY1,
elementX2,
elementY2,
selectionColors,
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
cx,
cy,
activeEmbeddable:
appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active",
});
}
}
const addSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elements, groupId);
const groupElements = getElementsInGroup(elementsMap, groupId);
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(groupElements);
selections.push({
@ -870,7 +857,7 @@ const _renderInteractiveScene = ({
let scrollBars;
if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars(
elements,
elementsMap,
normalizedWidth,
normalizedHeight,
appState,
@ -897,14 +884,14 @@ const _renderInteractiveScene = ({
return {
scrollBars,
atLeastOneVisibleElement: visibleElements.length > 0,
elements,
elementsMap,
};
};
const _renderStaticScene = ({
canvas,
rc,
elements,
elementsMap,
visibleElements,
scale,
appState,
@ -965,7 +952,7 @@ const _renderStaticScene = ({
// Paint visible elements
visibleElements
.filter((el) => !isIframeLikeOrItsLabel(el))
.filter((el) => !isIframeLikeElement(el))
.forEach((element) => {
try {
const frameId = element.frameId || appState.frameToHighlight?.id;
@ -977,16 +964,30 @@ const _renderStaticScene = ({
) {
context.save();
const frame = getTargetFrame(element, appState);
const frame = getTargetFrame(element, elementsMap, appState);
// TODO do we need to check isElementInFrame here?
if (frame && isElementInFrame(element, elements, appState)) {
if (frame && isElementInFrame(element, elementsMap, appState)) {
frameClip(frame, context, renderConfig, appState);
}
renderElement(element, rc, context, renderConfig, appState);
renderElement(
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
context.restore();
} else {
renderElement(element, rc, context, renderConfig, appState);
renderElement(
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
@ -998,11 +999,18 @@ const _renderStaticScene = ({
// render embeddables on top
visibleElements
.filter((el) => isIframeLikeOrItsLabel(el))
.filter((el) => isIframeLikeElement(el))
.forEach((element) => {
try {
const render = () => {
renderElement(element, rc, context, renderConfig, appState);
renderElement(
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
if (
isIframeLikeElement(element) &&
@ -1014,7 +1022,14 @@ const _renderStaticScene = ({
element.height
) {
const label = createPlaceholderEmbeddableLabel(element);
renderElement(label, rc, context, renderConfig, appState);
renderElement(
label,
elementsMap,
rc,
context,
renderConfig,
appState,
);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
@ -1032,9 +1047,9 @@ const _renderStaticScene = ({
) {
context.save();
const frame = getTargetFrame(element, appState);
const frame = getTargetFrame(element, elementsMap, appState);
if (frame && isElementInFrame(element, elements, appState)) {
if (frame && isElementInFrame(element, elementsMap, appState)) {
frameClip(frame, context, renderConfig, appState);
}
render();
@ -1448,6 +1463,7 @@ const renderLinkIcon = (
// This should be only called for exporting purposes
export const renderSceneToSvg = (
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: RenderableElementsMap,
rsvg: RoughSVG,
svgRoot: SVGElement,
files: BinaryFiles,
@ -1459,12 +1475,13 @@ export const renderSceneToSvg = (
// render elements
elements
.filter((el) => !isIframeLikeOrItsLabel(el))
.filter((el) => !isIframeLikeElement(el))
.forEach((element) => {
if (!element.isDeleted) {
try {
renderElementToSvg(
element,
elementsMap,
rsvg,
svgRoot,
files,
@ -1486,6 +1503,7 @@ export const renderSceneToSvg = (
try {
renderElementToSvg(
element,
elementsMap,
rsvg,
svgRoot,
files,