romgrk-2d-geometry
Version:
Javascript library for 2d geometry
706 lines (616 loc) • 25.1 kB
text/typescript
import {ray_shoot} from "../algorithms/ray_shooting";
import * as Utils from '../utils/utils'
import * as Distance from '../algorithms/distance'
import * as Intersection from "../algorithms/intersection";
import * as Relations from "../algorithms/relation";
import {
addToIntPoints, calculateInclusionFlags, filterDuplicatedIntersections,
getSortedArray, getSortedArrayOnLine, initializeInclusionFlags, insertBetweenIntPoints,
splitByIntersections
} from "../data_structures/smart_intersections";
import {Multiline} from "./Multiline";
import {intersectEdge2Line} from "../algorithms/intersection";
import {INSIDE, BOUNDARY} from "../utils/constants";
import {convertToString} from "../utils/attributes";
import { PlanarSet } from '../data_structures/PlanarSet';
import * as geom from '../classes'
const isPointLike = (n: any): n is [number, number] =>
Array.isArray(n) && n.length === 2 && typeof n[0] === 'number' && typeof n[1] === 'number'
const isPoints = (e: any): e is [number, number][] => Array.isArray(e) && e.every(isPointLike)
/**
* Class representing a polygon.<br/>
* Polygon in FlattenJS is a multipolygon comprised from a set of [faces]{@link geom.Face}. <br/>
* Face, in turn, is a closed loop of [edges]{@link geom.Edge}, where edge may be segment or circular arc<br/>
* @type {Polygon}
*/
export class Polygon {
static EMPTY = Object.freeze(new Polygon([]));
/**
* Container of faces (closed loops), may be empty
*/
faces: PlanarSet
/**
* Container of edges
*/
edges: PlanarSet
/**
* Constructor creates new instance of polygon. With no arguments new polygon is empty.<br/>
* Constructor accepts as argument array that define loop of shapes
* or array of arrays in case of multi polygon <br/>
* Loop may be defined in different ways: <br/>
* - array of shapes of type Segment or Arc <br/>
* - array of points (geom.Point) <br/>
* - array of numeric pairs which represent points <br/>
* - box or circle object <br/>
* Alternatively, it is possible to use polygon.addFace method
* @param {args} - array of shapes or array of arrays
*/
constructor(arg?: any) {
this.faces = new PlanarSet();
this.edges = new PlanarSet();
/* It may be array of something that may represent one loop (face) or
array of arrays that represent multiple loops
*/
if (Array.isArray(arg) && arg.every(Array.isArray)) {
const inputs = arg as any;
if (isPoints(inputs)) {
// one-loop polygon as array of pairs of numbers
this.faces.add(new geom.Face(this, inputs));
} else {
// multi-loop polygon
for (let loop of inputs) {
/* Check extra level of nesting for GeoJSON-style multi polygons */
if (Array.isArray(loop) && isPoints(loop[0])) {
for (let subloop of loop) {
this.faces.add(new geom.Face(this, subloop));
}
} else {
this.faces.add(new geom.Face(this, loop));
}
}
}
}
if (arg instanceof geom.Box) {
this.faces.add(new geom.Face(this, arg));
}
if (arg instanceof geom.Path) {
this.faces.add(new geom.Face(this, arg));
}
}
/**
* (Getter) Returns bounding box of the polygon
*/
get box() {
return [...this.faces].reduce((acc, face) => acc.merge(face.box), new geom.Box());
}
/**
* (Getter) Returns array of vertices
*/
get vertices() {
return [...this.edges].map(edge => edge.start);
}
/**
* Create new cloned instance of the polygon
*/
clone() {
const polygon = new Polygon();
for (let face of this.faces) {
polygon.addFace(face.shapes);
}
return polygon;
}
/**
* Return true is polygon has no edges
*/
isEmpty() {
return this.edges.size === 0;
}
/**
* Return true if polygon is valid for boolean operations
* Polygon is valid if <br/>
* 1. All faces are simple polygons (there are no self-intersected polygons) <br/>
* 2. All faces are orientable and there is no island inside island or hole inside hole - TODO <br/>
* 3. There is no intersections between faces (excluding touching) - TODO <br/>
* @returns {boolean}
*/
isValid() {
let valid = true;
// 1. Polygon is invalid if at least one face is not simple
for (let face of this.faces) {
if (!face.isSimple(this.edges)) {
valid = false;
break;
}
}
// 2. TODO: check if no island inside island and no hole inside hole
// 3. TODO: check the there is no intersection between faces
return valid;
}
/**
* Returns area of the polygon. Area of an island will be added, area of a hole will be subtracted
* @returns {number}
*/
area() {
let signedArea = [...this.faces].reduce((acc, face) => acc + face.signedArea(), 0);
return Math.abs(signedArea);
}
/**
* Add new face to polygon. Returns added face
* @param {Point[]|Segment[]|Arc[]|Circle|Box} args - new face may be create with one of the following ways: <br/>
* 1) array of points that describe closed path (edges are segments) <br/>
* 2) array of shapes (segments and arcs) which describe closed path <br/>
* 3) circle - will be added as counterclockwise arc <br/>
* 4) box - will be added as counterclockwise rectangle <br/>
* You can chain method face.reverse() is you need to change direction of the creates face
* @returns {Face}
*/
addFace(...args) {
// @ts-ignore
let face = new geom.Face(this, ...args);
this.faces.add(face);
return face;
}
/**
* Delete existing face from polygon
* @param {Face} face Face to be deleted
* @returns {boolean}
*/
deleteFace(face) {
for (let edge of face) {
this.edges.delete(edge);
}
return this.faces.delete(face);
}
/**
* Clear all faces and create new faces from edges
*/
recreateFaces() {
// Remove all faces
this.faces.clear();
for (let edge of this.edges) {
edge.face = null;
}
// Restore faces
let first;
let unassignedEdgeFound = true;
while (unassignedEdgeFound) {
unassignedEdgeFound = false;
for (let edge of this.edges) {
if (edge.face === null) {
first = edge;
unassignedEdgeFound = true;
break;
}
}
if (unassignedEdgeFound) {
let last = first;
do {
last = last.next;
} while (last.next !== first)
this.addFace(first, last);
}
}
}
/**
* Delete chain of edges from the face.
* @param {Face} face Face to remove chain
* @param {Edge} edgeFrom Start of the chain of edges to be removed
* @param {Edge} edgeTo End of the chain of edges to be removed
*/
removeChain(face, edgeFrom, edgeTo) {
// Special case: all edges removed
if (edgeTo.next === edgeFrom) {
this.deleteFace(face);
return;
}
for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) {
face.remove(edge);
this.edges.delete(edge); // delete from PlanarSet of edges and update index
if (face.isEmpty()) {
this.deleteFace(face); // delete from PlanarSet of faces and update index
break;
}
}
}
/**
* Add point as a new vertex and split edge. Point supposed to belong to an edge.
* When edge is split, new edge created from the start of the edge to the new vertex
* and inserted before current edge.
* Current edge is trimmed and updated.
* Method returns new edge added. If no edge added, it returns edge before vertex
* @param {Point} pt Point to be added as a new vertex
* @param {Edge} edge Edge to be split with new vertex and then trimmed from start
* @returns {Edge}
*/
addVertex(pt, edge) {
let shapes = edge.shape.split(pt);
// if (shapes.length < 2) return;
if (shapes[0] === null) // point incident to edge start vertex, return previous edge
return edge.prev;
if (shapes[1] === null) // point incident to edge end vertex, return edge itself
return edge;
let newEdge = new geom.Edge(shapes[0]);
let edgeBefore = edge.prev;
/* Insert first split edge into linked list after edgeBefore */
edge.face.insert(newEdge, edgeBefore);
// Remove old edge from edges container and 2d index
this.edges.delete(edge);
// Insert new edge to the edges container and 2d index
this.edges.add(newEdge);
// Update edge shape with second split edge keeping links
edge.shape = shapes[1];
// Add updated edge to the edges container and 2d index
this.edges.add(edge);
return newEdge;
}
/**
* Merge given edge with next edge and remove vertex between them
* @param {Edge} edge
*/
removeEndVertex(edge) {
const edge_next = edge.next
if (edge_next === edge) return
edge.face.merge_with_next_edge(edge)
this.edges.delete(edge_next)
}
/**
* Cut polygon with multiline and return array of new polygons
* Multiline should be constructed from a line with intersection point, see notebook:
* https://next.observablehq.com/@alexbol99/cut-polygon-with-line
* @param {Multiline} multiline
* @returns {Polygon[]}
*/
cut(multiline) {
let cutPolygons = [this.clone()];
for (let edge of multiline) {
if (edge.setInclusion(this) !== INSIDE)
continue;
let cut_edge_start = edge.shape.start;
let cut_edge_end = edge.shape.end;
let newCutPolygons = [];
for (let polygon of cutPolygons) {
if (polygon.findEdgeByPoint(cut_edge_start) === undefined) {
newCutPolygons.push(polygon);
} else {
let [cutPoly1, cutPoly2] = polygon.cutFace(cut_edge_start, cut_edge_end);
newCutPolygons.push(cutPoly1, cutPoly2);
}
}
cutPolygons = newCutPolygons;
}
return cutPolygons;
}
/**
* Cut face of polygon with a segment between two points and create two new polygons
* Supposed that a segments between points does not intersect any other edge
* @param {Point} pt1
* @param {Point} pt2
* @returns {Polygon[]}
*/
cutFace(pt1, pt2) {
let edge1 = this.findEdgeByPoint(pt1);
let edge2 = this.findEdgeByPoint(pt2);
if (edge1.face !== edge2.face)
return [];
// Cut face into two and create new polygon with two faces
let edgeBefore1 = this.addVertex(pt1, edge1);
edge2 = this.findEdgeByPoint(pt2);
let edgeBefore2 = this.addVertex(pt2, edge2);
let face = edgeBefore1.face;
let newEdge1 = new geom.Edge(
new geom.Segment(edgeBefore1.end, edgeBefore2.end)
);
let newEdge2 = new geom.Edge(
new geom.Segment(edgeBefore2.end, edgeBefore1.end)
);
// Swap links
edgeBefore1.next.prev = newEdge2;
newEdge2.next = edgeBefore1.next;
edgeBefore1.next = newEdge1;
newEdge1.prev = edgeBefore1;
edgeBefore2.next.prev = newEdge1;
newEdge1.next = edgeBefore2.next;
edgeBefore2.next = newEdge2;
newEdge2.prev = edgeBefore2;
// Insert new edge to the edges container and 2d index
this.edges.add(newEdge1);
this.edges.add(newEdge2);
// Add two new faces
let face1 = this.addFace(newEdge1, edgeBefore1);
let face2 = this.addFace(newEdge2, edgeBefore2);
// Remove old face
this.faces.delete(face);
return [face1.toPolygon(), face2.toPolygon()];
}
/**
* Return a result of cutting polygon with line
* @param {Line} line - cutting line
* @returns {Polygon} newPoly - resulted polygon
*/
cutWithLine(line) {
let newPoly = this.clone();
let multiline = new Multiline([line]);
// smart intersections
let intersections = {
int_points1: [],
int_points2: [],
int_points1_sorted: [],
int_points2_sorted: []
};
// intersect line with each edge of the polygon
// and create smart intersections
for (let edge of newPoly.edges) {
let ip = intersectEdge2Line(edge, line);
// for each intersection point
for (let pt of ip) {
addToIntPoints(multiline.first, pt, intersections.int_points1);
addToIntPoints(edge, pt, intersections.int_points2);
}
}
// No intersections - return a copy of the original polygon
if (intersections.int_points1.length === 0)
return newPoly;
// sort smart intersections
intersections.int_points1_sorted = getSortedArrayOnLine(line, intersections.int_points1);
intersections.int_points2_sorted = getSortedArray(intersections.int_points2);
// split by intersection points
splitByIntersections(multiline, intersections.int_points1_sorted);
splitByIntersections(newPoly, intersections.int_points2_sorted);
// filter duplicated intersection points
filterDuplicatedIntersections(intersections);
// sort intersection points again after filtering
intersections.int_points1_sorted = getSortedArrayOnLine(line, intersections.int_points1);
intersections.int_points2_sorted = getSortedArray(intersections.int_points2);
// initialize inclusion flags for edges of multiline incident to intersections
initializeInclusionFlags(intersections.int_points1);
// calculate inclusion flag for edges of multiline incident to intersections
calculateInclusionFlags(intersections.int_points1, newPoly);
// filter intersections between two edges that got same inclusion flag
for (let int_point1 of intersections.int_points1_sorted) {
if (int_point1.edge_before.bv === int_point1.edge_after.bv) {
intersections.int_points2[int_point1.id] = -1; // to be filtered out
int_point1.id = -1; // to be filtered out
}
}
intersections.int_points1 = intersections.int_points1.filter( int_point => int_point.id >= 0);
intersections.int_points2 = intersections.int_points2.filter( int_point => int_point.id >= 0);
// No intersections left after filtering - return a copy of the original polygon
if (intersections.int_points1.length === 0)
return newPoly;
// sort intersection points 3d time after filtering
intersections.int_points1_sorted = getSortedArrayOnLine(line, intersections.int_points1);
intersections.int_points2_sorted = getSortedArray(intersections.int_points2);
// Add 2 new inner edges between intersection points
let int_point1_prev = intersections.int_points1[0];
let new_edge;
for (let int_point1_curr of intersections.int_points1_sorted) {
if (int_point1_curr.edge_before.bv === INSIDE) {
new_edge = new geom.Edge(new geom.Segment(int_point1_prev.pt, int_point1_curr.pt)); // (int_point1_curr.edge_before.shape);
insertBetweenIntPoints(intersections.int_points2[int_point1_prev.id], intersections.int_points2[int_point1_curr.id], new_edge);
newPoly.edges.add(new_edge);
new_edge = new geom.Edge(new geom.Segment(int_point1_curr.pt, int_point1_prev.pt)); // (int_point1_curr.edge_before.shape.reverse());
insertBetweenIntPoints(intersections.int_points2[int_point1_curr.id], intersections.int_points2[int_point1_prev.id], new_edge);
newPoly.edges.add(new_edge);
}
int_point1_prev = int_point1_curr;
}
// Recreate faces
newPoly.recreateFaces();
return newPoly;
}
/**
* Returns the first found edge of polygon that contains given point
* If point is a vertex, return the edge where the point is an end vertex, not a start one
* @param {Point} pt
* @returns {Edge}
*/
findEdgeByPoint(pt) {
let edge;
for (let face of this.faces) {
edge = face.findEdgeByPoint(pt);
if (edge !== undefined)
break;
}
return edge;
}
/**
* Split polygon into array of polygons, where each polygon is an island with all
* hole that it contains
* @returns {geom.Polygon[]}
*/
splitToIslands() {
if (this.isEmpty()) return []; // return empty array if polygon is empty
let polygons = this.toArray(); // split into array of one-loop polygons
/* Sort polygons by area in descending order */
polygons.sort((polygon1, polygon2) => polygon2.area() - polygon1.area());
/* define orientation of the island by orientation of the first polygon in array */
let orientation = [...polygons[0].faces][0].orientation();
/* Create output array from polygons with same orientation as a first polygon (array of islands) */
let newPolygons = polygons.filter(polygon => [...polygon.faces][0].orientation() === orientation);
for (let polygon of polygons) {
let face = [...polygon.faces][0];
if (face.orientation() === orientation) continue; // skip same orientation
/* Proceed with opposite orientation */
/* Look if any of island polygons contains tested polygon as a hole */
for (let islandPolygon of newPolygons) {
if (face.shapes.every(shape => islandPolygon.contains(shape))) {
islandPolygon.addFace(face.shapes); // add polygon as a hole in islandPolygon
break;
}
}
}
// TODO: assert if not all polygons added into output
return newPolygons;
}
/**
* Reverse orientation of all faces to opposite
* @returns {Polygon}
*/
reverse() {
for (let face of this.faces) {
face.reverse();
}
return this;
}
/**
* Returns true if polygon contains shape: no point of shape lay outside of the polygon,
* false otherwise
* @param {Shape} shape - test shape
* @returns {boolean}
*/
contains(shape) {
if (shape instanceof geom.Point) {
let rel = ray_shoot(this, shape);
return rel === INSIDE || rel === BOUNDARY;
} else {
return Relations.cover(this, shape);
}
}
/**
* Return distance and shortest segment between polygon and other shape as array [distance, shortest_segment]
* @param {Shape} shape Shape of one of the types Point, Circle, Line, Segment, Arc or Polygon
* @returns {Number | Segment}
*/
distanceTo(shape) {
// let {Distance} = Flatten;
if (shape instanceof geom.Point) {
let [dist, shortest_segment] = Distance.point2polygon(shape, this);
shortest_segment = shortest_segment.reverse();
return [dist, shortest_segment];
}
if (shape instanceof geom.Circle ||
shape instanceof geom.Line ||
shape instanceof geom.Segment ||
shape instanceof geom.Arc) {
let [dist, shortest_segment] = Distance.shape2polygon(shape, this);
shortest_segment = shortest_segment.reverse();
return [dist, shortest_segment];
}
/* this method is bit faster */
if (shape instanceof geom.Polygon) {
let min_dist_and_segment = [Number.POSITIVE_INFINITY, new geom.Segment()] as const;
let dist, shortest_segment;
for (let edge of this.edges) {
// let [dist, shortest_segment] = Distance.shape2polygon(edge.shape, shape);
let min_stop = min_dist_and_segment[0];
[dist, shortest_segment] = Distance.shape2planarSet(edge.shape, shape.edges, min_stop);
if (Utils.LT(dist, min_stop)) {
min_dist_and_segment = [dist, shortest_segment];
}
}
return min_dist_and_segment;
}
}
/**
* Return array of intersection points between polygon and other shape
* @param shape Shape of the one of supported types <br/>
* @returns {Point[]}
*/
intersect(shape) {
if (shape instanceof geom.Point) {
return this.contains(shape) ? [shape] : [];
}
if (shape instanceof geom.Line) {
return Intersection.intersectLine2Polygon(shape, this);
}
if (shape instanceof geom.Ray) {
return Intersection.intersectRay2Polygon(shape, this);
}
if (shape instanceof geom.Circle) {
return Intersection.intersectCircle2Polygon(shape, this);
}
if (shape instanceof geom.Segment) {
return Intersection.intersectSegment2Polygon(shape, this);
}
if (shape instanceof geom.Arc) {
return Intersection.intersectArc2Polygon(shape, this);
}
if (shape instanceof geom.Polygon) {
return Intersection.intersectPolygon2Polygon(shape, this);
}
}
/**
* Returns new polygon translated by vector vec
* @param {Vector} vec
* @returns {Polygon}
*/
translate(vec) {
let newPolygon = new Polygon();
for (let face of this.faces) {
newPolygon.addFace(face.shapes.map(shape => shape.translate(vec)));
}
return newPolygon;
}
/**
* Return new polygon rotated by given angle around given point
* If point omitted, rotate around origin (0,0)
* Positive value of angle defines rotation counterclockwise, negative - clockwise
* @param {number} angle - rotation angle in radians
* @param {Point} center - rotation center, default is (0,0)
* @returns {Polygon} - new rotated polygon
*/
rotate(angle = 0, center = new geom.Point()) {
let newPolygon = new Polygon();
for (let face of this.faces) {
newPolygon.addFace(face.shapes.map(shape => shape.rotate(angle, center)));
}
return newPolygon;
}
/**
* Return new polygon with coordinates multiplied by scaling factor
* @param {number} sx - x-axis scaling factor
* @param {number} sy - y-axis scaling factor
* @returns {Polygon}
*/
scale(sx, sy) {
let newPolygon = new Polygon();
for (let face of this.faces) {
newPolygon.addFace(face.shapes.map(shape => shape.scale(sx, sy)));
}
return newPolygon;
}
/**
* Return new polygon transformed using affine transformation matrix
* @param {Matrix} matrix - affine transformation matrix
* @returns {Polygon} - new polygon
*/
transform(matrix = new geom.Matrix()) {
let newPolygon = new Polygon();
for (let face of this.faces) {
newPolygon.addFace(face.shapes.map(shape => shape.transform(matrix)));
}
return newPolygon;
}
/**
* This method returns an object that defines how data will be
* serialized when called JSON.stringify() method
* @returns {Object}
*/
toJSON() {
return [...this.faces].map(face => face.toJSON());
}
/**
* Transform all faces into array of polygons
* @returns {geom.Polygon[]}
*/
toArray() {
return [...this.faces].map(face => face.toPolygon());
}
/**
* Return string to draw polygon in svg
* @param attrs - an object with attributes for svg path element
* @returns {string}
*/
svg(attrs = {}) {
let svgStr = `\n<path ${convertToString({fillRule: "evenodd", fill: "lightcyan", ...attrs})} d="`;
for (let face of this.faces) {
svgStr += face.svg();
}
svgStr += `" >\n</path>`;
return svgStr;
}
}
/**
* Shortcut method to create new polygon
*/
export const polygon = (...args) => new geom.Polygon(...args);