diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx index 3ff21b3..e8f7129 100644 --- a/src/pages/editor/components/EditorCanvas.tsx +++ b/src/pages/editor/components/EditorCanvas.tsx @@ -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) => { + 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} /> + diff --git a/src/pages/editor/components/LayersList.tsx b/src/pages/editor/components/LayersList.tsx index 330a23c..91820be 100644 --- a/src/pages/editor/components/LayersList.tsx +++ b/src/pages/editor/components/LayersList.tsx @@ -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 case 'sticker': return + case 'custom-shape': + return default: return } @@ -112,6 +115,8 @@ const LayersList = () => { return 'انتخاب' case 'sticker': return 'استیکر' + case 'custom-shape': + return 'شکل سفارشی' default: return 'لایه' } diff --git a/src/pages/editor/components/canvas/CustomShapeDrawingLayer.tsx b/src/pages/editor/components/canvas/CustomShapeDrawingLayer.tsx new file mode 100644 index 0000000..600e563 --- /dev/null +++ b/src/pages/editor/components/canvas/CustomShapeDrawingLayer.tsx @@ -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 ( + + {/* Dashed preview line */} + + + {/* Vertex anchor dots */} + {points.map((p, i) => ( + + ))} + + {/* Close-shape hint: translucent ring on first vertex */} + {showCloseHint && ( + + )} + + ); +}; + +export default CustomShapeDrawingLayer; diff --git a/src/pages/editor/components/canvas/ObjectRenderer.tsx b/src/pages/editor/components/canvas/ObjectRenderer.tsx index d144c57..397d573 100644 --- a/src/pages/editor/components/canvas/ObjectRenderer.tsx +++ b/src/pages/editor/components/canvas/ObjectRenderer.tsx @@ -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 = ; + break; case "group": // Render group as a simple selection box // Objects inside group are rendered separately in EditorCanvas diff --git a/src/pages/editor/components/canvas/useCustomShapeDrawing.ts b/src/pages/editor/components/canvas/useCustomShapeDrawing.ts new file mode 100644 index 0000000..ac1186b --- /dev/null +++ b/src/pages/editor/components/canvas/useCustomShapeDrawing.ts @@ -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 + * + * 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([]); + + const [drawingState, setDrawingState] = useState(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) => { + 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) => { + 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, + }; +}; diff --git a/src/pages/editor/components/canvas/useDrawingHandlers.ts b/src/pages/editor/components/canvas/useDrawingHandlers.ts index 720db35..3a9de74 100644 --- a/src/pages/editor/components/canvas/useDrawingHandlers.ts +++ b/src/pages/editor/components/canvas/useDrawingHandlers.ts @@ -27,6 +27,9 @@ export const useDrawingHandlers = () => { }; const handleStageMouseDown = (e: Konva.KonvaEventObject) => { + // 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) => { - 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; diff --git a/src/pages/editor/components/canvas/useTransformHandlers.ts b/src/pages/editor/components/canvas/useTransformHandlers.ts index dcec0d2..09eab95 100644 --- a/src/pages/editor/components/canvas/useTransformHandlers.ts +++ b/src/pages/editor/components/canvas/useTransformHandlers.ts @@ -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, { diff --git a/src/pages/editor/components/sidebar/ObjectSettings.tsx b/src/pages/editor/components/sidebar/ObjectSettings.tsx index 2f6c6e1..b303a4b 100644 --- a/src/pages/editor/components/sidebar/ObjectSettings.tsx +++ b/src/pages/editor/components/sidebar/ObjectSettings.tsx @@ -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" && } + {roundedSelectedObject.type === "custom-shape" && ( + + )} + diff --git a/src/pages/editor/components/sidebar/ToolInstructions.tsx b/src/pages/editor/components/sidebar/ToolInstructions.tsx index 7bea3e1..2beefe6 100644 --- a/src/pages/editor/components/sidebar/ToolInstructions.tsx +++ b/src/pages/editor/components/sidebar/ToolInstructions.tsx @@ -11,6 +11,7 @@ import { ArrowInstruction, StickerInstruction, GridInstruction, + CustomShapeInstruction, } from "./instructions"; type ToolInstructionsProps = { @@ -42,6 +43,7 @@ const ToolInstructions = ({ tool }: ToolInstructionsProps) => { arrow: , sticker: , grid: , + "custom-shape": , }; return simpleInstructions[tool] ?? null; diff --git a/src/pages/editor/components/sidebar/ToolsBar.tsx b/src/pages/editor/components/sidebar/ToolsBar.tsx index 82cd98d..c3f3aae 100644 --- a/src/pages/editor/components/sidebar/ToolsBar.tsx +++ b/src/pages/editor/components/sidebar/ToolsBar.tsx @@ -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) => , tool: "arrow", label: "پیکان" }, { icon: (color, variant) => , tool: "line", label: "خط" }, { icon: (color, variant) => , tool: "sticker", label: "استیکر" }, + { icon: (color, variant) => , tool: "custom-shape", label: "شکل سفارشی" }, ]; type ToolsBarProps = { diff --git a/src/pages/editor/components/sidebar/instructions/CustomShapeInstruction.tsx b/src/pages/editor/components/sidebar/instructions/CustomShapeInstruction.tsx new file mode 100644 index 0000000..ba36d7b --- /dev/null +++ b/src/pages/editor/components/sidebar/instructions/CustomShapeInstruction.tsx @@ -0,0 +1,13 @@ +const CustomShapeInstruction = () => ( +
+

