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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user