mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
fix: exporting frame-overlapping elements belonging to other frames (#7584)
This commit is contained in:
parent
3b0593baa7
commit
46da032626
5 changed files with 92 additions and 19 deletions
|
@ -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({
|
const magicFrameChildren = getElementsOverlappingFrame(
|
||||||
elements: this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
bounds: magicFrame,
|
magicFrame,
|
||||||
type: "overlap",
|
).filter((el) => !isMagicFrameElement(el));
|
||||||
}).filter((el) => !isMagicFrameElement(el));
|
|
||||||
|
|
||||||
if (!magicFrameChildren.length) {
|
if (!magicFrameChildren.length) {
|
||||||
if (source === "button") {
|
if (source === "button") {
|
||||||
|
|
|
@ -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({
|
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||||
elements,
|
|
||||||
bounds: exportingFrame,
|
|
||||||
type: "overlap",
|
|
||||||
});
|
|
||||||
} else if (exportedElements.length > 1) {
|
} else if (exportedElements.length > 1) {
|
||||||
exportedElements = getSelectedElements(
|
exportedElements = getSelectedElements(
|
||||||
elements,
|
elements,
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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({
|
nextElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||||
elements,
|
|
||||||
bounds: exportingFrame,
|
|
||||||
type: "overlap",
|
|
||||||
});
|
|
||||||
} else if (frameRendering.enabled && frameRendering.name) {
|
} else if (frameRendering.enabled && frameRendering.name) {
|
||||||
nextElements = addFrameLabelsAsTextElements(elements, {
|
nextElements = addFrameLabelsAsTextElements(elements, {
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
|
|
|
@ -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());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue