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); const createEmptySprite = (): Sprite => ({ 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, }); 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; let cellWidth: number; let cellHeight: number; if (settingsStore.manualCellSizeEnabled) { cellWidth = settingsStore.manualCellWidth; cellHeight = settingsStore.manualCellHeight; } else { 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]; const [moving] = next.splice(currentIndex, 1); while (next.length < newIndex) { next.push(createEmptySprite()); } if (newIndex < l.sprites.length) { const target = l.sprites[newIndex]; const moving = l.sprites[currentIndex]; const newSprites = [...l.sprites]; newSprites[currentIndex] = target; newSprites[newIndex] = moving; l.sprites = newSprites; } else { const newSprites = [...l.sprites]; const [moved] = newSprites.splice(currentIndex, 1); while (newSprites.length < newIndex) { newSprites.push(createEmptySprite()); } newSprites.splice(newIndex, 0, moved); l.sprites = newSprites; } }; 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 {} } if (layers.value.length > 1) { // If there are multiple layers, we want to maintain frame alignment // so we replace the sprite with an empty one instead of shifting l.sprites[i] = createEmptySprite(); } else { // If there's only one layer, we can safely remove the frame l.sprites.splice(i, 1); } }; const removeSprites = (ids: string[]) => { const l = activeLayer.value; if (!l) return; 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 {} } if (layers.value.length > 1) { l.sprites[i] = createEmptySprite(); } else { 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, index?: number) => { 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, }; const currentSprites = [...l.sprites]; if (typeof index === 'number') { while (currentSprites.length < index) { currentSprites.push(createEmptySprite()); } currentSprites.splice(index, 0, next); } else { currentSprites.push(next); } l.sprites = currentSprites; }; 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) => { let sourceSprite: Sprite | undefined; for (const layer of layers.value) { sourceSprite = layer.sprites.find(s => s.id === spriteId); if (sourceSprite) break; } if (!sourceSprite) return; const targetLayer = layers.value.find(l => l.id === targetLayerId); if (!targetLayer) return; 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, }; while (targetLayer.sprites.length < targetFrameIndex) { targetLayer.sprites.push(createEmptySprite()); } if (targetFrameIndex < targetLayer.sprites.length) { const old = targetLayer.sprites[targetFrameIndex]; if (old.url && old.url.startsWith('blob:')) { try { URL.revokeObjectURL(old.url); } catch {} } targetLayer.sprites[targetFrameIndex] = copiedSprite; } else { 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) => { const sprites = layers.flatMap(l => (visibleOnly ? (l.visible ? l.sprites : []) : l.sprites)); return getMaxDimensionsSingle(sprites); };