feat: implement comprehensive canvas context menu system

Adds right-click context menu for canvas with full operation support:

**Clipboard Operations:**
- Cut/Copy/Paste with selection mask support
- Browser clipboard API integration for external images
- Internal clipboard buffer for canvas selections
- Toast notifications for user feedback

**Selection Operations:**
- Select All - creates full canvas selection with proper mask
- Deselect - clears active selection
- Selection state properly integrated with canvas operations

**Layer Operations:**
- New Layer - creates layer with history support
- Duplicate Layer - clones active layer
- Merge Down - merges layer with one below

**Transform Operations:**
- Rotate 90° CW - rotates active layer clockwise
- Flip Horizontal - mirrors layer horizontally
- Flip Vertical - mirrors layer vertically
- All transforms preserve image quality and support undo/redo

**Edit Operations:**
- Undo/Redo - integrated with history system
- Disabled states for unavailable operations
- Context-aware menu items

**New Files Created:**
- lib/clipboard-operations.ts - Cut/copy/paste implementation
- lib/canvas-operations.ts - Rotate/flip canvas functions

**Modified Files:**
- components/canvas/canvas-with-tools.tsx - Context menu integration
- store/selection-store.ts - Added selectAll() method
- core/commands/index.ts - Export all command types

**Technical Improvements:**
- Proper Selection type structure with mask/bounds
- History command integration for all operations
- Lazy-loaded operations for performance
- Toast feedback for all user actions
- Full TypeScript type safety

All operations work with undo/redo and maintain app state consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 17:16:06 +01:00
parent 27110f939e
commit 63a6801155
5 changed files with 534 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import { useCanvasStore, useLayerStore, useToolStore } from '@/store';
import { useHistoryStore } from '@/store/history-store';
import { useSelectionStore } from '@/store/selection-store';
import { useTextStore } from '@/store/text-store';
import { useContextMenuStore } from '@/store/context-menu-store';
import { drawMarchingAnts } from '@/lib/selection-utils';
import { getContext, drawGrid, drawCheckerboard } from '@/lib/canvas-utils';
import { renderText } from '@/lib/text-utils';
@@ -15,6 +16,18 @@ import type { BaseTool } from '@/tools';
import type { PointerState } from '@/types';
import { cn } from '@/lib/utils';
import { OnCanvasTextEditor } from './on-canvas-text-editor';
import {
Scissors,
Copy,
Clipboard,
Undo2,
Redo2,
Layers,
SquareDashedMousePointer,
RotateCw,
FlipHorizontal,
FlipVertical,
} from 'lucide-react';
export function CanvasWithTools() {
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -37,9 +50,10 @@ export function CanvasWithTools() {
const { layers, getActiveLayer } = useLayerStore();
const { activeTool, settings } = useToolStore();
const { executeCommand } = useHistoryStore();
const { activeSelection, selectionType, isMarching } = useSelectionStore();
const { executeCommand, canUndo, canRedo, undo, redo } = useHistoryStore();
const { activeSelection, selectionType, isMarching, clearSelection, selectAll } = useSelectionStore();
const { textObjects, editingTextId, isOnCanvasEditorActive } = useTextStore();
const { showContextMenu } = useContextMenuStore();
const [marchingOffset, setMarchingOffset] = useState(0);
const [isPanning, setIsPanning] = useState(false);
@@ -421,6 +435,155 @@ export function CanvasWithTools() {
}
};
// Handle context menu
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
const hasSelection = !!activeSelection;
const activeLayer = getActiveLayer();
const canMergeDown = activeLayer ? layers.findIndex((l) => l.id === activeLayer.id) < layers.length - 1 : false;
showContextMenu(e.clientX, e.clientY, [
// Clipboard operations
{
label: 'Cut',
icon: <Scissors className="h-4 w-4" />,
onClick: async () => {
const { cutSelection } = await import('@/lib/clipboard-operations');
cutSelection();
},
disabled: !hasSelection,
},
{
label: 'Copy',
icon: <Copy className="h-4 w-4" />,
onClick: async () => {
const { copySelection } = await import('@/lib/clipboard-operations');
copySelection();
},
disabled: !hasSelection,
},
{
label: 'Paste',
icon: <Clipboard className="h-4 w-4" />,
onClick: async () => {
const { pasteFromClipboard } = await import('@/lib/clipboard-operations');
await pasteFromClipboard();
},
},
{
separator: true,
label: '',
onClick: () => {},
},
// Selection operations
{
label: 'Select All',
icon: <SquareDashedMousePointer className="h-4 w-4" />,
onClick: () => selectAll(),
disabled: !activeLayer,
},
{
label: 'Deselect',
icon: <SquareDashedMousePointer className="h-4 w-4" />,
onClick: () => clearSelection(),
disabled: !hasSelection,
},
{
separator: true,
label: '',
onClick: () => {},
},
// Layer operations
{
label: 'New Layer',
icon: <Layers className="h-4 w-4" />,
onClick: async () => {
const { createLayerWithHistory } = await import('@/lib/layer-operations');
createLayerWithHistory({
name: `Layer ${layers.length + 1}`,
width,
height,
});
},
},
{
label: 'Duplicate Layer',
icon: <Copy className="h-4 w-4" />,
onClick: async () => {
if (!activeLayer) return;
const { duplicateLayerWithHistory } = await import('@/lib/layer-operations');
duplicateLayerWithHistory(activeLayer.id);
},
disabled: !activeLayer,
},
{
label: 'Merge Down',
icon: <Layers className="h-4 w-4" />,
onClick: async () => {
if (!activeLayer) return;
const { mergeLayerDownWithHistory } = await import('@/lib/layer-operations');
mergeLayerDownWithHistory(activeLayer.id);
},
disabled: !canMergeDown,
},
{
separator: true,
label: '',
onClick: () => {},
},
// Transform operations
{
label: 'Rotate 90° CW',
icon: <RotateCw className="h-4 w-4" />,
onClick: async () => {
if (!activeLayer) return;
const { rotateLayerWithHistory } = await import('@/lib/canvas-operations');
rotateLayerWithHistory(activeLayer.id, 90);
},
disabled: !activeLayer,
},
{
label: 'Flip Horizontal',
icon: <FlipHorizontal className="h-4 w-4" />,
onClick: async () => {
if (!activeLayer) return;
const { flipLayerWithHistory } = await import('@/lib/canvas-operations');
flipLayerWithHistory(activeLayer.id, 'horizontal');
},
disabled: !activeLayer,
},
{
label: 'Flip Vertical',
icon: <FlipVertical className="h-4 w-4" />,
onClick: async () => {
if (!activeLayer) return;
const { flipLayerWithHistory } = await import('@/lib/canvas-operations');
flipLayerWithHistory(activeLayer.id, 'vertical');
},
disabled: !activeLayer,
},
{
separator: true,
label: '',
onClick: () => {},
},
// Edit operations
{
label: 'Undo',
icon: <Undo2 className="h-4 w-4" />,
onClick: () => undo(),
disabled: !canUndo,
},
{
label: 'Redo',
icon: <Redo2 className="h-4 w-4" />,
onClick: () => redo(),
disabled: !canRedo,
},
]);
};
return (
<div
ref={containerRef}
@@ -434,6 +597,7 @@ export function CanvasWithTools() {
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
onContextMenu={handleContextMenu}
>
<canvas
ref={canvasRef}