Files
paint-ui/tools/shape-tool.ts
Sebastian Krüger 89a845feb3 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>
2025-11-21 02:43:15 +01:00

144 lines
3.8 KiB
TypeScript

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);
}
}