/** Copyright 2014-2019 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.perim = 0.0; // for caching the result 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.toString = function() { let l; if (this.inner && this.inner.length) { l = '/' + this.inner.map(i => i.toString()).join(','); } else { l = ''; } return `P[${this.points.length,this.area().toFixed(2)}${l}]`; }; // return which plane (x,y,z) this polygon is coplanar with PRO.alignment = function() { if (this._aligned) return this._aligned; let diff = {x: false, y: false, z: false}; let last = undefined; // flatten points into array for earcut() this.points.forEach(p => { if (last) { diff.x = diff.x || last.x !== p.x; diff.y = diff.y || last.y !== p.y; diff.z = diff.z || last.z !== p.z; } last = p; }); return this._aligned = diff.x === false ? 'yz' : diff.y === false ? 'xz' : 'xy'; }; // ensure alignment with XY plane. mark if axes are swapped. PRO.ensureXY = function() { if (this._swapped) return this; switch (this.alignment()) { case 'xy': break; case 'yz': this.swap(true,false)._swapped = true; break; case 'xz': this.swap(false,true)._swapped = true; break; default: throw `invalid alignment`; } return this; }; // restore to original planar alignment if swapped PRO.restoreXY = function() { if (!this._swapped) return this; switch (this.alignment()) { case 'xy': break; case 'yz': this.swap(true,false)._swapped = false; break; case 'xz': this.swap(false,true)._swapped = false; break; } return this; }; PRO.earcut = function() { // gather all points into a single array including inner polys // keeping track of array offset indices for inners let out = []; let holes = []; // flatten points into array for earcut() this.points.forEach(p => { out.push(p.x, p.y, p.z); }); // add hole offsets for inner polygons if (this.inner) { this.inner.forEach(p => { holes.push(out.length / 3); p.points.forEach(p => { out.push(p.x, p.y, p.z); }) }); } // perform earcut() let cut = earcut(out,holes,3); let ret = []; // preserve swaps in new polys for (let i=0; i= 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(pos.x, pos.y, pos.z)); pos.x += dir.x * lenx; pos.y += dir.y * leny; switch (dir.i++) { case 0: t = dir.x; dir.x = dir.y; dir.y = -t; break; case 1: t = dir.x; dir.x = dir.y; dir.y = t; break; case 2: t = dir.x; dir.x = dir.y; dir.y = -t; break; case 3: t = dir.x; dir.x = dir.y; dir.y = t; break; } lenx -= offset/2; leny -= offset/2; dir.i = dir.i % 4; } return this; }; /** * add points forming a circle around a center point * * @param {Point} center * @param {number} radius * @param {number} points * @param {boolean} clockwise */ PRO.centerCircle = function(center, radius, points, clockwise) { var angle = 0, add = 360 / points; if (clockwise) add = -add; while (points-- > 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) }); }; /** * hint 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(); } 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); }; PRO.renderSolid = function(layer, color) { layer.solid(this, color); }; /** * 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]).reverse() ]; }; /** * 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() { if (this.perim !== 0.0) { return this.perim; } var len = 0.0; this.forEachSegment(function(prev,next) { len += SQRT(prev.distToSq2D(next)); }, this.open); return this.perim = 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 poly.area() ? this.fillang : poly.fillang, clib = self.ClipperLib, ctyp = clib.ClipType, ptyp = clib.PolyType, cfil = clib.PolyFillType, clip = new clib.Clipper(), ctre = new clib.PolyTree(), sp1 = this.toClipper(), sp2 = poly.toClipper(); clip.AddPaths(sp1, ptyp.ptSubject, true); clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { poly = POLY().fromClipperTree(ctre, poly.getZ()); poly.forEach(function(p) { p.fillang = fillang; }) return poly; } else { return null; } }; /** * @param {Polygon} poly clipping mask * @returns {?Polygon[]} */ PRO.mask = function(poly) { var fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang, clib = self.ClipperLib, ctyp = clib.ClipType, ptyp = clib.PolyType, cfil = clib.PolyFillType, clip = new clib.Clipper(), ctre = new clib.PolyTree(), sp1 = this.toClipper(), sp2 = poly.toClipper(); clip.AddPaths(sp1, ptyp.ptSubject, true); clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { poly = POLY().fromClipperTree(ctre, poly.getZ()); poly.forEach(function(p) { p.fillang = fillang; }) return poly; } else { return null; } }; /** * return logical OR of two polygons' enclosed areas * * @param {Polygon} poly * @returns {?Polygon} intersected polygon or null if no intersection */ PRO.union = function(poly) { if (!this.overlaps(poly)) return null; var fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang, clib = self.ClipperLib, ctyp = clib.ClipType, ptyp = clib.PolyType, cfil = clib.PolyFillType, clip = new clib.Clipper(), ctre = new clib.PolyTree(), sp1 = this.toClipper(), sp2 = poly.toClipper(); clip.AddPaths(sp1, ptyp.ptSubject, true); clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctUnion, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { poly = POLY().fromClipperTree(ctre, poly.getZ()); if (poly.length === 1) { poly = poly[0]; poly.fillang = fillang; return poly; } } return null; }; /** * rotate such that first and last points are the points * furthest apart and lowest when there is a tie breaker */ PRO.spread = function() { var poly = this, points = poly.points, plen = points.length, i, pp, np, p, mdelta, newmax, max, shift, maxd; max = {d:0}; maxd = 0; // find two most distance points for (i=0; i maxd; maxd = MAX(maxd,p); // select lowest points (corner case = square) if (newmax || (mdelta < 0.001 && max.p1 && MIN(pp.z,np.z) < MIN(max.p1.z, max.p2.z))) { max = {p1:pp, p2:np, i:(i+1)%plen, d:p}; } } if (max.i > 0) { // shift array to start at "leftmost" Point shift = points.slice(max.i); shift.appendAll(points.slice(0,max.i)); return newPolygon(shift); } return poly; } /** ****************************************************************** * Connect to base and Helpers ******************************************************************* */ function newPolygon(points) { return new Polygon(points); } })();