[FEAT] add grid metrics
This commit is contained in:
@@ -204,6 +204,8 @@
|
||||
import type { Sprite } from '@/types/sprites';
|
||||
import { useDragSprite } from '@/composables/useDragSprite';
|
||||
import { useFileDrop } from '@/composables/useFileDrop';
|
||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||
|
||||
import type { Layer } from '@/types/sprites';
|
||||
|
||||
@@ -315,8 +317,16 @@
|
||||
}
|
||||
);
|
||||
|
||||
// Grid metrics
|
||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
||||
// Use the new useGridMetrics composable for consistent calculations
|
||||
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(() => {
|
||||
// Use all layers regardless of visibility to keep canvas size stable
|
||||
@@ -335,47 +345,20 @@
|
||||
});
|
||||
|
||||
const getCellPosition = (index: number) => {
|
||||
const col = index % props.columns;
|
||||
const row = Math.floor(index / props.columns);
|
||||
return {
|
||||
x: Math.round(col * gridMetrics.value.maxWidth),
|
||||
y: Math.round(row * gridMetrics.value.maxHeight),
|
||||
};
|
||||
return getCellPositionHelper(index, props.columns, gridMetrics.value);
|
||||
};
|
||||
|
||||
const getCellBackground = () => {
|
||||
const bg = settingsStore.backgroundColor;
|
||||
if (bg === 'transparent') {
|
||||
return 'transparent';
|
||||
}
|
||||
return bg;
|
||||
};
|
||||
// Use the new useBackgroundStyles composable for consistent background styling
|
||||
const { backgroundColor: cellBackgroundColor, backgroundImage: cellBackgroundImage, backgroundSize: cellBackgroundSize, backgroundPosition: cellBackgroundPosition } = useBackgroundStyles({
|
||||
backgroundColor: toRef(settingsStore, 'backgroundColor'),
|
||||
checkerboardEnabled: toRef(settingsStore, 'checkerboardEnabled'),
|
||||
darkMode: toRef(settingsStore, 'darkMode'),
|
||||
});
|
||||
|
||||
const getCellBackgroundImage = () => {
|
||||
const bg = settingsStore.backgroundColor;
|
||||
if (bg === 'transparent' && settingsStore.checkerboardEnabled) {
|
||||
// Checkerboard pattern for transparent backgrounds (dark mode friendly)
|
||||
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 getCellBackground = () => cellBackgroundColor.value;
|
||||
const getCellBackgroundImage = () => cellBackgroundImage.value;
|
||||
const getCellBackgroundSize = () => cellBackgroundSize.value;
|
||||
const getCellBackgroundPosition = () => cellBackgroundPosition.value;
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
width: `${cellDimensions.cellWidth}px`,
|
||||
height: `${cellDimensions.cellHeight}px`,
|
||||
backgroundColor: settingsStore.backgroundColor === 'transparent' ? '#f9fafb' : settingsStore.backgroundColor,
|
||||
backgroundImage: getPreviewBackgroundImage(),
|
||||
backgroundSize: settingsStore.backgroundColor === 'transparent' ? '20px 20px' : 'auto',
|
||||
backgroundPosition: settingsStore.backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
|
||||
backgroundImage: previewBackgroundImage,
|
||||
backgroundSize: previewBackgroundSize,
|
||||
backgroundPosition: previewBackgroundPosition,
|
||||
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
|
||||
}"
|
||||
@dragover.prevent="onDragOver"
|
||||
@@ -302,13 +302,13 @@
|
||||
</template>
|
||||
|
||||
<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 type { Layer, Sprite } from '@/types/sprites';
|
||||
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
||||
import { useZoom } from '@/composables/useZoom';
|
||||
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||
|
||||
const props = defineProps<{
|
||||
layers: Layer[];
|
||||
@@ -426,38 +426,30 @@
|
||||
return layer.sprites[currentFrameIndex.value] || null;
|
||||
});
|
||||
|
||||
// Computed cell dimensions
|
||||
const cellDimensions = computed(() => {
|
||||
// Use ALL layers (regardless of visibility) to keep preview size stable
|
||||
const allLayers = props.layers;
|
||||
// If manual cell size is enabled, use manual values
|
||||
if (settingsStore.manualCellSizeEnabled) {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
// Use the new useGridMetrics composable for consistent calculations
|
||||
const { gridMetrics } = useGridMetrics({
|
||||
layers: toRef(props, 'layers'),
|
||||
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
|
||||
});
|
||||
|
||||
// Helper for background image (dark mode friendly)
|
||||
const getPreviewBackgroundImage = () => {
|
||||
if (settingsStore.backgroundColor === 'transparent' && settingsStore.checkerboardEnabled) {
|
||||
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';
|
||||
};
|
||||
// Computed cell dimensions (for backward compatibility with existing code)
|
||||
const cellDimensions = computed(() => ({
|
||||
cellWidth: gridMetrics.value.maxWidth,
|
||||
cellHeight: gridMetrics.value.maxHeight,
|
||||
negativeSpacing: gridMetrics.value.negativeSpacing,
|
||||
}));
|
||||
|
||||
// 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
|
||||
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 type { Sprite, Layer } from '@/types/sprites';
|
||||
import { getMaxDimensions } from './useSprites';
|
||||
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
||||
import { useGridMetrics, type GridMetrics } from './useGridMetrics';
|
||||
|
||||
export interface CellPosition {
|
||||
col: number;
|
||||
@@ -70,70 +69,42 @@ export function useDragSprite(options: DragSpriteOptions) {
|
||||
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
|
||||
const highlightCell = ref<CellPosition | null>(null);
|
||||
|
||||
// Cache for max dimensions
|
||||
const lastMaxWidth = ref(1);
|
||||
const lastMaxHeight = ref(1);
|
||||
// Use the new useGridMetrics composable for consistent calculations
|
||||
const gridMetricsComposable = useGridMetrics({
|
||||
layers: options.layers,
|
||||
sprites: options.sprites,
|
||||
negativeSpacingEnabled: options.negativeSpacingEnabled,
|
||||
manualCellSizeEnabled: options.manualCellSizeEnabled,
|
||||
manualCellWidth: options.manualCellWidth,
|
||||
manualCellHeight: options.manualCellHeight,
|
||||
});
|
||||
|
||||
const calculateMaxDimensions = () => {
|
||||
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
||||
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 };
|
||||
const calculateMaxDimensions = (): GridMetrics => {
|
||||
return gridMetricsComposable.calculateCellDimensions();
|
||||
};
|
||||
|
||||
// Computed sprite positions
|
||||
const spritePositions = computed<SpritePosition[]>(() => {
|
||||
const sprites = getSprites();
|
||||
const columns = getColumns();
|
||||
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
|
||||
const metrics = calculateMaxDimensions();
|
||||
|
||||
return sprites.map((sprite, index) => {
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
const cellPos = gridMetricsComposable.getCellPosition(index, columns, metrics);
|
||||
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 {
|
||||
id: sprite.id,
|
||||
canvasX: Math.round(col * maxWidth + negativeSpacing + sprite.x),
|
||||
canvasY: Math.round(row * maxHeight + negativeSpacing + sprite.y),
|
||||
cellX: Math.round(col * maxWidth),
|
||||
cellY: Math.round(row * maxHeight),
|
||||
canvasX: canvasPos.canvasX,
|
||||
canvasY: canvasPos.canvasY,
|
||||
cellX: canvasPos.cellX,
|
||||
cellY: canvasPos.cellY,
|
||||
width: Math.round(sprite.width),
|
||||
height: Math.round(sprite.height),
|
||||
maxWidth: Math.round(maxWidth),
|
||||
maxHeight: Math.round(maxHeight),
|
||||
col,
|
||||
row,
|
||||
maxWidth: metrics.maxWidth,
|
||||
maxHeight: metrics.maxHeight,
|
||||
col: cellPos.col,
|
||||
row: cellPos.row,
|
||||
index,
|
||||
x: sprite.x,
|
||||
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