Browse Source

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
Aakansha Doshi 1 năm trước cách đây
mục cha
commit
b09b5cb5f4

+ 1 - 1
packages/excalidraw/components/canvases/InteractiveCanvas.tsx

@@ -1,5 +1,4 @@
 import React, { useEffect, useRef } from "react";
 import React, { useEffect, useRef } from "react";
-import { renderInteractiveScene } from "../../renderer/renderScene";
 import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
 import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
 import { CURSOR_TYPE } from "../../constants";
 import { CURSOR_TYPE } from "../../constants";
 import { t } from "../../i18n";
 import { t } from "../../i18n";
@@ -12,6 +11,7 @@ import type {
 } from "../../scene/types";
 } from "../../scene/types";
 import type { NonDeletedExcalidrawElement } from "../../element/types";
 import type { NonDeletedExcalidrawElement } from "../../element/types";
 import { isRenderThrottlingEnabled } from "../../reactUtils";
 import { isRenderThrottlingEnabled } from "../../reactUtils";
+import { renderInteractiveScene } from "../../renderer/interactiveScene";
 
 
 type InteractiveCanvasProps = {
 type InteractiveCanvasProps = {
   containerRef: React.RefObject<HTMLDivElement>;
   containerRef: React.RefObject<HTMLDivElement>;

+ 1 - 1
packages/excalidraw/components/canvases/StaticCanvas.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect, useRef } from "react";
 import React, { useEffect, useRef } from "react";
 import { RoughCanvas } from "roughjs/bin/canvas";
 import { RoughCanvas } from "roughjs/bin/canvas";
-import { renderStaticScene } from "../../renderer/renderScene";
+import { renderStaticScene } from "../../renderer/staticScene";
 import { isShallowEqual } from "../../utils";
 import { isShallowEqual } from "../../utils";
 import type { AppState, StaticCanvasAppState } from "../../types";
 import type { AppState, StaticCanvasAppState } from "../../types";
 import type {
 import type {

+ 75 - 0
packages/excalidraw/renderer/helpers.ts

@@ -0,0 +1,75 @@
+import { StaticCanvasAppState, AppState } from "../types";
+
+import { StaticCanvasRenderConfig } from "../scene/types";
+
+import { THEME_FILTER } from "../constants";
+
+export const fillCircle = (
+  context: CanvasRenderingContext2D,
+  cx: number,
+  cy: number,
+  radius: number,
+  stroke = true,
+) => {
+  context.beginPath();
+  context.arc(cx, cy, radius, 0, Math.PI * 2);
+  context.fill();
+  if (stroke) {
+    context.stroke();
+  }
+};
+
+export const getNormalizedCanvasDimensions = (
+  canvas: HTMLCanvasElement,
+  scale: number,
+): [number, number] => {
+  // When doing calculations based on canvas width we should used normalized one
+  return [canvas.width / scale, canvas.height / scale];
+};
+
+export const bootstrapCanvas = ({
+  canvas,
+  scale,
+  normalizedWidth,
+  normalizedHeight,
+  theme,
+  isExporting,
+  viewBackgroundColor,
+}: {
+  canvas: HTMLCanvasElement;
+  scale: number;
+  normalizedWidth: number;
+  normalizedHeight: number;
+  theme?: AppState["theme"];
+  isExporting?: StaticCanvasRenderConfig["isExporting"];
+  viewBackgroundColor?: StaticCanvasAppState["viewBackgroundColor"];
+}): CanvasRenderingContext2D => {
+  const context = canvas.getContext("2d")!;
+
+  context.setTransform(1, 0, 0, 1, 0, 0);
+  context.scale(scale, scale);
+
+  if (isExporting && theme === "dark") {
+    context.filter = THEME_FILTER;
+  }
+
+  // Paint background
+  if (typeof viewBackgroundColor === "string") {
+    const hasTransparence =
+      viewBackgroundColor === "transparent" ||
+      viewBackgroundColor.length === 5 || // #RGBA
+      viewBackgroundColor.length === 9 || // #RRGGBBA
+      /(hsla|rgba)\(/.test(viewBackgroundColor);
+    if (hasTransparence) {
+      context.clearRect(0, 0, normalizedWidth, normalizedHeight);
+    }
+    context.save();
+    context.fillStyle = viewBackgroundColor;
+    context.fillRect(0, 0, normalizedWidth, normalizedHeight);
+    context.restore();
+  } else {
+    context.clearRect(0, 0, normalizedWidth, normalizedHeight);
+  }
+
+  return context;
+};

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 366 - 1001
packages/excalidraw/renderer/interactiveScene.ts


+ 5 - 578
packages/excalidraw/renderer/renderElement.ts

@@ -20,27 +20,17 @@ import {
 } from "../element/typeChecks";
 } from "../element/typeChecks";
 import { getElementAbsoluteCoords } from "../element/bounds";
 import { getElementAbsoluteCoords } from "../element/bounds";
 import type { RoughCanvas } from "roughjs/bin/canvas";
 import type { RoughCanvas } from "roughjs/bin/canvas";
-import type { Drawable } from "roughjs/bin/core";
-import type { RoughSVG } from "roughjs/bin/svg";
 
 
 import {
 import {
-  SVGRenderConfig,
   StaticCanvasRenderConfig,
   StaticCanvasRenderConfig,
   RenderableElementsMap,
   RenderableElementsMap,
 } from "../scene/types";
 } from "../scene/types";
-import {
-  distance,
-  getFontString,
-  getFontFamilyString,
-  isRTL,
-  isTestEnv,
-} from "../utils";
-import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
+import { distance, getFontString, isRTL } from "../utils";
+import { getCornerRadius, isRightAngle } from "../math";
 import rough from "roughjs/bin/rough";
 import rough from "roughjs/bin/rough";
 import {
 import {
   AppState,
   AppState,
   StaticCanvasAppState,
   StaticCanvasAppState,
-  BinaryFiles,
   Zoom,
   Zoom,
   InteractiveCanvasAppState,
   InteractiveCanvasAppState,
   ElementsPendingErasure,
   ElementsPendingErasure,
@@ -50,9 +40,7 @@ import {
   BOUND_TEXT_PADDING,
   BOUND_TEXT_PADDING,
   ELEMENT_READY_TO_ERASE_OPACITY,
   ELEMENT_READY_TO_ERASE_OPACITY,
   FRAME_STYLE,
   FRAME_STYLE,
-  MAX_DECIMALS_FOR_SVG_EXPORT,
   MIME_TYPES,
   MIME_TYPES,
-  SVG_NS,
 } from "../constants";
 } from "../constants";
 import { getStroke, StrokeOptions } from "perfect-freehand";
 import { getStroke, StrokeOptions } from "perfect-freehand";
 import {
 import {
@@ -64,19 +52,16 @@ import {
   getBoundTextMaxWidth,
   getBoundTextMaxWidth,
 } from "../element/textElement";
 } from "../element/textElement";
 import { LinearElementEditor } from "../element/linearElementEditor";
 import { LinearElementEditor } from "../element/linearElementEditor";
-import {
-  createPlaceholderEmbeddableLabel,
-  getEmbedLink,
-} from "../element/embeddable";
+
 import { getContainingFrame } from "../frame";
 import { getContainingFrame } from "../frame";
-import { normalizeLink, toValidURL } from "../data/url";
 import { ShapeCache } from "../scene/ShapeCache";
 import { ShapeCache } from "../scene/ShapeCache";
 
 
 // using a stronger invert (100% vs our regular 93%) and saturate
 // using a stronger invert (100% vs our regular 93%) and saturate
 // as a temp hack to make images in dark theme look closer to original
 // as a temp hack to make images in dark theme look closer to original
 // color scheme (it's still not quite there and the colors look slightly
 // color scheme (it's still not quite there and the colors look slightly
 // desatured, alas...)
 // desatured, alas...)
-const IMAGE_INVERT_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
+export const IMAGE_INVERT_FILTER =
+  "invert(100%) hue-rotate(180deg) saturate(1.25)";
 
 
 const defaultAppState = getDefaultAppState();
 const defaultAppState = getDefaultAppState();
 
 
@@ -905,564 +890,6 @@ export const renderElement = (
   context.globalAlpha = 1;
   context.globalAlpha = 1;
 };
 };
 
 
-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;
-};
-
-export 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 pathsCache = new WeakMap<ExcalidrawFreeDrawElement, Path2D>([]);
 export const pathsCache = new WeakMap<ExcalidrawFreeDrawElement, Path2D>([]);
 
 
 export function generateFreeDrawShape(element: ExcalidrawFreeDrawElement) {
 export function generateFreeDrawShape(element: ExcalidrawFreeDrawElement) {

+ 370 - 0
packages/excalidraw/renderer/staticScene.ts

@@ -0,0 +1,370 @@
+import { FRAME_STYLE } from "../constants";
+import { getElementAbsoluteCoords } from "../element";
+
+import {
+  elementOverlapsWithFrame,
+  getTargetFrame,
+  isElementInFrame,
+} from "../frame";
+import {
+  isEmbeddableElement,
+  isIframeLikeElement,
+} from "../element/typeChecks";
+import { renderElement } from "../renderer/renderElement";
+import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
+import { StaticCanvasAppState, Zoom } from "../types";
+import {
+  ElementsMap,
+  ExcalidrawFrameLikeElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
+import {
+  StaticCanvasRenderConfig,
+  StaticSceneRenderConfig,
+} from "../scene/types";
+import {
+  EXTERNAL_LINK_IMG,
+  getLinkHandleFromCoords,
+} from "../components/hyperlink/helpers";
+import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
+import { throttleRAF } from "../utils";
+
+const strokeGrid = (
+  context: CanvasRenderingContext2D,
+  gridSize: number,
+  scrollX: number,
+  scrollY: number,
+  zoom: Zoom,
+  width: number,
+  height: number,
+) => {
+  const BOLD_LINE_FREQUENCY = 5;
+
+  enum GridLineColor {
+    Bold = "#cccccc",
+    Regular = "#e5e5e5",
+  }
+
+  const offsetX =
+    -Math.round(zoom.value / gridSize) * gridSize + (scrollX % gridSize);
+  const offsetY =
+    -Math.round(zoom.value / gridSize) * gridSize + (scrollY % gridSize);
+
+  const lineWidth = Math.min(1 / zoom.value, 1);
+
+  const spaceWidth = 1 / zoom.value;
+  const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)];
+
+  context.save();
+  context.lineWidth = lineWidth;
+
+  for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
+    const isBold =
+      Math.round(x - scrollX) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
+    context.beginPath();
+    context.setLineDash(isBold ? [] : lineDash);
+    context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
+    context.moveTo(x, offsetY - gridSize);
+    context.lineTo(x, offsetY + height + gridSize * 2);
+    context.stroke();
+  }
+  for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
+    const isBold =
+      Math.round(y - scrollY) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
+    context.beginPath();
+    context.setLineDash(isBold ? [] : lineDash);
+    context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
+    context.moveTo(offsetX - gridSize, y);
+    context.lineTo(offsetX + width + gridSize * 2, y);
+    context.stroke();
+  }
+  context.restore();
+};
+
+const frameClip = (
+  frame: ExcalidrawFrameLikeElement,
+  context: CanvasRenderingContext2D,
+  renderConfig: StaticCanvasRenderConfig,
+  appState: StaticCanvasAppState,
+) => {
+  context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY);
+  context.beginPath();
+  if (context.roundRect) {
+    context.roundRect(
+      0,
+      0,
+      frame.width,
+      frame.height,
+      FRAME_STYLE.radius / appState.zoom.value,
+    );
+  } else {
+    context.rect(0, 0, frame.width, frame.height);
+  }
+  context.clip();
+  context.translate(
+    -(frame.x + appState.scrollX),
+    -(frame.y + appState.scrollY),
+  );
+};
+
+let linkCanvasCache: any;
+const renderLinkIcon = (
+  element: NonDeletedExcalidrawElement,
+  context: CanvasRenderingContext2D,
+  appState: StaticCanvasAppState,
+  elementsMap: ElementsMap,
+) => {
+  if (element.link && !appState.selectedElementIds[element.id]) {
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
+    const [x, y, width, height] = getLinkHandleFromCoords(
+      [x1, y1, x2, y2],
+      element.angle,
+      appState,
+    );
+    const centerX = x + width / 2;
+    const centerY = y + height / 2;
+    context.save();
+    context.translate(appState.scrollX + centerX, appState.scrollY + centerY);
+    context.rotate(element.angle);
+
+    if (!linkCanvasCache || linkCanvasCache.zoom !== appState.zoom.value) {
+      linkCanvasCache = document.createElement("canvas");
+      linkCanvasCache.zoom = appState.zoom.value;
+      linkCanvasCache.width =
+        width * window.devicePixelRatio * appState.zoom.value;
+      linkCanvasCache.height =
+        height * window.devicePixelRatio * appState.zoom.value;
+      const linkCanvasCacheContext = linkCanvasCache.getContext("2d")!;
+      linkCanvasCacheContext.scale(
+        window.devicePixelRatio * appState.zoom.value,
+        window.devicePixelRatio * appState.zoom.value,
+      );
+      linkCanvasCacheContext.fillStyle = "#fff";
+      linkCanvasCacheContext.fillRect(0, 0, width, height);
+      linkCanvasCacheContext.drawImage(EXTERNAL_LINK_IMG, 0, 0, width, height);
+      linkCanvasCacheContext.restore();
+      context.drawImage(
+        linkCanvasCache,
+        x - centerX,
+        y - centerY,
+        width,
+        height,
+      );
+    } else {
+      context.drawImage(
+        linkCanvasCache,
+        x - centerX,
+        y - centerY,
+        width,
+        height,
+      );
+    }
+    context.restore();
+  }
+};
+const _renderStaticScene = ({
+  canvas,
+  rc,
+  elementsMap,
+  allElementsMap,
+  visibleElements,
+  scale,
+  appState,
+  renderConfig,
+}: StaticSceneRenderConfig) => {
+  if (canvas === null) {
+    return;
+  }
+
+  const { renderGrid = true, isExporting } = renderConfig;
+
+  const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
+    canvas,
+    scale,
+  );
+
+  const context = bootstrapCanvas({
+    canvas,
+    scale,
+    normalizedWidth,
+    normalizedHeight,
+    theme: appState.theme,
+    isExporting,
+    viewBackgroundColor: appState.viewBackgroundColor,
+  });
+
+  // Apply zoom
+  context.scale(appState.zoom.value, appState.zoom.value);
+
+  // Grid
+  if (renderGrid && appState.gridSize) {
+    strokeGrid(
+      context,
+      appState.gridSize,
+      appState.scrollX,
+      appState.scrollY,
+      appState.zoom,
+      normalizedWidth / appState.zoom.value,
+      normalizedHeight / appState.zoom.value,
+    );
+  }
+
+  const groupsToBeAddedToFrame = new Set<string>();
+
+  visibleElements.forEach((element) => {
+    if (
+      element.groupIds.length > 0 &&
+      appState.frameToHighlight &&
+      appState.selectedElementIds[element.id] &&
+      (elementOverlapsWithFrame(
+        element,
+        appState.frameToHighlight,
+        elementsMap,
+      ) ||
+        element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
+    ) {
+      element.groupIds.forEach((groupId) =>
+        groupsToBeAddedToFrame.add(groupId),
+      );
+    }
+  });
+
+  // Paint visible elements
+  visibleElements
+    .filter((el) => !isIframeLikeElement(el))
+    .forEach((element) => {
+      try {
+        const frameId = element.frameId || appState.frameToHighlight?.id;
+
+        if (
+          frameId &&
+          appState.frameRendering.enabled &&
+          appState.frameRendering.clip
+        ) {
+          context.save();
+
+          const frame = getTargetFrame(element, elementsMap, appState);
+
+          // TODO do we need to check isElementInFrame here?
+          if (frame && isElementInFrame(element, elementsMap, appState)) {
+            frameClip(frame, context, renderConfig, appState);
+          }
+          renderElement(
+            element,
+            elementsMap,
+            allElementsMap,
+            rc,
+            context,
+            renderConfig,
+            appState,
+          );
+          context.restore();
+        } else {
+          renderElement(
+            element,
+            elementsMap,
+            allElementsMap,
+            rc,
+            context,
+            renderConfig,
+            appState,
+          );
+        }
+        if (!isExporting) {
+          renderLinkIcon(element, context, appState, elementsMap);
+        }
+      } catch (error: any) {
+        console.error(error);
+      }
+    });
+
+  // render embeddables on top
+  visibleElements
+    .filter((el) => isIframeLikeElement(el))
+    .forEach((element) => {
+      try {
+        const render = () => {
+          renderElement(
+            element,
+            elementsMap,
+            allElementsMap,
+            rc,
+            context,
+            renderConfig,
+            appState,
+          );
+
+          if (
+            isIframeLikeElement(element) &&
+            (isExporting ||
+              (isEmbeddableElement(element) &&
+                renderConfig.embedsValidationStatus.get(element.id) !==
+                  true)) &&
+            element.width &&
+            element.height
+          ) {
+            const label = createPlaceholderEmbeddableLabel(element);
+            renderElement(
+              label,
+              elementsMap,
+              allElementsMap,
+              rc,
+              context,
+              renderConfig,
+              appState,
+            );
+          }
+          if (!isExporting) {
+            renderLinkIcon(element, context, appState, elementsMap);
+          }
+        };
+        // - when exporting the whole canvas, we DO NOT apply clipping
+        // - when we are exporting a particular frame, apply clipping
+        //   if the containing frame is not selected, apply clipping
+        const frameId = element.frameId || appState.frameToHighlight?.id;
+
+        if (
+          frameId &&
+          appState.frameRendering.enabled &&
+          appState.frameRendering.clip
+        ) {
+          context.save();
+
+          const frame = getTargetFrame(element, elementsMap, appState);
+
+          if (frame && isElementInFrame(element, elementsMap, appState)) {
+            frameClip(frame, context, renderConfig, appState);
+          }
+          render();
+          context.restore();
+        } else {
+          render();
+        }
+      } catch (error: any) {
+        console.error(error);
+      }
+    });
+};
+
+/** throttled to animation framerate */
+export const renderStaticSceneThrottled = throttleRAF(
+  (config: StaticSceneRenderConfig) => {
+    _renderStaticScene(config);
+  },
+  { trailing: true },
+);
+
+/**
+ * Static scene is the non-ui canvas where we render elements.
+ */
+export const renderStaticScene = (
+  renderConfig: StaticSceneRenderConfig,
+  throttle?: boolean,
+) => {
+  if (throttle) {
+    renderStaticSceneThrottled(renderConfig);
+    return;
+  }
+
+  _renderStaticScene(renderConfig);
+};

+ 653 - 0
packages/excalidraw/renderer/staticSvgScene.ts

@@ -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);
+        }
+      }
+    });
+};

