mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-20 12:44:38 +01:00
fix: split renderScene so that locales aren't imported unnecessarily (#7718)
* fix: split renderScene so that locales aren't imported unnecessarily * lint * split export code * rename renderScene to helpers.ts * add helpers * fix typo * fixes * move renderElementToSvg to export * lint * rename export to staticSvgScene * fix
This commit is contained in:
653
packages/excalidraw/renderer/staticSvgScene.ts
Normal file
653
packages/excalidraw/renderer/staticSvgScene.ts
Normal file
@@ -0,0 +1,653 @@
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { RoughSVG } from "roughjs/bin/svg";
|
||||
import {
|
||||
FRAME_STYLE,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
MIME_TYPES,
|
||||
SVG_NS,
|
||||
} from "../constants";
|
||||
import { normalizeLink, toValidURL } from "../data/url";
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
import {
|
||||
createPlaceholderEmbeddableLabel,
|
||||
getEmbedLink,
|
||||
} from "../element/embeddable";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
getLineHeightInPx,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
isArrowElement,
|
||||
isIframeLikeElement,
|
||||
isInitializedImageElement,
|
||||
isTextElement,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getContainingFrame } from "../frame";
|
||||
import { getCornerRadius, isPathALoop } from "../math";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
||||
import { AppState, BinaryFiles } from "../types";
|
||||
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||
|
||||
const roughSVGDrawWithPrecision = (
|
||||
rsvg: RoughSVG,
|
||||
drawable: Drawable,
|
||||
precision?: number,
|
||||
) => {
|
||||
if (typeof precision === "undefined") {
|
||||
return rsvg.draw(drawable);
|
||||
}
|
||||
const pshape: Drawable = {
|
||||
sets: drawable.sets,
|
||||
shape: drawable.shape,
|
||||
options: { ...drawable.options, fixedDecimalPlaceDigits: precision },
|
||||
};
|
||||
return rsvg.draw(pshape);
|
||||
};
|
||||
|
||||
const maybeWrapNodesInFrameClipPath = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
root: SVGElement,
|
||||
nodes: SVGElement[],
|
||||
frameRendering: AppState["frameRendering"],
|
||||
elementsMap: RenderableElementsMap,
|
||||
) => {
|
||||
if (!frameRendering.enabled || !frameRendering.clip) {
|
||||
return null;
|
||||
}
|
||||
const frame = getContainingFrame(element, elementsMap);
|
||||
if (frame) {
|
||||
const g = root.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`);
|
||||
nodes.forEach((node) => g.appendChild(node));
|
||||
return g;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderElementToSvg = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
files: BinaryFiles,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
renderConfig: SVGRenderConfig,
|
||||
) => {
|
||||
const offset = { x: offsetX, y: offsetY };
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap);
|
||||
|
||||
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
element as ExcalidrawTextElementWithContainer,
|
||||
elementsMap,
|
||||
);
|
||||
cx = (x2 - x1) / 2 - (boundTextCoords.x - x1);
|
||||
cy = (y2 - y1) / 2 - (boundTextCoords.y - y1);
|
||||
offsetX = offsetX + boundTextCoords.x - element.x;
|
||||
offsetY = offsetY + boundTextCoords.y - element.y;
|
||||
}
|
||||
}
|
||||
const degree = (180 * element.angle) / Math.PI;
|
||||
|
||||
// element to append node to, most of the time svgRoot
|
||||
let root = svgRoot;
|
||||
|
||||
// if the element has a link, create an anchor tag and make that the new root
|
||||
if (element.link) {
|
||||
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
|
||||
anchorTag.setAttribute("href", normalizeLink(element.link));
|
||||
root.appendChild(anchorTag);
|
||||
root = anchorTag;
|
||||
}
|
||||
|
||||
const addToRoot = (node: SVGElement, element: ExcalidrawElement) => {
|
||||
if (isTestEnv()) {
|
||||
node.setAttribute("data-id", element.id);
|
||||
}
|
||||
root.appendChild(node);
|
||||
};
|
||||
|
||||
const opacity =
|
||||
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
||||
element.opacity) /
|
||||
10000;
|
||||
|
||||
switch (element.type) {
|
||||
case "selection": {
|
||||
// Since this is used only during editing experience, which is canvas based,
|
||||
// this should not happen
|
||||
throw new Error("Selection rendering is not supported for SVG");
|
||||
}
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "ellipse": {
|
||||
const shape = ShapeCache.generateElementShape(element, null);
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
shape,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
node.setAttribute("fill-opacity", `${opacity}`);
|
||||
}
|
||||
node.setAttribute("stroke-linecap", "round");
|
||||
node.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
|
||||
const g = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
[node],
|
||||
renderConfig.frameRendering,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
addToRoot(g || node, element);
|
||||
break;
|
||||
}
|
||||
case "iframe":
|
||||
case "embeddable": {
|
||||
// render placeholder rectangle
|
||||
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
shape,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
const opacity = element.opacity / 100;
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
node.setAttribute("fill-opacity", `${opacity}`);
|
||||
}
|
||||
node.setAttribute("stroke-linecap", "round");
|
||||
node.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
addToRoot(node, element);
|
||||
|
||||
const label: ExcalidrawElement =
|
||||
createPlaceholderEmbeddableLabel(element);
|
||||
renderElementToSvg(
|
||||
label,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
root,
|
||||
files,
|
||||
label.x + offset.x - element.x,
|
||||
label.y + offset.y - element.y,
|
||||
renderConfig,
|
||||
);
|
||||
|
||||
// render embeddable element + iframe
|
||||
const embeddableNode = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
shape,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
embeddableNode.setAttribute("stroke-linecap", "round");
|
||||
embeddableNode.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
while (embeddableNode.firstChild) {
|
||||
embeddableNode.removeChild(embeddableNode.firstChild);
|
||||
}
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
|
||||
const embedLink = getEmbedLink(toValidURL(element.link || ""));
|
||||
|
||||
// if rendering embeddables explicitly disabled or
|
||||
// embedding documents via srcdoc (which doesn't seem to work for SVGs)
|
||||
// replace with a link instead
|
||||
if (
|
||||
renderConfig.renderEmbeddables === false ||
|
||||
embedLink?.type === "document"
|
||||
) {
|
||||
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
|
||||
anchorTag.setAttribute("href", normalizeLink(element.link || ""));
|
||||
anchorTag.setAttribute("target", "_blank");
|
||||
anchorTag.setAttribute("rel", "noopener noreferrer");
|
||||
anchorTag.style.borderRadius = `${radius}px`;
|
||||
|
||||
embeddableNode.appendChild(anchorTag);
|
||||
} else {
|
||||
const foreignObject = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"foreignObject",
|
||||
);
|
||||
foreignObject.style.width = `${element.width}px`;
|
||||
foreignObject.style.height = `${element.height}px`;
|
||||
foreignObject.style.border = "none";
|
||||
const div = foreignObject.ownerDocument!.createElementNS(SVG_NS, "div");
|
||||
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
||||
div.style.width = "100%";
|
||||
div.style.height = "100%";
|
||||
const iframe = div.ownerDocument!.createElement("iframe");
|
||||
iframe.src = embedLink?.link ?? "";
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.border = "none";
|
||||
iframe.style.borderRadius = `${radius}px`;
|
||||
iframe.style.top = "0";
|
||||
iframe.style.left = "0";
|
||||
iframe.allowFullscreen = true;
|
||||
div.appendChild(iframe);
|
||||
foreignObject.appendChild(div);
|
||||
|
||||
embeddableNode.appendChild(foreignObject);
|
||||
}
|
||||
addToRoot(embeddableNode, element);
|
||||
break;
|
||||
}
|
||||
case "line":
|
||||
case "arrow": {
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
|
||||
if (boundText) {
|
||||
maskPath.setAttribute("id", `mask-${element.id}`);
|
||||
const maskRectVisible = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"rect",
|
||||
);
|
||||
offsetX = offsetX || 0;
|
||||
offsetY = offsetY || 0;
|
||||
maskRectVisible.setAttribute("x", "0");
|
||||
maskRectVisible.setAttribute("y", "0");
|
||||
maskRectVisible.setAttribute("fill", "#fff");
|
||||
maskRectVisible.setAttribute(
|
||||
"width",
|
||||
`${element.width + 100 + offsetX}`,
|
||||
);
|
||||
maskRectVisible.setAttribute(
|
||||
"height",
|
||||
`${element.height + 100 + offsetY}`,
|
||||
);
|
||||
|
||||
maskPath.appendChild(maskRectVisible);
|
||||
const maskRectInvisible = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"rect",
|
||||
);
|
||||
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundText,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const maskX = offsetX + boundTextCoords.x - element.x;
|
||||
const maskY = offsetY + boundTextCoords.y - element.y;
|
||||
|
||||
maskRectInvisible.setAttribute("x", maskX.toString());
|
||||
maskRectInvisible.setAttribute("y", maskY.toString());
|
||||
maskRectInvisible.setAttribute("fill", "#000");
|
||||
maskRectInvisible.setAttribute("width", `${boundText.width}`);
|
||||
maskRectInvisible.setAttribute("height", `${boundText.height}`);
|
||||
maskRectInvisible.setAttribute("opacity", "1");
|
||||
maskPath.appendChild(maskRectInvisible);
|
||||
}
|
||||
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
if (boundText) {
|
||||
group.setAttribute("mask", `url(#mask-${element.id})`);
|
||||
}
|
||||
group.setAttribute("stroke-linecap", "round");
|
||||
|
||||
const shapes = ShapeCache.generateElementShape(element, renderConfig);
|
||||
shapes.forEach((shape) => {
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
shape,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
node.setAttribute("fill-opacity", `${opacity}`);
|
||||
}
|
||||
node.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
if (
|
||||
element.type === "line" &&
|
||||
isPathALoop(element.points) &&
|
||||
element.backgroundColor !== "transparent"
|
||||
) {
|
||||
node.setAttribute("fill-rule", "evenodd");
|
||||
}
|
||||
group.appendChild(node);
|
||||
});
|
||||
|
||||
const g = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
[group, maskPath],
|
||||
renderConfig.frameRendering,
|
||||
elementsMap,
|
||||
);
|
||||
if (g) {
|
||||
addToRoot(g, element);
|
||||
root.appendChild(g);
|
||||
} else {
|
||||
addToRoot(group, element);
|
||||
root.append(maskPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "freedraw": {
|
||||
const backgroundFillShape = ShapeCache.generateElementShape(
|
||||
element,
|
||||
renderConfig,
|
||||
);
|
||||
const node = backgroundFillShape
|
||||
? roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
backgroundFillShape,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
)
|
||||
: svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
node.setAttribute("fill-opacity", `${opacity}`);
|
||||
}
|
||||
node.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
node.setAttribute("stroke", "none");
|
||||
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("fill", element.strokeColor);
|
||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||
node.appendChild(path);
|
||||
|
||||
const g = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
[node],
|
||||
renderConfig.frameRendering,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
addToRoot(g || node, element);
|
||||
break;
|
||||
}
|
||||
case "image": {
|
||||
const width = Math.round(element.width);
|
||||
const height = Math.round(element.height);
|
||||
const fileData =
|
||||
isInitializedImageElement(element) && files[element.fileId];
|
||||
if (fileData) {
|
||||
const symbolId = `image-${fileData.id}`;
|
||||
let symbol = svgRoot.querySelector(`#${symbolId}`);
|
||||
if (!symbol) {
|
||||
symbol = svgRoot.ownerDocument!.createElementNS(SVG_NS, "symbol");
|
||||
symbol.id = symbolId;
|
||||
|
||||
const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image");
|
||||
|
||||
image.setAttribute("width", "100%");
|
||||
image.setAttribute("height", "100%");
|
||||
image.setAttribute("href", fileData.dataURL);
|
||||
|
||||
symbol.appendChild(image);
|
||||
|
||||
root.prepend(symbol);
|
||||
}
|
||||
|
||||
const use = svgRoot.ownerDocument!.createElementNS(SVG_NS, "use");
|
||||
use.setAttribute("href", `#${symbolId}`);
|
||||
|
||||
// in dark theme, revert the image color filter
|
||||
if (
|
||||
renderConfig.exportWithDarkMode &&
|
||||
fileData.mimeType !== MIME_TYPES.svg
|
||||
) {
|
||||
use.setAttribute("filter", IMAGE_INVERT_FILTER);
|
||||
}
|
||||
|
||||
use.setAttribute("width", `${width}`);
|
||||
use.setAttribute("height", `${height}`);
|
||||
use.setAttribute("opacity", `${opacity}`);
|
||||
|
||||
// We first apply `scale` transforms (horizontal/vertical mirroring)
|
||||
// on the <use> element, then apply translation and rotation
|
||||
// on the <g> element which wraps the <use>.
|
||||
// Doing this separately is a quick hack to to work around compositing
|
||||
// the transformations correctly (the transform-origin was not being
|
||||
// applied correctly).
|
||||
if (element.scale[0] !== 1 || element.scale[1] !== 1) {
|
||||
const translateX = element.scale[0] !== 1 ? -width : 0;
|
||||
const translateY = element.scale[1] !== 1 ? -height : 0;
|
||||
use.setAttribute(
|
||||
"transform",
|
||||
`scale(${element.scale[0]}, ${element.scale[1]}) translate(${translateX} ${translateY})`,
|
||||
);
|
||||
}
|
||||
|
||||
const g = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
g.appendChild(use);
|
||||
g.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
|
||||
if (element.roundness) {
|
||||
const clipPath = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"clipPath",
|
||||
);
|
||||
clipPath.id = `image-clipPath-${element.id}`;
|
||||
|
||||
const clipRect = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"rect",
|
||||
);
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
clipRect.setAttribute("width", `${element.width}`);
|
||||
clipRect.setAttribute("height", `${element.height}`);
|
||||
clipRect.setAttribute("rx", `${radius}`);
|
||||
clipRect.setAttribute("ry", `${radius}`);
|
||||
clipPath.appendChild(clipRect);
|
||||
addToRoot(clipPath, element);
|
||||
|
||||
g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`);
|
||||
}
|
||||
|
||||
const clipG = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
[g],
|
||||
renderConfig.frameRendering,
|
||||
elementsMap,
|
||||
);
|
||||
addToRoot(clipG || g, element);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// frames are not rendered and only acts as a container
|
||||
case "frame":
|
||||
case "magicframe": {
|
||||
if (
|
||||
renderConfig.frameRendering.enabled &&
|
||||
renderConfig.frameRendering.outline
|
||||
) {
|
||||
const rect = document.createElementNS(SVG_NS, "rect");
|
||||
|
||||
rect.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
|
||||
rect.setAttribute("width", `${element.width}px`);
|
||||
rect.setAttribute("height", `${element.height}px`);
|
||||
// Rounded corners
|
||||
rect.setAttribute("rx", FRAME_STYLE.radius.toString());
|
||||
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
||||
|
||||
rect.setAttribute("fill", "none");
|
||||
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
|
||||
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
||||
|
||||
addToRoot(rect, element);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (isTextElement(element)) {
|
||||
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||
if (opacity !== 1) {
|
||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||
node.setAttribute("fill-opacity", `${opacity}`);
|
||||
}
|
||||
|
||||
node.setAttribute(
|
||||
"transform",
|
||||
`translate(${offsetX || 0} ${
|
||||
offsetY || 0
|
||||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||
const lineHeightPx = getLineHeightInPx(
|
||||
element.fontSize,
|
||||
element.lineHeight,
|
||||
);
|
||||
const horizontalOffset =
|
||||
element.textAlign === "center"
|
||||
? element.width / 2
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
const direction = isRTL(element.text) ? "rtl" : "ltr";
|
||||
const textAnchor =
|
||||
element.textAlign === "center"
|
||||
? "middle"
|
||||
: element.textAlign === "right" || direction === "rtl"
|
||||
? "end"
|
||||
: "start";
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
||||
text.textContent = lines[i];
|
||||
text.setAttribute("x", `${horizontalOffset}`);
|
||||
text.setAttribute("y", `${i * lineHeightPx}`);
|
||||
text.setAttribute("font-family", getFontFamilyString(element));
|
||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||
text.setAttribute("fill", element.strokeColor);
|
||||
text.setAttribute("text-anchor", textAnchor);
|
||||
text.setAttribute("style", "white-space: pre;");
|
||||
text.setAttribute("direction", direction);
|
||||
text.setAttribute("dominant-baseline", "text-before-edge");
|
||||
node.appendChild(text);
|
||||
}
|
||||
|
||||
const g = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
[node],
|
||||
renderConfig.frameRendering,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
addToRoot(g || node, element);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
throw new Error(`Unimplemented type ${element.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const renderSceneToSvg = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: RenderableElementsMap,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
files: BinaryFiles,
|
||||
renderConfig: SVGRenderConfig,
|
||||
) => {
|
||||
if (!svgRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// render elements
|
||||
elements
|
||||
.filter((el) => !isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
if (!element.isDeleted) {
|
||||
try {
|
||||
renderElementToSvg(
|
||||
element,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files,
|
||||
element.x + renderConfig.offsetX,
|
||||
element.y + renderConfig.offsetY,
|
||||
renderConfig,
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// render embeddables on top
|
||||
elements
|
||||
.filter((el) => isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
if (!element.isDeleted) {
|
||||
try {
|
||||
renderElementToSvg(
|
||||
element,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files,
|
||||
element.x + renderConfig.offsetX,
|
||||
element.y + renderConfig.offsetY,
|
||||
renderConfig,
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user