mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: add support for regular polygon shape with customizable sides
This commit is contained in:
parent
2a0d15799c
commit
c8d38e87b0
24 changed files with 329 additions and 67 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<ExcalidrawRegularPolygonElement> => {
|
||||
// create base element
|
||||
const base = _newElementBase<ExcalidrawRegularPolygonElement>(
|
||||
"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 = <T extends boolean>(
|
||||
opts: {
|
||||
type: ExcalidrawArrowElement["type"];
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -40,6 +40,7 @@ import type {
|
|||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
NonDeleted,
|
||||
ExcalidrawRegularPolygonElement,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
@ -95,7 +96,12 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
|||
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 = <Point extends GlobalPoint | LocalPoint>(
|
||||
|
@ -282,6 +288,16 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns geometric shape for regular polygon
|
||||
*/
|
||||
export const getRegularPolygonShapePoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawRegularPolygonElement,
|
||||
): GeometricShape<Point> => {
|
||||
// We'll use the same shape calculation as other polygon-like elements
|
||||
return getPolygonShape<Point>(element as any);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the axis-aligned bounding box for a given element
|
||||
*/
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<fieldset>
|
||||
<legend>{t("labels.regularPolygonSides") /* Add this key to your translation files, e.g., "Number of sides" */}</legend>
|
||||
<input
|
||||
type="range"
|
||||
min={3}
|
||||
max={12}
|
||||
value={selected.sides}
|
||||
onChange={(e) => updateData(Number(e.target.value))}
|
||||
/>
|
||||
<span>{selected.sides}</span>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -140,7 +140,8 @@ export type ActionName =
|
|||
| "linkToElement"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool";
|
||||
| "toggleLassoTool"
|
||||
| "changeRegularPolygonSides";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
|
|
@ -136,6 +136,7 @@ import {
|
|||
newLinearElement,
|
||||
newTextElement,
|
||||
refreshTextDimensions,
|
||||
newRegularPolygonElement,
|
||||
} from "@excalidraw/element/newElement";
|
||||
|
||||
import {
|
||||
|
@ -6622,7 +6623,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
) {
|
||||
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<AppProps, AppState> {
|
|||
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<AppProps, AppState> {
|
|||
| "diamond"
|
||||
| "ellipse"
|
||||
| "iframe"
|
||||
| "embeddable",
|
||||
| "embeddable"
|
||||
| "regularPolygon",
|
||||
specificType?: string,
|
||||
simulatePressure?: boolean
|
||||
) {
|
||||
return this.state.currentItemRoundness === "round"
|
||||
? {
|
||||
|
@ -7832,22 +7838,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
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<AppProps, AppState> {
|
|||
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<AppProps, AppState> {
|
|||
type: "embeddable",
|
||||
...baseElementAttributes,
|
||||
});
|
||||
} else if (elementType === "regularPolygon") {
|
||||
element = newRegularPolygonElement({
|
||||
type: "regularPolygon",
|
||||
...baseElementAttributes,
|
||||
sides: 6, // Default to hexagon
|
||||
});
|
||||
} else {
|
||||
element = newElement({
|
||||
type: elementType,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -151,6 +151,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("toolBar.rectangle")}
|
||||
shortcuts={[KEYS.R, KEYS["2"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.regularPolygon")}
|
||||
shortcuts={["6"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.diamond")}
|
||||
shortcuts={[KEYS.D, KEYS["3"]]}
|
||||
|
|
|
@ -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)
|
||||
: ""}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow>
|
||||
|
|
|
@ -308,6 +308,17 @@ export const DiamondIcon = createIcon(
|
|||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const RegularPolygonIcon = createIcon(
|
||||
// Hexagon points - centered in a 24x24 viewport
|
||||
<polygon
|
||||
points="12,3 20.8,7.5 20.8,16.5 12,21 3.2,16.5 3.2,7.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
/>,
|
||||
tablerIconProps, // Use tablerIconProps for consistency
|
||||
);
|
||||
|
||||
// tabler-icons: circle
|
||||
export const EllipseIcon = createIcon(
|
||||
<g strokeWidth="1.5">
|
||||
|
@ -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 },
|
||||
);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -102,6 +102,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||
hand: true,
|
||||
laser: false,
|
||||
magicframe: false,
|
||||
regularPolygon: true,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
};
|
||||
|
||||
|
|
|
@ -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<typeof App>["updateScene"];
|
||||
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||
resetScene: InstanceType<typeof App>["resetScene"];
|
||||
getSceneElementsIncludingDeleted: InstanceType<
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue