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, }); export const useLayers = () => { const layers = ref([createEmptyLayer('Base')]); const activeLayerId = ref(layers.value[0].id); const columns = ref(4); 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 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 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 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 url = URL.createObjectURL(file); 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 }; }; img.onerror = () => { URL.revokeObjectURL(url); }; img.src = url; }; const addSprite = (file: File) => addSpriteWithResize(file); const addSpriteWithResize = (file: File) => { const l = activeLayer.value; if (!l) return; const url = URL.createObjectURL(file); 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, }; l.sprites = [...l.sprites, next]; }; img.onerror = () => URL.revokeObjectURL(url); img.src = url; }; const processImageFiles = async (files: File[]) => { for (const f of files) addSpriteWithResize(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 visibleLayers = computed(() => layers.value.filter(l => l.visible)); return { layers, visibleLayers, activeLayerId, activeLayer, columns, getMaxDimensions, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer, }; }; export const getMaxDimensionsAcrossLayers = (layers: Layer[]) => { const sprites = layers.flatMap(l => (l.visible ? l.sprites : [])); return getMaxDimensionsSingle(sprites); };