From c8d38e87b0cd525696215c918e844d9ca7e588fd Mon Sep 17 00:00:00 2001 From: Ayesha Imran Date: Mon, 28 Apr 2025 12:45:54 +0500 Subject: [PATCH] feat: add support for regular polygon shape with customizable sides --- packages/common/src/constants.ts | 6 +- packages/element/src/Shape.ts | 124 +++++++++++++++++- packages/element/src/bounds.ts | 15 +-- packages/element/src/distance.ts | 6 +- packages/element/src/newElement.ts | 25 +++- packages/element/src/renderElement.ts | 4 +- packages/element/src/shapes.ts | 20 ++- packages/element/src/typeChecks.ts | 15 ++- packages/element/src/types.ts | 17 ++- .../excalidraw/actions/actionLinearEditor.tsx | 40 +++++- packages/excalidraw/actions/types.ts | 5 +- packages/excalidraw/components/App.tsx | 42 +++--- .../CommandPalette/CommandPalette.tsx | 4 +- packages/excalidraw/components/HelpDialog.tsx | 6 +- .../excalidraw/components/Stats/index.tsx | 6 +- packages/excalidraw/components/icons.tsx | 15 ++- packages/excalidraw/components/shapes.tsx | 10 +- packages/excalidraw/data/restore.ts | 3 +- packages/excalidraw/locales/en.json | 4 +- .../excalidraw/renderer/staticSvgScene.ts | 3 +- packages/excalidraw/scene/types.ts | 5 +- packages/excalidraw/tests/helpers/api.ts | 12 +- .../excalidraw/tests/queries/toolQueries.ts | 4 +- packages/excalidraw/types.ts | 5 +- 24 files changed, 329 insertions(+), 67 deletions(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index cd3bd7a15..81546fd84 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -432,12 +432,12 @@ export const TOOL_TYPE = { freedraw: "freedraw", text: "text", image: "image", + regularPolygon: "regularPolygon", eraser: "eraser", hand: "hand", + laser: "laser", frame: "frame", magicframe: "magicframe", - embeddable: "embeddable", - laser: "laser", } as const; export const EDITOR_LS_KEYS = { @@ -473,4 +473,4 @@ export enum UserIdleState { ACTIVE = "active", AWAY = "away", IDLE = "idle", -} +} \ No newline at end of file diff --git a/packages/element/src/Shape.ts b/packages/element/src/Shape.ts index 4def41957..e981471f8 100644 --- a/packages/element/src/Shape.ts +++ b/packages/element/src/Shape.ts @@ -28,6 +28,7 @@ import type { ExcalidrawSelectionElement, ExcalidrawLinearElement, Arrowhead, + ExcalidrawRegularPolygonElement, } from "./types"; import type { Drawable, Options } from "roughjs/bin/core"; @@ -108,6 +109,14 @@ export const generateRoughOptions = ( } return options; } + case "regularPolygon": { + options.fillStyle = element.fillStyle; + options.fill = isTransparent(element.backgroundColor) + ? undefined + : element.backgroundColor; + // Add any specific options for polygons if needed, otherwise just return + return options; + } case "line": case "freedraw": { if (isPathALoop(element.points)) { @@ -127,6 +136,50 @@ export const generateRoughOptions = ( } }; +/** + * Returns the points for a regular polygon with the specified number of sides, + * centered within the element's bounds. + */ +export const getRegularPolygonPoints = ( + element: ExcalidrawElement, + sides: number = 6 +): [number, number][] => { + // Minimum number of sides for a polygon is 3 + if (sides < 3) { + sides = 3; + } + + const width = element.width; + const height = element.height; + + // Center of the element + const cx = width / 2; + const cy = height / 2; + + // Use the smaller dimension to ensure polygon fits within the element bounds + const radius = Math.min(width, height) / 2; + + // Calculate points for the regular polygon + const points: [number, number][] = []; + + // For regular polygons, we want to start from the top (angle = -π/2) + // so that polygons like hexagons have a flat top + const startAngle = -Math.PI / 2; + + for (let i = 0; i < sides; i++) { + // Calculate angle for this vertex + const angle = startAngle + (2 * Math.PI * i) / sides; + + // Calculate x and y for this vertex + const x = cx + radius * Math.cos(angle); + const y = cy + radius * Math.sin(angle); + + points.push([x, y]); + } + + return points; +}; + const modifyIframeLikeForRoughOptions = ( element: NonDeletedExcalidrawElement, isExporting: boolean, @@ -537,6 +590,75 @@ export const _generateElementShape = ( // `element.canvas` on rerenders return shape; } + case "regularPolygon": { + let shape: ElementShapes[typeof element.type]; + + const points = getRegularPolygonPoints( + element, + (element as ExcalidrawRegularPolygonElement).sides + ); + + if (element.roundness) { + // For rounded corners, we create a path with smooth corners + // using quadratic Bézier curves, similar to the diamond shape + const options = generateRoughOptions(element, true); + + // Calculate appropriate corner radius based on element size + const radius = getCornerRadius( + Math.min(element.width, element.height) / 4, + element + ); + + const pathData: string[] = []; + + // Process each vertex to create rounded corners between edges + for (let i = 0; i < points.length; i++) { + const current = points[i]; + const next = points[(i + 1) % points.length]; + const prev = points[(i - 1 + points.length) % points.length]; + + // Calculate vectors to previous and next points + const toPrev = [prev[0] - current[0], prev[1] - current[1]]; + const toNext = [next[0] - current[0], next[1] - current[1]]; + + // Normalize vectors and calculate corner points + const toPrevLength = Math.sqrt(toPrev[0] * toPrev[0] + toPrev[1] * toPrev[1]); + const toNextLength = Math.sqrt(toNext[0] * toNext[0] + toNext[1] * toNext[1]); + + // Move inward from vertex toward previous point (limited by half the distance) + const prevCorner = [ + current[0] + (toPrev[0] / toPrevLength) * Math.min(radius, toPrevLength / 2), + current[1] + (toPrev[1] / toPrevLength) * Math.min(radius, toPrevLength / 2) + ]; + + // Move inward from vertex toward next point (limited by half the distance) + const nextCorner = [ + current[0] + (toNext[0] / toNextLength) * Math.min(radius, toNextLength / 2), + current[1] + (toNext[1] / toNextLength) * Math.min(radius, toNextLength / 2) + ]; + + // First point needs a move command, others need line commands + if (i === 0) { + pathData.push(`M ${nextCorner[0]} ${nextCorner[1]}`); + } else { + // Draw line to the corner coming from previous point + pathData.push(`L ${prevCorner[0]} ${prevCorner[1]}`); + // Draw a quadratic curve around the current vertex to the corner going to next point + pathData.push(`Q ${current[0]} ${current[1]}, ${nextCorner[0]} ${nextCorner[1]}`); + } + } + + // Close the path to create a complete shape + pathData.push("Z"); + + shape = generator.path(pathData.join(" "), options); + } else { + // For non-rounded corners, use the simple polygon generator + shape = generator.polygon(points, generateRoughOptions(element)); + } + + return shape; + } default: { assertNever( element, @@ -610,4 +732,4 @@ const generateElbowArrowShape = ( d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`); return d.join(" "); -}; +}; \ No newline at end of file diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index d0c071f2c..761c085bc 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -1,11 +1,6 @@ import rough from "roughjs/bin/rough"; -import { - rescalePoints, - arrayToMap, - invariant, - sizeOf, -} from "@excalidraw/common"; +import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common"; import { degreesToRadians, @@ -62,7 +57,7 @@ import type { ElementsMap, ExcalidrawRectanguloidElement, ExcalidrawEllipseElement, - ElementsMapOrArray, + ExcalidrawRegularPolygonElement, } from "./types"; import type { Drawable, Op } from "roughjs/bin/core"; import type { Point as RoughPoint } from "roughjs/bin/geometry"; @@ -944,10 +939,10 @@ export const getElementBounds = ( }; export const getCommonBounds = ( - elements: ElementsMapOrArray, + elements: readonly ExcalidrawElement[], elementsMap?: ElementsMap, ): Bounds => { - if (!sizeOf(elements)) { + if (!elements.length) { return [0, 0, 0, 0]; } @@ -1145,4 +1140,4 @@ export const doBoundsIntersect = ( const [minX2, minY2, maxX2, maxY2] = bounds2; return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; -}; +}; \ No newline at end of file diff --git a/packages/element/src/distance.ts b/packages/element/src/distance.ts index d261faf7d..f6f79c5fb 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -39,7 +39,11 @@ export const distanceToBindableElement = ( return distanceToDiamondElement(element, p); case "ellipse": return distanceToEllipseElement(element, p); + case "regularPolygon": + // For regularPolygon, use the same distance calculation as rectangle + return distanceToRectanguloidElement(element, p); } + return Infinity; }; /** @@ -116,4 +120,4 @@ const distanceToEllipseElement = ( pointRotateRads(p, center, -element.angle as Radians), ellipse(center, element.width / 2, element.height / 2), ); -}; +}; \ No newline at end of file diff --git a/packages/element/src/newElement.ts b/packages/element/src/newElement.ts index 53a2f05ae..355d3022b 100644 --- a/packages/element/src/newElement.ts +++ b/packages/element/src/newElement.ts @@ -46,6 +46,7 @@ import type { ExcalidrawArrowElement, FixedSegment, ExcalidrawElbowArrowElement, + ExcalidrawRegularPolygonElement, } from "./types"; export type ElementConstructorOpts = MarkOptional< @@ -471,6 +472,28 @@ export const newLinearElement = ( }; }; +export const newRegularPolygonElement = ( + opts: { + type: "regularPolygon"; + sides?: number; + } & ElementConstructorOpts, +): NonDeleted => { + // create base element + const base = _newElementBase( + "regularPolygon", + opts, + ); + // set default size if none provided + const width = opts.width ?? 100; + const height = opts.height ?? 100; + return { + ...base, + sides: opts.sides ?? 6, // default to hexagon + width, + height, + }; +}; + export const newArrowElement = ( opts: { type: ExcalidrawArrowElement["type"]; @@ -532,4 +555,4 @@ export const newImageElement = ( scale: opts.scale ?? [1, 1], crop: opts.crop ?? null, }; -}; +}; \ No newline at end of file diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index c8091e8ed..8a0f94589 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -394,6 +394,7 @@ const drawElementOnCanvas = ( appState: StaticCanvasAppState, ) => { switch (element.type) { + case "regularPolygon": case "rectangle": case "iframe": case "embeddable": @@ -806,6 +807,7 @@ export const renderElement = ( break; } + case "regularPolygon": case "rectangle": case "diamond": case "ellipse": @@ -1077,4 +1079,4 @@ function getSvgPathFromStroke(points: number[][]): string { ) .join(" ") .replace(TO_FIXED_PRECISION, "$1"); -} +} \ No newline at end of file diff --git a/packages/element/src/shapes.ts b/packages/element/src/shapes.ts index 96542c538..d6be580b1 100644 --- a/packages/element/src/shapes.ts +++ b/packages/element/src/shapes.ts @@ -40,6 +40,7 @@ import type { ExcalidrawElement, ExcalidrawLinearElement, NonDeleted, + ExcalidrawRegularPolygonElement, } from "./types"; /** @@ -95,7 +96,12 @@ export const getElementShape = ( shouldTestInside(element), ); } - } + case "regularPolygon": + return getPolygonShape(element as any); +} + +// Add this throw to fix the "no return statement" error +throw new Error(`Unsupported element type: ${(element as any).type}`); }; export const getBoundTextShape = ( @@ -282,6 +288,16 @@ export const mapIntervalToBezierT =

( ); }; +/** + * Returns geometric shape for regular polygon + */ +export const getRegularPolygonShapePoints = ( + element: ExcalidrawRegularPolygonElement, +): GeometricShape => { + // We'll use the same shape calculation as other polygon-like elements + return getPolygonShape(element as any); +}; + /** * Get the axis-aligned bounding box for a given element */ @@ -395,4 +411,4 @@ export const isPathALoop = ( return distance <= LINE_CONFIRM_THRESHOLD / zoomValue; } return false; -}; +}; \ No newline at end of file diff --git a/packages/element/src/typeChecks.ts b/packages/element/src/typeChecks.ts index 54619726d..83f6e0736 100644 --- a/packages/element/src/typeChecks.ts +++ b/packages/element/src/typeChecks.ts @@ -28,6 +28,8 @@ import type { PointBinding, FixedPointBinding, ExcalidrawFlowchartNodeElement, + ExcalidrawRegularPolygonElement, + } from "./types"; export const isInitializedImageElement = ( @@ -159,6 +161,7 @@ export const isBindableElement = ( element.type === "embeddable" || element.type === "frame" || element.type === "magicframe" || + element.type === "regularPolygon" || (element.type === "text" && !element.containerId)) ); }; @@ -175,10 +178,17 @@ export const isRectanguloidElement = ( element.type === "embeddable" || element.type === "frame" || element.type === "magicframe" || + element.type === "regularPolygon" || (element.type === "text" && !element.containerId)) ); }; +export const isRegularPolygonElement = ( + element: unknown +): element is ExcalidrawRegularPolygonElement => { + return element != null && (element as any).type === "regularPolygon"; +}; + // TODO: Remove this when proper distance calculation is introduced // @see binding.ts:distanceToBindableElement() export const isRectangularElement = ( @@ -231,7 +241,8 @@ export const isExcalidrawElement = ( case "frame": case "magicframe": case "image": - case "selection": { + case "selection": + case "regularPolygon": { return true; } default: { @@ -337,4 +348,4 @@ export const isBounds = (box: unknown): box is Bounds => typeof box[0] === "number" && typeof box[1] === "number" && typeof box[2] === "number" && - typeof box[3] === "number"; + typeof box[3] === "number"; \ No newline at end of file diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index 3b40135d5..2a8332edc 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -165,6 +165,12 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & { name: string | null; }; +export type ExcalidrawRegularPolygonElement = _ExcalidrawElementBase & { + type: "regularPolygon"; + /** Number of sides in the regular polygon */ + sides: number; +}; + export type ExcalidrawMagicFrameElement = _ExcalidrawElementBase & { type: "magicframe"; name: string | null; @@ -195,7 +201,8 @@ export type ExcalidrawRectanguloidElement = | ExcalidrawFreeDrawElement | ExcalidrawIframeLikeElement | ExcalidrawFrameLikeElement - | ExcalidrawEmbeddableElement; + | ExcalidrawEmbeddableElement + | ExcalidrawRegularPolygonElement; /** * ExcalidrawElement should be JSON serializable and (eventually) contain @@ -212,7 +219,8 @@ export type ExcalidrawElement = | ExcalidrawFrameElement | ExcalidrawMagicFrameElement | ExcalidrawIframeElement - | ExcalidrawEmbeddableElement; + | ExcalidrawEmbeddableElement + | ExcalidrawRegularPolygonElement; export type ExcalidrawNonSelectionElement = Exclude< ExcalidrawElement, @@ -264,7 +272,8 @@ export type ExcalidrawBindableElement = | ExcalidrawIframeElement | ExcalidrawEmbeddableElement | ExcalidrawFrameElement - | ExcalidrawMagicFrameElement; + | ExcalidrawMagicFrameElement + | ExcalidrawRegularPolygonElement; export type ExcalidrawTextContainer = | ExcalidrawRectangleElement @@ -411,4 +420,4 @@ export type NonDeletedSceneElementsMap = Map< export type ElementsMapOrArray = | readonly ExcalidrawElement[] - | Readonly; + | Readonly; \ No newline at end of file diff --git a/packages/excalidraw/actions/actionLinearEditor.tsx b/packages/excalidraw/actions/actionLinearEditor.tsx index 1645554bf..4708ccbff 100644 --- a/packages/excalidraw/actions/actionLinearEditor.tsx +++ b/packages/excalidraw/actions/actionLinearEditor.tsx @@ -2,8 +2,6 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks"; -import { arrayToMap } from "@excalidraw/common"; - import type { ExcalidrawLinearElement } from "@excalidraw/element/types"; import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette"; @@ -52,7 +50,7 @@ export const actionToggleLinearEditor = register({ const editingLinearElement = appState.editingLinearElement?.elementId === selectedElement.id ? null - : new LinearElementEditor(selectedElement, arrayToMap(elements)); + : new LinearElementEditor(selectedElement); return { appState: { ...appState, @@ -82,3 +80,39 @@ export const actionToggleLinearEditor = register({ ); }, }); +export const actionChangeRegularPolygonSides = register({ + name: "changeRegularPolygonSides", + label: "labels.regularPolygonSides", + trackEvent: false, + perform: (elements, appState, value) => { + return { + elements: elements.map((el) => + el.type === "regularPolygon" + ? { ...el, sides: value } + : el + ), + appState, + commitToHistory: true, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, + PanelComponent: ({ elements, updateData }) => { + const selected = elements.find((el) => el.type === "regularPolygon"); + if (!selected) return null; + // Type guard for ExcalidrawRegularPolygonElement + if (selected.type !== "regularPolygon") return null; + return ( +

+ {t("labels.regularPolygonSides") /* Add this key to your translation files, e.g., "Number of sides" */} + updateData(Number(e.target.value))} + /> + {selected.sides} +
+ ); + }, +}); \ No newline at end of file diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index c63a122e0..e5d2e07c4 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -140,7 +140,8 @@ export type ActionName = | "linkToElement" | "cropEditor" | "wrapSelectionInFrame" - | "toggleLassoTool"; + | "toggleLassoTool" + | "changeRegularPolygonSides"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; @@ -206,4 +207,4 @@ export interface Action { /** if set to `true`, allow action to be performed in viewMode. * Defaults to `false` */ viewMode?: boolean; -} +} \ No newline at end of file diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 0381a2e39..3a09b94b7 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -136,6 +136,7 @@ import { newLinearElement, newTextElement, refreshTextDimensions, + newRegularPolygonElement, } from "@excalidraw/element/newElement"; import { @@ -6622,7 +6623,8 @@ class App extends React.Component { ) { this.createFrameElementOnPointerDown( pointerDownState, - this.state.activeTool.type, + // Ensure only frame or magicframe types are passed + this.state.activeTool.type as "frame" | "magicframe", ); } else if (this.state.activeTool.type === "laser") { this.laserTrails.startPath( @@ -6630,11 +6632,12 @@ class App extends React.Component { pointerDownState.lastCoords.y, ); } else if ( - this.state.activeTool.type !== "eraser" && - this.state.activeTool.type !== "hand" + ["selection", "rectangle", "diamond", "ellipse", "embeddable", "regularPolygon", "text", "image", "lasso"].includes( + this.state.activeTool.type + ) ) { this.createGenericElementOnPointerDown( - this.state.activeTool.type, + this.state.activeTool.type as "selection" | "rectangle" | "diamond" | "ellipse" | "embeddable" | "regularPolygon", pointerDownState, ); } @@ -7820,7 +7823,10 @@ class App extends React.Component { | "diamond" | "ellipse" | "iframe" - | "embeddable", + | "embeddable" + | "regularPolygon", + specificType?: string, + simulatePressure?: boolean ) { return this.state.currentItemRoundness === "round" ? { @@ -7832,22 +7838,15 @@ class App extends React.Component { } private createGenericElementOnPointerDown = ( - elementType: ExcalidrawGenericElement["type"] | "embeddable", + elementType: ExcalidrawGenericElement["type"] | "embeddable" | "regularPolygon", pointerDownState: PointerDownState, ): void => { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] - ? null - : this.getEffectiveGridSize(), + this.getEffectiveGridSize(), ); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); - const baseElementAttributes = { x: gridX, y: gridY, @@ -7858,10 +7857,13 @@ class App extends React.Component { strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, - roundness: this.getCurrentItemRoundness(elementType), + roundness: this.state.currentItemRoundness + ? { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + } + : null, locked: false, - frameId: topLayerFrame ? topLayerFrame.id : null, - } as const; + }; let element; if (elementType === "embeddable") { @@ -7869,6 +7871,12 @@ class App extends React.Component { type: "embeddable", ...baseElementAttributes, }); + } else if (elementType === "regularPolygon") { + element = newRegularPolygonElement({ + type: "regularPolygon", + ...baseElementAttributes, + sides: 6, // Default to hexagon + }); } else { element = newElement({ type: elementType, diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx index 8b45e3377..7b315acec 100644 --- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx +++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx @@ -484,7 +484,7 @@ function CommandPaletteInner({ const command: CommandPaletteItem = { label: t(`toolBar.${value}`), category: DEFAULT_CATEGORIES.tools, - shortcut, + shortcut: shortcut === null ? undefined : String(shortcut), icon, keywords: ["toolbar"], viewMode: false, @@ -961,4 +961,4 @@ const CommandItem = ({ )} ); -}; +}; \ No newline at end of file diff --git a/packages/excalidraw/components/HelpDialog.tsx b/packages/excalidraw/components/HelpDialog.tsx index 60fc40372..8f21ac907 100644 --- a/packages/excalidraw/components/HelpDialog.tsx +++ b/packages/excalidraw/components/HelpDialog.tsx @@ -151,6 +151,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("toolBar.rectangle")} shortcuts={[KEYS.R, KEYS["2"]]} /> + void }) => { ); -}; +}; \ No newline at end of file diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx index 11a5d6b5d..2e6f13f42 100644 --- a/packages/excalidraw/components/Stats/index.tsx +++ b/packages/excalidraw/components/Stats/index.tsx @@ -296,7 +296,9 @@ export const StatsInner = memo( > {appState.croppingElementId ? t("labels.imageCropping") - : t(`element.${singleElement.type}`)} + : singleElement && singleElement.type + ? singleElement.type.charAt(0).toUpperCase() + singleElement.type.slice(1) + : ""} @@ -442,4 +444,4 @@ export const StatsInner = memo( prev.appState.croppingElementId === next.appState.croppingElementId ); }, -); +); \ No newline at end of file diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx index f3808a69d..3be8ecc5c 100644 --- a/packages/excalidraw/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -308,6 +308,17 @@ export const DiamondIcon = createIcon( tablerIconProps, ); +export const RegularPolygonIcon = createIcon( + // Hexagon points - centered in a 24x24 viewport + , + tablerIconProps, // Use tablerIconProps for consistency +); + // tabler-icons: circle export const EllipseIcon = createIcon( @@ -920,7 +931,7 @@ export const shield = createIcon( ); export const file = createIcon( - "M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm32-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z", + "M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm48-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z", { width: 384, height: 512 }, ); @@ -2235,4 +2246,4 @@ export const elementLinkIcon = createIcon( , tablerIconProps, -); +); \ No newline at end of file diff --git a/packages/excalidraw/components/shapes.tsx b/packages/excalidraw/components/shapes.tsx index 7411a9e25..c9e532802 100644 --- a/packages/excalidraw/components/shapes.tsx +++ b/packages/excalidraw/components/shapes.tsx @@ -11,6 +11,7 @@ import { TextIcon, ImageIcon, EraserIcon, + RegularPolygonIcon, } from "./icons"; export const SHAPES = [ @@ -77,6 +78,13 @@ export const SHAPES = [ numericKey: KEYS["9"], fillable: false, }, + { + icon: RegularPolygonIcon, + value: "regularPolygon", + key: null, // TODO: Assign a unique letter key if needed + numericKey: null, // Removed conflicting key + fillable: true, + }, { icon: EraserIcon, value: "eraser", @@ -97,4 +105,4 @@ export const findShapeByKey = (key: string) => { ); }); return shape?.value || null; -}; +}; \ No newline at end of file diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 1811cbb57..39967206f 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -102,6 +102,7 @@ export const AllowedExcalidrawActiveTools: Record< hand: true, laser: false, magicframe: false, + regularPolygon: true, }; export type RestoredDataState = { @@ -831,4 +832,4 @@ export const restoreLibraryItems = ( } } return restoredItems; -}; +}; \ No newline at end of file diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index 381f2b67f..83de5e5e4 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -126,6 +126,7 @@ "increaseFontSize": "Increase font size", "unbindText": "Unbind text", "bindText": "Bind text to the container", + "regularPolygonSides": "Number of sides", "createContainerFromText": "Wrap text in a container", "link": { "edit": "Edit link", @@ -283,6 +284,7 @@ "ellipse": "Ellipse", "arrow": "Arrow", "line": "Line", + "regularPolygon": "Regular Polygon", "freedraw": "Draw", "text": "Text", "library": "Library", @@ -631,4 +633,4 @@ "itemNotAvailable": "Command is not available...", "shortcutHint": "For Command palette, use {{shortcut}}" } -} +} \ No newline at end of file diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index 0d3f5bad9..15b44263b 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -150,6 +150,7 @@ const renderElementToSvg = ( // this should not happen throw new Error("Selection rendering is not supported for SVG"); } + case "regularPolygon": // <-- Add this line case "rectangle": case "diamond": case "ellipse": { @@ -754,4 +755,4 @@ export const renderSceneToSvg = ( } } }); -}; +}; \ No newline at end of file diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 12a5e27a8..40ee5cb58 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -130,14 +130,12 @@ export type ScrollBars = { y: number; width: number; height: number; - deltaMultiplier: number; } | null; vertical: { x: number; y: number; width: number; height: number; - deltaMultiplier: number; } | null; }; @@ -156,4 +154,5 @@ export type ElementShapes = { image: null; frame: null; magicframe: null; -}; + regularPolygon: Drawable; +}; \ No newline at end of file diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 5ac1bca8c..d12cf2a5c 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -17,6 +17,7 @@ import { newLinearElement, newMagicFrameElement, newTextElement, + newRegularPolygonElement, } from "@excalidraw/element/newElement"; import { isLinearElementType } from "@excalidraw/element/typeChecks"; @@ -361,10 +362,19 @@ export class API { case "magicframe": element = newMagicFrameElement({ ...base, width, height }); break; + case "regularPolygon": + element = newRegularPolygonElement({ + type: type as "regularPolygon", + ...base, + }); + break; default: + // This check ensures all TOOL_TYPE values are handled. + // If a new tool type is added without updating this switch, + // TypeScript will error here. assertNever( type, - `API.createElement: unimplemented element type ${type}}`, + `API.createElement: unimplemented element type ${type as string}}`, ); break; } diff --git a/packages/excalidraw/tests/queries/toolQueries.ts b/packages/excalidraw/tests/queries/toolQueries.ts index 8413bf5fb..bbd8ab468 100644 --- a/packages/excalidraw/tests/queries/toolQueries.ts +++ b/packages/excalidraw/tests/queries/toolQueries.ts @@ -5,7 +5,7 @@ import { TOOL_TYPE } from "@excalidraw/common"; import type { ToolType } from "@excalidraw/excalidraw/types"; const _getAllByToolName = (container: HTMLElement, tool: ToolType | "lock") => { - const toolTitle = tool === "lock" ? "lock" : TOOL_TYPE[tool]; + const toolTitle = tool === "lock" ? "lock" : TOOL_TYPE[tool as keyof typeof TOOL_TYPE]; return queries.getAllByTestId(container, `toolbar-${toolTitle}`); }; @@ -24,4 +24,4 @@ export const [ _getAllByToolName, getMultipleError, getMissingError, -); +); \ No newline at end of file diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index ebc31029c..2476e2bda 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -138,6 +138,7 @@ export type ToolType = | "selection" | "lasso" | "rectangle" + | "regularPolygon" | "diamond" | "ellipse" | "arrow" @@ -601,7 +602,6 @@ export interface ExcalidrawProps { ) => JSX.Element | null; aiEnabled?: boolean; showDeprecatedFonts?: boolean; - renderScrollbars?: boolean; } export type SceneData = { @@ -784,7 +784,6 @@ export type UnsubscribeCallback = () => void; export interface ExcalidrawImperativeAPI { updateScene: InstanceType["updateScene"]; - mutateElement: InstanceType["mutateElement"]; updateLibrary: InstanceType["updateLibrary"]; resetScene: InstanceType["resetScene"]; getSceneElementsIncludingDeleted: InstanceType< @@ -917,4 +916,4 @@ export type Offsets = Partial<{ right: number; bottom: number; left: number; -}>; +}>; \ No newline at end of file