feat: implement Phase 10 - Shape Tools
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 <noreply@anthropic.com>
This commit is contained in:
1
components/shapes/index.ts
Normal file
1
components/shapes/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './shape-panel';
|
||||
287
components/shapes/shape-panel.tsx
Normal file
287
components/shapes/shape-panel.tsx
Normal file
@@ -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 (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Square className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Shapes</h2>
|
||||
</div>
|
||||
|
||||
{/* Shape Types */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
Type
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{SHAPES.map((shape) => (
|
||||
<button
|
||||
key={shape.type}
|
||||
onClick={() => handleShapeSelect(shape.type)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md p-2 text-xs transition-colors',
|
||||
settings.type === shape.type
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-accent text-foreground'
|
||||
)}
|
||||
>
|
||||
<shape.icon className="h-4 w-4" />
|
||||
<span>{shape.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fill and Stroke */}
|
||||
<div className="border-b border-border p-3 space-y-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Style
|
||||
</h3>
|
||||
|
||||
{/* Fill */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setFill(!settings.fill)}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<Paintbrush className="h-4 w-4" />
|
||||
<span>Fill</span>
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fill}
|
||||
onChange={(e) => setFill(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</div>
|
||||
{settings.fill && (
|
||||
<input
|
||||
type="color"
|
||||
value={settings.fillColor}
|
||||
onChange={(e) => handleFillColorChange(e.target.value)}
|
||||
className="w-full h-8 rounded cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stroke */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setStroke(!settings.stroke)}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<PenTool className="h-4 w-4" />
|
||||
<span>Stroke</span>
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.stroke}
|
||||
onChange={(e) => setStroke(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</div>
|
||||
{settings.stroke && (
|
||||
<>
|
||||
<input
|
||||
type="color"
|
||||
value={settings.strokeColor}
|
||||
onChange={(e) => handleStrokeColorChange(e.target.value)}
|
||||
className="w-full h-8 rounded cursor-pointer"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-muted-foreground">Width</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
value={settings.strokeWidth}
|
||||
onChange={(e) => setStrokeWidth(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.strokeWidth}px
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shape-specific settings */}
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Properties
|
||||
</h3>
|
||||
|
||||
{/* Rectangle: Corner Radius */}
|
||||
{settings.type === 'rectangle' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Corner Radius</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={settings.cornerRadius}
|
||||
onChange={(e) => setCornerRadius(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.cornerRadius}px
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Polygon: Sides */}
|
||||
{settings.type === 'polygon' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Sides</label>
|
||||
<input
|
||||
type="range"
|
||||
min="3"
|
||||
max="20"
|
||||
value={settings.sides}
|
||||
onChange={(e) => setSides(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.sides}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Star: Points and Inner Radius */}
|
||||
{settings.type === 'star' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Points</label>
|
||||
<input
|
||||
type="range"
|
||||
min="3"
|
||||
max="20"
|
||||
value={settings.sides}
|
||||
onChange={(e) => setSides(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.sides}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Inner Radius</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="90"
|
||||
step="5"
|
||||
value={settings.innerRadius * 100}
|
||||
onChange={(e) => setInnerRadius(Number(e.target.value) / 100)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{Math.round(settings.innerRadius * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Arrow: Head Size and Angle */}
|
||||
{settings.type === 'arrow' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Head Size</label>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="100"
|
||||
value={settings.arrowHeadSize}
|
||||
onChange={(e) => setArrowHeadSize(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.arrowHeadSize}px
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-foreground">Head Angle</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="60"
|
||||
value={settings.arrowHeadAngle}
|
||||
onChange={(e) => setArrowHeadAngle(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{settings.arrowHeadAngle}°
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user