+ 5 - 2
packages/excalidraw/scene/Renderer.ts

@@ -4,7 +4,9 @@ import {
   NonDeletedElementsMap,
   NonDeletedElementsMap,
   NonDeletedExcalidrawElement,
   NonDeletedExcalidrawElement,
 } from "../element/types";
 } from "../element/types";
-import { cancelRender } from "../renderer/renderScene";
+import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
+import { renderStaticSceneThrottled } from "../renderer/staticScene";
+
 import { AppState } from "../types";
 import { AppState } from "../types";
 import { memoize, toBrandedType } from "../utils";
 import { memoize, toBrandedType } from "../utils";
 import Scene from "./Scene";
 import Scene from "./Scene";
@@ -147,7 +149,8 @@ export class Renderer {
   // NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be
   // NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be
   // safe to break TS contract here (for upstream cases)
   // safe to break TS contract here (for upstream cases)
   public destroy() {
   public destroy() {
-    cancelRender();
+    renderInteractiveSceneThrottled.cancel();
+    renderStaticSceneThrottled.cancel();
     this.getRenderableElements.clear();
     this.getRenderableElements.clear();
   }
   }
 }
 }

+ 2 - 1
packages/excalidraw/scene/export.ts

@@ -11,7 +11,7 @@ import {
   getCommonBounds,
   getCommonBounds,
   getElementAbsoluteCoords,
   getElementAbsoluteCoords,
 } from "../element/bounds";
 } from "../element/bounds";
