fix: remove scene hack from export.ts & remove pass elementsMap to getContainingFrame (#7713)

* fix: remove scene hack from export.ts as its not needed anymore

* remove

* pass elementsMap to getContainingFrame

* remove unused `mapElementIds` param

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Aakansha Doshi 2024-02-21 16:34:20 +05:30 committed by GitHub
parent 2e719ff671
commit 361a9449bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 38 additions and 92 deletions

View File

@ -16,7 +16,7 @@ import {
import { deepCopyElement } from "./element/newElement"; import { deepCopyElement } from "./element/newElement";
import { mutateElement } from "./element/mutateElement"; import { mutateElement } from "./element/mutateElement";
import { getContainingFrame } from "./frame"; import { getContainingFrame } from "./frame";
import { isMemberOf, isPromiseLike } from "./utils"; import { arrayToMap, isMemberOf, isPromiseLike } from "./utils";
import { t } from "./i18n"; import { t } from "./i18n";
type ElementsClipboard = { type ElementsClipboard = {
@ -126,6 +126,7 @@ export const serializeAsClipboardJSON = ({
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | null; files: BinaryFiles | null;
}) => { }) => {
const elementsMap = arrayToMap(elements);
const framesToCopy = new Set( const framesToCopy = new Set(
elements.filter((element) => isFrameLikeElement(element)), elements.filter((element) => isFrameLikeElement(element)),
); );
@ -152,8 +153,8 @@ export const serializeAsClipboardJSON = ({
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: elements.map((element) => { elements: elements.map((element) => {
if ( if (
getContainingFrame(element) && getContainingFrame(element, elementsMap) &&
!framesToCopy.has(getContainingFrame(element)!) !framesToCopy.has(getContainingFrame(element, elementsMap)!)
) { ) {
const copiedElement = deepCopyElement(element); const copiedElement = deepCopyElement(element);
mutateElement(copiedElement, { mutateElement(copiedElement, {

View File

@ -1131,7 +1131,7 @@ class App extends React.Component<AppProps, AppState> {
display: isVisible ? "block" : "none", display: isVisible ? "block" : "none",
opacity: getRenderOpacity( opacity: getRenderOpacity(
el, el,
getContainingFrame(el), getContainingFrame(el, this.scene.getNonDeletedElementsMap()),
this.elementsPendingErasure, this.elementsPendingErasure,
), ),
["--embeddable-radius" as string]: `${getCornerRadius( ["--embeddable-radius" as string]: `${getCornerRadius(
@ -4399,7 +4399,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
).filter((element) => { ).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit // hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element, elementsMap);
return containingFrame && return containingFrame &&
this.state.frameRendering.enabled && this.state.frameRendering.enabled &&
this.state.frameRendering.clip this.state.frameRendering.clip
@ -7789,7 +7789,7 @@ class App extends React.Component<AppProps, AppState> {
); );
if (linearElement?.frameId) { if (linearElement?.frameId) {
const frame = getContainingFrame(linearElement); const frame = getContainingFrame(linearElement, elementsMap);
if (frame && linearElement) { if (frame && linearElement) {
if ( if (

View File

@ -21,7 +21,7 @@ import { mutateElement } from "./element/mutateElement";
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types"; import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene"; import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds"; import { getElementLineSegments } from "./element/bounds";
import { import {
doLineSegmentsIntersect, doLineSegmentsIntersect,
@ -377,25 +377,13 @@ export const getElementsInNewFrame = (
export const getContainingFrame = ( export const getContainingFrame = (
element: ExcalidrawElement, element: ExcalidrawElement,
/** elementsMap: ElementsMap,
* Optionally an elements map, in case the elements aren't in the Scene yet.
* Takes precedence over Scene elements, even if the element exists
* in Scene elements and not the supplied elements map.
*/
elementsMap?: Map<string, ExcalidrawElement>,
) => { ) => {
if (element.frameId) { if (!element.frameId) {
if (elementsMap) { return null;
return (elementsMap.get(element.frameId) ||
null) as null | ExcalidrawFrameLikeElement;
}
return (
(Scene.getScene(element)?.getElement(
element.frameId,
) as ExcalidrawFrameLikeElement) || null
);
} }
return null; return (elementsMap.get(element.frameId) ||
null) as null | ExcalidrawFrameLikeElement;
}; };
// --------------------------- Frame Operations ------------------------------- // --------------------------- Frame Operations -------------------------------
@ -697,7 +685,7 @@ export const getTargetFrame = (
return appState.selectedElementIds[_element.id] && return appState.selectedElementIds[_element.id] &&
appState.selectedElementsAreBeingDragged appState.selectedElementsAreBeingDragged
? appState.frameToHighlight ? appState.frameToHighlight
: getContainingFrame(_element); : getContainingFrame(_element, elementsMap);
}; };
// TODO: this a huge bottleneck for large scenes, optimise // TODO: this a huge bottleneck for large scenes, optimise

View File

@ -257,7 +257,8 @@ const generateElementCanvas = (
canvasOffsetY, canvasOffsetY,
boundTextElementVersion: boundTextElementVersion:
getBoundTextElement(element, elementsMap)?.version || null, getBoundTextElement(element, elementsMap)?.version || null,
containingFrameOpacity: getContainingFrame(element)?.opacity || 100, containingFrameOpacity:
getContainingFrame(element, elementsMap)?.opacity || 100,
}; };
}; };
@ -440,7 +441,8 @@ const generateElementWithCanvas = (
const boundTextElementVersion = const boundTextElementVersion =
getBoundTextElement(element, elementsMap)?.version || null; getBoundTextElement(element, elementsMap)?.version || null;
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100; const containingFrameOpacity =
getContainingFrame(element, elementsMap)?.opacity || 100;
if ( if (
!prevElementWithCanvas || !prevElementWithCanvas ||
@ -652,7 +654,7 @@ export const renderElement = (
) => { ) => {
context.globalAlpha = getRenderOpacity( context.globalAlpha = getRenderOpacity(
element, element,
getContainingFrame(element), getContainingFrame(element, elementsMap),
renderConfig.elementsPendingErasure, renderConfig.elementsPendingErasure,
); );
@ -924,11 +926,12 @@ const maybeWrapNodesInFrameClipPath = (
root: SVGElement, root: SVGElement,
nodes: SVGElement[], nodes: SVGElement[],
frameRendering: AppState["frameRendering"], frameRendering: AppState["frameRendering"],
elementsMap: RenderableElementsMap,
) => { ) => {
if (!frameRendering.enabled || !frameRendering.clip) { if (!frameRendering.enabled || !frameRendering.clip) {
return null; return null;
} }
const frame = getContainingFrame(element); const frame = getContainingFrame(element, elementsMap);
if (frame) { if (frame) {
const g = root.ownerDocument!.createElementNS(SVG_NS, "g"); const g = root.ownerDocument!.createElementNS(SVG_NS, "g");
g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`); g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`);
@ -990,7 +993,9 @@ export const renderElementToSvg = (
}; };
const opacity = const opacity =
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000; ((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
element.opacity) /
10000;
switch (element.type) { switch (element.type) {
case "selection": { case "selection": {
@ -1024,6 +1029,7 @@ export const renderElementToSvg = (
root, root,
[node], [node],
renderConfig.frameRendering, renderConfig.frameRendering,
elementsMap,
); );
addToRoot(g || node, element); addToRoot(g || node, element);
@ -1215,6 +1221,7 @@ export const renderElementToSvg = (
root, root,
[group, maskPath], [group, maskPath],
renderConfig.frameRendering, renderConfig.frameRendering,
elementsMap,
); );
if (g) { if (g) {
addToRoot(g, element); addToRoot(g, element);
@ -1258,6 +1265,7 @@ export const renderElementToSvg = (
root, root,
[node], [node],
renderConfig.frameRendering, renderConfig.frameRendering,
elementsMap,
); );
addToRoot(g || node, element); addToRoot(g || node, element);
@ -1355,6 +1363,7 @@ export const renderElementToSvg = (
root, root,
[g], [g],
renderConfig.frameRendering, renderConfig.frameRendering,
elementsMap,
); );
addToRoot(clipG || g, element); addToRoot(clipG || g, element);
} }
@ -1442,6 +1451,7 @@ export const renderElementToSvg = (
root, root,
[node], [node],
renderConfig.frameRendering, renderConfig.frameRendering,
elementsMap,
); );
addToRoot(g || node, element); addToRoot(g || node, element);

View File

@ -80,29 +80,16 @@ class Scene {
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>(); private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
private static sceneMapById = new Map<string, Scene>(); private static sceneMapById = new Map<string, Scene>();
static mapElementToScene( static mapElementToScene(elementKey: ElementKey, scene: Scene) {
elementKey: ElementKey,
scene: Scene,
/**
* needed because of frame exporting hack.
* elementId:Scene mapping will be removed completely, soon.
*/
mapElementIds = true,
) {
if (isIdKey(elementKey)) { if (isIdKey(elementKey)) {
if (!mapElementIds) {
return;
}
// for cases where we don't have access to the element object // for cases where we don't have access to the element object
// (e.g. restore serialized appState with id references) // (e.g. restore serialized appState with id references)
this.sceneMapById.set(elementKey, scene); this.sceneMapById.set(elementKey, scene);
} else { } else {
this.sceneMapByElement.set(elementKey, scene); this.sceneMapByElement.set(elementKey, scene);
if (!mapElementIds) { // if mapping element objects, also cache the id string when later
// if mapping element objects, also cache the id string when later // looking up by id alone
// looking up by id alone this.sceneMapById.set(elementKey.id, scene);
this.sceneMapById.set(elementKey.id, scene);
}
} }
} }
@ -256,7 +243,7 @@ class Scene {
return didChange; return didChange;
} }
replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) { replaceAllElements(nextElements: ElementsMapOrArray) {
this.elements = this.elements =
// ts doesn't like `Array.isArray` of `instanceof Map` // ts doesn't like `Array.isArray` of `instanceof Map`
nextElements instanceof Array nextElements instanceof Array
@ -269,7 +256,7 @@ class Scene {
nextFrameLikes.push(element); nextFrameLikes.push(element);
} }
this.elementsMap.set(element.id, element); this.elementsMap.set(element.id, element);
Scene.mapElementToScene(element, this, mapElementIds); Scene.mapElementToScene(element, this);
}); });
const nonDeletedElements = getNonDeletedElements(this.elements); const nonDeletedElements = getNonDeletedElements(this.elements);
this.nonDeletedElements = nonDeletedElements.elements; this.nonDeletedElements = nonDeletedElements.elements;

View File

@ -12,13 +12,7 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "../element/bounds"; } from "../element/bounds";
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
import { import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
arrayToMap,
cloneJSON,
distance,
getFontString,
toBrandedType,
} from "../utils";
import { AppState, BinaryFiles } from "../types"; import { AppState, BinaryFiles } from "../types";
import { import {
DEFAULT_EXPORT_PADDING, DEFAULT_EXPORT_PADDING,
@ -42,35 +36,11 @@ import {
import { newTextElement } from "../element"; import { newTextElement } from "../element";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import Scene from "./Scene";
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks"; import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
import { RenderableElementsMap } from "./types"; import { RenderableElementsMap } from "./types";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`; const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
// getContainerElement and getBoundTextElement and potentially other helpers
// depend on `Scene` which will not be available when these pure utils are
// called outside initialized Excalidraw editor instance or even if called
// from inside Excalidraw if the elements were never cached by Scene (e.g.
// for library elements).
//
// As such, before passing the elements down, we need to initialize a custom
// Scene instance and assign them to it.
//
// FIXME This is a super hacky workaround and we'll need to rewrite this soon.
const __createSceneForElementsHack__ = (
elements: readonly ExcalidrawElement[],
) => {
const scene = new Scene();
// we can't duplicate elements to regenerate ids because we need the
// orig ids when embedding. So we do another hack of not mapping element
// ids to Scene instances so that we don't override the editor elements
// mapping.
// We still need to clone the objects themselves to regen references.
scene.replaceAllElements(cloneJSON(elements), false);
return scene;
};
const truncateText = (element: ExcalidrawTextElement, maxWidth: number) => { const truncateText = (element: ExcalidrawTextElement, maxWidth: number) => {
if (element.width <= maxWidth) { if (element.width <= maxWidth) {
return element; return element;
@ -213,9 +183,6 @@ export const exportToCanvas = async (
return { canvas, scale: appState.exportScale }; return { canvas, scale: appState.exportScale };
}, },
) => { ) => {
const tempScene = __createSceneForElementsHack__(elements);
elements = tempScene.getNonDeletedElements();
const frameRendering = getFrameRenderingConfig( const frameRendering = getFrameRenderingConfig(
exportingFrame ?? null, exportingFrame ?? null,
appState.frameRendering ?? null, appState.frameRendering ?? null,
@ -281,8 +248,6 @@ export const exportToCanvas = async (
}, },
}); });
tempScene.destroy();
return canvas; return canvas;
}; };
@ -306,9 +271,6 @@ export const exportToSvg = async (
exportingFrame?: ExcalidrawFrameLikeElement | null; exportingFrame?: ExcalidrawFrameLikeElement | null;
}, },
): Promise<SVGSVGElement> => { ): Promise<SVGSVGElement> => {
const tempScene = __createSceneForElementsHack__(elements);
elements = tempScene.getNonDeletedElements();
const frameRendering = getFrameRenderingConfig( const frameRendering = getFrameRenderingConfig(
opts?.exportingFrame ?? null, opts?.exportingFrame ?? null,
appState.frameRendering ?? null, appState.frameRendering ?? null,
@ -470,8 +432,6 @@ export const exportToSvg = async (
}, },
); );
tempScene.destroy();
return svgRoot; return svgRoot;
}; };

View File

@ -57,7 +57,7 @@ export const getElementsWithinSelection = (
elementsMap, elementsMap,
); );
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element, elementsMap);
if (containingFrame) { if (containingFrame) {
const [fx1, fy1, fx2, fy2] = getElementBounds( const [fx1, fy1, fx2, fy2] = getElementBounds(
containingFrame, containingFrame,
@ -86,7 +86,7 @@ export const getElementsWithinSelection = (
: elementsInSelection; : elementsInSelection;
elementsInSelection = elementsInSelection.filter((element) => { elementsInSelection = elementsInSelection.filter((element) => {
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element, elementsMap);
if (containingFrame) { if (containingFrame) {
return elementOverlapsWithFrame(element, containingFrame, elementsMap); return elementOverlapsWithFrame(element, containingFrame, elementsMap);