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

This commit is contained in:
David Luzar 2024-01-19 14:41:22 +01:00 committed by GitHub
parent 3b0593baa7
commit 46da032626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 92 additions and 19 deletions

View File

@ -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<AppProps, AppState> {
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") {

View File

@ -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,

View File

@ -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)
);
};

View File

@ -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,

View File

@ -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());
});
});
});