engmsaleh / excalidraw

Virtual whiteboard for sketching hand-drawn like diagrams
https://excalidraw.com
MIT License
0 stars 0 forks source link

"Triangle" shapeTask #2

Open engmsaleh opened 1 week ago

engmsaleh commented 1 week ago

What - description of what you me to do Example: Hey @autopilot implement I need to extend shapes drawing with a new "Triangle" shape

Why - explain why this is important I need the ability to draw triangle shapes like square and circle as it already has

codeautopilot[bot] commented 1 week ago

Potential solution

To implement the "Triangle" shape in the Excalidraw application, we need to make changes across several files to ensure that the new shape is properly defined, rendered, and integrated into the UI. The steps involve defining the new shape type, updating rendering logic, adding the shape to the UI, and handling its creation.

How to implement

Step 1: Define the Triangle Shape Type

File: packages/excalidraw/element/types.ts

Add a new type definition for the 'Triangle' shape to ensure type safety and proper handling throughout the codebase.

export type ExcalidrawTriangleElement = _ExcalidrawElementBase & {
  type: "triangle";
};

export type ExcalidrawGenericElement =
  | ExcalidrawSelectionElement
  | ExcalidrawRectangleElement
  | ExcalidrawDiamondElement
  | ExcalidrawEllipseElement
  | ExcalidrawTriangleElement;

export type ExcalidrawElement =
  | ExcalidrawGenericElement
  | ExcalidrawTextElement
  | ExcalidrawLinearElement
  | ExcalidrawFreeDrawElement
  | ExcalidrawImageElement
  | ExcalidrawFrameElement
  | ExcalidrawMagicFrameElement
  | ExcalidrawIframeElement
  | ExcalidrawEmbeddableElement;

Step 2: Add Rendering Logic for the Triangle Shape

File: packages/excalidraw/renderer/renderElement.ts

Update the rendering logic to handle the 'Triangle' shape.

const drawElementOnCanvas = (
  element: NonDeletedExcalidrawElement,
  rc: RoughCanvas,
  context: CanvasRenderingContext2D,
  renderConfig: StaticCanvasRenderConfig,
  appState: StaticCanvasAppState,
) => {
  switch (element.type) {
    case "rectangle":
    case "iframe":
    case "embeddable":
    case "diamond":
    case "ellipse":
    case "triangle": {
      context.lineJoin = "round";
      context.lineCap = "round";
      rc.draw(ShapeCache.get(element)!);
      break;
    }
    // ... other cases
  }
};

export const renderElement = (
  element: NonDeletedExcalidrawElement,
  elementsMap: RenderableElementsMap,
  allElementsMap: NonDeletedSceneElementsMap,
  rc: RoughCanvas,
  context: CanvasRenderingContext2D,
  renderConfig: StaticCanvasRenderConfig,
  appState: StaticCanvasAppState,
) => {
  context.globalAlpha = getRenderOpacity(
    element,
    getContainingFrame(element, elementsMap),
    renderConfig.elementsPendingErasure,
  );

  switch (element.type) {
    case "magicframe":
    case "frame": {
      // ... existing code
      break;
    }
    case "freedraw": {
      // ... existing code
      break;
    }
    case "rectangle":
    case "diamond":
    case "ellipse":
    case "line":
    case "arrow":
    case "image":
    case "text":
    case "iframe":
    case "embeddable":
    case "triangle": {
      // ... existing code for rendering
      break;
    }
    default: {
      // @ts-ignore
      throw new Error(`Unimplemented type ${element.type}`);
    }
  }

  context.globalAlpha = 1;
};

Step 3: Add the Triangle Shape to the Shape Selection UI

File: packages/excalidraw/shapes.tsx

Add the triangle shape to the shape selection UI.

import {
  ArrowIcon,
  DiamondIcon,
  EllipseIcon,
  EraserIcon,
  FreedrawIcon,
  ImageIcon,
  LineIcon,
  RectangleIcon,
  SelectionIcon,
  TextIcon,
  TriangleIcon, // Import the Triangle icon
} from "./components/icons";
import { KEYS } from "./keys";

