123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745 |
- 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,
- getLineHeightInPx,
- } 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";
- 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) {
- // TODO set to `false` before merging
- 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.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);
- }
- }
- });
- };
|