324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
|
import type { Sprite, Layer } from '@/types/sprites';
|
|
import { getMaxDimensions } from './useSprites';
|
|
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
|
|
|
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<Sprite[]> | ComputedRef<Sprite[]> | Sprite[];
|
|
layers?: Ref<Layer[]> | ComputedRef<Layer[]> | Layer[];
|
|
columns: Ref<number> | number;
|
|
zoom?: Ref<number>;
|
|
allowCellSwap?: Ref<boolean>;
|
|
negativeSpacingEnabled?: Ref<boolean>;
|
|
manualCellSizeEnabled?: Ref<boolean>;
|
|
manualCellWidth?: Ref<number>;
|
|
manualCellHeight?: Ref<number>;
|
|
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<string | null>(null);
|
|
const activeSpriteCellIndex = ref<number | null>(null);
|
|
const dragStartX = ref(0);
|
|
const dragStartY = ref(0);
|
|
const dragOffsetX = ref(0);
|
|
const dragOffsetY = ref(0);
|
|
const currentHoverCell = ref<CellPosition | null>(null);
|
|
|
|
// Visual feedback
|
|
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
|
const highlightCell = ref<CellPosition | null>(null);
|
|
|
|
// Cache for max dimensions
|
|
const lastMaxWidth = ref(1);
|
|
const lastMaxHeight = ref(1);
|
|
|
|
const calculateMaxDimensions = () => {
|
|
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
|
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
|
|
|
// If manual cell size is enabled, use manual dimensions
|
|
if (manualCellSizeEnabled) {
|
|
const maxWidth = getManualCellWidth();
|
|
const maxHeight = getManualCellHeight();
|
|
// When manual cell size is used, negative spacing is not applied
|
|
const negativeSpacing = 0;
|
|
// Don't update lastMaxWidth/lastMaxHeight when in manual mode
|
|
return { maxWidth, maxHeight, negativeSpacing };
|
|
}
|
|
|
|
// Get all sprites to calculate dimensions from
|
|
// If layers are provided, use all visible layers; otherwise use current sprites
|
|
const layers = getLayers();
|
|
const spritesToMeasure = layers ? layers.filter(l => l.visible).flatMap(l => l.sprites) : getSprites();
|
|
|
|
// Otherwise, calculate based on sprite dimensions across all visible layers
|
|
const base = getMaxDimensions(spritesToMeasure);
|
|
// When switching back from manual mode, reset to actual sprite dimensions
|
|
const baseMaxWidth = Math.max(1, base.maxWidth);
|
|
const baseMaxHeight = Math.max(1, base.maxHeight);
|
|
lastMaxWidth.value = baseMaxWidth;
|
|
lastMaxHeight.value = baseMaxHeight;
|
|
|
|
// Calculate negative spacing using shared composable
|
|
const negativeSpacing = calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled);
|
|
|
|
// Add negative spacing to expand each cell
|
|
const maxWidth = baseMaxWidth + negativeSpacing;
|
|
const maxHeight = baseMaxHeight + negativeSpacing;
|
|
return { maxWidth, maxHeight, negativeSpacing };
|
|
};
|
|
|
|
// Computed sprite positions
|
|
const spritePositions = computed<SpritePosition[]>(() => {
|
|
const sprites = getSprites();
|
|
const columns = getColumns();
|
|
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
|
|
|
|
return sprites.map((sprite, index) => {
|
|
const col = index % columns;
|
|
const row = Math.floor(index / columns);
|
|
|
|
// With negative spacing, sprites are positioned at bottom-right of cell
|
|
// (spacing added to top and left)
|
|
return {
|
|
id: sprite.id,
|
|
canvasX: col * maxWidth + negativeSpacing + sprite.x,
|
|
canvasY: row * maxHeight + negativeSpacing + sprite.y,
|
|
cellX: col * maxWidth,
|
|
cellY: row * maxHeight,
|
|
width: sprite.width,
|
|
height: sprite.height,
|
|
maxWidth,
|
|
maxHeight,
|
|
col,
|
|
row,
|
|
index,
|
|
x: sprite.x,
|
|
y: sprite.y,
|
|
};
|
|
});
|
|
});
|
|
|
|
const findCellAtPosition = (x: number, y: number): CellPosition | null => {
|
|
const sprites = getSprites();
|
|
const columns = getColumns();
|
|
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
|
const col = Math.floor(x / maxWidth);
|
|
const row = Math.floor(y / maxHeight);
|
|
|
|
const totalRows = Math.ceil(sprites.length / columns);
|
|
if (col >= 0 && col < columns && row >= 0 && row < totalRows) {
|
|
const index = row * columns + col;
|
|
if (index < sprites.length) {
|
|
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 = cellCol * maxWidth;
|
|
const cellY = 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: pos.x - dragOffsetX.value,
|
|
y: 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,
|
|
};
|
|
}
|