|
@@ -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 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 locallySelectedIds = arrayToMap(selectedElements);
|
|
|
+
|
|
|
+ const selections: {
|
|
|
+ angle: number;
|
|
|
+ elementX1: number;
|
|
|
+ elementY1: number;
|
|
|
+ elementX2: number;
|
|
|
+ elementY2: number;
|
|
|
+ selectionColors: string[];
|
|
|
+ dashed?: boolean;
|
|
|
+ cx: number;
|
|
|
+ cy: number;
|
|
|
+ activeEmbeddable: boolean;
|
|
|
+ }[] = [];
|
|
|
+
|
|
|
+ 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);
|
|
|
- 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;
|
|
|
- },
|
|
|
- [],
|
|
|
- );
|
|
|
+ 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,
|