diff --git a/app.js b/app.js index 7cd3cbf2..3335be7d 100644 --- a/app.js +++ b/app.js @@ -261,7 +261,7 @@ const script = { "kiri/fill", "kiri/db", "kiri/slice", - "kiri/slicer", + // "kiri/slicer", "mode/fdm/driver", "mode/sla/driver", "mode/cam/driver", @@ -301,6 +301,7 @@ const script = { "kiri/fill", "kiri/slice", "kiri/slicer", + "kiri/slicer2", "mode/fdm/driver", "mode/sla/driver", "mode/cam/driver", diff --git a/src/kiri/slice.js b/src/kiri/slice.js index 1817cc71..f4233480 100644 --- a/src/kiri/slice.js +++ b/src/kiri/slice.js @@ -189,6 +189,13 @@ return scope.addTop(poly); }; + PRO.addTops = function(polys) { + polys.forEach(p => { + this.addTop(p); + }); + return this; + } + /** * Create a new top object given a polygon * diff --git a/src/kiri/slicer2.js b/src/kiri/slicer2.js new file mode 100644 index 00000000..4a06e889 --- /dev/null +++ b/src/kiri/slicer2.js @@ -0,0 +1,701 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + if (self.kiri.slicer2) return; + + let KIRI = self.kiri, + BASE = self.base, + CONF = BASE.config, + UTIL = BASE.util, + POLY = BASE.polygons, + newSlice = KIRI.newSlice, + newOrderedLine = BASE.newOrderedLine; + + class Slicer { + constructor(points, options) { + if (points) { + this.setPoints(points, options); + } + } + + setPoints(points, options) { + this.points = points; + this.zFlat = {}; // accumulated flat area at z height + this.zLine = {}; // count of z coplanar lines + this.zList = {}; // count of z values for auto slicing + this.zSum = 0; // used in bucketing calculations + return this + .computeBounds() + .computeFeatures(options) + .computeBuckets(); + } + + computeBounds() { + this.bounds = new THREE.Box3(); + this.bounds.setFromPoints(this.points); + return this; + } + + // gather z-index stats + // these are used for auto-slicing in laser + // and to flats detection in CAM mode + computeFeatures(options) { + const opt = options || {}; + const points = this.points; + const bounds = this.bounds; + const zFlat = this.zFlat; + const zLine = this.zLine; + const zList = this.zList; + + function countZ(z) { + z = UTIL.round(z,5); + zList[z] = (zList[z] || 0) + 1; + } + + for (let i = 0, il = points.length; i < il; ) { + let p1 = points[i++]; + let p2 = points[i++]; + let p3 = points[i++]; + // used in bucket calculations + this.zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z)); + // count occurrences of z values for auto slicing + if (opt.zlist) { + countZ(p1.z); + countZ(p2.z); + countZ(p3.z); + } + // use co-flat and co-line detection to adjust slice Z + if (p1.z === p2.z && p2.z === p3.z) { + // detect zFlat faces to avoid slicing directly on them + let zkey = p1.z.toFixed(5), + area = Math.abs(UTIL.area2(p1,p2,p3)) / 2; + if (!zFlat[zkey]) { + zFlat[zkey] = area; + } else { + zFlat[zkey] += area; + } + } else if (opt.zline) { + // detect zLine (curved region tops/bottoms) + // in cam used for ball and v mill tracing + if (p1.z === p2.z) { + let zkey = p1.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p2.z === p3.z) { + let zkey = p2.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p3.z === p1.z) { + let zkey = p3.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + } + } + + if (opt.trace) { + this.findTraceLines(); + } + + return this; + } + + // find slice candidates to trace for CNC ballmills and tapermills + findTraceLines() { + let zl = {}; + let le; + let zs = Object.entries(this.zLine).map(oe => { + return oe.map(v => parseFloat(v)); + }).sort((a,b) => { + return b[0] - a[0]; + }).forEach((e,i) => { + if (i > 0) { + let zd = le[0]-e[0]; + if (zd > 0.1 && e[1] > 100) { + // zl.push(e) + zl[e[0].toFixed(5)] = e[1]; + zFlat[e[0].toFixed(5)] = e[1]; + } + } + if (e[1] > 10) { + le = e; + } + }); + this.zLine = zl; + return this; + } + + /** + * bucket polygons into z-bounded groups (inside or crossing) + * to reduce the search space in complex models + */ + computeBuckets() { + let zSum = this.zSum; + let zMax = this.bounds.max.z; + let points = this.points; + let bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1); + let zScale = this.zScale = 1 / (zMax / bucketCount); + let buckets = this.buckets = []; + + if (bucketCount > 1) { + // create empty buckets + for (let i = 0; i < bucketCount + 1; i++) { + buckets.push([]); + } + + // copy triples into all matching z-buckets + for (let i = 0, il = points.length; i < il; ) { + let p1 = points[i++], + p2 = points[i++], + p3 = points[i++], + 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 (let j = bm; j < bM; j++) { + buckets[j].push(p1); + buckets[j].push(p2); + buckets[j].push(p3); + } + } + } + return this; + } + + // slice through points at given Z and return polygons + slice(z, options, index, total, mark) { + let opt = options || {}; + + if (Array.isArray(z)) { + const mark = UTIL.time(); + const rarr = []; + z.forEach((zv,zi) => { + rarr.push(this.slice(zv, opt, zi, z.length, mark)); + }); + return rarr; + } + + let phash = {}, + lines = [], + zScale = this.zScale, + buckets = this.buckets, + bucket = buckets.length ? buckets[Math.floor(z * zScale)] : points; + + if (!bucket) { + console.log({no_bucket_for_z: z}); + return; + } + + // iterate over matching buckets for this z offset + for (let i = 0, il = bucket.length; i < il; ) { + let p1 = bucket[i++]; + let p2 = bucket[i++]; + let p3 = bucket[i++]; + let where = {under: [], over: [], on: []}; + checkOverUnderOn(p1, z, where); + checkOverUnderOn(p2, z, where); + checkOverUnderOn(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 + 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}); + } + } + } + + let retn = { z }; + + if (lines.length) { + retn.lines = removeDuplicateLines(lines); + retn.tops = POLY.nest(connectLines(retn.lines)); + + if (opt.genso) { + retn.slice = newSlice(z).addTops(retn.tops); + } + } + + if (opt.each) { + opt.each(retn, index, total, UTIL.time() - mark); + } + + return retn; + } + + interval(step, options) { + let opt = options || {}, + bounds = this.bounds, + boff = opt.boff || 0, // bottom offset + toff = opt.toff || 0, // top offset + zmin = (opt.min || this.bounds.min.z) + boff, + zmax = (opt.max || this.bounds.max.z) - toff, + count = Math.floor((zmax - zmin) / step), + array = new Array(count); + + if (opt.down) { + for (let i=0, il=array.length; i 10000) { + DBUG.log("excessive path options @ "+paths.length+" #"+input.length); + return; + } + for (;;) { + stack.push(point); + + let 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 (let i=0; i 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) { + let longest = null, + emitted = 0, + closed = 0, + open = 0; + + paths.forEach(function(path) { + // 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; + let 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) { + let 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) { + let 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 (let i=0; i { + console.log('terrain', index, total, data); + } }); + let shadow = []; + // shadow generation bottom-up is faster + terrain.reverse().forEach(data => { + if (data.tops) { + shadow = POLY.union(shadow.appendAll(data.tops)); + } + }); + console.log({slicer, zindex, shadow, terrain}); + // do roughing slices + if (procRough) + slicer.slice(slicer.interval(roughDown, { down: true }), { each: (data, index, total) => { + // annotate slice + console.log('rough', data.z, data); + } }); +*/ // horizontal slices for rough/outline doSlicing(widget, {height:sliceDepth, cam:true, zmin:zBottom, noEmpty:true}, camSlicesDone, function(update) { onupdate(0.0 + update * 0.25, "slicing");