mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
refactor: decoupling global Scene state part-1 (#7577)
This commit is contained in:
parent
740a165452
commit
0415c616b1
31 changed files with 630 additions and 384 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue