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;
|
||||
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("y", "0");
|
||||
maskRectVisible.setAttribute("fill", "#fff");
|
||||
|
||||
@@ -6,12 +6,16 @@ import {
|
||||
FRAME_STYLE,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawTextElement,
|
||||
FractionalIndex,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { prepareElementsForExport } from "../../data";
|
||||
import * as exportUtils from "../../scene/export";
|
||||
import {
|
||||
@@ -192,6 +196,45 @@ describe("exportToSvg", () => {
|
||||
);
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user