diff --git a/app.js b/app.js index 74546208..94c33847 100644 --- a/app.js +++ b/app.js @@ -309,15 +309,13 @@ const script = { "add/array", "add/three", "geo/base", - // "geo/render", "geo/point", "geo/points", "geo/slope", "geo/line", "geo/bounds", - "geo/polygon", "geo/polygons", - // "geo/gyroid", + "geo/polygon", "geo/mesh", "data/local", "data/index", @@ -383,8 +381,8 @@ const script = { "geo/slope", "geo/line", "geo/bounds", - "geo/polygon", "geo/polygons", + "geo/polygon", "geo/gyroid", "geo/mesh", // "moto/broker", @@ -432,8 +430,8 @@ const script = { "geo/slope", "geo/line", "geo/bounds", - "geo/polygon", "geo/polygons", + "geo/polygon", "geo/gyroid", "kiri-mode/fdm/driver", "kiri-mode/fdm/slice", @@ -453,7 +451,6 @@ const script = { "add/array", "add/three", "geo/base", - // "geo/render", "geo/point", "geo/points", "geo/slope", diff --git a/src/geo/gyroid.js b/src/geo/gyroid.js index 8c054b48..d6ec1b82 100644 --- a/src/geo/gyroid.js +++ b/src/geo/gyroid.js @@ -4,216 +4,217 @@ (function() { - if (self.base.gyroid) return; +if (self.base.gyroid) return; - const base = self.base; - const PI2 = Math.PI * 2; - let cache = {}; - let lastVal; - let lastRes = 0; - let lastSlice = 0; +const base = self.base; +const PI2 = Math.PI * 2; - /** - * @param off {number} z offset value from 0-1 - * @param res {number} resolution (pixels/slices per side) - */ - function slice(off, res, val) { - // auto clear cach if it hasn't been hit in the last 20 seconds - // or the requested resolution or tip values have changed - let now = Date.now(); - if (res !== lastRes || val !== lastVal || now - lastSlice > 20000) { - // console.log({clear_cache: now}); - cache = {}; +let cache = {}; +let lastVal; +let lastRes = 0; +let lastSlice = 0; + +/** + * @param off {number} z offset value from 0-1 + * @param res {number} resolution (pixels/slices per side) + */ +function slice(off, res, val) { + // auto clear cach if it hasn't been hit in the last 20 seconds + // or the requested resolution or tip values have changed + let now = Date.now(); + if (res !== lastRes || val !== lastVal || now - lastSlice > 20000) { + // console.log({clear_cache: now}); + cache = {}; + } + lastVal = val; + lastRes = res; + lastSlice = now; + let rez = parseInt(res || 200); + let inc = PI2 / rez; + let z = PI2 * off; + let key = (z % PI2).round(3); + let hit = cache[key]; + if (hit) { + return hit; + } + let tip = val || 0; + let edge = []; + let vals = []; + let points = 0; + let points_lr = 0; + let points_td = 0; + for (let x=0; x { + let erow = edge[y]; + let lval = vrow[vrow.length - 1]; + vrow.forEach((val, x) => { + if ( + (lval <= tip && val >= tip) || (lval >= tip && val <= tip) || + (lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip) + ) { + erow[x] = 1; + points++; + points_lr++; + } + lval = val; + }) + }); + + // top-down threshold search (green) + for (let x=0; x= tip) || (lval >= tip && val <= tip) || + (lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip) + ) { + if (edge[y][x]) { + edge[y][x] = 3; + } else { + edge[y][x] = 2; + points++ + } + points_td++; + } + lval = val; } - let tip = val || 0; - let edge = []; - let vals = []; - let points = 0; - let points_lr = 0; - let points_td = 0; - for (let x=0; x points_lr ? 'lr' : 'td'; + + // create sparse representation + let sparse = []; + let center = rez / 2; + edge.forEach((row,y) => { + row.forEach((val,x) => { + if (val) { + let dx = Math.abs(x - center); + let dy = Math.abs(y - center); + sparse.push({x: x/rez, y: y/rez, val, dist: Math.max(dx,dy)}); + } + }); + }); + sparse.sort((a,b) => { + return b.dist - a.dist; + }); + + // join sparse points array by closest distance + let polys = []; + let chain; + let added; + let cleared = 0; + let maxdist = 0.05; + + do { + chain = null; + for (let i=0; i { - let erow = edge[y]; - let lval = vrow[vrow.length - 1]; - vrow.forEach((val, x) => { - if ( - (lval <= tip && val >= tip) || (lval >= tip && val <= tip) || - (lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip) - ) { - erow[x] = 1; - points++; - points_lr++; - } - lval = val; - }) - }); - - // top-down threshold search (green) - for (let x=0; x= tip) || (lval >= tip && val <= tip) || - (lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip) - ) { - if (edge[y][x]) { - edge[y][x] = 3; - } else { - edge[y][x] = 2; - points++ - } - points_td++; - } - lval = val; - } - } - - // deterime prevailing direction for chaining - let dir = points_td > points_lr ? 'lr' : 'td'; - - // create sparse representation - let sparse = []; - let center = rez / 2; - edge.forEach((row,y) => { - row.forEach((val,x) => { - if (val) { - let dx = Math.abs(x - center); - let dy = Math.abs(y - center); - sparse.push({x: x/rez, y: y/rez, val, dist: Math.max(dx,dy)}); - } - }); - }); - sparse.sort((a,b) => { - return b.dist - a.dist; - }); - - // join sparse points array by closest distance - let polys = []; - let chain; - let added; - let cleared = 0; - let maxdist = 0.05; - do { - chain = null; + added = false; + let target = chain[chain.length - 1]; + let cl_elm = null; + let cl_idx = null; + let cl_dst = Infinity; for (let i=0; i maxdist) { break; } + sparse[cl_idx] = null; + cleared++; + chain.push(cl_elm); + added = true; } - do { - added = false; - let target = chain[chain.length - 1]; - let cl_elm = null; - let cl_idx = null; - let cl_dst = Infinity; - for (let i=0; i maxdist) { - break; - } - sparse[cl_idx] = null; - cleared++; - chain.push(cl_elm); - added = true; - } - } while (added); - } while (cleared < sparse.length); + } while (added); + } while (cleared < sparse.length); - let psimple = polys - .map(poly => filter(poly, 0)) - .map(poly => filter(poly, inc)); + let psimple = polys + .map(poly => filter(poly, 0)) + .map(poly => filter(poly, inc)); - let slice = {edge, points, dir, polys: psimple}; - cache[key] = slice; - return slice; + let slice = {edge, points, dir, polys: psimple}; + cache[key] = slice; + return slice; +} + +// merge co-linear and distance threshold +function filter(poly, inc) { + if (poly.length <= 2) { + return poly; } - - // merge co-linear and distance threshold - function filter(poly, inc) { - if (poly.length <= 2) { - return poly; + let nupoly = [ poly[0] ]; + let e1 = poly[1]; + let e2 = null; + let last = poly.length - 2; + for (let i=1; i 1) return UTIL.distSq(p, p2); + if (t < 0) return util.distSq(p, p1); + if (t > 1) return util.distSq(p, p2); - return UTIL.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)); - }; - - // ---( begin fix distToLine )--- - - function dot(u, v) { - return u.x * v.x + u.y * v.y; + return util.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)); } - function norm(v) { - return Math.sqrt(dot(v,v)); - } - - function d(u, v) { - return norm({x: u.x - v.x, y: u.y - v.y}); - } - - function p2l(p, l1, l2) { - let v = {x: l2.x - l1.x, y: l2.y - l1.y}; - let w = {x: p.x - l1.x, y: p.y - l1.y}; - let c1 = dot(w, v); - let c2 = dot(v, v); - let b = c1 / c2; - if (isNaN(b)) { - // console.log('nan', {p, l1, l2, v, w, c1, c2}); - b = 0; - } - let pb = {x: l1.x + b * v.x, y: l1.y + b * v.y}; - return d(p, pb); - } - - // ---( end fix distToLine )--- - - /** - * - * @param {Point} p1 - * @param {Point} p2 - * @param {number} dist2 - * @returns {boolean} - */ - PRO.withinDist2 = function(p1, p2, dist2) { + withinDist2(p1, p2, dist2) { let ll2 = p1.distToSq2D(p2), dp1 = this.distToSq2D(p1), dp2 = this.distToSq2D(p2); @@ -231,71 +153,53 @@ // dist2 to the described line segment. if (dp1 > ll2 && dp2 > ll2) return false; return this.distToLineSq(p1, p2) < dist2; - }; + } - /** - * @param {Point} p2 - * @returns {Point} - */ - PRO.midPointTo = function(p2) { - return newPoint((this.x + p2.x)/2, (this.y + p2.y)/2, this.z); - }; + midPointTo(p2) { + return newPoint((this.x + p2.x) / 2, (this.y + p2.y) / 2, this.z); + } - /** - * @param {Point} p2 - * @returns {Point} - */ - PRO.midPointTo3D = function(p2) { + midPointTo3D(p2) { return newPoint( - (this.x + p2.x)/2, - (this.y + p2.y)/2, - (this.z + p2.z)/2 + (this.x + p2.x) / 2, + (this.y + p2.y) / 2, + (this.z + p2.z) / 2 ); - }; + } /** * non-scale corrected version of follow() - * - * @param slope - * @param mult - * @returns {Point} */ - PRO.projectOnSlope = function(slope, mult) { + projectOnSlope(slope, mult) { return newPoint( this.x + slope.dx * mult, this.y + slope.dy * mult, this.z); - }; + } - PRO.followTo = function(point, mult) { + followTo(point, mult) { return this.follow(this.slopeTo(point), mult); - }; + } /** * return a point along the line this from point to p2 offset * by a distance. positive distances are closer to this point. - * - * @param p2 - * @param dist */ - PRO.offsetPointFrom = function(p2, dist) { + offsetPointFrom(p2, dist) { let p1 = this, dx = p2.x - p1.x, dy = p2.y - p1.y, ls = dist / Math.sqrt(dx * dx + dy * dy), ox = dx * ls, oy = dy * ls; - return newPoint(p2.x - ox, p2.y - oy, p2.z, KEYS.NONE); - }; + return newPoint(p2.x - ox, p2.y - oy, p2.z, key.NONE); + } /** * return a point along the line this from point to p2 offset * by a distance. positive distances are farther from this point. - * - * @param p2 - * @param dist */ - PRO.offsetPointTo = function(p2, dist) { + offsetPointTo(p2, dist) { let p1 = this, dx = p2.x - p1.x, dy = p2.y - p1.y; @@ -306,93 +210,82 @@ ox = dx * ls, oy = dy * ls; - return newPoint(p1.x + ox, p1.y + oy, p2.z, KEYS.NONE); - }; + return newPoint(p1.x + ox, p1.y + oy, p2.z, key.NONE); + } - /** - * @param {Point} p2 - * @param {number} offset - * @returns {Line} - */ - PRO.offsetLineTo = function(p2, offset) { + offsetLineTo(p2, offset) { let p1 = this, dx = p2.x - p1.x, dy = p2.y - p1.y, ls = offset / Math.sqrt(dx * dx + dy * dy), ox = dx * ls, oy = dy * ls, - np1 = newPoint(p1.x - oy, p1.y + ox, p1.z, KEYS.NONE), - np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, KEYS.NONE); + np1 = newPoint(p1.x - oy, p1.y + ox, p1.z, key.NONE), + np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE); np1.op = p1; np2.op = p2; - return BASE.newLine(np1, np2, KEYS.NONE); - }; + return base.newLine(np1, np2, key.NONE); + } - PRO.offset = function(x, y, z) { + offset(x, y, z) { return newPoint(this.x + x, this.y + y, this.z + z); - }; + } /** * checks if a point is inside of a polygon * does not check children/holes - * - * @param {Polygon} poly - * @returns {boolean} */ - PRO.inPolygon = function(poly) { + inPolygon(poly) { if (!poly.bounds.containsXY(this.x, this.y)) return false; - let p = poly.points, pl = p.length, p1, p2, i, inside = false; + let p = poly.points, + pl = p.length, + p1, p2, i, inside = false; - for (i=0; i= this.y) != (p2.y >= this.y) && - (this.x <= (p2.x - p1.x) * (this.y - p1.y) / (p2.y - p1.y) + p1.x)) - { + (this.x <= (p2.x - p1.x) * (this.y - p1.y) / (p2.y - p1.y) + p1.x)) { inside = !inside; } } return inside; - }; + } /** * returns true if the point is inside of a polygon but * not inside any of it's children - * - * @param {Polygon | Polygon[]} poly - * @return {boolean} true if inside outer but not inner */ - PRO.isInPolygon = function(poly) { - let point = this, i; + isInPolygon(poly) { + let point = this, + i; if (Array.isArray(poly)) { - for (i=0; i 0) && !this.nearPolygon(poly, mindist2); - }; + } /** * returns a new point following given slope for given distance * same as projectOnSlope() but scaled - * - * @param {Slope} slope - * @param {number} distance - * @returns {Point} */ - PRO.follow = function(slope, distance) { + follow(slope, distance) { let ls = distance / Math.sqrt(slope.dx * slope.dx + slope.dy * slope.dy); return newPoint(this.x + slope.dx * ls, this.y + slope.dy * ls, this.z); - }; + } /** - * for point, return z-plane intersecting point on line to next point - * - * @param {Point} p - * @param {number} z - * @returns {Point} + * for point, return intersecting point on z to next point if points + * are on either size of z */ - PRO.intersectZ = function(p, z) { + intersectZ(p, z) { let dx = p.x - this.x, dy = p.y - this.y, dz = p.z - this.z, pct = 1 - ((p.z - z) / dz); return newPoint(this.x + dx * pct, this.y + dy * pct, this.z + dz * pct); - }; + } - /** - * @param {Point} p - * @returns {boolean} - */ - PRO.isEqual2D = function(p) { + isEqual2D(p) { return this === p || (this.x === p.x && this.y === p.y); - }; + } /** * returns true if points are close enough to be considered equivalent - * - * @param {Point} p - * @returns {boolean} */ - PRO.isMergable2D = function(p) { - return this.isEqual2D(p) || (this.distToSq2D(p) < CONF.precision_merge_sq); - }; + isMergable2D(p) { + return this.isEqual2D(p) || (this.distToSq2D(p) < config.precision_merge_sq); + } /** * compares 3D point - * - * @param {Point} p - * @returns {boolean} */ - PRO.isEqual = function(p) { + isEqual(p) { return this === p || (this.x === p.x && this.y === p.y && this.z === p.z); - }; + } - PRO.isEqual2D = function(p) { + isEqual2D(p) { return this === p || (this.x === p.x && this.y === p.y); - }; + } /** * returns true if points are close enough to be considered equivalent - * - * @param {Point} p - * @returns {boolean} */ - PRO.isMergable3D = function(p) { - return this.isEqual(p) || (this.distToSq3D(p) < CONF.precision_merge_sq); - }; + isMergable3D(p) { + return this.isEqual(p) || (this.distToSq3D(p) < config.precision_merge_sq); + } /** * return true if point is inside 2D square size dist*2 around p - * - * @param {Point} p - * @param {number} dist - * @returns {boolean} */ - PRO.isInBox = function(p, dist) { + isInBox(p, dist) { return Math.abs(this.x - p.x) < dist && Math.abs(this.y - p.y) < dist; - }; + } /** * return min distance from point to a polygon @@ -532,7 +392,7 @@ * @param {Polygon} poly * @param {number} [threshold] stop looking if under threshold */ - PRO.distToPolySegments = function(poly, threshold) { + distToPolySegments(poly, threshold) { let point = this, mindist = Infinity; poly.forEachSegment(function(p1, p2) { @@ -542,32 +402,31 @@ if (mindist <= threshold) return true; }); return mindist; - }; + } /** * @param {Polygon} poly * @param {number} [threshold] stop looking if under threshold */ - PRO.distToPolyPoints = function(poly, threshold) { - let point = this, mindist = Infinity; + distToPolyPoints(poly, threshold) { + let point = this, + mindist = Infinity; poly.forEachPoint(function(pp) { mindist = Math.min(mindist, point.distTo2D(pp)); if (mindist < threshold) return true; }); return mindist; - }; + } /** - * @param {Point[]} points - * @param {number} max * @returns {Point} nearest point (less than max) from array to this point */ - PRO.nearestTo = function(points, max) { + nearestTo(points, max) { if (!max) throw "missing max"; let mind = Infinity, minp = null, i, p, d; - for (i=0; i 0; if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab) return false; if ((c.x - b.x) * (this.y - b.y) - (c.y - b.y) * (this.x - b.x) > 0 != s_ab) return false; return true; - }; + } /** * returns true if point is on a line described by two points. * test sum of distances p1->this + this->p2 ~= p1->p2 whens * slopes from p1->this same as this->p2 - * - * @param {Point} p1 - * @param {Point} p2 - * @returns {boolean} */ - PRO.onLine = function(p1, p2) { - return this.distToLine(p1, p2) < CONF.precision_point_on_line; - }; + onLine(p1, p2) { + return this.distToLine(p1, p2) < config.precision_point_on_line; + } - /** - * - * @param {THREE.Vector3} delta - * @return {Point} new offset point - */ - PRO.add = function(delta) { + add(delta) { return newPoint(this.x + delta.x, this.y + delta.y, this.z + delta.z); - }; + } - /** - * - * @param {THREE.Vector3} delta - * @return {Point} new offset point - */ - PRO.sub = function(delta) { + sub(delta) { return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z); - }; + } - /** - * - * @param {THREE.Vector3} delta - */ - PRO.move = function(delta) { + move(delta) { this.x += delta.x; this.y += delta.y; this.z += delta.z; return this; - }; - - /** ****************************************************************** - * Connect to base and Helpers - ******************************************************************* */ - - /** - * - * @param {number} x - * @param {number} y - * @param {number} z - * @param {String} [key] - * @returns {Point} - */ - function newPoint(x, y, z, key) { - return new Point(x, y, z, key); } +} + +function dot(u, v) { + return u.x * v.x + u.y * v.y; +} + +function norm(v) { + return Math.sqrt(dot(v, v)); +} + +function d(u, v) { + return norm({ + x: u.x - v.x, + y: u.y - v.y + }); +} + +function p2l(p, l1, l2) { + let v = { + x: l2.x - l1.x, + y: l2.y - l1.y + }; + let w = { + x: p.x - l1.x, + y: p.y - l1.y + }; + let c1 = dot(w, v); + let c2 = dot(v, v); + let b = c1 / c2; + if (isNaN(b)) { + // console.log('nan', {p, l1, l2, v, w, c1, c2}); + b = 0; + } + let pb = { + x: l1.x + b * v.x, + y: l1.y + b * v.y + }; + return d(p, pb); +} +function newPoint(x, y, z, key) { + return new Point(x, y, z, key); +} + +base.Point = Point; +base.newPoint = newPoint; +base.pointFromClipper = function(cp, z) { + return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z); +}; })(); diff --git a/src/geo/points.js b/src/geo/points.js index a00fec0e..9409d1a5 100644 --- a/src/geo/points.js +++ b/src/geo/points.js @@ -4,128 +4,125 @@ (function() { - const BASE = self.base; - const CONF = BASE.config; +const base = self.base; +const { config } = base; - BASE.verticesToPoints = verticesToPoints; - BASE.pointsToVertices = pointsToVertices; +base.verticesToPoints = verticesToPoints; +base.pointsToVertices = pointsToVertices; - /** - * converts a geometry point array into a kiri point array - * with auto-decimation - * - * @param {Float32Array} array - * @returns {Array} - */ - function verticesToPoints(array, options) { - let parr = new Array(array.length / 3), - i = 0, - j = 0, - t = Date.now(), - hash = {}, - unique = 0, - passes = 0, - points, - oldpoints = parr.length, - newpoints; +/** + * converts a geometry point array into a kiri point array + * with auto-decimation + */ +function verticesToPoints(array, options) { + let parr = new Array(array.length / 3), + i = 0, + j = 0, + t = Date.now(), + hash = {}, + unique = 0, + passes = 0, + points, + oldpoints = parr.length, + newpoints; - // replace point objects with their equivalents - while (i < array.length) { - let p = BASE.newPoint(array[i++], array[i++], array[i++]), - k = p.key, - m = hash[k]; - if (!m) { - m = p; - hash[k] = p; - unique++; - } - parr[j++] = m; + // replace point objects with their equivalents + while (i < array.length) { + let p = base.newPoint(array[i++], array[i++], array[i++]), + k = p.key, + m = hash[k]; + if (!m) { + m = p; + hash[k] = p; + unique++; } + parr[j++] = m; + } - let {threshold, precision, maxpass} = options || {}; - // threshold = point count for triggering decimation - // precision = under which points are merged - // maxpass = max number of decimations - threshold = threshold > 0 ? threshold : CONF.decimate_threshold; - precision = precision >= 0 ? precision : CONF.precision_decimate; - maxpass = maxpass >= 0 ? maxpass : 10; + let {threshold, precision, maxpass} = options || {}; + // threshold = point count for triggering decimation + // precision = under which points are merged + // maxpass = max number of decimations + threshold = threshold > 0 ? threshold : config.decimate_threshold; + precision = precision >= 0 ? precision : config.precision_decimate; + maxpass = maxpass >= 0 ? maxpass : 10; - // decimate until all point spacing > precision - if (maxpass && precision > 0.0) - while (parr.length > threshold) { - let lines = [], line, dec = 0; - for (i=0; i= precision) break; - // skip lines where one of the points is already offset - if (line.p1.op || line.p2.op) continue; - // todo skip dropping lines where either point is a "sharp" on 3 vectors - // todo skip dropping lines where either point connects to a "long" line - line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2); - dec++; - } - // exit if nothing to decimate - if (dec === 0) break; - passes++; - // create new facets - points = new Array(oldpoints); - newpoints = 0; - for (i=0; i= maxpass) { - break; - } + // decimate until all point spacing > precision + if (maxpass && precision > 0.0) + while (parr.length > threshold) { + let lines = [], line, dec = 0; + for (i=0; i= precision) break; + // skip lines where one of the points is already offset + if (line.p1.op || line.p2.op) continue; + // todo skip dropping lines where either point is a "sharp" on 3 vectors + // todo skip dropping lines where either point connects to a "long" line + line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2); + dec++; + } + // exit if nothing to decimate + if (dec === 0) break; + passes++; + // create new facets + points = new Array(oldpoints); + newpoints = 0; + for (i=0; i= maxpass) { + break; } - return vertices; } + // if (passes) console.trace({passes, threshold, precision, maxpass}); + + if (passes) console.log({ + before: array.length / 3, + after: parr.length, + unique: unique, + decimations: passes, + time: (Date.now() - t) + }); + + return parr; +} + +function pointsToVertices(points) { + let vertices = new Float32Array(points.length * 3), + i = 0, vi = 0; + while (i < points.length) { + vertices[vi++] = points[i].x; + vertices[vi++] = points[i].y; + vertices[vi++] = points[i++].z; + } + return vertices; +} + })(); diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 6e2f7811..bbf0a797 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -5,80 +5,57 @@ // dep: geo.bounds (function() { - if (self.base.Polygon) return; +const base = self.base; +if (base.Polygon) return; - const BASE = self.base, - CONF = BASE.config, - UTIL = BASE.util, - 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, - newPoint = BASE.newPoint, - Bounds = BASE.Bounds; +const { config, util, polygons, newBounds, newPoint } = base; +const POLY = polygons, + DEG2RAD = Math.PI / 180, + clib = self.ClipperLib, + clip = clib.Clipper, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType; - let seqid = Math.round(Math.random() * 0xffffffff); +let seqid = Math.round(Math.random() * 0xffffffff); - /** ****************************************************************** - * Constructors - ******************************************************************* */ - - class Polygon { - constructor(points) { - this.id = seqid++; // polygon unique id - this.open = false; - this.points = []; // ordered array of points - this.depth = 0; // depth nested from top parent (density for support fill) - if (points) { - this.addPoints(points); - } - } - - get length() { - return this.points.length; - } - - get deepLength() { - let len = this.length; - if (this.inner) { - for (let inner of this.inner) { - len += inner.length; - } - } - return len; - } - - get bounds() { - if (this._bounds) { - return this._bounds; - } - let bounds = this._bounds = new Bounds(); - for (let point of this.points) { - bounds.update(point); - } - return bounds; +class Polygon { + constructor(points) { + this.id = seqid++; // polygon unique id + this.open = false; + this.points = []; // ordered array of points + this.depth = 0; // depth nested from top parent (density for support fill) + if (points) { + this.addPoints(points); } } - BASE.Polygon = Polygon; + get length() { + return this.points.length; + } - BASE.newPolygon = newPolygon; + get deepLength() { + let len = this.length; + if (this.inner) { + for (let inner of this.inner) { + len += inner.length; + } + } + return len; + } - const PRO = Polygon.prototype; + get bounds() { + if (this._bounds) { + return this._bounds; + } + let bounds = this._bounds = newBounds(); + for (let point of this.points) { + bounds.update(point); + } + return bounds; + } - Polygon.fromArray = function(array) { - return newPolygon().fromArray(array); - }; - - /** ****************************************************************** - * Polygon Prototype Functions - ******************************************************************* */ - - PRO.toString = function(verbose) { + toString(verbose) { let l; if (this.inner && this.inner.length) { l = '/' + this.inner.map(i => i.toString(verbose)).join(','); @@ -90,34 +67,34 @@ } else { return `P[${this.points.length,this.area().toFixed(2)}${l}]`; } - }; + } - PRO.toArray = function() { + toArray() { let ov = this.open ? 1 : 0; - return this.points.map((p,i) => i === 0 ? [ov,p.x,p.y,p.z] : [p.x,p.y,p.z]).flat(); - }; + return this.points.map((p, i) => i === 0 ? [ov, p.x, p.y, p.z] : [p.x, p.y, p.z]).flat(); + } - PRO.fromArray = function(array) { + fromArray(array) { this.open = array[0] === 1; - for (let i=1; i 0.0001) return false; } return true; } return false; - }; + } - PRO.xray = function(deep) { + xray(deep) { const xray = { id: this.id, len: this.points.length, @@ -129,13 +106,17 @@ xray.inner = deep ? this.inner.xray(deep) : this.inner; } return xray; - }; + } // return which plane (x,y,z) this polygon is coplanar with - PRO.alignment = function() { + alignment() { if (this._aligned) return this._aligned; - let diff = {x: false, y: false, z: false}; + let diff = { + x: false, + y: false, + z: false + }; let last = undefined; // flatten points into array for earcut() @@ -151,32 +132,43 @@ 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() { + ensureXY() { 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`; + 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() { + restoreXY() { 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; + 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() { + earcut() { // gather all points into a single array including inner polys // keeping track of array offset indices for inners let out = []; @@ -198,36 +190,26 @@ } // perform earcut() - let cut = self.earcut(out,holes,3); + let cut = self.earcut(out, holes, 3); let ret = []; // preserve swaps in new polys - for (let i=0; i 180) diff -= 360; - return Math.abs(diff); } // generate center crossing point cloud - PRO.centers = function(step, z, min, max, opt = {}) { + centers(step, z, min, max, opt = {}) { let cloud = [], bounds = this.bounds, lines = opt.lines || false, @@ -240,23 +222,23 @@ } } - for (let y of UTIL.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) { + for (let y of util.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) { let ints = []; for (let points of set) { let length = points.length; - for (let i=0; i y) || (p1.y > y && p2.y <= y) - ) ints.push([p1,p2]); + ) ints.push([p1, p2]); } } let cntr = []; if (ints.length && ints.length % 2 === 0) { for (let int of ints) { - let [p1, p2] = int; + let [p1, p2] = int; if (p2.y < p1.y) { let tp = p1; p1 = p2; @@ -297,23 +279,23 @@ } } - for (let x of UTIL.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) { + for (let x of util.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) { let ints = []; for (let points of set) { let length = points.length; - for (let i=0; i x) || (p1.x > x && p2.x <= x) - ) ints.push([p1,p2]); + ) ints.push([p1, p2]); } } let cntr = []; if (ints.length && ints.length % 2 === 0) { for (let int of ints) { - let [p1, p2] = int; + let [p1, p2] = int; if (p2.x < p1.x) { let tp = p1; p1 = p2; @@ -384,7 +366,7 @@ let poly = []; while (cloud.length) { if (poly.length === 0) { - poly = [ cloud.shift() ]; + poly = [cloud.shift()]; polys.push(poly); continue; } @@ -406,7 +388,7 @@ return polys .filter(poly => poly.length > 1) .map(poly => { - let np = BASE.newPolygon().setOpen(); + let np = base.newPolygon().setOpen(); for (let p of poly) { np.push(p); } @@ -416,9 +398,9 @@ np = np.clean(); return np; }); - }; + } - PRO.debur = function(dist) { + debur(dist) { if (this.len < 2) { return null; } @@ -426,10 +408,10 @@ pln = pa.length, open = this.open, newp = newPolygon().copyZ(this.z), - min = dist || BASE.config.precision_merge; + min = dist || base.config.precision_merge; let lo; newp.push(lo = pa[0]); - for (let i=1; i= min) { newp.push(lo = pa[i]); } @@ -440,15 +422,18 @@ return null; } return newp; - }; + } - PRO.miter = function(debug) { + miter(debug) { if (this.length < 3) return this; - const slo = [], pa = this.points, pln = pa.length, open = this.open; + const slo = [], + pa = this.points, + pln = pa.length, + open = this.open; let last; - for (let i=1; i 90; } if (!open) { // ang[pln-1] = slopeDiff(slo[pln-2], slo[pln-1]); - ang[0] = slopeDiff(slo[pln-1], slo[0]); - redo |= ang[pln-1] > 90; + ang[0] = slopeDiff(slo[pln - 1], slo[0]); + redo |= ang[pln - 1] > 90; redo |= ang[0] > 90; } if (redo) { const newp = newPolygon().copyZ(this.z); // newp.debug = this.debug = true; newp.open = open; - for (let i=0; i 179) { - const s = slo[(i+pln) % pln]; - const pp = pa[(i+pln-1) % pln]; - const ps = slo[(i+pln-1) % pln]; + const s = slo[(i + pln) % pln]; + const pp = pa[(i + pln - 1) % pln]; + const ps = slo[(i + pln - 1) % pln]; newp.push(p.follow(p.slopeTo(pp).normal(), 0.001)); newp.push(p.follow(s.clone().normal().invert(), 0.001)); } else if (d > 90) { - const s = slo[(i+pln) % pln]; - const pp = pa[(i+pln-1) % pln]; - const ps = slo[(i+pln-1) % pln]; + const s = slo[(i + pln) % pln]; + const pp = pa[(i + pln - 1) % pln]; + const ps = slo[(i + pln - 1) % pln]; newp.push(p.follow(p.slopeTo(pp), 0.001)); newp.push(p.follow(s, 0.001)); } else { @@ -494,16 +479,16 @@ return newp; } return this; - }; + } - PRO.createConvexHull = function(points) { + createConvexHull(points) { function removeMiddle(a, b, c) { let cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x); let 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) { + points.sort(function(a, b) { return a.x != b.x ? a.x - b.x : a.y - b.y; }); @@ -518,29 +503,30 @@ } hull.pop(); - this.addPoints(hull); - return this; - }; - PRO.stepsFromRoot = function() { - let p = this.parent, steps = 0; + return this; + } + + stepsFromRoot() { + let p = this.parent, + steps = 0; while (p) { if (p.inner && p.inner.length > 1) steps++; p = p.parent; } return steps; - }; + } - PRO.first = function() { + first() { return this.points[0]; - }; + } - PRO.last = function() { - return this.points[this.length-1]; - }; + last() { + return this.points[this.length - 1]; + } - PRO.swap = function(x,y) { + swap(x, y) { this._bounds = undefined; if (x) { for (let p of this.points) { @@ -553,15 +539,15 @@ } if (this.inner) { for (let inner of this.inner) { - inner.swap(x,y); + inner.swap(x, y); } } return this; } // return average of all point positions - PRO.average = function() { - let ap = newPoint(0,0,0,null); + average() { + let ap = newPoint(0, 0, 0, null); this.points.forEach(p => { ap.x += p.x; ap.y += p.y; @@ -571,14 +557,16 @@ ap.y /= this.points.length; ap.z /= this.points.length; return ap; - }; + } /** * @param {boolean} [point] return just the center point * @returns {Polygon|Point} a new polygon centered on x=0, y=0, z=0 */ - PRO.center = function(point) { - let ap = newPoint(0,0,0,null), np = newPolygon(), pa = this.points; + center(point) { + let ap = newPoint(0, 0, 0, null), + np = newPolygon(), + pa = this.points; pa.forEach(function(p) { ap.x += p.x; ap.y += p.y; @@ -596,13 +584,15 @@ )); }); return np; - }; + } /** * @returns {Point} center of a polygon assuming it's a circle */ - PRO.circleCenter = function() { - let x=0, y=0, l=this.points.length; + circleCenter() { + let x = 0, + y = 0, + l = this.points.length; for (let point of this.points) { x += point.x; y += point.y; @@ -610,7 +600,7 @@ x /= l; y /= l; return newPoint(x, y, this.points[0].z, null); - }; + } /** * add points forming a rectangle around a center point @@ -619,7 +609,7 @@ * @param {number} width * @param {number} height */ - PRO.centerRectangle = function(center, width, height) { + centerRectangle(center, width, height) { width /= 2; height /= 2; this.push(newPoint(center.x - width, center.y - height, center.z)); @@ -627,59 +617,80 @@ this.push(newPoint(center.x + width, center.y + height, center.z)); this.push(newPoint(center.x - width, center.y + height, center.z)); return this; - }; + } /** * create square spiral (used for purge blocks) */ - PRO.centerSpiral = function(center, lenx, leny, offset, count) { + centerSpiral(center, lenx, leny, offset, count) { count *= 4; offset /= 2; - let pos = { x: center.x - lenx/2, y: center.y + leny/2, z: center.z }, - dir = { x: 1, y: 0, i: 0 }, t; + let pos = { + x: center.x - lenx / 2, + y: center.y + leny / 2, + z: center.z + }, + dir = { + x: 1, + y: 0, + i: 0 + }, + t; while (count-- > 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; + 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; + 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) { - let angle = 0, add = 360 / points; + centerCircle(center, radius, points, clockwise) { + let 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, + 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; - }; + } /** * move all poly points by some offset */ - PRO.move = function(offset) { + move(offset) { this._bounds = undefined; this.points = this.points.map(point => point.move(offset)); if (this.inner) { @@ -688,13 +699,13 @@ } } return this; - }; + } /** * scale polygon around origin */ - PRO.scale = function(scale, round) { - let x,y,z; + scale(scale, round) { + let x, y, z; if (typeof(scale) === 'number') { x = y = z = scale; } else { @@ -720,12 +731,12 @@ } } return this; - }; + } /** * hint fill angle hinting from longest segment */ - PRO.hintFillAngle = function() { + hintFillAngle() { let index = 0, points = this.points, length = points.length, @@ -733,16 +744,20 @@ next, dist2, longest, - mincir = CONF.hint_min_circ, - minlen = CONF.hint_len_min, - maxlen = CONF.hint_len_max || Infinity; + mincir = config.hint_min_circ, + minlen = config.hint_len_min, + maxlen = config.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}; + longest = { + p1: prev, + p2: next, + len: dist2 + }; } } @@ -751,7 +766,7 @@ } return this.fillang; - }; + } /** * todo make more efficient @@ -759,7 +774,7 @@ * @param {Boolean} deep * @returns {Polygon} */ - PRO.clone = function(deep) { + clone(deep) { let np = newPolygon().copyZ(this.z), ln = this.length, i = 0; @@ -775,10 +790,10 @@ } return np; - }; + } // special shallow for-render-or-read-only cloning - PRO.cloneZ = function(z, stop) { + cloneZ(z, stop) { let p = newPolygon(); p.z = z; p.open = this.open; @@ -787,14 +802,14 @@ p.inner = this.inner.map(p => p.cloneZ(z, true)); } return p; - }; + } - PRO.copyZ = function(z) { + copyZ(z) { if (z !== undefined) { this.z = z; } return this; - }; + } /** * set all points' z value @@ -802,51 +817,43 @@ * @param {number} z * @returns {Polygon} this */ - PRO.setZ = function(z) { + setZ(z) { let 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)}); + if (this.inner) this.inner.forEach(function(c) { + c.setZ(z) + }); return this; - }; + } /** * @returns {number} z value of first point */ - PRO.getZ = function(i) { + getZ(i) { return this.z !== undefined ? this.z : this.points[i || 0].z; - }; + } /** - * - * @param {Layer} layer - * @param {number} color - * @param {boolean} [recursive] - * @param {boolean} [open] */ - PRO.render = function(layer, color, recursive, open) { + render(layer, color, recursive, open) { layer.poly(this, color, recursive, open); - }; + } - PRO.renderSolid = function(layer, color) { + renderSolid(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)); + add(x, y, z) { + this.push(newPoint(x, y, z)); return this; - }; + } - PRO.addObj = function(obj) { + addObj(obj) { if (Array.isArray(obj)) { for (let o of obj) { this.addObj(o); @@ -858,11 +865,8 @@ /** * append array of points to polygon and return polygon - * - * @param {Point[]} points - * @returns {Polygon} */ - PRO.addPoints = function(points) { + addPoints(points) { let poly = this, length = points.length, i = 0; @@ -870,138 +874,120 @@ poly.push(points[i++]); } return this; - }; + } /** * append point to polygon and return point - * - * @param {Point} p - * @returns {Point} */ - PRO.push = function(p) { + push(p) { // clone any point belonging to another polygon if (p.poly) p = p.clone(); p.poly = this; this.points.push(p); return p; - }; + } /** * append point to polygon and return polygon - * - * @param {Point} p - * @returns {Polygon} */ - PRO.append = function(p) { + append(p) { this.push(p); return this; - }; + } /** close polygon */ - PRO.setClosed = function() { + setClosed() { this.open = false; return this; - }; + } /** open polygon */ - PRO.setOpen = function() { + setOpen() { this.open = true; return this; - }; + } - PRO.isOpen = function() { + isOpen() { return this.open; - }; + } - PRO.isClosed = function() { + isClosed() { return !this.open; - }; + } - PRO.appearsClosed = function() { + appearsClosed() { return this.first().isEqual(this.last()); - }; + } - PRO.setClockwise = function() { + setClockwise() { if (!this.isClockwise()) this.reverse(); return this; - }; + } - PRO.setCounterClockwise = function() { + setCounterClockwise() { if (this.isClockwise()) this.reverse(); return this; - }; + } - PRO.isClockwise = function() { + isClockwise() { return this.area(true) > 0; - }; + } - PRO.showKey = function() { - return [this.first().key,this.last().key,this.length].join('~~'); - }; + showKey() { + 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) { + alignWinding(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) { + opposeWinding(poly, toLongest) { if (toLongest && this.length > poly.length) { poly.opposeWinding(this, false); } else if (this.isClockwise() === poly.isClockwise()) { this.reverse(); } - }; + } /** * @returns {boolean} true if both polygons wind the same way */ - PRO.sameWindings = function(poly) { + sameWindings(poly) { return this.isClockwise() === poly.isClockwise(); - }; + } /** * reverse direction of polygon points. - * @returns {Polygon} self */ - PRO.reverse = function() { + reverse() { if (this.area2) { 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) { + isNested(parent) { if (parent.bounds.contains(this.bounds)) { - return this.isInside(parent, CONF.precision_nested_sq); + return this.isInside(parent, config.precision_nested_sq); } return false; - }; + } - PRO.forEachPointEaseDown = function(fn, fromPoint) { + forEachPointEaseDown(fn, fromPoint) { let index = this.findClosestPointTo(fromPoint).index, fromZ = fromPoint.z, offset = 0, @@ -1026,7 +1012,7 @@ // ease down on this segment } else { // too short: clone n move z - next = next.clone().setZ(fromZ - dist2next/2); + next = next.clone().setZ(fromZ - dist2next / 2); } fromZ = next.z; } else if (offset === 0 && next.z < fromZ) { @@ -1040,9 +1026,9 @@ } return last; - }; + } - PRO.forEachPoint = function(fn, close, start) { + forEachPoint(fn, close, start) { let index = start || 0, points = this.points, length = points.length, @@ -1055,9 +1041,9 @@ if (fn(points[pos], pos, points, offset++)) return; index++; } - }; + } - PRO.forEachSegment = function(fn, open, start) { + forEachSegment(fn, open, start) { let index = start || 0, points = this.points, length = points.length, @@ -1066,19 +1052,19 @@ while (count-- > 0) { pos1 = index % length; - pos2 = (index+1) % 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, deep) { + intersections(lp1, lp2, deep) { let list = []; this.forEachSegment(function(pp1, pp2, ip1, ip2) { - let int = UTIL.intersect(lp1, lp2, pp1, pp2, BASE.key.SEGINT, false); + let int = util.intersect(lp1, lp2, pp1, pp2, base.key.SEGINT, false); if (int) { list.push(int); // console.log('pp1.pos',pp1.pos,'to',ip1); @@ -1088,7 +1074,7 @@ } }); list.sort(function(p1, p2) { - return UTIL.distSq(lp1, p1) - UTIL.distSq(lp1, p2); + return util.distSq(lp1, p1) - util.distSq(lp1, p2); }); if (deep && this.inner) { this.inner.forEach(p => { @@ -1097,28 +1083,28 @@ }); } 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) { + bisect(p1, p2) { if (this.isOpen()) return null; let copy = this.clone().setClockwise(); let int = copy.intersections(p1, p2); - if (!int || int.length !== 2) return null; + if (!int || int.length !== 2) return null; - return [ copy.emitSegment(int[0], int[1]), copy.emitSegment(int[1], int[0]).reverse() ]; - }; + 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) { + emitSegment(i1, i2) { let poly = newPolygon(), start = i1.p2.pos, end = i2.p1.pos; @@ -1135,41 +1121,41 @@ poly.push(i2); // console.log({emit: poly}); return poly; - }; + } /** * @param {Polygon} poly * @param {number} [tolerance] * @returns {boolean} any points inside OR on edge */ - PRO.hasPointsInside = function(poly, tolerance) { + hasPointsInside(poly, tolerance) { if (!poly.overlaps(this)) return false; let mid, exit = false; this.forEachSegment(function(prev, next) { // check midpoint on long lines - if (prev.distTo2D(next) > CONF.precision_midpoint_check_dist) { + if (prev.distTo2D(next) > config.precision_midpoint_check_dist) { mid = prev.midPointTo(next); - if (mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { + if (mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || config.precision_close_to_poly_sq)) { return exit = true; } } - if (next.inPolygon(poly) || next.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { + if (next.inPolygon(poly) || next.nearPolygon(poly, tolerance || config.precision_close_to_poly_sq)) { return exit = true; } }); return exit; - }; + } /** * returns true if any point on this polygon * is within radius of a point on the target */ - PRO.isNear = function(poly, radius, cache) { - const midcheck = CONF.precision_midpoint_check_dist; - const dist = radius || CONF.precision_close_to_poly_sq; + isNear(poly, radius, cache) { + const midcheck = config.precision_midpoint_check_dist; + const dist = radius || config.precision_close_to_poly_sq; let near = false; let mem = cache ? this.cacheNear = this.cacheNear || {} : undefined; @@ -1194,7 +1180,7 @@ } return near; - }; + } /** * TODO replace isNested() with isInside() ? @@ -1203,15 +1189,15 @@ * @param {number} [tolerance] * @returns {boolean} all points inside OR on edge */ - PRO.isInside = function(poly, tolerance) { + isInside(poly, tolerance) { // throw new Error("isInside"); - const neardist = tolerance || CONF.precision_close_to_poly_sq; + const neardist = tolerance || config.precision_close_to_poly_sq; if (!this.bounds.isNested(poly.bounds, neardist * 3)) { return false; } let mid, - midcheck = CONF.precision_midpoint_check_dist, + midcheck = config.precision_midpoint_check_dist, exit = true; this.forEachSegment(function(prev, next) { @@ -1230,7 +1216,7 @@ }, this.open); return exit; - }; + } /** * @param {Polygon} poly @@ -1241,24 +1227,15 @@ // return (poly && poly.isInside(this, tolerance) && poly.isOutsideAll(this.inner, tolerance)); // }; - /** - * - * @param polys - * @returns {boolean} - */ - PRO.containedBySet = function(polys) { + containedBySet(polys) { if (!polys) return false; - for (let i=0; i 0; - }; + } /** * remove all inner polygons - * @returns {Polygon} self */ - PRO.clearInner = function() { + clearInner() { this.inner = null; return this; - }; + } - PRO.newUndeleted = function() { + newUndeleted() { let 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()); - }; + circularity() { + return (4 * Math.PI * this.area()) / util.sqr(this.perimeter()); + } - PRO.circularityDeep = function() { - return (4 * PI * this.areaDeep()) / UTIL.sqr(this.perimeter()); - }; + circularityDeep() { + return (4 * Math.PI * this.areaDeep()) / util.sqr(this.perimeter()); + } /** * @returns {number} perimeter length (sum of all segment lengths) */ - PRO.perimeter = function() { + perimeter() { if (this.perim) { return this.perim; } let len = 0.0; - this.forEachSegment(function(prev,next) { - len += SQRT(prev.distToSq2D(next)); + this.forEachSegment(function(prev, next) { + len += Math.sqrt(prev.distToSq2D(next)); }, this.open); return this.perim = len; - }; + } - PRO.perimeterDeep = function() { + perimeterDeep() { let len = this.perimeter(); - if (this.inner) this.inner.forEach(function(p) { len += p.perimeter() }); + if (this.inner) this.inner.forEach(function(p) { + len += p.perimeter() + }); return len; - }; + } /** * calculate and return the area enclosed by the polygon. @@ -1342,20 +1320,20 @@ * @param {boolean} [raw] * @returns {number} area */ - PRO.area = function(raw) { + area(raw) { if (this.length < 3) { return 0; } if (this.area2 === undefined) { this.area2 = 0.0; - for (let p=this.points,pl=p.length,pi=0,p1,p2; pi 0 means first point didn't match if (mi) { let nupoints = []; - for (let i=mi; i 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(), @@ -1611,7 +1579,7 @@ clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - poly = POLY().fromClipperTree(ctre, poly.getZ()); + poly = POLY.fromClipperTree(ctre, poly.getZ()); poly.forEach(function(p) { p.fillang = fillang; }) @@ -1619,18 +1587,14 @@ } else { return null; } - }; + } /** * @param {Polygon} poly clipping mask * @returns {?Polygon[]} */ - PRO.mask = function(poly, nullOnEquiv) { + mask(poly, nullOnEquiv) { let 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(), @@ -1640,7 +1604,7 @@ clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - poly = POLY().fromClipperTree(ctre, poly.getZ()); + poly = POLY.fromClipperTree(ctre, poly.getZ()); poly.forEach(function(p) { p.fillang = fillang; }) @@ -1651,9 +1615,9 @@ } else { return null; } - }; + } - PRO.cut = function(polys, inter) { + cut(polys, inter) { let target = this; if (!target.open) { @@ -1667,21 +1631,17 @@ } } - let clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), + let clip = new clib.Clipper(), ctre = new clib.PolyTree(), type = inter ? ctyp.ctIntersection : ctyp.ctDifference, sp1 = target.toClipper(), - sp2 = POLY().toClipper(polys); + sp2 = POLY.toClipper(polys); clip.AddPaths(sp1, ptyp.ptSubject, false); clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(type, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - let cuts = POLY().fromClipperTree(ctre, target.getZ(), null, null, 0); + let cuts = POLY.fromClipperTree(ctre, target.getZ(), null, null, 0); cuts.forEach(no => { // heal open but really closed polygons because cutting // has to open the poly to perform the cut. but the result @@ -1696,64 +1656,61 @@ } else { return null; } - }; + } - PRO.intersect = function(poly, min) { + intersect(poly, min) { if (!this.overlaps(poly)) return null; - let clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), + let clip = new clib.Clipper(), ctre = new clib.PolyTree(), sp1 = this.toClipper(), sp2 = poly.toClipper(), minarea = min >= 0 ? min : 0.1; if (this.isInside(poly)) { - return [ this ]; + return [this]; } clip.AddPaths(sp1, ptyp.ptSubject, true); clip.AddPaths(sp2, ptyp.ptClip, true); if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftNonZero)) { - let inter = POLY() + let inter = POLY .fromClipperTreeUnion(ctre, poly.getZ(), minarea) // .filter(p => p.isEquivalent(this) || p.isInside(this)) - .filter(p => p.isInside(this)) - ; + .filter(p => p.isInside(this)); return inter; } return null; } - PRO.areaDiff = function(poly) { + areaDiff(poly) { let a1 = this.area(), a2 = poly.area(); return (a1 > a2) ? a2 / a1 : a1 / a2; - }; + } // does not work with nested polys - PRO.simplify = function(opt = {}) { + simplify(opt = {}) { let z = this.getZ(); // use expand / deflate technique instead if (opt.pump) { - let p2 = POLY().offset([ this ], opt.pump, { z }); + let p2 = POLY.offset([this], opt.pump, { + z + }); if (p2) { - p2 = POLY().offset(p2, -opt.pump, { z }); + p2 = POLY.offset(p2, -opt.pump, { + z + }); return p2; } return null; } - let clib = self.ClipperLib, - cfil = clib.PolyFillType, - clip = this.toClipper(), + let clip = this.toClipper(), res = clib.Clipper.SimplifyPolygons(clip, cfil.pftNonZero); if (!(res && res.length)) { @@ -1763,15 +1720,15 @@ return res.map(array => { let poly = newPolygon(); for (let pt of array) { - poly.push(BASE.pointFromClipper(pt, z)); + poly.push(base.pointFromClipper(pt, z)); } return poly; }); - }; + } - PRO.unionMatch = function(polys) { + unionMatch(polys) { return polys.filter(poly => poly.isEquivalent(this)).length; - }; + } /** * return logical OR of two polygons' enclosed areas @@ -1779,56 +1736,77 @@ * @param {Polygon} poly * @returns {?Polygon} intersected polygon, null if no intersection, or all when indicated */ - PRO.union = function(poly, min, all) { + union(poly, min, all) { if (!this.overlaps(poly)) return null; - PRO.union = function(poly, min, all) { - if (!this.overlaps(poly)) return null; + let fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + sp1 = this.toClipper(), + sp2 = poly.toClipper(), + minarea = min >= 0 ? min : 0.1; - let 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(), - minarea = min >= 0 ? min : 0.1; + clip.AddPaths(sp1, ptyp.ptSubject, true); + clip.AddPaths(sp2, ptyp.ptClip, true); - clip.AddPaths(sp1, ptyp.ptSubject, true); - clip.AddPaths(sp2, ptyp.ptClip, true); - - if (clip.Execute(ctyp.ctUnion, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - let union = POLY().fromClipperTreeUnion(ctre, poly.getZ(), minarea); - if (all) { - if (union.length === 2) { - return null; - // if (this.unionMatch(union) || poly.unionMatch(union)) { - // return null; - // } - } - return union; - } - if (union.length === 1) { - union = union[0]; - union.fillang = fillang; - return union; - } else { - console.trace({check_union_call_path: union, this: this, poly}); + if (clip.Execute(ctyp.ctUnion, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { + let union = POLY.fromClipperTreeUnion(ctre, poly.getZ(), minarea); + if (all) { + if (union.length === 2) { + return null; + // if (this.unionMatch(union) || poly.unionMatch(union)) { + // return null; + // } } + return union; } + if (union.length === 1) { + union = union[0]; + union.fillang = fillang; + return union; + } else { + console.trace({ + check_union_call_path: union, + this: this, + poly + }); + } + } - return null; - }; - }; - - /** ****************************************************************** - * Connect to base and Helpers - ******************************************************************* */ - - function newPolygon(points) { - return new Polygon(points); + return null; } +} + +// use Slope.angleDiff() then re-test path mitering / rendering +function slopeDiff(s1, s2) { + const n1 = s1.angle; + const n2 = s2.angle; + let diff = n2 - n1; + while (diff < -180) diff += 360; + while (diff > 180) diff -= 360; + return Math.abs(diff); +} + +function fromClipperPath(path, z) { + let poly = newPolygon(), + i = 0, + l = path.length; + while (i < l) { + // poly.push(newPoint(null,null,z,null,path[i++])); + poly.push(base.pointFromClipper(path[i++], z)); + } + return poly; +} + +function newPolygon(points) { + return new Polygon(points); +} + +base.Polygon = Polygon; +base.newPolygon = newPolygon; + +Polygon.fromArray = function(array) { + return newPolygon().fromArray(array); +}; })(); diff --git a/src/geo/polygons.js b/src/geo/polygons.js index cb864e66..d958ecdc 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -4,971 +4,954 @@ (function() { - if (!self.base) self.base = {}; - if (self.base.polygons) return; +const base = self.base; +if (base.polygons) return; - const BASE = self.base, - UTIL = BASE.util, - CONF = BASE.config, - DEG2RAD = Math.PI / 180, - ABS = Math.abs, - SQRT = Math.sqrt, - SQR = UTIL.sqr, - NOKEY = BASE.key.NONE, - newPoint = BASE.newPoint, - numOrDefault = UTIL.numOrDefault; +const { util, config, newPoint } = base; +const { sqr, numOrDefault } = util; - const POLYS = BASE.polygons = { - rayIntersect, - alignWindings, - setWinding, - fillArea, - subtract, - flatten, - offset, - trimTo, - expand, - expand_lines, - points, - route, - union, - inset, - nest, - diff, - setZ, - filter, - toClipper, - fromClipperNode, - fromClipperTree, - fromClipperTreeUnion, - cleanClipperTree, - fingerprintCompare, - fingerprint - }; +const DEG2RAD = Math.PI / 180, + SQRT = Math.sqrt, + SQR = util.sqr, + ABS = Math.abs; - /** ****************************************************************** - * Polygon array utility functions - ******************************************************************* */ +const clib = self.ClipperLib, + clip = clib.Clipper, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType; - function setZ(polys, z) { - for (let poly of polys) { - poly.setZ(z); +const POLYS = base.polygons = { + rayIntersect, + alignWindings, + setWinding, + fillArea, + subtract, + flatten, + offset, + trimTo, + expand, + expand_lines, + points, + route, + union, + inset, + nest, + diff, + setZ, + filter, + toClipper, + fromClipperNode, + fromClipperTree, + fromClipperTreeUnion, + cleanClipperTree, + fingerprintCompare, + fingerprint +}; + +function setZ(polys, z) { + for (let poly of polys) { + poly.setZ(z); + } + return polys; +} + +function toClipper(polys = []) { + let out = []; + for (let poly of polys) { + poly.toClipper(out); + } + return out; +} + +function fromClipperNode(tnode, z) { + let poly = base.newPolygon(); + for (let point of tnode.m_polygon) { + poly.push(base.pointFromClipper(point, z)); + } + poly.open = tnode.IsOpen; + return poly; +}; + +function fromClipperTree(tnode, z, tops, parent, minarea) { + let poly, + polys = tops || [], + min = numOrDefault(minarea, 0.1); + + for (let child of tnode.m_Childs) { + poly = fromClipperNode(child, z); + // throw out all tiny polygons + if (!poly.open && poly.area() < min) { + continue; + } + if (parent) { + parent.addInner(poly); + } else { + polys.push(poly); + } + if (child.m_Childs) { + fromClipperTree(child, z, polys, parent ? null : poly, minarea); } - return polys; } - function toClipper(polys = []) { - let out = []; - for (let poly of polys) { - poly.toClipper(out); + return polys; +}; + +function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { + let polys = tops || [], poly; + + for (let child of tnode.m_Childs) { + poly = fromClipperNode(child, z); + if (!poly.open && minarea && poly.area() < minarea) { + continue; + } + if (parent) { + parent.addInner(poly); + } else { + polys.push(poly); + } + if (child.m_Childs) { + fromClipperTreeUnion(child, z, minarea, polys, parent ? null : poly); } - return out; } - function fromClipperNode(tnode, z) { - let poly = BASE.newPolygon(); - for (let point of tnode.m_polygon) { - poly.push(BASE.pointFromClipper(point, z)); + return polys; +}; + +function cleanClipperTree(tree) { + if (tree.m_Childs) + for (let child of tree.m_Childs) { + child.m_polygon = clip.CleanPolygon(child.m_polygon, config.clipperClean); + cleanClipperTree(child.m_Childs); + } + + return tree; +}; + +function filter(array, output, fn) { + for (let poly of array) { + poly = fn(poly); + if (poly) { + if (Array.isArray(poly)) { + output.appendAll(poly); + } else { + output.push(poly); + } } - poly.open = tnode.IsOpen; - return poly; - }; + } + return output; +} - function fromClipperTree(tnode, z, tops, parent, minarea) { - let poly, - polys = tops || [], - min = numOrDefault(minarea, 0.1); +function points(polys) { + return polys.length ? polys.map(p => p.deepLength).reduce((a,v) => a+v) : 0; +} - for (let child of tnode.m_Childs) { - poly = fromClipperNode(child, z); - // throw out all tiny polygons - if (!poly.open && poly.area() < min) { +/** + * todo use clipper polytree? + * + * use bounding boxes and sliceIntersection + * to determine parent/child nesting. returns a + * array of trees. + * + * @param {Polygon[]} polygon soup + * @param {boolean} deep allow nesting beyond 2 levels + * @param {boolean} opentop prevent open polygons from having inners + * @returns {Polygon[]} top level parent polygons + */ +function nest(polygons, deep, opentop) { + if (!polygons) { + return polygons; + } + // sort groups by size + polygons.sort(function (a, b) { + return a.area() - b.area(); + }); + let i, poly; + // clear parent/child links if they exist + for (i = 0; i < polygons.length; i++) { + poly = polygons[i]; + poly.parent = null; + poly.inner = null; + } + // nest groups if fully contained by a parent + for (i = 0; i < polygons.length - 1; i++) { + poly = polygons[i]; + // find the smallest suitable parent + for (let j = i + 1; j < polygons.length; j++) { + let parent = polygons[j]; + // prevent open polys from having inners + if (opentop && parent.isOpen()) { continue; } - if (parent) { + if (poly.isNested(parent)) { parent.addInner(poly); + break; + } + } + } + // tops have an even # depth + let tops = [], + p; + // assign a depth level to each group + for (i = 0; i < polygons.length; i++) { + p = polygons[i]; + poly = p; + poly.depth = 0; + while (p.parent) { + poly.depth++; + p = p.parent; + } + if (deep) { + if (poly.depth === 0) tops.push(poly); + } else { + if (poly.depth % 2 === 0) { + tops.push(poly); } else { - polys.push(poly); + poly.inner = null; } - if (child.m_Childs) { - fromClipperTree(child, z, polys, parent ? null : poly, minarea); - } - } - - return polys; - }; - - function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { - let polys = tops || [], poly; - - for (let child of tnode.m_Childs) { - poly = fromClipperNode(child, z); - if (!poly.open && minarea && poly.area() < minarea) { - continue; - } - if (parent) { - parent.addInner(poly); - } else { - polys.push(poly); - } - if (child.m_Childs) { - fromClipperTreeUnion(child, z, minarea, polys, parent ? null : poly); - } - } - - return polys; - }; - - function cleanClipperTree(tree) { - let clib = self.ClipperLib, - clip = clib.Clipper; - - if (tree.m_Childs) - for (let child of tree.m_Childs) { - child.m_polygon = clip.CleanPolygon(child.m_polygon, CONF.clipperClean); - cleanClipperTree(child.m_Childs); - } - - return tree; - }; - - function filter(array, output, fn) { - for (let poly of array) { - poly = fn(poly); - if (poly) { - if (Array.isArray(poly)) { - output.appendAll(poly); - } else { - output.push(poly); - } - } - } - return output; - } - - function points(polys) { - return polys.length ? polys.map(p => p.deepLength).reduce((a,v) => a+v) : 0; - } - - /** - * todo use clipper polytree? - * - * use bounding boxes and sliceIntersection - * to determine parent/child nesting. returns a - * array of trees. - * - * @param {Polygon[]} polygon soup - * @param {boolean} deep allow nesting beyond 2 levels - * @param {boolean} opentop prevent open polygons from having inners - * @returns {Polygon[]} top level parent polygons - */ - function nest(polygons, deep, opentop) { - if (!polygons) { - return polygons; - } - // sort groups by size - polygons.sort(function (a, b) { - return a.area() - b.area(); - }); - let i, poly; - // clear parent/child links if they exist - for (i = 0; i < polygons.length; i++) { - poly = polygons[i]; - poly.parent = null; - poly.inner = null; - } - // nest groups if fully contained by a parent - for (i = 0; i < polygons.length - 1; i++) { - poly = polygons[i]; - // find the smallest suitable parent - for (let j = i + 1; j < polygons.length; j++) { - let parent = polygons[j]; - // prevent open polys from having inners - if (opentop && parent.isOpen()) { - continue; - } - if (poly.isNested(parent)) { - parent.addInner(poly); - break; - } - } - } - // tops have an even # depth - let tops = [], - p; - // assign a depth level to each group - for (i = 0; i < polygons.length; i++) { - p = polygons[i]; - poly = p; - poly.depth = 0; - while (p.parent) { - poly.depth++; - p = p.parent; - } - if (deep) { - if (poly.depth === 0) tops.push(poly); - } else { - if (poly.depth % 2 === 0) { - tops.push(poly); - } else { - poly.inner = null; - } - } - } - return tops; - } - - /** - * sets windings for parents one way - * and children in opposition - * - * @param {Polygon[]} array - * @param {boolean} CW - * @param {boolean} [recurse] - */ - function setWinding(array, CW, recurse) { - if (!array) return; - let poly, i = 0; - while (i < array.length) { - poly = array[i++]; - if (poly.isClockwise() !== CW) poly.reverse(); - if (recurse && poly.inner) setWinding(poly.inner, !CW, false); } } + return tops; +} - /** - * ensure all polygons have the same winding direction. - * try to use reversals that touch the fewest nodes. - * - * @param {Polygon[]} polys - * @return {boolean} true if aligned clockwise - */ - function alignWindings(polys) { - let len = polys.length, - fwd = 0, - pts = 0, - i = 0, - setCW, - poly; - while (i < len) { - poly = polys[i++]; - pts += poly.length; - if (poly.isClockwise()) fwd += poly.length; - } - i = 0; - setCW = fwd > (pts/2); - while (i < len) { - poly = polys[i++]; - if (poly.isClockwise() != setCW) poly.reverse(); - } - return setCW; +/** + * sets windings for parents one way + * and children in opposition + * + * @param {Polygon[]} array + * @param {boolean} CW + * @param {boolean} [recurse] + */ +function setWinding(array, CW, recurse) { + if (!array) return; + let poly, i = 0; + while (i < array.length) { + poly = array[i++]; + if (poly.isClockwise() !== CW) poly.reverse(); + if (recurse && poly.inner) setWinding(poly.inner, !CW, false); } +} - function setContains(setA, poly) { - for (let i=0; i (pts/2); + while (i < len) { + poly = polys[i++]; + if (poly.isClockwise() != setCW) poly.reverse(); + } + return setCW; +} - function flatten(polys, to, crush) { - to = to || []; - polys.forEach(function(poly) { - poly.flattenTo(to); - if (crush) poly.inner = null; +function setContains(setA, poly) { + for (let i=0; i= min) { + to.push(poly); + out.push(poly); + } }); return to; } - /** - * Diff two sets of polygons and return A-B, B-A. - * no polygons in a given set can overlap ... only between sets - * - * @param {Polygon[]} setA - * @param {Polygon[]} setB - * @param {Polygon[]} outA - * @param {Polygon[]} outB - * @param {number} [z] - * @param {number} [minArea] - * @returns {Polygon[]} out - */ - function subtract(setA, setB, outA, outB, z, minArea, opt = {}) { - let min = minArea || 0.1, - out = []; - - function filter(from, to = []) { - from.forEach(function(poly) { - if (poly.area() >= min) { - to.push(poly); - out.push(poly); - } - }); - return to; + if (opt.prof) { + if (setA.length === 0 || setB.length === 0) { + console.log('sub_zero', {setA, setB}); } - - if (opt.prof) { - if (setA.length === 0 || setB.length === 0) { - console.log('sub_zero', {setA, setB}); - } - opt.prof.pin = (opt.prof.pin || 0) + points(setA) + points(setB); - opt.prof.call = (opt.prof.call || 0) + 1; - } - - // wasm diff currently doesn't seem to be any faster - if (false && opt.wasm && geo.wasm) { - let oA = outA ? [] : undefined; - let oB = outB ? [] : undefined; - geo.wasm.js.diff(setA, setB, z, oA, oB); - if (oA) { - outA.appendAll(filter(oA)); - } - if (oB) { - outB.appendAll(filter(oB)); - } - } else { - let clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), - ctre = new clib.PolyTree(), - sp1 = toClipper(setA), - sp2 = toClipper(setB); - - // more expensive? worth it? - clip.StrictlySimple = true; - if (outA) { - clip.AddPaths(sp1, ptyp.ptSubject, true); - clip.AddPaths(sp2, ptyp.ptClip, true); - if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - cleanClipperTree(ctre); - filter(fromClipperTree(ctre, z, null, null, min), outA); - } - } - if (outB) { - if (outA) { - ctre.Clear(); - clip.Clear(); - } - clip.AddPaths(sp2, ptyp.ptSubject, true); - clip.AddPaths(sp1, ptyp.ptClip, true); - if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { - cleanClipperTree(ctre); - filter(fromClipperTree(ctre, z, null, null, min), outB); - } - } - } - - if (opt.prof) { - opt.prof.pout = (opt.prof.pout || 0) + points(out); - } - - return out; + opt.prof.pin = (opt.prof.pin || 0) + points(setA) + points(setB); + opt.prof.call = (opt.prof.call || 0) + 1; } - /** - * Slice.doProjectedFills() - * Print.init w/ brims - * - * clipper is natively less efficient at merging many polygons. this iterative - * approach skips attempting to merge polys lacking overlapping bounding boxes - * and can quickly check if the attempt to union two polys outputs the same - * two input polys. the latter bit is the key to greater speed. - * - * @param {Polygon[]} polys - * @returns {Polygon[]} - */ - function union(polys, minarea, all, opt = {}) { - if (polys.length < 2) return polys; - - if (opt.wasm && geo.wasm) { - let min = minarea || 0.01; - // let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v); - // if (deepLength < 15000) - try { - return geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min); - } catch (e) { - console.log({union_fail: polys, minarea, all}); - } - } - - let out = polys.slice(), i, j, union, uset = []; - - outer: for (i=0; i p.deepLength).reduce((a,v) => a+v); + // if (deepLength < 15000) + try { + return geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min); + } catch (e) { + console.log({union_fail: polys, minarea, all}); + } + } + + let out = polys.slice(), i, j, union, uset = []; + + outer: for (i=0; i !p.open); + + // cause inner / outer polys to be reversed from each other + alignWindings(polys); + for (let poly of polys) { + if (poly.inner) { + setWinding(poly.inner, !poly.isClockwise()); + } + } + + let orig = polys, + count = numOrDefault(opts.count, 1), + depth = numOrDefault(opts.depth, 0), + clean = opts.clean !== false, + simple = opts.simple !== false, + fill = numOrDefault(opts.fill, clib.PolyFillType.pftNonZero), + join = numOrDefault(opts.join, clib.JoinType.jtMiter), + type = numOrDefault(opts.type, clib.EndType.etClosedPolygon), + // if dist is array with values, shift out next offset + offs = Array.isArray(dist) ? (dist.length > 1 ? dist.shift() : dist[0]) : dist, + mina = numOrDefault(opts.minArea, 0.1), + zed = opts.z || 0; + + if (opts.wasm && geo.wasm) { + try { + polys = geo.wasm.js.offset(polys, offs, zed, clean ? config.clipperClean : 0, simple ? 1 : 0); + } catch (e) { + console.log('wasm error', e.message || e); + opts.wasm = false; + return offset(polys, dist, opts); + } + } else { + let coff = new clib.ClipperOffset(opts.miter, opts.arc), ctre = new clib.PolyTree(); - coff.AddPaths(poly.toClipper(), cjnt.jtMiter, cety.etOpenSquare); - coff.Execute(ctre, distance * fact); - - return fromClipperTree(ctre, z, null, null, 0); - } - - /** - * @param {Polygon[]} polys - * @param {number} distance offset - * @param {number} [z] defaults to 0 - * @param {Polygon[]} [out] optional collector - * @param {number} [count] offset passes (0 == until no space left) - * @param {number} [distance2] after first offset pass - * @param {Function} [collector] receives output of each pass - * @returns {Polygon[]} last offset - */ - function expand(polys, distance, z, out, count, distance2, collector, min) { - return offset(polys, [distance, distance2 || distance], { - z, outs: out, call: collector, minArea: min, count, flat: true - }); - } - - /** - * offset an array of polygons by distance with options to recurse - * and return resulting gaps from offsets for thin wall detection in - * in FDM mode and uncleared areas in CAM mode. - */ - function offset(polys, dist, opts = {}) { - // do not offset open lines - polys = polys.filter(p => !p.open); - - // cause inner / outer polys to be reversed from each other - alignWindings(polys); + // setup offset for (let poly of polys) { - if (poly.inner) { - setWinding(poly.inner, !poly.isClockwise()); - } + // convert to clipper format + poly = poly.toClipper(); + if (clean) poly = clib.Clipper.CleanPolygons(poly, config.clipperClean); + if (simple) poly = clib.Clipper.SimplifyPolygons(poly, fill); + coff.AddPaths(poly, join, type); } - - let orig = polys, - count = numOrDefault(opts.count, 1), - depth = numOrDefault(opts.depth, 0), - clean = opts.clean !== false, - simple = opts.simple !== false, - fill = numOrDefault(opts.fill, ClipperLib.PolyFillType.pftNonZero), - join = numOrDefault(opts.join, ClipperLib.JoinType.jtMiter), - type = numOrDefault(opts.type, ClipperLib.EndType.etClosedPolygon), - // if dist is array with values, shift out next offset - offs = Array.isArray(dist) ? (dist.length > 1 ? dist.shift() : dist[0]) : dist, - mina = numOrDefault(opts.minArea, 0.1), - zed = opts.z || 0; - - if (opts.wasm && geo.wasm) { - try { - polys = geo.wasm.js.offset(polys, offs, zed, clean ? CONF.clipperClean : 0, simple ? 1 : 0); - } catch (e) { - console.log('wasm error', e.message || e); - opts.wasm = false; - return offset(polys, dist, opts); - } - } else { - let coff = new ClipperLib.ClipperOffset(opts.miter, opts.arc), - ctre = new ClipperLib.PolyTree(); - - // setup offset - for (let poly of polys) { - // convert to clipper format - poly = poly.toClipper(); - if (clean) poly = ClipperLib.Clipper.CleanPolygons(poly, CONF.clipperClean); - if (simple) poly = ClipperLib.Clipper.SimplifyPolygons(poly, fill); - coff.AddPaths(poly, join, type); - } - // perform offset - coff.Execute(ctre, offs * CONF.clipper); - // convert back from clipper output format - polys = fromClipperTree(ctre, zed, null, null, mina); - } - - - // if specified, perform offset gap analysis - if (opts.gaps && polys.length) { - let oneg = offset(polys, -offs, { - fill: opts.fill, join: opts.join, type: opts.type, z: opts.z, minArea: mina - }); - let suba = []; - let diff = subtract(orig, oneg, suba, null, zed); - opts.gaps.append(suba, opts.flat); - } - - // if offset fails, consider last polygons as gap areas - if (opts.gaps && !polys.length) { - opts.gaps.append(orig, opts.flat); - } - - // if specified, perform up to *count* successive offsets - if (polys.length) { - // ensure opts has offset accumulator array - opts.outs = opts.outs || []; - // store polys in accumulator - opts.outs.append(polys, opts.flat); - // callback for expand() compatibility - if (opts.call) { - opts.call(polys, count, depth); - } - // check for more offsets - if (count > 1) { - // decrement count, increment depth - opts.count = count - 1; - opts.depth = depth + 1; - // call next offset - offset(polys, dist, opts); - } - } - - return opts.flat ? opts.outs : polys; + // perform offset + coff.Execute(ctre, offs * config.clipper); + // convert back from clipper output format + polys = fromClipperTree(ctre, zed, null, null, mina); } - /** - * progressive insetting that does inset + outset to debur as well - * as performing subtractive analysis between initial layer shell (ref) - * and last offset (cmp) to produce gap candidates (for thinfill) - */ - function inset(polys, dist, count, z, wasm) { - let total = count; - let layers = []; - let ref = polys; - let depth = 0; - while (count-- > 0 && ref && ref.length) { - let off = offset(ref, -dist, {z, wasm}); - let mid = offset(off, dist / 2, {z, wasm}); - let cmp = offset(off, dist, {z, wasm}); - let gap = []; - let aref = ref.map(p => p.areaDeep()).reduce((a,p) => a +p); - let cref = cmp.length ? cmp.map(p => p.areaDeep()).reduce((a,p) => a + p) : 0; - // threshold subtraction to area deltas > 0.1 % to filter out false - // positives where inset/outset are identical floating point error - if (Math.abs(aref - cref) > 1 - (Math.abs(aref / cref) / 1000)) { - subtract(ref, cmp, gap, null, z); - } - layers.push({idx: total-count, off, mid, gap}); - // fixup depth cues - for (let m of mid) { - m.depth = depth++; - if (m.inner) { - for (let mi of m.inner) { - mi.depth = m.depth; - } - } - } - ref = off; - } - return layers; + + // if specified, perform offset gap analysis + if (opts.gaps && polys.length) { + let oneg = offset(polys, -offs, { + fill: opts.fill, join: opts.join, type: opts.type, z: opts.z, minArea: mina + }); + let suba = []; + let diff = subtract(orig, oneg, suba, null, zed); + opts.gaps.append(suba, opts.flat); } - /** - * todo use clipper opne poly clipping? - * - * @param {Polygon[]} polys - * @param {number} angle (-90 to 90) - * @param {number} spacing - * @param {Polygon[]} [output] - * @param {number} [minLen] - * @param {number} [maxLen] - * @returns {Point[]} supplied output or new array - */ - function fillArea(polys, angle, spacing, output, minLen, maxLen) { - if (polys.length === 0) return; + // if offset fails, consider last polygons as gap areas + if (opts.gaps && !polys.length) { + opts.gaps.append(orig, opts.flat); + } - let i = 1, - p0 = polys[0], - zpos = p0.getZ(), - bounds = p0.bounds.clone(), - raySlope; - - // ensure angle is in the -90:90 range - angle = angle % 180; - while (angle < -90) angle += 180; - while (angle > 90) angle -= 180; - - // X,Y ray slope derived from angle - raySlope = BASE.newSlope(0,0, - Math.cos(angle * DEG2RAD) * spacing, - Math.sin(angle * DEG2RAD) * spacing - ); - - // compute union of top boundaries - while (i < polys.length) { - bounds.merge(polys[i++].bounds); + // if specified, perform up to *count* successive offsets + if (polys.length) { + // ensure opts has offset accumulator array + opts.outs = opts.outs || []; + // store polys in accumulator + opts.outs.append(polys, opts.flat); + // callback for expand() compatibility + if (opts.call) { + opts.call(polys, count, depth); } + // check for more offsets + if (count > 1) { + // decrement count, increment depth + opts.count = count - 1; + opts.depth = depth + 1; + // call next offset + offset(polys, dist, opts); + } + } - // ray stepping is an axis from the line perpendicular to the ray - let rayint = output || [], - stepX = -raySlope.dy, - stepY = raySlope.dx, - iterX = ABS(ABS(stepX) > 0 ? bounds.width() / stepX : 0), - iterY = ABS(ABS(stepY) > 0 ? bounds.height() / stepY : 0), - dist = SQRT(SQR(iterX * stepX) + SQR(iterY * stepY)), - step = SQRT(SQR(stepX) + SQR(stepY)), - steps = dist / step, - start = angle < 0 ? { x:bounds.minx, y:bounds.miny, z:zpos } : { x:bounds.maxx, y:bounds.miny, z:zpos }, - clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), - ctre = new clib.PolyTree(), - minlen = BASE.config.clipper * (minLen || 0), - maxlen = BASE.config.clipper * (maxLen || 0), - lines = []; + return opts.flat ? opts.outs : polys; +} - // store origin as start/affinity point for fill - rayint.origin = newPoint(start.x, start.y, start.z); - - for (i = 0; i < steps; i++) { - lines.push([ - { - X: (start.x - raySlope.dx * 1000) * CONF.clipper, - Y: (start.y - raySlope.dy * 1000) * CONF.clipper - },{ - X: (start.x + raySlope.dx * 1000) * CONF.clipper, - Y: (start.y + raySlope.dy * 1000) * CONF.clipper +/** + * progressive insetting that does inset + outset to debur as well + * as performing subtractive analysis between initial layer shell (ref) + * and last offset (cmp) to produce gap candidates (for thinfill) + */ +function inset(polys, dist, count, z, wasm) { + let total = count; + let layers = []; + let ref = polys; + let depth = 0; + while (count-- > 0 && ref && ref.length) { + let off = offset(ref, -dist, {z, wasm}); + let mid = offset(off, dist / 2, {z, wasm}); + let cmp = offset(off, dist, {z, wasm}); + let gap = []; + let aref = ref.map(p => p.areaDeep()).reduce((a,p) => a +p); + let cref = cmp.length ? cmp.map(p => p.areaDeep()).reduce((a,p) => a + p) : 0; + // threshold subtraction to area deltas > 0.1 % to filter out false + // positives where inset/outset are identical floating point error + if (Math.abs(aref - cref) > 1 - (Math.abs(aref / cref) / 1000)) { + subtract(ref, cmp, gap, null, z); + } + layers.push({idx: total-count, off, mid, gap}); + // fixup depth cues + for (let m of mid) { + m.depth = depth++; + if (m.inner) { + for (let mi of m.inner) { + mi.depth = m.depth; } - ]); - start.x += stepX; - start.y += stepY; + } } + ref = off; + } + return layers; +} - clip.AddPaths(lines, ptyp.ptSubject, false); - clip.AddPaths(toClipper(polys), ptyp.ptClip, true); +/** + * todo use clipper opne poly clipping? + * + * @param {Polygon[]} polys + * @param {number} angle (-90 to 90) + * @param {number} spacing + * @param {Polygon[]} [output] + * @param {number} [minLen] + * @param {number} [maxLen] + * @returns {Point[]} supplied output or new array + */ +function fillArea(polys, angle, spacing, output, minLen, maxLen) { + if (polys.length === 0) return; + let i = 1, + p0 = polys[0], + zpos = p0.getZ(), + bounds = p0.bounds.clone(), + raySlope; + + // ensure angle is in the -90:90 range + angle = angle % 180; + while (angle < -90) angle += 180; + while (angle > 90) angle -= 180; + + // X,Y ray slope derived from angle + raySlope = base.newSlope(0,0, + Math.cos(angle * DEG2RAD) * spacing, + Math.sin(angle * DEG2RAD) * spacing + ); + + // compute union of top boundaries + while (i < polys.length) { + bounds.merge(polys[i++].bounds); + } + + // ray stepping is an axis from the line perpendicular to the ray + let rayint = output || [], + stepX = -raySlope.dy, + stepY = raySlope.dx, + iterX = ABS(ABS(stepX) > 0 ? bounds.width() / stepX : 0), + iterY = ABS(ABS(stepY) > 0 ? bounds.height() / stepY : 0), + dist = SQRT(SQR(iterX * stepX) + SQR(iterY * stepY)), + step = SQRT(SQR(stepX) + SQR(stepY)), + steps = dist / step, + start = angle < 0 ? { x:bounds.minx, y:bounds.miny, z:zpos } : { x:bounds.maxx, y:bounds.miny, z:zpos }, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + minlen = base.config.clipper * (minLen || 0), + maxlen = base.config.clipper * (maxLen || 0), lines = []; - if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { - for (let poly of ctre.m_AllPolys) { - if (minlen || maxlen) { - let plen = clib.JS.PerimeterOfPath(poly.m_polygon, false, 1); - if (minlen && plen < minlen) continue; - if (maxlen && plen > maxlen) continue; - } - let p1 = BASE.pointFromClipper(poly.m_polygon[0], zpos); - let p2 = BASE.pointFromClipper(poly.m_polygon[1], zpos); - let od = rayint.origin.distToLineNew(p1,p2) / spacing; - lines.push([p1, p2, od]); + // store origin as start/affinity point for fill + rayint.origin = newPoint(start.x, start.y, start.z); + + for (i = 0; i < steps; i++) { + lines.push([ + { + X: (start.x - raySlope.dx * 1000) * config.clipper, + Y: (start.y - raySlope.dy * 1000) * config.clipper + },{ + X: (start.x + raySlope.dx * 1000) * config.clipper, + Y: (start.y + raySlope.dy * 1000) * config.clipper } - } - - lines.sort(function(a,b) { - return a[2] - b[2]; - }) - - for (let line of lines) { - let dist = Math.round(line[2]); - line[0].index = dist; - line[1].index = dist; - rayint.push(line[0]); - rayint.push(line[1]); - } - - return rayint; + ]); + start.x += stepX; + start.y += stepY; } - /** - * tracing a ray through a slice's polygons, find and return - * a sorted list of all intersecting points. - * - * @param {Point} start - * @param {Slope} slope - * @param {Polygon[]} polygons - * @param {boolean} [for_fill] - * @returns {Point[]} - */ - function rayIntersect(start, slope, polygons, for_fill) { - let i = 0, - flat = [], - points = [], - conf = BASE.config, - merge_dist = for_fill ? conf.precision_fill_merge : conf.precision_merge; - // todo use new flatten() function above - polygons.forEach(function(p) { - p.flattenTo(flat); - }); - polygons = flat; - while (i < polygons.length) { - let polygon = polygons[i++], - pp = polygon.points, - pl = pp.length;; - for (let j = 0; j < pl; j++) { - let j2 = (j + 1) % pl, - ip = UTIL.intersectRayLine(start, slope, pp[j], pp[j2]); - if (ip) { - // add group object to point for cull detection - ip.group = polygon; - // add point to point list - points.push(ip); - // if point is near a group endpoint, add position marker for culling - if (ip.isNear(pp[j], merge_dist)) { - ip.pos = j; - ip.mod = pl; - } else if (ip.isNear(pp[j2], merge_dist)) { - ip.pos = j2; - ip.mod = pl; - } + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(toClipper(polys), ptyp.ptClip, true); + + lines = []; + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + for (let poly of ctre.m_AllPolys) { + if (minlen || maxlen) { + let plen = clib.JS.PerimeterOfPath(poly.m_polygon, false, 1); + if (minlen && plen < minlen) continue; + if (maxlen && plen > maxlen) continue; + } + let p1 = base.pointFromClipper(poly.m_polygon[0], zpos); + let p2 = base.pointFromClipper(poly.m_polygon[1], zpos); + let od = rayint.origin.distToLineNew(p1,p2) / spacing; + lines.push([p1, p2, od]); + } + } + + lines.sort(function(a,b) { + return a[2] - b[2]; + }) + + for (let line of lines) { + let dist = Math.round(line[2]); + line[0].index = dist; + line[1].index = dist; + rayint.push(line[0]); + rayint.push(line[1]); + } + + return rayint; +} + +/** + * tracing a ray through a slice's polygons, find and return + * a sorted list of all intersecting points. + * + * @param {Point} start + * @param {Slope} slope + * @param {Polygon[]} polygons + * @param {boolean} [for_fill] + * @returns {Point[]} + */ +function rayIntersect(start, slope, polygons, for_fill) { + let i = 0, + flat = [], + points = [], + conf = base.config, + merge_dist = for_fill ? conf.precision_fill_merge : conf.precision_merge; + // todo use new flatten() function above + polygons.forEach(function(p) { + p.flattenTo(flat); + }); + polygons = flat; + while (i < polygons.length) { + let polygon = polygons[i++], + pp = polygon.points, + pl = pp.length;; + for (let j = 0; j < pl; j++) { + let j2 = (j + 1) % pl, + ip = util.intersectRayLine(start, slope, pp[j], pp[j2]); + if (ip) { + // add group object to point for cull detection + ip.group = polygon; + // add point to point list + points.push(ip); + // if point is near a group endpoint, add position marker for culling + if (ip.isNear(pp[j], merge_dist)) { + ip.pos = j; + ip.mod = pl; + } else if (ip.isNear(pp[j2], merge_dist)) { + ip.pos = j2; + ip.mod = pl; } } } - if (points.length > 0) { - let del = false; - // sort on distance from ray origin - points.sort(function (p1, p2) { - // handle passing through line-common end points - if (!(p1.del || p2.del) && p1.isNear(p2, merge_dist)) { - let line = []; - if (!p1.isNear(p1.p1, merge_dist)) line.push(p1.p1); - if (!p1.isNear(p1.p2, merge_dist)) line.push(p1.p2); - if (!p2.isNear(p2.p1, merge_dist)) line.push(p2.p1); - if (!p2.isNear(p2.p2, merge_dist)) line.push(p2.p2); - /** - * when true, points are coincident on collinear lines but - * not passing through endpoints on each. kill them. this case - * was added later. see below for what else can happen. - */ - if (line.length < 2) { - console.log("sliceInt: line common ep fail: "+line.length); - } else - if (line.length > 2) { - p1.del = true; - p2.del = true; - } else - /** - * when a ray intersects two equal points, they are either inside or outside. - * to determine which, we create a line from the two points connected to them - * and test intersect the ray with that line. if it intersects, the points are - * inside and we keep one of them. otherwise, they are outside and we drop both. - */ - if (!UTIL.intersectRayLine(start, slope, line[0], line[1])) { - del = true; - p1.del = true; - p2.del = true; - } else { - del = true; - p1.del = true; - } + } + if (points.length > 0) { + let del = false; + // sort on distance from ray origin + points.sort(function (p1, p2) { + // handle passing through line-common end points + if (!(p1.del || p2.del) && p1.isNear(p2, merge_dist)) { + let line = []; + if (!p1.isNear(p1.p1, merge_dist)) line.push(p1.p1); + if (!p1.isNear(p1.p2, merge_dist)) line.push(p1.p2); + if (!p2.isNear(p2.p1, merge_dist)) line.push(p2.p1); + if (!p2.isNear(p2.p2, merge_dist)) line.push(p2.p2); + /** + * when true, points are coincident on collinear lines but + * not passing through endpoints on each. kill them. this case + * was added later. see below for what else can happen. + */ + if (line.length < 2) { + console.log("sliceInt: line common ep fail: "+line.length); + } else + if (line.length > 2) { + p1.del = true; + p2.del = true; + } else + /** + * when a ray intersects two equal points, they are either inside or outside. + * to determine which, we create a line from the two points connected to them + * and test intersect the ray with that line. if it intersects, the points are + * inside and we keep one of them. otherwise, they are outside and we drop both. + */ + if (!util.intersectRayLine(start, slope, line[0], line[1])) { + del = true; + p1.del = true; + p2.del = true; + } else { + del = true; + p1.del = true; } - return p1.dist - p2.dist; // sort on 'a' dist from ray origin - }); - /** - * cull invalid lines between groups on same/different levels depending - * ok = same level (even), same group - * ok = same level (odd), diff group - * ok = diff level (even-odd) - */ - if (for_fill) { - let p1, p2; - i = 0; - pl = points.length; - while (i < pl) { - p1 = points[i++]; - while (p1 && p1.del && i < pl) p1 = points[i++]; - p2 = points[i++]; - while (p2 && p2.del && i < pl) p2 = points[i++]; - if (p1 && p2 && p1.group && p1.group) { - let p1g = p1.group, - p2g = p2.group, - even = (p1g.depth % 2 === 0), // point is on an even depth group - same = (p1g === p2g); // points intersect same group - if (p1g.depth === p2g.depth) { - // TODO this works sometimes and not others - //if ((even && !same) || (same && !even)) { - // p1.del = true; - // p2.del = true; - // del = true; - //} - // check cull co-linear with group edge - if (same && p1.mod && p2.mod) { - let diff = ABS(p1.pos - p2.pos); - if (diff === 1 || diff === p1.mod - 1) { - p1.del = true; - p2.del = true; - del = true; - } + } + return p1.dist - p2.dist; // sort on 'a' dist from ray origin + }); + /** + * cull invalid lines between groups on same/different levels depending + * ok = same level (even), same group + * ok = same level (odd), diff group + * ok = diff level (even-odd) + */ + if (for_fill) { + let p1, p2; + i = 0; + pl = points.length; + while (i < pl) { + p1 = points[i++]; + while (p1 && p1.del && i < pl) p1 = points[i++]; + p2 = points[i++]; + while (p2 && p2.del && i < pl) p2 = points[i++]; + if (p1 && p2 && p1.group && p1.group) { + let p1g = p1.group, + p2g = p2.group, + even = (p1g.depth % 2 === 0), // point is on an even depth group + same = (p1g === p2g); // points intersect same group + if (p1g.depth === p2g.depth) { + // TODO this works sometimes and not others + //if ((even && !same) || (same && !even)) { + // p1.del = true; + // p2.del = true; + // del = true; + //} + // check cull co-linear with group edge + if (same && p1.mod && p2.mod) { + let diff = ABS(p1.pos - p2.pos); + if (diff === 1 || diff === p1.mod - 1) { + p1.del = true; + p2.del = true; + del = true; } } } } } - // handle deletions, if found - if (del) { - let np = []; - for (i = 0; i < points.length; i++) { - let p = points[i]; - if (!p.del) { - np.push(p); - } + } + // handle deletions, if found + if (del) { + let np = []; + for (i = 0; i < points.length; i++) { + let p = points[i]; + if (!p.del) { + np.push(p); } - points = np; } + points = np; } - return points; } + return points; +} - function fingerprint(polys) { - let finger = []; - flatten(polys).sort((a,b) => { - return a.area() > b.area(); - }).forEach(p => { - finger.push({ - c: p.circularityDeep(), - p: p.perimeterDeep(), - b: p.bounds - }); +function fingerprint(polys) { + let finger = []; + flatten(polys).sort((a,b) => { + return a.area() > b.area(); + }).forEach(p => { + finger.push({ + c: p.circularityDeep(), + p: p.perimeterDeep(), + b: p.bounds }); - return finger; - } + }); + return finger; +} - // compare fingerprint arrays - function fingerprintCompare(a, b) { - // true if array is the same object - if (a === b) { - return true; - } - // fail on missing array - if (!a || !b) { - return false; - } - // require identical length arrays - if (a.length !== b.length) { - return false; - } - for (let i=0; i 0.001) { - return false; - } - // test perimeter - if (Math.abs(ra.p - rb.p) > 0.01) { - return false; - } - // test bounds - if (ra.b.delta(rb.b) > 0.01) { - return false; - } - } +// compare fingerprint arrays +function fingerprintCompare(a, b) { + // true if array is the same object + if (a === b) { return true; } - - // plan a route through an array of polygon center points - // starting with the polygon center closest to "start" - function route(polys, start) { - let centers = []; - let first, minDist = Infinity; - for (let poly of polys) { - let center = poly.average(); - let rec = {poly, center, used: false}; - let dist = center.distTo2D(start); - if (dist < minDist) { - first = rec; - minDist = dist; - } - centers.push(rec); + // fail on missing array + if (!a || !b) { + return false; + } + // require identical length arrays + if (a.length !== b.length) { + return false; + } + for (let i=0; i 0.001) { + return false; } - first.used = true; - let routed = [ first ]; - for (;;) { - let closest; - let minDist = Infinity; - for (let rec of centers) { - if (!rec.used) { - let dist = rec.center.distTo2D(first.center); - if (dist < minDist) { - minDist = dist; - closest = rec; - } + // test perimeter + if (Math.abs(ra.p - rb.p) > 0.01) { + return false; + } + // test bounds + if (ra.b.delta(rb.b) > 0.01) { + return false; + } + } + return true; +} + +// plan a route through an array of polygon center points +// starting with the polygon center closest to "start" +function route(polys, start) { + let centers = []; + let first, minDist = Infinity; + for (let poly of polys) { + let center = poly.average(); + let rec = {poly, center, used: false}; + let dist = center.distTo2D(start); + if (dist < minDist) { + first = rec; + minDist = dist; + } + centers.push(rec); + } + first.used = true; + let routed = [ first ]; + for (;;) { + let closest; + let minDist = Infinity; + for (let rec of centers) { + if (!rec.used) { + let dist = rec.center.distTo2D(first.center); + if (dist < minDist) { + minDist = dist; + closest = rec; } } - if (!closest) { - break; - } else { - closest.used = true; - routed.push(first = closest); - } } - return routed.map(r => r.poly); + if (!closest) { + break; + } else { + closest.used = true; + routed.push(first = closest); + } } + return routed.map(r => r.poly); +} })(); diff --git a/src/geo/render.js b/src/geo/render.js deleted file mode 100644 index becf435a..00000000 --- a/src/geo/render.js +++ /dev/null @@ -1,104 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -(function() { - - if (!self.base) self.base = {}; - if (self.base.render) return; - - const BASE = self.base, - materialCache = {}, - line_width = 1; - - BASE.render = { - wireframe : wireframe - }; - - /** ****************************************************************** - * Render Functions - ******************************************************************* */ - - /** - * render triangle array as line segments. use of {@link newOrderedLine} - * allows lines to be cached by normalizing key order. - * - * @param {THREE.Group} group - * @param {Point[]} points - * @param {number} color - * @returns {THREE.Line} - */ - function wireframe(group, points, color) { - if (points.length % 3 != 0) throw "invalid line : "+points.length; - let lines = new THREE.BufferGeometry(), - hash = {}, - added = 0, - vertices = []; - for (let i = 0; i < points.length; i += 3) { - let p1 = points[i], - p2 = points[i + 1], - p3 = points[i + 2], - l1 = newOrderedLine(p1, p2), - l2 = newOrderedLine(p2, p3), - l3 = newOrderedLine(p3, p1); - if (!hash[l1.key]) { - vertices.appendAll([ - p1.x, p1.y, p1.z, - p2.x, p2.y, p2.z - ]); - hash[l1.key] = ++added; - } - if (!hash[l2.key]) { - vertices.appendAll([ - p2.x, p2.y, p2.z, - p3.x, p3.y, p3.z - ]); - hash[l2.key] = ++added; - } - if (!hash[l3.key]) { - vertices.appendAll([ - p3.x, p3.y, p3.z, - p1.x, p1.y, p1.z - ]); - hash[l3.key] = ++added; - } - } - lines.setAttribute('position', new THREE.BufferAttribute( vertices.toFloat32(), 3 ) ); - let mesh = new THREE.LineSegments(lines, getMaterial(color)); - group.add(mesh); - return mesh; - } - - /** ****************************************************************** - * Connect to base and Helpers - ******************************************************************* */ - - /** - * @param {number} color - * @returns {THREE.LineBasicMaterial} - */ - function getMaterial(color) { - let material = materialCache[color]; - if (!material) { - material = new THREE.LineBasicMaterial({ - fog:false, - color: color, - linewidth: line_width - }); - materialCache[color] = material; - } - return material; - } - - /** - * required for line caching in {@link base.render.wireframe} - * - * @param {Point} p1 - * @param {Point} p2 - * @returns {Line} - */ - function newOrderedLine(p1, p2) { - return p1.key < p2.key ? BASE.newLine(p1, p2) : BASE.newLine(p2, p1); - } - -})(); diff --git a/src/geo/slicer.js b/src/geo/slicer.js index 77c74073..ab6f1ef6 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -8,772 +8,772 @@ */ (function() { - let base = self.base; - if (base.slice) return; +const base = self.base; +if (base.slice) return; - let { config, util, polygons } = base - let { newOrderedLine, newPolygon, newPoint } = base; +const { config, util, polygons } = base +const { newOrderedLine, newPolygon, newPoint } = base; - function dval(v, dv) { - return v !== undefined ? v : dv; +function dval(v, dv) { + return v !== undefined ? v : dv; +} + +/** + * Given an array of points as triples, a bounding box and a set of + * slicing controls, emit an array of Slice objects to the ondone() + * function. onupdate() will be called with two parameters (% completion + * and an optional message) so that the UI can report progress to the user. + * + * @param {Point[]} points vertex array + * @param {Object} options slicing parameters + */ +async function slice(points, options = {}) { + let zMin = options.zmin || 0, + zMax = options.zmax || 0, + zInc = options.zinc || 0, + zIndexes = options.indices || [], + minStep = options.minstep || 0, + zFlat = {}, // map area of z index flat areas + zList = {}, // fast map of z indexes + zScale, // bucket span in z units + zSum = 0.0, // sanity check that points enclose non-zere volume + buckets = [], // banded/grouped faces to speed up slice/search + overlapMax = options.overlap || 0.75, + bucketMax = options.bucketmax || 100, + onupdate = options.onupdate || function() {}, + sliceFn = dval(options.slicer, sliceZ), + { debug, flat, autoDim } = options, + i, j, p1, p2, p3; + + if (!(points && points.length)) { + throw "missing points array"; + } + + // convert threejs position array into points array + if (flat) { + let array = []; + for (i=0, j=points.length; i= zMin) { + // detect faces co-planar with Z and sum the enclosed area + let zkey = p1.z, + area = Math.abs(util.area2(p1,p2,p3)) / 2; + if (!zFlat[zkey]) { + zFlat[zkey] = area; + } else { + zFlat[zkey] += area; + } + } + if (autoDim) { + zMin = Math.min(zMin, p1.z, p2.z, p3.z); + zMax = Math.max(zMax, p1.z, p2.z, p3.z); + } + zList[p1.z] = p1.z; + zList[p2.z] = p2.z; + zList[p3.z] = p3.z; + } + + if (zInc) { + for (i = zMin; i <= zMax; i += zInc) { + zIndexes.push(i); + } + } else { + zIndexes = Object.values(zList).sort((a,b) => a - b); + if (minStep > 0) { + let lastOut; + zIndexes = zIndexes.filter(v => { + if (lastOut !== undefined && v - lastOut < minStep) { + return false; + } else { + lastOut = v; + return true; + } + }); + } } /** - * Given an array of points as triples, a bounding box and a set of - * slicing controls, emit an array of Slice objects to the ondone() - * function. onupdate() will be called with two parameters (% completion - * and an optional message) so that the UI can report progress to the user. - * - * @param {Point[]} points vertex array - * @param {Object} options slicing parameters + * bucket polygons into z-bounded groups (inside or crossing) + * to reduce the search space in complex models */ - async function slice(points, options = {}) { - let zMin = options.zmin || 0, - zMax = options.zmax || 0, - zInc = options.zinc || 0, - zIndexes = options.indices || [], - minStep = options.minstep || 0, - zFlat = {}, // map area of z index flat areas - zList = {}, // fast map of z indexes - zScale, // bucket span in z units - zSum = 0.0, // sanity check that points enclose non-zere volume - buckets = [], // banded/grouped faces to speed up slice/search - overlapMax = options.overlap || 0.75, - bucketMax = options.bucketmax || 100, - onupdate = options.onupdate || function() {}, - sliceFn = dval(options.slicer, sliceZ), - { debug, flat, autoDim } = options, - i, j, p1, p2, p3; + let zSpan = zMax - zMin; + let zSpanAvg = zSum / points.length; + let bucketCount = options.bucket !== false ? + Math.min(bucketMax, Math.max(1, Math.floor(zSpan / zSpanAvg))) : 1; - if (!(points && points.length)) { - throw "missing points array"; - } + zScale = 1 / (zSpan / bucketCount); - // convert threejs position array into points array - if (flat) { - let array = []; - for (i=0, j=points.length; i 1) { + let failAt = (points.length * overlapMax) | 0, bucket; + // copy triples into all matching z-buckets + outer: for (i = 0; i < points.length;) { p1 = points[i++]; p2 = points[i++]; p3 = points[i++]; - // used to calculate buckets (rough sum of z span) - zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z)); - // use co-flat and co-line detection to adjust slice Z - if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) { - // detect faces co-planar with Z and sum the enclosed area - let zkey = p1.z, - area = Math.abs(util.area2(p1,p2,p3)) / 2; - if (!zFlat[zkey]) { - zFlat[zkey] = area; - } else { - zFlat[zkey] += area; + let zm = Math.min(p1.z, p2.z, p3.z) - zMin, + zM = Math.max(p1.z, p2.z, p3.z) - zMin, + bm = Math.floor(zm * zScale), + bM = Math.min(Math.ceil(zM * zScale), bucketCount); + // add point to all buckets in range + for (j = bm; j < bM; j++) { + bucket = buckets[j].points; + bucket.push(p1); + bucket.push(p2); + bucket.push(p3); + // fail if single bucket exceeds threshold + if (bucket.length > failAt) { + if (debug) console.log({ bucketFail: bucket.length }); + bucketCount = 1; + break outer; } } - if (autoDim) { - zMin = Math.min(zMin, p1.z, p2.z, p3.z); - zMax = Math.max(zMax, p1.z, p2.z, p3.z); + } + } + + // fallback if we can't partition point space + if (bucketCount === 1) { + buckets = [{ points, slices: [] }]; + console.log({fallback: buckets}); + } + + // create buckets data structure + for (let z of zIndexes) { + let index = bucketCount <= 1 ? 0 : + Math.min( Math.floor((z - zMin) * zScale), bucketCount - 1 ); + buckets[index].slices.push(z); + onupdate((i / zIndexes.length) * 0.1); + } + + async function sliceBuckets() { + let output = []; + let count = 0; + let opt = { ...options, zMin, zMax }; + + for (let bucket of buckets) { + let { points, slices } = bucket; + for (let z of slices) { + output.push(await sliceFn(z, points, opt)); + onupdate(0.1 + (count++ / zIndexes.length) * 0.9); } - zList[p1.z] = p1.z; - zList[p2.z] = p2.z; - zList[p3.z] = p3.z; } - if (zInc) { - for (i = zMin; i <= zMax; i += zInc) { - zIndexes.push(i); + return output; + } + + // create slices from each bucketed region + let slices = sliceFn ? await sliceBuckets() : []; + slices = slices.sort((a,b) => a.z - b.z); + + return { slices, points, zMin, zMax, zIndexes, zFlat }; +} + +/** + * given a point, append to the correct + * 'where' objec tarray (on, over or under) + * + * @param {Point} p + * @param {number} z offset + * @param {Obejct} where + */ +function checkUnderOverOn(p, z, where) { + let delta = p.z - z; + if (Math.abs(delta) < config.precision_slice_z) { // on + where.on.push(p); + } else if (delta < 0) { // under + where.under.push(p); + } else { // over + where.over.push(p); + } +} + +/** + * Given a point over and under a z offset, calculate + * and return the intersection point on that z plane + * + * @param {Point} over + * @param {Point} under + * @param {number} z offset + * @returns {Point} intersection point + */ +function intersectPoints(over, under, z) { + let ip = []; + for (let i = 0; i < over.length; i++) { + for (let j = 0; j < under.length; j++) { + ip.push(over[i].intersectZ(under[j], z)); + } + } + return ip; +} + +/** + * Ensure points are unique with a cache/key algorithm + */ +function getCachedPoint(phash, p) { + let cached = phash[p.key]; + if (!cached) { + phash[p.key] = p; + return p; + } + return cached; +} + +/** + * Given two points and hints about their edges, + * return a new Line object with points sorted + * lexicographically by key. This allows for future + * line de-duplication and joins. + * + * @param {Object} phash + * @param {Point} p1 + * @param {Point} p2 + * @param {boolean} [coplanar] + * @param {boolean} [edge] + * @returns {Line} + */ +function makeZLine(phash, p1, p2, coplanar, edge) { + p1 = getCachedPoint(phash, p1); + p2 = getCachedPoint(phash, p2); + let line = newOrderedLine(p1,p2); + line.coplanar = coplanar || false; + line.edge = edge || false; + return line; +} + +/** + * process a single z-slice on a single mesh and + * add to slices array + * + * @param {number} z + */ +async function sliceZ(z, points, options = {}) { + let { zMin, zMax, under, over, both } = options, + groupFn = dval(options.groupr, both ? null : sliceConnect), + phash = {}, + lines = [], + p1, p2, p3; + + // default to 'over' selection with 2 points on a line + if (!under && !both) over = true; + + // iterate over matching buckets for this z offset + for (let i = 0; i < points.length; ) { + p1 = points[i++]; + p2 = points[i++]; + p3 = points[i++]; + let where = {under: [], over: [], on: []}; + checkUnderOverOn(p1, z, where); + checkUnderOverOn(p2, z, where); + checkUnderOverOn(p3, z, where); + if (where.under.length === 3 || where.over.length === 3) { + // does not intersect (all 3 above or below) + } else if (where.on.length === 2) { + // one side of triangle is on the Z plane and 3rd is below + // drop lines with 3rd above because that leads to ambiguities + // with complex nested polygons on flat surface + let add2 = both || + (over && (where.over.length === 1 || z === zMax)) || + (under && (where.under.length === 1 || z === zMin)); + if (add2) { + lines.push(makeZLine(phash, where.on[0], where.on[1], false, true)); } + } else if (where.on.length === 3) { + // triangle is coplanar with Z + // we drop these because this face is attached to 3 others + // that will satisfy the if above (line) with 2 points + } else if (where.under.length === 0 || where.over.length === 0) { + // does not intersect but one point is on the slice Z plane } else { - zIndexes = Object.values(zList).sort((a,b) => a - b); - if (minStep > 0) { - let lastOut; - zIndexes = zIndexes.filter(v => { - if (lastOut !== undefined && v - lastOut < minStep) { - return false; - } else { - lastOut = v; - return true; - } + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length < 2 && where.on.length === 1) { + line.push(where.on[0]); + } + if (line.length === 2) { + lines.push(makeZLine(phash, line[0], line[1])); + } else { + console.log({msg: "invalid ips", line: line, where: where}); + } + } + } + + if (lines.length == 0 && options.noEmpty) { + return; + } + + // de-dup and group lines + lines = removeDuplicateLines(lines); + + let rval = { z, lines }; + if (groupFn) rval.groups = groupFn(lines, z, options); + + return rval; +} + +/** + * Given an array of input lines (line soup), find the path through + * joining line ends that encompasses the greatest area without self + * interesection. Eliminate used points and repeat. Unjoined lines + * are permitted and handled after all other cases are handled. + * + * @param {Line[]} input + * @param {number} [index] + * @returns {Array} + */ +function sliceConnect(input, z, opt = {}) { + let { debug, both } = opt; + + if (both) { + if (debug) console.log('unable to connect lines sliced with "both" option'); + return []; + } + + // map points to all other points they're connected to + let pmap = {}, + points = [], + output = [], + connect = [], + emitted = 0, + forks = false, + frays = false, + bridge = config.bridgeLineGapDistance, + bridgeMax = config.bridgeLineGapDistanceMax, + p1, p2, gl; + + function cachedPoint(p) { + let cp = pmap[p.key]; + if (cp) return cp; + points.push(p); + pmap[p.key] = p; + return p; + } + + function addConnected(p1, p2) { + if (!p1.group) p1.group = [ p2 ]; + else p1.group.push(p2); + } + + function perimeter(array) { + if (!array.perimeter) { + array.perimeter = newPolygon().addPoints(array).perimeter(); + } + return array.perimeter; + } + + /** + * follow points through connected lines to form candidate output paths + */ + function findNextPath(point, current, branches, depth = 1) { + let path = []; + if (current) { + current.push(path); + } + + for (;;) { + // prevent point re-use + point.del = true; + // add point to path + path.push(point); + + let links = point.group.filter(p => !p.del); + + // no need to recurse at the start + if (links.length === 2 && depth === 1) { + point = links[0]; + // if (debug) console.log({start_mid: point, depth}); + continue; + } + + // if fork in the road, follow all paths to their end + // and find the longest path + let root = !current, nc; + if (links.length > 1) { + // if (debug) console.log('fork!', {links: links.length, depth, root}); + if (root) { + current = [ path ]; + branches = [ ]; + } + for (let p of links) { + branches.push(nc = current.slice()); + let rpath = findNextPath(p, nc, branches, depth + 1); + // allow point re-use in other path searches + for (let p of rpath) p.del = false; + } + // flatten and sort in ascending perimeter + let flat = branches.map(b => b.flat()).sort((a,b) => { + return perimeter(b) - perimeter(a); }); + let npath = flat[0]; + if (debug) console.log({ + root, + branches: branches.slice(), + flat, path, npath + }); + if (root) { + for (let p of npath) p.del = true; + return npath; + } else { + return path; + } + // return root ? npath : path; + } else { + // choose next (unused) point + point = links[0]; + } + + // hit an open end or branch + if (!point || point.del) { + return path; } } - /** - * bucket polygons into z-bounded groups (inside or crossing) - * to reduce the search space in complex models - */ - let zSpan = zMax - zMin; - let zSpanAvg = zSum / points.length; - let bucketCount = options.bucket !== false ? - Math.min(bucketMax, Math.max(1, Math.floor(zSpan / zSpanAvg))) : 1; + throw "invalid state"; + } - zScale = 1 / (zSpan / bucketCount); + // emit a polygon if it can be cleaned and still have 2 or more points + function emit(poly) { + emitted += poly.length; + poly = poly.clean(); + if (poly.length > 2 || true) output.push(poly); + if (debug) console.log('xray',poly); + } + + // given an array of paths, emit longest to shortest + // eliminating points from the paths as they are emitted + // shorter paths any point eliminated are eliminated as candidates. + function emitPath(path) { + let closed = path[0].group.indexOf(path.peek()) >= 0; + if (closed && path.length > 2) { + if (debug) console.log({ closed: path.length, path }); + emit(newPolygon().addPoints(path)); + } else if (path.length > 1) { + let gap = path[0].distTo2D(path.peek()).round(4); + if (debug) console.log({ open: path.length, gap, path }); + connect.push(path); + } + } + + // create point map, unique point list and point group arrays + input.forEach(function(line) { + p1 = cachedPoint(line.p1); + p2 = cachedPoint(line.p2); + addConnected(p1,p2); + addConnected(p2,p1); + }); + + // console.log({points, forks: points.filter(p => p.group.length !== 2)}); + // for each unused point, find the longest non-intersecting path + + for (let point of points) { + gl = point.group.length; + forks = forks || gl > 2; + frays = frays || gl < 2; + } + if (debug && (forks || frays)) console.log({forks, frays}); + + // process paths starting with forks + if (forks) { + if (debug) console.log('process forks'); + for (let point of points) { + // must not have been used and be a dangling end + if (!point.del && point.group.length > 2) { + let path = findNextPath(point); + if (path) emitPath(path); + } + } } + + // process paths with dangling endpoints + if (frays) { + if (debug) console.log('process frays'); + for (let point of points) { + // must not have been used and be a dangling end + if (!point.del && point.group.length === 1) { + let path = findNextPath(point); + if (path) emitPath(path); + } + } } + + // process normal paths + if (debug) console.log('process mids'); + for (let point of points) { + // must not have been used and be a dangling end + if (!point.del) { + let path = findNextPath(point); + if (path) emitPath(path); + } + } + + if (debug) console.log({ + points, + emitted, + used: points.filter(p => p.del), + free: points.filter(p => !p.del), + }); + + if (debug && connect.length) console.log({connect}); + if (debug) connect = connect.map(a => a.slice()); + + // progressively connect open polygons within a bridge distance + let iter = 1000; + let mingap; + if (true) do { + mingap = Infinity; + + outer: for (let i=0; i 0 && bridge && bridge < bridgeMax && mingap < bridgeMax); + + if (debug) console.log({ remain: connect.filter(c => !c.delete) }); + + for (let array of connect) { + if (array.delete) continue; if (debug) { + let first = array[0]; + let last = array.peek(); + let dist = first.distToSq2D(last); console.log({ - zMin, zMax, zIndexes, zScale, zSum, zSpanAvg, - points, bucketCount, - options, buckets + dist: dist.round(4), + merged: array.merged || false, + array }); } - /** short-circuit for microscopic and invalid objects */ - if (zSpan == 0 || zSum == 0 || points.length == 0) { - return {}; - } - - // create empty buckets - for (i = 0; i < bucketCount; i++) { - buckets.push({ points: [], slices: [] }); - } - - if (bucketCount > 1) { - let failAt = (points.length * overlapMax) | 0, bucket; - // copy triples into all matching z-buckets - outer: for (i = 0; i < points.length;) { - p1 = points[i++]; - p2 = points[i++]; - p3 = points[i++]; - let zm = Math.min(p1.z, p2.z, p3.z) - zMin, - zM = Math.max(p1.z, p2.z, p3.z) - zMin, - bm = Math.floor(zm * zScale), - bM = Math.min(Math.ceil(zM * zScale), bucketCount); - // add point to all buckets in range - for (j = bm; j < bM; j++) { - bucket = buckets[j].points; - bucket.push(p1); - bucket.push(p2); - bucket.push(p3); - // fail if single bucket exceeds threshold - if (bucket.length > failAt) { - if (debug) console.log({ bucketFail: bucket.length }); - bucketCount = 1; - break outer; - } - } - } - } - - // fallback if we can't partition point space - if (bucketCount === 1) { - buckets = [{ points, slices: [] }]; - console.log({fallback: buckets}); - } - - // create buckets data structure - for (let z of zIndexes) { - let index = bucketCount <= 1 ? 0 : - Math.min( Math.floor((z - zMin) * zScale), bucketCount - 1 ); - buckets[index].slices.push(z); - onupdate((i / zIndexes.length) * 0.1); - } - - async function sliceBuckets() { - let output = []; - let count = 0; - let opt = { ...options, zMin, zMax }; - - for (let bucket of buckets) { - let { points, slices } = bucket; - for (let z of slices) { - output.push(await sliceFn(z, points, opt)); - onupdate(0.1 + (count++ / zIndexes.length) * 0.9); - } - } - - return output; - } - - // create slices from each bucketed region - let slices = sliceFn ? await sliceBuckets() : []; - slices = slices.sort((a,b) => a.z - b.z); - - return { slices, points, zMin, zMax, zIndexes, zFlat }; + emit(newPolygon().addPoints(array)); } - /** - * given a point, append to the correct - * 'where' objec tarray (on, over or under) - * - * @param {Point} p - * @param {number} z offset - * @param {Obejct} where - */ - function checkUnderOverOn(p, z, where) { - let delta = p.z - z; - if (Math.abs(delta) < config.precision_slice_z) { // on - where.on.push(p); - } else if (delta < 0) { // under - where.under.push(p); - } else { // over - where.over.push(p); + if (debug) console.log({ emitted }); + if (debug && emitted < points.length) console.log({ leftovers:points.length - emitted }); + + return output; +} + +/** + * eliminate duplicate lines and interior-only lines (coplanar) + * + * lines are sorted using lexicographic point keys such that + * they are comparable even if their points are reversed. hinting + * for deletion, co-planar and suspect shared edge is detectable at + * this time. + * + * @param {Line[]} lines + * @returns {Line[]} + */ +function removeDuplicateLines(lines) { + let output = [], + tmplines = [], + points = [], + pmap = {}; + + function cachePoint(p) { + let cp = pmap[p.key]; + if (cp) return cp; + points.push(p); + pmap[p.key] = p; + return p; + } + + function addLinesToPoint(point, line) { + cachePoint(point); + if (!point.group) point.group = [ line ]; + else point.group.push(line); + } + + // mark duplicates for deletion preserving edges + lines.sort(function (l1, l2) { + if (l1.key === l2.key) { + l1.del = !l1.edge; + l2.del = !l2.edge; + return 0; + } + return l1.key < l2.key ? -1 : 1; + }); + + // associate points with their lines, cull deleted + for (let line of lines) { + if (!line.del) { + tmplines.push(line); + addLinesToPoint(line.p1, line); + addLinesToPoint(line.p2, line); } } - /** - * Given a point over and under a z offset, calculate - * and return the intersection point on that z plane - * - * @param {Point} over - * @param {Point} under - * @param {number} z offset - * @returns {Point} intersection point - */ - function intersectPoints(over, under, z) { - let ip = []; - for (let i = 0; i < over.length; i++) { - for (let j = 0; j < under.length; j++) { - ip.push(over[i].intersectZ(under[j], z)); - } + // merge collinear lines + for (let point of points) { + // only merge when point connects to exactly one other point + if (point.group.length != 2) { + continue; + } + let l1 = point.group[0], + l2 = point.group[1]; + if (l1.isCollinear(l2)) { + l1.del = true; + l2.del = true; + // find new endpoints that are not shared point + let p1 = l1.p1 != point ? l1.p1 : l1.p2, + p2 = l2.p1 != point ? l2.p1 : l2.p2, + newline = newOrderedLine(p1,p2); + // remove deleted lines from associated points + p1.group.remove(l1); + p1.group.remove(l2); + p2.group.remove(l1); + p2.group.remove(l2); + // associate new line with points + p1.group.push(newline); + p2.group.push(newline); + // add new line to lines array + newline.edge = l1.edge || l2.edge; + tmplines.push(newline); } - return ip; } - /** - * Ensure points are unique with a cache/key algorithm - */ - function getCachedPoint(phash, p) { - let cached = phash[p.key]; - if (!cached) { - phash[p.key] = p; - return p; + // mark duplicates for deletion + // but preserve one if it's an edge + tmplines.sort(function (l1, l2) { + if (l1.key === l2.key) { + l1.del = true; + l2.del = !l2.edge; + return 0; + } + return l1.key < l2.key ? -1 : 1; + }); + + // create new line array culling deleted + for (let line of tmplines) { + if (!line.del) { + output.push(line); + line.p1.group = null; + line.p2.group = null; } - return cached; } - /** - * Given two points and hints about their edges, - * return a new Line object with points sorted - * lexicographically by key. This allows for future - * line de-duplication and joins. - * - * @param {Object} phash - * @param {Point} p1 - * @param {Point} p2 - * @param {boolean} [coplanar] - * @param {boolean} [edge] - * @returns {Line} - */ - function makeZLine(phash, p1, p2, coplanar, edge) { - p1 = getCachedPoint(phash, p1); - p2 = getCachedPoint(phash, p2); - let line = newOrderedLine(p1,p2); - line.coplanar = coplanar || false; - line.edge = edge || false; - return line; - } + return output; +} - /** - * process a single z-slice on a single mesh and - * add to slices array - * - * @param {number} z - */ - async function sliceZ(z, points, options = {}) { - let { zMin, zMax, under, over, both } = options, - groupFn = dval(options.groupr, both ? null : sliceConnect), - phash = {}, - lines = [], - p1, p2, p3; - - // default to 'over' selection with 2 points on a line - if (!under && !both) over = true; - - // iterate over matching buckets for this z offset - for (let i = 0; i < points.length; ) { - p1 = points[i++]; - p2 = points[i++]; - p3 = points[i++]; - let where = {under: [], over: [], on: []}; - checkUnderOverOn(p1, z, where); - checkUnderOverOn(p2, z, where); - checkUnderOverOn(p3, z, where); - if (where.under.length === 3 || where.over.length === 3) { - // does not intersect (all 3 above or below) - } else if (where.on.length === 2) { - // one side of triangle is on the Z plane and 3rd is below - // drop lines with 3rd above because that leads to ambiguities - // with complex nested polygons on flat surface - let add2 = both || - (over && (where.over.length === 1 || z === zMax)) || - (under && (where.under.length === 1 || z === zMin)); - if (add2) { - lines.push(makeZLine(phash, where.on[0], where.on[1], false, true)); - } - } else if (where.on.length === 3) { - // triangle is coplanar with Z - // we drop these because this face is attached to 3 others - // that will satisfy the if above (line) with 2 points - } else if (where.under.length === 0 || where.over.length === 0) { - // does not intersect but one point is on the slice Z plane - } else { - // compute two point intersections and construct line - let line = intersectPoints(where.over, where.under, z); - if (line.length < 2 && where.on.length === 1) { - line.push(where.on[0]); - } - if (line.length === 2) { - lines.push(makeZLine(phash, line[0], line[1])); - } else { - console.log({msg: "invalid ips", line: line, where: where}); - } - } - } - - if (lines.length == 0 && options.noEmpty) { - return; - } - - // de-dup and group lines - lines = removeDuplicateLines(lines); - - let rval = { z, lines }; - if (groupFn) rval.groups = groupFn(lines, z, options); - - return rval; - } - - /** - * Given an array of input lines (line soup), find the path through - * joining line ends that encompasses the greatest area without self - * interesection. Eliminate used points and repeat. Unjoined lines - * are permitted and handled after all other cases are handled. - * - * @param {Line[]} input - * @param {number} [index] - * @returns {Array} - */ - function sliceConnect(input, z, opt = {}) { - let { debug, both } = opt; - - if (both) { - if (debug) console.log('unable to connect lines sliced with "both" option'); - return []; - } - - // map points to all other points they're connected to - let pmap = {}, - points = [], - output = [], - connect = [], - emitted = 0, - forks = false, - frays = false, - bridge = config.bridgeLineGapDistance, - bridgeMax = config.bridgeLineGapDistanceMax, - p1, p2, gl; - - function cachedPoint(p) { - let cp = pmap[p.key]; - if (cp) return cp; - points.push(p); - pmap[p.key] = p; - return p; - } - - function addConnected(p1, p2) { - if (!p1.group) p1.group = [ p2 ]; - else p1.group.push(p2); - } - - function perimeter(array) { - if (!array.perimeter) { - array.perimeter = newPolygon().addPoints(array).perimeter(); - } - return array.perimeter; - } - - /** - * follow points through connected lines to form candidate output paths - */ - function findNextPath(point, current, branches, depth = 1) { - let path = []; - if (current) { - current.push(path); - } - - for (;;) { - // prevent point re-use - point.del = true; - // add point to path - path.push(point); - - let links = point.group.filter(p => !p.del); - - // no need to recurse at the start - if (links.length === 2 && depth === 1) { - point = links[0]; - // if (debug) console.log({start_mid: point, depth}); - continue; - } - - // if fork in the road, follow all paths to their end - // and find the longest path - let root = !current, nc; - if (links.length > 1) { - // if (debug) console.log('fork!', {links: links.length, depth, root}); - if (root) { - current = [ path ]; - branches = [ ]; - } - for (let p of links) { - branches.push(nc = current.slice()); - let rpath = findNextPath(p, nc, branches, depth + 1); - // allow point re-use in other path searches - for (let p of rpath) p.del = false; - } - // flatten and sort in ascending perimeter - let flat = branches.map(b => b.flat()).sort((a,b) => { - return perimeter(b) - perimeter(a); - }); - let npath = flat[0]; - if (debug) console.log({ - root, - branches: branches.slice(), - flat, path, npath - }); - if (root) { - for (let p of npath) p.del = true; - return npath; - } else { - return path; - } - // return root ? npath : path; - } else { - // choose next (unused) point - point = links[0]; - } - - // hit an open end or branch - if (!point || point.del) { - return path; - } - } - - throw "invalid state"; - } - - // emit a polygon if it can be cleaned and still have 2 or more points - function emit(poly) { - emitted += poly.length; - poly = poly.clean(); - if (poly.length > 2 || true) output.push(poly); - if (debug) console.log('xray',poly); - } - - // given an array of paths, emit longest to shortest - // eliminating points from the paths as they are emitted - // shorter paths any point eliminated are eliminated as candidates. - function emitPath(path) { - let closed = path[0].group.indexOf(path.peek()) >= 0; - if (closed && path.length > 2) { - if (debug) console.log({ closed: path.length, path }); - emit(newPolygon().addPoints(path)); - } else if (path.length > 1) { - let gap = path[0].distTo2D(path.peek()).round(4); - if (debug) console.log({ open: path.length, gap, path }); - connect.push(path); - } - } - - // create point map, unique point list and point group arrays - input.forEach(function(line) { - p1 = cachedPoint(line.p1); - p2 = cachedPoint(line.p2); - addConnected(p1,p2); - addConnected(p2,p1); - }); - - // console.log({points, forks: points.filter(p => p.group.length !== 2)}); - // for each unused point, find the longest non-intersecting path - - for (let point of points) { - gl = point.group.length; - forks = forks || gl > 2; - frays = frays || gl < 2; - } - if (debug && (forks || frays)) console.log({forks, frays}); - - // process paths starting with forks - if (forks) { - if (debug) console.log('process forks'); - for (let point of points) { - // must not have been used and be a dangling end - if (!point.del && point.group.length > 2) { - let path = findNextPath(point); - if (path) emitPath(path); - } - } } - - // process paths with dangling endpoints - if (frays) { - if (debug) console.log('process frays'); - for (let point of points) { - // must not have been used and be a dangling end - if (!point.del && point.group.length === 1) { - let path = findNextPath(point); - if (path) emitPath(path); - } - } } - - // process normal paths - if (debug) console.log('process mids'); - for (let point of points) { - // must not have been used and be a dangling end - if (!point.del) { - let path = findNextPath(point); - if (path) emitPath(path); - } - } - - if (debug) console.log({ - points, - emitted, - used: points.filter(p => p.del), - free: points.filter(p => !p.del), - }); - - if (debug && connect.length) console.log({connect}); - if (debug) connect = connect.map(a => a.slice()); - - // progressively connect open polygons within a bridge distance - let iter = 1000; - let mingap; - if (true) do { - mingap = Infinity; - - outer: for (let i=0; i 0 && bridge && bridge < bridgeMax && mingap < bridgeMax); - - if (debug) console.log({ remain: connect.filter(c => !c.delete) }); - - for (let array of connect) { - if (array.delete) continue; - - if (debug) { - let first = array[0]; - let last = array.peek(); - let dist = first.distToSq2D(last); - console.log({ - dist: dist.round(4), - merged: array.merged || false, - array - }); - } - - emit(newPolygon().addPoints(array)); - } - - if (debug) console.log({ emitted }); - if (debug && emitted < points.length) console.log({ leftovers:points.length - emitted }); - - return output; - } - - /** - * eliminate duplicate lines and interior-only lines (coplanar) - * - * lines are sorted using lexicographic point keys such that - * they are comparable even if their points are reversed. hinting - * for deletion, co-planar and suspect shared edge is detectable at - * this time. - * - * @param {Line[]} lines - * @returns {Line[]} - */ - function removeDuplicateLines(lines) { - let output = [], - tmplines = [], - points = [], - pmap = {}; - - function cachePoint(p) { - let cp = pmap[p.key]; - if (cp) return cp; - points.push(p); - pmap[p.key] = p; - return p; - } - - function addLinesToPoint(point, line) { - cachePoint(point); - if (!point.group) point.group = [ line ]; - else point.group.push(line); - } - - // mark duplicates for deletion preserving edges - lines.sort(function (l1, l2) { - if (l1.key === l2.key) { - l1.del = !l1.edge; - l2.del = !l2.edge; - return 0; - } - return l1.key < l2.key ? -1 : 1; - }); - - // associate points with their lines, cull deleted - for (let line of lines) { - if (!line.del) { - tmplines.push(line); - addLinesToPoint(line.p1, line); - addLinesToPoint(line.p2, line); - } - } - - // merge collinear lines - for (let point of points) { - // only merge when point connects to exactly one other point - if (point.group.length != 2) { - continue; - } - let l1 = point.group[0], - l2 = point.group[1]; - if (l1.isCollinear(l2)) { - l1.del = true; - l2.del = true; - // find new endpoints that are not shared point - let p1 = l1.p1 != point ? l1.p1 : l1.p2, - p2 = l2.p1 != point ? l2.p1 : l2.p2, - newline = newOrderedLine(p1,p2); - // remove deleted lines from associated points - p1.group.remove(l1); - p1.group.remove(l2); - p2.group.remove(l1); - p2.group.remove(l2); - // associate new line with points - p1.group.push(newline); - p2.group.push(newline); - // add new line to lines array - newline.edge = l1.edge || l2.edge; - tmplines.push(newline); - } - } - - // mark duplicates for deletion - // but preserve one if it's an edge - tmplines.sort(function (l1, l2) { - if (l1.key === l2.key) { - l1.del = true; - l2.del = !l2.edge; - return 0; - } - return l1.key < l2.key ? -1 : 1; - }); - - // create new line array culling deleted - for (let line of tmplines) { - if (!line.del) { - output.push(line); - line.p1.group = null; - line.p2.group = null; - } - } - - return output; - } - - base.slice = slice; - base.sliceZ = sliceZ; +base.slice = slice; +base.sliceZ = sliceZ; })(); diff --git a/src/geo/slope.js b/src/geo/slope.js index 6187cdce..c93227ef 100644 --- a/src/geo/slope.js +++ b/src/geo/slope.js @@ -4,147 +4,105 @@ (function() { - if (self.base.Slope) return; +const base = self.base; +if (base.Slope) return; - const BASE = self.base, - CONF = BASE.config, - ABS = Math.abs, - PRO = Slope.prototype, - DEG2RAD = Math.PI / 180, - RAD2DEG = 180 / Math.PI; +const { config } = base; +const ABS = Math.abs, + DEG2RAD = Math.PI / 180, + RAD2DEG = 180 / Math.PI; - BASE.Slope = Slope; - BASE.newSlope = newSlope; - BASE.newSlopeFromAngle = function(angle) { - return newSlope(0,0, - Math.cos(angle * DEG2RAD), - Math.sin(angle * DEG2RAD) - ); - }; - - /** - * - * @param p1 - * @param p2 - * @param dx - * @param dy - * @constructor - */ - function Slope(p1, p2, dx, dy) { +class Slope { + constructor(p1, p2, dx, dy) { this.dx = p1 && p2 ? p2.x - p1.x : dx; this.dy = p1 && p2 ? p2.y - p1.y : dy; this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG; } - /** ****************************************************************** - * Slope Prototype Functions - ******************************************************************* */ - - PRO.toString = function() { + toString() { return [this.dx, this.dy, this.angle].join(','); - }; + } - PRO.clone = function() { + clone() { return new Slope(null, null, this.dx, this.dy); - }; + } - /** - * @param {Slope} s - * @returns {boolean} - */ - PRO.isSame = function(s) { + isSame(s) { // if very close to vertical or horizontal, they're the same - if (ABS(this.dx) <= CONF.precision_merge && ABS(s.dx) <= CONF.precision_merge) return true; - if (ABS(this.dy) <= CONF.precision_merge && ABS(s.dy) <= CONF.precision_merge) return true; + if (ABS(this.dx) <= config.precision_merge && ABS(s.dx) <= config.precision_merge) return true; + if (ABS(this.dy) <= config.precision_merge && ABS(s.dy) <= config.precision_merge) return true; // check angle within a range - let prec = Math.min(1/Math.sqrt(this.dx * this.dx + this.dy * this.dy), CONF.precision_slope_merge); - return angleWithinDelta(this.angle, s.angle, prec || CONF.precision_slope); - }; + let prec = Math.min(1/Math.sqrt(this.dx * this.dx + this.dy * this.dy), config.precision_slope_merge); + return angleWithinDelta(this.angle, s.angle, prec || config.precision_slope); + } - /** - * turn slope 90 degrees - * - * @returns {Slope} - */ - PRO.normal = function() { + normal() { let t = this.dx; this.dx = -this.dy; this.dy = t; this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG; return this; - }; + } - PRO.invert = function() { + invert() { this.dx = -this.dx; this.dy = -this.dy; this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG; return this; } - PRO.toUnit = function() { + toUnit() { let max = Math.max(ABS(this.dx), ABS(this.dy)); this.dx = this.dx / max; this.dy = this.dy / max; return this; - }; + } - PRO.factor = function(f) { + factor(f) { this.dx *= f; this.dy *= f; return this; - }; + } - /** - * reverse (180 degree) slope - * - * @returns {Slope} - */ - PRO.invert = function() { + invert() { this.dx = -this.dx; this.dy = -this.dy; this.angle = 180 - this.angle; return this; - }; + } - PRO.angleDiff = function(s2,sign) { + angleDiff(s2,sign) { const n1 = this.angle; const n2 = s2.angle; let diff = n2 - n1; while (diff < -180) diff += 360; while (diff > 180) diff -= 360; return sign ? diff : Math.abs(diff); - }; - - /** ****************************************************************** - * Connect to base and Helpers - ******************************************************************* */ - - /** - * returns true if the difference between a & b is less than v - * - * @param {number} a - * @param {number} b - * @param {number} v - * @returns {boolean} - */ - function minDeltaABS(a,b,v) { - return ABS(a-b) < v; } +} - function angleWithinDelta(a1, a2, delta) { - return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta); - } +/** + * returns true if the difference between a & b is less than v + */ +function minDeltaABS(a,b,v) { + return ABS(a-b) < v; +} - /** - * - * @param p1 - * @param p2 - * @param dx - * @param dy - * @returns {Slope} - */ - function newSlope(p1, p2, dx, dy) { - return new Slope(p1, p2, dx, dy); - } +function angleWithinDelta(a1, a2, delta) { + return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta); +} + +function newSlope(p1, p2, dx, dy) { + return new Slope(p1, p2, dx, dy); +} + +base.Slope = Slope; +base.newSlope = newSlope; +base.newSlopeFromAngle = function(angle) { + return newSlope(0,0, + Math.cos(angle * DEG2RAD), + Math.sin(angle * DEG2RAD) + ); +}; })(); diff --git a/src/geo/wasm.js b/src/geo/wasm.js index 5dff5d43..aa5bc42f 100644 --- a/src/geo/wasm.js +++ b/src/geo/wasm.js @@ -5,202 +5,202 @@ // only active in workers if (!self.window) (function() { - if (!self.geo) self.geo = { - enable, - disable, - count: { - offset: 0, - union: 0, - diff: 0 - } - }; - - const factor = self.base.config.clipper; - const geo = self.geo; - - function log() { - console.log(...arguments); +if (!self.geo) self.geo = { + enable, + disable, + count: { + offset: 0, + union: 0, + diff: 0 } +}; - function writePolys(view, polys) { - let pcount = 0; - for (let poly of polys) { - pcount += writePoly(view, poly); - } - return pcount; +const factor = self.base.config.clipper; +const geo = self.geo; + +function log() { + console.log(...arguments); +} + +function writePolys(view, polys) { + let pcount = 0; + for (let poly of polys) { + pcount += writePoly(view, poly); } + return pcount; +} - function writePoly(view, poly, inner) { - if (inner) { - poly.setCounterClockwise(); +function writePoly(view, poly, inner) { + if (inner) { + poly.setCounterClockwise(); + } else { + poly.setClockwise(); + } + let count = 1; + let points = poly.points; + let inners = poly.inner; + view.writeU16(points.length, true); + for (let i=0, il=points.length; i 0) { + poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0); + } + return poly; +} + +function readPolys(view, z, out = []) { + for (;;) { + let poly = readPoly(view, z); + if (poly) { + out.push(poly); } else { - poly.setClockwise(); + break; } - let count = 1; - let points = poly.points; - let inners = poly.inner; - view.writeU16(points.length, true); - for (let i=0, il=points.length; i 0) { - poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0); - } - return poly; +function polyOffset(polys, offset, z, clean, simple) { + geo.count.offset++; + let wasm = geo.wasm, + buffer = geo.wasm.shared, + pcount = writePolys(new DataWriter(wasm.heap, buffer), polys), + resat = wasm.fn.offset(buffer, pcount, offset * factor, clean, simple), + out = readPolys(new DataReader(wasm.heap, resat), z); + return polyNest(out); +} + +function polyUnion(polys, z) { + geo.count.union++; + let wasm = geo.wasm, + buffer = geo.wasm.shared, + pcount = writePolys(new DataWriter(wasm.heap, buffer), polys), + resat = wasm.fn.union(buffer, pcount), + out = readPolys(new DataReader(wasm.heap, resat), z); + return polyNest(out); +} + +function polyDiff(polysA, polysB, z, AB, BA) { + geo.count.diff++; + let wasm = geo.wasm, + buffer = geo.wasm.shared, + writer = new DataWriter(wasm.heap, buffer), + pcountA = writePolys(writer, polysA), + pcountB = writePolys(writer, polysB), + resat = wasm.fn.diff(buffer, pcountA, pcountB, AB?1:0, BA?1:0, base.config.clipperClean), + reader = new DataReader(wasm.heap, resat); + if (AB) { + AB.appendAll(polyNest(readPolys(reader, z))); } + if (BA) { + BA.appendAll(polyNest(readPolys(reader, z))); + } +} - function readPolys(view, z, out = []) { - for (;;) { - let poly = readPoly(view, z); - if (poly) { - out.push(poly); - } else { +// nest closed polygons without existing parent / child relationships +function polyNest(polys) { + polys.sort((a,b) => { + return b.bounds.minx - a.bounds.minx; + }); + // from smallest to largest, check for enclosing bounds and nest + for (let i=0, il=polys.length; i { - return b.bounds.minx - a.bounds.minx; +function readString(pos, len) { + let view = new DataReader(geo.wasm.heap, pos); + let out = []; + while (len-- > 0) { + out.push(String.fromCharCode(view.readU8())); + } + return out.join(''); +} + +function enable() { + if (geo.wasm || geo._wasm) { + return; + } + geo._wasm = 'loading'; + fetch('/wasm/kiri-geo.wasm') + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, { + env: { + debug_string: (len, ptr) => { console.log('wasm', readString(ptr, len)) } + }, + wasi_snapshot_preview1: { + // args_get: (count,bufsize) => { return 0 }, + // args_sizes_get: (count,bufsize) => { }, + // environ_get: (count,bufsize) => { return 0 }, + // environ_sizes_get: (count,bufsize) => { }, + proc_exit: (code) => { return code } + } + })) + .then(results => { + // console.log({enabled: geo.wasm}); + delete geo._wasm; + let { module, instance } = results; + let { exports } = instance; + let heap = new DataView(exports.memory.buffer); + let wasm = geo.wasm = { + heap, + exports, + memory: exports.memory, + memmax: exports.memory.buffer.byteLength, + malloc: exports.mem_get, + free: exports.mem_clr + }; + wasm.shared = wasm.malloc(1024 * 1024 * 30), + wasm.fn = { + diff: exports.poly_diff, + union: exports.poly_union, + offset: exports.poly_offset + }; + wasm.js = { + diff: polyDiff, + union: polyUnion, + offset: polyOffset + }; }); - // from smallest to largest, check for enclosing bounds and nest - for (let i=0, il=polys.length; i 0) { - out.push(String.fromCharCode(view.readU8())); - } - return out.join(''); - } - - function enable() { - if (geo.wasm || geo._wasm) { - return; - } - geo._wasm = 'loading'; - fetch('/wasm/kiri-geo.wasm') - .then(response => response.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes, { - env: { - debug_string: (len, ptr) => { console.log('wasm', readString(ptr, len)) } - }, - wasi_snapshot_preview1: { - // args_get: (count,bufsize) => { return 0 }, - // args_sizes_get: (count,bufsize) => { }, - // environ_get: (count,bufsize) => { return 0 }, - // environ_sizes_get: (count,bufsize) => { }, - proc_exit: (code) => { return code } - } - })) - .then(results => { - // console.log({enabled: geo.wasm}); - delete geo._wasm; - let { module, instance } = results; - let { exports } = instance; - let heap = new DataView(exports.memory.buffer); - let wasm = geo.wasm = { - heap, - exports, - memory: exports.memory, - memmax: exports.memory.buffer.byteLength, - malloc: exports.mem_get, - free: exports.mem_clr - }; - wasm.shared = wasm.malloc(1024 * 1024 * 30), - wasm.fn = { - diff: exports.poly_diff, - union: exports.poly_union, - offset: exports.poly_offset - }; - wasm.js = { - diff: polyDiff, - union: polyUnion, - offset: polyOffset - }; - }); - } - - function disable() { - if (geo.wasm) { - delete geo.wasm; - // console.log({disabled: geo}); - } +function disable() { + if (geo.wasm) { + delete geo.wasm; + // console.log({disabled: geo}); } +} })();