export const SHAPES = [
  {
    icon: SelectionIcon,
    value: "selection",
    key: KEYS.V,
    numericKey: KEYS["1"],
    fillable: true,
  },
  {
    icon: RectangleIcon,
    value: "rectangle",
    key: KEYS.R,
    numericKey: KEYS["2"],
    fillable: true,
  },
  {
    icon: DiamondIcon,
    value: "diamond",
    key: KEYS.D,
    numericKey: KEYS["3"],
    fillable: true,
  },
  {
    icon: EllipseIcon,
    value: "ellipse",
    key: KEYS.O,
    numericKey: KEYS["4"],
    fillable: true,
  },
  {
    icon: ArrowIcon,
    value: "arrow",
    key: KEYS.A,
    numericKey: KEYS["5"],
    fillable: true,
  },
  {
    icon: LineIcon,
    value: "line",
    key: KEYS.L,
    numericKey: KEYS["6"],
    fillable: true,
  },
  {
    icon: FreedrawIcon,
    value: "freedraw",
    key: [KEYS.P, KEYS.X],
    numericKey: KEYS["7"],
    fillable: false,
  },
  {
    icon: TextIcon,
    value: "text",
    key: KEYS.T,
    numericKey: KEYS["8"],
    fillable: false,
  },
  {
    icon: ImageIcon,
    value: "image",
    key: null,
    numericKey: KEYS["9"],
    fillable: false,
  },
  {
    icon: EraserIcon,
    value: "eraser",
    key: KEYS.E,
    numericKey: KEYS["0"],
    fillable: false,
  },
  {
    icon: TriangleIcon, // Add the Triangle icon
    value: "triangle", // Define the value for the triangle shape
    key: KEYS.TRIANGLE, // Define a key for the triangle shape
    numericKey: KEYS["10"], // Define a numeric key for the triangle shape
    fillable: true, // Set fillable to true if the triangle can be filled
  },
] as const;

export const findShapeByKey = (key: string) => {
  const shape = SHAPES.find((shape, index) => {
    return (
      (shape.numericKey != null && key === shape.numericKey.toString()) ||
      (shape.key &&
        (typeof shape.key === "string"
          ? shape.key === key
          : (shape.key as readonly string[]).includes(key)))
    );
  });
  return shape?.value || null;
};

Step 4: Handle the Creation of the Triangle Element

File: packages/excalidraw/element/newElement.ts

Add the logic for creating a new 'Triangle' element.

export const newTriangleElement = (
  opts: ElementConstructorOpts,
): NonDeleted<ExcalidrawGenericElement> => {
  return _newElementBase<ExcalidrawGenericElement>("triangle", opts);
};

export const newElement = (
  opts: {
    type: ExcalidrawGenericElement["type"];
  } & ElementConstructorOpts,
): NonDeleted<ExcalidrawGenericElement> => {
  switch (opts.type) {
    case "triangle":
      return newTriangleElement(opts);
    // Add other cases if necessary
    default:
      return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
  }
};

Step 5: Integrate the Triangle Shape into the Shape Selection UI

File: packages/excalidraw/components/Actions.tsx

Ensure that the triangle shape is correctly displayed in the shape selection toolbar and that it can be selected and used for drawing.

import { triangleIcon } from "./icons"; // Ensure you have this icon

const SHAPES = [
  // ... other shapes
  {
    value: "triangle",
    icon: triangleIcon,
    key: "T",
    numericKey: 3,
    fillable: true,
  },
];

