[FEAT] add grid metrics
This commit is contained in:
@@ -204,6 +204,8 @@
|
|||||||
import type { Sprite } from '@/types/sprites';
|
import type { Sprite } from '@/types/sprites';
|
||||||
import { useDragSprite } from '@/composables/useDragSprite';
|
import { useDragSprite } from '@/composables/useDragSprite';
|
||||||
import { useFileDrop } from '@/composables/useFileDrop';
|
import { useFileDrop } from '@/composables/useFileDrop';
|
||||||
|
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||||
|
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||||
|
|
||||||
import type { Layer } from '@/types/sprites';
|
import type { Layer } from '@/types/sprites';
|
||||||
|
|
||||||
@@ -315,8 +317,16 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Grid metrics
|
// Use the new useGridMetrics composable for consistent calculations
|
||||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
const { gridMetrics: gridMetricsRef, getCellPosition: getCellPositionHelper } = useGridMetrics({
|
||||||
|
layers: toRef(props, 'layers'),
|
||||||
|
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||||
|
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||||
|
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||||
|
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridMetrics = gridMetricsRef;
|
||||||
|
|
||||||
const totalCells = computed(() => {
|
const totalCells = computed(() => {
|
||||||
// Use all layers regardless of visibility to keep canvas size stable
|
// Use all layers regardless of visibility to keep canvas size stable
|
||||||
@@ -335,47 +345,20 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getCellPosition = (index: number) => {
|
const getCellPosition = (index: number) => {
|
||||||
const col = index % props.columns;
|
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
||||||
const row = Math.floor(index / props.columns);
|
|
||||||
return {
|
|
||||||
x: Math.round(col * gridMetrics.value.maxWidth),
|
|
||||||
y: Math.round(row * gridMetrics.value.maxHeight),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCellBackground = () => {
|
// Use the new useBackgroundStyles composable for consistent background styling
|
||||||
const bg = settingsStore.backgroundColor;
|
const { backgroundColor: cellBackgroundColor, backgroundImage: cellBackgroundImage, backgroundSize: cellBackgroundSize, backgroundPosition: cellBackgroundPosition } = useBackgroundStyles({
|
||||||
if (bg === 'transparent') {
|
backgroundColor: toRef(settingsStore, 'backgroundColor'),
|
||||||
return 'transparent';
|
checkerboardEnabled: toRef(settingsStore, 'checkerboardEnabled'),
|
||||||
}
|
darkMode: toRef(settingsStore, 'darkMode'),
|
||||||
return bg;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const getCellBackgroundImage = () => {
|
const getCellBackground = () => cellBackgroundColor.value;
|
||||||
const bg = settingsStore.backgroundColor;
|
const getCellBackgroundImage = () => cellBackgroundImage.value;
|
||||||
if (bg === 'transparent' && settingsStore.checkerboardEnabled) {
|
const getCellBackgroundSize = () => cellBackgroundSize.value;
|
||||||
// Checkerboard pattern for transparent backgrounds (dark mode friendly)
|
const getCellBackgroundPosition = () => cellBackgroundPosition.value;
|
||||||
const color = settingsStore.darkMode ? '#4b5563' : '#d1d5db';
|
|
||||||
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
|
||||||
}
|
|
||||||
return 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCellBackgroundSize = () => {
|
|
||||||
const bg = settingsStore.backgroundColor;
|
|
||||||
if (bg === 'transparent') {
|
|
||||||
return '20px 20px';
|
|
||||||
}
|
|
||||||
return 'auto';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCellBackgroundPosition = () => {
|
|
||||||
const bg = settingsStore.backgroundColor;
|
|
||||||
if (bg === 'transparent') {
|
|
||||||
return '0 0, 0 10px, 10px -10px, -10px 0px';
|
|
||||||
}
|
|
||||||
return '0 0';
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDrag = (event: MouseEvent) => {
|
const startDrag = (event: MouseEvent) => {
|
||||||
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
||||||
|
|||||||
@@ -73,9 +73,9 @@
|
|||||||
width: `${cellDimensions.cellWidth}px`,
|
width: `${cellDimensions.cellWidth}px`,
|
||||||
height: `${cellDimensions.cellHeight}px`,
|
height: `${cellDimensions.cellHeight}px`,
|
||||||
backgroundColor: settingsStore.backgroundColor === 'transparent' ? '#f9fafb' : settingsStore.backgroundColor,
|
backgroundColor: settingsStore.backgroundColor === 'transparent' ? '#f9fafb' : settingsStore.backgroundColor,
|
||||||
backgroundImage: getPreviewBackgroundImage(),
|
backgroundImage: previewBackgroundImage,
|
||||||
backgroundSize: settingsStore.backgroundColor === 'transparent' ? '20px 20px' : 'auto',
|
backgroundSize: previewBackgroundSize,
|
||||||
backgroundPosition: settingsStore.backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
|
backgroundPosition: previewBackgroundPosition,
|
||||||
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
|
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
|
||||||
}"
|
}"
|
||||||
@dragover.prevent="onDragOver"
|
@dragover.prevent="onDragOver"
|
||||||
@@ -302,13 +302,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
|
import { ref, onMounted, watch, onUnmounted, computed, toRef } from 'vue';
|
||||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||||
import type { Layer, Sprite } from '@/types/sprites';
|
import type { Layer, Sprite } from '@/types/sprites';
|
||||||
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
|
||||||
import { useZoom } from '@/composables/useZoom';
|
import { useZoom } from '@/composables/useZoom';
|
||||||
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
||||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||||
|
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
@@ -426,38 +426,30 @@
|
|||||||
return layer.sprites[currentFrameIndex.value] || null;
|
return layer.sprites[currentFrameIndex.value] || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed cell dimensions
|
// Use the new useGridMetrics composable for consistent calculations
|
||||||
const cellDimensions = computed(() => {
|
const { gridMetrics } = useGridMetrics({
|
||||||
// Use ALL layers (regardless of visibility) to keep preview size stable
|
layers: toRef(props, 'layers'),
|
||||||
const allLayers = props.layers;
|
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||||
// If manual cell size is enabled, use manual values
|
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||||
if (settingsStore.manualCellSizeEnabled) {
|
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||||
return {
|
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
||||||
cellWidth: Math.round(settingsStore.manualCellWidth),
|
|
||||||
cellHeight: Math.round(settingsStore.manualCellHeight),
|
|
||||||
negativeSpacing: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, calculate from sprite dimensions across ALL layers
|
|
||||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(allLayers);
|
|
||||||
const allSprites = allLayers.flatMap(l => l.sprites);
|
|
||||||
const negativeSpacing = Math.round(calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled));
|
|
||||||
return {
|
|
||||||
cellWidth: Math.round(maxWidth + negativeSpacing),
|
|
||||||
cellHeight: Math.round(maxHeight + negativeSpacing),
|
|
||||||
negativeSpacing,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper for background image (dark mode friendly)
|
// Computed cell dimensions (for backward compatibility with existing code)
|
||||||
const getPreviewBackgroundImage = () => {
|
const cellDimensions = computed(() => ({
|
||||||
if (settingsStore.backgroundColor === 'transparent' && settingsStore.checkerboardEnabled) {
|
cellWidth: gridMetrics.value.maxWidth,
|
||||||
const color = settingsStore.darkMode ? '#4b5563' : '#d1d5db';
|
cellHeight: gridMetrics.value.maxHeight,
|
||||||
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
negativeSpacing: gridMetrics.value.negativeSpacing,
|
||||||
}
|
}));
|
||||||
return 'none';
|
|
||||||
};
|
// Use the new useBackgroundStyles composable for consistent background styling
|
||||||
|
const { backgroundImage: previewBackgroundImage, backgroundSize: previewBackgroundSize, backgroundPosition: previewBackgroundPosition } = useBackgroundStyles({
|
||||||
|
backgroundColor: toRef(settingsStore, 'backgroundColor'),
|
||||||
|
checkerboardEnabled: toRef(settingsStore, 'checkerboardEnabled'),
|
||||||
|
darkMode: toRef(settingsStore, 'darkMode'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPreviewBackgroundImage = () => previewBackgroundImage.value;
|
||||||
|
|
||||||
// Dragging state
|
// Dragging state
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
|||||||
113
src/composables/useBackgroundStyles.ts
Normal file
113
src/composables/useBackgroundStyles.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { computed, type Ref, type ComputedRef } from 'vue';
|
||||||
|
|
||||||
|
export interface BackgroundStylesOptions {
|
||||||
|
backgroundColor: Ref<string> | ComputedRef<string> | string;
|
||||||
|
checkerboardEnabled?: Ref<boolean> | ComputedRef<boolean> | boolean;
|
||||||
|
darkMode?: Ref<boolean> | ComputedRef<boolean> | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackgroundStyles {
|
||||||
|
backgroundColor: string;
|
||||||
|
backgroundImage: string;
|
||||||
|
backgroundSize: string;
|
||||||
|
backgroundPosition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for generating consistent background styles across components.
|
||||||
|
* Handles transparent backgrounds with checkerboard patterns and dark mode.
|
||||||
|
*/
|
||||||
|
export function useBackgroundStyles(options: BackgroundStylesOptions) {
|
||||||
|
// Helper to get reactive values
|
||||||
|
const getBackgroundColor = () => (typeof options.backgroundColor === 'string' ? options.backgroundColor : options.backgroundColor.value);
|
||||||
|
const getCheckerboardEnabled = () => (typeof options.checkerboardEnabled === 'boolean' ? options.checkerboardEnabled : options.checkerboardEnabled?.value ?? true);
|
||||||
|
const getDarkMode = () => (typeof options.darkMode === 'boolean' ? options.darkMode : options.darkMode?.value ?? false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the background color.
|
||||||
|
*/
|
||||||
|
const backgroundColor = computed(() => {
|
||||||
|
const bg = getBackgroundColor();
|
||||||
|
return bg === 'transparent' ? 'transparent' : bg;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the background image (checkerboard pattern for transparent backgrounds).
|
||||||
|
*/
|
||||||
|
const backgroundImage = computed(() => {
|
||||||
|
const bg = getBackgroundColor();
|
||||||
|
const checkerboardEnabled = getCheckerboardEnabled();
|
||||||
|
const darkMode = getDarkMode();
|
||||||
|
|
||||||
|
if (bg === 'transparent' && checkerboardEnabled) {
|
||||||
|
// Checkerboard pattern for transparent backgrounds (dark mode friendly)
|
||||||
|
const color = darkMode ? '#4b5563' : '#d1d5db';
|
||||||
|
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
||||||
|
}
|
||||||
|
return 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the background size.
|
||||||
|
*/
|
||||||
|
const backgroundSize = computed(() => {
|
||||||
|
const bg = getBackgroundColor();
|
||||||
|
return bg === 'transparent' ? '20px 20px' : 'auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the background position.
|
||||||
|
*/
|
||||||
|
const backgroundPosition = computed(() => {
|
||||||
|
const bg = getBackgroundColor();
|
||||||
|
return bg === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all background styles as a single object.
|
||||||
|
*/
|
||||||
|
const backgroundStyles = computed<BackgroundStyles>(() => ({
|
||||||
|
backgroundColor: backgroundColor.value,
|
||||||
|
backgroundImage: backgroundImage.value,
|
||||||
|
backgroundSize: backgroundSize.value,
|
||||||
|
backgroundPosition: backgroundPosition.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor,
|
||||||
|
backgroundImage,
|
||||||
|
backgroundSize,
|
||||||
|
backgroundPosition,
|
||||||
|
backgroundStyles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone helper to get background styles.
|
||||||
|
* Useful when you don't need the full composable.
|
||||||
|
*/
|
||||||
|
export function getBackgroundStyles(
|
||||||
|
backgroundColor: string,
|
||||||
|
options: {
|
||||||
|
checkerboardEnabled?: boolean;
|
||||||
|
darkMode?: boolean;
|
||||||
|
} = {}
|
||||||
|
): BackgroundStyles {
|
||||||
|
const checkerboardEnabled = options.checkerboardEnabled ?? true;
|
||||||
|
const darkMode = options.darkMode ?? false;
|
||||||
|
|
||||||
|
const bg = backgroundColor === 'transparent' ? 'transparent' : backgroundColor;
|
||||||
|
|
||||||
|
let bgImage = 'none';
|
||||||
|
if (backgroundColor === 'transparent' && checkerboardEnabled) {
|
||||||
|
const color = darkMode ? '#4b5563' : '#d1d5db';
|
||||||
|
bgImage = `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: bg,
|
||||||
|
backgroundImage: bgImage,
|
||||||
|
backgroundSize: backgroundColor === 'transparent' ? '20px 20px' : 'auto',
|
||||||
|
backgroundPosition: backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { Sprite, Layer } from '@/types/sprites';
|
import type { Sprite, Layer } from '@/types/sprites';
|
||||||
import { getMaxDimensions } from './useSprites';
|
import { useGridMetrics, type GridMetrics } from './useGridMetrics';
|
||||||
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
|
||||||
|
|
||||||
export interface CellPosition {
|
export interface CellPosition {
|
||||||
col: number;
|
col: number;
|
||||||
@@ -70,70 +69,42 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
||||||
const highlightCell = ref<CellPosition | null>(null);
|
const highlightCell = ref<CellPosition | null>(null);
|
||||||
|
|
||||||
// Cache for max dimensions
|
// Use the new useGridMetrics composable for consistent calculations
|
||||||
const lastMaxWidth = ref(1);
|
const gridMetricsComposable = useGridMetrics({
|
||||||
const lastMaxHeight = ref(1);
|
layers: options.layers,
|
||||||
|
sprites: options.sprites,
|
||||||
|
negativeSpacingEnabled: options.negativeSpacingEnabled,
|
||||||
|
manualCellSizeEnabled: options.manualCellSizeEnabled,
|
||||||
|
manualCellWidth: options.manualCellWidth,
|
||||||
|
manualCellHeight: options.manualCellHeight,
|
||||||
|
});
|
||||||
|
|
||||||
const calculateMaxDimensions = () => {
|
const calculateMaxDimensions = (): GridMetrics => {
|
||||||
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
return gridMetricsComposable.calculateCellDimensions();
|
||||||
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
|
||||||
|
|
||||||
// If manual cell size is enabled, use manual dimensions
|
|
||||||
if (manualCellSizeEnabled) {
|
|
||||||
const maxWidth = getManualCellWidth();
|
|
||||||
const maxHeight = getManualCellHeight();
|
|
||||||
// When manual cell size is used, negative spacing is not applied
|
|
||||||
const negativeSpacing = 0;
|
|
||||||
// Don't update lastMaxWidth/lastMaxHeight when in manual mode
|
|
||||||
return { maxWidth, maxHeight, negativeSpacing };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all sprites to calculate dimensions from
|
|
||||||
// If layers are provided, use ALL layers (regardless of visibility) to keep canvas size stable
|
|
||||||
const layers = getLayers();
|
|
||||||
const spritesToMeasure = layers ? layers.flatMap(l => l.sprites) : getSprites();
|
|
||||||
|
|
||||||
// Otherwise, calculate based on sprite dimensions across all visible layers
|
|
||||||
const base = getMaxDimensions(spritesToMeasure);
|
|
||||||
// When switching back from manual mode, reset to actual sprite dimensions
|
|
||||||
const baseMaxWidth = Math.max(1, base.maxWidth);
|
|
||||||
const baseMaxHeight = Math.max(1, base.maxHeight);
|
|
||||||
lastMaxWidth.value = baseMaxWidth;
|
|
||||||
lastMaxHeight.value = baseMaxHeight;
|
|
||||||
|
|
||||||
// Calculate negative spacing using shared composable
|
|
||||||
const negativeSpacing = Math.round(calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled));
|
|
||||||
|
|
||||||
// Add negative spacing to expand each cell
|
|
||||||
const maxWidth = Math.round(baseMaxWidth + negativeSpacing);
|
|
||||||
const maxHeight = Math.round(baseMaxHeight + negativeSpacing);
|
|
||||||
return { maxWidth, maxHeight, negativeSpacing };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed sprite positions
|
// Computed sprite positions
|
||||||
const spritePositions = computed<SpritePosition[]>(() => {
|
const spritePositions = computed<SpritePosition[]>(() => {
|
||||||
const sprites = getSprites();
|
const sprites = getSprites();
|
||||||
const columns = getColumns();
|
const columns = getColumns();
|
||||||
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
|
const metrics = calculateMaxDimensions();
|
||||||
|
|
||||||
return sprites.map((sprite, index) => {
|
return sprites.map((sprite, index) => {
|
||||||
const col = index % columns;
|
const cellPos = gridMetricsComposable.getCellPosition(index, columns, metrics);
|
||||||
const row = Math.floor(index / columns);
|
const canvasPos = gridMetricsComposable.getSpriteCanvasPosition(sprite, index, columns, metrics);
|
||||||
|
|
||||||
// With negative spacing, sprites are positioned at bottom-right of cell
|
|
||||||
// (spacing added to top and left)
|
|
||||||
return {
|
return {
|
||||||
id: sprite.id,
|
id: sprite.id,
|
||||||
canvasX: Math.round(col * maxWidth + negativeSpacing + sprite.x),
|
canvasX: canvasPos.canvasX,
|
||||||
canvasY: Math.round(row * maxHeight + negativeSpacing + sprite.y),
|
canvasY: canvasPos.canvasY,
|
||||||
cellX: Math.round(col * maxWidth),
|
cellX: canvasPos.cellX,
|
||||||
cellY: Math.round(row * maxHeight),
|
cellY: canvasPos.cellY,
|
||||||
width: Math.round(sprite.width),
|
width: Math.round(sprite.width),
|
||||||
height: Math.round(sprite.height),
|
height: Math.round(sprite.height),
|
||||||
maxWidth: Math.round(maxWidth),
|
maxWidth: metrics.maxWidth,
|
||||||
maxHeight: Math.round(maxHeight),
|
maxHeight: metrics.maxHeight,
|
||||||
col,
|
col: cellPos.col,
|
||||||
row,
|
row: cellPos.row,
|
||||||
index,
|
index,
|
||||||
x: sprite.x,
|
x: sprite.x,
|
||||||
y: sprite.y,
|
y: sprite.y,
|
||||||
|
|||||||
165
src/composables/useGridMetrics.ts
Normal file
165
src/composables/useGridMetrics.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { computed, type Ref, type ComputedRef } from 'vue';
|
||||||
|
import type { Sprite, Layer } from '@/types/sprites';
|
||||||
|
import { getMaxDimensions } from './useSprites';
|
||||||
|
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
||||||
|
|
||||||
|
export interface GridMetrics {
|
||||||
|
maxWidth: number;
|
||||||
|
maxHeight: number;
|
||||||
|
negativeSpacing: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CellPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
col: number;
|
||||||
|
row: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpriteCanvasPosition {
|
||||||
|
canvasX: number;
|
||||||
|
canvasY: number;
|
||||||
|
cellX: number;
|
||||||
|
cellY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridMetricsOptions {
|
||||||
|
layers?: Ref<Layer[]> | ComputedRef<Layer[]> | Layer[];
|
||||||
|
sprites?: Ref<Sprite[]> | ComputedRef<Sprite[]> | Sprite[];
|
||||||
|
negativeSpacingEnabled?: Ref<boolean> | boolean;
|
||||||
|
manualCellSizeEnabled?: Ref<boolean> | boolean;
|
||||||
|
manualCellWidth?: Ref<number> | number;
|
||||||
|
manualCellHeight?: Ref<number> | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for calculating grid metrics and sprite positions.
|
||||||
|
* Provides a single source of truth for cell dimensions and positioning calculations.
|
||||||
|
*/
|
||||||
|
export function useGridMetrics(options: GridMetricsOptions = {}) {
|
||||||
|
// Helper to get reactive values
|
||||||
|
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
||||||
|
const getSprites = () => (options.sprites ? (Array.isArray(options.sprites) ? options.sprites : options.sprites.value) : []);
|
||||||
|
const getNegativeSpacingEnabled = () => (typeof options.negativeSpacingEnabled === 'boolean' ? options.negativeSpacingEnabled : options.negativeSpacingEnabled?.value ?? false);
|
||||||
|
const getManualCellSizeEnabled = () => (typeof options.manualCellSizeEnabled === 'boolean' ? options.manualCellSizeEnabled : options.manualCellSizeEnabled?.value ?? false);
|
||||||
|
const getManualCellWidth = () => (typeof options.manualCellWidth === 'number' ? options.manualCellWidth : options.manualCellWidth?.value ?? 64);
|
||||||
|
const getManualCellHeight = () => (typeof options.manualCellHeight === 'number' ? options.manualCellHeight : options.manualCellHeight?.value ?? 64);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate cell dimensions including negative spacing.
|
||||||
|
* This is the canonical calculation used across all components.
|
||||||
|
*/
|
||||||
|
const calculateCellDimensions = (): GridMetrics => {
|
||||||
|
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
||||||
|
|
||||||
|
// If manual cell size is enabled, use manual dimensions
|
||||||
|
if (manualCellSizeEnabled) {
|
||||||
|
return {
|
||||||
|
maxWidth: Math.round(getManualCellWidth()),
|
||||||
|
maxHeight: Math.round(getManualCellHeight()),
|
||||||
|
negativeSpacing: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sprites to measure from layers or direct sprites array
|
||||||
|
const layers = getLayers();
|
||||||
|
const spritesToMeasure = layers ? layers.flatMap(l => l.sprites) : getSprites();
|
||||||
|
|
||||||
|
// Calculate base dimensions from sprites
|
||||||
|
const base = getMaxDimensions(spritesToMeasure);
|
||||||
|
const baseMaxWidth = Math.max(1, base.maxWidth);
|
||||||
|
const baseMaxHeight = Math.max(1, base.maxHeight);
|
||||||
|
|
||||||
|
// Calculate negative spacing
|
||||||
|
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
||||||
|
const negativeSpacing = Math.round(calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled));
|
||||||
|
|
||||||
|
// Add negative spacing to expand each cell
|
||||||
|
return {
|
||||||
|
maxWidth: Math.round(baseMaxWidth + negativeSpacing),
|
||||||
|
maxHeight: Math.round(baseMaxHeight + negativeSpacing),
|
||||||
|
negativeSpacing,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the position of a cell in the grid.
|
||||||
|
*/
|
||||||
|
const getCellPosition = (index: number, columns: number, metrics: GridMetrics): CellPosition => {
|
||||||
|
const col = index % columns;
|
||||||
|
const row = Math.floor(index / columns);
|
||||||
|
return {
|
||||||
|
x: Math.round(col * metrics.maxWidth),
|
||||||
|
y: Math.round(row * metrics.maxHeight),
|
||||||
|
col,
|
||||||
|
row,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the canvas position of a sprite within a cell.
|
||||||
|
* This is the final position where the sprite should be rendered.
|
||||||
|
*/
|
||||||
|
const getSpriteCanvasPosition = (sprite: Sprite, cellIndex: number, columns: number, metrics: GridMetrics): SpriteCanvasPosition => {
|
||||||
|
const cellPos = getCellPosition(cellIndex, columns, metrics);
|
||||||
|
return {
|
||||||
|
canvasX: Math.round(cellPos.x + metrics.negativeSpacing + sprite.x),
|
||||||
|
canvasY: Math.round(cellPos.y + metrics.negativeSpacing + sprite.y),
|
||||||
|
cellX: cellPos.x,
|
||||||
|
cellY: cellPos.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed grid metrics that automatically updates when dependencies change.
|
||||||
|
*/
|
||||||
|
const gridMetrics = computed(() => calculateCellDimensions());
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Computed values
|
||||||
|
gridMetrics,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
calculateCellDimensions,
|
||||||
|
getCellPosition,
|
||||||
|
getSpriteCanvasPosition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone helper to calculate cell dimensions.
|
||||||
|
* Useful when you don't need the full composable.
|
||||||
|
*/
|
||||||
|
export function getGridMetrics(
|
||||||
|
spritesOrLayers: Sprite[] | Layer[],
|
||||||
|
options: {
|
||||||
|
negativeSpacingEnabled?: boolean;
|
||||||
|
manualCellSizeEnabled?: boolean;
|
||||||
|
manualCellWidth?: number;
|
||||||
|
manualCellHeight?: number;
|
||||||
|
} = {}
|
||||||
|
): GridMetrics {
|
||||||
|
if (options.manualCellSizeEnabled) {
|
||||||
|
return {
|
||||||
|
maxWidth: Math.round(options.manualCellWidth ?? 64),
|
||||||
|
maxHeight: Math.round(options.manualCellHeight ?? 64),
|
||||||
|
negativeSpacing: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have layers or sprites
|
||||||
|
const isLayers = spritesOrLayers.length > 0 && 'sprites' in spritesOrLayers[0];
|
||||||
|
const sprites = isLayers ? (spritesOrLayers as Layer[]).flatMap(l => l.sprites) : (spritesOrLayers as Sprite[]);
|
||||||
|
|
||||||
|
const base = getMaxDimensions(sprites);
|
||||||
|
const baseMaxWidth = Math.max(1, base.maxWidth);
|
||||||
|
const baseMaxHeight = Math.max(1, base.maxHeight);
|
||||||
|
|
||||||
|
const negativeSpacing = Math.round(calculateNegativeSpacing(sprites, options.negativeSpacingEnabled ?? false));
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxWidth: Math.round(baseMaxWidth + negativeSpacing),
|
||||||
|
maxHeight: Math.round(baseMaxHeight + negativeSpacing),
|
||||||
|
negativeSpacing,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user