Files
paint-ui/lib/clipboard-operations.ts

196 lines
5.3 KiB
TypeScript
Raw Permalink Normal View History

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>
2025-11-21 17:16:06 +01:00
/**
* Clipboard operations for cut/copy/paste
*/
import { useLayerStore, useCanvasStore } from '@/store';
import { useSelectionStore } from '@/store/selection-store';
import { useHistoryStore } from '@/store/history-store';
import { useToastStore } from '@/store/toast-store';
import { DrawCommand } from '@/core/commands';
import { createLayerWithHistory } from './layer-operations';
// In-memory clipboard for canvas data
let clipboardCanvas: HTMLCanvasElement | null = null;
/**
* Copy the selected region to clipboard
*/
export function copySelection(): void {
const { getActiveLayer } = useLayerStore.getState();
const { activeSelection } = useSelectionStore.getState();
const { addToast } = useToastStore.getState();
const layer = getActiveLayer();
if (!layer || !layer.canvas) {
addToast('No active layer', 'error');
return;
}
if (!activeSelection) {
addToast('No selection to copy', 'warning');
return;
}
const { mask } = activeSelection;
const { bounds, data: maskData } = mask;
const ctx = layer.canvas.getContext('2d');
if (!ctx) return;
// Create a temporary canvas for the selection
clipboardCanvas = document.createElement('canvas');
clipboardCanvas.width = bounds.width;
clipboardCanvas.height = bounds.height;
const clipCtx = clipboardCanvas.getContext('2d');
if (!clipCtx) return;
// Get the image data from the layer
const imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
// Apply mask to image data
for (let i = 0; i < maskData.length; i++) {
const alpha = maskData[i];
const pixelIndex = i * 4 + 3;
imageData.data[pixelIndex] = Math.min(imageData.data[pixelIndex], alpha);
}
// Put the masked image data into clipboard canvas
clipCtx.putImageData(imageData, 0, 0);
addToast('Selection copied', 'success');
}
/**
* Cut the selected region (copy + delete)
*/
export function cutSelection(): void {
const { getActiveLayer } = useLayerStore.getState();
const { activeSelection } = useSelectionStore.getState();
const { addToast } = useToastStore.getState();
const { executeCommand } = useHistoryStore.getState();
const layer = getActiveLayer();
if (!layer || !layer.canvas) {
addToast('No active layer', 'error');
return;
}
if (!activeSelection) {
addToast('No selection to cut', 'warning');
return;
}
// First, copy the selection
copySelection();
// Then delete the selection
const { mask } = activeSelection;
const { bounds, data: maskData } = mask;
const ctx = layer.canvas.getContext('2d');
if (!ctx) return;
// Create command for history
const command = new DrawCommand(layer.id, 'Cut');
// Get the image data
const imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
// Apply mask to delete (set alpha to 0)
for (let i = 0; i < maskData.length; i++) {
const alpha = maskData[i];
if (alpha > 0) {
const pixelIndex = i * 4 + 3;
imageData.data[pixelIndex] = 0;
}
}
// Put the modified image data back
ctx.putImageData(imageData, bounds.x, bounds.y);
// Capture after state and execute command
command.captureAfterState();
executeCommand(command);
addToast('Selection cut', 'success');
}
/**
* Paste from clipboard
*/
export async function pasteFromClipboard(): Promise<void> {
const { width, height } = useCanvasStore.getState();
const { addToast } = useToastStore.getState();
// Try to paste from browser clipboard first
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
const img = new Image();
const url = URL.createObjectURL(blob);
img.onload = () => {
// Create new layer with pasted image
createLayerWithHistory({
name: 'Pasted Layer',
width: img.width,
height: img.height,
});
// Draw the image onto the new layer
const { getActiveLayer } = useLayerStore.getState();
const layer = getActiveLayer();
if (layer && layer.canvas) {
const ctx = layer.canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0);
addToast('Image pasted', 'success');
}
}
URL.revokeObjectURL(url);
};
img.src = url;
return;
}
}
}
} catch (error) {
// Clipboard API not available or no image in clipboard
console.warn('Clipboard API failed:', error);
}
// Fallback to internal clipboard
if (clipboardCanvas) {
createLayerWithHistory({
name: 'Pasted Layer',
width: clipboardCanvas.width,
height: clipboardCanvas.height,
});
// Draw the clipboard canvas onto the new layer
const { getActiveLayer } = useLayerStore.getState();
const layer = getActiveLayer();
if (layer && layer.canvas) {
const ctx = layer.canvas.getContext('2d');
if (ctx) {
ctx.drawImage(clipboardCanvas, 0, 0);
addToast('Selection pasted', 'success');
}
}
} else {
addToast('Nothing to paste', 'warning');
}
}
/**
* Check if clipboard has content
*/
export function hasClipboardContent(): boolean {
return clipboardCanvas !== null;
}