diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index 4a91474..395eb10 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -40,10 +40,10 @@ @touchmove="handleTouchMove" @touchend="stopDrag" @contextmenu.prevent.stop - @dragover="handleDragOver" - @dragenter="handleDragEnter" - @dragleave="onDragLeave" - @drop="handleDrop" + @dragover="handleGridDragOver" + @dragenter="handleGridDragEnter" + @dragleave="handleGridDragLeave" + @drop="handleGridDrop" :class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }" > @@ -174,7 +174,7 @@ import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue'; import { useSettingsStore } from '@/stores/useSettingsStore'; import type { Sprite } from '@/types/sprites'; - import { useDragSprite } from '@/composables/useDragSprite'; + import { useDragSprite, type CellPosition } from '@/composables/useDragSprite'; import { useFileDrop } from '@/composables/useFileDrop'; import { useGridMetrics } from '@/composables/useGridMetrics'; import { useBackgroundStyles } from '@/composables/useBackgroundStyles'; @@ -204,6 +204,7 @@ (e: 'removeSprites', ids: string[]): void; (e: 'replaceSprite', id: string, file: File): void; (e: 'addSprite', file: File, index?: number): void; + (e: 'addSprites', files: File[], index?: number): void; (e: 'addSpriteWithResize', file: File): void; (e: 'rotateSprite', id: string, angle: number): void; (e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void; @@ -541,10 +542,50 @@ } }; - const onDragLeave = (event: DragEvent) => { + // Grid Drag & Drop + // Grid Drag & Drop + + const handleGridDragEnter = (event: DragEvent) => { + handleDragEnter(event); + }; + + const handleGridDragOver = (event: DragEvent) => { + handleDragOver(event); + }; + + const handleGridDragLeave = (event: DragEvent) => { handleDragLeave(event, gridContainerRef.value); }; + const handleGridDrop = (event: DragEvent) => { + handleDragLeave(event, gridContainerRef.value); // Reset drag over state + isDragOver.value = false; + + if (!event.dataTransfer?.files.length) return; + + // Calculate target cell immediately on drop + let targetCellIndex: number | undefined; + const pos = getMousePosition(event as unknown as MouseEvent, props.zoom); + if (pos) { + const cell = findCellAtPosition(pos.x, pos.y); + if (cell) { + targetCellIndex = cell.index; + } + } + + const files = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('image/')); + + if (files.length === 0) return; + + if (targetCellIndex !== undefined) { + // Add sprites starting from the target cell index + emit('addSprites', files, targetCellIndex); + } else { + // Fallback to default behavior (append) + emit('addSprites', files); + } + }; + onMounted(() => { document.addEventListener('mouseup', stopDrag); document.addEventListener('keydown', handleKeyDown); diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts index 0078025..6bcd40d 100644 --- a/src/composables/useLayers.ts +++ b/src/composables/useLayers.ts @@ -286,6 +286,63 @@ export const useLayers = () => { reader.readAsDataURL(file); }; + const addSprites = async (files: File[], index?: number) => { + const l = activeLayer.value; + if (!l) return; + + const promises = files.map(file => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => { + const url = e.target?.result as string; + const img = new Image(); + img.onload = () => { + resolve({ + id: crypto.randomUUID(), + file, + img, + url, + width: img.width, + height: img.height, + x: 0, + y: 0, + rotation: 0, + flipX: false, + flipY: false, + }); + }; + img.onerror = () => { + console.error('Failed to load sprite image:', file.name); + reject(new Error(`Failed to load sprite image: ${file.name}`)); + }; + img.src = url; + }; + reader.onerror = () => { + console.error('Failed to read sprite image file:', file.name); + reject(new Error(`Failed to read sprite image file: ${file.name}`)); + }; + reader.readAsDataURL(file); + }); + }); + + try { + const newSprites = await Promise.all(promises); + const currentSprites = [...l.sprites]; + + if (typeof index === 'number') { + while (currentSprites.length < index) { + currentSprites.push(createEmptySprite()); + } + currentSprites.splice(index, 0, ...newSprites); + } else { + currentSprites.push(...newSprites); + } + l.sprites = currentSprites; + } catch (error) { + console.error('Error adding sprites:', error); + } + }; + const processImageFiles = async (files: File[]) => { for (const f of files) addSprite(f); }; @@ -376,6 +433,7 @@ export const useLayers = () => { flipSprite, replaceSprite, addSprite, + addSprites, processImageFiles, alignSprites, addLayer, diff --git a/src/views/EditorView.vue b/src/views/EditorView.vue index ef02973..461f1b3 100644 --- a/src/views/EditorView.vue +++ b/src/views/EditorView.vue @@ -309,6 +309,7 @@ @flip-sprite="flipSprite" @copy-sprite-to-frame="copySpriteToFrame" @open-pixel-editor="openPixelEditor" + @add-sprites="addSprites" />
@@ -372,8 +373,29 @@ const projectStore = useProjectStore(); const { closeProject, loadProjectData } = useProjectManager(); const settingsStore = useSettingsStore(); - const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, removeSprites, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer, rotateSprite, flipSprite, copySpriteToFrame } = - useLayers(); + const { + layers, + visibleLayers, + activeLayer, + activeLayerId, + columns, + updateSpritePosition, + updateSpriteInLayer, + updateSpriteCell, + removeSprite, + removeSprites, + replaceSprite, + addSprite, + addSprites, + processImageFiles, + alignSprites, + addLayer, + removeLayer, + moveLayer, + rotateSprite, + flipSprite, + copySpriteToFrame, + } = useLayers(); const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers( layers,