UNPKG

flatten-js

Version:

Javascript library for 2d geometry

410 lines (362 loc) 15.1 kB
/** * Created by Alex Bol on 3/15/2017. */ "use strict"; module.exports = function(Flatten) { let {Edge, Face, PlanarSet, Box} = Flatten; let {ray_shoot} = Flatten; /** * Class representing a polygon.<br/> * Polygon in FlattenJS is a multipolygon comprised from a set of [faces]{@link Flatten.Face}. <br/> * Face, in turn, is a closed loop of [edges]{@link Flatten.Edge}, where edge may be segment or circular arc<br/> * @type {Polygon} */ Flatten.Polygon = class Polygon { /** * Constructor creates new instance of polygon.<br/> * New polygon is empty. Add face to the polygon using method <br/> * <code> * polygon.addFace(Points[]|Segments[]|Arcs[]) * </code> */ constructor() { /** * Container of faces (closed loops), may be empty * @type {PlanarSet} */ this.faces = new PlanarSet(); /** * Container of edges * @type {PlanarSet} */ this.edges = new PlanarSet(); } /** * (Getter) Returns bounding box of the polygon * @returns {Box} */ get box() { return [...this.faces].reduce( (acc, face) => acc.merge(face.box), new Box() ); } /** * (Getter) Returns array of vertices * @returns {Array} */ get vertices() { return [...this.edges].map( edge => edge.start); } /** * Return true is polygon has no edges * @returns {boolean} */ isEmpty() { return this.edges.size === 0; } /** * Add new face to polygon. Returns added face * @param {Points[]|Segments[]|Arcs[]|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) { let face = new 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) { let deleted = this.edges.delete(edge); } let deleted = this.faces.delete(face); return deleted; } /** * 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(this.edges, 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. * @param {Edge} edge Edge to be split with new vertex and then trimmed from start * @param {Point} pt Point to be added as a new vertex * @returns {Edge} */ addVertex(pt, edge) { let shapes = edge.shape.split(pt); if (shapes.length < 2) return; let newEdge = new Flatten.Edge(shapes[0]); let edgeBefore = edge.prev; /* Insert first split edge into linked list after edgeBefore */ edge.face.insert(this.edges, newEdge, edgeBefore); // Remove old edge from edges container and 2d index this.edges.delete(edge); // 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; } reverse() { for (let face of this.faces) { face.reverse(); } return this; } /** * Create new copied instance of the polygon * @returns {Polygon} */ clone() { let polygon = new Polygon(); for (let face of this.faces) { let shapes = []; for (let edge of face) { shapes.push(edge.shape.clone()); } polygon.addFace(shapes); } return polygon; } /** * 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); } /** * Returns true if polygon contains point, including polygon boundary, false otherwise * Point in polygon test based on ray shooting algorithm * @param {Point} point - test point * @returns {boolean} */ contains(point) { let rel = ray_shoot(this, point); return (rel == Flatten.INSIDE || rel == Flatten.BOUNDARY) ? true : false; } /** * 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 Flatten.Point) { let [dist, shortest_segment] = Distance.point2polygon(shape, this); shortest_segment = shortest_segment.reverse(); return [dist, shortest_segment]; } if (shape instanceof Flatten.Circle || shape instanceof Flatten.Line || shape instanceof Flatten.Segment || shape instanceof Flatten.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 Flatten.Polygon) { let min_dist_and_segment = [Number.POSITIVE_INFINITY, new Flatten.Segment()]; 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 (Flatten.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 Flatten.Point) { return this.contains(shape) ? [shape] : []; } if (shape instanceof Flatten.Line) { return Polygon.intersectLine2Polygon(shape, this); } if (shape instanceof Flatten.Circle || shape instanceof Flatten.Segment || shape instanceof Flatten.Arc) { return Polygon.intersectShape2Polygon(shape, this); } if (shape instanceof Flatten.Polygon) { return Polygon.intersectPolygon2Polygon(shape, this); } } /** * 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 new polygon translated by vector vec * @param {Vector} vec * @returns {Polygon} */ translate(vec) { let newPolygon = new Polygon(); for (let face of this.faces) { let shapes = []; for (let edge of face) { shapes.push(edge.shape.translate(vec)); } newPolygon.addFace(shapes); } 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 counter clockwise, 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 Flatten.Point()) { let newPolygon = new Polygon(); for (let face of this.faces) { let shapes = []; for (let edge of face) { shapes.push(edge.shape.rotate(angle, center)); } newPolygon.addFace(shapes); } return newPolygon; } /** * Return new polygon transformed using affine transformation matrix * @param {Matrix} matrix - affine transformation matrix * @returns {Polygon} - new polygon */ transform(matrix = new Flatten.Matrix()) { let newPolygon = new Polygon(); for (let face of this.faces) { let shapes = []; for (let edge of face) { shapes.push(edge.shape.transform(matrix)); } newPolygon.addFace(shapes); } return newPolygon; } static intersectShape2Polygon(shape, polygon) { let ip = []; if ( polygon.isEmpty() || shape.box.not_intersect(polygon.box) ) { return ip; } let resp_edges = polygon.edges.search(shape.box); for (let edge of resp_edges) { for (let pt of shape.intersect(edge.shape)) { ip.push(pt); } } return ip; } static intersectLine2Polygon(line, polygon) { let ip = []; if ( polygon.isEmpty() ) { return ip; } for (let edge of polygon.edges) { for (let pt of line.intersect(edge.shape)) { ip.push(pt); } } return ip; } static intersectPolygon2Polygon(polygon1, polygon2) { let ip = []; if (polygon1.isEmpty() || polygon2.isEmpty()) { return ip; } if (polygon1.box.not_intersect(polygon2.box)) { return ip; } for (let edge1 of polygon1.edges) { for (let pt of Polygon.intersectShape2Polygon(edge1.shape, polygon2)) { ip.push(pt); } } return ip; } /** * Return string to draw polygon in svg * @param attrs - an object with attributes for svg path element, * like "stroke", "strokeWidth", "fill", "fillRule", "fillOpacity" * Defaults are stroke:"black", strokeWidth:"1", fill:"lightcyan", fillRule:"evenodd", fillOpacity: "1" * @returns {string} */ svg(attrs = {}) { let {stroke, strokeWidth, fill, fillRule, fillOpacity, id, className} = attrs; // let restStr = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, ""); let id_str = (id && id.length > 0) ? `id="${id}"` : ""; let class_str = (className && className.length > 0) ? `class="${className}"` : ""; let svgStr = `\n<path stroke="${stroke || "black"}" stroke-width="${strokeWidth || 1}" fill="${fill || "lightcyan"}" fill-rule="${fillRule || "evenodd"}" fill-opacity="${fillOpacity || 1.0}" ${id_str} ${class_str} d="`; for (let face of this.faces) { svgStr += face.svg(); } svgStr += `" >\n</path>`; return svgStr; } /** * 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()); } } };