summaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/renderer/staticSvgScene.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/renderer/staticSvgScene.ts')
-rw-r--r--packages/excalidraw/renderer/staticSvgScene.ts744
1 files changed, 744 insertions, 0 deletions
diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts
new file mode 100644
index 0000000..b14faf7
--- /dev/null
+++ b/packages/excalidraw/renderer/staticSvgScene.ts
@@ -0,0 +1,744 @@
+import type { Drawable } from "roughjs/bin/core";
+import type { 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, hashString } from "../element";
+import {
+ createPlaceholderEmbeddableLabel,
+ getEmbedLink,
+} from "../element/embeddable";
+import { LinearElementEditor } from "../element/linearElementEditor";
+import {
+ getBoundTextElement,
+ getContainerElement,
+} from "../element/textElement";
+import {
+ isArrowElement,
+ isIframeLikeElement,
+ isInitializedImageElement,
+ isTextElement,
+} from "../element/typeChecks";
+import type {
+ ExcalidrawElement,
+ ExcalidrawTextElementWithContainer,
+ NonDeletedExcalidrawElement,
+} from "../element/types";
+import { getContainingFrame } from "../frame";
+import { ShapeCache } from "../scene/ShapeCache";
+import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
+import type { AppState, BinaryFiles } from "../types";
+import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
+import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
+import { getVerticalOffset } from "../fonts";
+import { getCornerRadius, isPathALoop } from "../shapes";
+import { getUncroppedWidthAndHeight } from "../element/cropElement";
+import { getLineHeightInPx } from "../element/textMeasurements";
+
+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 { reuseImages = true } = renderConfig;
+
+ let symbolId = `image-${fileData.id}`;
+
+ let uncroppedWidth = element.width;
+ let uncroppedHeight = element.height;
+ if (element.crop) {
+ ({ width: uncroppedWidth, height: uncroppedHeight } =
+ getUncroppedWidthAndHeight(element));
+
+ symbolId = `image-crop-${fileData.id}-${hashString(
+ `${uncroppedWidth}x${uncroppedHeight}`,
+ )}`;
+ }
+
+ if (!reuseImages) {
+ symbolId = `image-${element.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("href", fileData.dataURL);
+ image.setAttribute("preserveAspectRatio", "none");
+
+ if (element.crop || !reuseImages) {
+ image.setAttribute("width", `${uncroppedWidth}`);
+ image.setAttribute("height", `${uncroppedHeight}`);
+ } else {
+ image.setAttribute("width", "100%");
+ image.setAttribute("height", "100%");
+ }
+
+ symbol.appendChild(image);
+
+ (root.querySelector("defs") || 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);
+ }
+
+ let normalizedCropX = 0;
+ let normalizedCropY = 0;
+
+ if (element.crop) {
+ const { width: uncroppedWidth, height: uncroppedHeight } =
+ getUncroppedWidthAndHeight(element);
+ normalizedCropX =
+ element.crop.x / (element.crop.naturalWidth / uncroppedWidth);
+ normalizedCropY =
+ element.crop.y / (element.crop.naturalHeight / uncroppedHeight);
+ }
+
+ const adjustedCenterX = cx + normalizedCropX;
+ const adjustedCenterY = cy + normalizedCropY;
+
+ use.setAttribute("width", `${width + normalizedCropX}`);
+ use.setAttribute("height", `${height + normalizedCropY}`);
+ 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) {
+ use.setAttribute(
+ "transform",
+ `translate(${adjustedCenterX} ${adjustedCenterY}) scale(${
+ element.scale[0]
+ } ${
+ element.scale[1]
+ }) translate(${-adjustedCenterX} ${-adjustedCenterY})`,
+ );
+ }
+
+ const g = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
+
+ if (element.crop) {
+ const mask = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
+ mask.setAttribute("id", `mask-image-crop-${element.id}`);
+ mask.setAttribute("fill", "#fff");
+ const maskRect = svgRoot.ownerDocument!.createElementNS(
+ SVG_NS,
+ "rect",
+ );
+
+ maskRect.setAttribute("x", `${normalizedCropX}`);
+ maskRect.setAttribute("y", `${normalizedCropY}`);
+ maskRect.setAttribute("width", `${width}`);
+ maskRect.setAttribute("height", `${height}`);
+
+ mask.appendChild(maskRect);
+ root.appendChild(mask);
+ g.setAttribute("mask", `url(#${mask.id})`);
+ }
+
+ g.appendChild(use);
+ g.setAttribute(
+ "transform",
+ `translate(${offsetX - normalizedCropX} ${
+ offsetY - normalizedCropY
+ }) rotate(${degree} ${adjustedCenterX} ${adjustedCenterY})`,
+ );
+
+ 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 verticalOffset = getVerticalOffset(
+ element.fontFamily,
+ element.fontSize,
+ lineHeightPx,
+ );
+ 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 + verticalOffset}`);
+ 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", "alphabetic");
+ 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) {
+ if (
+ isTextElement(element) &&
+ element.containerId &&
+ elementsMap.has(element.containerId)
+ ) {
+ // will be rendered with the container
+ return;
+ }
+
+ try {
+ renderElementToSvg(
+ element,
+ elementsMap,
+ rsvg,
+ svgRoot,
+ files,
+ element.x + renderConfig.offsetX,
+ element.y + renderConfig.offsetY,
+ renderConfig,
+ );
+
+ const boundTextElement = getBoundTextElement(element, elementsMap);
+ if (boundTextElement) {
+ renderElementToSvg(
+ boundTextElement,
+ elementsMap,
+ rsvg,
+ svgRoot,
+ files,
+ boundTextElement.x + renderConfig.offsetX,
+ boundTextElement.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);
+ }
+ }
+ });
+};