98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import type { Point, Rect, ShapeData } from '../types';
|
|
import { colliderCorners, containsWorldPoint } from './collision';
|
|
|
|
export class Shape {
|
|
type: ShapeData['type'];
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
rotation: number;
|
|
points: Point[];
|
|
|
|
constructor(data: ShapeData) {
|
|
this.type = data.type === 'circle' ? 'oval' : data.type;
|
|
this.x = data.x;
|
|
this.y = data.y;
|
|
this.width = Math.max(1, data.width);
|
|
this.height = Math.max(1, data.height);
|
|
this.rotation = data.rotation ?? 0;
|
|
this.points = data.points ? data.points.map((p) => ({ x: p.x, y: p.y })) : [];
|
|
}
|
|
|
|
clone(): Shape {
|
|
return new Shape({
|
|
type: this.type,
|
|
x: this.x,
|
|
y: this.y,
|
|
width: this.width,
|
|
height: this.height,
|
|
rotation: this.rotation,
|
|
points: this.points.map((p) => ({ x: p.x, y: p.y })),
|
|
});
|
|
}
|
|
|
|
getCenter(): Point {
|
|
return { x: this.x + this.width / 2, y: this.y + this.height / 2 };
|
|
}
|
|
|
|
getBounds(): Rect {
|
|
if (this.type === 'polygon' && this.points.length > 0) {
|
|
const xs = this.points.map((p) => p.x);
|
|
const ys = this.points.map((p) => p.y);
|
|
const minX = Math.min(...xs);
|
|
const maxX = Math.max(...xs);
|
|
const minY = Math.min(...ys);
|
|
const maxY = Math.max(...ys);
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
}
|
|
if (this.rotation % 360 === 0) {
|
|
return { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
}
|
|
const corners = colliderCorners({
|
|
x: this.x,
|
|
y: this.y,
|
|
width: this.width,
|
|
height: this.height,
|
|
rotation: this.rotation,
|
|
});
|
|
const xs = corners.map((p) => p.x);
|
|
const ys = corners.map((p) => p.y);
|
|
const minX = Math.min(...xs);
|
|
const maxX = Math.max(...xs);
|
|
const minY = Math.min(...ys);
|
|
const maxY = Math.max(...ys);
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
}
|
|
|
|
containsPoint(p: Point): boolean {
|
|
if (this.type === 'polygon' && this.points.length > 0) {
|
|
let inside = false;
|
|
for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) {
|
|
const xi = this.points[i].x;
|
|
const yi = this.points[i].y;
|
|
const xj = this.points[j].x;
|
|
const yj = this.points[j].y;
|
|
const intersect = yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / ((yj - yi) || 1e-9) + xi;
|
|
if (intersect) inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
const c = { x: this.x, y: this.y, width: this.width, height: this.height, rotation: this.rotation };
|
|
if (this.type === 'oval') {
|
|
const center = this.getCenter();
|
|
const rad = (-this.rotation * Math.PI) / 180;
|
|
const cos = Math.cos(rad);
|
|
const sin = Math.sin(rad);
|
|
const dx = p.x - center.x;
|
|
const dy = p.y - center.y;
|
|
const lx = dx * cos - dy * sin;
|
|
const ly = dx * sin + dy * cos;
|
|
const rx = this.width / 2;
|
|
const ry = this.height / 2;
|
|
return (lx * lx) / (rx * rx) + (ly * ly) / (ry * ry) <= 1 + 1e-8;
|
|
}
|
|
return containsWorldPoint(c, p);
|
|
}
|
|
}
|