import { Line } from "./line"; import { Vector2, IVector2 } from "./vector2"; /** * Immutable rectangle class. */ export class Rect { private _x: number; private _y: number; private _width: number; private _height: number; public get x(): number { return this._x; } public get y(): number { return this._y; } public get width(): number { return this._width; } public get height(): number { return this._height; } public get centerX(): number { return this._x + this._width / 2; } public get centerY(): number { return this._y + this._height / 2; } public get right(): number { return this._x + this._width; } public get bottom(): number { return this._y + this._height; } public get top(): number { return this._y; } public get left(): number { return this._x; } public get center(): Vector2 { return new Vector2({ x: this.x + this.width / 2, y: this.y + this.height / 2, }); } public get dimensions(): Vector2 { return new Vector2({ x: this.width, y: this.height }); } public static FromPoint(point: IVector2, size: number): Rect { return new Rect({ x: point.x, y: point.y, width: size, height: size, }); } public static FromPoints(p1: IVector2, p2: IVector2): Rect { return new Rect({ x: Math.min(p1.x, p2.x), y: Math.min(p1.y, p2.y), width: Math.abs(p1.x - p2.x), height: Math.abs(p1.y - p2.y), }); } public static FromCenter(props: { center: IVector2; dimensions: IVector2; }): Rect { return new Rect({ x: props.center.x - props.dimensions.x / 2, y: props.center.y - props.dimensions.y / 2, width: props.dimensions.x, height: props.dimensions.y, }); } public withRight(value: number): Rect { return new Rect({ x: this.x, y: this.y, width: value - this.x, height: this.height, }); } public withWidth(value: number): Rect { return new Rect({ x: this.x, y: this.y, width: value, height: this.height, }); } public withHeight(value: number): Rect { return new Rect({ x: this.x, y: this.y, width: this.width, height: value, }); } public withBottom(value: number): Rect { return new Rect({ x: this.x, y: this.y, width: this.width, height: value - this.y, }); } public withX(value: number): Rect { return new Rect({ x: value, y: this.y, width: this.width, height: this.height, }); } public withY(value: number): Rect { return new Rect({ x: this.x, y: value, width: this.width, height: this.height, }); } public withTop(value: number): Rect { return this.withY(value); } public withLeft(value: number): Rect { return this.withX(value); } /** * bottomRight is held constant. */ public withTopLeft(topLeft: IVector2): Rect { return Rect.FromPoints(topLeft, this.bottomRight); } /** * bottomLeft is held constant. */ public withTopRight(topRight: IVector2): Rect { return Rect.FromPoints(topRight, this.bottomLeft); } /** * topLeft is held constant. */ public withBottomRight(bottomRight: IVector2): Rect { return Rect.FromPoints(bottomRight, this.topLeft); } /** * topRight is held constant. */ public withBottomLeft(bottomLeft: IVector2): Rect { return Rect.FromPoints(bottomLeft, this.topRight); } public withCenter(center: IVector2): Rect { return new Rect({ x: center.x - this.width / 2, y: center.y - this.height / 2, width: this.width, height: this.height, }); } /** * center is held constant */ public withScale(props: { width?: number; height?: number }): Rect { return new Rect({ x: this.centerX - (props.width || this.width) / 2, y: this.centerY - (props.height || this.height) / 2, width: props.width || this.width, height: props.height || this.height, }); } public get topLeft(): Vector2 { return new Vector2({ x: this.x, y: this.y, }); } public get topRight(): Vector2 { return new Vector2({ x: this.right, y: this.y, }); } public get bottomRight(): Vector2 { return new Vector2({ x: this.right, y: this.bottom, }); } public get bottomLeft(): Vector2 { return new Vector2({ x: this.x, y: this.bottom, }); } constructor(props: { x: number; y: number; width: number; height: number }) { this._x = props.x; this._y = props.y; this._width = props.width; this._height = props.height; } static DeserializeRect(s: string): Rect { const [x, y, w, h] = s.split("|").map((x) => Number(x)); return new Rect({ x, y, width: w, height: h }); } /** * Return the four edges of this Rect as Lines. */ getLinesFromRect(): Line[] { return [ new Line({ x1: this.x, y1: this.y, x2: this.x + this.width, y2: this.y }), new Line({ x1: this.x, y1: this.y, x2: this.x, y2: this.y + this.height, }), new Line({ x1: this.x + this.width, y1: this.y + this.height, x2: this.x + this.width, y2: this.y, }), new Line({ x1: this.x + this.width, y1: this.y + this.height, x2: this.x, y2: this.y + this.height, }), ]; } /** * Return the four corners of this Rect. */ getCorners(): Vector2[] { return [ new Vector2({ x: this.x, y: this.y }), new Vector2({ x: this.x + this.width, y: this.y }), new Vector2({ x: this.x, y: this.y + this.height }), new Vector2({ x: this.x + this.width, y: this.y + this.height }), ]; } serialize(): string { return `${this.x}|${this.y}|${this.width}|${this.height}`; } // consider overlapping edges as intersection, but not overlapping corners. intersects( other: Rect, props: { edgesOnlyIsAnIntersection: boolean } = { edgesOnlyIsAnIntersection: false, } ): boolean { const intersection = this.getIntersection(other, true); if (props.edgesOnlyIsAnIntersection) { return ( !!intersection && (intersection.width > 0 || intersection.height > 0) ); } else { return !!intersection && intersection.width * intersection.height > 0; } } completelyContains(smaller: Rect): boolean { return ( this.x <= smaller.x && this.x + this.width >= smaller.x + smaller.width && this.y <= smaller.y && this.y + this.height >= smaller.y + smaller.height ); } getIntersection( other: Rect, edgesOnlyIsAnIntersection = false ): Rect | undefined { const xmin = Math.max(this.x, other.x); const xmax1 = this.x + this.width; const xmax2 = other.x + other.width; const xmax = Math.min(xmax1, xmax2); if (xmax > xmin || (edgesOnlyIsAnIntersection && xmax >= xmin)) { const ymin = Math.max(this.y, other.y); const ymax1 = this.y + this.height; const ymax2 = other.y + other.height; const ymax = Math.min(ymax1, ymax2); if (ymax >= ymin || (edgesOnlyIsAnIntersection && ymax >= ymin)) { return new Rect({ x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin, }); } } return undefined; } contains(p: IVector2): boolean { return ( p.x >= this.x && p.x < this.x + this.width && p.y >= this.y && p.y < this.y + this.height ); } clone(): Rect { return new Rect({ x: this.x, y: this.y, width: this.width, height: this.height, }); } add(p: IVector2): Rect { return this.translate(p); } subtract(p: IVector2): Rect { return this.translate({ x: -p.x, y: -p.y }); } translate(p: IVector2): Rect { return new Rect({ x: this.x + p.x, y: this.y + p.y, width: this.width, height: this.height, }); } scale(p: Vector2): Rect { return new Rect({ x: this.x, y: this.y, width: this.width * p.x, height: this.height * p.y, }); } centeredAtOrigin(): Rect { return new Rect({ x: -this.width / 2, y: -this.height / 2, width: this.width, height: this.height, }); } equals(o: Rect | undefined | null): boolean { if (!o) { return false; } return ( this.x === o.x && this.y === o.y && this.width === o.width && this.height === o.height ); } toJSON(): any { return { x: this.x, y: this.y, w: this.width, h: this.height, reviver: "Rect", }; } hash(): string { return this.toString(); } /** * Adds amount to both width and height. */ extend(amount: number): Rect { return new Rect({ x: this.x, y: this.y, width: this.width + amount, height: this.height + amount, }); } shrink(amount: number): Rect { return new Rect({ x: this.x + amount, y: this.y + amount, width: Math.max(this.width - amount * 2, 0), height: Math.max(this.height - amount * 2, 0), }); } floor(): Rect { return new Rect({ x: Math.floor(this.x), y: Math.floor(this.y), width: Math.floor(this.width), height: Math.floor(this.height), }); } /** * Grow the Rect by amount in all directions. */ expand(amount: number): Rect { return this.shrink(-amount); } transform(trans: Vector2, scale: number): Rect { const topLeft = this.topLeft.transform(trans, scale); const botRight = this.bottomRight.transform(trans, scale); return new Rect({ x: topLeft.x, y: topLeft.y, width: botRight.x - topLeft.x, height: botRight.y - topLeft.y, }); } static Deserialize(obj: any): Rect { if ( !obj.hasOwnProperty("x") || !obj.hasOwnProperty("y") || !obj.hasOwnProperty("w") || !obj.hasOwnProperty("h") ) { console.error("Failed deserializing Rect"); } return new Rect({ x: obj.x, y: obj.y, width: obj.w, height: obj.h, }); } static Serialize(r: Rect): string { return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, }); } toString(): string { return `[${this.x}, ${this.y}]`; } }