diff --git a/src/geo/base.js b/src/geo/base.js index fbe310c3..9be730d7 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -307,6 +307,8 @@ }; BASE.config = { + // size of gcode debug arrow head + debug_arrow : 0.25, // default # of decimal places in generated gcode gcode_decimals : 4, // heal disjoint polygons in slicing (experimental) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 2c6012fb..10f2bb58 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -25,8 +25,8 @@ fillArea : fillArea, subtract : subtract, flatten : flatten, + offset : offset, trimTo : trimTo, - expand2 : expand2, expand : expand, expand_lines: expand_lines, union : union, @@ -46,6 +46,10 @@ * Polygon array utility functions ******************************************************************* */ + function numOrDefault(num, def) { + return num !== undefined ? num : def; + } + function toClipper(polys,debug) { let out = []; polys.forEach(function(poly) { poly.toClipper(out,debug) }); @@ -62,9 +66,9 @@ }; function fromClipperTree(tnode, z, tops, parent, minarea) { - let polys = tops || [], - poly, - min = minarea || 0.1; + let poly, + polys = tops || [], + min = numOrDefault(minarea, 0.1); tnode.m_Childs.forEach(function(child) { poly = fromClipperNode(child, z); @@ -473,75 +477,79 @@ } /** - * by "over" expanding then contracting, this causes shells that are too - * close together to merge and cancel out. it's a more expensive operation - * but prevents shells that are too close together to extrude properly. - * - * @param {Polygon[]} polys - * @param {number} dist1 first offset distance - * @param {number} dist2 2nd thru last offset distance - * @param {Polygon[]} out optional collector - * @param {number} count offset passes (0 == until no space left) - * @param {Function} collector receives output of each pass - * @param {Function} thins receives output of each pass - * @param {number} [z] defaults to 0 - * @returns {Polygon[]} last offset + * 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 expand2(polys, dist1, dist2, out, count, collector, thins, z) { - // prepare alignments for clipper lib + function offset(polys, dist, opt) { + // cause inner / outer polys to be reversed from each other alignWindings(polys); polys.forEach(function(poly) { - if (poly.inner) setWinding(poly.inner, !poly.isClockwise()); + if (poly.inner) { + setWinding(poly.inner, !poly.isClockwise()); + } }); - let fact = CONF.clipper, - clib = self.ClipperLib, - clip = clib.Clipper, - cpft = clib.PolyFillType, - cjnt = clib.JoinType, - cety = clib.EndType, - coff = new clib.ClipperOffset(), - ctre = new clib.PolyTree(), - orig = polys, - over = dist1 * 0.45; + let orig = polys, + opts = opt || {}, + depth = numOrDefault(opt.depth, 0), + clean = opts.clean !== false, + simple = opts.simple !== false, + fill = opts.fill || ClipperLib.PolyFillType.pftNonZero, + join = opts.join || ClipperLib.JoinType.jtMiter, + type = opts.type || ClipperLib.EndType.etClosedPolygon, + coff = new ClipperLib.ClipperOffset(), + ctre = new ClipperLib.PolyTree(), + // 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 = opt.z || 0; - // inset + // setup offset polys.forEach(function(poly) { - let clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean); - let simple = clip.SimplifyPolygons(clean, cpft.pftNonZero); - coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon); + // 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); - coff.Execute(ctre, (dist1 + over) * fact); - polys = fromClipperTree(ctre, z); + // if specified, perform offset gap analysis + if (opt.gaps && polys.length) { + let negoff = offset(polys, -offs, { + fill: opt.fill, join: opt.join, type: opt.type, z: opt.z, minArea: mina + }); + let suba = []; + let diff = subtract(orig, negoff, suba, null, zed); + opt.gaps.push(suba); + } - // outset - coff = new clib.ClipperOffset(); - ctre = new clib.PolyTree(); - polys.forEach(function(poly) { - let clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean); - let simple = clip.SimplifyPolygons(clean, cpft.pftNonZero); - coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon); - }); - coff.Execute(ctre, -over * fact); - polys = fromClipperTree(ctre, z); + if (opt.gaps && !polys.length) { + opt.gaps.push(orig); + } - // detect possible thin walls - if (thins) { - let circ1 = sumCirc(orig), - circ2 = sumCirc(polys), - diff = Math.abs(1 - (circ1 / circ2)); - // if circularity of original and offset differ, possible wall gaps - if (diff > 0.2) { - thins(orig, out.length ? polys : null, diff, -over); + // if specified, perform up to *count* successive offsets + if (polys.length) { + // ensure opts has offset accumulator array + opt.outs = opt.outs || []; + // store polys in accumulator + opt.outs.push(polys); + if (opt.count > 1) { + // decrement count, increment depth + opt.count--; + opt.depth = depth + 1; + // call next offset + offset(polys, dist, opt); } } - // process - if (out) out.appendAll(polys); - if (collector) collector(polys, count); - if ((count === 0 || count > 1) && polys.length > 0) { - expand2(polys, dist2 || dist1, dist2, out, count > 0 ? count-1 : 0, collector, thins, z); + // if the polys fail to offset at the top level, consider them gaps + if (opt.gaps && polys.length === 0 && depth === 0) { + opt.gaps.push(orig); } return polys; diff --git a/src/kiri/codec.js b/src/kiri/codec.js index d9bacfd1..473605c8 100644 --- a/src/kiri/codec.js +++ b/src/kiri/codec.js @@ -122,6 +122,7 @@ 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) @@ -135,6 +136,7 @@ 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); diff --git a/src/kiri/init.js b/src/kiri/init.js index 75709b76..18138a0f 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -1513,7 +1513,7 @@ antiBacklash: UC.newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, bound:UC.bound(0,3), convert:UC.toInt, modes:FDM, expert:true}), sliceRotation: LOCAL ? UC.newInput(LANG.ad_slrt_s, {title:LANG.ad_slrt_l, bound:UC.bound(-45,45), convert:UC.toFloat, modes:FDM, expert:true}) : null, gcodePauseLayers: UC.newInput(LANG.ag_paws_s, {title:LANG.ag_paws_l, modes:FDM, expert:true, comma:true}), - detectThinWalls: LOCAL ? UC.newBoolean(LANG.ad_thin_s, onBooleanClick, {title: LANG.ad_thin_l, modes:FDM, expert:true}) : null, + detectThinWalls: UC.newBoolean(LANG.ad_thin_s, onBooleanClick, {title: LANG.ad_thin_l, modes:FDM, expert:true}), outputLayerRetract: UC.newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l, modes:FDM, expert:true}), // SLA diff --git a/src/kiri/print.js b/src/kiri/print.js index 88cab303..945b19a4 100644 --- a/src/kiri/print.js +++ b/src/kiri/print.js @@ -670,29 +670,63 @@ } function outputFills(lines, options) { + if (!lines || lines.length === 0) { + return; + } let p, p1, p2, dist, len, found, group, mindist, t1, t2, marked = 0, start = 0, skip = false, lastIndex = -1, opt = options || {}, + near = opt.near || false, fast = opt.fast || false, - fill = opt.fill >= 0 ? opt.fill : fillMult; + fill = opt.fill >= 0 ? opt.fill : fillMult, + thinDist = near ? thinWall * 3 : thinWall; while (lines && marked < lines.length) { - found = false; group = null; + found = false; mindist = Infinity; + // use next nearest line strategy + if (near) + for (i=0; i lastIndex) { group = p.index; } if (group !== null) { - if (p.index !== group) break; + if (p.index !== group) { + break; + } if (p.index % 2 === 0) { t1 = lines[i]; t2 = lines[i+1]; @@ -712,7 +746,7 @@ } // go back to start and try again - if (!found) { + if (!near && !found) { if (start === 0 && lastIndex === -1) { console.log('infinite loop', lines, { marked, options, i, group, start, lastIndex, @@ -725,11 +759,11 @@ continue; } - dist = startPoint.distTo2D(p1); - len = p1.distTo2D(p2); + dist = startPoint.distToSq2D(p1); + len = p1.distToSq2D(p2); // go back to start when dist > retractDist - if (!fast && !skip && dist > retractDist) { + if (!near && !fast && !skip && dist > retractDist) { skip = true; start = 0; lastIndex = -1; @@ -747,7 +781,7 @@ // and segment length is less than thinWall then // just extrude to midpoint of next segment. this is // to avoid shaking the printer to death. - if (dist <= thinWall && len <= thinWall) { + if (dist <= thinDist && len <= thinDist) { p2 = p1.midPointTo(p2); addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder); } else { @@ -762,7 +796,7 @@ } // bridge ends of fill when they're close together - if (dist < thinWall) { + if (dist < thinDist) { addOutput(preout, p1, fill, fillSpeed, extruder); } else { addOutput(preout, p1, 0, moveSpeed, extruder); @@ -854,10 +888,13 @@ outputTraces(poly); }); - // output inner polygons + // output outer polygons if (dir === -1) outputTraces([].appendAll(next.innerTraces() || [])); + // output thin fill + outputFills(next.thin_fill, {near: true}); + // then output solid and sparse fill outputFills(next.fill_lines); outputSparse(next.fill_sparse, sparseMult); diff --git a/src/kiri/slice.js b/src/kiri/slice.js index b3f00249..40e46ae2 100644 --- a/src/kiri/slice.js +++ b/src/kiri/slice.js @@ -296,28 +296,53 @@ }; /** - * given two arrays of points (lines), eliminate intersections of the second - * to the first, then return a unified array. + * given an array of arrays of points (lines), eliminate intersections + * between groups, then return a unified array of shortest non-intersects. * - * @param {Point[]} r1 - * @param {Point[]} r2 * @returns {Point[]} */ - function cullIntersections(r1, r2) { - if (!(r1 && r2 && r1.length && r2.length)) return; - let valid = r2.slice(); - outer: for (let i=0; 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)); } - return valid.length > 2 ? valid : []; + let good = []; + for (let i=0, il=aa.length; i 2 ? good : []; } /** @@ -353,10 +378,6 @@ top.inner = []; let last = [], z = top.poly.getZ(); - if (opt.thin) { - top.thin_fill = []; - } - if (count) { // permit offset of 0 for laser and drag knife if (offset1 === 0 && count === 1) { @@ -364,51 +385,29 @@ top.traces = last; } else { if (opt.thin) { - // thin wall offset strategy - let on1s2 = offset1 * 2, - on2s2 = offsetN * 2; - POLY.expand2( - top_poly, // reference polygon(s) - -offset1, // first inset distance - -offsetN, // subsequent inset distnace - top.traces, // accumulator array - count, // numberof insets to perform - // 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); - }); - }); - }, - // thin wall probe - // p1 = original polys - // p2 = collected polys - // diff = circularity difference - // dist = over-expansion distance - function(p1, p2, diff, dist) { - if (p2) { - // nth offset - let pall = POLY.nest(POLY.flatten([].appendAll(p1).appendAll(p2)).clone()), - pnew1 = POLY.expand(pall, -dist, z, null, 1), - r1 = fillArea(pnew1, 45, offsetN, [], dist / 2, on2s2), - r2 = fillArea(pnew1, 135, offsetN, [], dist / 2, on2s2), - rall = top.thin_fill.appendAll(cullIntersections(r1, r2)); - } else { - // first offset - let pall = POLY.nest(POLY.flatten([].appendAll(p1).appendAll(p2)).clone()), - pnew1 = POLY.expand(pall, -dist, z, null, 1), - r1 = fillArea(pnew1, 45, offsetN, [], 0, on1s2), - r2 = fillArea(pnew1, 135, offsetN, [], 0, on1s2), - rall = top.thin_fill.appendAll(cullIntersections(r1, r2)); - } - // top.thinner.appendAll(pnew1).appendAll(pnew2); - }, - z); + 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; + top.traces.push(p); + }); + }); + + // 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), + )); + }); } else { // standard wall offsetting strategy POLY.expand( @@ -796,7 +795,7 @@ // create empty filled line array for each top tops.forEach(function(top) { - top.fill_lines = top.thin_fill || []; + top.fill_lines = []; let tofill = [], angfill = []; trims.forEach(function(solid) { @@ -825,24 +824,6 @@ return true; }; - /** - * fill thin areas, if present - * @return {boolean} true if filled, false if not - */ - PRO.doThinFill = function(spacing, angle) { - this.tops.forEach(function(top) { - if (top.thin_fill && top.thin_fill.length > 0) { - if (top.fill_lines) { - top.fill_lines.appendAll(top.thin_fill) - } else { - top.fill_lines = top.thin_fill; - } - } - }); - - return true; - }; - /** * calculate external overhangs requiring support * this is done bottom-up diff --git a/src/mode/fdm/driver.js b/src/mode/fdm/driver.js index 0897acf2..da0369b9 100644 --- a/src/mode/fdm/driver.js +++ b/src/mode/fdm/driver.js @@ -189,14 +189,8 @@ }, "solids"); forSlices(0.35, 0.5, function(slice) { slice.doSolidsFill(fillSpacing, sliceFillAngle, minSolid); - slice.doThinFill(fillSpacing, sliceFillAngle); sliceFillAngle += 90.0; }, "solids"); - } else if (spro.detectThinWalls) { - forSlices(0.35, 0.5, function(slice) { - slice.doThinFill(fillSpacing, sliceFillAngle); - sliceFillAngle += 90.0; - }, "thins"); } // calculations only relevant when supports are enabled @@ -485,8 +479,10 @@ // solid fill if (thin) { + solids.lines(top.thin_fill, 0x77bbcc); solids.lines(top.fill_lines, 0x77bbcc); } else { + solids.noodle_lines(top.thin_fill, extoff, 0x88aadd, 0x77bbcc, s.z); solids.noodle_lines(top.fill_lines, extoff, 0x88aadd, 0x77bbcc, s.z); } @@ -1347,9 +1343,9 @@ let ao2 = BASE.newSlopeFromAngle(rs.angle - 25); let sp = BASE.newPoint(point.x, point.y, point.z); move.push(sp); - move.push(sp.projectOnSlope(ao1, 0.5)); + move.push(sp.projectOnSlope(ao1, BASE.config.debug_arrow)); move.push(sp); - move.push(sp.projectOnSlope(ao2, 0.5)); + move.push(sp.projectOnSlope(ao2, BASE.config.debug_arrow)); } } last = point; diff --git a/web/kiri/index.css b/web/kiri/index.css index 208f694a..ce6d4f17 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -368,6 +368,7 @@ th, tr, td, label { } .pop-lcol > div { flex-direction: column; + justify-content: center; align-items: center; margin: 2px; padding: 5px; @@ -425,6 +426,11 @@ th, tr, td, label { .pop-lcol svg { font-size: 20px !important; } + #mod-x { + position: absolute; + font-size: 30px; + top: 8px; + } } .ext-buttons button {