رسم شکل سفارشی

+
    +
  • • روی کانوس کلیک کنید تا نقاط اضافه شود
  • +
  • • روی نقطه اول کلیک کنید تا شکل بسته شود
  • +
  • • دوبار کلیک کنید تا شکل تکمیل شود
  • +
  • • Escape را بزنید تا لغو شود
  • +
+
+); + +export default CustomShapeInstruction; diff --git a/src/pages/editor/components/sidebar/instructions/index.ts b/src/pages/editor/components/sidebar/instructions/index.ts index b90fdfb..9a9d234 100644 --- a/src/pages/editor/components/sidebar/instructions/index.ts +++ b/src/pages/editor/components/sidebar/instructions/index.ts @@ -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"; diff --git a/src/pages/editor/components/sidebar/settings/CustomShapeSettings.tsx b/src/pages/editor/components/sidebar/settings/CustomShapeSettings.tsx new file mode 100644 index 0000000..ac38864 --- /dev/null +++ b/src/pages/editor/components/sidebar/settings/CustomShapeSettings.tsx @@ -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) => 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 ( +
+ onUpdate(selectedObject.id, { fill: value })} + /> + +
+ +
+ + +
+
+ + {fillType === "gradient" && ( +
+ + onUpdate(selectedObject.id, { gradient: { ...gradient, from: value } }) + } + /> + + onUpdate(selectedObject.id, { gradient: { ...gradient, to: value } }) + } + /> +
+ +
+ + onUpdate(selectedObject.id, { + gradient: { ...gradient, angle: Number(e.target.value) }, + }) + } + className="w-full" + /> + + 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" + /> +
+
+
+ )} + + onUpdate(selectedObject.id, { stroke: value })} + /> + + + onUpdate(selectedObject.id, { + strokeWidth: parseInt(e.target.value) || 0, + }) + } + /> +
+ ); +}; + +export default CustomShapeSettings; diff --git a/src/pages/editor/components/sidebar/settings/index.ts b/src/pages/editor/components/sidebar/settings/index.ts index b75ab03..d2bca31 100644 --- a/src/pages/editor/components/sidebar/settings/index.ts +++ b/src/pages/editor/components/sidebar/settings/index.ts @@ -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"; diff --git a/src/pages/editor/components/tools/CustomShape.tsx b/src/pages/editor/components/tools/CustomShape.tsx new file mode 100644 index 0000000..73ad164 --- /dev/null +++ b/src/pages/editor/components/tools/CustomShape.tsx @@ -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(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 ( + { + 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; diff --git a/src/pages/editor/components/tools/index.ts b/src/pages/editor/components/tools/index.ts index 3b2feea..4325a84 100644 --- a/src/pages/editor/components/tools/index.ts +++ b/src/pages/editor/components/tools/index.ts @@ -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"; diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts index 38ed03b..711b130 100644 --- a/src/pages/editor/store/editorStore.types.ts +++ b/src/pages/editor/store/editorStore.types.ts @@ -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 = {