/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ "use strict"; var gs_base_polygon = exports; (function() { if (!self.base) self.base = {}; if (self.base.Polygon) return; var BASE = self.base, CONF = BASE.config, UTIL = BASE.util, DBUG = BASE.debug, KEYS = BASE.key, SQRT = Math.sqrt, POLY = function() { return BASE.polygons }, ABS = Math.abs, MIN = Math.min, MAX = Math.max, PI = Math.PI, DEG2RAD = PI / 180, Bounds = BASE.Bounds, newPoint = BASE.newPoint, PRO = Polygon.prototype, nest_test_slope = BASE.newSlope(newPoint(0,0,0,KEYS.NONE), newPoint(1,0,0,KEYS.NONE)), min_area_mult = 2.0, seqid = 1; BASE.Polygon = Polygon; BASE.newPolygon = newPolygon; /** ****************************************************************** * Constructors ******************************************************************* */ /** * @param {Points[]} [points] to seed poly * @constructor */ function Polygon(points) { this.id = seqid++; // polygon unique id this.open = false; this.length = 0; // number of points this.points = []; // ordered array of points this.area2 = 0.0; // computed as 2x area (sign = direction) this.bounds = new Bounds(); this.inner = null; // array of enclosed polygons (if any) this.parent = null; // enclosing parent polygon this.depth = 0; // depth nested from top parent (density for support fill) this.delete = false; // for culling during tracing this.fillang = null; // hinted fill angle this.fills = null; // fill lines (only used by supports currently) if (points) this.addPoints(points); } /** ****************************************************************** * Polygon Filter/Chain Functions ******************************************************************* */ Polygon.filterTooSmall = function(p) { return p.length < 3 ? null : p; }; Polygon.filterTooSkinny = function(p) { return (p.circularityDeep() * p.areaDeep()) < 0.25 ? null : p; }; Polygon.filterArea = function(area) { return function(p) { return p.area() >= area ? p : null; }; }; Polygon.filterDeleted = function(p) { return p.delete ? null : p; }; Polygon.filterInside = function(pin) { return function(p) { return pin.contains(p); } }; Polygon.filterCollect = function(out) { return function(p) { out.push(p); return p; } }; Polygon.filterEvolve = function(p) { return p.evolve(); }; Polygon.filterChain = function(filters) { return function() { var f = filters; return function(p) { var len = f.length, idx = 0; while (idx < len && (p = f[idx++](p))) ; return p; } }(); }; /** ****************************************************************** * Polygon Prototype Functions ******************************************************************* */ PRO.createConvexHull = function(points) { function removeMiddle(a, b, c) { var cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x); var dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y); return cross < 0 || cross == 0 && dot <= 0; } points.sort(function (a, b) { return a.x != b.x ? a.x - b.x : a.y - b.y; }); var n = points.length; var hull = []; for (var i = 0; i < 2 * n; i++) { var j = i < n ? i : 2 * n - 1 - i; while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j])) hull.pop(); hull.push(points[j]); } hull.pop(); this.addPoints(hull); return this; }; PRO.stepsFromRoot = function() { var p = this.parent, steps = 0; while (p) { if (p.inner && p.inner.length > 1) steps++; p = p.parent; } return steps; }; PRO.first = function() { return this.points[0]; }; PRO.last = function() { return this.points[this.length-1]; }; PRO.swap = function(x,y) { var poly = this, points = poly.points, length = points.length; poly.bounds = new Bounds(); for (var i=0; i 0) { this.push(newPoint( UTIL.round(Math.cos(angle * DEG2RAD) * radius, 7) + center.x, UTIL.round(Math.sin(angle * DEG2RAD) * radius, 7) + center.y, center.z )); angle += add; } return this; }; /** * offset all points * @param {THREE.Vector3} offset * @returns {Polygon} */ PRO.move = function(offset) { var scope = this, bounds = scope.bounds = new Bounds(); scope.points.forEach(function(p) { p.move(offset); bounds.update(p); }); if (scope.inner) scope.inner.forEach(function(p) { p.move(offset) }); return scope; }; PRO.scale = function(scale, round) { var scope = this, bounds = scope.bounds = new Bounds(); scope.points.forEach(function(p) { p.x *= scale; p.y *= scale; p.z *= scale; if (round) { p.x = UTIL.round(p.x, round); p.y = UTIL.round(p.y, round); p.z = UTIL.round(p.z, round); } bounds.update(p); }); if (scope.inner) scope.inner.forEach(function(i) { i.scale(scale,round) }); }; /** * add fill angle hinting from longest segment */ PRO.hintFillAngle = function() { var index = 0, points = this.points, length = points.length, prev, next, dist2, longest, mincir = CONF.hint_min_circ, minlen = CONF.hint_len_min, maxlen = CONF.hint_len_max || Infinity; while (index < length) { prev = points[index]; next = points[++index % length]; dist2 = prev.distToSq2D(next); if (dist2 >= minlen && dist2 <= maxlen && (!longest || dist2 > longest.len)) { longest = {p1:prev, p2:next, len:dist2}; } } if (longest && this.circularity() >= mincir) { this.fillang = longest.p1.slopeTo(longest.p2).normal(); // console.log([this.fillang,this.getZ(),this.id]); } return this.fillang; }; /** * todo make more efficient * * @param {Boolean} deep * @returns {Polygon} */ PRO.clone = function(deep) { var np = newPolygon(), ln = this.length, i = 0; while (i < ln) np.push(this.points[i++]); np.fillang = this.fillang; np.depth = this.depth; np.open = this.open; if (deep && this.inner) { np.inner = this.inner.clone(); } return np; }; /** * set all points' z value * * @param {number} z * @returns {Polygon} this */ PRO.setZ = function(z) { var ar = this.points, ln = ar.length, i = 0; while (i < ln) ar[i++].z = z; if (this.inner) this.inner.forEach(function(c) {c.setZ(z)}); return this; }; /** * @returns {number} z value of first point */ PRO.getZ = function() { return this.points[0].z; }; /** * * @param {Layer} layer * @param {number} color * @param {boolean} [recursive] * @param {boolean} [open] */ PRO.render = function(layer, color, recursive, open) { layer.poly(this, color, recursive, open); }; /** * add new point and return polygon reference for chaining * * @param {number} x * @param {number} y * @param {number} [z] * @returns {Polygon} */ PRO.add = function(x,y,z) { this.push(newPoint(x,y,z)); return this; }; /** * append array of points to polygon and return polygon * * @param {Point[]} points * @returns {Polygon} */ PRO.addPoints = function(points) { var poly = this, length = points.length, i = 0; while (i < length) { poly.push(points[i++]); } return this; }; /** * append point to polygon and return point * * @param {Point} p * @returns {Point} */ PRO.push = function(p) { // clone any point belonging to another polygon if (p.poly) p = p.clone(); p.poly = this; this.length++; this.points.push(p); this.bounds.update(p); return p; }; /** * append point to polygon and return polygon * * @param {Point} p * @returns {Polygon} */ PRO.append = function(p) { this.push(p); return this; }; /** close polygon */ PRO.setClosed = function() { this.open = false; return this; }; /** open polygon */ PRO.setOpen = function() { this.open = true; return this; }; PRO.isOpen = function() { return this.open; }; PRO.isClosed = function() { return !this.open; }; PRO.setClockwise = function() { if (!this.isClockwise()) this.reverse(); return this; }; PRO.setCounterClockwise = function() { if (this.isClockwise()) this.reverse(); return this; }; PRO.isClockwise = function() { return this.area(true) > 0; }; PRO.showKey = function() { return [this.first().key,this.last().key,this.length].join('~~'); }; /** * set this polygon's winding in alignment with the supplied polygon * * @param {Polygon} poly * @param [boolean] toLongest * @returns {Polygon} self */ PRO.alignWinding = function(poly, toLongest) { if (toLongest && this.length > poly.length) { poly.alignWinding(this, false); } else if (this.isClockwise() !== poly.isClockwise()) { this.reverse(); } }; /** * set this polygon's winding in opposition to supplied polygon * * @param {Polygon} poly * @param [boolean] toLongest * @returns {Polygon} self */ PRO.opposeWinding = function(poly, toLongest) { if (toLongest && this.length > poly.length) { poly.opposeWinding(this, false); } else if (this.isClockwise() === poly.isClockwise()) { this.reverse(); } }; /** * reverse direction of polygon points. * @returns {Polygon} self */ PRO.reverse = function() { this.area2 = -this.area2; this.points = this.points.reverse(); return this; }; /** * return true if this polygon is (likely) nested inside parent * * @param {Polygon} parent * @returns {boolean} */ PRO.isNested = function(parent) { if (parent.bounds.contains(this.bounds)) { //var int = POLY().rayIntersect(this.bounds.leftMost, nest_test_slope, [parent], false); //return int.length % 2 === 1 || this.isInside(parent, CONF.precision_nested_sq); return this.isInside(parent, CONF.precision_nested_sq); } return false; }; PRO.forEachPointEaseDown = function(fn, fromPoint) { var index = this.findClosestPointTo(fromPoint).index, fromZ = fromPoint.z, offset = 0, points = this.points, length = points.length, touch = -1, // first point to touch target z targetZ = points[0].z, dist2next, last, next, done; while (true) { next = points[index % length]; if (last && next.z < fromZ) { var deltaZ = fromZ - next.z; dist2next = last.distTo2D(next); if (dist2next > deltaZ * 2) { // too long: synth intermediate fn(last.followTo(next, deltaZ).setZ(next.z), offset++); } else if (dist2next >= deltaZ) { // ease down on this segment } else { // too short: clone n move z next = next.clone().setZ(fromZ - dist2next/2); } fromZ = next.z; } else if (offset === 0 && next.z < fromZ) { next = next.clone().setZ(fromZ); } last = next; fn(next, offset++); if ((index % length) === touch) break; if (touch < 0 && next.z <= targetZ) touch = (index % length); index++; } return last; }; PRO.forEachPoint = function(fn, close, start) { var index = start || 0, points = this.points, length = points.length, count = close ? length + 1 : length, offset = 0, pos; while (count-- > 0) { pos = index % length; if (fn(points[pos], pos, points, offset++)) return; index++; } }; PRO.forEachSegment = function(fn, open, start) { var index = start || 0, points = this.points, length = points.length, count = open ? length - 1 : length, pos1, pos2; while (count-- > 0) { pos1 = index % length; pos2 = (index+1) % length; if (fn(points[pos1], points[pos2], pos1, pos2)) return; index++; } }; /** * returns intersections sorted by closest to lp1 */ PRO.intersections = function(lp1, lp2) { var list = []; this.forEachSegment(function(pp1, pp2, ip1, ip2) { var int = UTIL.intersect(lp1, lp2, pp1, pp2, BASE.key.SEGINT, false); if (int) { list.push(int); pp1.pos = ip1; pp2.pos = ip2; } }); list.sort(function(p1, p2) { return UTIL.distSq(lp1, p1) - UTIL.distSq(lp1, p2); }); return list; }; /** * using two points, split polygon into two open polygons * or return null if p1,p2 does not intersect or poly is open */ PRO.bisect = function(p1, p2) { if (this.isOpen()) return null; var copy = this.clone().setClockwise(); var int = copy.intersections(p1, p2); if (!int || int.length !== 2) return null; return [ copy.emitSegment(int[0], int[1]), copy.emitSegment(int[1], int[0]) ]; }; /** * emit new open poly between two intersection points of a clockwise poly. * used in cam tabs and fdm output perimeter traces on infill */ PRO.emitSegment = function(i1, i2) { var poly = newPolygon(), start = i1.p2.pos, end = i2.p1.pos; poly.setOpen(); poly.push(i1); this.forEachPoint(function(p, pos) { poly.push(p); if (p === i2.p1) return true; }, true, start); poly.push(i2); return poly; }; /** * @param {Polygon} poly * @param {number} [tolerance] * @returns {boolean} any points inside OR on edge */ PRO.hasPointsInside = function(poly, tolerance) { if (!poly.overlaps(this)) return false; var mid, exit = false; this.forEachSegment(function(prev, next) { // check midpoint on long lines if (prev.distTo2D(next) > CONF.precision_midpoint_check_dist) { mid = prev.midPointTo(next); if (mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { return exit = true; } } if (next.inPolygon(poly) || next.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { return exit = true; } }); return exit; }; /** * TODO replace isNested() with isInside() ? * * @param {Polygon} poly * @param {number} [tolerance] * @returns {boolean} all points inside OR on edge */ PRO.isInside = function(poly, tolerance) { // throw new Error("isInside"); if (!( // poly.overlaps(this) && this.bounds.isNested(poly.bounds) )) return false; var mid, midcheck, exit = true; this.forEachSegment(function(prev, next) { // check midpoint on long lines if (prev.distTo2D(next) > CONF.precision_midpoint_check_dist) { mid = prev.midPointTo(next); if (!(mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq))) { exit = false; return true; } } if (!(next.inPolygon(poly) || next.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq))) { exit = false; return true; } }); return exit; }; /** * @param {Polygon} poly * @param {number} [tolerance] * @returns {boolean} all points inside poly AND not inside children */ PRO.contains = function(poly, tolerance) { return (poly && poly.isInside(this, tolerance) && poly.isOutsideAll(this.inner, tolerance)); }; /** * * @param polys * @returns {boolean} */ PRO.containedBySet = function(polys) { if (!polys) return false; for (var i=0; i 0; }; /** * remove all inner polygons * @returns {Polygon} self */ PRO.clearInner = function() { this.inner = null; return this; }; PRO.newUndeleted = function() { var poly = newPolygon(); this.forEachPoint(function(p) { if (!p.del) poly.push(p); }); return poly; }; /** * http://www.ehow.com/how_5138742_calculate-circularity.html * @returns {number} 0.0 - 1.0 from flat to perfectly circular */ PRO.circularity = function() { return (4 * PI * this.area()) / UTIL.sqr(this.perimeter()); }; PRO.circularityDeep = function() { return (4 * PI * this.areaDeep()) / UTIL.sqr(this.perimeter()); }; /** * @returns {number} perimeter length (sum of all segment lengths) */ PRO.perimeter = function() { var len = 0.0; this.forEachSegment(function(prev,next) { len += SQRT(prev.distToSq2D(next)); }, this.open); return len; }; PRO.perimeterDeep = function() { var len = this.perimeter(); if (this.inner) this.inner.forEach(function(p) { len += p.perimeter() }); return len; }; /** * calculate and return the area enclosed by the polygon. * if raw is true, return a signed area equal to 2x the * enclosed area which also indicates winding direction. * * @param {boolean} [raw] * @returns {number} area */ PRO.area = function(raw) { if (this.length < 3) return 0; if (this.area2 === 0.0) { for (var p=this.points,pl=p.length,pi=0,p1,p2; pi