feat: add som shape parsing

This commit is contained in:
hyper 2026-04-12 20:44:35 +08:00
parent 18eae59891
commit 6281044f14
2 changed files with 231 additions and 2 deletions

View File

@ -8,6 +8,8 @@ export interface ParsedShape {
width: number; width: number;
/** Grid height (number of rows) */ /** Grid height (number of rows) */
height: number; height: number;
/** Number of occupied (filled) cells */
count: number;
/** Origin X coordinate within the grid */ /** Origin X coordinate within the grid */
originX: number; originX: number;
/** Origin Y coordinate within the grid */ /** Origin Y coordinate within the grid */
@ -76,7 +78,7 @@ export function parseShapeString(input: string): ParsedShape {
} }
if (filledPoints.size === 0) { if (filledPoints.size === 0) {
return { grid: [[]], width: 0, height: 1, originX: 0, originY: 0 }; return { grid: [[]], width: 0, height: 1, count: 0, originX: 0, originY: 0 };
} }
// Calculate bounding box to normalize the array // Calculate bounding box to normalize the array
@ -103,5 +105,5 @@ export function parseShapeString(input: string): ParsedShape {
const normalizedOriginX = originX - minX; const normalizedOriginX = originX - minX;
const normalizedOriginY = originY - minY; const normalizedOriginY = originY - minY;
return { grid, width, height, originX: normalizedOriginX, originY: normalizedOriginY }; return { grid, width, height, count: filledPoints.size, originX: normalizedOriginX, originY: normalizedOriginY };
} }

View File

@ -0,0 +1,227 @@
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 validatePlacement(
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,
};
}