diff --git a/src/kiri/codec.js b/src/kiri/codec.js index 1909d4a8..261dfc73 100644 --- a/src/kiri/codec.js +++ b/src/kiri/codec.js @@ -92,12 +92,6 @@ type: 'slice', z: this.z, index: this.index, - camMode: this.camMode, - tops: encode(this.tops, state), - bridges: encode(this.bridges, state), - flats: encode(this.flats, state), - solids: encode(this.solids, state), - supports: encode(this.supports, state), render: encode(this.render, state) }; }; @@ -106,12 +100,6 @@ let slice = KIRI.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); slice.index = v.index; - slice.camMode = v.camMode; - slice.tops = decode(v.tops, state); - slice.bridges = decode(v.bridges, state); - slice.flats = decode(v.flats, state); - slice.solids = decode(v.solids, state); - slice.supports = decode(v.supports, state); slice.render = decode(v.render, state) return slice; @@ -156,28 +144,28 @@ return { type: 'top', poly: encode(this.poly, state), - traces: encode(this.traces, state), - inner: encode(this.inner, state), - // thinner: encode(this.thinner, state), - solids: encode(this.solids, state), - thin_fill: encodePointArray(this.thin_fill, state), - fill_lines: encodePointArray(this.fill_lines, state), - fill_sparse: encode(this.fill_sparse, state), - polish: encode(this.polish, state) + // traces: encode(this.traces, state), + // inner: encode(this.inner, state), + // // thinner: encode(this.thinner, state), + // solids: encode(this.solids, state), + // thin_fill: encodePointArray(this.thin_fill, state), + // fill_lines: encodePointArray(this.fill_lines, state), + // fill_sparse: encode(this.fill_sparse, state), + // polish: encode(this.polish, state) }; }; registerDecoder('top', function(v, state) { let top = KIRI.newTop(decode(v.poly, state)); - top.traces = decode(v.traces, state); - top.inner = decode(v.inner, state); - // top.thinner = decode(v.thinner, state); - top.solids = decode(v.solids, state); - top.thin_fill = decodePointArray(v.thin_fill, state); - top.fill_lines = decodePointArray(v.fill_lines, state); - top.fill_sparse = decode(v.fill_sparse, state); - top.polish = decode(v.polish, state); + // top.traces = decode(v.traces, state); + // top.inner = decode(v.inner, state); + // // top.thinner = decode(v.thinner, state); + // top.solids = decode(v.solids, state); + // top.thin_fill = decodePointArray(v.thin_fill, state); + // top.fill_lines = decodePointArray(v.fill_lines, state); + // top.fill_sparse = decode(v.fill_sparse, state); + // top.polish = decode(v.polish, state); return top; }); diff --git a/src/kiri/init.js b/src/kiri/init.js index b0b17dad..4faff676 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -1595,21 +1595,6 @@ ]), layers: UC.setGroup($("layers")), - layerTrace: UC.newBoolean(LANG.la_trce, onLayerToggle, {modes:FDM_LASER}), - layerFacing: UC.newBoolean(LANG.la_face, onLayerToggle, {modes:[]}), // see sliceRender() in CAM - layerRough: UC.newBoolean(LANG.la_ruff, onLayerToggle, {modes:CAM}), - layerFinish: UC.newBoolean(LANG.la_fini, onLayerToggle, {modes:CAM}), - layerFinishX: UC.newBoolean(LANG.la_finx, onLayerToggle, {modes:CAM}), - layerFinishY: UC.newBoolean(LANG.la_finy, onLayerToggle, {modes:CAM}), - layerFill: UC.newBoolean(LANG.la_fill, onLayerToggle, {modes:FDM}), - layerSparse: UC.newBoolean(LANG.la_sprs, onLayerToggle, {modes:FDM}), - layerSupport: UC.newBoolean(LANG.la_sprt, onLayerToggle, {modes:FDM}), - layerOutline: UC.newBoolean(LANG.la_olin, onLayerToggle, {modes:GCODE}), - layerDelta: UC.newBoolean(LANG.la_dlta, onLayerToggle, {modes:FDM}), - layerSolid: UC.newBoolean(LANG.la_slds, onLayerToggle, {modes:FDM}), - layerPrint: UC.newBoolean(LANG.la_prnt, onLayerToggle, {modes:GCODE}), - layerMoves: UC.newBoolean(LANG.la_move, onLayerToggle, {modes:GCODE}), - layerParts: UC.newBoolean(LANG.la_part, onLayerToggle, {modes:CAM}) }); function showLaserKnife() { diff --git a/src/kiri/layer.js b/src/kiri/layer.js index ed969ca0..b80622ac 100644 --- a/src/kiri/layer.js +++ b/src/kiri/layer.js @@ -227,14 +227,6 @@ LP.noodle = function(poly, offset, color, traceColor, open) { if (!poly) return; - // if (!Array.isArray(poly)) { - // poly = [ poly ]; - // } - // poly.forEach(p => { - // this.add(color, {path: p, offset}); - // }); - // return; - if (Array.isArray(poly)) { poly = POLY.flatten(poly, [], true); } else { @@ -427,4 +419,120 @@ this.changed = false; } + Layer.renderSetup = function() { + const API = KIRI.api, UC = API.uc, UI = API.ui; + const dyn = UI.dyn = {}; + const layers = $("layers"); + UC.setGroup(layers); + layers.innerHTML = ''; + return Layer; + }; + + Layer.renderSlices = function(slices) { + if (slices && slices.length) { + slices.forEach(slice => Layer.renderSlice(slice)); + } + return Layer; + }; + + Layer.renderSlice = function(slice) { + let group = new THREE.Group(); + let render = slice.render; + let view = slice.view; + + if (!render) return; + + function addPoly(vertices, poly) { + const points = poly.points, len = points.length; + for (let i=1; i { + ctrl.group.forEach(mat => { + mat.visible = ctrl.toggle.checked; + }); + }) + }; + } + const ctrl = DYN[layer]; + ctrl.toggle.checked = true; + + const { polys, lines, faces, paths } = data; + if (polys.length || lines.length) { + const mat = new THREE.LineBasicMaterial({ color: data.color.line }); + const geo = new THREE.Geometry(), vert = geo.vertices; + for (let i=0, il=polys.length; i { + const { index, faces, z } = path; + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(faces, 3)); + geo.setIndex(index); + geo.computeFaceNormals(); + geo.computeVertexNormals(); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.z = z; + mesh.castShadow = true; + mesh.receiveShadow = true; + group.add(mesh); + }); + ctrl.group.push(mat); + } + } + + slice.view.add(group); + }; })(); diff --git a/src/kiri/main.js b/src/kiri/main.js index 74bcb283..7633c248 100644 --- a/src/kiri/main.js +++ b/src/kiri/main.js @@ -599,9 +599,6 @@ } function setWidgetVisibility(bool) { - if (bool) { - UI.layerParts.checked = true; - } forAllWidgets(w => { if (bool) { w.show(); @@ -621,10 +618,6 @@ return; } - if (MODE === MODES.CAM) { - setWidgetVisibility(UI.layerParts.checked); - } - showSlider(); if (typeof(layer) === 'string' || typeof(layer) === 'number') { @@ -638,9 +631,7 @@ API.var.layer_hi = layer; API.event.emit("slider.label"); - let print = UI.layerPrint.checked, - moves = UI.layerMoves.checked, - cam = MODE === MODES.CAM, + let cam = MODE === MODES.CAM, sla = MODE === MODES.SLA, hi = cam ? API.var.layer_max - API.var.layer_lo : API.var.layer_hi, lo = cam ? API.var.layer_max - API.var.layer_hi : API.var.layer_lo; @@ -648,7 +639,7 @@ updateSlider(); forAllWidgets(function(widget) { - if (print) return widget.hideSlices(); + if (currentPrint) return widget.hideSlices(); let slices = widget.slices; if (!slices) return; @@ -656,46 +647,16 @@ for (let j = 0; j < slices.length; j++) { let slice = slices[j]; slice.view.visible = j >= lo && j <= hi; - let layers = slice.layers; - layers.outline.setVisible(cam ? - UI.layerOutline.checked : - UI.layerOutline.checked - ); - layers.trace.setVisible(cam ? - UI.layerRough.checked : - UI.layerTrace.checked - ); - layers.bridge.setVisible(cam ? - UI.layerFinishX.checked : - UI.layerDelta.checked - ); - layers.flat.setVisible(cam ? - UI.layerFinishY.checked : - UI.layerDelta.checked - ); - layers.solid.setVisible(cam ? - UI.layerFinish.checked : - UI.layerSolid.checked - ); - layers.fill.setVisible(cam ? - UI.layerFacing.checked : - UI.layerFill.checked - ); - layers.sparse.setVisible(UI.layerSparse.checked); - layers.support.setVisible(UI.layerSupport.checked); } }); if (currentPrint) { let len = currentPrint.getLayerCount(); for (let j = 0; j < len; j++) { - currentPrint.showLayer(j, print && j >= lo && j <= hi, moves); + currentPrint.showLayer(j, print && j >= lo && j <= hi); } } - UI.layerPrint.parentNode.style.display = currentPrint && !sla ? '' : 'none'; - UI.layerMoves.parentNode.style.display = currentPrint && !sla ? '' : 'none'; - SPACE.update(); } @@ -810,7 +771,6 @@ SPACE.platform.remove(currentPrint.group); currentPrint = null; } - UI.layerPrint.checked = false; } function clearSlices() { diff --git a/src/kiri/render.js b/src/kiri/render.js index 684f55f7..ad694121 100644 --- a/src/kiri/render.js +++ b/src/kiri/render.js @@ -8,6 +8,7 @@ class Render { constructor() { this.layers = {}; + this.profiles = {}; } setLayer(layer, colors) { @@ -36,12 +37,35 @@ return this; } - addPoly(poly) { - this.current.polys.push(poly); + addLines(lines, options) { + if (options) { + for (let i=0; i { + poly.earcut().forEach(ep => { + ep.forEachPoint(p => { faces.push(p.x, p.y, p.z) }); + }); + }); + if (options && options.outline) { + this.addPolys(poly.clone()); + } + } + addFlats(polys, options) { const opts = options || {}; const offset = opts.offset || 1; @@ -81,18 +118,23 @@ return; } - const profile = new THREE.Shape(); - profile.moveTo(-offset, -offset); - profile.lineTo(-offset, offset); - profile.lineTo( offset, offset); - profile.lineTo( offset, -offset); + const profiles = this.profiles; + if (!profiles[offset]) { + const profile = new THREE.Shape(); + profile.moveTo(-offset, -offset); + profile.lineTo(-offset, offset); + profile.lineTo( offset, offset); + profile.lineTo( offset, -offset); + profiles[offset] = profile; + } + const profile = profiles[offset].clone(); polys.forEach(poly => { const contour = []; poly.points.forEach(p => { contour.push(new THREE.Vector2(p.x, p.y)); }); - const {index, faces} = ProfiledContourGeometry(profile, contour, true); + const {index, faces} = ProfiledContourGeometry(profile, contour, poly.isClosed()); this.current.paths.push({ index, faces, z: poly.getZ() }); }); return this; @@ -101,9 +143,9 @@ function flat(polys) { if (Array.isArray(polys)) { - return POLY.flatten(polys, [], true); + return POLY.flatten(polys.clone(true), [], true); } else { - return POLY.flatten([polys], [], true); + return POLY.flatten([polys.clone(true)], [], true); } } diff --git a/src/kiri/slice.js b/src/kiri/slice.js index 88aeb972..e5c55a04 100644 --- a/src/kiri/slice.js +++ b/src/kiri/slice.js @@ -7,15 +7,11 @@ if (self.kiri.Slice) return; const KIRI = self.kiri, - FILL = KIRI.fill, - FILLFIXED = KIRI.fill_fixed, PRO = Slice.prototype, BASE = self.base, UTIL = BASE.util, POLY = BASE.polygons, - NOKEY = BASE.key.NONE, - fillArea = POLY.fillArea, - newPoint = BASE.newPoint; + NOKEY = BASE.key.NONE; KIRI.Top = Top; KIRI.newTop = newTop; @@ -34,31 +30,14 @@ function Slice(z, view) { this.z = z; // z-index this.index = 0; // slice index - this.lines = null; // slice raw (for rendermode/debug only) - this.groups = null; // grouped lines (for rendermode/debug only) + this.lines = null; // slice raw + this.groups = null; // grouped lines this.up = null; // slice above (linked list) this.down = null; // slice below (linked list) this.tops = []; // array of Top objects this.view = view; // for rendering this slice - // bridge area polygons - this.bridges = null; - // flat area polygons - this.flats = null; - // areas requiring solid fill (from bridge/flats) - this.solids = { - poly: null, - trimmed: null - }; - this.offsets = null; // support clipping offsets - this.supports = null; // external support areas - this.isSolidFill = false; - this.isSparseFill = false; - this.camMode = null; // CAM mode - this.layers = null; this.finger = null; // cached fingerprint this.render = null; // will replace most of the layer output data - - if (view) this.addLayers(view); } /** @@ -71,16 +50,16 @@ */ function Top(polygon) { this.poly = polygon; // outline poly - this.traces = null; // array of offset/inset trace polygons (ordered outer to inner) - this.inner = null; // array of inner fillable areas (inset from last trace) - this.fill_lines = null; // solid fill lines (array of points) - this.fill_sparse = null; // sparse fill area open polygons (poly lines) - this.solids = null; // solid fill regions in otherwise sparse fill (from solids.trimmed) } + Top.prototype.clone = function(deep) { + let top = new Top(this.poly.clone(deep)); + return top; + }; + /** * return innermost traces under a given top. for FDM, this represents - * the outline shell that the fill touches. + * the outline shell that the fill touches. used by Print, Laser */ Top.prototype.innerTraces = function() { let traces = this.traces, @@ -91,13 +70,9 @@ return array; }; - Top.prototype.clone = function(deep) { - let top = new Top(this.poly.clone(deep)); - return top; - }; - /** - * Appends all outermost trace polygons into a given array and returns it + * Appends all outermost trace polygons into a given array + * and returns it. used by Print * * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons @@ -125,14 +100,37 @@ * returns a cloned slice the option of a deep clone on the top polys */ PRO.clone = function(deep) { - let from = this, - slice = newSlice(from.z, from.view); + const from = this, slice = newSlice(from.z, from.view); from.tops.forEach(function(top) { slice.addTop(top.poly.clone(deep)); }); return slice; }; + PRO.topPolys = function() { + return this.tops.map(top => top.poly); + }; + + // FDM / SLA only + PRO.topInners = function() { + return this.tops.map(top => top.last).flat().filter(poly => poly); + }; + + // FDM / SLA only + PRO.topFillOff = function() { + return this.tops.map(top => top.fill_off).flat().filter(poly => poly); + }; + + // FDM only + PRO.topFill = function() { + return this.tops.map(top => top.fill_lines).flat().filter(poly => poly); + }; + + // FDM only + PRO.topShells = function() { + return this.tops.map(top => top.shells).flat().filter(poly => poly); + }; + /** * produces a fingerprint for a slice that should be the same for * layers that are identical. this happens in parts with unchanging @@ -143,41 +141,14 @@ if (this.finger) { return this.finger; } - return this.finger = POLY.fingerprint(this.gatherTopPolys([])); + return this.finger = POLY.fingerprint(this.topPolys()); }; /** * returns true if the layers' fingerprints are the same */ PRO.fingerprintSame = function(slice) { - if (!slice) { - return false; - } - return POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()); - }; - - /** - * create layer objects for client-side rendering - * - * @param {THREE.Group} view - */ - PRO.addLayers = function(view) { - if (this.layers) return; - - // create views client side only - function nl() { return KIRI.newLayer(view) } - - // if to support creation in worker space - this.layers = { - outline: nl(), - trace: nl(), // also cam roughing - bridge: nl(), // also cam finishx - flat: nl(), // also cam finishy - solid: nl(), // also cam finish - fill: nl(), // also cam facing - sparse: nl(), - support: nl() - }; + return slice ? POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()) : false; }; /** @@ -185,18 +156,18 @@ * * @param {Polygon} poly to merge into a top */ - PRO.mergeTop = function(poly) { - let scope = this, - tops = scope.tops, - union, i; - for (i=0; i { @@ -222,12 +193,12 @@ * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons */ - PRO.gatherTopPolys = function(out) { - this.tops.forEach(function(top) { - out.push(top.poly); - }); - return out; - }; + // PRO.gatherTopPolys = function(out) { + // this.tops.forEach(function(top) { + // out.push(top.poly); + // }); + // return out; + // }; /** * Appends all inner trace inner polygons (holes) @@ -236,12 +207,12 @@ * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons */ - PRO.gatherTopPolyInners = function(out) { - this.tops.forEach(function(top) { - if (top.poly.inner) out.appendAll(top.poly.inner); - }); - return out; - }; + // PRO.gatherTopPolyInners = function(out) { + // this.tops.forEach(function(top) { + // if (top.poly.inner) out.appendAll(top.poly.inner); + // }); + // return out; + // }; /** * Appends all trace polygons into a given array and returns it @@ -249,12 +220,12 @@ * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons */ - PRO.gatherTraces = function(out) { - this.tops.forEach(function(top) { - out.appendAll(top.traces); - }); - return out; - }; + // PRO.gatherTraces = function(out) { + // this.tops.forEach(function(top) { + // out.appendAll(top.traces); + // }); + // return out; + // }; /** * Appends all innermost trace polygons into a given array and returns it @@ -262,12 +233,12 @@ * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons */ - PRO.gatherInner = function(out) { - this.tops.forEach(function(top) { - out.appendAll(top.inner); - }); - return out; - }; + // PRO.gatherInner = function(out) { + // this.tops.forEach(function(top) { + // out.appendAll(top.inner); + // }); + // return out; + // }; /** * Appends all solid area polygons into a given array and returns it @@ -275,12 +246,12 @@ * @param {Polygon[]} out array to populate * @returns {Polygon[]} array of top polygons */ - PRO.gatherSolids = function(out) { - this.tops.forEach(function(top) { - out.appendAll(top.solids); - }); - return out; - }; + // PRO.gatherSolids = function(out) { + // this.tops.forEach(function(top) { + // out.appendAll(top.solids); + // }); + // return out; + // }; /** * return all fill lines. includes points for solid layers, @@ -288,903 +259,12 @@ * * @param {Point[]} [lines] array to append to */ - PRO.gatherFillLines = function(lines) { - this.tops.forEach(function(top) { - if (top.fill_lines) lines.appendAll(top.fill_lines); - }); - return lines; - }; - - /** - * Clear solid area cache in preparation for a new slicing action - */ - PRO.invalidateSolids = function() { - let solids = this.solids; - solids.poly = []; - solids.trimmed = null; - }; - - /** - * Clear support cache in preparation for a new slicing calculation - */ - PRO.invalidateSupports = function() { - this.supports = null; - }; - - /** - * given an array of arrays of points (lines), eliminate intersections - * between groups, then return a unified array of shortest non-intersects. - * - * @returns {Point[]} - */ - function cullIntersections() { - function toLines(pts) { - let lns = []; - for (let i=0, il=pts.length; i t); - if (aOa.length < 1) return; - let aa = toLines(aOa.shift()); - while (aOa.length) { - let bb = toLines(aOa.shift()); - loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); - } - let good = []; - for (let i=0, il=aa.length; i 2 ? good : []; - } - - /** - * Compute offset shell polygons. For FDM, the first offset is usually half - * of the nozzle width. Each subsequent offset is a full nozzle width. User - * parameters control tweaks to these numbers to allow for better shell bonding. - * The last shell generated is a "fillOffset" shell. Fill lines are clipped to - * this polygon. Adjusting fillOffset controls bonding of infill to the shells. - * - * @param {number} count - * @param {number} offset1 first offset - * @param {number} offsetN all subsequent offsets - * @param {number} fillOffset - * @param {Obejct} options - */ - PRO.doShells = function(count, offset1, offsetN, fillOffset, options) { - let slice = this, - opt = options || {}; - - slice.tops.forEach(function(top) { - let top_poly = [ top.poly ]; - - if (slice.index === 0) { - // console.log({slice_top_0: top_poly, count}); - // segment polygon - } - - if (opt.vase) { - top.poly = top.poly.clone(false); - } - // top.thinner = []; - top.traces = []; - top.inner = []; - let last = [], - gaps = [], - z = top.poly.getZ(); - - if (count) { - // permit offset of 0 for laser and drag knife - if (offset1 === 0 && count === 1) { - last = top_poly.clone(true); - top.traces = last; - } else { - if (opt.thin) { - top.thin_fill = []; - let oso = {z, count, gaps: [], outs: [], minArea: 0.05}; - POLY.offset(top_poly, [-offset1, -offsetN], oso); - - oso.outs.forEach((polys, i) => { - polys.forEach(p => { - p.depth = i; - if (p.inner) { - p.inner.forEach(pi => pi.depth = i); - } - top.traces.push(p); - }); - last = polys; - }); - - // slice.solids.trimmed = slice.solids.trimmed || []; - oso.gaps.forEach((polys, i) => { - let off = (i == 0 ? offset1 : offsetN); - polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0}); - // polys.forEach(p => { slice.solids.trimmed.push(p); }); - top.thin_fill.appendAll(cullIntersections( - fillArea(polys, 45, off/2, [], 0.01, off*2), - fillArea(polys, 135, off/2, [], 0.01, off*2), - // fillArea(polys, 90, off, [], 0.05, off*4), - // fillArea(polys, 180, off, [], 0.05, off*4), - )); - gaps = polys; - }); - } else { - // standard wall offsetting strategy - POLY.expand( - top_poly, // reference polygon(s) - -offset1, // first inset distance - z, // set new polys to this z - top.traces, // accumulator array - count, // number of insets to perform - -offsetN, // subsequent inset distance - // on each new offset trace ... - function(polys, countNow) { - last = polys; - // mark each poly with depth (offset #) starting at 0 - polys.forEach(function(p) { - p.depth = count - countNow; - if (p.inner) p.inner.forEach(function(pi) { - // use negative offset for inners - pi.depth = -(count - countNow); - }); - }); - }); - } - } - } else { - // no shells, just infill, is permitted - last = [top.poly]; - } - - // generate fill offset poly set from last offset to top.inner - if (fillOffset && last.length > 0) { - // if gaps present, remove that area from fill inset - if (gaps.length) { - let nulast = []; - POLY.subtract(last, gaps, nulast, null, slice.z); - last = nulast; - } - last.forEach(function(inner) { - POLY.offset([inner], -fillOffset, {outs: top.inner, flat: true, z: slice.z}); - }); - } - }); - }; - - /** - * Clear fill cache in preparation for a slice or re-slice of a widget - */ - PRO.invalidateFill = function() { - this.tops.forEach(function(top) { - top.fill_lines = null; - top.fill_sparse = null; - }); - }; - - /** - * Calculate thin-wall sections and fill appropriately - * - * @param {number} minDist - */ - PRO.doThinWallDetection = function(mindist) { - this.tops.forEach(function(top) { - if (top.inner && top.inner.length > 0) { - // using next line2line algo from print lib - } - }); - }; - - /** - * Create an entirely solid layer by filling all top polygons - * with an alternating pattern. - * - * @param {number} linewidth - * @param {number} angle - * @param {number} density - */ - PRO.doSolidLayerFill = function(spacing, angle) { - this.isSolidFill = false; - - if (this.tops.length === 0) return; - if (typeof(angle) != 'number') return; - - this.tops.forEach(function(top) { - if (top.inner && top.inner.length > 0) { - top.fill_lines = fillArea(top.inner, angle, spacing, null); - } else { - top.fill_lines = null; - } - }); - - this.isSolidFill = true; - }; - - /** - * Take output from pluggable sparse infill algorithm and clip to - * the bounds of the top polygons and their inner solid areas. - */ - PRO.doSparseLayerFill = function(options) { - let process = options.process, - spacing = options.spacing, // spacing space between fill lines - density = options.density, // density of infill 0.0 - 1.0 - bounds = options.bounds, // bounding box of widget - height = options.height, // z layer height - type = options.type || 'hex'; - - this.isSparseFill = false; - if (this.tops.length === 0 || density === 0.0 || this.isSolidFill) { - return; - } - - let scope = this, - tops = scope.tops, - down = scope.down, - clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), - ctre = new clib.PolyTree(), - poly, - polys = [], - lines = [], - line = [], - solids = [], - // callback passed to pluggable infill algorithm - target = { - // slice and slice property access - slice: function() { return scope }, - zIndex: function() { return scope.index }, - zValue: function() { return scope.z }, - // various option map access - options: function() { return options }, - lineWidth: function() { return options.lineWidth }, - bounds: function() { return bounds }, - zHeight: function() { return height }, - offset: function() { return spacing }, - density: function() { return density }, - // output functions - emit: function(x,y) { - if (isNaN(x)) { - solids.push(x); - } else { - line.push(newPoint(x,y,scope.z)); - scope.isSparseFill = true; - } - }, - newline: function() { - if (line.length > 0) { - lines.push(line); - line = []; - } - } - }; - - // use specified fill type - if (type && FILL[type]) { - FILL[type](target); - } else { - console.log({missing_infill: type}); - return; - } - - // force emit of last line - target.newline(); - - // prepare top infill structure - tops.forEach(function(top) { - top.fill_sparse = []; - polys.appendAll(top.inner); - polys.appendAll(top.solids); - }); - - // update fill fingerprint for this slice - scope._fill_finger = POLY.fingerprint(polys); - - let skippable = FILLFIXED[type] ? true : false; - let miss = false; - // if the layer below has the same fingerprint, - // we may be able to clone the infill instead of regenerating it - if (skippable && scope.fingerprintSame(down)) { - // the fill fingerprint can slightly different because of solid projections - if (down._fill_finger && POLY.fingerprintCompare(scope._fill_finger, down._fill_finger)) { - for (let i=0; i { - return poly.clone().setZ(scope.z); - }); - } else { - miss = true; - } - } - // if any of the fills as missing from below, re-compute - if (!miss) { - return; - } - } - } - - let sparse_clip = scope.isSparseFill; - - // solid fill areas - if (solids.length) { - tops.forEach(top => { - if (!top.inner) return; - let masks = top.inner.slice(); - if (top.solids) { - masks = POLY.subtract(masks, top.solids, [], null, scope.z); - } - let angl = process.sliceFillAngle * ((scope.index % 2) + 1); - solids.forEach(solid => { - let inter = [], - fillable = []; - masks.forEach(mask => { - let p = solid.mask(mask); - if (p && p.length) inter.appendAll(p); - }); - // offset fill area to accommodate trace - if (inter.length) { - POLY.expand(inter, -options.lineWidth/2, scope.z, fillable); - } - // fill intersected areas - if (inter.length) { - scope.isSparseFill = true; - if (!top.fill_lines) { - top.fill_lines = []; - } - inter.forEach(p => { - p.forEachSegment((p1, p2) => { - top.fill_lines.push(p1, p2); - }); - }); - } - if (fillable.length) { - let lines = POLY.fillArea(fillable, angl, options.lineWidth); - top.fill_lines.appendAll(lines); - } - }); - }); - } - - // if only solids were added and no lines to clip - if (!sparse_clip) { - return; - } - - clip.AddPaths(lines, ptyp.ptSubject, false); - clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); - - if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { - ctre.m_AllPolys.forEach(function(node) { - poly = POLY.fromClipperNode(node, scope.z); - tops.forEach(function(top) { - // use only polygons inside this top - if (poly.isInside(top.poly)) { - top.fill_sparse.push(poly); - } - }); - }); - } - }; - - /** - * Find difference between fill inset poly on two adjacent layers. - * Used to calculate bridges, flats and then solid projections. - * 'expand' is used for top offsets in SLA mode - */ - PRO.doDiff = function(minArea, expand, fakedown) { - let down = this.down; - - if (!down) { - if (fakedown) { - down = newSlice(-1); - } else { - return; - } - } - - let top = this, - bottom = down; - - top.bridges = null; - bottom.flats = null; - - let topInner = expand ? top.gatherTopPolys([]) : top.gatherInner([]), - bottomInner = expand ? bottom.gatherTopPolys([]) : bottom.gatherInner([]), - bridges = [], - flats = []; - - // skip diffing layers that are identical - if (this.fingerprintSame(bottom)) { - top.bridges = bridges; - bottom.flats = flats; - return; - } - - POLY.subtract(topInner, bottomInner, bridges, flats, this.z, minArea); - - if (expand) { - top.bridges = []; - bottom.flats = []; - POLY.expand(bridges, expand, top.z, top.bridges, 1, null, null, 0.0001); - POLY.expand(flats, expand, top.z, bottom.flats, 1, null, null, 0.0001); - } else { - top.bridges = bridges; - bottom.flats = flats; - } - }; - - /** - * - * - * @param {Polygon[]} polys - */ - PRO.addSolidFills = function(polys) { - if (this.solids.poly) this.solids.poly.appendAll(polys); - }; - - /** - * project bottom flats down - */ - PRO.projectFlats = function(count) { - if (this.isSolidFill || !this.down || !this.flats) return; - projectSolid(this, this.flats, count, false, true); - }; - - /** - * project top bridges up - */ - PRO.projectBridges = function(count) { - if (this.isSolidFill || !this.up || !this.bridges) return; - projectSolid(this, this.bridges, count, true, true); - }; - - /** - * fill projected areas and store line data - * @return {boolean} true if filled, false if not - */ - PRO.doSolidsFill = function(spacing, angle, minArea) { - - let minarea = minArea || 1, - scope = this, - tops = scope.tops, - solids = scope.solids, - unioned = POLY.union(solids.poly), - isSLA = (spacing === undefined && angle === undefined); - - if (solids.length === 0) return false; - if (unioned.length === 0) return false; - - let masks, - trims = [], - inner = isSLA ? scope.gatherTopPolys([]) : scope.gatherInner([]); - - // trim each solid to the inner bounds - unioned.forEach(function(p) { - p.setZ(scope.z); - inner.forEach(function(i) { - if (p.del) return; - masks = p.mask(i); - if (masks && masks.length > 0) { - p.del = true; - trims.appendAll(masks); - } - }); - }); - - // then merge the resulting solids - solids.unioned = unioned; - solids.trimmed = trims; - - // clear old solids and make array for new - tops.forEach(function(top) { top.solids = [] }); - - // parent each solid polygon inside the smallest bounding top - trims.forEach(function(solid) { - tops.forEach(function(top) { - if (top.poly.overlaps(solid)) { - if (!solid.parent || solid.parent.area() > top.poly.area()) { - if (solid.areaDeep() < minarea) { - // console.log({i:scope.index,cull_solid:solid,area:solid.areaDeep()}); - return; - } - solid.parent = top.poly; - top.solids.push(solid); - } - } - }); - }); - - // for SLA to bypass line infill - if (isSLA) { - return true; - } - - // create empty filled line array for each top - tops.forEach(function(top) { - top.fill_lines = []; - let tofill = [], - angfill = []; - trims.forEach(function(solid) { - if (solid.parent === top.poly) { - if (solid.fillang) { - angfill.push(solid); - } else { - tofill.push(solid); - } - } - }); - if (tofill.length > 0) { - fillArea(tofill, angle, spacing, top.fill_lines); - top.fill_lines_norm = {angle:angle,spacing:spacing}; - } - if (angfill.length > 0) { - top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; - angfill.forEach(function(af) { - fillArea([af], af.fillang.angle + 45, spacing, top.fill_lines); - top.fill_lines_ang.list.push(af.fillang.angle + 45); - top.fill_lines_ang.poly.push(af.clone()); - }); - } - - }); - return true; - }; - - /** - * calculate external overhangs requiring support - * this is done bottom-up - * - * @param {number} minOffset trigger for unsupported distance - * @param {number} maxBridge max length before mid supports added - * @param {number} expand outer support clip - * @param {number} offset inner support clip - * @param {number} gap layers between supports and part - */ - PRO.doSupport = function(minOffset, maxBridge, expand, minArea, pillarSize, offset, gap) { - let min = minArea || 0.01, - size = (pillarSize || 1), - slice = this, - mergeDist = size * 3, // pillar merge dist - tops = slice.gatherTopPolys([]), - trimTo = tops; - - // creates outer clip offset from tops - if (expand) POLY.expand(tops, expand, slice.z, trimTo = []); - - // create inner clip offset from tops - POLY.expand(tops, offset, slice.z, slice.offsets = []); - - // skip support detection for bottom layer - if (!slice.down) return; - - let traces = POLY.flatten(slice.gatherTraces([])), - fill = slice.gatherFillLines([]), - points = [], - down = slice.down, - down_tops = down.gatherTopPolys([]), - down_traces = POLY.flatten(down.gatherTraces([])); - - // check if point is supported by layer below - function checkSupported(point) { - // skip points close to other support points - for (let i=0; i= maxBridge) { - let slope = p1.slopeTo(p2).factor(1/dist), - segs = Math.floor(dist / maxBridge) + 1, - seglen = dist / segs; - while (i < segs) { - checkSupported(p1.projectOnSlope(slope, i++ * seglen)); - } - } - if (poly) checkSupported(p2); - } - - // check trace line support needs - traces.forEach(function(trace) { - trace.forEachSegment(function(p1, p2) { checkLine(p1, p2, true) }); - }); - - let supports = []; - - // add offset solids to supports (or fill depending) - fill.forEachPair(function(p1,p2) { checkLine(p1, p2, false) }); - // if (top.bridges) POLY.expand(top.bridges, -maxBridge/2, top.z, supports, 1); - - // skip the rest if no points or supports - if (!(points.length || supports.length)) return; - - let pillars = []; - - // TODO project points down instead of unioned pillars - // TODO merge point/rect into hull of next nearest (up to maxBridge/2 away) - // TODO eliminate unions in favor of progress hulling (using previous w/nearness) - // TODO align pillar diamond along line (when doing line checks) - - // for each point, create a bounding rectangle - points.forEach(function(point) { - pillars.push(BASE.newPolygon().centerRectangle(point, size/2, size/2)); - }); - - // merge pillars and replace with convex hull of outer points (aka smoothing) - pillars = POLY.union(pillars).forEach(function(pillar) { - supports.push(BASE.newPolygon().createConvexHull(pillar.points)); - }); - - // return top.supports = supports; - // then union supports - supports = POLY.union(supports); - - // constrain support poly to top polys - supports = POLY.trimTo(supports, trimTo); - - let depth = 0; - while (down && supports.length > 0) { - down.supports = down.supports || []; - - let trimmed = [], - culled = []; - - // clip supports to shell offsets - POLY.subtract(supports, down.gatherTopPolys([]), trimmed, null, slice.z, min); - - // set depth hint on support polys for infill density - trimmed.forEach(function(trim) { - if (trim.area() < 0.1) return; - culled.push(trim.setZ(down.z)); - }); - - // exit when no more support polys exist - if (culled.length === 0) break; - - // new bridge polys for next pass (skip first layer below) - if (depth >= gap) down.supports.appendAll(culled); - - supports = culled; - down = down.down; - depth++; - } - - }; - - /** - * @param {number} linewidth - * @param {number} angle - * @param {number} density - * @param {number} offset - */ - PRO.doSupportFill = function(linewidth, density, minArea) { - // return; - let slice = this, - supports = slice.supports, - nsB = [], - nsC = [], - min = minArea || 0.1; - - // create support clip offset - // POLY.expand(slice.gatherTopPolys([]), offset, slice.z, slice.offsets = []); - - if (!supports) return; - - // union supports - supports = POLY.union(supports); - - // trim to clip offsets - POLY.subtract(supports, slice.offsets, nsB, null, slice.z, min); - supports = nsB; - - // also trim to lower offsets, if they exist - if (slice.down) { - POLY.subtract(nsB, slice.down.offsets, nsC, null, slice.z, min); - supports = nsC; - } - - if (supports) supports.forEach(function (poly) { - // angle based on width/height ratio - let angle = (poly.bounds.width() / poly.bounds.height() > 1) ? 90 : 0; - // calculate fill density - let spacing = linewidth * (1 / density); - // inset support poly for fill lines 33% of nozzle width - let inset = POLY.offset([poly], -linewidth/3, {flat: true, z: slice.z}); - // do the fill - if (inset.length > 0) { - fillArea(inset, angle, spacing, poly.fills = []); - } - return true; - }); - - // re-assign new supports back to slice - slice.supports = supports; - }; - - /** - * for printing output optimization - * calls down to the outermost shell in this slice - * - * @param {Point} target - * @return {Object} - */ - PRO.findClosestPointTo = function(target) { - let min, find; - - if (this.tops.length) { - this.tops.forEach(function(top) { - find = top.poly.findClosestPointTo(target); - if (!min || find.distance < min.distance) { - min = find; - } - }); - } else if (this.supports) { - this.supports.forEach(function(poly) { - find = poly.findClosestPointTo(target); - if (!min || find.distance < min.distance) { - min = find; - } - }); - } - - return min; - }; - - /** ****************************************************************** - * Connect to kiri and Helpers - ******************************************************************* */ - - /** - * - * @param {Point} p1 - * @param {Point} p2 - * @param {Polygon | Polygon[]} poly - * @param {number} [minDist2] square of min distance - * @returns {?Point} - */ - function lineCrossesPoly(p1, p2, poly, minDist2) { - let ip; - if (Array.isArray(poly)) { - for (let i=0; i 0) { - if (up) projectSolid(slice.up, polys, count-1, true, false); - else projectSolid(slice.down, polys, count-1, false, false); - } - } - - /** - * - * @param layer - * @param poly - * @param colors - * @param idx - * @param {boolean} [recurse] - * @param {boolean} [open] - * @returns {THREE.Line} - */ - function renderPolygon(layer, poly, colors, idx, recurse, open) { - layer.poly(poly, colors[idx % colors.length], recurse, open); - if (recurse && poly.inner) { - poly.inner.forEach(function(inner) { - renderPolygon(layer, inner, colors, idx + 1, false, open); - }); - } - } + // PRO.gatherFillLines = function(lines) { + // this.tops.forEach(function(top) { + // if (top.fill_lines) lines.appendAll(top.fill_lines); + // }); + // return lines; + // }; function newTop(poly) { return new Top(poly); diff --git a/src/mode/cam/client.js b/src/mode/cam/client.js index e384ab07..152c43a4 100644 --- a/src/mode/cam/client.js +++ b/src/mode/cam/client.js @@ -26,94 +26,13 @@ }); } - CAM.renderSlice = function(slice) { - let group = new THREE.Group(); - let render = slice.render; - let view = slice.view; - - function addPoly(vertices, poly) { - const points = poly.points, len = points.length; - for (let i=1; i { - const { index, faces, z } = path; - const geo = new THREE.BufferGeometry(); - geo.setAttribute('position', new THREE.BufferAttribute(faces, 3)); - geo.setIndex(index); - geo.computeFaceNormals(); - geo.computeVertexNormals(); - const mesh = new THREE.Mesh(geo, mat); - mesh.position.z = z; - mesh.castShadow = true; - mesh.receiveShadow = true; - group.add(mesh); - }); - } - } - - slice.view.add(group); - }; - CAM.sliceRender = function(widget) { + return KIRI.Layer.renderSetup().renderSlices(widget.slices); + let slices = widget.slices; if (!slices) return; slices.forEach(function(slice) { - if (slice.render) return CAM.renderSlice(slice); - let tops = slice.tops, layers = slice.layers, outline = layers.outline, diff --git a/src/mode/cam/slice.js b/src/mode/cam/slice.js index be2ce310..b58ec465 100644 --- a/src/mode/cam/slice.js +++ b/src/mode/cam/slice.js @@ -136,11 +136,6 @@ sliceDrillReg(settings, sliceAll, zThru); } - if (procDrill) { - maxToolDiam = Math.max(maxToolDiam, drillToolDiam); - sliceDrill(drillTool, tslices, sliceAll); - } - // identify through holes thruHoles = tshadow.map(p => p.inner || []).flat(); @@ -156,7 +151,9 @@ slice.z = z; slice.index = sliceIndex++; slice.camMode = PRO.LEVEL; - slice.tops[0].traces = POLY.setZ(facing.clone(), slice.z); + slice.output() + .setLayer("facing", {face: 0, line: 0}) + .addPolys(POLY.setZ(facing.clone(true), slice.z)); sliceAll.push(slice); } } @@ -226,7 +223,9 @@ } }); - slice.tops[0].traces = offset; + slice.output() + .setLayer("roughing", {face: 0, line: 0}) + .addPolys(offset); slice.index = sliceIndex++; updateOp(index, slices.length); }); @@ -299,12 +298,14 @@ } } - slice.tops[0].traces = offset; - slice.index = sliceIndex++; - if (addTabsOutline && slice.z <= zMin + tabHeight) { - addCutoutTabs(slice, outlineToolDiam, tabWidth, proc.camTabsCount, proc.camTabsAngle); + offset = addCutoutTabs(offset, slice.z, outlineToolDiam, tabWidth, proc.camTabsCount, proc.camTabsAngle); } + + slice.index = sliceIndex++; + slice.output() + .setLayer("outline", {face: 0, line: 0}) + .addPolys(offset); }); sliceAll.appendAll(slices); @@ -337,6 +338,11 @@ } } + if (procDrill) { + maxToolDiam = Math.max(maxToolDiam, drillToolDiam); + sliceDrill(drillTool, tslices, sliceAll); + } + // used in printSetup() widget.terrain = terrain; widget.maxToolDiam = maxToolDiam; @@ -347,7 +353,7 @@ // drilling op function sliceDrill(tool, slices, output) { let drills = [], - drillToolDiam = tool.flueDiameter(), + drillToolDiam = tool.fluteDiameter(), centerDiff = drillToolDiam * 0.1, area = (drillToolDiam/2) * (drillToolDiam/2) * Math.PI, areaDelta = area * 0.05; @@ -388,7 +394,9 @@ point.y = center.y; }); slice.camMode = PRO.DRILL; - slice.addTop(null).traces = [ drill ]; + slice.output() + .setLayer("drill", {face: 0, line: 0}) + .addPolys(drill); output.append(slice); }); } @@ -425,21 +433,23 @@ .append(point.clone().setZ(bounds.max.z - stock.z - mz))); }); slice.camMode = PRO.DRILL; - slice.addTop(null).traces = polys; + slice.output() + .setLayer("register", {face: 0, line: 0}) + .addPolys(polys); output.append(slice); } } // cut outside traces at the right points - function addCutoutTabs(slice, toolDiam, tabWidth, tabCount, tabAngle) { + function addCutoutTabs(offset, z, toolDiam, tabWidth, tabCount, tabAngle) { // skip if no tops | traces - if (slice.tops.length === 0) return; + if (offset.length === 0) return offset; let notabs = 0; let nutrace = []; // find trace with greatest area - slice.tops[0].traces.forEach(function(trace, index) { + offset.forEach(function(trace, index) { // required to match computed order of cutouts trace.setClockwise(); @@ -447,7 +457,7 @@ let count = tabCount, angle = tabAngle, angle_inc = 360 / count, - center = trace.bounds.center(slice.z), + center = trace.bounds.center(z), offset = (tabWidth + toolDiam) / 2, ints = [], segs = []; @@ -498,7 +508,7 @@ console.log(`unable to compute tabs for ${notabs} traces @ z=${slice.z}`); } - slice.tops[0].traces = nutrace; + return nutrace; } })(); diff --git a/src/mode/cam/topo.js b/src/mode/cam/topo.js index 7aa0fa02..b10ee8e7 100644 --- a/src/mode/cam/topo.js +++ b/src/mode/cam/topo.js @@ -184,7 +184,7 @@ slice.lines = newlines = []; newtop = slice.addTop(newPolygon().setOpen()).poly; newtrace = newPolygon().setOpen(); - sliceout = slice.tops[0].traces = [ ]; + sliceout = []; for (y = minY; y < maxY; y += resolution) { if (pocketOnly && (data[gridx * stepsy + gridy] || 0) === 0) { end_poly(); @@ -224,6 +224,9 @@ end_poly(); if (sliceout.length > 0) { newslices.push(slice); + slice.output() + .setLayer("contour x", {face: 0, line: 0}) + .addPolys(sliceout); } onupdate(++stepsTaken, stepsTotal, "contour x"); } @@ -241,7 +244,7 @@ slice.lines = newlines = []; newtop = slice.addTop(newPolygon().setOpen()).poly; newtrace = newPolygon().setOpen(); - sliceout = slice.tops[0].traces = [ ]; + sliceout = []; for (x = minX; x <= maxX; x += resolution) { if (pocketOnly && (data[gridx * stepsy + gridy] || 0) === 0) { end_poly(); @@ -281,6 +284,9 @@ end_poly(); if (sliceout.length > 0) { newslices.push(slice); + slice.output() + .setLayer("contour y", {face: 0, line: 0}) + .addPolys(sliceout); } onupdate(++stepsTaken, stepsTotal, "contour y"); } diff --git a/src/mode/fdm/client.js b/src/mode/fdm/client.js index cede8c01..fd68251a 100644 --- a/src/mode/fdm/client.js +++ b/src/mode/fdm/client.js @@ -29,6 +29,8 @@ * @param {number} mode to use to select colors */ FDM.sliceRender = function(widget) { + return KIRI.Layer.renderSetup().renderSlices(widget.slices); + let slices = widget.slices; let settings = widget.settings; let extnum = widget.getExtruder(settings); diff --git a/src/mode/fdm/slice.js b/src/mode/fdm/slice.js index 0a320d69..fcc7bad8 100644 --- a/src/mode/fdm/slice.js +++ b/src/mode/fdm/slice.js @@ -6,10 +6,21 @@ let KIRI = self.kiri, BASE = self.base, + POLY = BASE.polygons, UTIL = BASE.util, CONF = BASE.config, FDM = KIRI.driver.FDM, - SLICER = KIRI.slicer; + SLICER = KIRI.slicer, + fillArea = POLY.fillArea, + newPoint = BASE.newPoint, + newSlice = KIRI.newSlice, + FILL = KIRI.fill, + FILLFIXED = KIRI.fill_fixed, + isThin = false; + + function thin(opt) { + return isThin ? null : opt; + } /** * DRIVER SLICE CONTRACT @@ -45,6 +56,8 @@ sliceFillAngle = spro.sliceFillAngle, view = widget.mesh && widget.mesh.newGroup ? widget.mesh.newGroup() : null; + isThin = settings.controller.thinRender; + if (!(sliceHeight > 0 && sliceHeight < 100)) { return ondone("invalid slice height"); } @@ -109,9 +122,6 @@ // reset (if necessary) for solids and support projections slices.forEach(function(slice) { slice.extruder = extruder; - slice.invalidateFill(); - slice.invalidateSolids(); - slice.invalidateSupports(); }); let supportEnabled = spro.sliceSupportEnable && spro.sliceSupportDensity > 0.0, @@ -124,25 +134,27 @@ slice.index > slices.length - spro.sliceTopLayers-1 || spro.sliceFillSparse > 0.95 ) && !vaseMode; - slice.doShells(spro.sliceShells, firstOffset, shellOffset, fillOffset, { + doShells(slice, spro.sliceShells, firstOffset, shellOffset, fillOffset, { vase: vaseMode, thin: spro.detectThinWalls }); - if (solid) slice.doSolidLayerFill(fillSpacing, sliceFillAngle); + if (solid) { + doSolidLayerFill(slice, fillSpacing, sliceFillAngle); + } sliceFillAngle += 90.0; }, "offsets"); // calculations only relevant when solid layers are used if (doSolidLayers) { forSlices(0.2, 0.34, function(slice) { - slice.doDiff(minSolid); + if (slice.index > 0) doDiff(slice, minSolid); }, "diff"); forSlices(0.34, 0.35, function(slice) { - slice.projectFlats(solidLayers); - slice.projectBridges(solidLayers); + projectFlats(slice, solidLayers); + projectBridges(slice, solidLayers); }, "solids"); forSlices(0.35, 0.5, function(slice) { - slice.doSolidsFill(fillSpacing, sliceFillAngle, minSolid); + doSolidsFill(slice, fillSpacing, sliceFillAngle, minSolid); sliceFillAngle += 90.0; }, "solids"); } @@ -150,17 +162,17 @@ // calculations only relevant when supports are enabled if (supportEnabled) { forSlices(0.5, 0.7, function(slice) { - slice.doSupport(spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset, spro.sliceSupportGap); + if (slice.index > 0) doSupport(slice, spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset, spro.sliceSupportGap); }, "support"); forSlices(0.7, 0.8, function(slice) { - slice.doSupportFill(nozzleSize, spro.sliceSupportDensity, supportMinArea); + doSupportFill(slice, nozzleSize, spro.sliceSupportDensity, supportMinArea); }, "support"); } // sparse layers only present when non-vase mose and sparse % > 0 if (!vaseMode && spro.sliceFillSparse > 0.0) { forSlices(0.8, 1.0, function(slice) { - slice.doSparseLayerFill({ + doSparseLayerFill(slice, { settings: settings, process: spro, device: sdev, @@ -369,4 +381,763 @@ return Math.max(min,Math.min(max,v)); } + + /** + * Compute offset shell polygons. For FDM, the first offset is usually half + * of the nozzle width. Each subsequent offset is a full nozzle width. User + * parameters control tweaks to these numbers to allow for better shell bonding. + * The last shell generated is a "fillOffset" shell. Fill lines are clipped to + * this polygon. Adjusting fillOffset controls bonding of infill to the shells. + * + * @param {number} count + * @param {number} offset1 first offset + * @param {number} offsetN all subsequent offsets + * @param {number} fillOffset + * @param {Obejct} options + */ + function doShells(slice, count, offset1, offsetN, fillOffset, options) { + const opt = options || {}; + + slice.solids = []; + + slice.tops.forEach(function(top) { + let top_poly = [ top.poly ]; + + if (slice.index === 0) { + // console.log({slice_top_0: top_poly, count}); + // segment polygon + } + + if (opt.vase) { + // remove top poly inners in vase mode + top.poly = top.poly.clone(false); + } + + top.shells = []; + top.fill_off = []; + top.fill_lines = []; + + let last = [], + gaps = [], + z = top.poly.getZ(); + + if (count) { + // permit offset of 0 for laser and drag knife + if (offset1 === 0 && count === 1) { + last = top_poly.clone(true); + top.shells = last; + } else { + if (opt.thin) { + top.thin_fill = []; + let oso = {z, count, gaps: [], outs: [], minArea: 0.05}; + POLY.offset(top_poly, [-offset1, -offsetN], oso); + + oso.outs.forEach((polys, i) => { + polys.forEach(p => { + p.depth = i; + if (p.fill_off) { + p.fill_off.forEach(pi => pi.depth = i); + } + top.shells.push(p); + }); + last = polys; + }); + + // slice.solids.trimmed = slice.solids.trimmed || []; + oso.gaps.forEach((polys, i) => { + let off = (i == 0 ? offset1 : offsetN); + polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0}); + // polys.forEach(p => { slice.solids.trimmed.push(p); }); + top.thin_fill.appendAll(cullIntersections( + fillArea(polys, 45, off/2, [], 0.01, off*2), + fillArea(polys, 135, off/2, [], 0.01, off*2), + // fillArea(polys, 90, off, [], 0.05, off*4), + // fillArea(polys, 180, off, [], 0.05, off*4), + )); + gaps = polys; + }); + } else { + // standard wall offsetting strategy + POLY.expand( + top_poly, // reference polygon(s) + -offset1, // first inset distance + z, // set new polys to this z + top.shells, // accumulator array + count, // number of insets to perform + -offsetN, // subsequent inset distance + // on each new offset trace ... + function(polys, countNow) { + last = polys; + // mark each poly with depth (offset #) starting at 0 + polys.forEach(function(p) { + p.depth = count - countNow; + if (p.fill_off) p.fill_off.forEach(function(pi) { + // use negative offset for inners + pi.depth = -(count - countNow); + }); + }); + }); + } + } + } else { + // no shells, just infill, is permitted + last = [top.poly]; + } + + // generate fill offset poly set from last offset to top.fill_off + if (fillOffset && last.length > 0) { + // if gaps present, remove that area from fill inset + if (gaps.length) { + let nulast = []; + POLY.subtract(last, gaps, nulast, null, slice.z); + last = nulast; + } + last.forEach(function(inner) { + POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z: slice.z}); + }); + } + + // for diffing + top.last = last; + + slice.output() + .setLayer('shells', { face: 0x0000aa, line: 0x0000aa }) + .addPolys(top.shells, thin({ offset: offset1 })) + .setLayer('slice', { face: 0, line: 0x888800 }) + .addPolys(top.poly) + .setLayer('offset', { face: 0, line: 0x888888 }) + .addPolys(top.fill_off) + .setLayer('last', { face: 0, line: 0x008888 }) + .addPolys(top.last) + ; + }); + }; + + /** + * Create an entirely solid layer by filling all top polygons + * with an alternating pattern. + * + * @param {number} linewidth + * @param {number} angle + * @param {number} density + */ + function doSolidLayerFill(slice, spacing, angle) { + if (slice.tops.length === 0 || typeof(angle) != 'number') { + slice.isSolidLayer = false; + return; + } + + const render = slice.output(); + + slice.tops.forEach(function(top) { + const lines = fillArea(top.fill_off, angle, spacing, null); + top.fill_lines.appendAll(lines); + render + .setLayer('fill', { face: 0x0000aa, line: 0x0000aa }) + .addLines(lines, thin({ offset: spacing/2 })); + }); + + slice.isSolidLayer = true; + }; + + /** + * Take output from pluggable sparse infill algorithm and clip to + * the bounds of the top polygons and their inner solid areas. + */ + function doSparseLayerFill(slice, options) { + let process = options.process, + spacing = options.spacing, // spacing space between fill lines + density = options.density, // density of infill 0.0 - 1.0 + bounds = options.bounds, // bounding box of widget + height = options.height, // z layer height + type = options.type || 'hex'; + + if (slice.tops.length === 0 || density === 0.0 || slice.isSolidLayer) { + slice.isSparseFill = false; + return; + } + + let tops = slice.tops, + down = slice.down, + clib = self.ClipperLib, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + poly, + polys = [], + lines = [], + line = [], + solids = [], + // callback passed to pluggable infill algorithm + target = { + // slice and slice property access + slice: function() { return slice }, + zIndex: function() { return slice.index }, + zValue: function() { return slice.z }, + // various option map access + options: function() { return options }, + lineWidth: function() { return options.lineWidth }, + bounds: function() { return bounds }, + zHeight: function() { return height }, + offset: function() { return spacing }, + density: function() { return density }, + // output functions + emit: function(x,y) { + if (isNaN(x)) { + solids.push(x); + } else { + line.push(newPoint(x,y,slice.z)); + slice.isSparseFill = true; + } + }, + newline: function() { + if (line.length > 0) { + lines.push(line); + line = []; + } + } + }; + + // use specified fill type + if (type && FILL[type]) { + FILL[type](target); + } else { + console.log({missing_infill: type}); + return; + } + + // force emit of last line + target.newline(); + + // prepare top infill structure + tops.forEach(function(top) { + top.fill_sparse = []; + polys.appendAll(top.fill_off); + polys.appendAll(top.solids); + }); + + function ondone() { + tops.forEach(top => { + slice.output() + .setLayer('infill', { face: 0x0000aa, line: 0x0000aa }) + .addPolys(top.fill_sparse, thin({ offset: spacing/2 })) + }); + } + + // update fill fingerprint for this slice + slice._fill_finger = POLY.fingerprint(polys); + + let skippable = FILLFIXED[type] ? true : false; + let miss = false; + // if the layer below has the same fingerprint, + // we may be able to clone the infill instead of regenerating it + if (skippable && slice.fingerprintSame(down)) { + // the fill fingerprint can slightly different because of solid projections + if (down._fill_finger && POLY.fingerprintCompare(slice._fill_finger, down._fill_finger)) { + for (let i=0; i { + return poly.clone().setZ(slice.z); + }); + } else { + miss = true; + } + } + // if any of the fills as missing from below, re-compute + if (!miss) { + ondone(); + return; + } + } + } + + let sparse_clip = slice.isSparseFill; + + // solid fill areas + if (solids.length) { + tops.forEach(top => { + if (!top.fill_off) return; + let masks = top.fill_off.slice(); + if (top.solids) { + masks = POLY.subtract(masks, top.solids, [], null, slice.z); + } + let angl = process.sliceFillAngle * ((slice.index % 2) + 1); + solids.forEach(solid => { + let inter = [], + fillable = []; + masks.forEach(mask => { + let p = solid.mask(mask); + if (p && p.length) inter.appendAll(p); + }); + // offset fill area to accommodate trace + if (inter.length) { + POLY.expand(inter, -options.lineWidth/2, slice.z, fillable); + } + // fill intersected areas + if (inter.length) { + slice.isSparseFill = true; + inter.forEach(p => { + p.forEachSegment((p1, p2) => { + top.fill_lines.push(p1, p2); + }); + }); + } + if (fillable.length) { + let lines = POLY.fillArea(fillable, angl, options.lineWidth); + top.fill_lines.appendAll(lines); + } + }); + }); + } + + // if only solids were added and no lines to clip + if (!sparse_clip) { + ondone(); + return; + } + + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + ctre.m_AllPolys.forEach(function(node) { + poly = POLY.fromClipperNode(node, slice.z); + tops.forEach(function(top) { + // use only polygons inside this top + if (poly.isInside(top.poly)) { + top.fill_sparse.push(poly); + } + }); + }); + } + + ondone(); + }; + + /** + * Find difference between fill inset poly on two adjacent layers. + * Used to calculate bridges, flats and then solid projections. + * 'expand' is used for top offsets in SLA mode + */ + function doDiff(slice, minArea, sla) { + const top = slice, + down = slice.down, + topInner = sla ? top.topPolys() : top.topInners(), + downInner = sla ? down.downPolys() : down.topInners(), + bridges = top.bridges = [], + flats = down.flats = []; + + // skip diffing layers that are identical + if (slice.fingerprintSame(down)) { + top.bridges = bridges; + down.flats = flats; + return; + } + + POLY.subtract(topInner, downInner, bridges, flats, slice.z, minArea); + + top.output() + .setLayer('bridges', { face: 0x00aaaa, line: 0x00aaaa }) + .addAreas(bridges) + + down.output() + .setLayer('flats', { face: 0xaa00aa, line: 0xaa00aa }) + .addAreas(flats) + }; + + /** + * + * + * @param {Polygon[]} polys + */ + function addSolidFills(slice, polys) { + slice.solids.appendAll(polys); + }; + + /** + * project bottom flats down + */ + function projectFlats(slice, count) { + if (slice.isSolidLayer || !slice.down || !slice.flats) return; + projectSolid(slice, slice.flats, count, false, true); + }; + + /** + * project top bridges up + */ + function projectBridges(slice, count) { + if (slice.isSolidLayer || !slice.up || !slice.bridges) return; + projectSolid(slice, slice.bridges, count, true, true); + }; + + /** + * fill projected areas and store line data + * @return {boolean} true if filled, false if not + */ + function doSolidsFill(slice, spacing, angle, minArea) { + + const render = slice.output(); + + let minarea = minArea || 1, + tops = slice.tops, + solids = slice.solids, + unioned = POLY.union(solids), + isSLA = (spacing === undefined && angle === undefined); + + if (solids.length === 0) return false; + if (unioned.length === 0) return false; + + let masks, + trims = [], + inner = isSLA ? slice.topPolys() : slice.topFillOff(); + + // trim each solid to the inner bounds + unioned.forEach(function(p) { + p.setZ(slice.z); + inner.forEach(function(i) { + if (p.del) return; + masks = p.mask(i); + if (masks && masks.length > 0) { + p.del = true; + trims.appendAll(masks); + } + }); + }); + + // clear old solids and make array for new + tops.forEach(top => { top.solids = [] }); + + // replace solids with merged and trimmed solids + slice.solids = solids = trims; + + // parent each solid polygon inside the smallest bounding top + solids.forEach(function(solid) { + tops.forEach(function(top) { + if (top.poly.overlaps(solid)) { + if (!solid.parent || solid.parent.area() > top.poly.area()) { + if (solid.areaDeep() < minarea) { + // console.log({i:slice.index,cull_solid:solid,area:solid.areaDeep()}); + return; + } + solid.parent = top.poly; + top.solids.push(solid); + } + } + }); + }); + // emit solid areas + render.setLayer("solids", { face: 0x00dd00 }).addAreas(solids); + + // for SLA to bypass line infill + if (isSLA) { + return true; + } + + // create empty filled line array for each top + tops.forEach(function(top) { + const tofill = []; + const angfill = []; + const newfill = []; + // determine fill orientation from top + solids.forEach(function(solid) { + if (solid.parent === top.poly) { + if (solid.fillang) { + angfill.push(solid); + } else { + tofill.push(solid); + } + } + }); + if (tofill.length > 0) { + fillArea(tofill, angle, spacing, newfill); + top.fill_lines_norm = {angle:angle,spacing:spacing}; + } + if (angfill.length > 0) { + top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; + angfill.forEach(function(af) { + fillArea([af], af.fillang.angle + 45, spacing, newfill); + top.fill_lines_ang.list.push(af.fillang.angle + 45); + top.fill_lines_ang.poly.push(af.clone()); + }); + } + + top.fill_lines.appendAll(newfill); + render.setLayer("fill", { line: 0, face: 0 }).addLines(newfill); + }); + + return true; + }; + + /** + * calculate external overhangs requiring support + * this is done bottom-up + * + * @param {number} minOffset trigger for unsupported distance + * @param {number} maxBridge max length before mid supports added + * @param {number} expand outer support clip + * @param {number} offset inner support clip + * @param {number} gap layers between supports and part + */ + function doSupport(slice, minOffset, maxBridge, expand, minArea, pillarSize, offset, gap) { + let min = minArea || 0.01, + size = (pillarSize || 1), + mergeDist = size * 3, // pillar merge dist + tops = slice.topPolys(), + trimTo = tops; + + // creates outer clip offset from tops + if (expand) { + POLY.expand(tops, expand, slice.z, trimTo = []); + } + + // create inner clip offset from tops + POLY.expand(tops, offset, slice.z, slice.offsets = []); + + let traces = POLY.flatten(slice.topShells().clone(true)), + fill = slice.topFill(), + points = [], + down = slice.down, + down_tops = down.topPolys(), + down_traces = POLY.flatten(down.topShells().clone(true)); + + // check if point is supported by layer below + function checkPointSupport(point) { + // skip points close to other support points + for (let i=0; i= maxBridge) { + let slope = p1.slopeTo(p2).factor(1/dist), + segs = Math.floor(dist / maxBridge) + 1, + seglen = dist / segs; + while (i < segs) { + checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); + } + } + if (poly) checkPointSupport(p2); + } + + // check trace line support needs + traces.forEach(function(trace) { + trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); + }); + + let supports = []; + + // add offset solids to supports (or fill depending) + fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); + // if (top.bridges) POLY.expand(top.bridges, -maxBridge/2, top.z, supports, 1); + + // skip the rest if no points or supports + if (!(points.length || supports.length)) return; + + let pillars = []; + + // TODO project points down instead of unioned pillars + // TODO merge point/rect into hull of next nearest (up to maxBridge/2 away) + // TODO eliminate unions in favor of progress hulling (using previous w/nearness) + // TODO align pillar diamond along line (when doing line checks) + + // for each point, create a bounding rectangle + points.forEach(function(point) { + pillars.push(BASE.newPolygon().centerRectangle(point, size/2, size/2)); + }); + + // merge pillars and replace with convex hull of outer points (aka smoothing) + pillars = POLY.union(pillars).forEach(function(pillar) { + supports.push(BASE.newPolygon().createConvexHull(pillar.points)); + }); + + // return top.supports = supports; + // then union supports + supports = POLY.union(supports); + + // constrain support poly to top polys + supports = POLY.trimTo(supports, trimTo); + + let depth = 0; + while (down && supports.length > 0) { + down.supports = down.supports || []; + + let trimmed = [], + culled = []; + + // clip supports to shell offsets + POLY.subtract(supports, down.topPolys(), trimmed, null, slice.z, min); + + // set depth hint on support polys for infill density + trimmed.forEach(function(trim) { + if (trim.area() < 0.1) return; + culled.push(trim.setZ(down.z)); + }); + + // exit when no more support polys exist + if (culled.length === 0) break; + + // new bridge polys for next pass (skip first layer below) + if (depth >= gap) { + down.supports.appendAll(culled); + // down.output() + // .setLayer("support", { line: 0xff0000 }) + // .addPolys(culled, thin({ offset: 0.4 })) + } + + supports = culled; + down = down.down; + depth++; + } + + }; + + /** + * @param {number} linewidth + * @param {number} angle + * @param {number} density + * @param {number} offset + */ + function doSupportFill(slice, linewidth, density, minArea) { + let supports = slice.supports, + nsB = [], + nsC = [], + min = minArea || 0.1; + + // create support clip offset + // POLY.expand(slice.gatherTopPolys([]), offset, slice.z, slice.offsets = []); + + if (!supports) return; + + // union supports + supports = POLY.union(supports); + + // trim to clip offsets + if (slice.offsets) POLY.subtract(supports, slice.offsets, nsB, null, slice.z, min); + supports = nsB; + + // also trim to lower offsets, if they exist + if (slice.down && slice.down.offsets) { + POLY.subtract(nsB, slice.down.offsets, nsC, null, slice.z, min); + supports = nsC; + } + + if (supports) supports.forEach(function (poly) { + // angle based on width/height ratio + let angle = (poly.bounds.width() / poly.bounds.height() > 1) ? 90 : 0; + // calculate fill density + let spacing = linewidth * (1 / density); + // inset support poly for fill lines 33% of nozzle width + let inset = POLY.offset([poly], -linewidth/3, {flat: true, z: slice.z}); + // do the fill + if (inset.length > 0) { + fillArea(inset, angle, spacing, poly.fills = []); + if (poly.fills.length); + slice.output() + .setLayer("support", { line: 0xff0000 }) + .addLines(poly.fills, thin({ offset: 0.4 })) + } + return true; + }); + + // re-assign new supports back to slice + slice.supports = supports; + + slice.output() + .setLayer("support", { line: 0xff0000 }) + .addPolys(supports, thin({ offset: 0.4 })) + }; + + /** + * + * @param {Slice} slice + * @param {Polygon[]} polys + * @param {number} count + * @param {boolean} up + * @param {boolean} first + * @returns {*} + */ + function projectSolid(slice, polys, count, up, first) { + if (!slice || slice.isSolidLayer || count <= 0) { + return; + } + let clones = polys.clone(true); + if (first) { + clones.forEach(function(p) { + p.hintFillAngle(); + }); + } + addSolidFills(slice, clones); + if (count > 0) { + if (up) projectSolid(slice.up, polys, count-1, true, false); + else projectSolid(slice.down, polys, count-1, false, false); + } + } + + /** + * given an array of arrays of points (lines), eliminate intersections + * between groups, then return a unified array of shortest non-intersects. + * + * @returns {Point[]} + */ + function cullIntersections() { + function toLines(pts) { + let lns = []; + for (let i=0, il=pts.length; i t); + if (aOa.length < 1) return; + let aa = toLines(aOa.shift()); + while (aOa.length) { + let bb = toLines(aOa.shift()); + loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); + } + let good = []; + for (let i=0, il=aa.length; i 2 ? good : []; + } + })();