import { Component, ComponentNames } from '.'; import type { Coord2D, Dimension2D } from '../interfaces'; import { dotProduct, rotateVector } from '../utils'; export class BoundingBox extends Component { public center: Coord2D; public dimension: Dimension2D; public rotation: number; constructor(center: Coord2D, dimension: Dimension2D, rotation?: number) { super(ComponentNames.BoundingBox); this.center = center; this.dimension = dimension; this.rotation = rotation ?? 0; } public isCollidingWith(box: BoundingBox): boolean { // optimization; when neither rotates just check if they overlap if (this.rotation == 0 && box.rotation == 0) { const thisTopLeft = this.getTopLeft(); const thisBottomRight = this.getBottomRight(); const thatTopLeft = box.getTopLeft(); const thatBottomRight = box.getBottomRight(); if ( thisBottomRight.x <= thatTopLeft.x || thisTopLeft.x >= thatBottomRight.x || thisBottomRight.y <= thatTopLeft.y || thisTopLeft.y >= thatBottomRight.y ) { return false; } return true; } // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { for (let i = 0; i < poly.length; i++) { const [A, B] = [poly[i], poly[(i + 1) % poly.length]]; const normal: Coord2D = { x: B.y - A.y, y: A.x - B.x }; const [[minThis, maxThis], [minBox, maxBox]] = boxes.map((box) => box.reduce( ([min, max], vertex) => { const projection = dotProduct(normal, vertex); return [Math.min(min, projection), Math.max(max, projection)]; }, [Infinity, -Infinity] ) ); if (maxThis < minBox || maxBox < minThis) return false; } } return true; } public getVertices(): Coord2D[] { return [ { x: -this.dimension.width / 2, y: -this.dimension.height / 2 }, { x: -this.dimension.width / 2, y: this.dimension.height / 2 }, { x: this.dimension.width / 2, y: this.dimension.height / 2 }, { x: this.dimension.width / 2, y: -this.dimension.height / 2 } ] .map((vertex) => rotateVector(vertex, this.rotation)) // rotate .map((vertex) => { // translate return { x: vertex.x + this.center.x, y: vertex.y + this.center.y }; }); } public getRotationInPiOfUnitCircle(): number { let rads = this.rotation * (Math.PI / 180); if (rads >= Math.PI) { // Physics system guarantees rotation \in [0, 360) rads -= Math.PI; } return rads; } public getOutscribedBoxDims(): Dimension2D { let rads = this.getRotationInPiOfUnitCircle(); const { width, height } = this.dimension; if (rads == 0) return this.dimension; if (rads <= Math.PI / 2) { return { width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)), height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)) }; } rads -= Math.PI / 2; return { width: Math.abs(height * Math.cos(rads) + width * Math.sin(rads)), height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)) }; } public getTopLeft(): Coord2D { return { x: this.center.x - this.dimension.width / 2, y: this.center.y - this.dimension.height / 2 }; } public getBottomRight(): Coord2D { return { x: this.center.x + this.dimension.width / 2, y: this.center.y + this.dimension.height / 2 }; } }