classes/polygon.js

  1. /**
  2. * Created by Alex Bol on 3/15/2017.
  3. */
  4. "use strict";
  5. module.exports = function(Flatten) {
  6. let {Edge, Face, PlanarSet, Box} = Flatten;
  7. let {ray_shoot} = Flatten;
  8. /**
  9. * Class representing a polygon.<br/>
  10. * Polygon in FlattenJS is a multipolygon comprised from a set of [faces]{@link Flatten.Face}. <br/>
  11. * Face, in turn, is a closed loop of [edges]{@link Flatten.Edge}, where edge may be segment or circular arc<br/>
  12. * @type {Polygon}
  13. */
  14. Flatten.Polygon = class Polygon {
  15. /**
  16. * Constructor creates new instance of polygon.<br/>
  17. * New polygon is empty. Add face to the polygon using method <br/>
  18. * <code>
  19. * polygon.addFace(Points[]|Segments[]|Arcs[])
  20. * </code>
  21. */
  22. constructor() {
  23. /**
  24. * Container of faces (closed loops), may be empty
  25. * @type {PlanarSet}
  26. */
  27. this.faces = new PlanarSet();
  28. /**
  29. * Container of edges
  30. * @type {PlanarSet}
  31. */
  32. this.edges = new PlanarSet();
  33. }
  34. /**
  35. * (Getter) Returns bounding box of the polygon
  36. * @returns {Box}
  37. */
  38. get box() {
  39. return [...this.faces].reduce( (acc, face) => acc.merge(face.box), new Box() );
  40. }
  41. /**
  42. * (Getter) Returns array of vertices
  43. * @returns {Array}
  44. */
  45. get vertices() {
  46. return [...this.edges].map( edge => edge.start);
  47. }
  48. /**
  49. * Return true is polygon has no edges
  50. * @returns {boolean}
  51. */
  52. isEmpty() {
  53. return this.edges.size === 0;
  54. }
  55. /**
  56. * Add new face to polygon. Returns added face
  57. * @param {Points[]|Segments[]|Arcs[]|Circle|Box} args - new face may be create with one of the following ways: <br/>
  58. * 1) array of points that describe closed path (edges are segments) <br/>
  59. * 2) array of shapes (segments and arcs) which describe closed path <br/>
  60. * 3) circle - will be added as counterclockwise arc <br/>
  61. * 4) box - will be added as counterclockwise rectangle <br/>
  62. * You can chain method face.reverse() is you need to change direction of the creates face
  63. * @returns {Face}
  64. */
  65. addFace(...args) {
  66. let face = new Face(this, ...args);
  67. this.faces.add(face);
  68. return face;
  69. }
  70. /**
  71. * Delete existing face from polygon
  72. * @param {Face} face Face to be deleted
  73. * @returns {boolean}
  74. */
  75. deleteFace(face) {
  76. for (let edge of face) {
  77. let deleted = this.edges.delete(edge);
  78. }
  79. let deleted = this.faces.delete(face);
  80. return deleted;
  81. }
  82. /**
  83. * Delete chain of edges from the face.
  84. * @param {Face} face Face to remove chain
  85. * @param {Edge} edgeFrom Start of the chain of edges to be removed
  86. * @param {Edge} edgeTo End of the chain of edges to be removed
  87. */
  88. removeChain(face, edgeFrom, edgeTo) {
  89. // Special case: all edges removed
  90. if (edgeTo.next === edgeFrom) {
  91. this.deleteFace(face);
  92. return;
  93. }
  94. for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next ) {
  95. face.remove(this.edges, edge);
  96. // this.edges.delete(edge); // delete from PlanarSet of edges and update index
  97. if (face.isEmpty()) {
  98. this.deleteFace(face); // delete from PlanarSet of faces and update index
  99. break;
  100. }
  101. }
  102. }
  103. /**
  104. * Add point as a new vertex and split edge. Point supposed to belong to an edge.
  105. * When edge is split, new edge created from the start of the edge to the new vertex
  106. * and inserted before current edge.
  107. * Current edge is trimmed and updated. Method returns new edge added.
  108. * @param {Edge} edge Edge to be split with new vertex and then trimmed from start
  109. * @param {Point} pt Point to be added as a new vertex
  110. * @returns {Edge}
  111. */
  112. addVertex(pt, edge) {
  113. let shapes = edge.shape.split(pt);
  114. if (shapes.length < 2) return;
  115. let newEdge = new Flatten.Edge(shapes[0]);
  116. let edgeBefore = edge.prev;
  117. /* Insert first split edge into linked list after edgeBefore */
  118. edge.face.insert(this.edges, newEdge, edgeBefore);
  119. // Remove old edge from edges container and 2d index
  120. this.edges.delete(edge);
  121. // Update edge shape with second split edge keeping links
  122. edge.shape = shapes[1];
  123. // Add updated edge to the edges container and 2d index
  124. this.edges.add(edge);
  125. return newEdge;
  126. }
  127. reverse() {
  128. for (let face of this.faces) {
  129. face.reverse();
  130. }
  131. return this;
  132. }
  133. /**
  134. * Create new copied instance of the polygon
  135. * @returns {Polygon}
  136. */
  137. clone() {
  138. let polygon = new Polygon();
  139. for (let face of this.faces) {
  140. let shapes = [];
  141. for (let edge of face) {
  142. shapes.push(edge.shape.clone());
  143. }
  144. polygon.addFace(shapes);
  145. }
  146. return polygon;
  147. }
  148. /**
  149. * Returns area of the polygon. Area of an island will be added, area of a hole will be subtracted
  150. * @returns {number}
  151. */
  152. area() {
  153. let signedArea = [...this.faces].reduce((acc,face) => acc + face.signedArea(), 0);
  154. return Math.abs(signedArea);
  155. }
  156. /**
  157. * Returns true if polygon contains point, including polygon boundary, false otherwise
  158. * Point in polygon test based on ray shooting algorithm
  159. * @param {Point} point - test point
  160. * @returns {boolean}
  161. */
  162. contains(point) {
  163. let rel = ray_shoot(this, point);
  164. return (rel == Flatten.INSIDE || rel == Flatten.BOUNDARY) ? true : false;
  165. }
  166. /**
  167. * Return distance and shortest segment between polygon and other shape as array [distance, shortest_segment]
  168. * @param {Shape} shape Shape of one of the types Point, Circle, Line, Segment, Arc or Polygon
  169. * @returns {Number | Segment}
  170. */
  171. distanceTo(shape) {
  172. let {Distance} = Flatten;
  173. if (shape instanceof Flatten.Point) {
  174. let [dist, shortest_segment] = Distance.point2polygon(shape, this);
  175. shortest_segment = shortest_segment.reverse();
  176. return [dist, shortest_segment];
  177. }
  178. if (shape instanceof Flatten.Circle ||
  179. shape instanceof Flatten.Line ||
  180. shape instanceof Flatten.Segment ||
  181. shape instanceof Flatten.Arc) {
  182. let [dist, shortest_segment] = Distance.shape2polygon(shape, this);
  183. shortest_segment = shortest_segment.reverse();
  184. return [dist, shortest_segment];
  185. }
  186. /* this method is bit faster */
  187. if (shape instanceof Flatten.Polygon) {
  188. let min_dist_and_segment = [Number.POSITIVE_INFINITY, new Flatten.Segment()];
  189. let dist, shortest_segment;
  190. for (let edge of this.edges) {
  191. // let [dist, shortest_segment] = Distance.shape2polygon(edge.shape, shape);
  192. let min_stop = min_dist_and_segment[0];
  193. [dist, shortest_segment] = Distance.shape2planarSet(edge.shape, shape.edges, min_stop);
  194. if (Flatten.Utils.LT(dist, min_stop)) {
  195. min_dist_and_segment = [dist, shortest_segment];
  196. }
  197. }
  198. return min_dist_and_segment;
  199. }
  200. }
  201. /**
  202. * Return array of intersection points between polygon and other shape
  203. * @param shape Shape of the one of supported types <br/>
  204. * @returns {Point[]}
  205. */
  206. intersect(shape) {
  207. if (shape instanceof Flatten.Point) {
  208. return this.contains(shape) ? [shape] : [];
  209. }
  210. if (shape instanceof Flatten.Line) {
  211. return Polygon.intersectLine2Polygon(shape, this);
  212. }
  213. if (shape instanceof Flatten.Circle ||
  214. shape instanceof Flatten.Segment ||
  215. shape instanceof Flatten.Arc) {
  216. return Polygon.intersectShape2Polygon(shape, this);
  217. }
  218. if (shape instanceof Flatten.Polygon) {
  219. return Polygon.intersectPolygon2Polygon(shape, this);
  220. }
  221. }
  222. /**
  223. * Return true if polygon is valid for boolean operations
  224. * Polygon is valid if <br/>
  225. * 1. All faces are simple polygons (there are no self-intersected polygons) <br/>
  226. * 2. All faces are orientable and there is no island inside island or hole inside hole - TODO <br/>
  227. * 3. There is no intersections between faces (excluding touching) - TODO <br/>
  228. * @returns {boolean}
  229. */
  230. isValid() {
  231. let valid = true;
  232. // 1. Polygon is invalid if at least one face is not simple
  233. for (let face of this.faces) {
  234. if (!face.isSimple(this.edges)) {
  235. valid = false;
  236. break;
  237. }
  238. }
  239. // 2. TODO: check if no island inside island and no hole inside hole
  240. // 3. TODO: check the there is no intersection between faces
  241. return valid;
  242. }
  243. /**
  244. * Returns new polygon translated by vector vec
  245. * @param {Vector} vec
  246. * @returns {Polygon}
  247. */
  248. translate(vec) {
  249. let newPolygon = new Polygon();
  250. for (let face of this.faces) {
  251. let shapes = [];
  252. for (let edge of face) {
  253. shapes.push(edge.shape.translate(vec));
  254. }
  255. newPolygon.addFace(shapes);
  256. }
  257. return newPolygon;
  258. }
  259. /**
  260. * Return new polygon rotated by given angle around given point
  261. * If point omitted, rotate around origin (0,0)
  262. * Positive value of angle defines rotation counter clockwise, negative - clockwise
  263. * @param {number} angle - rotation angle in radians
  264. * @param {Point} center - rotation center, default is (0,0)
  265. * @returns {Polygon} - new rotated polygon
  266. */
  267. rotate(angle=0, center=new Flatten.Point()) {
  268. let newPolygon = new Polygon();
  269. for (let face of this.faces) {
  270. let shapes = [];
  271. for (let edge of face) {
  272. shapes.push(edge.shape.rotate(angle, center));
  273. }
  274. newPolygon.addFace(shapes);
  275. }
  276. return newPolygon;
  277. }
  278. /**
  279. * Return new polygon transformed using affine transformation matrix
  280. * @param {Matrix} matrix - affine transformation matrix
  281. * @returns {Polygon} - new polygon
  282. */
  283. transform(matrix = new Flatten.Matrix()) {
  284. let newPolygon = new Polygon();
  285. for (let face of this.faces) {
  286. let shapes = [];
  287. for (let edge of face) {
  288. shapes.push(edge.shape.transform(matrix));
  289. }
  290. newPolygon.addFace(shapes);
  291. }
  292. return newPolygon;
  293. }
  294. static intersectShape2Polygon(shape, polygon) {
  295. let ip = [];
  296. if ( polygon.isEmpty() || shape.box.not_intersect(polygon.box) ) {
  297. return ip;
  298. }
  299. let resp_edges = polygon.edges.search(shape.box);
  300. for (let edge of resp_edges) {
  301. for (let pt of shape.intersect(edge.shape)) {
  302. ip.push(pt);
  303. }
  304. }
  305. return ip;
  306. }
  307. static intersectLine2Polygon(line, polygon) {
  308. let ip = [];
  309. if ( polygon.isEmpty() ) {
  310. return ip;
  311. }
  312. for (let edge of polygon.edges) {
  313. for (let pt of line.intersect(edge.shape)) {
  314. ip.push(pt);
  315. }
  316. }
  317. return ip;
  318. }
  319. static intersectPolygon2Polygon(polygon1, polygon2) {
  320. let ip = [];
  321. if (polygon1.isEmpty() || polygon2.isEmpty()) {
  322. return ip;
  323. }
  324. if (polygon1.box.not_intersect(polygon2.box)) {
  325. return ip;
  326. }
  327. for (let edge1 of polygon1.edges) {
  328. for (let pt of Polygon.intersectShape2Polygon(edge1.shape, polygon2)) {
  329. ip.push(pt);
  330. }
  331. }
  332. return ip;
  333. }
  334. /**
  335. * Return string to draw polygon in svg
  336. * @param attrs - an object with attributes for svg path element,
  337. * like "stroke", "strokeWidth", "fill", "fillRule", "fillOpacity"
  338. * Defaults are stroke:"black", strokeWidth:"1", fill:"lightcyan", fillRule:"evenodd", fillOpacity: "1"
  339. * @returns {string}
  340. */
  341. svg(attrs = {}) {
  342. let {stroke, strokeWidth, fill, fillRule, fillOpacity, id, className} = attrs;
  343. // let restStr = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, "");
  344. let id_str = (id && id.length > 0) ? `id="${id}"` : "";
  345. let class_str = (className && className.length > 0) ? `class="${className}"` : "";
  346. 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="`;
  347. for (let face of this.faces) {
  348. svgStr += face.svg();
  349. }
  350. svgStr += `" >\n</path>`;
  351. return svgStr;
  352. }
  353. /**
  354. * This method returns an object that defines how data will be
  355. * serialized when called JSON.stringify() method
  356. * @returns {Object}
  357. */
  358. toJSON() {
  359. return [...this.faces].map(face => face.toJSON());
  360. }
  361. }
  362. };