239 lines
5.0 KiB
TypeScript
239 lines
5.0 KiB
TypeScript
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<string>,
|
|
): 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 validateShapePlacement(
|
|
shape: ParsedShape,
|
|
transform: Transform2D,
|
|
boardWidth: number,
|
|
boardHeight: number,
|
|
occupiedCells: Set<string>,
|
|
): { 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,
|
|
};
|
|
}
|