export const ShapesSwitcher = ({
  activeTool,
  appState,
  app,
  UIOptions,
}: {
  activeTool: UIAppState["activeTool"];
  appState: UIAppState;
  app: AppClassProperties;
  UIOptions: AppProps["UIOptions"];
}) => {
  const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);

  const frameToolSelected = activeTool.type === "frame";
  const laserToolSelected = activeTool.type === "laser";
  const embeddableToolSelected = activeTool.type === "embeddable";

  const { TTDDialogTriggerTunnel } = useTunnels();

  return (
    <>
      {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
        if (
          UIOptions.tools?.[
            value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
          ] === false
        ) {
          return null;
        }

        const label = t(`toolBar.${value}`);
        const letter =
          key && capitalizeString(typeof key === "string" ? key : key[0]);
        const shortcut = letter
          ? `${letter} ${t("helpDialog.or")} ${numericKey}`
          : `${numericKey}`;
        return (
          <ToolButton
            className={clsx("Shape", { fillable })}
            key={value}
            type="radio"
            icon={icon}
            checked={activeTool.type === value}
            name="editor-current-shape"
            title={`${capitalizeString(label)} — ${shortcut}`}
            keyBindingLabel={numericKey || letter}
            aria-label={capitalizeString(label)}
            aria-keyshortcuts={shortcut}
            data-testid={`toolbar-${value}`}
            onPointerDown={({ pointerType }) => {
              if (!appState.penDetected && pointerType === "pen") {
                app.togglePenMode(true);
              }
            }}
            onChange={({ pointerType }) => {
              if (appState.activeTool.type !== value) {
                trackEvent("toolbar", value, "ui");
              }
              if (value === "image") {
                app.setActiveTool({
                  type: value,
                  insertOnCanvasDirectly: pointerType !== "mouse",
                });
              } else {
                app.setActiveTool({ type: value });
              }
            }}
          />
        );
      })}
      <div className="App-toolbar__divider" />

      <DropdownMenu open={isExtraToolsMenuOpen}>
        <DropdownMenu.Trigger
          className={clsx("App-toolbar__extra-tools-trigger", {
            "App-toolbar__extra-tools-trigger--selected":
              frameToolSelected ||
              embeddableToolSelected ||
              // in collab we're already highlighting the laser button
              // outside toolbar, so let's not highlight extra-tools button
              // on top of it
              (laserToolSelected && !app.props.isCollaborating),
          })}
          onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
          title={t("toolBar.extraTools")}
        >
          {extraToolsIcon}
          {app.props.aiEnabled !== false && (
            <div
              style={{
                display: "inline-flex",
                marginLeft: "auto",
                padding: "2px 4px",
                borderRadius: 6,
                fontSize: 8,
                fontFamily: "Cascadia, monospace",
                position: "absolute",
                background: "var(--color-promo)",
                color: "var(--color-surface-lowest)",
                bottom: 3,
                right: 4,
              }}
            >
              AI
            </div>
          )}
        </DropdownMenu.Trigger>
        <DropdownMenu.Content
          onClickOutside={() => setIsExtraToolsMenuOpen(false)}
          onSelect={() => setIsExtraToolsMenuOpen(false)}
          className="App-toolbar__extra-tools-dropdown"
        >
          <DropdownMenu.Item
            onSelect={() => app.setActiveTool({ type: "frame" })}
            icon={frameToolIcon}
            shortcut={KEYS.F.toLocaleUpperCase()}
            data-testid="toolbar-frame"
            selected={frameToolSelected}
          >
            {t("toolBar.frame")}
          </DropdownMenu.Item>
          <DropdownMenu.Item
            onSelect={() => app.setActiveTool({ type: "embeddable" })}
            icon={EmbedIcon}
            data-testid="toolbar-embeddable"
            selected={embeddableToolSelected}
          >
            {t("toolBar.embeddable")}
          </DropdownMenu.Item>
          <DropdownMenu.Item
            onSelect={() => app.setActiveTool({ type: "laser" })}
            icon={laserPointerToolIcon}
            data-testid="toolbar-laser"
            selected={laserToolSelected}
            shortcut={KEYS.K.toLocaleUpperCase()}
          >
            {t("toolBar.laser")}
          </DropdownMenu.Item>
          <div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
            Generate
          </div>
          {app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
          <DropdownMenu.Item
            onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
            icon={mermaidLogoIcon}
            data-testid="toolbar-embeddable"
          >
            {t("toolBar.mermaidToExcalidraw")}
          </DropdownMenu.Item>
          {app.props.aiEnabled !== false && (
            <>
              <DropdownMenu.Item
                onSelect={() => app.onMagicframeToolSelect()}
                icon={MagicIcon}
                data-testid="toolbar-magicframe"
              >
                {t("toolBar.magicframe")}
                <DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
              </DropdownMenu.Item>
              <DropdownMenu.Item
                onSelect={() => {
                  trackEvent("ai", "open-settings", "d2c");
                  app.setOpenDialog({
                    name: "settings",
                    source: "settings",
                    tab: "diagram-to-code",
                  });
                }}
                icon={OpenAIIcon}
                data-testid="toolbar-magicSettings"
              >
                {t("toolBar.magicSettings")}
              </DropdownMenu.Item>
            </>
          )}
        </DropdownMenu.Content>
      </DropdownMenu>
    </>
  );
};

