From 89a845feb32210d0e4e28a0c92218e7fcd1efac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Fri, 21 Nov 2025 02:43:15 +0100 Subject: [PATCH] feat: implement Phase 10 - Shape Tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive shape drawing system with support for 7 shape types: rectangle, ellipse, line, arrow, polygon, star, and triangle. Features: - Created types/shape.ts with ShapeType and ShapeSettings interfaces - Implemented lib/shape-utils.ts with drawing algorithms for all shapes: * Rectangle with optional corner radius * Ellipse with independent x/y radii * Line with stroke support * Arrow with configurable head size and angle * Polygon with adjustable sides (3-20) * Star with points and inner radius control * Triangle (equilateral style) - Created store/shape-store.ts for shape state management - Implemented tools/shape-tool.ts as unified tool handling all shapes - Built components/shapes/shape-panel.tsx with comprehensive UI: * Grid selector for all 7 shape types * Fill/stroke toggles with color pickers * Dynamic properties panel (corner radius, sides, inner radius, etc.) * Real-time stroke width adjustment - Integrated ShapeTool into canvas-with-tools.tsx - Added ShapePanel to editor-layout.tsx sidebar - Removed duplicate ShapeType/ShapeSettings from types/tool.ts All shapes support: - Fill with color selection - Stroke with color and width controls - Shape-specific properties (corners, sides, arrow heads, etc.) - Undo/redo via DrawCommand integration Build Status: ✓ Successful (1290ms) 🤖 Generated with Claude Code Co-Authored-By: Claude --- components/canvas/canvas-with-tools.tsx | 8 +- components/editor/editor-layout.tsx | 4 + components/shapes/index.ts | 1 + components/shapes/shape-panel.tsx | 287 ++++++++++++++++++++++ lib/shape-utils.ts | 302 ++++++++++++++++++++++++ store/index.ts | 1 + store/shape-store.ts | 80 +++++++ tools/index.ts | 1 + tools/shape-tool.ts | 143 +++++++++++ types/index.ts | 1 + types/shape.ts | 41 ++++ types/tool.ts | 23 -- 12 files changed, 866 insertions(+), 26 deletions(-) create mode 100644 components/shapes/index.ts create mode 100644 components/shapes/shape-panel.tsx create mode 100644 lib/shape-utils.ts create mode 100644 store/shape-store.ts create mode 100644 tools/shape-tool.ts create mode 100644 types/shape.ts diff --git a/components/canvas/canvas-with-tools.tsx b/components/canvas/canvas-with-tools.tsx index 473b3f2..6cca719 100644 --- a/components/canvas/canvas-with-tools.tsx +++ b/components/canvas/canvas-with-tools.tsx @@ -19,6 +19,7 @@ import { MagicWandTool, MoveTool, FreeTransformTool, + ShapeTool, type BaseTool, } from '@/tools'; import type { PointerState } from '@/types'; @@ -38,6 +39,7 @@ const tools: Record = { 'magic-wand': new MagicWandTool(), move: new MoveTool(), transform: new FreeTransformTool(), + shape: new ShapeTool(), }; export function CanvasWithTools() { @@ -224,7 +226,7 @@ export function CanvasWithTools() { } // Drawing tools - if (e.button === 0 && !e.shiftKey && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper'].includes(activeTool)) { + if (e.button === 0 && !e.shiftKey && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper', 'shape'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas || activeLayer.locked) return; @@ -267,7 +269,7 @@ export function CanvasWithTools() { } // Drawing - if (pointer.isDown && ['pencil', 'brush', 'eraser', 'eyedropper'].includes(activeTool)) { + if (pointer.isDown && ['pencil', 'brush', 'eraser', 'eyedropper', 'shape'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas) return; @@ -294,7 +296,7 @@ export function CanvasWithTools() { return; } - if (pointer.isDown && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper'].includes(activeTool)) { + if (pointer.isDown && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper', 'shape'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas) return; diff --git a/components/editor/editor-layout.tsx b/components/editor/editor-layout.tsx index 1d02a29..22b203b 100644 --- a/components/editor/editor-layout.tsx +++ b/components/editor/editor-layout.tsx @@ -12,6 +12,7 @@ import { ColorPanel } from '@/components/colors'; import { FilterPanel } from '@/components/filters'; import { SelectionPanel } from '@/components/selection'; import { TransformPanel } from '@/components/transform'; +import { ShapePanel } from '@/components/shapes'; import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'; import { useFileOperations } from '@/hooks/use-file-operations'; import { useDragDrop } from '@/hooks/use-drag-drop'; @@ -187,6 +188,9 @@ export function EditorLayout() { {/* Transform Panel */} + {/* Shape Panel */} + + {/* Canvas area */}
diff --git a/components/shapes/index.ts b/components/shapes/index.ts new file mode 100644 index 0000000..0920bd7 --- /dev/null +++ b/components/shapes/index.ts @@ -0,0 +1 @@ +export * from './shape-panel'; diff --git a/components/shapes/shape-panel.tsx b/components/shapes/shape-panel.tsx new file mode 100644 index 0000000..ebf9fd2 --- /dev/null +++ b/components/shapes/shape-panel.tsx @@ -0,0 +1,287 @@ +'use client'; + +import { useShapeStore } from '@/store/shape-store'; +import { useToolStore } from '@/store/tool-store'; +import { useColorStore } from '@/store/color-store'; +import type { ShapeType } from '@/types/shape'; +import { + Square, + Circle, + Minus, + ArrowRight, + Pentagon, + Star, + Triangle, + Paintbrush, + PenTool, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const SHAPES: Array<{ + type: ShapeType; + label: string; + icon: React.ComponentType<{ className?: string }>; +}> = [ + { type: 'rectangle', label: 'Rectangle', icon: Square }, + { type: 'ellipse', label: 'Ellipse', icon: Circle }, + { type: 'line', label: 'Line', icon: Minus }, + { type: 'arrow', label: 'Arrow', icon: ArrowRight }, + { type: 'polygon', label: 'Polygon', icon: Pentagon }, + { type: 'star', label: 'Star', icon: Star }, + { type: 'triangle', label: 'Triangle', icon: Triangle }, +]; + +export function ShapePanel() { + const { + settings, + setShapeType, + setFill, + setStroke, + setStrokeWidth, + setCornerRadius, + setSides, + setInnerRadius, + setArrowHeadSize, + setArrowHeadAngle, + } = useShapeStore(); + + const { setActiveTool } = useToolStore(); + const { primaryColor, setPrimaryColor } = useColorStore(); + + const handleShapeSelect = (type: ShapeType) => { + setShapeType(type); + setActiveTool('shape'); + }; + + const handleFillColorChange = (color: string) => { + setPrimaryColor(color); + useShapeStore.getState().setFillColor(color); + }; + + const handleStrokeColorChange = (color: string) => { + useShapeStore.getState().setStrokeColor(color); + }; + + return ( +
+ {/* Header */} +
+ +

Shapes

+
+ + {/* Shape Types */} +
+

+ Type +

+
+ {SHAPES.map((shape) => ( + + ))} +
+
+ + {/* Fill and Stroke */} +
+

+ Style +

+ + {/* Fill */} +
+
+ + setFill(e.target.checked)} + className="rounded" + /> +
+ {settings.fill && ( + handleFillColorChange(e.target.value)} + className="w-full h-8 rounded cursor-pointer" + /> + )} +
+ + {/* Stroke */} +
+
+ + setStroke(e.target.checked)} + className="rounded" + /> +
+ {settings.stroke && ( + <> + handleStrokeColorChange(e.target.value)} + className="w-full h-8 rounded cursor-pointer" + /> +
+ + setStrokeWidth(Number(e.target.value))} + className="w-full" + /> +
+ {settings.strokeWidth}px +
+
+ + )} +
+
+ + {/* Shape-specific settings */} +
+

