import { Vector2D } from '../Vector2D'; import { Matrix } from '../Matrix'; import { Shape } from './Shape'; import { Point } from './Point'; import { Line } from './Line'; import { Circle } from './Circle'; import { Polygon } from './Polygon'; import { Polyline } from './Polyline'; import { intersectPointRectangleHelper, intersectLineRectangleHelper, intersectRectangleCircleHelper, intersectPolygonRectangleHelper, intersectPolylineRectangleHelper } from './internal/intersections'; export class Rectangle extends Shape { p: Vector2D; s: Vector2D; constructor(p = new Vector2D(), s = new Vector2D()) { super(); this.p = p; this.s = s; } get x() { return this.p.x; } set x(x: number) { this.p.x = x; } get y() { return this.p.y; } set y(y: number) { this.p.y = y; } get width() { return this.s.x; } set width(width: number) { this.s.x = width; } get height() { return this.s.y; } set height(height: number) { this.s.y = height; } get size() { return this.s; } set size(size: Vector2D) { this.s = size; } left() { return this.x; } setLeft(x: number) { const dx = x - this.x; this.x += dx; this.width -= dx; } moveLeft(x: number) { this.x = x; } top() { return this.y; } setTop(y: number) { const dy = y - this.y; this.y += dy; this.height -= dy; } moveTop(y: number) { this.y = y; } right() { return this.x + this.width; } setRight(x: number) { this.width = x - this.x; } moveRight(x: number) { this.x = x - this.width; } bottom() { return this.y + this.height; } setBottom(y: number) { this.height = y - this.y; } moveBottom(y: number) { this.y = y - this.height; } topLeft() { return new Vector2D(this.x, this.y); } setTopLeft(p: Vector2D) { this.setLeft(p.x); this.setTop(p.y); } moveTopLeft(p: Vector2D) { this.moveLeft(p.x); this.moveTop(p.y); } topRight() { return new Vector2D(this.x + this.width, this.y); } setTopRight(p: Vector2D) { this.setRight(p.x); this.setTop(p.y); } moveTopRight(p: Vector2D) { this.moveRight(p.x); this.moveTop(p.y); } bottomRight() { return new Vector2D(this.x + this.width, this.y + this.height); } setBottomRight(p: Vector2D) { this.setRight(p.x); this.setBottom(p.y); } moveBottomRight(p: Vector2D) { this.moveRight(p.x); this.moveBottom(p.y); } bottomLeft() { return new Vector2D(this.x, this.y + this.height); } setBottomLeft(p: Vector2D) { this.setLeft(p.x); this.setBottom(p.y); } moveBottomLeft(p: Vector2D) { this.moveLeft(p.x); this.moveBottom(p.y); } center() { return new Vector2D(this.x + this.width / 2, this.y + this.height / 2); } moveCenter(p: Vector2D) { this.x = p.x - this.width / 2; this.y = p.y - this.height / 2; } clone() { return new Rectangle(this.p.clone(), this.s.clone()); } isNull() { return this.s.isNull(); } fuzzyIsNull(epsilon?: number) { return this.s.fuzzyIsNull(epsilon); } equals(other: Rectangle) { return this.p.equals(other.p) && this.s.equals(other.s); } fuzzyEquals(other: Rectangle, epsilon?: number) { return this.p.fuzzyEquals(other.p, epsilon) && this.s.fuzzyEquals(other.s, epsilon); } isValid() { return this.width > 0 && this.height > 0; } translate(dx: number, dy: number) { this.p.translate(dx, dy); return this; } translated(dx: number, dy: number) { return new Rectangle(this.p.translated(dx, dy), this.s.clone()); } transform(matrix: Matrix) { if (matrix.m12 === 0 && matrix.m21 === 0) { let x = matrix.m11 * this.x + matrix.dx; let y = matrix.m22 * this.y + matrix.dy; let w = matrix.m11 * this.width; let h = matrix.m22 * this.height; if (w < 0) { w = -w; x -= w; } if (h < 0) { h = -h; y -= h; } this.p.x = x; this.p.y = y; this.s.x = w; this.s.y = h; return this; } let p = this.topLeft().transformed(matrix); let xmin = p.x; let ymin = p.y; let xmax = p.x; let ymax = p.y; p = this.topRight().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); p = this.bottomRight().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); p = this.bottomLeft().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); this.p.x = xmin; this.p.y = ymin; this.s.x = xmax - xmin; this.s.y = ymax - ymin; return this; } transformed(matrix: Matrix) { if (matrix.m12 === 0 && matrix.m21 === 0) { let x = matrix.m11 * this.x + matrix.dx; let y = matrix.m22 * this.y + matrix.dy; let w = matrix.m11 * this.width; let h = matrix.m22 * this.height; if (w < 0) { w = -w; x -= w; } if (h < 0) { h = -h; y -= h; } return new Rectangle(new Vector2D(x, y), new Vector2D(w, h)); } let p = this.topLeft().transformed(matrix); let xmin = p.x; let ymin = p.y; let xmax = p.x; let ymax = p.y; p = this.topRight().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); p = this.bottomRight().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); p = this.bottomLeft().transformed(matrix); xmin = Math.min(xmin, p.x); ymin = Math.min(ymin, p.y); xmax = Math.max(xmax, p.x); ymax = Math.max(ymax, p.y); return new Rectangle(new Vector2D(xmin, ymin), new Vector2D(xmax - xmin, ymax - ymin)); } draw(context: CanvasRenderingContext2D, stroked?: boolean, filled?: boolean) { if (filled) context.fillRect(this.x, this.y, this.width, this.height); if (stroked || !filled) { context.beginPath(); context.rect(this.x, this.y, this.width, this.height); context.stroke(); } } adjusted(dx1: number, dy1: number, dx2: number, dy2: number) { return new Rectangle(new Vector2D(this.x + dx1, this.y + dy1), new Vector2D(this.width + dx2 - dx1, this.height + dy2 - dy1)); } united(other: Rectangle) { let left = this.x; let right = this.x; if (this.width < 0) left += this.width; else right += this.width; if (other.width < 0) { left = Math.min(left, other.x + other.width); right = Math.max(right, other.x); } else { left = Math.min(left, other.x); right = Math.max(right, other.x + other.width); } let top = this.y; let bottom = this.y; if (this.height < 0) top += this.height; else bottom += this.height; if (other.height < 0) { top = Math.min(top, other.y + other.height); bottom = Math.max(bottom, other.y); } else { top = Math.min(top, other.y); bottom = Math.max(bottom, other.y + other.height); } return new Rectangle(new Vector2D(left, top), new Vector2D(right - left, bottom - top)); } intersected(other: Rectangle) { let l1 = this.x; let r1 = this.x; if (this.width < 0) l1 += this.width; else r1 += this.width; let l2 = other.x; let r2 = other.x; if (other.width < 0) l2 += other.width; else r2 += other.width; if (l1 > r2 || l2 > r1) return new Rectangle(); let t1 = this.y; let b1 = this.y; if (this.height < 0) t1 += this.height; else b1 += this.height; let t2 = other.y; let b2 = other.y; if (other.height < 0) t2 += other.height; else b2 += other.height; if (t1 > b2 || t2 > b1) return new Rectangle(); const p = new Vector2D(Math.max(l1, l2), Math.max(t1, t2)); const s = new Vector2D(Math.min(r1, r2) - p.x, Math.min(b1, b2) - p.y); return new Rectangle(p, s); } normalized() { const r = this.clone(); if (r.width < 0) { r.x += r.width; r.width = -r.width; } if (r.height < 0) { r.y += r.height; r.height = -r.height; } return r; } boundingRectangle() { return this.normalized(); } map(callbackfn: (value: Vector2D, index: number, rect: Rectangle) => Vector2D, thisArg?: any) { return new Rectangle(callbackfn.call(thisArg, this.p, 0, this), callbackfn.call(thisArg, this.s, 1, this)); } containsPoint(other: Point, epsilon?: number) { let l = this.x; let r = this.x; if (this.width < 0) l += this.width; else r += this.width; if (other.x <= l || other.x >= r) return false; let t = this.y; let b = this.y; if (this.height < 0) t += this.height; else b += this.height; if (other.y <= t || other.y >= b) return false; return true; } containsLine(other: Line, epsilon?: number) { return containsPointHelper(this, other.p1, epsilon) && containsPointHelper(this, other.p2, epsilon); } containsCircle(other: Circle, epsilon?: number) { const center = this.center(); return Math.abs(other.cx - center.x) + other.r < Math.abs(this.width / 2) && Math.abs(other.cy - center.y) + other.r < Math.abs(this.height / 2); } containsRectangle(other: Rectangle, epsilon?: number) { let l1 = this.x; let r1 = this.x; if (this.width < 0) l1 += this.width; else r1 += this.width; let l2 = other.x; let r2 = other.x; if (other.width < 0) l2 += other.width; else r2 += other.width; if (l2 <= l1 || r2 >= r1) return false; let t1 = this.y; let b1 = this.y; if (this.height < 0) t1 += this.height; else b1 += this.height; let t2 = other.y; let b2 = other.y; if (other.height < 0) t2 += other.height; else b2 += other.height; if (t2 <= t1 || b2 >= b1) return false; return true; } containsPolygon(other: Polygon, epsilon?: number) { if (other.isEmpty()) return false; for (let i = 0; i < other.points.length; i++) if (!containsPointHelper(this, other.points[i], epsilon)) return false; return true; } containsPolyline(other: Polyline, epsilon?: number) { if (other.isEmpty()) return false; for (let i = 0; i < other.points.length; i++) if (!containsPointHelper(this, other.points[i], epsilon)) return false; return true; } contains(other: Shape, epsilon?: number) { return other.containsRectangle(this, epsilon); } intersectsPoint(other: Point, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPointRectangleHelper(this, other, other.toVector2D(), this.topLeft(), this.topRight(), this.bottomRight(), this.bottomLeft(), callbackfn, thisArg, epsilon); } intersectsLine(other: Line, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectLineRectangleHelper(this, other, other.p1, other.p2, this.topLeft(), this.topRight(), this.bottomRight(), this.bottomLeft(), callbackfn, thisArg, epsilon); } intersectsCircle(other: Circle, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectRectangleCircleHelper(this, other, this.topLeft(), this.topRight(), this.bottomRight(), this.bottomLeft(), other.c, other.r, callbackfn, thisArg, epsilon); } intersectsRectangle(other: Rectangle, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { const thisTopLeft = this.topLeft(); const thisTopRight = this.topRight(); const thisBottomRight = this.bottomRight(); const thisBottomLeft = this.bottomLeft(); const otherTopLeft = other.topLeft(); const otherTopRight = other.topRight(); const otherBottomRight = other.bottomRight(); const otherBottomLeft = other.bottomLeft(); return intersectLineRectangleHelper(this, other, thisTopLeft, thisTopRight, otherTopLeft, otherTopRight, otherBottomRight, otherBottomLeft, callbackfn, thisArg, epsilon) || intersectLineRectangleHelper(this, other, thisTopRight, thisBottomRight, otherTopLeft, otherTopRight, otherBottomRight, otherBottomLeft, callbackfn, thisArg, epsilon) || intersectLineRectangleHelper(this, other, thisBottomRight, thisBottomLeft, otherTopLeft, otherTopRight, otherBottomRight, otherBottomLeft, callbackfn, thisArg, epsilon) || intersectLineRectangleHelper(this, other, thisBottomLeft, thisTopLeft, otherTopLeft, otherTopRight, otherBottomRight, otherBottomLeft, callbackfn, thisArg, epsilon); } intersectsPolygon(other: Polygon, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPolygonRectangleHelper(this, other, other.points, this.topLeft(), this.topRight(), this.bottomRight(), this.bottomLeft(), callbackfn, thisArg, epsilon); } intersectsPolyline(other: Polyline, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPolylineRectangleHelper(this, other, other.points, this.topLeft(), this.topRight(), this.bottomRight(), this.bottomLeft(), callbackfn, thisArg, epsilon); } intersects(other: Shape, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number) { return other.intersectsRectangle(this, callbackfn, thisArg, epsilon); } static create(topLeft?: Vector2D, size?: Vector2D) { return new Rectangle(topLeft, size); } static readonly className: string = 'Rectangle'; } function containsPointHelper(rect: Rectangle, point: Vector2D, epsilon?: number) { let l = rect.x; let r = rect.x; if (rect.width < 0) l += rect.width; else r += rect.width; if (point.x <= l || point.x >= r) return false; let t = rect.y; let b = rect.y; if (rect.height < 0) t += rect.height; else b += rect.height; if (point.y <= t || point.y >= b) return false; return true; }