import type { Layer, Selection } from '@/types'; import { useLayerStore } from '@/store/layer-store'; import { useSelectionStore } from '@/store/selection-store'; import { useHistoryStore } from '@/store/history-store'; import { DrawCommand } from '@/core/commands/draw-command'; import { cloneCanvas } from './canvas-utils'; /** * Copy selected pixels to clipboard (as canvas) */ export function copySelection(): HTMLCanvasElement | null { const { activeSelection } = useSelectionStore.getState(); const { activeLayerId, layers } = useLayerStore.getState(); if (!activeSelection) return null; const layer = layers.find((l) => l.id === activeLayerId); if (!layer?.canvas) return null; const { mask } = activeSelection; const { bounds } = mask; // Create a canvas for the copied pixels const copyCanvas = document.createElement('canvas'); copyCanvas.width = bounds.width; copyCanvas.height = bounds.height; const ctx = copyCanvas.getContext('2d'); if (!ctx) return null; const layerCtx = layer.canvas.getContext('2d'); if (!layerCtx) return null; const imageData = layerCtx.getImageData( bounds.x, bounds.y, bounds.width, bounds.height ); // Apply mask to image data for (let y = 0; y < bounds.height; y++) { for (let x = 0; x < bounds.width; x++) { const maskIdx = (bounds.y + y) * mask.width + (bounds.x + x); const dataIdx = (y * bounds.width + x) * 4; const alpha = mask.data[maskIdx]; if (alpha === 0) { // Clear unselected pixels imageData.data[dataIdx + 3] = 0; } else if (alpha < 255) { // Apply partial transparency for feathered edges imageData.data[dataIdx + 3] = (imageData.data[dataIdx + 3] * alpha) / 255; } } } ctx.putImageData(imageData, 0, 0); return copyCanvas; } /** * Cut selected pixels (copy and delete) */ export function cutSelection(): HTMLCanvasElement | null { const copiedCanvas = copySelection(); if (copiedCanvas) { deleteSelection(); } return copiedCanvas; } /** * Delete selected pixels */ export function deleteSelection(): void { const { activeSelection } = useSelectionStore.getState(); const { activeLayerId, layers } = useLayerStore.getState(); const { executeCommand } = useHistoryStore.getState(); if (!activeSelection) return; const layer = layers.find((l) => l.id === activeLayerId); if (!layer?.canvas) return; const ctx = layer.canvas.getContext('2d'); if (!ctx) return; // Create a draw command for undo const command = new DrawCommand(layer.id, 'Delete Selection'); const { mask } = activeSelection; // Delete pixels within selection ctx.save(); ctx.globalCompositeOperation = 'destination-out'; const imageData = ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height); for (let y = 0; y < mask.height; y++) { for (let x = 0; x < mask.width; x++) { const maskIdx = y * mask.width + x; const alpha = mask.data[maskIdx]; if (alpha > 0) { const dataIdx = (y * mask.width + x) * 4; if (alpha === 255) { imageData.data[dataIdx + 3] = 0; } else { // Reduce alpha for feathered edges const currentAlpha = imageData.data[dataIdx + 3]; imageData.data[dataIdx + 3] = currentAlpha * (1 - alpha / 255); } } } } ctx.putImageData(imageData, 0, 0); ctx.restore(); command.captureAfterState(); executeCommand(command); } /** * Paste canvas content at position */ export function pasteCanvas( canvas: HTMLCanvasElement, x: number = 0, y: number = 0 ): void { const { activeLayerId, layers } = useLayerStore.getState(); const { executeCommand } = useHistoryStore.getState(); const layer = layers.find((l) => l.id === activeLayerId); if (!layer?.canvas) return; const ctx = layer.canvas.getContext('2d'); if (!ctx) return; // Create a draw command for undo const command = new DrawCommand(layer.id, 'Paste'); ctx.drawImage(canvas, x, y); command.captureAfterState(); executeCommand(command); } /** * Fill selection with color */ export function fillSelection(color: string): void { const { activeSelection } = useSelectionStore.getState(); const { activeLayerId, layers } = useLayerStore.getState(); const { executeCommand } = useHistoryStore.getState(); if (!activeSelection) return; const layer = layers.find((l) => l.id === activeLayerId); if (!layer?.canvas) return; const ctx = layer.canvas.getContext('2d'); if (!ctx) return; // Create a draw command for undo const command = new DrawCommand(layer.id, 'Fill Selection'); const { mask } = activeSelection; const imageData = ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height); // Parse color const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d')!; tempCtx.fillStyle = color; tempCtx.fillRect(0, 0, 1, 1); const colorData = tempCtx.getImageData(0, 0, 1, 1).data; // Fill pixels within selection for (let y = 0; y < mask.height; y++) { for (let x = 0; x < mask.width; x++) { const maskIdx = y * mask.width + x; const alpha = mask.data[maskIdx]; if (alpha > 0) { const dataIdx = (y * mask.width + x) * 4; if (alpha === 255) { imageData.data[dataIdx] = colorData[0]; imageData.data[dataIdx + 1] = colorData[1]; imageData.data[dataIdx + 2] = colorData[2]; imageData.data[dataIdx + 3] = colorData[3]; } else { // Blend for feathered edges const blendAlpha = alpha / 255; imageData.data[dataIdx] = imageData.data[dataIdx] * (1 - blendAlpha) + colorData[0] * blendAlpha; imageData.data[dataIdx + 1] = imageData.data[dataIdx + 1] * (1 - blendAlpha) + colorData[1] * blendAlpha; imageData.data[dataIdx + 2] = imageData.data[dataIdx + 2] * (1 - blendAlpha) + colorData[2] * blendAlpha; } } } } ctx.putImageData(imageData, 0, 0); command.captureAfterState(); executeCommand(command); } /** * Stroke selection outline with color */ export function strokeSelection(color: string, width: number = 1): void { const { activeSelection } = useSelectionStore.getState(); const { activeLayerId, layers } = useLayerStore.getState(); const { executeCommand } = useHistoryStore.getState(); if (!activeSelection) return; const layer = layers.find((l) => l.id === activeLayerId); if (!layer?.canvas) return; const ctx = layer.canvas.getContext('2d'); if (!ctx) return; // Create a draw command for undo const command = new DrawCommand(layer.id, 'Stroke Selection'); const { mask } = activeSelection; // Find edges ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = width; for (let y = 1; y < mask.height - 1; y++) { for (let x = 1; x < mask.width - 1; x++) { const idx = y * mask.width + x; if (mask.data[idx] === 0) continue; // Check if this pixel is on the edge const isEdge = mask.data[idx - 1] === 0 || // left mask.data[idx + 1] === 0 || // right mask.data[idx - mask.width] === 0 || // top mask.data[idx + mask.width] === 0; // bottom if (isEdge) { ctx.fillRect(x, y, 1, 1); } } } ctx.restore(); command.captureAfterState(); executeCommand(command); } /** * Expand selection by pixels */ export function expandSelection(pixels: number): void { const { activeSelection } = useSelectionStore.getState(); if (!activeSelection) return; // TODO: Implement morphological dilation // This is a placeholder - full implementation would use proper dilation algorithm console.log('Expand selection by', pixels, 'pixels'); } /** * Contract selection by pixels */ export function contractSelection(pixels: number): void { const { activeSelection } = useSelectionStore.getState(); if (!activeSelection) return; // TODO: Implement morphological erosion // This is a placeholder - full implementation would use proper erosion algorithm console.log('Contract selection by', pixels, 'pixels'); }