-import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
+import { renderSceneToSvg } from "../renderer/staticSvgScene";
 import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
 import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
 import { AppState, BinaryFiles } from "../types";
 import { AppState, BinaryFiles } from "../types";
 import {
 import {
@@ -38,6 +38,7 @@ import { Mutable } from "../utility-types";
 import { newElementWith } from "../element/mutateElement";
 import { newElementWith } from "../element/mutateElement";
 import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
 import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
 import { RenderableElementsMap } from "./types";
 import { RenderableElementsMap } from "./types";
+import { renderStaticScene } from "../renderer/staticScene";
 
 
 const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 
 

+ 2 - 2
packages/excalidraw/tests/App.test.tsx

@@ -1,12 +1,12 @@
 import ReactDOM from "react-dom";
 import ReactDOM from "react-dom";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
 import { reseed } from "../random";
 import { reseed } from "../random";
 import { render, queryByTestId } from "../tests/test-utils";
 import { render, queryByTestId } from "../tests/test-utils";
 
 
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
 import { vi } from "vitest";
 import { vi } from "vitest";
 
 
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 describe("Test <App/>", () => {
 describe("Test <App/>", () => {
   beforeEach(async () => {
   beforeEach(async () => {

+ 2 - 2
packages/excalidraw/tests/contextmenu.test.tsx

@@ -12,7 +12,7 @@ import {
   togglePopover,
   togglePopover,
 } from "./test-utils";
 } from "./test-utils";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
 import { reseed } from "../random";
 import { reseed } from "../random";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
@@ -39,7 +39,7 @@ const mouse = new Pointer("mouse");
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderStaticScene.mockClear();
   renderStaticScene.mockClear();

+ 7 - 3
packages/excalidraw/tests/dragCreate.test.tsx

@@ -1,6 +1,7 @@
 import ReactDOM from "react-dom";
 import ReactDOM from "react-dom";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
+import * as InteractiveScene from "../renderer/interactiveScene";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
 import {
 import {
   render,
   render,
@@ -15,8 +16,11 @@ import { vi } from "vitest";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderInteractiveScene = vi.spyOn(
+  InteractiveScene,
+  "renderInteractiveScene",
+);
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();

+ 8 - 3
packages/excalidraw/tests/linearElementEditor.test.tsx

@@ -8,7 +8,9 @@ import {
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
 import { centerPoint } from "../math";
 import { centerPoint } from "../math";
 import { reseed } from "../random";
 import { reseed } from "../random";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
+import * as InteractiveCanvas from "../renderer/interactiveScene";
+
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
 import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
 import { API } from "../tests/helpers/api";
 import { API } from "../tests/helpers/api";
@@ -26,8 +28,11 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
 import { vi } from "vitest";
 import { vi } from "vitest";
 import { arrayToMap } from "../utils";
 import { arrayToMap } from "../utils";
 
 
-const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderInteractiveScene = vi.spyOn(
+  InteractiveCanvas,
+  "renderInteractiveScene",
+);
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 const { h } = window;
 const { h } = window;
 const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
 const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;

+ 7 - 3
packages/excalidraw/tests/move.test.tsx

@@ -1,7 +1,8 @@
 import ReactDOM from "react-dom";
 import ReactDOM from "react-dom";
 import { render, fireEvent } from "./test-utils";
 import { render, fireEvent } from "./test-utils";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
+import * as InteractiveCanvas from "../renderer/interactiveScene";
 import { reseed } from "../random";
 import { reseed } from "../random";
 import { bindOrUnbindLinearElement } from "../element/binding";
 import { bindOrUnbindLinearElement } from "../element/binding";
 import {
 import {
@@ -16,8 +17,11 @@ import { vi } from "vitest";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderInteractiveScene = vi.spyOn(
+  InteractiveCanvas,
+  "renderInteractiveScene",
+);
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();

+ 7 - 3
packages/excalidraw/tests/multiPointCreate.test.tsx

@@ -6,7 +6,8 @@ import {
   restoreOriginalGetBoundingClientRect,
   restoreOriginalGetBoundingClientRect,
 } from "./test-utils";
 } from "./test-utils";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
+import * as InteractiveCanvas from "../renderer/interactiveScene";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
 import { ExcalidrawLinearElement } from "../element/types";
 import { ExcalidrawLinearElement } from "../element/types";
 import { reseed } from "../random";
 import { reseed } from "../random";
@@ -15,8 +16,11 @@ import { vi } from "vitest";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderInteractiveScene = vi.spyOn(
+  InteractiveCanvas,
+  "renderInteractiveScene",
+);
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();

+ 2 - 2
packages/excalidraw/tests/regressionTests.test.tsx

@@ -3,7 +3,7 @@ import { ExcalidrawElement } from "../element/types";
 import { CODES, KEYS } from "../keys";
 import { CODES, KEYS } from "../keys";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
 import { reseed } from "../random";
 import { reseed } from "../random";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
 import { setDateTimeForTests } from "../utils";
 import { setDateTimeForTests } from "../utils";
 import { API } from "./helpers/api";
 import { API } from "./helpers/api";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
@@ -19,7 +19,7 @@ import { vi } from "vitest";
 
 
 const { h } = window;
 const { h } = window;
 
 
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 const mouse = new Pointer("mouse");
 const mouse = new Pointer("mouse");
 const finger1 = new Pointer("touch", 1);
 const finger1 = new Pointer("touch", 1);

+ 7 - 3
packages/excalidraw/tests/selection.test.tsx

@@ -7,7 +7,8 @@ import {
   assertSelectedElements,
   assertSelectedElements,
 } from "./test-utils";
 } from "./test-utils";
 import { Excalidraw } from "../index";
 import { Excalidraw } from "../index";
-import * as Renderer from "../renderer/renderScene";
+import * as StaticScene from "../renderer/staticScene";
+import * as InteractiveCanvas from "../renderer/interactiveScene";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
 import { reseed } from "../random";
 import { reseed } from "../random";
 import { API } from "./helpers/api";
 import { API } from "./helpers/api";
@@ -18,8 +19,11 @@ import { vi } from "vitest";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
-const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
+const renderInteractiveScene = vi.spyOn(
+  InteractiveCanvas,
+  "renderInteractiveScene",
+);
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
 
 
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác