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:
@@ -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';
|
||||
|
||||
143
tools/shape-tool.ts
Normal file
143
tools/shape-tool.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user