diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index acbe56741..d9471d657 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -348,6 +348,7 @@ import { updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, + getElementsOverlappingFrame, } from "../frame"; import { excludeElementsInFramesFromSelection, @@ -395,7 +396,7 @@ import { import { Emitter } from "../emitter"; import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; import { MagicCacheData, diagramToHTML } from "../data/magic"; -import { elementsOverlappingBBox, exportToBlob } from "../../utils/export"; +import { exportToBlob } from "../../utils/export"; import { COLOR_PALETTE } from "../colors"; import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; @@ -1803,11 +1804,10 @@ class App extends React.Component { return; } - const magicFrameChildren = elementsOverlappingBBox({ - elements: this.scene.getNonDeletedElements(), - bounds: magicFrame, - type: "overlap", - }).filter((el) => !isMagicFrameElement(el)); + const magicFrameChildren = getElementsOverlappingFrame( + this.scene.getNonDeletedElements(), + magicFrame, + ).filter((el) => !isMagicFrameElement(el)); if (!magicFrameChildren.length) { if (source === "button") { diff --git a/packages/excalidraw/data/index.ts b/packages/excalidraw/data/index.ts index 7e93542ac..0c63053a9 100644 --- a/packages/excalidraw/data/index.ts +++ b/packages/excalidraw/data/index.ts @@ -11,7 +11,6 @@ import { NonDeletedExcalidrawElement, } from "../element/types"; import { t } from "../i18n"; -import { elementsOverlappingBBox } from "../../utils/export"; import { isSomeElementSelected, getSelectedElements } from "../scene"; import { exportToCanvas, exportToSvg } from "../scene/export"; import { ExportType } from "../scene/types"; @@ -20,6 +19,7 @@ import { cloneJSON } from "../utils"; import { canvasToBlob } from "./blob"; import { fileSave, FileSystemHandle } from "./filesystem"; import { serializeAsJSON } from "./json"; +import { getElementsOverlappingFrame } from "../frame"; export { loadFromBlob } from "./blob"; export { loadFromJSON, saveAsJSON } from "./json"; @@ -56,11 +56,7 @@ export const prepareElementsForExport = ( isFrameLikeElement(exportedElements[0]) ) { exportingFrame = exportedElements[0]; - exportedElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + exportedElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (exportedElements.length > 1) { exportedElements = getSelectedElements( elements, diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 3818e6684..db58bb626 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -21,7 +21,10 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene"; import { getElementLineSegments } from "./element/bounds"; -import { doLineSegmentsIntersect } from "../utils/export"; +import { + doLineSegmentsIntersect, + elementsOverlappingBBox, +} from "../utils/export"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; // --------------------------- Frame State ------------------------------------ @@ -664,3 +667,19 @@ export const getFrameLikeTitle = ( // TODO name frames AI only is specific to AI frames return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`; }; + +export const getElementsOverlappingFrame = ( + elements: readonly ExcalidrawElement[], + frame: ExcalidrawFrameLikeElement, +) => { + return ( + elementsOverlappingBBox({ + elements, + bounds: frame, + type: "overlap", + }) + // removes elements who are overlapping, but are in a different frame, + // and thus invisible in target frame + .filter((el) => !el.frameId || el.frameId === frame.id) + ); +}; diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index cc84569a6..9c357a21f 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -26,8 +26,8 @@ import { getInitializedImageElements, updateImageCache, } from "../element/image"; -import { elementsOverlappingBBox } from "../../utils/export"; import { + getElementsOverlappingFrame, getFrameLikeElements, getFrameLikeTitle, getRootElements, @@ -168,11 +168,7 @@ const prepareElementsForRender = ({ let nextElements: readonly ExcalidrawElement[]; if (exportingFrame) { - nextElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + nextElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (frameRendering.enabled && frameRendering.name) { nextElements = addFrameLabelsAsTextElements(elements, { exportWithDarkMode, diff --git a/packages/excalidraw/tests/scene/export.test.ts b/packages/excalidraw/tests/scene/export.test.ts index 5287aa8cf..ec9a0e6bf 100644 --- a/packages/excalidraw/tests/scene/export.test.ts +++ b/packages/excalidraw/tests/scene/export.test.ts @@ -406,5 +406,67 @@ describe("exporting frames", () => { (frame.height + getFrameNameHeight("svg")).toString(), ); }); + + it("should not export frame-overlapping elements belonging to different frame", async () => { + const frame1 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frame2 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 200, + y: 0, + }); + + const frame1Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 0, + y: 50, + frameId: frame1.id, + }); + const frame2Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 50, + y: 0, + frameId: frame2.id, + }); + + // low-level exportToSvg api expects elements to be pre-filtered, so let's + // use the filter we use in the editor + const { exportedElements, exportingFrame } = prepareElementsForExport( + [frame1Child, frame1, frame2Child, frame2], + { + selectedElementIds: { [frame1.id]: true }, + }, + true, + ); + + const svg = await exportToSvg({ + elements: exportedElements, + files: null, + exportPadding: 0, + exportingFrame, + }); + + // frame shouldn't be exported + expect(svg.querySelector(`[data-id="${frame1.id}"]`)).toBeNull(); + // frame1 child should be epxorted + expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull(); + // frame2 child should not be exported even if it physically overlaps with + // frame1 + expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame1.width.toString()); + expect(svg.getAttribute("height")).toBe(frame1.height.toString()); + }); }); });