import { ref, computed, type Ref, type ComputedRef } from 'vue'; import type { Sprite, Layer } from '@/types/sprites'; import { useGridMetrics, type GridMetrics } from './useGridMetrics'; export interface CellPosition { col: number; row: number; index: number; } export interface SpritePosition { id: string; canvasX: number; canvasY: number; cellX: number; cellY: number; width: number; height: number; maxWidth: number; maxHeight: number; col: number; row: number; index: number; x: number; y: number; } export interface DragSpriteOptions { sprites: Ref | ComputedRef | Sprite[]; layers?: Ref | ComputedRef | Layer[]; columns: Ref | number; zoom?: Ref; allowCellSwap?: Ref; negativeSpacingEnabled?: Ref; manualCellSizeEnabled?: Ref; manualCellWidth?: Ref; manualCellHeight?: Ref; getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null; onUpdateSprite: (id: string, x: number, y: number) => void; onUpdateSpriteCell?: (id: string, newIndex: number) => void; onDraw: () => void; } export function useDragSprite(options: DragSpriteOptions) { const { getMousePosition, onUpdateSprite, onUpdateSpriteCell, onDraw } = options; // Helper to get reactive values const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value); const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null); const getColumns = () => (typeof options.columns === 'number' ? options.columns : options.columns.value); const getZoom = () => options.zoom?.value ?? 1; const getAllowCellSwap = () => options.allowCellSwap?.value ?? false; const getNegativeSpacingEnabled = () => options.negativeSpacingEnabled?.value ?? false; const getManualCellSizeEnabled = () => options.manualCellSizeEnabled?.value ?? false; const getManualCellWidth = () => options.manualCellWidth?.value ?? 64; const getManualCellHeight = () => options.manualCellHeight?.value ?? 64; // Drag state const isDragging = ref(false); const activeSpriteId = ref(null); const activeSpriteCellIndex = ref(null); const dragStartX = ref(0); const dragStartY = ref(0); const dragOffsetX = ref(0); const dragOffsetY = ref(0); const currentHoverCell = ref(null); // Visual feedback const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null); const highlightCell = ref(null); // Use the new useGridMetrics composable for consistent calculations const gridMetricsComposable = useGridMetrics({ layers: options.layers, sprites: options.sprites, negativeSpacingEnabled: options.negativeSpacingEnabled, manualCellSizeEnabled: options.manualCellSizeEnabled, manualCellWidth: options.manualCellWidth, manualCellHeight: options.manualCellHeight, }); const calculateMaxDimensions = (): GridMetrics => { return gridMetricsComposable.calculateCellDimensions(); }; // Computed sprite positions const spritePositions = computed(() => { const sprites = getSprites(); const columns = getColumns(); const metrics = calculateMaxDimensions(); return sprites.map((sprite, index) => { const cellPos = gridMetricsComposable.getCellPosition(index, columns, metrics); const canvasPos = gridMetricsComposable.getSpriteCanvasPosition(sprite, index, columns, metrics); return { id: sprite.id, canvasX: canvasPos.canvasX, canvasY: canvasPos.canvasY, cellX: canvasPos.cellX, cellY: canvasPos.cellY, width: Math.round(sprite.width), height: Math.round(sprite.height), maxWidth: metrics.maxWidth, maxHeight: metrics.maxHeight, col: cellPos.col, row: cellPos.row, index, x: sprite.x, y: sprite.y, }; }); }); const findCellAtPosition = (x: number, y: number): CellPosition | null => { const columns = getColumns(); const { maxWidth, maxHeight } = calculateMaxDimensions(); const col = Math.floor(x / maxWidth); const row = Math.floor(y / maxHeight); // Allow dropping anywhere in the columns, assuming infinite rows effectively if (col >= 0 && col < columns && row >= 0) { const index = row * columns + col; return { col, row, index }; } return null; }; const findSpriteAtPosition = (x: number, y: number): Sprite | null => { const sprites = getSprites(); const positions = spritePositions.value; for (let i = positions.length - 1; i >= 0; i--) { const pos = positions[i]; if (x >= pos.canvasX && x <= pos.canvasX + pos.width && y >= pos.canvasY && y <= pos.canvasY + pos.height) { return sprites.find(s => s.id === pos.id) || null; } } return null; }; const startDrag = (event: MouseEvent) => { const pos = getMousePosition(event, getZoom()); if (!pos) return; const clickedSprite = findSpriteAtPosition(pos.x, pos.y); if (clickedSprite) { isDragging.value = true; activeSpriteId.value = clickedSprite.id; dragStartX.value = pos.x; dragStartY.value = pos.y; const spritePosition = spritePositions.value.find(p => p.id === clickedSprite.id); if (spritePosition) { dragOffsetX.value = pos.x - spritePosition.canvasX; dragOffsetY.value = pos.y - spritePosition.canvasY; activeSpriteCellIndex.value = spritePosition.index; const startCell = findCellAtPosition(pos.x, pos.y); if (startCell) { currentHoverCell.value = startCell; highlightCell.value = null; } } } }; const handleInCellMovement = (mouseX: number, mouseY: number, spriteIndex: number) => { if (!activeSpriteId.value) return; const sprites = getSprites(); const columns = getColumns(); const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions(); // Use the sprite's current index in the array to calculate cell position const cellCol = spriteIndex % columns; const cellRow = Math.floor(spriteIndex / columns); const cellX = Math.round(cellCol * maxWidth); const cellY = Math.round(cellRow * maxHeight); // Calculate new position relative to cell origin (without the negative spacing offset) // The sprite's x,y is stored relative to where it would be drawn after the negativeSpacing offset const newX = mouseX - cellX - negativeSpacing - dragOffsetX.value; const newY = mouseY - cellY - negativeSpacing - dragOffsetY.value; // The sprite can move within the full expanded cell area // Allow negative values up to -negativeSpacing so sprite can fill the expanded area const constrainedX = Math.floor(Math.max(-negativeSpacing, Math.min(maxWidth - negativeSpacing - sprites[spriteIndex].width, newX))); const constrainedY = Math.floor(Math.max(-negativeSpacing, Math.min(maxHeight - negativeSpacing - sprites[spriteIndex].height, newY))); onUpdateSprite(activeSpriteId.value, constrainedX, constrainedY); onDraw(); }; const drag = (event: MouseEvent) => { if (!isDragging.value || !activeSpriteId.value || activeSpriteCellIndex.value === null) return; const pos = getMousePosition(event, getZoom()); if (!pos) return; const sprites = getSprites(); const spriteIndex = sprites.findIndex(s => s.id === activeSpriteId.value); if (spriteIndex === -1) return; const hoverCell = findCellAtPosition(pos.x, pos.y); currentHoverCell.value = hoverCell; if (getAllowCellSwap() && hoverCell) { if (hoverCell.index !== activeSpriteCellIndex.value) { highlightCell.value = hoverCell; ghostSprite.value = { id: activeSpriteId.value, x: Math.round(pos.x - dragOffsetX.value), y: Math.round(pos.y - dragOffsetY.value), }; onDraw(); } else { highlightCell.value = null; ghostSprite.value = null; handleInCellMovement(pos.x, pos.y, spriteIndex); } } else { handleInCellMovement(pos.x, pos.y, spriteIndex); } }; const stopDrag = () => { if (isDragging.value && getAllowCellSwap() && activeSpriteId.value && activeSpriteCellIndex.value !== null && currentHoverCell.value && activeSpriteCellIndex.value !== currentHoverCell.value.index) { if (onUpdateSpriteCell) { onUpdateSpriteCell(activeSpriteId.value, currentHoverCell.value.index); } onUpdateSprite(activeSpriteId.value, 0, 0); } isDragging.value = false; activeSpriteId.value = null; activeSpriteCellIndex.value = null; currentHoverCell.value = null; highlightCell.value = null; ghostSprite.value = null; onDraw(); }; // Touch event handlers const handleTouchStart = (event: TouchEvent) => { if (event.touches.length === 1) { const touch = event.touches[0]; const mouseEvent = { clientX: touch.clientX, clientY: touch.clientY, preventDefault: () => {}, } as unknown as MouseEvent; startDrag(mouseEvent); } }; const handleTouchMove = (event: TouchEvent) => { if (isDragging.value) { event.preventDefault(); } if (event.touches.length === 1) { const touch = event.touches[0]; const mouseEvent = { clientX: touch.clientX, clientY: touch.clientY, preventDefault: () => {}, } as unknown as MouseEvent; drag(mouseEvent); } }; return { // State isDragging, activeSpriteId, ghostSprite, highlightCell, spritePositions, // Methods startDrag, drag, stopDrag, handleTouchStart, handleTouchMove, findSpriteAtPosition, findCellAtPosition, calculateMaxDimensions, }; }