Browse Source

fix: exporting frame-overlapping elements belonging to other frames (#7584)

David Luzar 1 year ago
parent
commit
46da032626

+ 6 - 6
packages/excalidraw/components/App.tsx

@@ -348,6 +348,7 @@ import {
   updateFrameMembershipOfSelectedElements,
   updateFrameMembershipOfSelectedElements,
   isElementInFrame,
   isElementInFrame,
   getFrameLikeTitle,
   getFrameLikeTitle,
+  getElementsOverlappingFrame,
 } from "../frame";
 } from "../frame";
 import {
 import {
   excludeElementsInFramesFromSelection,
   excludeElementsInFramesFromSelection,
@@ -395,7 +396,7 @@ import {
 import { Emitter } from "../emitter";
 import { Emitter } from "../emitter";
 import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
 import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
 import { MagicCacheData, diagramToHTML } from "../data/magic";
 import { MagicCacheData, diagramToHTML } from "../data/magic";
-import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
+import { exportToBlob } from "../../utils/export";
 import { COLOR_PALETTE } from "../colors";
 import { COLOR_PALETTE } from "../colors";
 import { ElementCanvasButton } from "./MagicButton";
 import { ElementCanvasButton } from "./MagicButton";
 import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
 import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
@@ -1803,11 +1804,10 @@ class App extends React.Component<AppProps, AppState> {
       return;
       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 (!magicFrameChildren.length) {
       if (source === "button") {
       if (source === "button") {

+ 2 - 6
packages/excalidraw/data/index.ts

@@ -11,7 +11,6 @@ import {
   NonDeletedExcalidrawElement,
   NonDeletedExcalidrawElement,
 } from "../element/types";
 } from "../element/types";
 import { t } from "../i18n";
 import { t } from "../i18n";
-import { elementsOverlappingBBox } from "../../utils/export";
 import { isSomeElementSelected, getSelectedElements } from "../scene";
 import { isSomeElementSelected, getSelectedElements } from "../scene";
 import { exportToCanvas, exportToSvg } from "../scene/export";
 import { exportToCanvas, exportToSvg } from "../scene/export";
 import { ExportType } from "../scene/types";
 import { ExportType } from "../scene/types";
@@ -20,6 +19,7 @@ import { cloneJSON } from "../utils";
 import { canvasToBlob } from "./blob";
 import { canvasToBlob } from "./blob";
 import { fileSave, FileSystemHandle } from "./filesystem";
 import { fileSave, FileSystemHandle } from "./filesystem";
 import { serializeAsJSON } from "./json";
 import { serializeAsJSON } from "./json";
+import { getElementsOverlappingFrame } from "../frame";
 
 
 export { loadFromBlob } from "./blob";
 export { loadFromBlob } from "./blob";
 export { loadFromJSON, saveAsJSON } from "./json";
 export { loadFromJSON, saveAsJSON } from "./json";
@@ -56,11 +56,7 @@ export const prepareElementsForExport = (
       isFrameLikeElement(exportedElements[0])
       isFrameLikeElement(exportedElements[0])
     ) {
     ) {
       exportingFrame = exportedElements[0];
       exportingFrame = exportedElements[0];
-      exportedElements = elementsOverlappingBBox({
-        elements,
-        bounds: exportingFrame,
-        type: "overlap",
-      });
+      exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
     } else if (exportedElements.length > 1) {
     } else if (exportedElements.length > 1) {
       exportedElements = getSelectedElements(
       exportedElements = getSelectedElements(
         elements,
         elements,

+ 20 - 1
packages/excalidraw/frame.ts

@@ -21,7 +21,10 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene";
 import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
 import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
 import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
 import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
 import { getElementLineSegments } from "./element/bounds";
 import { getElementLineSegments } from "./element/bounds";
-import { doLineSegmentsIntersect } from "../utils/export";
+import {
+  doLineSegmentsIntersect,
+  elementsOverlappingBBox,
+} from "../utils/export";
 import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
 import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
 
 
 // --------------------------- Frame State ------------------------------------
 // --------------------------- Frame State ------------------------------------
@@ -664,3 +667,19 @@ export const getFrameLikeTitle = (
   // TODO name frames AI only is specific to AI frames
   // TODO name frames AI only is specific to AI frames
   return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`;
   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)
+  );
+};

+ 2 - 6
packages/excalidraw/scene/export.ts

@@ -26,8 +26,8 @@ import {
   getInitializedImageElements,
   getInitializedImageElements,
   updateImageCache,
   updateImageCache,
 } from "../element/image";
 } from "../element/image";
-import { elementsOverlappingBBox } from "../../utils/export";
 import {
 import {
+  getElementsOverlappingFrame,
   getFrameLikeElements,
   getFrameLikeElements,
   getFrameLikeTitle,
   getFrameLikeTitle,
   getRootElements,
   getRootElements,
@@ -168,11 +168,7 @@ const prepareElementsForRender = ({
   let nextElements: readonly ExcalidrawElement[];
   let nextElements: readonly ExcalidrawElement[];
 
 
   if (exportingFrame) {
   if (exportingFrame) {
-    nextElements = elementsOverlappingBBox({
-      elements,
-      bounds: exportingFrame,
-      type: "overlap",
-    });
+    nextElements = getElementsOverlappingFrame(elements, exportingFrame);
   } else if (frameRendering.enabled && frameRendering.name) {
   } else if (frameRendering.enabled && frameRendering.name) {
     nextElements = addFrameLabelsAsTextElements(elements, {
     nextElements = addFrameLabelsAsTextElements(elements, {
       exportWithDarkMode,
       exportWithDarkMode,

+ 62 - 0
packages/excalidraw/tests/scene/export.test.ts

@@ -406,5 +406,67 @@ describe("exporting frames", () => {
         (frame.height + getFrameNameHeight("svg")).toString(),
         (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());
+    });
   });
   });
 });
 });