costum shape

This commit is contained in:
hamid zarghami
2026-06-13 16:22:46 +03:30
parent e6fc300fba
commit f9113ef9c8
17 changed files with 610 additions and 3 deletions
+13 -1
View File
@@ -10,8 +10,10 @@ import { useSelectionHandlers } from "./canvas/useSelectionHandlers";
import { useTransformHandlers } from "./canvas/useTransformHandlers";
import { useObjectHandlers } from "./canvas/useObjectHandlers";
import { useMaskGroupState } from "./canvas/useMaskGroupState";
import { useCustomShapeDrawing } from "./canvas/useCustomShapeDrawing";
import ObjectsLayer from "./canvas/ObjectsLayer";
import GuidesLayer from "./canvas/GuidesLayer";
import CustomShapeDrawingLayer from "./canvas/CustomShapeDrawingLayer";
import {
clearSmartGuides,
drawSmartGuides,
@@ -97,6 +99,8 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
})();
const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers();
const { drawingState, handleStageClick: handleCustomShapeClick, handleStageMouseMove: handleCustomShapeMouseMove } =
useCustomShapeDrawing();
useKeyboardMovement();
const { handleSelect, handleCellClick, handleCellDblClick } = useSelectionHandlers();
@@ -371,6 +375,11 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
handleStageMouseDown(e);
};
const handleCombinedMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
handleStageMouseMove(e);
handleCustomShapeMouseMove(e);
};
const scaledW = stageSize.width * finalScale;
const scaledH = stageSize.height * finalScale;
@@ -502,9 +511,11 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
height={stageSize.height * finalScale}
scaleX={finalScale}
scaleY={finalScale}
style={{ cursor: tool === "custom-shape" ? "crosshair" : undefined }}
onMouseDown={handleStageMouseDownWithGuideReset}
onMouseMove={handleStageMouseMove}
onMouseMove={handleCombinedMouseMove}
onMouseUp={handleStageMouseUp}
onClick={handleCustomShapeClick}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
>
@@ -563,6 +574,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
onGuideDragEnd={handleGuideDragEnd}
/>
<Layer ref={smartGuidesLayerRef} listening={false} visible={false} />
<CustomShapeDrawingLayer drawingState={drawingState} />
</Stage>
</div>
</div>
@@ -16,6 +16,7 @@ import {
Layer,
MouseSquare,
Sticker,
Polygon,
} from 'iconsax-react'
import { useState } from 'react'
import { useEditorStore } from '../store/editorStore'
@@ -63,6 +64,8 @@ const LayersList = () => {
return <MouseSquare size={20} color="black" />
case 'sticker':
return <Sticker size={20} color="black" />
case 'custom-shape':
return <Polygon size={20} color="black" />
default:
return <Shapes size={20} color="black" />
}
@@ -112,6 +115,8 @@ const LayersList = () => {
return 'انتخاب'
case 'sticker':
return 'استیکر'
case 'custom-shape':
return 'شکل سفارشی'
default:
return 'لایه'
}
@@ -0,0 +1,83 @@
import { Layer, Line, Circle } from "react-konva";
import type { CustomShapeDrawingState } from "./useCustomShapeDrawing";
const CLOSE_THRESHOLD = 12;
const ANCHOR_RADIUS = 5;
const FIRST_ANCHOR_COLOR = "#ef4444"; // red — marks the closing vertex
const ANCHOR_COLOR = "#3b82f6";
const PREVIEW_LINE_COLOR = "#3b82f6";
const CLOSE_HINT_FILL = "rgba(239, 68, 68, 0.15)";
type CustomShapeDrawingLayerProps = {
drawingState: CustomShapeDrawingState;
};
/**
* Konva Layer rendered on top of all objects while the user is drawing a custom polygon.
* Shows:
* • A dashed preview line through all placed vertices + the current cursor position
* • Circular anchor handles at each vertex (red for the first, blue for the rest)
* • A translucent red ring around the first vertex when the cursor is close enough to close the shape
*
* This layer has `listening={false}` so it never intercepts mouse events.
*/
const CustomShapeDrawingLayer = ({ drawingState }: CustomShapeDrawingLayerProps) => {
const { isActive, points, previewPoint } = drawingState;
if (!isActive || points.length === 0) return null;
// Build the flat points array for the preview line
const allDisplayPoints = previewPoint ? [...points, previewPoint] : points;
const linePoints = allDisplayPoints.flatMap((p) => [p.x, p.y]);
const showCloseHint =
previewPoint !== null &&
points.length >= 3 &&
Math.hypot(previewPoint.x - points[0].x, previewPoint.y - points[0].y) <=
CLOSE_THRESHOLD;
return (
<Layer listening={false}>
{/* Dashed preview line */}
<Line
points={linePoints}
stroke={PREVIEW_LINE_COLOR}
strokeWidth={2}
dash={[6, 3]}
perfectDrawEnabled={false}
lineCap="round"
lineJoin="round"
/>
{/* Vertex anchor dots */}
{points.map((p, i) => (
<Circle
key={i}
x={p.x}
y={p.y}
radius={ANCHOR_RADIUS}
fill={i === 0 ? FIRST_ANCHOR_COLOR : ANCHOR_COLOR}
stroke="white"
strokeWidth={1.5}
perfectDrawEnabled={false}
/>
))}
{/* Close-shape hint: translucent ring on first vertex */}
{showCloseHint && (
<Circle
x={points[0].x}
y={points[0].y}
radius={CLOSE_THRESHOLD}
stroke={FIRST_ANCHOR_COLOR}
strokeWidth={1.5}
fill={CLOSE_HINT_FILL}
dash={[4, 2]}
perfectDrawEnabled={false}
/>
)}
</Layer>
);
};
export default CustomShapeDrawingLayer;
@@ -14,6 +14,7 @@ import {
LinkShape,
VideoShape,
AudioShape,
CustomShape,
} from "../tools";
import EditorTable from "@/components/EditorTable";
@@ -257,6 +258,9 @@ const ObjectRenderer = ({
/>
);
break;
case "custom-shape":
shapeElement = <CustomShape key={obj.id} {...commonProps} />;
break;
case "group":
// Render group as a simple selection box
// Objects inside group are rendered separately in EditorCanvas
@@ -0,0 +1,224 @@
import { useState, useEffect, useCallback, useRef } from "react";
import Konva from "konva";
import { useEditorStore } from "@/pages/editor/store/editorStore";
import type { EditorObject } from "@/pages/editor/store/editorStore";
// --- Constants ---
/** Radius (in canvas units, before zoom) within which clicking snaps to / closes on the first vertex. */
const CLOSE_THRESHOLD = 12;
/** Minimum number of vertices required to close a shape. */
const MIN_POINTS = 3;
// --- Types ---
/** A 2-D point in canvas (stage-local) coordinates. */
export type DrawPoint = { x: number; y: number };
/** Reactive state consumed by CustomShapeDrawingLayer for rendering the in-progress polygon. */
export type CustomShapeDrawingState = {
isActive: boolean;
points: DrawPoint[];
previewPoint: DrawPoint | null;
};
const INITIAL_STATE: CustomShapeDrawingState = {
isActive: false,
points: [],
previewPoint: null,
};
// --- Helpers ---
const getCanvasPos = (stage: Konva.Stage): DrawPoint | null => {
const pos = stage.getPointerPosition();
if (!pos) return null;
return {
x: pos.x / stage.scaleX(),
y: pos.y / stage.scaleY(),
};
};
const isNearPoint = (a: DrawPoint, b: DrawPoint, threshold: number): boolean => {
const dx = a.x - b.x;
const dy = a.y - b.y;
return dx * dx + dy * dy <= threshold * threshold;
};
/**
* Converts absolute canvas points into an EditorObject.
* The bounding-box top-left becomes (obj.x, obj.y); all points are stored relative to it.
* width/height reflect the axis-aligned bounding box so the transformer can display correct handles.
*/
const buildCustomShapeObject = (points: DrawPoint[]): EditorObject => {
const xs = points.map((p) => p.x);
const ys = points.map((p) => p.y);
const minX = Math.min(...xs);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const maxY = Math.max(...ys);
// Flat [x1-minX, y1-minY, x2-minX, y2-minY, …] — Konva.Line.points format
const normalizedPoints = points.flatMap((p) => [p.x - minX, p.y - minY]);
return {
id: `custom-shape-${Date.now()}`,
type: "custom-shape",
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
points: normalizedPoints,
closed: true,
fill: "#3b82f6",
fillType: "solid",
stroke: "#1e40af",
strokeWidth: 2,
opacity: 1,
rotation: 0,
};
};
// --- Hook ---
/**
* Manages the click-by-click custom polygon drawing state machine.
*
* Usage in EditorCanvas:
* - Attach handleStageClick → Stage onClick
* - Attach handleStageMouseMove → Stage onMouseMove
* - Render <CustomShapeDrawingLayer drawingState={drawingState} />
*
* The hook integrates with the Zustand editor store (addObject, undo history, tool switch)
* and cleans up its own keyboard listener on unmount / tool change.
*/
export const useCustomShapeDrawing = () => {
// Use a ref for the mutable points array so event callbacks always see the latest
// value without needing to be recreated on every state update.
const pointsRef = useRef<DrawPoint[]>([]);
const [drawingState, setDrawingState] = useState<CustomShapeDrawingState>(INITIAL_STATE);
const { tool, addObject, setSelectedObjectId, setTool } = useEditorStore();
const reset = useCallback(() => {
pointsRef.current = [];
setDrawingState(INITIAL_STATE);
}, []);
// Cancel drawing when user switches away from the custom-shape tool
useEffect(() => {
if (tool !== "custom-shape") {
reset();
}
}, [tool, reset]);
// Escape key cancels in-progress drawing
useEffect(() => {
if (tool !== "custom-shape") return;
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
return;
}
if (e.key === "Escape") {
e.preventDefault();
reset();
setTool("select");
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [tool, reset, setTool]);
/** Finalise the polygon, persist it to the store, and switch to select tool. */
const commitShape = useCallback(() => {
const points = pointsRef.current;
if (points.length < MIN_POINTS) return;
const obj = buildCustomShapeObject(points);
useEditorStore.getState().commitObjectHistoryBeforeChange();
addObject(obj);
setSelectedObjectId(obj.id);
setTool("select");
reset();
}, [addObject, setSelectedObjectId, setTool, reset]);
/**
* Stage click handler.
*
* Only acts when tool === "custom-shape" and the click target is the Stage itself
* (not an existing shape). Uses MouseEvent.detail to distinguish single vs double clicks:
* - detail === 1 → single click → add vertex or close shape if near first vertex
* - detail >= 2 → second click of a double-click → close the shape immediately
*/
const handleStageClick = useCallback(
(e: Konva.KonvaEventObject<MouseEvent>) => {
if (tool !== "custom-shape") return;
// Only act on bare-stage clicks; ignore clicks that land on existing objects
const stage = e.target.getStage();
if (!stage || e.target !== stage) return;
const pos = getCanvasPos(stage);
if (!pos) return;
// Double-click (detail >= 2): close the shape without adding another vertex
if (e.evt.detail >= 2) {
if (pointsRef.current.length >= MIN_POINTS) {
commitShape();
}
return;
}
const points = pointsRef.current;
// Snap-to-close: if cursor is within threshold of the first vertex, close the shape
if (
points.length >= MIN_POINTS &&
isNearPoint(pos, points[0], CLOSE_THRESHOLD)
) {
commitShape();
return;
}
// Add vertex
pointsRef.current = [...points, pos];
setDrawingState({
isActive: true,
points: pointsRef.current,
previewPoint: pos,
});
},
[tool, commitShape],
);
/** Updates the live preview segment as the mouse moves across the canvas. */
const handleStageMouseMove = useCallback(
(e: Konva.KonvaEventObject<MouseEvent>) => {
if (tool !== "custom-shape" || !drawingState.isActive) return;
const stage = e.target.getStage();
if (!stage) return;
const pos = getCanvasPos(stage);
if (!pos) return;
setDrawingState((prev) => ({ ...prev, previewPoint: pos }));
},
[tool, drawingState.isActive],
);
return {
drawingState,
handleStageClick,
handleStageMouseMove,
};
};
@@ -27,6 +27,9 @@ export const useDrawingHandlers = () => {
};
const handleStageMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
// custom-shape drawing is fully managed by useCustomShapeDrawing
if (tool === "custom-shape") return;
const stage = e.target.getStage();
if (!stage) return;
@@ -95,7 +98,7 @@ export const useDrawingHandlers = () => {
};
const handleStageMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (!isDrawing || tool === "select" || tool === "text") return;
if (!isDrawing || tool === "select" || tool === "text" || tool === "custom-shape") return;
const stage = e.target.getStage();
if (!stage) return;
@@ -481,6 +481,29 @@ export const useTransformHandlers = (
: undefined,
});
}
} else if (selectedObject.type === "custom-shape") {
// Scale each vertex in-place; no width/height anchor needed
const oldPoints = selectedObject.points ?? [];
const newPoints = oldPoints.map((val, i) =>
i % 2 === 0 ? val * scaleX : val * scaleY,
);
// Recompute axis-aligned bounding box from the scaled points
const xs = newPoints.filter((_, i) => i % 2 === 0);
const ys = newPoints.filter((_, i) => i % 2 !== 0);
const newWidth =
xs.length > 0 ? Math.max(...xs) - Math.min(...xs) : 0;
const newHeight =
ys.length > 0 ? Math.max(...ys) - Math.min(...ys) : 0;
updateObject(selectedObject.id, {
x: node.x(),
y: node.y(),
rotation: node.rotation(),
points: newPoints,
width: Math.max(1, newWidth),
height: Math.max(1, newHeight),
});
} else {
// Default for other objects (image, etc.)
updateObject(selectedObject.id, {
@@ -4,6 +4,7 @@ import TextInstruction from "./instructions/TextInstruction";
import {
AlignmentSettings,
AudioSettings,
CustomShapeSettings,
EntranceAnimationSettings,
GridSettings,
LineSettings,
@@ -97,6 +98,10 @@ const ObjectSettings = ({ selectedObject, pageWidth, pageHeight, onUpdate, onDel
{roundedSelectedObject.type === "grid" && <GridSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />}
{roundedSelectedObject.type === "custom-shape" && (
<CustomShapeSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
)}
<EntranceAnimationSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
<TransformSettings selectedObject={roundedSelectedObject} onUpdate={handleRoundedUpdate} />
@@ -11,6 +11,7 @@ import {
ArrowInstruction,
StickerInstruction,
GridInstruction,
CustomShapeInstruction,
} from "./instructions";
type ToolInstructionsProps = {
@@ -42,6 +43,7 @@ const ToolInstructions = ({ tool }: ToolInstructionsProps) => {
arrow: <ArrowInstruction />,
sticker: <StickerInstruction />,
grid: <GridInstruction />,
"custom-shape": <CustomShapeInstruction />,
};
return simpleInstructions[tool] ?? null;
@@ -11,6 +11,7 @@ import {
Music,
Minus,
Link2,
Magicpen,
} from "iconsax-react";
import type { ToolType } from "../../store/editorStore";
@@ -26,6 +27,7 @@ const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broke
{ icon: (color, variant) => <ArrowLeft size={20} color={color} variant={variant} />, tool: "arrow", label: "پیکان" },
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
{ icon: (color, variant) => <Magicpen size={20} color={color} variant={variant} />, tool: "custom-shape", label: "شکل سفارشی" },
];
type ToolsBarProps = {
@@ -0,0 +1,13 @@
const CustomShapeInstruction = () => (
<div className="space-y-2 text-sm text-gray-600">
<p className="font-medium text-gray-700">رسم شکل سفارشی</p>
<ul className="space-y-1">
<li> روی کانوس کلیک کنید تا نقاط اضافه شود</li>
<li> روی نقطه اول کلیک کنید تا شکل بسته شود</li>
<li> دوبار کلیک کنید تا شکل تکمیل شود</li>
<li> Escape را بزنید تا لغو شود</li>
</ul>
</div>
);
export default CustomShapeInstruction;
@@ -12,3 +12,4 @@ export { default as LineInstruction } from "./LineInstruction";
export { default as ArrowInstruction } from "./ArrowInstruction";
export { default as StickerInstruction } from "./StickerInstruction";
export { default as GridInstruction } from "./GridInstruction";
export { default as CustomShapeInstruction } from "./CustomShapeInstruction";
@@ -0,0 +1,137 @@
import ColorPicker from "@/components/ColorPicker";
import Input from "@/components/Input";
import type { EditorObject } from "@/pages/editor/store/editorStore";
type CustomShapeSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
/**
* Sidebar settings panel for a completed custom-shape polygon.
* Supports solid fill, gradient fill, stroke colour, and stroke width.
* Mirrors the visual and UX conventions of ShapeSettings.
*/
const CustomShapeSettings = ({ selectedObject, onUpdate }: CustomShapeSettingsProps) => {
const fill = selectedObject.fill ?? "#3b82f6";
const fillType = selectedObject.fillType ?? "solid";
const gradient = selectedObject.gradient ?? {
from: "#3b82f6",
to: "#1d4ed8",
angle: 135,
};
const stroke = selectedObject.stroke ?? "#1e40af";
const strokeWidth = selectedObject.strokeWidth ?? 2;
return (
<div className="space-y-4">
<ColorPicker
label="رنگ پر"
value={fill}
readOnly={fillType === "gradient"}
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
/>
<div className="space-y-2">
<label className="text-sm">نوع Fill</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => onUpdate(selectedObject.id, { fillType: "solid" })}
className={`px-3 py-1.5 rounded-lg border text-xs ${
fillType === "solid"
? "border-gray-800 text-gray-900"
: "border-border text-gray-500"
}`}
>
رنگ ساده
</button>
<button
type="button"
onClick={() =>
onUpdate(selectedObject.id, {
fillType: "gradient",
gradient: selectedObject.gradient ?? gradient,
})
}
className={`px-3 py-1.5 rounded-lg border text-xs ${
fillType === "gradient"
? "border-gray-800 text-gray-900"
: "border-border text-gray-500"
}`}
>
گرادیانت
</button>
</div>
</div>
{fillType === "gradient" && (
<div className="space-y-3 border border-border rounded-xl p-3">
<ColorPicker
label="رنگ شروع"
value={gradient.from}
onChange={(value) =>
onUpdate(selectedObject.id, { gradient: { ...gradient, from: value } })
}
/>
<ColorPicker
label="رنگ پایان"
value={gradient.to}
onChange={(value) =>
onUpdate(selectedObject.id, { gradient: { ...gradient, to: value } })
}
/>
<div className="space-y-1">
<label className="text-sm">زاویه گرادیانت</label>
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={360}
value={gradient.angle}
onChange={(e) =>
onUpdate(selectedObject.id, {
gradient: { ...gradient, angle: Number(e.target.value) },
})
}
className="w-full"
/>
<input
type="number"
min={0}
max={360}
value={gradient.angle}
onChange={(e) =>
onUpdate(selectedObject.id, {
gradient: { ...gradient, angle: Number(e.target.value) || 0 },
})
}
className="w-16 h-9 rounded-lg border border-border px-2 text-xs"
/>
</div>
</div>
</div>
)}
<ColorPicker
label="رنگ خط"
value={stroke}
onChange={(value) => onUpdate(selectedObject.id, { stroke: value })}
/>
<Input
label="ضخامت خط"
type="number"
value={strokeWidth}
min={0}
onChange={(e) =>
onUpdate(selectedObject.id, {
strokeWidth: parseInt(e.target.value) || 0,
})
}
/>
</div>
);
};
export default CustomShapeSettings;
@@ -1,4 +1,5 @@
export { default as TextSettings } from "./TextSettings";
export { default as CustomShapeSettings } from "./CustomShapeSettings";
export { default as ShapeSettings } from "./ShapeSettings";
export { default as LineSettings } from "./LineSettings";
export { default as SizeSettings } from "./SizeSettings";
@@ -0,0 +1,83 @@
import { useRef } from "react";
import { Line } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
/**
* Renders a completed custom polygon shape using Konva.Line with closed=true.
*
* Points are stored as a flat array [x1,y1, x2,y2, …] relative to obj.x/obj.y,
* matching Konva's native Line.points format. This design leaves room for future
* Bézier curve support (by adding a separate controlPoints field) and path editing
* (by rendering individual vertex handles when in edit mode).
*/
const CustomShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Line>(null);
const points = obj.points ?? [];
// Need at least 3 vertices (6 values) to render a polygon
if (points.length < 6) return null;
const actualStrokeWidth = obj.strokeWidth ?? 2;
const hasStroke = actualStrokeWidth > 0;
const displayStroke = isSelected
? (hasStroke ? obj.stroke : "#3b82f6")
: (hasStroke ? obj.stroke : undefined);
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 2)
: actualStrokeWidth;
// Derive bounding box width/height from points for gradient calculation
const xs = points.filter((_, i) => i % 2 === 0);
const ys = points.filter((_, i) => i % 2 !== 0);
const bboxW = xs.length > 0 ? Math.max(...xs) - Math.min(...xs) : 100;
const bboxH = ys.length > 0 ? Math.max(...ys) - Math.min(...ys) : 100;
const gradientProps = getKonvaGradientProps(
obj.fillType,
obj.gradient,
bboxW,
bboxH,
"rect",
);
return (
<Line
ref={shapeRef}
id={obj.id}
name="canvas-object"
x={obj.x}
y={obj.y}
points={points}
closed={obj.closed ?? true}
fill={obj.fill ?? "#3b82f6"}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
perfectDrawEnabled={false}
rotation={obj.rotation ?? 0}
opacity={obj.opacity ?? 1}
draggable={draggable}
onClick={(e) => {
if (shapeRef.current) {
onSelect(obj.id, shapeRef.current, e);
}
}}
onDragMove={(e) => {
e.target.getLayer()?.batchDraw();
}}
onDragEnd={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x(),
y: node.y(),
});
}}
/>
);
};
export default CustomShape;
@@ -1,4 +1,5 @@
export { default as RectangleShape } from "./RectangleShape";
export { default as CustomShape } from "./CustomShape";
export { default as CircleShape } from "./CircleShape";
export { default as TriangleShape } from "./TriangleShape";
export { default as AbstractShape } from "./AbstractShape";
+9 -1
View File
@@ -25,7 +25,8 @@ export type ToolType =
| "audio"
| "link"
| "grid"
| "group";
| "group"
| "custom-shape";
export type TableCell = {
id: string;
@@ -91,6 +92,13 @@ export type EditorObject = {
entranceDurationMs?: number;
/** تأخیر قبل از شروع انیمیشن ورود به میلی‌ثانیه (پیش‌فرض ۰) */
entranceDelayMs?: number;
/**
* نقاط شکل چندضلعی سفارشی — آرایه تخت [x1,y1, x2,y2, …] نسبت به obj.x / obj.y.
* همان فرمت Konva.Line.points؛ برای افزودن Bézier در آینده می‌توان controlPoints جداگانه اضافه کرد.
*/
points?: number[];
/** آیا شکل چندضلعی بسته است (پیش‌فرض true برای custom-shape کامل‌شده) */
closed?: boolean;
};
export type PageGuide = {