593 lines
20 KiB
JavaScript
593 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.Vertex = exports.Polygon = undefined;
|
|
|
|
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
|
|
|
var _line = require('./line');
|
|
|
|
var _linkedlist = require('./linkedlist');
|
|
|
|
var _matrix = require('./matrix');
|
|
|
|
var _plane = require('./plane');
|
|
|
|
var _sylvester = require('./sylvester');
|
|
|
|
var _vector = require('./vector');
|
|
|
|
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
|
|
|
|
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var Polygon = exports.Polygon = function () {
|
|
function Polygon() {
|
|
_classCallCheck(this, Polygon);
|
|
}
|
|
|
|
_createClass(Polygon, [{
|
|
key: 'v',
|
|
|
|
// Returns the vertex at the given position on the vertex list, numbered from 1.
|
|
value: function v(i) {
|
|
return this.vertices.at(i - 1).data;
|
|
}
|
|
|
|
// Returns the node in the vertices linked list that refers to the given vertex.
|
|
|
|
}, {
|
|
key: 'nodeFor',
|
|
value: function nodeFor(vertex) {
|
|
return this.vertices.withData(vertex);
|
|
}
|
|
|
|
// Returns a new polygon with the same vertices as the receiver. The vertices
|
|
// will not be duplicates, they refer to the same objects as the vertices in this
|
|
// polygon, but the linked list and nodes used to point to them are separate and
|
|
// can be manipulated independently of this one.
|
|
|
|
}, {
|
|
key: 'dup',
|
|
value: function dup() {
|
|
return Polygon.create(this.vertices, this.plane);
|
|
}
|
|
|
|
// Translates the polygon by the given vector and returns the polygon.
|
|
|
|
}, {
|
|
key: 'translate',
|
|
value: function translate(vector) {
|
|
var P = vector.elements || vector;
|
|
this.vertices.each(function (node) {
|
|
var E = node.data.elements;
|
|
node.data.setElements([E[0] + P[0], E[1] + P[1], E[2] + (P[2] || 0)]);
|
|
});
|
|
this.plane = this.plane.translate(vector);
|
|
this.updateTrianglePlanes(function (plane) {
|
|
return plane.translate(vector);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
// Rotates the polygon about the given line and returns the polygon.
|
|
|
|
}, {
|
|
key: 'rotate',
|
|
value: function rotate(t, line) {
|
|
var R = _matrix.Matrix.Rotation(t, line.direction);
|
|
this.vertices.each(function (node) {
|
|
node.data.setElements(node.data.rotate(R, line).elements);
|
|
});
|
|
this.plane = this.plane.rotate(R, line);
|
|
this.updateTrianglePlanes(function (plane) {
|
|
return plane.rotate(R, line);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
// Scales the polygon relative to the given point and returns the polygon.
|
|
|
|
}, {
|
|
key: 'scale',
|
|
value: function scale(k, point) {
|
|
var P = point.elements || point;
|
|
this.vertices.each(function (node) {
|
|
var E = node.data.elements;
|
|
node.data.setElements([P[0] + k * (E[0] - P[0]), P[1] + k * (E[1] - P[1]), (P[2] || 0) + k * (E[2] - (P[2] || 0))]);
|
|
});
|
|
var anchor = this.vertices.first.data;
|
|
this.plane.anchor.setElements(anchor);
|
|
this.updateTrianglePlanes(function (plane) {
|
|
return _plane.Plane.create(anchor, plane.normal);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
// Updates the plane properties of all the cached triangles belonging to
|
|
// the polygon according to the given function. For example, suppose you
|
|
// just rotated the polygon, you should call:
|
|
//
|
|
// poly.updateTrianglePlanes(function(plane) { return plane.rotate(t, line); });
|
|
//
|
|
// This method is called automatically by Polygon.translate, Polygon.rotate
|
|
// and Polygon.scale transformation methods.
|
|
|
|
}, {
|
|
key: 'updateTrianglePlanes',
|
|
value: function updateTrianglePlanes(fn) {
|
|
var i = void 0;
|
|
if (this.cached.triangles !== null) {
|
|
i = this.cached.triangles.length;
|
|
while (i--) {
|
|
this.cached.triangles[i].plane = fn(this.cached.triangles[i].plane);
|
|
}
|
|
}
|
|
if (this.cached.surfaceIntegralElements !== null) {
|
|
i = this.cached.surfaceIntegralElements.length;
|
|
while (i--) {
|
|
this.cached.surfaceIntegralElements[i].plane = fn(this.cached.surfaceIntegralElements[i].plane);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns true iff the polygon is a triangle
|
|
|
|
}, {
|
|
key: 'isTriangle',
|
|
value: function isTriangle() {
|
|
return this.vertices.length === 3;
|
|
}
|
|
|
|
// Returns a collection of triangles used for calculating area and center of mass.
|
|
// Some of the triangles will not lie inside the polygon - this collection is essentially
|
|
// a series of itervals in a surface integral, so some are 'negative'. If you want the
|
|
// polygon broken into constituent triangles, use toTriangles(). This method is used
|
|
// because it's much faster than toTriangles().
|
|
// The triangles generated share vertices with the original polygon, so they transform
|
|
// with the polygon. They are cached after first calculation and should remain in sync
|
|
// with changes to the parent polygon.
|
|
|
|
}, {
|
|
key: 'trianglesForSurfaceIntegral',
|
|
value: function trianglesForSurfaceIntegral() {
|
|
if (this.cached.surfaceIntegralElements !== null) {
|
|
return this.cached.surfaceIntegralElements;
|
|
}
|
|
var triangles = [];
|
|
var firstVertex = this.vertices.first.data;
|
|
var plane = this.plane;
|
|
this.vertices.each(function (node, i) {
|
|
if (i < 2) {
|
|
return;
|
|
}
|
|
var points = [firstVertex, node.prev.data, node.data];
|
|
// If the vertices lie on a straigh line, give the polygon's own plane. If the
|
|
// element has no area, it doesn't matter which way its normal faces.
|
|
triangles.push(Polygon.create(points, _plane.Plane.fromPoints(points) || plane));
|
|
});
|
|
return this.setCache('surfaceIntegralElements', triangles);
|
|
}
|
|
|
|
// Returns the area of the polygon. Requires that the polygon
|
|
// be converted to triangles, so use with caution.
|
|
|
|
}, {
|
|
key: 'area',
|
|
value: function area() {
|
|
if (this.isTriangle()) {
|
|
// Area is half the modulus of the cross product of two sides
|
|
var A = this.vertices.first;
|
|
var B = A.next;
|
|
var C = B.next;
|
|
A = A.data.elements;
|
|
B = B.data.elements;
|
|
C = C.data.elements;
|
|
|
|
return 0.5 * _vector.Vector.create([(A[1] - B[1]) * (C[2] - B[2]) - (A[2] - B[2]) * (C[1] - B[1]), (A[2] - B[2]) * (C[0] - B[0]) - (A[0] - B[0]) * (C[2] - B[2]), (A[0] - B[0]) * (C[1] - B[1]) - (A[1] - B[1]) * (C[0] - B[0])]).modulus();
|
|
}
|
|
|
|
var trigs = this.trianglesForSurfaceIntegral();
|
|
var area = 0;
|
|
for (var i = 0; i < trigs.length; i++) {
|
|
area += trigs[i].area() * trigs[i].plane.normal.dot(this.plane.normal);
|
|
}
|
|
|
|
return area;
|
|
}
|
|
|
|
// Returns the centroid of the polygon. Requires division into
|
|
// triangles - use with caution
|
|
|
|
}, {
|
|
key: 'centroid',
|
|
value: function centroid() {
|
|
if (this.isTriangle()) {
|
|
var A = this.v(1).elements;
|
|
var B = this.v(2).elements;
|
|
var C = this.v(3).elements;
|
|
return _vector.Vector.create([(A[0] + B[0] + C[0]) / 3, (A[1] + B[1] + C[1]) / 3, (A[2] + B[2] + C[2]) / 3]);
|
|
}
|
|
|
|
var V = _vector.Vector.Zero(3);
|
|
var trigs = this.trianglesForSurfaceIntegral();
|
|
var M = 0;
|
|
var i = trigs.length;
|
|
while (i--) {
|
|
var _A = trigs[i].area() * trigs[i].plane.normal.dot(this.plane.normal);
|
|
M += _A;
|
|
var P = V.elements;
|
|
var _C = trigs[i].centroid().elements;
|
|
|
|
V.setElements([P[0] + _C[0] * _A, P[1] + _C[1] * _A, P[2] + _C[2] * _A]);
|
|
}
|
|
|
|
return V.x(1 / M);
|
|
}
|
|
|
|
// Returns the polygon's projection on the given plane as another polygon
|
|
|
|
}, {
|
|
key: 'projectionOn',
|
|
value: function projectionOn(plane) {
|
|
var points = [];
|
|
this.vertices.each(function (node) {
|
|
points.push(plane.pointClosestTo(node.data));
|
|
});
|
|
return Polygon.create(points);
|
|
}
|
|
|
|
// Removes the given vertex from the polygon as long as it's not triangular.
|
|
|
|
}, {
|
|
key: 'removeVertex',
|
|
value: function removeVertex(vertex) {
|
|
if (this.isTriangle()) {
|
|
return;
|
|
}
|
|
var node = this.nodeFor(vertex);
|
|
if (node === null) {
|
|
return null;
|
|
}
|
|
this.clearCache();
|
|
|
|
// Previous and next entries in the main vertex list
|
|
var prev = node.prev;
|
|
|
|
var next = node.next;
|
|
var prevWasConvex = prev.data.isConvex(this);
|
|
var nextWasConvex = next.data.isConvex(this);
|
|
if (node.data.isConvex(this)) {
|
|
this.convexVertices.remove(this.convexVertices.withData(node.data));
|
|
} else {
|
|
this.reflexVertices.remove(this.reflexVertices.withData(node.data));
|
|
}
|
|
this.vertices.remove(node);
|
|
// Deal with previous vertex's change of class
|
|
if (prevWasConvex !== prev.data.isConvex(this)) {
|
|
if (prevWasConvex) {
|
|
this.convexVertices.remove(this.convexVertices.withData(prev.data));
|
|
this.reflexVertices.append(new _linkedlist.LinkedList.Node(prev.data));
|
|
} else {
|
|
this.reflexVertices.remove(this.reflexVertices.withData(prev.data));
|
|
this.convexVertices.append(new _linkedlist.LinkedList.Node(prev.data));
|
|
}
|
|
}
|
|
// Deal with next vertex's change of class
|
|
if (nextWasConvex !== next.data.isConvex(this)) {
|
|
if (nextWasConvex) {
|
|
this.convexVertices.remove(this.convexVertices.withData(next.data));
|
|
this.reflexVertices.append(new _linkedlist.LinkedList.Node(next.data));
|
|
} else {
|
|
this.reflexVertices.remove(this.reflexVertices.withData(next.data));
|
|
this.convexVertices.append(new _linkedlist.LinkedList.Node(next.data));
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
// Returns true iff the point is strictly inside the polygon
|
|
|
|
}, {
|
|
key: 'contains',
|
|
value: function contains(point) {
|
|
return this.containsByWindingNumber(point);
|
|
}
|
|
|
|
// Returns true iff the given point is strictly inside the polygon using the winding number method
|
|
|
|
}, {
|
|
key: 'containsByWindingNumber',
|
|
value: function containsByWindingNumber(point) {
|
|
var P = point.elements || point;
|
|
if (!this.plane.contains(P)) {
|
|
return false;
|
|
}
|
|
if (this.hasEdgeContaining(P)) {
|
|
return false;
|
|
}
|
|
|
|
var theta = 0;
|
|
var loops = 0;
|
|
var self = this;
|
|
this.vertices.each(function (node) {
|
|
var V = node.data.elements;
|
|
var W = node.next.data.elements;
|
|
var A = _vector.Vector.create([V[0] - P[0], V[1] - P[1], V[2] - (P[2] || 0)]);
|
|
var B = _vector.Vector.create([W[0] - P[0], W[1] - P[1], W[2] - (P[2] || 0)]);
|
|
var dt = A.angleFrom(B);
|
|
if (dt === null || dt === 0) {
|
|
return;
|
|
}
|
|
theta += (A.cross(B).isParallelTo(self.plane.normal) ? 1 : -1) * dt;
|
|
if (theta >= 2 * Math.PI - _sylvester.Sylvester.precision) {
|
|
loops++;
|
|
theta -= 2 * Math.PI;
|
|
}
|
|
if (theta <= -2 * Math.PI + _sylvester.Sylvester.precision) {
|
|
loops--;
|
|
theta += 2 * Math.PI;
|
|
}
|
|
});
|
|
return loops !== 0;
|
|
}
|
|
|
|
// Returns true if the given point lies on an edge of the polygon
|
|
// May cause problems with 'hole-joining' edges
|
|
|
|
}, {
|
|
key: 'hasEdgeContaining',
|
|
value: function hasEdgeContaining(point) {
|
|
var P = point.elements || point;
|
|
var success = false;
|
|
this.vertices.each(function (node) {
|
|
if (!success && _line.Line.Segment.create(node.data, node.next.data).contains(P)) {
|
|
success = true;
|
|
}
|
|
});
|
|
|
|
return success;
|
|
}
|
|
|
|
// Returns an array of 3-vertex polygons that the original has been split into
|
|
// Stores the first calculation for faster retrieval later on
|
|
|
|
}, {
|
|
key: 'toTriangles',
|
|
value: function toTriangles() {
|
|
if (this.cached.triangles !== null) {
|
|
return this.cached.triangles;
|
|
}
|
|
return this.setCache('triangles', this.triangulateByEarClipping());
|
|
}
|
|
|
|
// Implementation of ear clipping algorithm
|
|
// Found in 'Triangulation by ear clipping', by David Eberly
|
|
// at http://www.geometrictools.com
|
|
// This will not deal with overlapping sections - contruct your polygons sensibly
|
|
|
|
}, {
|
|
key: 'triangulateByEarClipping',
|
|
value: function triangulateByEarClipping() {
|
|
var _this = this;
|
|
|
|
var poly = this.dup();
|
|
var triangles = [];
|
|
|
|
while (!poly.isTriangle()) {
|
|
var success = false;
|
|
var trig = void 0;
|
|
var mainNode = void 0;
|
|
|
|
var _loop = function _loop() {
|
|
success = true;
|
|
// Ear tips must be convex vertices - let's pick one at random
|
|
var convexNode = poly.convexVertices.randomNode();
|
|
var mainNode = poly.vertices.withData(convexNode.data);
|
|
// For convex vertices, this order will always be anticlockwise
|
|
var trig = Polygon.create([mainNode.data, mainNode.next.data, mainNode.prev.data], _this.plane);
|
|
// Now test whether any reflex vertices lie within the ear
|
|
poly.reflexVertices.each(function (node) {
|
|
// eslint-disable-line no-loop-func
|
|
// Don't test points belonging to this triangle. node won't be
|
|
// equal to convexNode as node is reflex and vertex is convex.
|
|
if (node.data !== mainNode.prev.data && node.data !== mainNode.next.data) {
|
|
if (trig.contains(node.data) || trig.hasEdgeContaining(node.data)) {
|
|
success = false;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
while (!success) {
|
|
_loop();
|
|
}
|
|
triangles.push(trig);
|
|
poly.removeVertex(mainNode.data);
|
|
}
|
|
// Need to do this to renumber the remaining vertices
|
|
triangles.push(Polygon.create(poly.vertices, this.plane));
|
|
return triangles;
|
|
}
|
|
|
|
// Sets the polygon's vertices
|
|
|
|
}, {
|
|
key: 'setVertices',
|
|
value: function setVertices(points, plane) {
|
|
var pointSet = points.toArray ? points.toArray() : points;
|
|
this.plane = plane && plane.normal ? plane.dup() : _plane.Plane.fromPoints(pointSet);
|
|
if (this.plane === null) {
|
|
return null;
|
|
}
|
|
this.vertices = new _linkedlist.LinkedList.Circular();
|
|
|
|
// Construct linked list of vertices. If each point is already a polygon
|
|
// vertex, we reference it rather than creating a new vertex.
|
|
var i = pointSet.length;
|
|
while (i--) {
|
|
var newVertex = pointSet[i].isConvex ? pointSet[i] : new Polygon.Vertex(pointSet[i]);
|
|
this.vertices.prepend(new _linkedlist.LinkedList.Node(newVertex));
|
|
}
|
|
this.clearCache();
|
|
this.populateVertexTypeLists();
|
|
return this;
|
|
}
|
|
|
|
// Constructs lists of convex and reflex vertices based on the main vertex list.
|
|
|
|
}, {
|
|
key: 'populateVertexTypeLists',
|
|
value: function populateVertexTypeLists() {
|
|
this.convexVertices = new _linkedlist.LinkedList.Circular();
|
|
this.reflexVertices = new _linkedlist.LinkedList.Circular();
|
|
var self = this;
|
|
this.vertices.each(function (node) {
|
|
// Split vertices into convex / reflex groups
|
|
// The LinkedList.Node class wraps each vertex so it can belong to many linked lists.
|
|
self[node.data.type(self) + 'Vertices'].append(new _linkedlist.LinkedList.Node(node.data));
|
|
});
|
|
}
|
|
|
|
// Gives the polygon its own local set of vertex points, allowing it to be
|
|
// transformed independently of polygons it may be sharing vertices with.
|
|
|
|
}, {
|
|
key: 'copyVertices',
|
|
value: function copyVertices() {
|
|
this.clearCache();
|
|
this.vertices.each(function (node) {
|
|
node.data = new Polygon.Vertex(node.data);
|
|
});
|
|
this.populateVertexTypeLists();
|
|
}
|
|
|
|
// Clear any cached properties
|
|
|
|
}, {
|
|
key: 'clearCache',
|
|
value: function clearCache() {
|
|
this.cached = {
|
|
triangles: null,
|
|
surfaceIntegralElements: null
|
|
};
|
|
}
|
|
|
|
// Set cached value and return the value
|
|
|
|
}, {
|
|
key: 'setCache',
|
|
value: function setCache(key, value) {
|
|
this.cached[key] = value;
|
|
return value;
|
|
}
|
|
|
|
// Returns a string representation of the polygon's vertices.
|
|
|
|
}, {
|
|
key: 'inspect',
|
|
value: function inspect() {
|
|
var points = [];
|
|
this.vertices.each(function (node) {
|
|
points.push(node.data.inspect());
|
|
});
|
|
return points.join(' -> ');
|
|
}
|
|
|
|
// Constructor function
|
|
|
|
}], [{
|
|
key: 'create',
|
|
value: function create(points, plane) {
|
|
var P = new Polygon();
|
|
return P.setVertices(points, plane);
|
|
}
|
|
}]);
|
|
|
|
return Polygon;
|
|
}();
|
|
|
|
var Vertex = exports.Vertex = function (_Vector) {
|
|
_inherits(Vertex, _Vector);
|
|
|
|
function Vertex(point) {
|
|
_classCallCheck(this, Vertex);
|
|
|
|
var _this2 = _possibleConstructorReturn(this, (Vertex.__proto__ || Object.getPrototypeOf(Vertex)).call(this));
|
|
|
|
_this2.setElements(point);
|
|
if (_this2.elements.length === 2) {
|
|
_this2.elements.push(0);
|
|
}
|
|
if (_this2.elements.length !== 3) {
|
|
var _ret2;
|
|
|
|
return _ret2 = null, _possibleConstructorReturn(_this2, _ret2);
|
|
}
|
|
return _this2;
|
|
}
|
|
|
|
// Returns true iff the vertex's internal angle is 0 <= x < 180
|
|
// in the context of the given polygon object. Returns null if the
|
|
// vertex does not exist in the polygon.
|
|
|
|
|
|
_createClass(Vertex, [{
|
|
key: 'isConvex',
|
|
value: function isConvex(polygon) {
|
|
var node = polygon.nodeFor(this);
|
|
if (node === null) {
|
|
return null;
|
|
}
|
|
var prev = node.prev.data;
|
|
var next = node.next.data;
|
|
var A = next.subtract(this);
|
|
var B = prev.subtract(this);
|
|
var theta = A.angleFrom(B);
|
|
if (theta <= _sylvester.Sylvester.precision) {
|
|
return true;
|
|
}
|
|
if (Math.abs(theta - Math.PI) <= _sylvester.Sylvester.precision) {
|
|
return false;
|
|
}
|
|
return A.cross(B).dot(polygon.plane.normal) > 0;
|
|
}
|
|
|
|
// Returns true iff the vertex's internal angle is 180 <= x < 360
|
|
|
|
}, {
|
|
key: 'isReflex',
|
|
value: function isReflex(polygon) {
|
|
var result = this.isConvex(polygon);
|
|
return result === null ? null : !result;
|
|
}
|
|
}, {
|
|
key: 'type',
|
|
value: function type(polygon) {
|
|
var result = this.isConvex(polygon);
|
|
return result === null ? null : result ? 'convex' : 'reflex';
|
|
}
|
|
|
|
// Method for converting a set of arrays/vectors/whatever to a set of Vertex objects
|
|
|
|
}], [{
|
|
key: 'convert',
|
|
value: function convert(points) {
|
|
var pointSet = points.toArray ? points.toArray() : points;
|
|
var list = [];
|
|
var n = pointSet.length;
|
|
for (var i = 0; i < n; i++) {
|
|
list.push(new Vertex(pointSet[i]));
|
|
}
|
|
return list;
|
|
}
|
|
}]);
|
|
|
|
return Vertex;
|
|
}(_vector.Vector);
|
|
|
|
Polygon.Vertex = Vertex; |