Open engmsaleh opened 1 week ago
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.
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;
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;
};
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;
};
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);
}
};
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>
</>
);
};
SHAPES
array.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:
Ok, what about the drawElementOnCanvas
and renderElement
implementation code ?
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