import CircularLinkedList from '../data_structures/circular_linked_list' import * as Utils from '../utils/utils' import { CW, ORIENTATION } from '../utils/constants' import * as geom from './index' import { Box } from './Box' import type { Edge } from './Edge' import type { Point } from './Point' import type { Polygon } from './Polygon' import type { PlanarSet } from '../data_structures/PlanarSet' /** * Class representing a face (closed loop) in a [polygon]{@link geom.Polygon} object. * Face is a circular bidirectional linked list of [edges]{@link geom.Edge}. * Face object cannot be instantiated with a constructor. * Instead, use [polygon.addFace()]{@link geom.Polygon#addFace} method. *
* Note, that face only set entry point to the linked list of edges but does not contain edges by itself. * Container of edges is a property of the polygon object.
* * @example * // Face implements "next" iterator which enables to iterate edges in for loop: * for (let edge of face) { * console.log(edge.shape.length) // do something * } * * // Instead, it is possible to iterate edges as linked list, starting from face.first: * let edge = face.first; * do { * console.log(edge.shape.length); // do something * edge = edge.next; * } while (edge != face.first) */ export class Face extends CircularLinkedList { _box: Box | undefined _orientation: number | undefined constructor(polygon: Polygon) constructor(polygon: Polygon, points: geom.Point[]) constructor(polygon: Polygon, shape: [number, number][]) constructor(polygon: Polygon, shape: (geom.Segment | geom.Arc)[]) constructor(polygon: Polygon, path: geom.Path) constructor(polygon: Polygon, face: Face) constructor(polygon: Polygon, shape: geom.Box | geom.Circle) constructor(polygon: Polygon, a: geom.Edge, b: geom.Edge) constructor(polygon: Polygon, a?: unknown, b?: unknown) { super() this._box = undefined this._orientation = undefined if (!a && !b) { return } /* If passed an array it supposed to be: * 1) array of shapes that performs close loop or * 2) array of points that performs set of vertices */ if (a !== undefined && b === undefined) { if (a instanceof Array) { const shapes = a if (shapes.length === 0) return /* array of geom.Points */ if (shapes.every((shape) => shape instanceof geom.Point)) { let segments = Face.points2segments(shapes) this.shapes2face(polygon.edges, segments) } else if (shapes.every((shape) => shape instanceof Array && shape.length === 2)) { /* array of points as pairs of numbers */ const points = shapes.map((shape) => new geom.Point(shape[0], shape[1])) const segments = Face.points2segments(points) this.shapes2face(polygon.edges, segments) } else if (shapes[0] instanceof geom.Segment || shapes[0] instanceof geom.Arc || shapes[0] instanceof geom.Bezier || shapes[0] instanceof geom.Quadratic) { /* array of edges */ this.shapes2face(polygon.edges, shapes) } // this is from JSON.parse object else if (shapes.every((shape) => shape.name === 'segment' || shape.name === 'arc')) { let flattenShapes = [] for (let shape of shapes) { if (shape.name === 'segment') { flattenShapes.push(new geom.Segment(shape)) } else { flattenShapes.push(new geom.Arc(shape)) } } this.shapes2face(polygon.edges, flattenShapes) } } else if (a instanceof Face) { /* Create new face and copy edges into polygon.edges set */ const face = a this.first = face.first this.last = face.last for (const edge of face) { polygon.edges.add(edge) } } else if (a instanceof geom.Circle) { /* Instantiate face from a circle in CW orientation */ this.shapes2face(polygon.edges, [a.toArc(CW)]) } else if (a instanceof geom.Box) { /* Instantiate face from a box in CW orientation */ const box = a this.shapes2face(polygon.edges, [ new geom.Segment(new geom.Point(box.xmin, box.ymin), new geom.Point(box.xmax, box.ymin)), new geom.Segment(new geom.Point(box.xmax, box.ymin), new geom.Point(box.xmax, box.ymax)), new geom.Segment(new geom.Point(box.xmax, box.ymax), new geom.Point(box.xmin, box.ymax)), new geom.Segment(new geom.Point(box.xmin, box.ymax), new geom.Point(box.xmin, box.ymin)), ]) } else if (a instanceof geom.Path) { /* Instantiate face from a path in CW orientation */ const path = a this.shapes2face(polygon.edges, path.parts) } } /* If passed two edges, consider them as start and end of the face loop */ /* THIS METHOD WILL BE USED BY BOOLEAN OPERATIONS */ /* Assume that edges already copied to polygon.edges set in the clip algorithm !!! */ if (a instanceof geom.Edge && b instanceof geom.Edge) { this.first = a // first edge in face or undefined this.last = b // last edge in face or undefined this.last.next = this.first this.first.prev = this.last // set arc length this.setArcLength() // this.box = this.getBox(); // this.orientation = this.getOrientation(); // face direction ccw or cw } } /** * Return array of edges from first to last */ get edges() { return this.toArray() } /** * Return array of shapes which comprise face */ get shapes() { return this.edges.map((edge) => edge.shape.clone()) } /** * Return bounding box of the face */ get box() { if (this._box === undefined) { let box = geom.Box.VOID for (let edge of this) { box = box.merge(edge.box) } this._box = box } return this._box } /** * Get all edges length */ get perimeter() { return this.last.arc_length + this.last.length } /** * Get point on face boundary at given length * @param length - The length along the face boundary */ pointAtLength(length: number) { if (length > this.perimeter || length < 0) return null let point = null for (let edge of this) { if (length >= edge.arc_length && (edge === this.last || length < edge.next.arc_length)) { point = edge.pointAtLength(length - edge.arc_length) break } } return point } static points2segments(points: Point[]) { let segments = [] for (let i = 0; i < points.length; i++) { // skip zero length segment if (points[i].equalTo(points[(i + 1) % points.length])) continue segments.push(new geom.Segment(points[i], points[(i + 1) % points.length])) } return segments } shapes2face(edges, shapes) { for (let shape of shapes) { let edge = new geom.Edge(shape) this.append(edge) // this.box = this.box.merge(shape.box); edges.add(edge) } // this.orientation = this.getOrientation(); // face direction ccw or cw } /** * Append edge after the last edge of the face (and before the first edge).
* @param edge - Edge to be appended to the linked list */ append(edge: Edge) { super.append(edge) // set arc length this.setOneEdgeArcLength(edge) edge.face = this // edges.add(edge); // Add new edges into edges container return this } /** * Insert edge newEdge into the linked list after the edge edgeBefore
* @param newEdge - Edge to be inserted into linked list * @param edgeBefore - Edge to insert newEdge after it */ insert(newEdge: Edge, edgeBefore: Edge) { super.insert(newEdge, edgeBefore) // set arc length this.setOneEdgeArcLength(newEdge) newEdge.face = this return this } /** * Remove the given edge from the linked list of the face
* @param edge - Edge to be removed */ remove(edge: Edge) { super.remove(edge) // Recalculate arc length this.setArcLength() return this } /** * Merge current edge with the next edge. Given edge will be extended, * next edge after it will be removed. The distortion of the polygon * is on the responsibility of the user of this method * @param edge - edge to be extended */ mergeWithNextEdge(edge: Edge) { edge.shape.end.x = edge.next.shape.end.x edge.shape.end.y = edge.next.shape.end.y this.remove(edge.next) return this } /** * Reverse orientation of the face: first edge become last and vice a verse, * all edges starts and ends swapped, direction of arcs inverted. If face was oriented * clockwise, it becomes counterclockwise and vice versa */ reverse() { // collect edges in revert order with reverted shapes let edges = [] let edge_tmp = this.last do { // reverse shape edge_tmp.shape = edge_tmp.shape.reverse() edges.push(edge_tmp) edge_tmp = edge_tmp.prev } while (edge_tmp !== this.last) // restore linked list this.first = undefined this.last = undefined for (let edge of edges) { if (this.first === undefined) { edge.prev = edge edge.next = edge this.first = edge this.last = edge } else { // append to end edge.prev = this.last this.last.next = edge // update edge to be last this.last = edge // restore circular links this.last.next = this.first this.first.prev = this.last } // set arc length this.setOneEdgeArcLength(edge) } // Recalculate orientation, if set if (this._orientation !== undefined) { this._orientation = undefined this._orientation = this.orientation() } } /** * Set arc_length property for each of the edges in the face. * Arc_length of the edge it the arc length from the first edge of the face */ setArcLength() { for (let edge of this) { this.setOneEdgeArcLength(edge) edge.face = this } } setOneEdgeArcLength(edge) { if (edge === this.first) { edge.arc_length = 0.0 } else { edge.arc_length = edge.prev.arc_length + edge.prev.length } } /** * Returns the absolute value of the area of the face */ area() { return Math.abs(this.signedArea()) } /** * Returns signed area of the simple face. * Face is simple if it has no self intersections that change its orientation. * Then the area will be positive if the orientation of the face is clockwise, * and negative if orientation is counterclockwise. * It may be zero if polygon is degenerated. */ signedArea() { let sArea = 0 let ymin = this.box.ymin for (let edge of this) { sArea += edge.shape.definiteIntegral(ymin) } return sArea } /** * Return face orientation: one of geom.ORIENTATION.CW, geom.ORIENTATION.CCW, geom.ORIENTATION.NOT_ORIENTABLE
* According to Green theorem the area of a closed curve may be calculated as double integral, * and the sign of the integral will be defined by the direction of the curve. * When the integral ("signed area") will be negative, direction is counterclockwise, * when positive - clockwise and when it is zero, polygon is not orientable. * See {@link https://mathinsight.org/greens_theorem_find_area} */ orientation() { if (this._orientation === undefined) { let area = this.signedArea() if (Utils.EQ_0(area)) { this._orientation = ORIENTATION.NOT_ORIENTABLE } else if (Utils.LT(area, 0)) { this._orientation = ORIENTATION.CW } else { this._orientation = ORIENTATION.CCW } } return this._orientation } /** * Returns true if face of the polygon is simple (no self-intersection points found) * NOTE: this method is incomplete because it does not exclude touching points. * Self intersection test should check if polygon change orientation in the test point. * @param edges - reference to polygon edges to provide search index */ isSimple(edges: PlanarSet) { return Face.getSelfIntersections(this, edges, true).length === 0 } static getSelfIntersections(face, edges, exitOnFirst = false) { let int_points = [] // calculate intersections for (let edge1 of face) { // request edges of polygon in the box of edge1 let resp = edges.search(edge1.box) // for each edge2 in response for (let edge2 of resp) { // Skip itself if (edge1 === edge2) continue // Skip is edge2 belongs to another face if (edge2.face !== face) continue // Skip next and previous edge if both are segment (if one of them arc - calc intersection) if ( edge1.shape instanceof geom.Segment && edge2.shape instanceof geom.Segment && (edge1.next === edge2 || edge1.prev === edge2) ) continue // calculate intersections between edge1 and edge2 let ip = edge1.shape.intersect(edge2.shape) // for each intersection point for (let pt of ip) { // skip start-end connections if (pt.equalTo(edge1.start) && pt.equalTo(edge2.end) && edge2 === edge1.prev) continue if (pt.equalTo(edge1.end) && pt.equalTo(edge2.start) && edge2 === edge1.next) continue int_points.push(pt) if (exitOnFirst) break } if (int_points.length > 0 && exitOnFirst) break } if (int_points.length > 0 && exitOnFirst) break } return int_points } /** * Returns edge which contains given point */ findEdgeByPoint(p: Point) { let edgeFound: Edge for (let edge of this) { if (p.equalTo(edge.shape.start)) continue if (p.equalTo(edge.shape.end) || edge.shape.contains(p)) { edgeFound = edge break } } return edgeFound } /** * Returns new polygon created from one face */ toPolygon() { return new geom.Polygon(this.shapes) } toJSON() { return this.edges.map((edge) => edge.toJSON()) } }