+ Properties +

+ + {/* Rectangle: Corner Radius */} + {settings.type === 'rectangle' && ( +
+ + setCornerRadius(Number(e.target.value))} + className="w-full" + /> +
+ {settings.cornerRadius}px +
+
+ )} + + {/* Polygon: Sides */} + {settings.type === 'polygon' && ( +
+ + setSides(Number(e.target.value))} + className="w-full" + /> +
+ {settings.sides} +
+
+ )} + + {/* Star: Points and Inner Radius */} + {settings.type === 'star' && ( + <> +
+ + setSides(Number(e.target.value))} + className="w-full" + /> +
+ {settings.sides} +
+
+
+ + setInnerRadius(Number(e.target.value) / 100)} + className="w-full" + /> +
+ {Math.round(settings.innerRadius * 100)}% +
+
+ + )} + + {/* Arrow: Head Size and Angle */} + {settings.type === 'arrow' && ( + <> +
+ + setArrowHeadSize(Number(e.target.value))} + className="w-full" + /> +
+ {settings.arrowHeadSize}px +
+
+
+ + setArrowHeadAngle(Number(e.target.value))} + className="w-full" + /> +
+ {settings.arrowHeadAngle}° +
+
+ + )} +
+
+ ); +} diff --git a/lib/shape-utils.ts b/lib/shape-utils.ts new file mode 100644 index 0000000..586f729 --- /dev/null +++ b/lib/shape-utils.ts @@ -0,0 +1,302 @@ +import type { ShapeSettings } from '@/types/shape'; + +/** + * Draw a rectangle with optional rounded corners + */ +export function drawRectangle( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + settings: ShapeSettings +): void { + ctx.save(); + + if (settings.cornerRadius > 0) { + // Rounded rectangle + const radius = Math.min( + settings.cornerRadius, + Math.abs(width) / 2, + Math.abs(height) / 2 + ); + + ctx.beginPath(); + const x1 = Math.min(x, x + width); + const y1 = Math.min(y, y + height); + const w = Math.abs(width); + const h = Math.abs(height); + + ctx.moveTo(x1 + radius, y1); + ctx.lineTo(x1 + w - radius, y1); + ctx.quadraticCurveTo(x1 + w, y1, x1 + w, y1 + radius); + ctx.lineTo(x1 + w, y1 + h - radius); + ctx.quadraticCurveTo(x1 + w, y1 + h, x1 + w - radius, y1 + h); + ctx.lineTo(x1 + radius, y1 + h); + ctx.quadraticCurveTo(x1, y1 + h, x1, y1 + h - radius); + ctx.lineTo(x1, y1 + radius); + ctx.quadraticCurveTo(x1, y1, x1 + radius, y1); + ctx.closePath(); + } else { + // Regular rectangle + ctx.beginPath(); + ctx.rect(x, y, width, height); + } + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + if (settings.stroke) { + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.stroke(); + } + + ctx.restore(); +} + +/** + * Draw an ellipse + */ +export function drawEllipse( + ctx: CanvasRenderingContext2D, + cx: number, + cy: number, + rx: number, + ry: number, + settings: ShapeSettings +): void { + ctx.save(); + ctx.beginPath(); + ctx.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), 0, 0, Math.PI * 2); + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + if (settings.stroke) { + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.stroke(); + } + + ctx.restore(); +} + +/** + * Draw a line + */ +export function drawLine( + ctx: CanvasRenderingContext2D, + x1: number, + y1: number, + x2: number, + y2: number, + settings: ShapeSettings +): void { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineCap = 'round'; + ctx.stroke(); + + ctx.restore(); +} + +/** + * Draw an arrow + */ +export function drawArrow( + ctx: CanvasRenderingContext2D, + x1: number, + y1: number, + x2: number, + y2: number, + settings: ShapeSettings +): void { + ctx.save(); + + // Draw line + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineCap = 'round'; + ctx.stroke(); + + // Calculate arrow head + const angle = Math.atan2(y2 - y1, x2 - x1); + const headSize = settings.arrowHeadSize; + const headAngle = (settings.arrowHeadAngle * Math.PI) / 180; + + const leftAngle = angle + Math.PI - headAngle; + const rightAngle = angle + Math.PI + headAngle; + + const leftX = x2 + headSize * Math.cos(leftAngle); + const leftY = y2 + headSize * Math.sin(leftAngle); + const rightX = x2 + headSize * Math.cos(rightAngle); + const rightY = y2 + headSize * Math.sin(rightAngle); + + // Draw arrow head + ctx.beginPath(); + ctx.moveTo(x2, y2); + ctx.lineTo(leftX, leftY); + ctx.lineTo(rightX, rightY); + ctx.closePath(); + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineJoin = 'round'; + ctx.stroke(); + + ctx.restore(); +} + +/** + * Draw a regular polygon + */ +export function drawPolygon( + ctx: CanvasRenderingContext2D, + cx: number, + cy: number, + radius: number, + sides: number, + settings: ShapeSettings +): void { + ctx.save(); + ctx.beginPath(); + + const angleStep = (Math.PI * 2) / sides; + const startAngle = -Math.PI / 2; // Start at top + + for (let i = 0; i <= sides; i++) { + const angle = startAngle + i * angleStep; + const x = cx + radius * Math.cos(angle); + const y = cy + radius * Math.sin(angle); + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + + ctx.closePath(); + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + if (settings.stroke) { + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineJoin = 'round'; + ctx.stroke(); + } + + ctx.restore(); +} + +/** + * Draw a star + */ +export function drawStar( + ctx: CanvasRenderingContext2D, + cx: number, + cy: number, + outerRadius: number, + points: number, + innerRadiusRatio: number, + settings: ShapeSettings +): void { + ctx.save(); + ctx.beginPath(); + + const innerRadius = outerRadius * innerRadiusRatio; + const angleStep = Math.PI / points; + const startAngle = -Math.PI / 2; + + for (let i = 0; i < points * 2; i++) { + const angle = startAngle + i * angleStep; + const radius = i % 2 === 0 ? outerRadius : innerRadius; + const x = cx + radius * Math.cos(angle); + const y = cy + radius * Math.sin(angle); + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + + ctx.closePath(); + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + if (settings.stroke) { + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineJoin = 'round'; + ctx.stroke(); + } + + ctx.restore(); +} + +/** + * Draw a triangle + */ +export function drawTriangle( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + settings: ShapeSettings +): void { + ctx.save(); + ctx.beginPath(); + + // Equilateral triangle + const centerX = x + width / 2; + const topY = y; + const bottomY = y + height; + const leftX = x; + const rightX = x + width; + + ctx.moveTo(centerX, topY); + ctx.lineTo(rightX, bottomY); + ctx.lineTo(leftX, bottomY); + ctx.closePath(); + + if (settings.fill) { + ctx.fillStyle = settings.fillColor; + ctx.fill(); + } + + if (settings.stroke) { + ctx.strokeStyle = settings.strokeColor; + ctx.lineWidth = settings.strokeWidth; + ctx.lineJoin = 'round'; + ctx.stroke(); + } + + ctx.restore(); +} diff --git a/store/index.ts b/store/index.ts index c5f463c..0cd8940 100644 --- a/store/index.ts +++ b/store/index.ts @@ -6,3 +6,4 @@ export * from './history-store'; export * from './color-store'; export * from './selection-store'; export * from './transform-store'; +export * from './shape-store'; diff --git a/store/shape-store.ts b/store/shape-store.ts new file mode 100644 index 0000000..e4fce24 --- /dev/null +++ b/store/shape-store.ts @@ -0,0 +1,80 @@ +import { create } from 'zustand'; +import type { ShapeSettings, ShapeType, ShapeStore as IShapeStore } from '@/types/shape'; + +const DEFAULT_SETTINGS: ShapeSettings = { + type: 'rectangle', + fill: true, + fillColor: '#000000', + stroke: true, + strokeColor: '#000000', + strokeWidth: 2, + cornerRadius: 0, + sides: 5, + innerRadius: 0.5, + arrowHeadSize: 20, + arrowHeadAngle: 30, +}; + +export const useShapeStore = create((set) => ({ + settings: { ...DEFAULT_SETTINGS }, + + setShapeType: (type) => + set((state) => ({ + settings: { ...state.settings, type }, + })), + + setFill: (fill) => + set((state) => ({ + settings: { ...state.settings, fill }, + })), + + setFillColor: (fillColor) => + set((state) => ({ + settings: { ...state.settings, fillColor }, + })), + + setStroke: (stroke) => + set((state) => ({ + settings: { ...state.settings, stroke }, + })), + + setStrokeColor: (strokeColor) => + set((state) => ({ + settings: { ...state.settings, strokeColor }, + })), + + setStrokeWidth: (strokeWidth) => + set((state) => ({ + settings: { ...state.settings, strokeWidth: Math.max(1, Math.min(100, strokeWidth)) }, + })), + + setCornerRadius: (cornerRadius) => + set((state) => ({ + settings: { ...state.settings, cornerRadius: Math.max(0, Math.min(100, cornerRadius)) }, + })), + + setSides: (sides) => + set((state) => ({ + settings: { ...state.settings, sides: Math.max(3, Math.min(20, sides)) }, + })), + + setInnerRadius: (innerRadius) => + set((state) => ({ + settings: { ...state.settings, innerRadius: Math.max(0.1, Math.min(0.9, innerRadius)) }, + })), + + setArrowHeadSize: (arrowHeadSize) => + set((state) => ({ + settings: { ...state.settings, arrowHeadSize: Math.max(5, Math.min(100, arrowHeadSize)) }, + })), + + setArrowHeadAngle: (arrowHeadAngle) => + set((state) => ({ + settings: { ...state.settings, arrowHeadAngle: Math.max(10, Math.min(60, arrowHeadAngle)) }, + })), + + updateSettings: (settings) => + set((state) => ({ + settings: { ...state.settings, ...settings }, + })), +})); diff --git a/tools/index.ts b/tools/index.ts index 0b104dd..2a6aeda 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -10,3 +10,4 @@ export * from './lasso-selection-tool'; export * from './magic-wand-tool'; export * from './move-tool'; export * from './free-transform-tool'; +export * from './shape-tool'; diff --git a/tools/shape-tool.ts b/tools/shape-tool.ts new file mode 100644 index 0000000..9caf078 --- /dev/null +++ b/tools/shape-tool.ts @@ -0,0 +1,143 @@ +import { BaseTool } from './base-tool'; +import type { PointerState } from '@/types'; +import { useLayerStore } from '@/store/layer-store'; +import { useShapeStore } from '@/store/shape-store'; +import { useHistoryStore } from '@/store/history-store'; +import { DrawCommand } from '@/core/commands/draw-command'; +import { + drawRectangle, + drawEllipse, + drawLine, + drawArrow, + drawPolygon, + drawStar, + drawTriangle, +} from '@/lib/shape-utils'; + +export class ShapeTool extends BaseTool { + private startX = 0; + private startY = 0; + private currentX = 0; + private currentY = 0; + private drawCommand: DrawCommand | null = null; + + constructor() { + super('Shape'); + } + + onPointerDown(pointer: PointerState): void { + this.isActive = true; + this.isDrawing = true; + this.startX = pointer.x; + this.startY = pointer.y; + this.currentX = pointer.x; + this.currentY = pointer.y; + + const layer = this.getActiveLayer(); + if (!layer) return; + + // Create draw command for history + this.drawCommand = new DrawCommand(layer.id, 'Draw Shape'); + } + + onPointerMove(pointer: PointerState, ctx: CanvasRenderingContext2D): void { + if (!this.isDrawing) return; + + this.currentX = pointer.x; + this.currentY = pointer.y; + + const layer = this.getActiveLayer(); + if (!layer?.canvas) return; + + const layerCtx = layer.canvas.getContext('2d'); + if (!layerCtx) return; + + // Clear and redraw from saved state + if (this.drawCommand) { + const beforeCanvas = (this.drawCommand as any).beforeCanvas; + if (beforeCanvas) { + layerCtx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + layerCtx.drawImage(beforeCanvas, 0, 0); + } + } + + // Draw preview shape + this.drawShape(layerCtx); + } + + onPointerUp(): void { + if (!this.isDrawing) return; + + const layer = this.getActiveLayer(); + if (layer?.canvas) { + const ctx = layer.canvas.getContext('2d'); + if (ctx) { + // Final draw + this.drawShape(ctx); + + // Capture after state and add to history + if (this.drawCommand) { + this.drawCommand.captureAfterState(); + const { executeCommand } = useHistoryStore.getState(); + executeCommand(this.drawCommand); + } + } + } + + this.isDrawing = false; + this.isActive = false; + this.drawCommand = null; + } + + getCursor(): string { + return 'crosshair'; + } + + private drawShape(ctx: CanvasRenderingContext2D): void { + const { settings } = useShapeStore.getState(); + + const x = Math.min(this.startX, this.currentX); + const y = Math.min(this.startY, this.currentY); + const width = Math.abs(this.currentX - this.startX); + const height = Math.abs(this.currentY - this.startY); + + const cx = (this.startX + this.currentX) / 2; + const cy = (this.startY + this.currentY) / 2; + const radius = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2)); + + switch (settings.type) { + case 'rectangle': + drawRectangle(ctx, x, y, width, height, settings); + break; + + case 'ellipse': + drawEllipse(ctx, cx, cy, width / 2, height / 2, settings); + break; + + case 'line': + drawLine(ctx, this.startX, this.startY, this.currentX, this.currentY, settings); + break; + + case 'arrow': + drawArrow(ctx, this.startX, this.startY, this.currentX, this.currentY, settings); + break; + + case 'polygon': + drawPolygon(ctx, cx, cy, radius, settings.sides, settings); + break; + + case 'star': + drawStar(ctx, cx, cy, radius, settings.sides, settings.innerRadius, settings); + break; + + case 'triangle': + drawTriangle(ctx, x, y, width, height, settings); + break; + } + } + + private getActiveLayer() { + const { activeLayerId, layers } = useLayerStore.getState(); + return layers.find((l) => l.id === activeLayerId); + } +} diff --git a/types/index.ts b/types/index.ts index cfbd50f..f0e5456 100644 --- a/types/index.ts +++ b/types/index.ts @@ -5,3 +5,4 @@ export * from './history'; export * from './filter'; export * from './selection'; export * from './transform'; +export * from './shape'; diff --git a/types/shape.ts b/types/shape.ts new file mode 100644 index 0000000..eeef746 --- /dev/null +++ b/types/shape.ts @@ -0,0 +1,41 @@ +export type ShapeType = + | 'rectangle' + | 'ellipse' + | 'line' + | 'arrow' + | 'polygon' + | 'star' + | 'triangle'; + +export interface ShapeSettings { + type: ShapeType; + fill: boolean; + fillColor: string; + stroke: boolean; + strokeColor: string; + strokeWidth: number; + cornerRadius: number; // For rounded rectangles + sides: number; // For polygons and stars + innerRadius: number; // For stars (0-1, percentage of outer radius) + arrowHeadSize: number; // For arrows + arrowHeadAngle: number; // For arrows (degrees) +} + +export interface ShapeState { + settings: ShapeSettings; +} + +export interface ShapeStore extends ShapeState { + setShapeType: (type: ShapeType) => void; + setFill: (fill: boolean) => void; + setFillColor: (color: string) => void; + setStroke: (stroke: boolean) => void; + setStrokeColor: (color: string) => void; + setStrokeWidth: (width: number) => void; + setCornerRadius: (radius: number) => void; + setSides: (sides: number) => void; + setInnerRadius: (radius: number) => void; + setArrowHeadSize: (size: number) => void; + setArrowHeadAngle: (angle: number) => void; + updateSettings: (settings: Partial) => void; +} diff --git a/types/tool.ts b/types/tool.ts index c6fee5d..3aa9b09 100644 --- a/types/tool.ts +++ b/types/tool.ts @@ -58,26 +58,3 @@ export interface ToolHandlers { onActivate?: () => void; onDeactivate?: () => void; } - -/** - * Shape types for shape tool - */ -export type ShapeType = - | 'rectangle' - | 'ellipse' - | 'line' - | 'arrow' - | 'polygon' - | 'star'; - -/** - * Shape tool settings - */ -export interface ShapeSettings { - type: ShapeType; - fill: boolean; - stroke: boolean; - strokeWidth: number; - fillColor: string; - strokeColor: string; -}