/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ "use strict"; var gs_kiri_slicer = exports; (function() { if (!self.kiri) self.kiri = {}; if (self.kiri.slicer) return; var slicer = self.kiri.slicer = { slice: slice, sliceWidget: sliceWidget }; var KIRI = self.kiri, BASE = self.base, CONF = BASE.config, UTIL = BASE.util, POLY = BASE.polygons, time = UTIL.time, newSlice = KIRI.newSlice, newOrderedLine = BASE.newOrderedLine; /** * Convenience method. Gets a Widget's points and calls slice() * * @param {Widget} widget * @param {Object} options * @param {Function} ondone callback when slicing complete * @param {Function} onupdate callback on incremental updates */ function sliceWidget(widget, options, ondone, onupdate) { slice(widget.getPoints(), widget.getBoundingBox(), options, ondone, onupdate); } /** * 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 {Array} points vertex array * @param {Bounds} bounds bounding box for points * @param {Object} options slicing parameters * @param {Function} ondone callback when slicing done * @param {Function} onupdate callback to report slicing progress */ function slice(points, bounds, options, ondone, onupdate) { var topoMode = options.topo, ox = 0, oy = 0; // handle rotating meshes for CAM finishing. // slicer expects things just so, so we alter // geometry to satisfy if (options.swapX || options.swapY) { points = points.slice(); var btmp = new THREE.Box3(), pref = {}, cached; btmp.setFromPoints(points); if (options.swapX) ox = -btmp.max.x; if (options.swapY) oy = -btmp.max.y; // array re-uses points so we need // to be careful not to alter a point // more than once for (var p, index=0; index bounds.min.z) { var zkey = p1.z < bounds.max.z ? p1.z + 0.001 : p1.z, area = Math.abs(UTIL.area2(p1,p2,p3))/2; if (!zFlat[zkey]) { zFlat[zkey] = area; } else { zFlat[zkey] += area; } } } } /** bucket polygons into z-bounded groups */ var bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1); zScale = 1 / (zMax / bucketCount); if (bucketCount > 1) { // create empty buckets for (i = 0; i < bucketCount + 1; i++) buckets.push([]); // copy triples into all matching z-buckets for (i = 0; i < points.length;) { p1 = points[i++]; p2 = points[i++]; p3 = points[i++]; var zm = Math.min(p1.z, p2.z, p3.z), zM = Math.max(p1.z, p2.z, p3.z), bm = Math.floor(zm * zScale), bM = Math.ceil(zM * zScale); for (j = bm; j < bM; j++) { buckets[j].push(p1); buckets[j].push(p2); buckets[j].push(p3); } } } // create zIndexes array (0 = auto-detect) if (zInc === 0) { // find unique z-index offsets for slicing var key, zl = []; for (key in zList) { if (!zList.hasOwnProperty(key)) continue; zl.push(parseFloat(key)); } zl.sort(function(a,b) { return a - b}); for (i = 0; i < zl.length-1; i++) { zIndexes.push((zl[i] + zl[i+1]) / 2); } zIndexes.sort(function(a,b) { return a - b}); } else if (options.cam) { // re-divide slice height so that top and // bottom slices fall exactly on those faces zInc = (zMax - zMin) / (Math.floor(zMax / zInc) + 1); for (i = zMin; i < zMax; i += zInc) { zIndexes.push(i); } for (key in zFlat) { // todo make threshold for flat detection configurable if (!zFlat.hasOwnProperty(key) || zFlat[key] < 100) continue; key = parseFloat(key); if (!zIndexes.contains(key) && key >= zMin) zIndexes.push(key); } // sort top down zIndexes.sort(function(a,b) { return b-a; }); } else { // FDM is the simplest case. Just offset and slice at // predicable offsets. First offset is half a slice height. // this was once configurable, but that turned out not to be // terribly useful. In future, first layer height may be // adjusted separately, but that would also require other // settings changes (flow rate, etc). // ... only this one special case for first layer :) if (options.firstHeight) { zIndexes.push(options.firstHeight / 2); zMin = options.firstHeight; } for (i = zMin + zOff; i < zMax; i += zInc) { zIndexes.push(i); } } // create a Slice for each z offset in the zIndexes array for (var i = 0; i < zIndexes.length; i++) { zPos = zIndexes[i]; sliceZ(zPos); // kill slicing if onupdate() returns 42 if (onupdate(i / zIndexes.length) === 42) { return ondone(null); } } // for cam, mark top and bottom as mandatory (hasFlats) if (options.cam && slices.length > 0) { slices[0].hasFlats = true; slices[slices.length-1].hasFlats = true; } // connect slices into linked list for island/bridge projections for (i=1; i 0; // for topo slices, we just need the raw lines if (!topoMode) { slice.groups = connectLines(slice.lines, slices.length); POLY.nest(slice.groups).forEach(function(top) { slice.addTop(top) }); } // fixup un-rotates polygons for CAM if (options.swapX || options.swapY) { var move = {x:ox, y:oy, z:0}; slice.camMode = options.swapX ? CPRO.FINISH_X : CPRO.FINISH_Y; if (topoMode) { var lines = slice.lines, llen = lines.length, idx, line; // shared points causing problems for (idx=0; idx 10000) { DBUG.log("excessive path options @ "+paths.length+" #"+input.length); return; } for (;;) { stack.push(point); var last = point, links = point.group; path.push(point); // use del to mark traversed path point.del = true; // set so point isn't used in another polygon search point.pos = search++; // seed path with two points to prevent redundant opposing seeks if (path.length === 1) { from = point; point = links[0]; continue; } if (links.length > 2) { // TODO optimize when > 2 and limit to left-most and right-most branches // for now, pursue all possible branches links.forEach(function(nextp) { // do not backtrack if (nextp === from) { return; } if (nextp.del) { paths.push(sliceAtTerm(path,nextp)); } else { findPathsMinRecurse(nextp, path.slice(), paths, point); } }); break; } else { point = links[0] === from ? links[1] : links[0]; from = last; // hit an open end if (!point) { path.open = true; paths.push(path); break; } // hit a point previously in the path (or start) if (point.del) { paths.push(sliceAtTerm(path,point)); break; } } } for (var i=0; i 2) output.push(poly.clean()); // if (poly.length > 2) output.push(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 emitLongestAsPolygon(paths) { var longest = null, emitted = 0, closed = 0, open = 0; paths.forEach(function(path, index) { // use longest perimeter vs longest path? if (!longest || path.length > longest.length) longest = path; if (!path.open) closed++; else open++; }); // it gets more complicated with multiple possible output paths if (closed > 1 && open === 0) { // add polygon to path (for area sorting) paths.forEach(function(path) { path.poly = BASE.newPolygon().addPoints(path) }); // sort descending by area VS (length below -- better in most cases) // paths.sort(function(a,b) { return b.poly.area() - a.poly.area() }); // sort descending by length paths.sort(function(a,b) { return b.poly.length - a.poly.length }); // emit polygons largest to smallest // omit polygon if it intersects previously emitted (has del points) paths.forEach(function(path) { if (path.length < 3) return; var len = path.length, i; for (i = 0; i < len; i++) if (path[i].del) return; for (i = 0; i < len; i++) path[i].del = true; emit(path.poly); emitted++; }); } else { if (longest.open) { connect.push(longest); } else { emit(BASE.newPolygon().addPoints(longest)); } } } // create point map, unique point list and point group arrays input.forEach(function(line) { p1 = cachedPoint(line.p1.round(7)); p2 = cachedPoint(line.p2.round(7)); addConnected(p1,p2); addConnected(p2,p1); }); // first trace paths starting at dangling endpoinds (bad polygon soup) points.forEach(function(point) { // must not have been used and be a dangling end if (point.pos === 0 && point.group.length === 1) { var path = [], paths = []; findPathsMinRecurse(point, path, paths); if (paths.length > 0) emitLongestAsPolygon(paths); } }); // for each point, find longest path back to self points.forEach(function(point) { // must not have been used or be at a split if (point.pos === 0 && point.group.length === 2) { var path = [], paths = []; findPathsMinRecurse(point, path, paths); if (paths.length > 0) emitLongestAsPolygon(paths); } }); // return true if points are deemed "close enough" close a polygon function close(p1,p2) { return p1.distToSq2D(p2) <= 0.01; } // reconnect dangling/open polygons to closest endpoint for (var i=0; i