diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index 5b483d2..1c0ad25 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -227,7 +227,7 @@ (e: 'removeSprite', id: string): void; (e: 'removeSprites', ids: string[]): void; (e: 'replaceSprite', id: string, file: File): void; - (e: 'addSprite', file: File): void; + (e: 'addSprite', file: File, index?: number): void; (e: 'addSpriteWithResize', file: File): void; (e: 'rotateSprite', id: string, angle: number): void; (e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void; @@ -263,6 +263,7 @@ handleTouchStart, handleTouchMove, findSpriteAtPosition, + findCellAtPosition, calculateMaxDimensions, } = useDragSprite({ sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []), @@ -293,6 +294,7 @@ const showContextMenu = ref(false); const contextMenuX = ref(0); const contextMenuY = ref(0); + const contextMenuIndex = ref(null); const contextMenuSpriteId = ref(null); const selectedSpriteIds = ref>(new Set()); const replacingSpriteId = ref(null); @@ -383,6 +385,7 @@ if (!pos) return; const clickedSprite = findSpriteAtPosition(pos.x, pos.y); + contextMenuIndex.value = findCellAtPosition(pos.x, pos.y)?.index ?? null; contextMenuSpriteId.value = clickedSprite?.id || null; if (clickedSprite) { @@ -529,13 +532,18 @@ if (replacingSpriteId.value) { emit('replaceSprite', replacingSpriteId.value, file); } else { - emit('addSprite', file); + if (contextMenuIndex.value !== null) { + emit('addSprite', file, contextMenuIndex.value); + } else { + emit('addSprite', file); + } } } else { alert('Please select an image file.'); } } replacingSpriteId.value = null; + contextMenuIndex.value = null; input.value = ''; }; diff --git a/src/components/auth/AuthModal.vue b/src/components/auth/AuthModal.vue index 078d0b8..b5566b0 100644 --- a/src/components/auth/AuthModal.vue +++ b/src/components/auth/AuthModal.vue @@ -1,131 +1,107 @@ diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index 68c0000..d0d61a3 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -5,7 +5,7 @@
- + diff --git a/src/components/layout/navbar/NavbarSocials.vue b/src/components/layout/navbar/NavbarSocials.vue index 568d025..183b0bb 100644 --- a/src/components/layout/navbar/NavbarSocials.vue +++ b/src/components/layout/navbar/NavbarSocials.vue @@ -14,7 +14,7 @@ diff --git a/src/components/layout/navbar/NavbarUserMenu.vue b/src/components/layout/navbar/NavbarUserMenu.vue index 2f07feb..f5e5099 100644 --- a/src/components/layout/navbar/NavbarUserMenu.vue +++ b/src/components/layout/navbar/NavbarUserMenu.vue @@ -1,76 +1,68 @@ diff --git a/src/components/project/NewProjectModal.vue b/src/components/project/NewProjectModal.vue index 51219cd..5b6d521 100644 --- a/src/components/project/NewProjectModal.vue +++ b/src/components/project/NewProjectModal.vue @@ -4,58 +4,31 @@

New Project

