mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	refactor: factor out shape generation from renderElement.ts pt 2 (#6878)
				
					
				
			This commit is contained in:
		@@ -10,7 +10,7 @@ import { distance2d, rotate, rotatePoint } from "../math";
 | 
			
		||||
import rough from "roughjs/bin/rough";
 | 
			
		||||
import { Drawable, Op } from "roughjs/bin/core";
 | 
			
		||||
import { Point } from "../types";
 | 
			
		||||
import { generateRoughOptions } from "../renderer/renderElement";
 | 
			
		||||
import { generateRoughOptions } from "../scene/Shape";
 | 
			
		||||
import {
 | 
			
		||||
  isArrowElement,
 | 
			
		||||
  isFreeDrawElement,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  ExcalidrawImageElement,
 | 
			
		||||
@@ -16,24 +14,13 @@ import {
 | 
			
		||||
  isArrowElement,
 | 
			
		||||
  hasBoundTextElement,
 | 
			
		||||
} from "../element/typeChecks";
 | 
			
		||||
import {
 | 
			
		||||
  getDiamondPoints,
 | 
			
		||||
  getElementAbsoluteCoords,
 | 
			
		||||
  getArrowheadPoints,
 | 
			
		||||
} from "../element/bounds";
 | 
			
		||||
import { RoughCanvas } from "roughjs/bin/canvas";
 | 
			
		||||
import { Drawable, Options } from "roughjs/bin/core";
 | 
			
		||||
import { RoughSVG } from "roughjs/bin/svg";
 | 
			
		||||
import { RoughGenerator } from "roughjs/bin/generator";
 | 
			
		||||
import { getElementAbsoluteCoords } from "../element/bounds";
 | 
			
		||||
import type { RoughCanvas } from "roughjs/bin/canvas";
 | 
			
		||||
import type { Drawable } from "roughjs/bin/core";
 | 
			
		||||
import type { RoughSVG } from "roughjs/bin/svg";
 | 
			
		||||
 | 
			
		||||
import { StaticCanvasRenderConfig } from "../scene/types";
 | 
			
		||||
import {
 | 
			
		||||
  distance,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getFontFamilyString,
 | 
			
		||||
  isRTL,
 | 
			
		||||
  isTransparent,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
 | 
			
		||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
 | 
			
		||||
import rough from "roughjs/bin/rough";
 | 
			
		||||
import {
 | 
			
		||||
@@ -97,10 +84,6 @@ const shouldResetImageFilter = (
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
 | 
			
		||||
 | 
			
		||||
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
 | 
			
		||||
 | 
			
		||||
const getCanvasPadding = (element: ExcalidrawElement) =>
 | 
			
		||||
  element.type === "freedraw" ? element.strokeWidth * 12 : 20;
 | 
			
		||||
 | 
			
		||||
@@ -384,369 +367,11 @@ const drawElementOnCanvas = (
 | 
			
		||||
  context.globalAlpha = 1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const elementWithCanvasCache = new WeakMap<
 | 
			
		||||
export const elementWithCanvasCache = new WeakMap<
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawElementWithCanvas
 | 
			
		||||
>();
 | 
			
		||||
 | 
			
		||||
export const generateRoughOptions = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  continuousPath = false,
 | 
			
		||||
): Options => {
 | 
			
		||||
  const options: Options = {
 | 
			
		||||
    seed: element.seed,
 | 
			
		||||
    strokeLineDash:
 | 
			
		||||
      element.strokeStyle === "dashed"
 | 
			
		||||
        ? getDashArrayDashed(element.strokeWidth)
 | 
			
		||||
        : element.strokeStyle === "dotted"
 | 
			
		||||
        ? getDashArrayDotted(element.strokeWidth)
 | 
			
		||||
        : undefined,
 | 
			
		||||
    // for non-solid strokes, disable multiStroke because it tends to make
 | 
			
		||||
    // dashes/dots overlay each other
 | 
			
		||||
    disableMultiStroke: element.strokeStyle !== "solid",
 | 
			
		||||
    // for non-solid strokes, increase the width a bit to make it visually
 | 
			
		||||
    // similar to solid strokes, because we're also disabling multiStroke
 | 
			
		||||
    strokeWidth:
 | 
			
		||||
      element.strokeStyle !== "solid"
 | 
			
		||||
        ? element.strokeWidth + 0.5
 | 
			
		||||
        : element.strokeWidth,
 | 
			
		||||
    // when increasing strokeWidth, we must explicitly set fillWeight and
 | 
			
		||||
    // hachureGap because if not specified, roughjs uses strokeWidth to
 | 
			
		||||
    // calculate them (and we don't want the fills to be modified)
 | 
			
		||||
    fillWeight: element.strokeWidth / 2,
 | 
			
		||||
    hachureGap: element.strokeWidth * 4,
 | 
			
		||||
    roughness: element.roughness,
 | 
			
		||||
    stroke: element.strokeColor,
 | 
			
		||||
    preserveVertices: continuousPath,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case "rectangle":
 | 
			
		||||
    case "embeddable":
 | 
			
		||||
    case "diamond":
 | 
			
		||||
    case "ellipse": {
 | 
			
		||||
      options.fillStyle = element.fillStyle;
 | 
			
		||||
      options.fill = isTransparent(element.backgroundColor)
 | 
			
		||||
        ? undefined
 | 
			
		||||
        : element.backgroundColor;
 | 
			
		||||
      if (element.type === "ellipse") {
 | 
			
		||||
        options.curveFitting = 1;
 | 
			
		||||
      }
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
    case "line":
 | 
			
		||||
    case "freedraw": {
 | 
			
		||||
      if (isPathALoop(element.points)) {
 | 
			
		||||
        options.fillStyle = element.fillStyle;
 | 
			
		||||
        options.fill =
 | 
			
		||||
          element.backgroundColor === "transparent"
 | 
			
		||||
            ? undefined
 | 
			
		||||
            : element.backgroundColor;
 | 
			
		||||
      }
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
    case "arrow":
 | 
			
		||||
      return options;
 | 
			
		||||
    default: {
 | 
			
		||||
      throw new Error(`Unimplemented type ${element.type}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const modifyEmbeddableForRoughOptions = (
 | 
			
		||||
  element: NonDeletedExcalidrawElement,
 | 
			
		||||
  isExporting: boolean,
 | 
			
		||||
) => {
 | 
			
		||||
  if (
 | 
			
		||||
    element.type === "embeddable" &&
 | 
			
		||||
    (isExporting || !element.validated) &&
 | 
			
		||||
    isTransparent(element.backgroundColor) &&
 | 
			
		||||
    isTransparent(element.strokeColor)
 | 
			
		||||
  ) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...element,
 | 
			
		||||
      roughness: 0,
 | 
			
		||||
      backgroundColor: "#d3d3d3",
 | 
			
		||||
      fillStyle: "solid",
 | 
			
		||||
    } as const;
 | 
			
		||||
  }
 | 
			
		||||
  return element;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates the element's shape and puts it into the cache.
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @param generator
 | 
			
		||||
 */
 | 
			
		||||
export const generateElementShape = (
 | 
			
		||||
  element: NonDeletedExcalidrawElement,
 | 
			
		||||
  generator: RoughGenerator,
 | 
			
		||||
  isExporting: boolean = false,
 | 
			
		||||
): Drawable | Drawable[] | null => {
 | 
			
		||||
  const cachedShape = isExporting ? undefined : ShapeCache.get(element);
 | 
			
		||||
 | 
			
		||||
  if (cachedShape) {
 | 
			
		||||
    return cachedShape;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // `null` indicates no rc shape applicable for this element type
 | 
			
		||||
  // (= do not generate anything)
 | 
			
		||||
  if (cachedShape === undefined) {
 | 
			
		||||
    let shape: Drawable | Drawable[] | null = null;
 | 
			
		||||
 | 
			
		||||
    elementWithCanvasCache.delete(element);
 | 
			
		||||
 | 
			
		||||
    switch (element.type) {
 | 
			
		||||
      case "rectangle":
 | 
			
		||||
      case "embeddable": {
 | 
			
		||||
        // this is for rendering the stroke/bg of the embeddable, especially
 | 
			
		||||
        // when the src url is not set
 | 
			
		||||
 | 
			
		||||
        if (element.roundness) {
 | 
			
		||||
          const w = element.width;
 | 
			
		||||
          const h = element.height;
 | 
			
		||||
          const r = getCornerRadius(Math.min(w, h), element);
 | 
			
		||||
          shape = generator.path(
 | 
			
		||||
            `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${
 | 
			
		||||
              h - r
 | 
			
		||||
            } Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
 | 
			
		||||
              h - r
 | 
			
		||||
            } L 0 ${r} Q 0 0, ${r} 0`,
 | 
			
		||||
            generateRoughOptions(
 | 
			
		||||
              modifyEmbeddableForRoughOptions(element, isExporting),
 | 
			
		||||
              true,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          shape = generator.rectangle(
 | 
			
		||||
            0,
 | 
			
		||||
            0,
 | 
			
		||||
            element.width,
 | 
			
		||||
            element.height,
 | 
			
		||||
            generateRoughOptions(
 | 
			
		||||
              modifyEmbeddableForRoughOptions(element, isExporting),
 | 
			
		||||
              false,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        ShapeCache.set(element, shape);
 | 
			
		||||
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case "diamond": {
 | 
			
		||||
        const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
 | 
			
		||||
          getDiamondPoints(element);
 | 
			
		||||
        if (element.roundness) {
 | 
			
		||||
          const verticalRadius = getCornerRadius(
 | 
			
		||||
            Math.abs(topX - leftX),
 | 
			
		||||
            element,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const horizontalRadius = getCornerRadius(
 | 
			
		||||
            Math.abs(rightY - topY),
 | 
			
		||||
            element,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          shape = generator.path(
 | 
			
		||||
            `M ${topX + verticalRadius} ${topY + horizontalRadius} L ${
 | 
			
		||||
              rightX - verticalRadius
 | 
			
		||||
            } ${rightY - horizontalRadius}
 | 
			
		||||
            C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
 | 
			
		||||
              rightX - verticalRadius
 | 
			
		||||
            } ${rightY + horizontalRadius}
 | 
			
		||||
            L ${bottomX + verticalRadius} ${bottomY - horizontalRadius}
 | 
			
		||||
            C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
 | 
			
		||||
              bottomX - verticalRadius
 | 
			
		||||
            } ${bottomY - horizontalRadius}
 | 
			
		||||
            L ${leftX + verticalRadius} ${leftY + horizontalRadius}
 | 
			
		||||
            C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${
 | 
			
		||||
              leftY - horizontalRadius
 | 
			
		||||
            }
 | 
			
		||||
            L ${topX - verticalRadius} ${topY + horizontalRadius}
 | 
			
		||||
            C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
 | 
			
		||||
              topY + horizontalRadius
 | 
			
		||||
            }`,
 | 
			
		||||
            generateRoughOptions(element, true),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          shape = generator.polygon(
 | 
			
		||||
            [
 | 
			
		||||
              [topX, topY],
 | 
			
		||||
              [rightX, rightY],
 | 
			
		||||
              [bottomX, bottomY],
 | 
			
		||||
              [leftX, leftY],
 | 
			
		||||
            ],
 | 
			
		||||
            generateRoughOptions(element),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        ShapeCache.set(element, shape);
 | 
			
		||||
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case "ellipse":
 | 
			
		||||
        shape = generator.ellipse(
 | 
			
		||||
          element.width / 2,
 | 
			
		||||
          element.height / 2,
 | 
			
		||||
          element.width,
 | 
			
		||||
          element.height,
 | 
			
		||||
          generateRoughOptions(element),
 | 
			
		||||
        );
 | 
			
		||||
        ShapeCache.set(element, shape);
 | 
			
		||||
 | 
			
		||||
        break;
 | 
			
		||||
      case "line":
 | 
			
		||||
      case "arrow": {
 | 
			
		||||
        const options = generateRoughOptions(element);
 | 
			
		||||
 | 
			
		||||
        // points array can be empty in the beginning, so it is important to add
 | 
			
		||||
        // initial position to it
 | 
			
		||||
        const points = element.points.length ? element.points : [[0, 0]];
 | 
			
		||||
 | 
			
		||||
        // curve is always the first element
 | 
			
		||||
        // this simplifies finding the curve for an element
 | 
			
		||||
        if (!element.roundness) {
 | 
			
		||||
          if (options.fill) {
 | 
			
		||||
            shape = [generator.polygon(points as [number, number][], options)];
 | 
			
		||||
          } else {
 | 
			
		||||
            shape = [
 | 
			
		||||
              generator.linearPath(points as [number, number][], options),
 | 
			
		||||
            ];
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          shape = [generator.curve(points as [number, number][], options)];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // add lines only in arrow
 | 
			
		||||
        if (element.type === "arrow") {
 | 
			
		||||
          const { startArrowhead = null, endArrowhead = "arrow" } = element;
 | 
			
		||||
 | 
			
		||||
          const getArrowheadShapes = (
 | 
			
		||||
            element: ExcalidrawLinearElement,
 | 
			
		||||
            shape: Drawable[],
 | 
			
		||||
            position: "start" | "end",
 | 
			
		||||
            arrowhead: Arrowhead,
 | 
			
		||||
          ) => {
 | 
			
		||||
            const arrowheadPoints = getArrowheadPoints(
 | 
			
		||||
              element,
 | 
			
		||||
              shape,
 | 
			
		||||
              position,
 | 
			
		||||
              arrowhead,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (arrowheadPoints === null) {
 | 
			
		||||
              return [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Other arrowheads here...
 | 
			
		||||
            if (arrowhead === "dot") {
 | 
			
		||||
              const [x, y, r] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
              return [
 | 
			
		||||
                generator.circle(x, y, r, {
 | 
			
		||||
                  ...options,
 | 
			
		||||
                  fill: element.strokeColor,
 | 
			
		||||
                  fillStyle: "solid",
 | 
			
		||||
                  stroke: "none",
 | 
			
		||||
                }),
 | 
			
		||||
              ];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (arrowhead === "triangle") {
 | 
			
		||||
              const [x, y, x2, y2, x3, y3] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
              // always use solid stroke for triangle arrowhead
 | 
			
		||||
              delete options.strokeLineDash;
 | 
			
		||||
 | 
			
		||||
              return [
 | 
			
		||||
                generator.polygon(
 | 
			
		||||
                  [
 | 
			
		||||
                    [x, y],
 | 
			
		||||
                    [x2, y2],
 | 
			
		||||
                    [x3, y3],
 | 
			
		||||
                    [x, y],
 | 
			
		||||
                  ],
 | 
			
		||||
                  {
 | 
			
		||||
                    ...options,
 | 
			
		||||
                    fill: element.strokeColor,
 | 
			
		||||
                    fillStyle: "solid",
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Arrow arrowheads
 | 
			
		||||
            const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
            if (element.strokeStyle === "dotted") {
 | 
			
		||||
              // for dotted arrows caps, reduce gap to make it more legible
 | 
			
		||||
              const dash = getDashArrayDotted(element.strokeWidth - 1);
 | 
			
		||||
              options.strokeLineDash = [dash[0], dash[1] - 1];
 | 
			
		||||
            } else {
 | 
			
		||||
              // for solid/dashed, keep solid arrow cap
 | 
			
		||||
              delete options.strokeLineDash;
 | 
			
		||||
            }
 | 
			
		||||
            return [
 | 
			
		||||
              generator.line(x3, y3, x2, y2, options),
 | 
			
		||||
              generator.line(x4, y4, x2, y2, options),
 | 
			
		||||
            ];
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          if (startArrowhead !== null) {
 | 
			
		||||
            const shapes = getArrowheadShapes(
 | 
			
		||||
              element,
 | 
			
		||||
              shape,
 | 
			
		||||
              "start",
 | 
			
		||||
              startArrowhead,
 | 
			
		||||
            );
 | 
			
		||||
            shape.push(...shapes);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (endArrowhead !== null) {
 | 
			
		||||
            if (endArrowhead === undefined) {
 | 
			
		||||
              // Hey, we have an old arrow here!
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const shapes = getArrowheadShapes(
 | 
			
		||||
              element,
 | 
			
		||||
              shape,
 | 
			
		||||
              "end",
 | 
			
		||||
              endArrowhead,
 | 
			
		||||
            );
 | 
			
		||||
            shape.push(...shapes);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ShapeCache.set(element, shape);
 | 
			
		||||
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case "freedraw": {
 | 
			
		||||
        generateFreeDrawShape(element);
 | 
			
		||||
 | 
			
		||||
        if (isPathALoop(element.points)) {
 | 
			
		||||
          // generate rough polygon to fill freedraw shape
 | 
			
		||||
          shape = generator.polygon(element.points as [number, number][], {
 | 
			
		||||
            ...generateRoughOptions(element),
 | 
			
		||||
            stroke: "none",
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          shape = null;
 | 
			
		||||
        }
 | 
			
		||||
        ShapeCache.set(element, shape);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case "text":
 | 
			
		||||
      case "image": {
 | 
			
		||||
        // just to ensure we don't regenerate element.canvas on rerenders
 | 
			
		||||
        ShapeCache.set(element, null);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return shape;
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const generateElementWithCanvas = (
 | 
			
		||||
  element: NonDeletedExcalidrawElement,
 | 
			
		||||
  renderConfig: StaticCanvasRenderConfig,
 | 
			
		||||
@@ -962,7 +587,6 @@ export const renderElement = (
 | 
			
		||||
  renderConfig: StaticCanvasRenderConfig,
 | 
			
		||||
  appState: StaticCanvasAppState,
 | 
			
		||||
) => {
 | 
			
		||||
  const generator = rc.generator;
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case "frame": {
 | 
			
		||||
      if (
 | 
			
		||||
@@ -1000,7 +624,10 @@ export const renderElement = (
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case "freedraw": {
 | 
			
		||||
      generateElementShape(element, generator);
 | 
			
		||||
      // TODO investigate if we can do this in situ. Right now we need to call
 | 
			
		||||
      // beforehand because math helpers (such as getElementAbsoluteCoords)
 | 
			
		||||
      // rely on existing shapes
 | 
			
		||||
      ShapeCache.generateElementShape(element);
 | 
			
		||||
 | 
			
		||||
      if (renderConfig.isExporting) {
 | 
			
		||||
        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
 | 
			
		||||
@@ -1038,7 +665,10 @@ export const renderElement = (
 | 
			
		||||
    case "image":
 | 
			
		||||
    case "text":
 | 
			
		||||
    case "embeddable": {
 | 
			
		||||
      generateElementShape(element, generator, renderConfig.isExporting);
 | 
			
		||||
      // TODO investigate if we can do this in situ. Right now we need to call
 | 
			
		||||
      // beforehand because math helpers (such as getElementAbsoluteCoords)
 | 
			
		||||
      // rely on existing shapes
 | 
			
		||||
      ShapeCache.generateElementShape(element, renderConfig.isExporting);
 | 
			
		||||
      if (renderConfig.isExporting) {
 | 
			
		||||
        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
 | 
			
		||||
        const cx = (x1 + x2) / 2 + appState.scrollX;
 | 
			
		||||
@@ -1255,7 +885,6 @@ export const renderElementToSvg = (
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const degree = (180 * element.angle) / Math.PI;
 | 
			
		||||
  const generator = rsvg.generator;
 | 
			
		||||
 | 
			
		||||
  // element to append node to, most of the time svgRoot
 | 
			
		||||
  let root = svgRoot;
 | 
			
		||||
@@ -1280,10 +909,10 @@ export const renderElementToSvg = (
 | 
			
		||||
    case "rectangle":
 | 
			
		||||
    case "diamond":
 | 
			
		||||
    case "ellipse": {
 | 
			
		||||
      generateElementShape(element, generator);
 | 
			
		||||
      const shape = ShapeCache.generateElementShape(element);
 | 
			
		||||
      const node = roughSVGDrawWithPrecision(
 | 
			
		||||
        rsvg,
 | 
			
		||||
        ShapeCache.get(element)!,
 | 
			
		||||
        shape,
 | 
			
		||||
        MAX_DECIMALS_FOR_SVG_EXPORT,
 | 
			
		||||
      );
 | 
			
		||||
      if (opacity !== 1) {
 | 
			
		||||
@@ -1310,10 +939,10 @@ export const renderElementToSvg = (
 | 
			
		||||
    }
 | 
			
		||||
    case "embeddable": {
 | 
			
		||||
      // render placeholder rectangle
 | 
			
		||||
      generateElementShape(element, generator, true);
 | 
			
		||||
      const shape = ShapeCache.generateElementShape(element, true);
 | 
			
		||||
      const node = roughSVGDrawWithPrecision(
 | 
			
		||||
        rsvg,
 | 
			
		||||
        ShapeCache.get(element)!,
 | 
			
		||||
        shape,
 | 
			
		||||
        MAX_DECIMALS_FOR_SVG_EXPORT,
 | 
			
		||||
      );
 | 
			
		||||
      const opacity = element.opacity / 100;
 | 
			
		||||
@@ -1347,7 +976,7 @@ export const renderElementToSvg = (
 | 
			
		||||
      // render embeddable element + iframe
 | 
			
		||||
      const embeddableNode = roughSVGDrawWithPrecision(
 | 
			
		||||
        rsvg,
 | 
			
		||||
        ShapeCache.get(element)!,
 | 
			
		||||
        shape,
 | 
			
		||||
        MAX_DECIMALS_FOR_SVG_EXPORT,
 | 
			
		||||
      );
 | 
			
		||||
      embeddableNode.setAttribute("stroke-linecap", "round");
 | 
			
		||||
@@ -1453,14 +1082,14 @@ export const renderElementToSvg = (
 | 
			
		||||
        maskRectInvisible.setAttribute("opacity", "1");
 | 
			
		||||
        maskPath.appendChild(maskRectInvisible);
 | 
			
		||||
      }
 | 
			
		||||
      generateElementShape(element, generator);
 | 
			
		||||
      const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
 | 
			
		||||
      if (boundText) {
 | 
			
		||||
        group.setAttribute("mask", `url(#mask-${element.id})`);
 | 
			
		||||
      }
 | 
			
		||||
      group.setAttribute("stroke-linecap", "round");
 | 
			
		||||
 | 
			
		||||
      ShapeCache.get(element)!.forEach((shape) => {
 | 
			
		||||
      const shapes = ShapeCache.generateElementShape(element);
 | 
			
		||||
      shapes.forEach((shape) => {
 | 
			
		||||
        const node = roughSVGDrawWithPrecision(
 | 
			
		||||
          rsvg,
 | 
			
		||||
          shape,
 | 
			
		||||
@@ -1501,11 +1130,13 @@ export const renderElementToSvg = (
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case "freedraw": {
 | 
			
		||||
      generateElementShape(element, generator);
 | 
			
		||||
      generateFreeDrawShape(element);
 | 
			
		||||
      const shape = ShapeCache.get(element);
 | 
			
		||||
      const node = shape
 | 
			
		||||
        ? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
 | 
			
		||||
      const backgroundFillShape = ShapeCache.generateElementShape(element);
 | 
			
		||||
      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}`);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										362
									
								
								src/scene/Shape.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								src/scene/Shape.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,362 @@
 | 
			
		||||
import type { Drawable, Options } from "roughjs/bin/core";
 | 
			
		||||
import type { RoughGenerator } from "roughjs/bin/generator";
 | 
			
		||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
 | 
			
		||||
import type { ElementShapes } from "./types";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  ExcalidrawSelectionElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { isPathALoop, getCornerRadius } from "../math";
 | 
			
		||||
import { generateFreeDrawShape } from "../renderer/renderElement";
 | 
			
		||||
import { isTransparent, assertNever } from "../utils";
 | 
			
		||||
 | 
			
		||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
 | 
			
		||||
 | 
			
		||||
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
 | 
			
		||||
 | 
			
		||||
export const generateRoughOptions = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  continuousPath = false,
 | 
			
		||||
): Options => {
 | 
			
		||||
  const options: Options = {
 | 
			
		||||
    seed: element.seed,
 | 
			
		||||
    strokeLineDash:
 | 
			
		||||
      element.strokeStyle === "dashed"
 | 
			
		||||
        ? getDashArrayDashed(element.strokeWidth)
 | 
			
		||||
        : element.strokeStyle === "dotted"
 | 
			
		||||
        ? getDashArrayDotted(element.strokeWidth)
 | 
			
		||||
        : undefined,
 | 
			
		||||
    // for non-solid strokes, disable multiStroke because it tends to make
 | 
			
		||||
    // dashes/dots overlay each other
 | 
			
		||||
    disableMultiStroke: element.strokeStyle !== "solid",
 | 
			
		||||
    // for non-solid strokes, increase the width a bit to make it visually
 | 
			
		||||
    // similar to solid strokes, because we're also disabling multiStroke
 | 
			
		||||
    strokeWidth:
 | 
			
		||||
      element.strokeStyle !== "solid"
 | 
			
		||||
        ? element.strokeWidth + 0.5
 | 
			
		||||
        : element.strokeWidth,
 | 
			
		||||
    // when increasing strokeWidth, we must explicitly set fillWeight and
 | 
			
		||||
    // hachureGap because if not specified, roughjs uses strokeWidth to
 | 
			
		||||
    // calculate them (and we don't want the fills to be modified)
 | 
			
		||||
    fillWeight: element.strokeWidth / 2,
 | 
			
		||||
    hachureGap: element.strokeWidth * 4,
 | 
			
		||||
    roughness: element.roughness,
 | 
			
		||||
    stroke: element.strokeColor,
 | 
			
		||||
    preserveVertices: continuousPath,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case "rectangle":
 | 
			
		||||
    case "embeddable":
 | 
			
		||||
    case "diamond":
 | 
			
		||||
    case "ellipse": {
 | 
			
		||||
      options.fillStyle = element.fillStyle;
 | 
			
		||||
      options.fill = isTransparent(element.backgroundColor)
 | 
			
		||||
        ? undefined
 | 
			
		||||
        : element.backgroundColor;
 | 
			
		||||
      if (element.type === "ellipse") {
 | 
			
		||||
        options.curveFitting = 1;
 | 
			
		||||
      }
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
    case "line":
 | 
			
		||||
    case "freedraw": {
 | 
			
		||||
      if (isPathALoop(element.points)) {
 | 
			
		||||
        options.fillStyle = element.fillStyle;
 | 
			
		||||
        options.fill =
 | 
			
		||||
          element.backgroundColor === "transparent"
 | 
			
		||||
            ? undefined
 | 
			
		||||
            : element.backgroundColor;
 | 
			
		||||
      }
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
    case "arrow":
 | 
			
		||||
      return options;
 | 
			
		||||
    default: {
 | 
			
		||||
      throw new Error(`Unimplemented type ${element.type}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const modifyEmbeddableForRoughOptions = (
 | 
			
		||||
  element: NonDeletedExcalidrawElement,
 | 
			
		||||
  isExporting: boolean,
 | 
			
		||||
) => {
 | 
			
		||||
  if (
 | 
			
		||||
    element.type === "embeddable" &&
 | 
			
		||||
    (isExporting || !element.validated) &&
 | 
			
		||||
    isTransparent(element.backgroundColor) &&
 | 
			
		||||
    isTransparent(element.strokeColor)
 | 
			
		||||
  ) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...element,
 | 
			
		||||
      roughness: 0,
 | 
			
		||||
      backgroundColor: "#d3d3d3",
 | 
			
		||||
      fillStyle: "solid",
 | 
			
		||||
    } as const;
 | 
			
		||||
  }
 | 
			
		||||
  return element;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates the roughjs shape for given element.
 | 
			
		||||
 *
 | 
			
		||||
 * Low-level. Use `ShapeCache.generateElementShape` instead.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export const _generateElementShape = (
 | 
			
		||||
  element: Exclude<NonDeletedExcalidrawElement, ExcalidrawSelectionElement>,
 | 
			
		||||
  generator: RoughGenerator,
 | 
			
		||||
  isExporting: boolean = false,
 | 
			
		||||
): Drawable | Drawable[] | null => {
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case "rectangle":
 | 
			
		||||
    case "embeddable": {
 | 
			
		||||
      let shape: ElementShapes[typeof element.type];
 | 
			
		||||
      // this is for rendering the stroke/bg of the embeddable, especially
 | 
			
		||||
      // when the src url is not set
 | 
			
		||||
 | 
			
		||||
      if (element.roundness) {
 | 
			
		||||
        const w = element.width;
 | 
			
		||||
        const h = element.height;
 | 
			
		||||
        const r = getCornerRadius(Math.min(w, h), element);
 | 
			
		||||
        shape = generator.path(
 | 
			
		||||
          `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${
 | 
			
		||||
            h - r
 | 
			
		||||
          } Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
 | 
			
		||||
            h - r
 | 
			
		||||
          } L 0 ${r} Q 0 0, ${r} 0`,
 | 
			
		||||
          generateRoughOptions(
 | 
			
		||||
            modifyEmbeddableForRoughOptions(element, isExporting),
 | 
			
		||||
            true,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        shape = generator.rectangle(
 | 
			
		||||
          0,
 | 
			
		||||
          0,
 | 
			
		||||
          element.width,
 | 
			
		||||
          element.height,
 | 
			
		||||
          generateRoughOptions(
 | 
			
		||||
            modifyEmbeddableForRoughOptions(element, isExporting),
 | 
			
		||||
            false,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    case "diamond": {
 | 
			
		||||
      let shape: ElementShapes[typeof element.type];
 | 
			
		||||
 | 
			
		||||
      const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
 | 
			
		||||
        getDiamondPoints(element);
 | 
			
		||||
      if (element.roundness) {
 | 
			
		||||
        const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
 | 
			
		||||
 | 
			
		||||
        const horizontalRadius = getCornerRadius(
 | 
			
		||||
          Math.abs(rightY - topY),
 | 
			
		||||
          element,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        shape = generator.path(
 | 
			
		||||
          `M ${topX + verticalRadius} ${topY + horizontalRadius} L ${
 | 
			
		||||
            rightX - verticalRadius
 | 
			
		||||
          } ${rightY - horizontalRadius}
 | 
			
		||||
            C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
 | 
			
		||||
            rightX - verticalRadius
 | 
			
		||||
          } ${rightY + horizontalRadius}
 | 
			
		||||
            L ${bottomX + verticalRadius} ${bottomY - horizontalRadius}
 | 
			
		||||
            C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
 | 
			
		||||
            bottomX - verticalRadius
 | 
			
		||||
          } ${bottomY - horizontalRadius}
 | 
			
		||||
            L ${leftX + verticalRadius} ${leftY + horizontalRadius}
 | 
			
		||||
            C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${
 | 
			
		||||
            leftY - horizontalRadius
 | 
			
		||||
          }
 | 
			
		||||
            L ${topX - verticalRadius} ${topY + horizontalRadius}
 | 
			
		||||
            C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
 | 
			
		||||
            topY + horizontalRadius
 | 
			
		||||
          }`,
 | 
			
		||||
          generateRoughOptions(element, true),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        shape = generator.polygon(
 | 
			
		||||
          [
 | 
			
		||||
            [topX, topY],
 | 
			
		||||
            [rightX, rightY],
 | 
			
		||||
            [bottomX, bottomY],
 | 
			
		||||
            [leftX, leftY],
 | 
			
		||||
          ],
 | 
			
		||||
          generateRoughOptions(element),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    case "ellipse": {
 | 
			
		||||
      const shape: ElementShapes[typeof element.type] = generator.ellipse(
 | 
			
		||||
        element.width / 2,
 | 
			
		||||
        element.height / 2,
 | 
			
		||||
        element.width,
 | 
			
		||||
        element.height,
 | 
			
		||||
        generateRoughOptions(element),
 | 
			
		||||
      );
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    case "line":
 | 
			
		||||
    case "arrow": {
 | 
			
		||||
      let shape: ElementShapes[typeof element.type];
 | 
			
		||||
      const options = generateRoughOptions(element);
 | 
			
		||||
 | 
			
		||||
      // points array can be empty in the beginning, so it is important to add
 | 
			
		||||
      // initial position to it
 | 
			
		||||
      const points = element.points.length ? element.points : [[0, 0]];
 | 
			
		||||
 | 
			
		||||
      // curve is always the first element
 | 
			
		||||
      // this simplifies finding the curve for an element
 | 
			
		||||
      if (!element.roundness) {
 | 
			
		||||
        if (options.fill) {
 | 
			
		||||
          shape = [generator.polygon(points as [number, number][], options)];
 | 
			
		||||
        } else {
 | 
			
		||||
          shape = [generator.linearPath(points as [number, number][], options)];
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        shape = [generator.curve(points as [number, number][], options)];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // add lines only in arrow
 | 
			
		||||
      if (element.type === "arrow") {
 | 
			
		||||
        const { startArrowhead = null, endArrowhead = "arrow" } = element;
 | 
			
		||||
 | 
			
		||||
        const getArrowheadShapes = (
 | 
			
		||||
          element: ExcalidrawLinearElement,
 | 
			
		||||
          shape: Drawable[],
 | 
			
		||||
          position: "start" | "end",
 | 
			
		||||
          arrowhead: Arrowhead,
 | 
			
		||||
        ) => {
 | 
			
		||||
          const arrowheadPoints = getArrowheadPoints(
 | 
			
		||||
            element,
 | 
			
		||||
            shape,
 | 
			
		||||
            position,
 | 
			
		||||
            arrowhead,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (arrowheadPoints === null) {
 | 
			
		||||
            return [];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Other arrowheads here...
 | 
			
		||||
          if (arrowhead === "dot") {
 | 
			
		||||
            const [x, y, r] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
              generator.circle(x, y, r, {
 | 
			
		||||
                ...options,
 | 
			
		||||
                fill: element.strokeColor,
 | 
			
		||||
                fillStyle: "solid",
 | 
			
		||||
                stroke: "none",
 | 
			
		||||
              }),
 | 
			
		||||
            ];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (arrowhead === "triangle") {
 | 
			
		||||
            const [x, y, x2, y2, x3, y3] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
            // always use solid stroke for triangle arrowhead
 | 
			
		||||
            delete options.strokeLineDash;
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
              generator.polygon(
 | 
			
		||||
                [
 | 
			
		||||
                  [x, y],
 | 
			
		||||
                  [x2, y2],
 | 
			
		||||
                  [x3, y3],
 | 
			
		||||
                  [x, y],
 | 
			
		||||
                ],
 | 
			
		||||
                {
 | 
			
		||||
                  ...options,
 | 
			
		||||
                  fill: element.strokeColor,
 | 
			
		||||
                  fillStyle: "solid",
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Arrow arrowheads
 | 
			
		||||
          const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
 | 
			
		||||
 | 
			
		||||
          if (element.strokeStyle === "dotted") {
 | 
			
		||||
            // for dotted arrows caps, reduce gap to make it more legible
 | 
			
		||||
            const dash = getDashArrayDotted(element.strokeWidth - 1);
 | 
			
		||||
            options.strokeLineDash = [dash[0], dash[1] - 1];
 | 
			
		||||
          } else {
 | 
			
		||||
            // for solid/dashed, keep solid arrow cap
 | 
			
		||||
            delete options.strokeLineDash;
 | 
			
		||||
          }
 | 
			
		||||
          return [
 | 
			
		||||
            generator.line(x3, y3, x2, y2, options),
 | 
			
		||||
            generator.line(x4, y4, x2, y2, options),
 | 
			
		||||
          ];
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (startArrowhead !== null) {
 | 
			
		||||
          const shapes = getArrowheadShapes(
 | 
			
		||||
            element,
 | 
			
		||||
            shape,
 | 
			
		||||
            "start",
 | 
			
		||||
            startArrowhead,
 | 
			
		||||
          );
 | 
			
		||||
          shape.push(...shapes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (endArrowhead !== null) {
 | 
			
		||||
          if (endArrowhead === undefined) {
 | 
			
		||||
            // Hey, we have an old arrow here!
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const shapes = getArrowheadShapes(
 | 
			
		||||
            element,
 | 
			
		||||
            shape,
 | 
			
		||||
            "end",
 | 
			
		||||
            endArrowhead,
 | 
			
		||||
          );
 | 
			
		||||
          shape.push(...shapes);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    case "freedraw": {
 | 
			
		||||
      let shape: ElementShapes[typeof element.type];
 | 
			
		||||
      generateFreeDrawShape(element);
 | 
			
		||||
 | 
			
		||||
      if (isPathALoop(element.points)) {
 | 
			
		||||
        // generate rough polygon to fill freedraw shape
 | 
			
		||||
        shape = generator.polygon(element.points as [number, number][], {
 | 
			
		||||
          ...generateRoughOptions(element),
 | 
			
		||||
          stroke: "none",
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        shape = null;
 | 
			
		||||
      }
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    case "frame":
 | 
			
		||||
    case "text":
 | 
			
		||||
    case "image": {
 | 
			
		||||
      const shape: ElementShapes[typeof element.type] = null;
 | 
			
		||||
      // we return (and cache) `null` to make sure we don't regenerate
 | 
			
		||||
      // `element.canvas` on rerenders
 | 
			
		||||
      return shape;
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      assertNever(
 | 
			
		||||
        element,
 | 
			
		||||
        `generateElementShape(): Unimplemented type ${(element as any)?.type}`,
 | 
			
		||||
      );
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,28 +1,27 @@
 | 
			
		||||
import { Drawable } from "roughjs/bin/core";
 | 
			
		||||
import { RoughGenerator } from "roughjs/bin/generator";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { generateElementShape } from "../renderer/renderElement";
 | 
			
		||||
 | 
			
		||||
type ElementShape = Drawable | Drawable[] | null;
 | 
			
		||||
 | 
			
		||||
type ElementShapes = {
 | 
			
		||||
  freedraw: Drawable | null;
 | 
			
		||||
  arrow: Drawable[];
 | 
			
		||||
  line: Drawable[];
 | 
			
		||||
  text: null;
 | 
			
		||||
  image: null;
 | 
			
		||||
};
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawSelectionElement,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { elementWithCanvasCache } from "../renderer/renderElement";
 | 
			
		||||
import { _generateElementShape } from "./Shape";
 | 
			
		||||
import { ElementShape, ElementShapes } from "./types";
 | 
			
		||||
 | 
			
		||||
export class ShapeCache {
 | 
			
		||||
  private static rg = new RoughGenerator();
 | 
			
		||||
  private static cache = new WeakMap<ExcalidrawElement, ElementShape>();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Retrieves shape from cache if available. Use this only if shape
 | 
			
		||||
   * is optional and you have a fallback in case it's not cached.
 | 
			
		||||
   */
 | 
			
		||||
  public static get = <T extends ExcalidrawElement>(element: T) => {
 | 
			
		||||
    return ShapeCache.cache.get(
 | 
			
		||||
      element,
 | 
			
		||||
    ) as T["type"] extends keyof ElementShapes
 | 
			
		||||
      ? ElementShapes[T["type"]] | undefined
 | 
			
		||||
      : Drawable | null | undefined;
 | 
			
		||||
      : ElementShape | undefined;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  public static set = <T extends ExcalidrawElement>(
 | 
			
		||||
@@ -41,15 +40,29 @@ export class ShapeCache {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generates & caches shape for element if not already cached, otherwise
 | 
			
		||||
   * return cached shape.
 | 
			
		||||
   * returns cached shape.
 | 
			
		||||
   */
 | 
			
		||||
  public static generateElementShape = <T extends ExcalidrawElement>(
 | 
			
		||||
  public static generateElementShape = <
 | 
			
		||||
    T extends Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
 | 
			
		||||
  >(
 | 
			
		||||
    element: T,
 | 
			
		||||
    isExporting = false,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const shape = generateElementShape(
 | 
			
		||||
    // when exporting, always regenerated to guarantee the latest shape
 | 
			
		||||
    const cachedShape = isExporting ? undefined : ShapeCache.get(element);
 | 
			
		||||
 | 
			
		||||
    // `null` indicates no rc shape applicable for this element type,
 | 
			
		||||
    // but it's considered a valid cache value (= do not regenerate)
 | 
			
		||||
    if (cachedShape !== undefined) {
 | 
			
		||||
      return cachedShape;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    elementWithCanvasCache.delete(element);
 | 
			
		||||
 | 
			
		||||
    const shape = _generateElementShape(
 | 
			
		||||
      element,
 | 
			
		||||
      ShapeCache.rg,
 | 
			
		||||
      /* so it prefers cache */ false,
 | 
			
		||||
      isExporting,
 | 
			
		||||
    ) as T["type"] extends keyof ElementShapes
 | 
			
		||||
      ? ElementShapes[T["type"]]
 | 
			
		||||
      : Drawable | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { RoughCanvas } from "roughjs/bin/canvas";
 | 
			
		||||
import type { RoughCanvas } from "roughjs/bin/canvas";
 | 
			
		||||
import { Drawable } from "roughjs/bin/core";
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
@@ -90,3 +91,18 @@ export type ScrollBars = {
 | 
			
		||||
    height: number;
 | 
			
		||||
  } | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ElementShape = Drawable | Drawable[] | null;
 | 
			
		||||
 | 
			
		||||
export type ElementShapes = {
 | 
			
		||||
  rectangle: Drawable;
 | 
			
		||||
  ellipse: Drawable;
 | 
			
		||||
  diamond: Drawable;
 | 
			
		||||
  embeddable: Drawable;
 | 
			
		||||
  freedraw: Drawable | null;
 | 
			
		||||
  arrow: Drawable[];
 | 
			
		||||
  line: Drawable[];
 | 
			
		||||
  text: null;
 | 
			
		||||
  image: null;
 | 
			
		||||
  frame: null;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user