fix(editor): cardinal direction arrows with label are invisible in exported SVG (#11441)
fix: arrows with bound text labels missing from SVG export Axis-aligned (horizontal/vertical) arrows with a bound label vanished from SVG exports. The label-gap mask defaulted to objectBoundingBox units, whose region collapses to zero area for a zero-size bounding box, masking out the whole line. Pin the mask to userSpaceOnUse with an explicit user-space region (the coords already used by the visible rect). Fixes #11439
This commit is contained in:
@@ -291,6 +291,14 @@ const renderElementToSvg = (
|
|||||||
);
|
);
|
||||||
offsetX = offsetX || 0;
|
offsetX = offsetX || 0;
|
||||||
offsetY = offsetY || 0;
|
offsetY = offsetY || 0;
|
||||||
|
// Pin the mask to user space; the default maskUnits="objectBoundingBox"
|
||||||
|
// collapses to zero area for axis-aligned arrows (zero-size bbox),
|
||||||
|
// hiding the whole line from SVG exports (#11439).
|
||||||
|
maskPath.setAttribute("maskUnits", "userSpaceOnUse");
|
||||||
|
maskPath.setAttribute("x", "0");
|
||||||
|
maskPath.setAttribute("y", "0");
|
||||||
|
maskPath.setAttribute("width", `${element.width + 100 + offsetX}`);
|
||||||
|
maskPath.setAttribute("height", `${element.height + 100 + offsetY}`);
|
||||||
maskRectVisible.setAttribute("x", "0");
|
maskRectVisible.setAttribute("x", "0");
|
||||||
maskRectVisible.setAttribute("y", "0");
|
maskRectVisible.setAttribute("y", "0");
|
||||||
maskRectVisible.setAttribute("fill", "#fff");
|
maskRectVisible.setAttribute("fill", "#fff");
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import {
|
|||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { prepareElementsForExport } from "../../data";
|
import { prepareElementsForExport } from "../../data";
|
||||||
import * as exportUtils from "../../scene/export";
|
import * as exportUtils from "../../scene/export";
|
||||||
import {
|
import {
|
||||||
@@ -192,6 +196,45 @@ describe("exportToSvg", () => {
|
|||||||
);
|
);
|
||||||
expect(svgElement.innerHTML).toMatchSnapshot();
|
expect(svgElement.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #11439: a perfectly horizontal/vertical arrow has a zero-size bounding box.
|
||||||
|
// The bound-text "gap" mask must use userSpaceOnUse units, otherwise its
|
||||||
|
// objectBoundingBox region collapses to zero area and the whole arrow line
|
||||||
|
// disappears from the SVG export (only the label remains).
|
||||||
|
it("keeps a horizontal arrow with a bound label visible (#11439)", async () => {
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow-11439",
|
||||||
|
width: 200,
|
||||||
|
height: 0,
|
||||||
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(200, 0)],
|
||||||
|
boundElements: [{ type: "text", id: "label-11439" }],
|
||||||
|
});
|
||||||
|
const label = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "label-11439",
|
||||||
|
text: "label",
|
||||||
|
width: 50,
|
||||||
|
height: 20,
|
||||||
|
containerId: "arrow-11439",
|
||||||
|
});
|
||||||
|
|
||||||
|
const svgElement = await exportUtils.exportToSvg(
|
||||||
|
[arrow, label] as NonDeletedExcalidrawElement[],
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mask = svgElement.querySelector("mask");
|
||||||
|
expect(mask).not.toBeNull();
|
||||||
|
expect(mask?.getAttribute("maskUnits")).toBe("userSpaceOnUse");
|
||||||
|
// a degenerate (objectBoundingBox) region would be zero-area here
|
||||||
|
expect(Number(mask?.getAttribute("width"))).toBeGreaterThan(0);
|
||||||
|
expect(Number(mask?.getAttribute("height"))).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// the masked arrow group still renders its line (not clipped away)
|
||||||
|
expect(svgElement.querySelector("g[mask] path")).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("exporting frames", () => {
|
describe("exporting frames", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user