-
- + px
- + px
-
+
- +
-
+
- +
@@ -70,46 +43,49 @@ diff --git a/src/components/project/ProjectList.vue b/src/components/project/ProjectList.vue index 41fbc4c..0b80029 100644 --- a/src/components/project/ProjectList.vue +++ b/src/components/project/ProjectList.vue @@ -1,104 +1,102 @@ diff --git a/src/components/project/SaveProjectModal.vue b/src/components/project/SaveProjectModal.vue index e45aa90..1a18a99 100644 --- a/src/components/project/SaveProjectModal.vue +++ b/src/components/project/SaveProjectModal.vue @@ -1,64 +1,57 @@ diff --git a/src/composables/useDragSprite.ts b/src/composables/useDragSprite.ts index 503dd1e..f82d859 100644 --- a/src/composables/useDragSprite.ts +++ b/src/composables/useDragSprite.ts @@ -113,18 +113,15 @@ export function useDragSprite(options: DragSpriteOptions) { }); 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) { + // Allow dropping anywhere in the columns, assuming infinite rows effectively + if (col >= 0 && col < columns && row >= 0) { const index = row * columns + col; - if (index < sprites.length) { - return { col, row, index }; - } + return { col, row, index }; } return null; }; diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts index 47d8b8c..79750fc 100644 --- a/src/composables/useLayers.ts +++ b/src/composables/useLayers.ts @@ -15,6 +15,20 @@ 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(); @@ -103,17 +117,66 @@ export const useLayers = () => { 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); + + // Remove the moving sprite first + const [moving] = next.splice(currentIndex, 1); + + // Determine the actual index to insert at, considering we removed one item + // If the target index was greater than current index, it shifts down by 1 in the original array perspective? + // Actually simpler: we just want to put 'moving' at 'newIndex' in the final array. + + // If newIndex is beyond the current bounds (after removal), fill with placeholders + while (next.length < newIndex) { + next.push(createEmptySprite()); + } + + // Now insert + // If newIndex is within bounds, we might be swapping if there was something there + // But the DragSprite logic implies we are "moving to this cell". + // If there is existing content at newIndex, we should swap or splice? + // The previous implementation did a swap if newIndex < length (before removal). + + // Let's stick to the "swap" logic if there's a sprite there, or "move" if we are reordering. + // Wait, Drag and Drop usually implies "insert here" or "swap with this". + // useDragSprite says: "if allowCellSwap... updateSpriteCell". + + // The original logic: + // if (newIndex < next.length) -> swap + // else -> splice (move) + + // Re-evaluating original logic: + // next has NOT had the item removed yet in the original logic 'if' block. + + // Let's implement robust swap/move logic. + // 1. If target is empty placeholder -> just move there (replace placeholder). + // 2. If target has sprite -> swap. + // 3. If target is out of bounds -> pad and move. + + if (newIndex < l.sprites.length) { + // Perform Swap + const target = l.sprites[newIndex]; + const moving = l.sprites[currentIndex]; + + // Clone array + const newSprites = [...l.sprites]; + newSprites[currentIndex] = target; + newSprites[newIndex] = moving; + l.sprites = newSprites; + } else { + // Move to previously empty/non-existent cell + const newSprites = [...l.sprites]; + // Remove from old pos + const [moved] = newSprites.splice(currentIndex, 1); + // Pad + while (newSprites.length < newIndex) { + newSprites.push(createEmptySprite()); + } + // Insert (or push if equal length) + newSprites.splice(newIndex, 0, moved); + l.sprites = newSprites; } - l.sprites = next; }; const removeSprite = (id: string) => { @@ -203,7 +266,7 @@ export const useLayers = () => { reader.readAsDataURL(file); }; - const addSprite = (file: File) => { + const addSprite = (file: File, index?: number) => { const l = activeLayer.value; if (!l) return; const reader = new FileReader(); @@ -224,7 +287,28 @@ export const useLayers = () => { flipX: false, flipY: false, }; - l.sprites = [...l.sprites, next]; + + const currentSprites = [...l.sprites]; + + if (typeof index === 'number') { + // If index is provided, insert there (padding if needed) + while (currentSprites.length < index) { + currentSprites.push(createEmptySprite()); + } + // If valid index, replace if empty or splice? + // "Adds it not in the one I selected". + // If I select a cell, I expect it to go there. + // If the cell is empty (placeholder), replace it. + // If the cell has a sprite, maybe insert/shift? + // Usually "Add" implies append, but context menu "Add sprite" on a cell implies "Put it here". + // Let's Insert (Shift others) for safety, or check if empty. + // But simpler: just splice it in. + currentSprites.splice(index, 0, next); + } else { + // No index, append to end + currentSprites.push(next); + } + l.sprites = currentSprites; }; img.onerror = () => { console.error('Failed to load sprite image:', file.name); @@ -299,19 +383,7 @@ export const useLayers = () => { // 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, - }); + targetLayer.sprites.push(createEmptySprite()); } // Replace or insert the sprite at the target index diff --git a/src/composables/useProjectManager.ts b/src/composables/useProjectManager.ts new file mode 100644 index 0000000..83285e2 --- /dev/null +++ b/src/composables/useProjectManager.ts @@ -0,0 +1,95 @@ +import { ref, toRef, watch } from 'vue'; +import { useLayers, createEmptyLayer, getMaxDimensionsAcrossLayers } from '@/composables/useLayers'; +import { useSettingsStore } from '@/stores/useSettingsStore'; +import { useProjectStore, type Project } from '@/stores/useProjectStore'; +import { useExportLayers } from '@/composables/useExportLayers'; + +// Global state for editor visibility +const isEditorActive = ref(false); + +export const useProjectManager = () => { + const settingsStore = useSettingsStore(); + const projectStore = useProjectStore(); + const { layers, columns, activeLayerId, visibleLayers } = useLayers(); + + const { generateProjectJSON, loadProjectData } = useExportLayers( + layers, + columns, + toRef(settingsStore, 'negativeSpacingEnabled'), + activeLayerId, + toRef(settingsStore, 'backgroundColor'), + toRef(settingsStore, 'manualCellSizeEnabled'), + toRef(settingsStore, 'manualCellWidth'), + toRef(settingsStore, 'manualCellHeight') + ); + + // Watch for sprites to automatically open editor (legacy behavior support) + watch( + () => layers.value.some(l => l.sprites.length > 0), + hasSprites => { + if (hasSprites) isEditorActive.value = true; + }, + { immediate: true } + ); + + const createProject = (config: { width: number; height: number; columns: number; rows: number }) => { + // 1. Reset Settings + settingsStore.setManualCellSize(config.width, config.height); + settingsStore.manualCellSizeEnabled = true; + + // 2. Reset Layers + const newLayer = createEmptyLayer('Base'); + layers.value = [newLayer]; + activeLayerId.value = newLayer.id; + + // 3. Set Columns + columns.value = config.columns; + + // 4. Reset Project Store + projectStore.currentProject = null; + + // 5. Force Editor Active + isEditorActive.value = true; + }; + + const openProject = async (project: Project) => { + try { + if (project.data) { + await loadProjectData(project.data); + } + projectStore.currentProject = project; + isEditorActive.value = true; + } catch (e) { + console.error('Failed to open project', e); + alert('Failed to open project data'); + } + }; + + const saveProject = async (name: string) => { + try { + const data = await generateProjectJSON(); + + if (projectStore.currentProject && projectStore.currentProject.name === name) { + await projectStore.updateProject(projectStore.currentProject.id, data); + } else { + await projectStore.createProject(name, data); + } + } catch (e) { + console.error(e); + alert('Failed to save project'); + throw e; // Re-throw to let caller know + } + }; + + const closeEditor = () => { + isEditorActive.value = false; + }; + + return { + isEditorActive, + createProject, + openProject, + saveProject, + closeEditor, + }; +}; diff --git a/src/stores/useProjectStore.ts b/src/stores/useProjectStore.ts index 6537a4f..e6b89c2 100644 --- a/src/stores/useProjectStore.ts +++ b/src/stores/useProjectStore.ts @@ -80,29 +80,29 @@ export const useProjectStore = defineStore('project', () => { } async function loadProject(id: string) { - if (!authStore.user) return; - isLoading.value = true; - try { - const record = await authStore.pb.collection('projects').getOne(id); - currentProject.value = { - id: record.id, - name: record.name, - data: record.data, - created: record.created, - updated: record.updated - }; - } catch (error) { - console.error("Failed to load project", error); - throw error; - } finally { - isLoading.value = false; - } + if (!authStore.user) return; + isLoading.value = true; + try { + const record = await authStore.pb.collection('projects').getOne(id); + currentProject.value = { + id: record.id, + name: record.name, + data: record.data, + created: record.created, + updated: record.updated, + }; + } catch (error) { + console.error('Failed to load project', error); + throw error; + } finally { + isLoading.value = false; + } } async function deleteProject(id: string) { if (!authStore.user) return; if (!confirm('Are you sure you want to delete this project?')) return; - + isLoading.value = true; try { await authStore.pb.collection('projects').delete(id); @@ -125,6 +125,6 @@ export const useProjectStore = defineStore('project', () => { createProject, updateProject, loadProject, - deleteProject + deleteProject, }; }); diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index af2cf22..45ef835 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,7 +1,7 @@