import { fuzzyEqual } from '../../math/float'; import { Vector2D } from '../Vector2D'; import { Matrix } from '../Matrix'; import { intersectLineLine } from '../intersections'; import { Shape } from './Shape'; import { Point } from './Point'; import { Line } from './Line'; import { Circle } from './Circle'; import { Rectangle } from './Rectangle'; import { Polyline } from './Polyline'; import { intersectPointPolygonHelper, intersectLinePolygonHelper, intersectPolygonCircleHelper, intersectPolygonRectangleHelper, intersectPolylinePolygonHelper } from './internal/intersections'; export const enum FillRule { OddEven, Winding } export class Polygon extends Shape { points: Vector2D[]; constructor(points: Vector2D[] = []) { super(); this.points = points; } get size() { return this.points.length; } set size(size: number) { this.points.length = size; } clone() { return new Polygon(this.points.map(p => p.clone())); } isEmpty() { return this.points.length === 0; } first() { return this.points[0]; } last() { return this.points[this.points.length - 1]; } isNull() { if (this.points.length === 0) return true; let p1 = this.points[0]; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (!p1.equals(p2)) return false; p1 = p2; } return true; } fuzzyIsNull(epsilon?: number) { if (this.points.length === 0) return true; let p1 = this.points[0]; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (!p1.fuzzyEquals(p2, epsilon)) return false; p1 = p2; } return true; } equals(other: Polygon) { if (this.points.length !== other.points.length) return false; for (let i = 0; i < this.points.length; i++) if (!this.points[i].equals(other.points[i])) return false; return true; } fuzzyEquals(other: Polygon, epsilon?: number) { if (this.points.length !== other.points.length) return false; for (let i = 0; i < this.points.length; i++) if (!this.points[i].fuzzyEquals(other.points[i], epsilon)) return false; return true; } translate(dx: number, dy: number) { for (let i = 0; i < this.points.length; i++) this.points[i].translate(dx, dy); return this; } translated(dx: number, dy: number) { return new Polygon(this.points.map(p => p.translated(dx, dy))); } transform(matrix: Matrix) { for (let i = 0; i < this.points.length; i++) this.points[i].transform(matrix); return this; } transformed(matrix: Matrix) { return new Polygon(this.points.map(p => p.transformed(matrix))); } draw(context: CanvasRenderingContext2D, stroked?: boolean, filled?: boolean) { if (this.points.length < 2) return; context.beginPath(); context.moveTo(this.points[0].x, this.points[0].y); for (let i = 1; i < this.points.length; i++) context.lineTo(this.points[i].x, this.points[i].y); context.closePath(); if (filled) context.fill(); if (stroked || !filled) context.stroke(); } forEach(callbackfn: (point: Vector2D, index: number, polygon: Polygon) => void, thisArg?: any) { for (let i = 0; i < this.points.length; i++) callbackfn.call(thisArg, this.points[i], i, this); } map(callbackfn: (point: Vector2D, index: number, polygon: Polygon) => Vector2D, thisArg?: any) { return new Polygon(this.points.map((point, index) => callbackfn.call(thisArg, point, index, this))); } get(index: number): Vector2D { return this.points[index]; } set(index: number, point: Vector2D) { this.points[index] = point; } boundingRectangle() { if (this.points.length === 0) return new Rectangle(); const p = this.points[0]; let minx, maxx, miny, maxy; minx = maxx = p.x; miny = maxy = p.y; for (let i = 1; i < this.points.length; i++) { const p = this.points[i]; if (p.x < minx) minx = p.x; else if (p.x > maxx) maxx = p.x; if (p.y < miny) miny = p.y; else if (p.y > maxy) maxy = p.y; } return new Rectangle(new Vector2D(minx, miny), new Vector2D(maxx - minx, maxy - miny)); } raycast(point: Vector2D, fillRule = FillRule.OddEven, epsilon?: number) { if (this.points.length === 0) return false; let windingCount = 0; let last = this.points[0]; const first = this.points[0]; for (let i = 1; i < this.points.length; ++i) { const curr = this.points[i]; windingCount += raycastHelper(last, curr, point, epsilon); last = curr; } if (!last.fuzzyEquals(first, epsilon)) windingCount += raycastHelper(last, first, point, epsilon); return fillRule == FillRule.Winding ? (windingCount !== 0) : ((windingCount % 2) !== 0); } containsPoint(other: Point, epsilon?: number) { return this.raycast(other.toVector2D(), FillRule.OddEven, epsilon); } containsLine(other: Line, epsilon?: number) { if (this.points.length === 0) return false; const p3 = other.p1; const p4 = other.p2; const first = this.points[0]; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (intersectLineLine(p1, p2, p3, p4, epsilon)) return false; p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) // enclosing subpath case if (intersectLineLine(p1, first, p3, p4, epsilon)) return false; return this.raycast(p3, FillRule.OddEven, epsilon); } containsCircle(other: Circle, epsilon?: number) { if (this.points.length === 0) return false; const first = this.points[0]; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (other.c.distanceToLine(p1, p2.minus(p1).normalized(epsilon), epsilon) <= other.r) return false; p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) // enclosing subpath case if (other.c.distanceToLine(p1, first.minus(p1).normalized(epsilon), epsilon) <= other.r) return false; return this.raycast(other.c, FillRule.OddEven, epsilon); } containsRectangle(other: Rectangle, epsilon?: number) { if (this.points.length === 0) return false; const topLeft = other.topLeft(); const topRight = other.topRight(); const bottomRight = other.bottomRight(); const bottomLeft = other.bottomLeft(); const first = this.points[0]; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (intersectLineLine(p1, p2, topLeft, topRight, epsilon) || intersectLineLine(p1, p2, topRight, bottomRight, epsilon) || intersectLineLine(p1, p2, bottomRight, bottomLeft, epsilon) || intersectLineLine(p1, p2, bottomLeft, topLeft, epsilon)) return false; p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) { // enclosing subpath case if (intersectLineLine(p1, first, topLeft, topRight, epsilon) || intersectLineLine(p1, first, topRight, bottomRight, epsilon) || intersectLineLine(p1, first, bottomRight, bottomLeft, epsilon) || intersectLineLine(p1, first, bottomLeft, topLeft, epsilon)) return false; } return this.raycast(topLeft, FillRule.OddEven, epsilon); } containsPolygon(other: Polygon, epsilon?: number) { if (this.points.length === 0 || other.points.length === 0) return false; const first = this.points[0]; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (!containsPolygonHelper(other, p1, p2, epsilon)) return false; p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) { // enclosing subpath case const p2 = first; if (!containsPolygonHelper(other, p1, p2, epsilon)) return false; } return this.raycast(other.points[0], FillRule.OddEven, epsilon); } containsPolyline(other: Polyline, epsilon?: number) { if (this.points.length === 0 || other.points.length === 0) return false; const first = this.points[0]; let p1 = this.points[0]; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (!containsPolylineHelper(other, p1, p2, epsilon)) return false; p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) { // enclosing subpath case if (!containsPolylineHelper(other, p1, first, epsilon)) return false; } return this.raycast(other.points[0], FillRule.OddEven, epsilon); } contains(other: Shape, epsilon?: number) { return other.containsPolygon(this, epsilon); } intersectsPoint(other: Point, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPointPolygonHelper(this, other, other.toVector2D(), this.points, callbackfn, thisArg, epsilon); } intersectsLine(other: Line, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectLinePolygonHelper(this, other, other.p1, other.p2, this.points, callbackfn, thisArg, epsilon); } intersectsCircle(other: Circle, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPolygonCircleHelper(this, other, this.points, other.c, other.r, callbackfn, thisArg, epsilon); } intersectsRectangle(other: Rectangle, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPolygonRectangleHelper(this, other, this.points, other.topLeft(), other.topRight(), other.bottomRight(), other.bottomLeft(), callbackfn, thisArg, epsilon); } intersectsPolygon(other: Polygon, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { if (this.points.length === 0 || other.points.length === 0) return false; const first = this.points[0]; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; if (intersectLinePolygonHelper(this, other, p1, p2, other.points, callbackfn, thisArg, epsilon)) return true; p1 = p2; } return !p1.fuzzyEquals(first, epsilon) // this enclosing subpath case && intersectLinePolygonHelper(this, other, p1, first, other.points, callbackfn, thisArg, epsilon); } intersectsPolyline(other: Polyline, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number): boolean { return intersectPolylinePolygonHelper(this, other, other.points, this.points, callbackfn, thisArg, epsilon); } intersects(other: Shape, callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number) { return other.intersectsPolygon(this, callbackfn, thisArg, epsilon); } intersectsSelf(callbackfn?: (points: Vector2D[], thisShape: Shape, otherShape: Shape) => any, thisArg?: any, epsilon?: number) { if (this.points.length === 0) return false; const first = this.points[0]; const lastIndex = this.points.length - 1; let p1 = first; for (let i = 1; i < this.points.length; i++) { const p2 = this.points[i]; let p3 = p2; for (let j = i + 1; j < this.points.length; j++) { const p4 = this.points[j]; const points = intersectLineLine(p1, p2, p3, p4, epsilon); if (points !== undefined) { if (j - 1 === i // joint case && points[0].fuzzyEquals(p2, epsilon)) { points.shift(); p3 = p4; continue; } if (callbackfn === undefined || callbackfn.call(thisArg, points, this, this)) return true; } p3 = p4; } p1 = p2; } if (!p1.fuzzyEquals(first, epsilon)) { // enclosing subpath case const p2 = first; let p3 = p2; for (let i = 1; i < this.points.length; i++) { const p4 = this.points[i]; const points = intersectLineLine(p1, p2, p3, p4, epsilon); if (points !== undefined) { if (i === 1 // fisrt joint case && points[points.length - 1].fuzzyEquals(first, epsilon)) { points.pop(); p3 = p4; continue; } if (i === lastIndex // second joint case && points[0].fuzzyEquals(p4, epsilon)) { points.shift(); p3 = p4; continue; } if (callbackfn === undefined || callbackfn.call(thisArg, points, this, this)) return true; } p3 = p4; } } return false; } static create(points?: Vector2D[]) { return new Polygon(points); } static fromRectangle(rect: Rectangle) { return new Polygon([rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft()]); } static readonly className: string = 'Polygon'; } function raycastHelper(p1: Vector2D, p2: Vector2D, pos: Vector2D, epsilon?: number) { let x1 = p1.x; let y1 = p1.y; let x2 = p2.x; let y2 = p2.y; let dir = 1; const y = pos.y; if (fuzzyEqual(y1, y2, epsilon)) return 0; else if (y2 < y1) { const x_tmp = x2; x2 = x1; x1 = x_tmp; const y_tmp = y2; y2 = y1; y1 = y_tmp; dir = -1; } if (y >= y1 && y < y2) { const x = x1 + ((x2 - x1) / (y2 - y1)) * (y - y1); if (x <= pos.x) return dir; } return 0; } function containsPolygonHelper(otherShape: Polygon, p1: Vector2D, p2: Vector2D, epsilon?: number) { const first = otherShape.points[0]; let p3 = first; for (let j = 1; j < otherShape.points.length; j++) { const p4 = otherShape.points[j]; if (intersectLineLine(p1, p2, p3, p4, epsilon)) return false; p3 = p4; } return p3.fuzzyEquals(first, epsilon) // enclosing subpath case || !intersectLineLine(p1, p2, p3, first, epsilon); } function containsPolylineHelper(otherShape: Polyline, p1: Vector2D, p2: Vector2D, epsilon?: number) { let p3 = otherShape.points[0]; for (let j = 1; j < otherShape.points.length; j++) { const p4 = otherShape.points[j]; if (intersectLineLine(p1, p2, p3, p4, epsilon)) return false; p3 = p4; } return true; }