import { computed, ref, watch } from 'vue'; import type { Layer, Sprite } from '@/types/sprites'; import { getMaxDimensions as getMaxDimensionsSingle, useSprites as useSpritesSingle } from './useSprites'; import { useSettingsStore } from '@/stores/useSettingsStore'; export const createEmptyLayer = (name: string): Layer => ({ id: crypto.randomUUID(), name, sprites: [], visible: true, locked: false, }); const layers = ref([createEmptyLayer('Base')]); const activeLayerId = ref(layers.value[0].id); const columns = ref(4); export const useLayers = () => { const settingsStore = useSettingsStore(); watch(columns, val => { const num = typeof val === 'number' ? val : parseInt(String(val)); const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1; if (safe !== columns.value) columns.value = safe; }); const activeLayer = computed(() => layers.value.find(l => l.id === activeLayerId.value) || layers.value[0]); const getMaxDimensions = (sprites: Sprite[]) => getMaxDimensionsSingle(sprites); const visibleLayers = computed(() => layers.value.filter(l => l.visible)); const hasSprites = computed(() => layers.value.some(l => l.sprites.length > 0)); const updateSpritePosition = (id: string, x: number, y: number) => { const l = activeLayer.value; if (!l) return; const i = l.sprites.findIndex(s => s.id === id); if (i !== -1) { l.sprites[i].x = Math.floor(x); l.sprites[i].y = Math.floor(y); } }; const updateSpriteInLayer = (layerId: string, spriteId: string, x: number, y: number) => { const l = layers.value.find(layer => layer.id === layerId); if (!l) return; const i = l.sprites.findIndex(s => s.id === spriteId); if (i !== -1) { l.sprites[i].x = Math.floor(x); l.sprites[i].y = Math.floor(y); } }; const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => { const l = activeLayer.value; if (!l || !l.sprites.length) return; // Determine the cell dimensions to align within let cellWidth: number; let cellHeight: number; if (settingsStore.manualCellSizeEnabled) { // Use manual cell size (without negative spacing) cellWidth = settingsStore.manualCellWidth; cellHeight = settingsStore.manualCellHeight; } else { // Use auto-calculated dimensions based on ALL visible layers (not just active layer) const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value); cellWidth = maxWidth; cellHeight = maxHeight; } l.sprites = l.sprites.map(sprite => { let x = sprite.x; let y = sprite.y; switch (position) { case 'left': x = 0; break; case 'center': x = Math.floor((cellWidth - sprite.width) / 2); break; case 'right': x = Math.floor(cellWidth - sprite.width); break; case 'top': y = 0; break; case 'middle': y = Math.floor((cellHeight - sprite.height) / 2); break; case 'bottom': y = Math.floor(cellHeight - sprite.height); break; } return { ...sprite, x: Math.floor(x), y: Math.floor(y) }; }); }; const updateSpriteCell = (id: string, newIndex: number) => { const l = activeLayer.value; if (!l) return; const currentIndex = l.sprites.findIndex(s => s.id === id); if (currentIndex === -1 || currentIndex === newIndex) return; const next = [...l.sprites]; if (newIndex < next.length) { const moving = { ...next[currentIndex] }; const target = { ...next[newIndex] }; next[currentIndex] = target; next[newIndex] = moving; } else { const [moved] = next.splice(currentIndex, 1); next.splice(newIndex, 0, moved); } l.sprites = next; }; const removeSprite = (id: string) => { const l = activeLayer.value; if (!l) return; const i = l.sprites.findIndex(s => s.id === id); if (i === -1) return; const s = l.sprites[i]; if (s.url && s.url.startsWith('blob:')) { try { URL.revokeObjectURL(s.url); } catch {} } l.sprites.splice(i, 1); }; const removeSprites = (ids: string[]) => { const l = activeLayer.value; if (!l) return; // Sort indices in descending order to avoid shift issues when splicing const indicesToRemove: number[] = []; ids.forEach(id => { const i = l.sprites.findIndex(s => s.id === id); if (i !== -1) indicesToRemove.push(i); }); indicesToRemove.sort((a, b) => b - a); indicesToRemove.forEach(i => { const s = l.sprites[i]; if (s.url && s.url.startsWith('blob:')) { try { URL.revokeObjectURL(s.url); } catch {} } l.sprites.splice(i, 1); }); }; const rotateSprite = (id: string, angle: number) => { const l = activeLayer.value; if (!l) return; const s = l.sprites.find(s => s.id === id); if (s) { s.rotation = (s.rotation + angle) % 360; } }; const flipSprite = (id: string, direction: 'horizontal' | 'vertical') => { const l = activeLayer.value; if (!l) return; const s = l.sprites.find(s => s.id === id); if (s) { if (direction === 'horizontal') s.flipX = !s.flipX; if (direction === 'vertical') s.flipY = !s.flipY; } }; const replaceSprite = (id: string, file: File) => { const l = activeLayer.value; if (!l) return; const i = l.sprites.findIndex(s => s.id === id); if (i === -1) return; const old = l.sprites[i]; if (old.url && old.url.startsWith('blob:')) { try { URL.revokeObjectURL(old.url); } catch {} } const reader = new FileReader(); reader.onload = e => { const url = e.target?.result as string; const img = new Image(); img.onload = () => { l.sprites[i] = { id: old.id, file, img, url, width: img.width, height: img.height, x: old.x, y: old.y, rotation: old.rotation, flipX: old.flipX || false, flipY: old.flipY || false }; }; img.onerror = () => { console.error('Failed to load replacement image:', file.name); }; img.src = url; }; reader.onerror = () => { console.error('Failed to read replacement image file:', file.name); }; reader.readAsDataURL(file); }; const addSprite = (file: File) => { const l = activeLayer.value; if (!l) return; const reader = new FileReader(); reader.onload = e => { const url = e.target?.result as string; const img = new Image(); img.onload = () => { const next: Sprite = { id: crypto.randomUUID(), file, img, url, width: img.width, height: img.height, x: 0, y: 0, rotation: 0, flipX: false, flipY: false, }; l.sprites = [...l.sprites, next]; }; img.onerror = () => { console.error('Failed to load sprite image:', file.name); }; img.src = url; }; reader.onerror = () => { console.error('Failed to read sprite image file:', file.name); }; reader.readAsDataURL(file); }; const processImageFiles = async (files: File[]) => { for (const f of files) addSprite(f); }; const addLayer = (name?: string) => { const l = createEmptyLayer(name || `Layer ${layers.value.length + 1}`); layers.value.push(l); activeLayerId.value = l.id; }; const removeLayer = (id: string) => { if (layers.value.length === 1) return; const idx = layers.value.findIndex(l => l.id === id); if (idx === -1) return; layers.value.splice(idx, 1); if (activeLayerId.value === id) activeLayerId.value = layers.value[0].id; }; const moveLayer = (id: string, direction: 'up' | 'down') => { const idx = layers.value.findIndex(l => l.id === id); if (idx === -1) return; if (direction === 'up' && idx > 0) { const [l] = layers.value.splice(idx, 1); layers.value.splice(idx - 1, 0, l); } if (direction === 'down' && idx < layers.value.length - 1) { const [l] = layers.value.splice(idx, 1); layers.value.splice(idx + 1, 0, l); } }; const copySpriteToFrame = (spriteId: string, targetLayerId: string, targetFrameIndex: number) => { // Find the source sprite in any layer let sourceSprite: Sprite | undefined; for (const layer of layers.value) { sourceSprite = layer.sprites.find(s => s.id === spriteId); if (sourceSprite) break; } if (!sourceSprite) return; // Find target layer const targetLayer = layers.value.find(l => l.id === targetLayerId); if (!targetLayer) return; // Create a deep copy of the sprite with a new ID const copiedSprite: Sprite = { id: crypto.randomUUID(), file: sourceSprite.file, img: sourceSprite.img, url: sourceSprite.url, width: sourceSprite.width, height: sourceSprite.height, x: sourceSprite.x, y: sourceSprite.y, rotation: sourceSprite.rotation, flipX: sourceSprite.flipX, flipY: sourceSprite.flipY, }; // Expand the sprites array if necessary with empty placeholder sprites while (targetLayer.sprites.length < targetFrameIndex) { targetLayer.sprites.push({ id: crypto.randomUUID(), file: new File([], 'empty'), img: new Image(), url: '', width: 0, height: 0, x: 0, y: 0, rotation: 0, flipX: false, flipY: false, }); } // Replace or insert the sprite at the target index if (targetFrameIndex < targetLayer.sprites.length) { // Replace existing sprite at this frame const old = targetLayer.sprites[targetFrameIndex]; if (old.url && old.url.startsWith('blob:')) { try { URL.revokeObjectURL(old.url); } catch {} } targetLayer.sprites[targetFrameIndex] = copiedSprite; } else { // Add at the end targetLayer.sprites.push(copiedSprite); } }; return { layers, visibleLayers, activeLayerId, activeLayer, columns, getMaxDimensions, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, removeSprites, rotateSprite, flipSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer, copySpriteToFrame, hasSprites, }; }; export const getMaxDimensionsAcrossLayers = (layers: Layer[], visibleOnly: boolean = false) => { // When visibleOnly is false (default), consider ALL layers to keep canvas size stable // When visibleOnly is true (export), only consider visible layers const sprites = layers.flatMap(l => (visibleOnly ? (l.visible ? l.sprites : []) : l.sprites)); return getMaxDimensionsSingle(sprites); };