import type { ParsedShape } from './parse-shape'; /** * Represents a 2D point in grid coordinates. */ export interface Point2D { x: number; y: number; } /** * 2D transformation to apply to a shape. */ export interface Transform2D { /** Translation offset in grid units */ offset: Point2D; /** Rotation in degrees (0, 90, 180, 270) */ rotation: number; /** Whether to flip horizontally */ flipX: boolean; /** Whether to flip vertically */ flipY: boolean; } /** * Default transform (identity). */ export const IDENTITY_TRANSFORM: Transform2D = { offset: { x: 0, y: 0 }, rotation: 0, flipX: false, flipY: false, }; /** * Gets all occupied cell coordinates from a shape. */ export function getOccupiedCells(shape: ParsedShape): Point2D[] { const cells: Point2D[] = []; for (let y = 0; y < shape.height; y++) { for (let x = 0; x < shape.width; x++) { if (shape.grid[y]?.[x]) { cells.push({ x, y }); } } } return cells; } /** * Applies a 2D transformation to a point. */ export function transformPoint( point: Point2D, transform: Transform2D, shapeWidth: number, shapeHeight: number ): Point2D { let { x, y } = point; // Apply flips if (transform.flipX) { x = shapeWidth - 1 - x; } if (transform.flipY) { y = shapeHeight - 1 - y; } // Apply rotation (around origin 0,0) const rotation = ((transform.rotation % 360) + 360) % 360; let rotatedX = x; let rotatedY = y; switch (rotation) { case 90: rotatedX = y; rotatedY = -x; break; case 180: rotatedX = -x; rotatedY = -y; break; case 270: rotatedX = -y; rotatedY = x; break; } // Apply offset return { x: rotatedX + transform.offset.x, y: rotatedY + transform.offset.y, }; } /** * Transforms a shape and returnss its occupied cells in world coordinates. */ export function transformShape(shape: ParsedShape, transform: Transform2D): Point2D[] { const cells = getOccupiedCells(shape); return cells.map(cell => transformPoint(cell, transform, shape.width, shape.height) ); } /** * Checks if two transformed shapes collide (share any occupied cell). */ export function checkCollision( shapeA: ParsedShape, transformA: Transform2D, shapeB: ParsedShape, transformB: Transform2D ): boolean { const cellsA = transformShape(shapeA, transformA); const cellsB = transformShape(shapeB, transformB); const setA = new Set(cellsA.map(c => `${c.x},${c.y}`)); for (const cell of cellsB) { if (setA.has(`${cell.x},${cell.y}`)) { return true; } } return false; } /** * Checks if a transformed shape collides with any occupied cells on a board. * @param shape The shape to check * @param transform The transform to apply to the shape * @param occupiedCells Set of occupied board cells in "x,y" format */ export function checkBoardCollision( shape: ParsedShape, transform: Transform2D, occupiedCells: Set ): boolean { const cells = transformShape(shape, transform); for (const cell of cells) { if (occupiedCells.has(`${cell.x},${cell.y}`)) { return true; } } return false; } /** * Checks if a transformed shape is within bounds. * @param shape The shape to check * @param transform The transform to apply to the shape * @param boardWidth Board width * @param boardHeight Board height */ export function checkBounds( shape: ParsedShape, transform: Transform2D, boardWidth: number, boardHeight: number ): boolean { const cells = transformShape(shape, transform); for (const cell of cells) { if (cell.x < 0 || cell.x >= boardWidth || cell.y < 0 || cell.y >= boardHeight) { return false; } } return true; } /** * Validates that a placement is both in bounds and collision-free. * @returns Object with `valid` flag and optional `reason` string */ export function validatePlacement( shape: ParsedShape, transform: Transform2D, boardWidth: number, boardHeight: number, occupiedCells: Set ): { valid: true } | { valid: false; reason: string } { if (!checkBounds(shape, transform, boardWidth, boardHeight)) { return { valid: false, reason: '超出边界' }; } if (checkBoardCollision(shape, transform, occupiedCells)) { return { valid: false, reason: '与已有形状重叠' }; } return { valid: true }; } /** * Rotates a transform by the given degrees. * @param current The current transform * @param degrees Degrees to rotate (typically 90, 180, or 270) */ export function rotateTransform(current: Transform2D, degrees: number): Transform2D { return { ...current, rotation: ((current.rotation + degrees) % 360 + 360) % 360, }; } /** * Flips a transform horizontally. */ export function flipXTransform(current: Transform2D): Transform2D { return { ...current, flipX: !current.flipX, }; } /** * Flips a transform vertically. */ export function flipYTransform(current: Transform2D): Transform2D { return { ...current, flipY: !current.flipY, }; }