196 lines
5.3 KiB
TypeScript
196 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|