223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
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<Layer[]>([createEmptyLayer('Base')]);
|
|
const activeLayerId = ref<string>(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);
|
|
};
|