Summary

  1. Import the triangle icon.
  2. Add the triangle shape to the SHAPES array.
  3. Ensure the triangle shape is displayed and selectable in the shape selection toolbar.

These changes will integrate the 'Triangle' shape into the shape selection UI, allowing users to select and draw triangles just like other shapes.

Click here to create a Pull Request with the proposed solution

Files used for this task:

Changes on packages/excalidraw/element/types.ts To add a new type definition for the 'Triangle' shape in the `types.ts` file, you need to follow the existing pattern for other shapes like `rectangle`, `diamond`, and `ellipse`. This will ensure type safety and proper handling throughout the codebase. Here's a step-by-step guide to achieve this: 1. **Define the `ExcalidrawTriangleElement` type**: - This type should extend the `_ExcalidrawElementBase` type and include a `type` property set to `"triangle"`. 2. **Update the `ExcalidrawGenericElement` union type**: - Add the new `ExcalidrawTriangleElement` to this union type. 3. **Update the `ExcalidrawElement` union type**: - Add the new `ExcalidrawTriangleElement` to this union type as well. Here is the modified code: ```typescript // Add this new type definition for the Triangle shape export type ExcalidrawTriangleElement = _ExcalidrawElementBase & { type: "triangle"; }; // Update the ExcalidrawGenericElement union type export type ExcalidrawGenericElement = | ExcalidrawSelectionElement | ExcalidrawRectangleElement | ExcalidrawDiamondElement | ExcalidrawEllipseElement | ExcalidrawTriangleElement; // Add Triangle here // Update the ExcalidrawElement union type export type ExcalidrawElement = | ExcalidrawGenericElement | ExcalidrawTextElement | ExcalidrawLinearElement | ExcalidrawFreeDrawElement | ExcalidrawImageElement | ExcalidrawFrameElement | ExcalidrawMagicFrameElement | ExcalidrawIframeElement | ExcalidrawEmbeddableElement; ``` This change ensures that the `Triangle` shape is recognized as a valid shape type throughout the codebase, maintaining type safety and proper handling. With this modification, you have completed the necessary changes for this file. The other files will need to handle the creation, rendering, and UI integration of the `Triangle` shape.
Changes on packages/excalidraw/renderer/renderElement.ts To add rendering logic for the 'Triangle' shape in the `renderElement.ts` file, you need to follow these steps: 1. **Define the Triangle Shape in the `drawElementOnCanvas` Function**: - Add a case for the 'Triangle' shape in the `drawElementOnCanvas` function to handle its drawing logic. 2. **Update the `renderElement` Function**: - Ensure that the `renderElement` function can handle the 'Triangle' shape by adding it to the switch-case structure. Here's a concrete proposal for the changes: ### Step 1: Define the Triangle Shape in the `drawElementOnCanvas` Function Add a new case for the 'Triangle' shape in the `drawElementOnCanvas` function. This function is responsible for drawing the element on the canvas. ```typescript const drawElementOnCanvas = ( element: NonDeletedExcalidrawElement, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { switch (element.type) { case "rectangle": case "iframe": case "embeddable": case "diamond": case "ellipse": case "triangle": { // Add this case context.lineJoin = "round"; context.lineCap = "round"; rc.draw(ShapeCache.get(element)!); break; } // ... other cases } }; ``` ### Step 2: Update the `renderElement` Function Add the 'Triangle' shape to the switch-case structure in the `renderElement` function to ensure it is rendered correctly. ```typescript export const renderElement = ( element: NonDeletedExcalidrawElement, elementsMap: RenderableElementsMap, allElementsMap: NonDeletedSceneElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { context.globalAlpha = getRenderOpacity( element, getContainingFrame(element, elementsMap), renderConfig.elementsPendingErasure, ); switch (element.type) { case "magicframe": case "frame": { // ... existing code break; } case "freedraw": { // ... existing code break; } case "rectangle": case "diamond": case "ellipse": case "line": case "arrow": case "image": case "text": case "iframe": case "embeddable": case "triangle": { // Add this case // ... existing code for rendering break; } default: { // @ts-ignore throw new Error(`Unimplemented type ${element.type}`); } } context.globalAlpha = 1; }; ``` ### Summary By adding the 'Triangle' shape to both the `drawElementOnCanvas` and `renderElement` functions, you ensure that the triangle shape is correctly rendered on the canvas. This involves defining how the triangle should be drawn and ensuring that the rendering logic can handle this new shape type.
Changes on packages/excalidraw/shapes.tsx To add the definition for the 'Triangle' shape in the `packages/excalidraw/shapes.tsx` file, you need to follow these steps: 1. **Import the Triangle Icon**: Ensure you have an icon for the triangle shape. If not, you need to create one or use an existing one. 2. **Add the Triangle Shape Definition**: Add the triangle shape to the `SHAPES` array with its properties, including the icon, value, key, numericKey, and fillable status. Here's how you can modify the file: ### Updated `packages/excalidraw/shapes.tsx` ```typescript import { ArrowIcon, DiamondIcon, EllipseIcon, EraserIcon, FreedrawIcon, ImageIcon, LineIcon, RectangleIcon, SelectionIcon, TextIcon, TriangleIcon, // Import the Triangle icon } from "./components/icons"; import { KEYS } from "./keys"; export const SHAPES = [ { icon: SelectionIcon, value: "selection", key: KEYS.V, numericKey: KEYS["1"], fillable: true, }, { icon: RectangleIcon, value: "rectangle", key: KEYS.R, numericKey: KEYS["2"], fillable: true, }, { icon: DiamondIcon, value: "diamond", key: KEYS.D, numericKey: KEYS["3"], fillable: true, }, { icon: EllipseIcon, value: "ellipse", key: KEYS.O, numericKey: KEYS["4"], fillable: true, }, { icon: ArrowIcon, value: "arrow", key: KEYS.A, numericKey: KEYS["5"], fillable: true, }, { icon: LineIcon, value: "line", key: KEYS.L, numericKey: KEYS["6"], fillable: true, }, { icon: FreedrawIcon, value: "freedraw", key: [KEYS.P, KEYS.X], numericKey: KEYS["7"], fillable: false, }, { icon: TextIcon, value: "text", key: KEYS.T, numericKey: KEYS["8"], fillable: false, }, { icon: ImageIcon, value: "image", key: null, numericKey: KEYS["9"], fillable: false, }, { icon: EraserIcon, value: "eraser", key: KEYS.E, numericKey: KEYS["0"], fillable: false, }, { icon: TriangleIcon, // Add the Triangle icon value: "triangle", // Define the value for the triangle shape key: KEYS.TRIANGLE, // Define a key for the triangle shape numericKey: KEYS["10"], // Define a numeric key for the triangle shape fillable: true, // Set fillable to true if the triangle can be filled }, ] as const; export const findShapeByKey = (key: string) => { const shape = SHAPES.find((shape, index) => { return ( (shape.numericKey != null && key === shape.numericKey.toString()) || (shape.key && (typeof shape.key === "string" ? shape.key === key : (shape.key as readonly string[]).includes(key))) ); }); return shape?.value || null; }; ``` ### Explanation: 1. **Import the Triangle Icon**: Ensure you have a `TriangleIcon` in your `./components/icons` file. If not, you need to create one. 2. **Add the Triangle Shape Definition**: Add a new object to the `SHAPES` array with the properties for the triangle shape: - `icon`: The icon for the triangle shape. - `value`: The value identifier for the triangle shape. - `key`: The keyboard shortcut for selecting the triangle shape. You need to define `KEYS.TRIANGLE` in your `./keys` file. - `numericKey`: The numeric key for selecting the triangle shape. - `fillable`: Set to `true` if the triangle shape can be filled with color. This change will add the triangle shape to the list of available shapes in the application. Make sure to coordinate with the other files mentioned in the plan to ensure the triangle shape is fully integrated into the application.
Changes on packages/excalidraw/element/newElement.ts To add the logic for creating a new 'Triangle' element in the `newElement.ts` file, you need to follow these steps: 1. **Define the new element type**: Add a function to create a new 'Triangle' element, similar to how other elements are defined. 2. **Set initial properties and dimensions**: Ensure that the new 'Triangle' element has its initial properties and dimensions set correctly. Here's how you can modify the `newElement.ts` file to include the 'Triangle' element: ### Step-by-Step Implementation 1. **Add the new function to create a 'Triangle' element**: ```typescript export const newTriangleElement = ( opts: ElementConstructorOpts, ): NonDeleted => { return _newElementBase("triangle", opts); }; ``` 2. **Update the `newElement` function to handle the 'Triangle' type**: ```typescript export const newElement = ( opts: { type: ExcalidrawGenericElement["type"]; } & ElementConstructorOpts, ): NonDeleted => { switch (opts.type) { case "triangle": return newTriangleElement(opts); // Add other cases if necessary default: return _newElementBase(opts.type, opts); } }; ``` ### Updated `newElement.ts` File Here's the updated file with the new 'Triangle' element logic: ```typescript import type { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ExcalidrawLinearElement, ExcalidrawGenericElement, NonDeleted, TextAlign, GroupId, VerticalAlign, Arrowhead, ExcalidrawFreeDrawElement, FontFamilyValues, ExcalidrawTextContainer, ExcalidrawFrameElement, ExcalidrawEmbeddableElement, ExcalidrawMagicFrameElement, ExcalidrawIframeElement, ElementsMap, } from "./types"; import { arrayToMap, getFontString, getUpdatedTimestamp, isTestEnv, } from "../utils"; import { randomInteger, randomId } from "../random"; import { bumpVersion, newElementWith } from "./mutateElement"; import { getNewGroupIdsForDuplication } from "../groups"; import type { AppState } from "../types"; import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; import { measureText, normalizeText, wrapText, getBoundTextMaxWidth, getDefaultLineHeight, } from "./textElement"; import { DEFAULT_ELEMENT_PROPS, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, VERTICAL_ALIGN, } from "../constants"; import type { MarkOptional, Merge, Mutable } from "../utility-types"; export type ElementConstructorOpts = MarkOptional< Omit, | "width" | "height" | "angle" | "groupIds" | "frameId" | "index" | "boundElements" | "seed" | "version" | "versionNonce" | "link" | "strokeStyle" | "fillStyle" | "strokeColor" | "backgroundColor" | "roughness" | "strokeWidth" | "roundness" | "locked" | "opacity" | "customData" >; const _newElementBase = ( type: T["type"], { x, y, strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor, backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor, fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle, strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth, strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle, roughness = DEFAULT_ELEMENT_PROPS.roughness, opacity = DEFAULT_ELEMENT_PROPS.opacity, width = 0, height = 0, angle = 0, groupIds = [], frameId = null, index = null, roundness = null, boundElements = null, link = null, locked = DEFAULT_ELEMENT_PROPS.locked, ...rest }: ElementConstructorOpts & Omit, "type">, ) => { // assign type to guard against excess properties const element: Merge = { id: rest.id || randomId(), type, x, y, width, height, angle, strokeColor, backgroundColor, fillStyle, strokeWidth, strokeStyle, roughness, opacity, groupIds, frameId, index, roundness, seed: rest.seed ?? randomInteger(), version: rest.version || 1, versionNonce: rest.versionNonce ?? 0, isDeleted: false as false, boundElements, updated: getUpdatedTimestamp(), link, locked, customData: rest.customData, }; return element; }; export const newElement = ( opts: { type: ExcalidrawGenericElement["type"]; } & ElementConstructorOpts, ): NonDeleted => { switch (opts.type) { case "triangle": return newTriangleElement(opts); // Add other cases if necessary default: return _newElementBase(opts.type, opts); } }; export const newTriangleElement = ( opts: ElementConstructorOpts, ): NonDeleted => { return _newElementBase("triangle", opts); }; // ... (rest of the file remains unchanged) ``` This implementation ensures that the 'Triangle' element is created with the necessary properties and dimensions. The other files mentioned in the plan will handle the rendering, type definitions, and UI integration for the 'Triangle' shape.
Changes on packages/excalidraw/components/Actions.tsx To add the 'Triangle' shape to the shape selection UI and handle its selection and drawing actions in the `packages/excalidraw/components/Actions.tsx` file, follow these steps: 1. **Import the Triangle Icon**: Ensure you have an icon for the triangle shape. If not, create or obtain one and import it. 2. **Update the SHAPES Array**: Add the 'Triangle' shape to the `SHAPES` array with its properties, including the icon, key, and other relevant details. 3. **Handle the Triangle Shape in the UI**: Ensure that the triangle shape is correctly displayed in the shape selection toolbar and that it can be selected and used for drawing. Here is the modified code with the necessary changes: ### Step 1: Import the Triangle Icon First, ensure you have an icon for the triangle shape. For this example, let's assume you have an icon named `triangleIcon`. ```tsx import { triangleIcon } from "./icons"; // Ensure you have this icon ``` ### Step 2: Update the SHAPES Array Add the 'Triangle' shape to the `SHAPES` array. ```tsx const SHAPES = [ // ... other shapes { value: "triangle", icon: triangleIcon, key: "T", numericKey: 3, fillable: true, }, ]; ``` ### Step 3: Handle the Triangle Shape in the UI Ensure that the triangle shape is correctly displayed in the shape selection toolbar and that it can be selected and used for drawing. ```tsx export const ShapesSwitcher = ({ activeTool, appState, app, UIOptions, }: { activeTool: UIAppState["activeTool"]; appState: UIAppState; app: AppClassProperties; UIOptions: AppProps["UIOptions"]; }) => { const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); const frameToolSelected = activeTool.type === "frame"; const laserToolSelected = activeTool.type === "laser"; const embeddableToolSelected = activeTool.type === "embeddable"; const { TTDDialogTriggerTunnel } = useTunnels(); return ( <> {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { if ( UIOptions.tools?.[ value as Extract ] === false ) { return null; } const label = t(`toolBar.${value}`); const letter = key && capitalizeString(typeof key === "string" ? key : key[0]); const shortcut = letter ? `${letter} ${t("helpDialog.or")} ${numericKey}` : `${numericKey}`; return ( { if (!appState.penDetected && pointerType === "pen") { app.togglePenMode(true); } }} onChange={({ pointerType }) => { if (appState.activeTool.type !== value) { trackEvent("toolbar", value, "ui"); } if (value === "image") { app.setActiveTool({ type: value, insertOnCanvasDirectly: pointerType !== "mouse", }); } else { app.setActiveTool({ type: value }); } }} /> ); })}
setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)} title={t("toolBar.extraTools")} > {extraToolsIcon} {app.props.aiEnabled !== false && (
AI
)}
setIsExtraToolsMenuOpen(false)} onSelect={() => setIsExtraToolsMenuOpen(false)} className="App-toolbar__extra-tools-dropdown" > app.setActiveTool({ type: "frame" })} icon={frameToolIcon} shortcut={KEYS.F.toLocaleUpperCase()} data-testid="toolbar-frame" selected={frameToolSelected} > {t("toolBar.frame")} app.setActiveTool({ type: "embeddable" })} icon={EmbedIcon} data-testid="toolbar-embeddable" selected={embeddableToolSelected} > {t("toolBar.embeddable")} app.setActiveTool({ type: "laser" })} icon={laserPointerToolIcon} data-testid="toolbar-laser" selected={laserToolSelected} shortcut={KEYS.K.toLocaleUpperCase()} > {t("toolBar.laser")}
Generate
{app.props.aiEnabled !== false && } app.setOpenDialog({ name: "ttd", tab: "mermaid" })} icon={mermaidLogoIcon} data-testid="toolbar-embeddable" > {t("toolBar.mermaidToExcalidraw")} {app.props.aiEnabled !== false && ( <> app.onMagicframeToolSelect()} icon={MagicIcon} data-testid="toolbar-magicframe" > {t("toolBar.magicframe")} AI { trackEvent("ai", "open-settings", "d2c"); app.setOpenDialog({ name: "settings", source: "settings", tab: "diagram-to-code", }); }} icon={OpenAIIcon} data-testid="toolbar-magicSettings" > {t("toolBar.magicSettings")} )}
); }; ``` ### Summary 1. **Import the triangle icon**. 2. **Add the triangle shape to the `SHAPES` array**. 3. **Ensure the triangle shape is displayed and selectable in the shape selection toolbar**. These changes will integrate the 'Triangle' shape into the shape selection UI, allowing users to select and draw triangles just like other shapes.
engmsaleh commented 1 week ago

Ok, what about the drawElementOnCanvas and renderElement implementation code ?