diff --git a/src/geo/base.js b/src/geo/base.js index d7ea18bf..5406d5ab 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -91,11 +91,11 @@ function area2(p1, p2, p3) { } function isCloseTo(v1, v2, dist) { - return Math.abs(v1 - v2) <= (dist || base.config.precision_merge); + return Math.abs(v1 - v2) <= (dist ?? base.config.precision_merge); } -function inCloseRange(val, min, max) { - return (isCloseTo(val, min) || val >= min) && (isCloseTo(val, max) || val <= max); +function inCloseRange(val, min, max, precisiom) { + return (isCloseTo(val, min, precisiom) || val >= min) && (isCloseTo(val, max, precisiom) || val <= max); } /** @@ -232,7 +232,7 @@ function intersect(p1, p2, p3, p4, test, parallelok) { /** * used by {@link rayIntersect} and {@link Polygon.trace} */ -function intersectRayLine(ro, s1, p1, p2, infinite) { +function intersectRayLine(ro, s1, p1, p2) { let keys = base.key, p1x = ro.x, p1y = ro.y, @@ -259,11 +259,11 @@ function intersectRayLine(ro, s1, p1, p2, infinite) { a = n1 / d; b = n2 / d; - if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) { + if (inCloseRange(b, 0, 1, 0.00) && a >= 0) { let ip = newPoint( p1x + (a * s1x), p1y + (a * s1y), - p2.z || ro.z, + p2.z ?? ro.z, keys.NONE ); ip.dist = a; diff --git a/src/geo/point.js b/src/geo/point.js index a29b98c4..add35a8c 100644 --- a/src/geo/point.js +++ b/src/geo/point.js @@ -114,11 +114,16 @@ class Point { return this.key; } - clone() { + clone(keys) { let p = newPoint(this.x, this.y, this.z, this._key); if (this.a !== undefined) { p.a = this.a; } + if (keys) { + for (let key of keys) { + p[key] = this[key]; + } + } return p; } diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 846515f9..7eee6ad0 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1104,6 +1104,14 @@ export class Polygon { return this.first().isEqual(this.last()); } + closeIf(dist = 1) { + if (this.first().distTo2D(this.last()) <= dist) { + return this.setClosed(); + } else { + return this.setOpen(); + } + } + fixClosed() { if (this.appearsClosed()) { this.points.pop(); @@ -2042,13 +2050,9 @@ export class Polygon { // use expand / deflate technique instead if (opt.pump) { - let p2 = POLY.offset([this], opt.pump, { - z - }); + let p2 = POLY.offset([this], opt.pump, { z }); if (p2) { - p2 = POLY.offset(p2, -opt.pump, { - z - }); + p2 = POLY.offset(p2, -opt.pump, { z }); return p2; } return null; @@ -2318,7 +2322,7 @@ export class Polygon { // split long straight lines into segments no longer than max // and return a new polygon - segment(max = 1) { + segment(max = 1, mark) { const newp = []; const points = this.points; const length = points.length; @@ -2330,6 +2334,7 @@ export class Polygon { const dy = p2.y - p1.y; const dl = Math.sqrt(dx * dx + dy * dy); newp.push(p1); + if (mark) p1.segment = p1; if (dl < max) { continue; } @@ -2344,10 +2349,12 @@ export class Polygon { newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2)); ox += ix; oy += iy; + // mark new point with first point of originating segment + if (mark) newp.peek().segment = p1; } } if (newp.length > length) { - return newPolygon().addPoints(newp.map(p => p.clone())).setOpenValue(this.open); + return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(this.open); } return this; } diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 6d4d8128..1d5c5212 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -645,7 +645,7 @@ export function offset(polys, dist, opts = {}) { for (let poly of polys) { // convert to clipper format poly = poly.toClipper(); - if (clean) poly = CleanPolygons(poly, config.clipperClean); + if (clean) poly = CleanPolygons(poly, opts.cleanDist ?? config.clipperClean); if (simple) poly = SimplifyPolygons(poly, fill); coff.AddPaths(poly, join, type); } @@ -871,6 +871,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { * @returns {Point[]} */ export function rayIntersect(start, slope, polygons, for_fill) { + throw "deprecated"; let i = 0, flat = [], points = [], diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index a332391e..748fa4e4 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -115,9 +115,9 @@ const LISTS = { ], thin: [ { name: "off" }, - { name: "type 1" }, - { name: "type 2" }, - { name: "type 3" } + { name: "adaptive" }, + { name: "legacy 1" }, + { name: "legacy 2" } ] }; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 0567d53d..a322958d 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -192,7 +192,7 @@ async function path(levels, update, opts = {}) { // Math.abs(op.y - lp.y), // Math.abs(op.z - lp.z)); // if (moved < 0.0001) return; - if( is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit + if (is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit // checks if a new poly should be started if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { current = newPolygon().setOpen(); @@ -210,7 +210,6 @@ async function path(levels, update, opts = {}) { } else if (out.emit) { // explicity G1 in CAM mode // just a non-move in other modes - // checks if a new poly should be started if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { current = newPolygon().setOpen(); diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 934f7394..d684d046 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -144,7 +144,9 @@ class Slice { } else { // create top object from object bundle passed back by slicePost() let top = new Top(data.poly); - top.thin_fill = data.thin_fill ? data.thin_fill.map(p => newPoint(p.x,p.y,p.z)) : undefined; + top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z)); + top.thin_wall = data.thin_wall; + top.thin_sort = data.thin_sort; top.fill_lines = data.fill_lines; top.fill_sparse = data.fill_sparse; top.fill_off = data.fill_off; diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index bb34b97f..73b93a1f 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -82,11 +82,12 @@ export function menu() { sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}), _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), - sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), separator: newBlank({ class:"set-sep", driven }), sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), @@ -178,7 +179,7 @@ export function menu() { outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), - antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toFloat, bound:bound(0,3)}), outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index a2222402..7c321bf6 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -164,31 +164,173 @@ function thin_type_2(params) { function thin_type_3(params) { // offsetN usually = extrusion width (nozzle diameter) - let { z, top, count, offsetN, last, gaps } = params; + let { z, top, count, offsetN } = params; // produce trace from outside of poly inward no more than max inset - let { noodle, remain } = top.poly.noodle(offsetN * count); + let noodleWidth = offsetN * count; + let { noodle, remain } = top.poly.noodle(noodleWidth); - // top.shells = noodle; - top.gaps = last = remain; + // re-expand inner offset so fill offsets align properly with expectation + remain = POLY.offset(remain, offsetN / 2); - let thin = top.thin_fill = []; - let sparse = top.fill_sparse = []; + // render inset "noodle" + // shells.appendAll(noodle); + + let debugRemain = false; + let debugExtrusion = false; + + let shells = top.shells; // only used for debug + let traces = top.thin_wall = []; + + let midR = offsetN * 0.5; // nominal extrusion width + let minR = midR * 0.75; // smallest interior fill or single wall width + let maxR = midR * 1.5; // max double wall extrusion width + let maxF = midR * 2; // max interior extrusion width + + let lines = []; + let polys = []; + + for (let i=0; i a.shell); + + // console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces }); + return { traces, last: remain }; +} + +// trace a single extrusion line around the inside of the noodle poly +function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { + let { + brute, + showInset, + showChainIntersect, + minArea = midR * midR, + shellStep = 1 / 100, // diameter step as % of nozzle + lines = [], + polys = [], + remain = [], + traces = [], + shell = 0 + } = opt; + + let sstep = 1 / shellStep; + let insets = []; let scale = 1000; - let minR = offsetN / 2; - let maxR = minR * 1.5; - let minSpur = offsetN * 2; + let dstep = minR / 4; + let dtotl = 0; - let showNoodle = false; - let mergeChains = true; - let interpolateShortSpur = true; - let showChainRawPoints = false; - let showChainInterpPoints = true; - let showMidline = true; - let showMidPoints = false; - let showMidNormals = false; - let showMidRadii = true; - let showNexuses = false; + let inset = POLY.offset(noodle, -dstep, { + join: ClipperLib.JoinType.jtRound, + arc: (1/dstep) * 4 + }); + while (inset && inset.length) { + dtotl += dstep; + for (let p of POLY.flatten(inset, [])) { + p.dtotl = dtotl; + } + insets.push(inset); + if (showInset) { + polys.appendAll(inset); + } + if (!brute) { + break; + } + inset = POLY.offset(inset, -dstep, { + minArea: 0 + }); + } + + // project from first inset and find greated offset intersection. + // this examples show how to brute force create a medial axis from + // micro stepping insets. the same intersect technique used with + // medial axis can be applied to this structure to produce an + // equivalent output. not as computationally efficient, but much simpler. + if (brute) { + let { intersectRayLine } = base.util; + let tests = insets.slice(1); + let source = POLY.flatten(insets[0], []); + for (let poly of source) { + // segment the smallest inset and test against all other insets + poly.segment(minR).forEachSegment((p1, p2) => { + let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; + let len = pointDist(p1, p2); + let dx = (p1.x - p2.x) / len; + let dy = (p1.y - p2.y) / len; + let np2 = { x: np1.x + dy * 10, y: np1.y - dx * 10 }; + let max = { dtotl: poly.dtotl, dist: Infinity, maxd: Infinity }; + let term = false; + outer: for (let test of tests) { + for (let tpoly of POLY.flatten(test, [])) { + tpoly.forEachSegment((tp1, tp2) => { + let int = intersectRayLine(np1, { dx: dy, dy: -dx }, tp1, tp2); + if (int && int.dist > int.p1.poly.dtotl) { + return; + } + if (int && int.dist <= max.maxd && int.p1.poly.dtotl > max.dtotl) { + max.dtotl = int.p1.poly.dtotl; + max.dist = int.dist; + max.int = int; + } else if (int && int.dist <= max.maxd && int.p1.poly.dtotl === max.dtotl && int.dist < max.dist) { + max.dtotl = int.p1.poly.dtotl; + max.dist = int.dist; + max.int = int; + } + }); + if (term) { + break outer; + } + } + } + if (max.int) { + lines.push(new Point(np1.x, np1.y)); + lines.push(new Point(max.int.x, max.int.y)); + } + }); + } + return { lines, polys }; + } let pointMap = new Map(); let lineMap = new Map(); @@ -240,11 +382,8 @@ function thin_type_3(params) { return rec; } - let chains = []; - // for each standalone noodle, construct medial axis // map medial axis points into de-duplicated segments - // which will be re-connected into chains for (let poly of noodle) { // scale the polygon (including inners) so the medial axis code works properly poly = poly.clone(true).scale({ x:scale, y:scale, z:scale }); @@ -253,349 +392,172 @@ function thin_type_3(params) { // construct medial axis lines. this appears to produce duplicate // reverse segments in a subset of cases let ma = JSPoly.construct_medial_axis(out, inr); - // dedup the points so we can track them uniquely (for chain intersections) + // dedup the points so we can track them uniquely for (let { point0, point1 } of ma) { pointsToLine(point0, point1); } } - // connect segments into chains - for (let seg of lineMap.values()) { - let { p0, p1 } = seg; - let len = seg.len = pointDist(p0, p1); - // filter out lines where both points' radii are under the - // minR threshold. shorten and fixup lines where minR falls - // somewhere along the gradient between ends. - if (interpolateShortSpur) - if (p0.r < minR || p1.r < minR) { - if (p0.r > minR && len > minSpur) { - // move p1 toward p0 until minR met - let pct = (minR - p1.r) / (p0.r - p1.r); - let dx = p1.x - p0.x; - let dy = p1.y - p0.y; - p1.x -= dx * pct; - p1.y -= dy * pct; - p1.r = minR; - pointMap.delete(p1.key); - } else if (p1.r > minR && len > minSpur) { - // move p0 toward p1 until minR met - let pct = (minR - p0.r) / (p1.r - p0.r); - let dx = p0.x - p1.x; - let dy = p0.y - p1.y; - p0.x -= dx * pct; - p0.y -= dy * pct; - p0.r = minR; - pointMap.delete(p0.key); - } else { - // both points eliminated - pointMap.delete(p0.key); - pointMap.delete(p1.key); - lineMap.delete(seg.key); - continue; - } - } - // for each line segment either add it to a chain or start a new chain - let add; - for (let chain of chains) { - if (p0.count === 2 && p0 === chain[0]) { - // prefix chain with p1 - chain.splice(0,0,p1); - add = true; - } else if (p0.count === 2 && p0 === chain.peek()) { - // append chain with p1 - chain.push(p1); - add = true; - } else if (p1.count === 2 && p1 === chain[0]) { - // prefix chain with p0 - chain.splice(0,0,p0); - add = true; - } else if (p1.count === 2 && p1 === chain.peek()) { - // append chain with p0 - chain.push(p0); - add = true; - } - if (add) { - chain.total += len; - break; - } - } - if (!add) { - chains.push([ p0, p1 ]); - chains.peek().total = len; - } - } - - // todo: merge chains at next of 3 or more preferencing - // merging longer chains. then shorten remaining nexus chain - // intersections by minR - - // render inset "noodle" - if (showNoodle) { - top.shells.appendAll(noodle); - } - // divide a segment/redius into 1 or more equal subsegments function div(cr) { if (cr <= maxR) { return [ cr ]; } else if (cr <= maxR * 2) { - return [ cr / 2, cr / 2 ]; + let rem = (cr - midR * 2); + if (rem < minR) { + return [ cr / 2, cr / 2 ]; + } + return [ midR, rem, midR ]; } else if (cr <= maxR * 3) { - let rem = cr - minR * 2; - if (rem < maxR) { - return [ minR, rem, minR ]; - } else { - return [ minR, rem / 2, rem / 2, minR ]; + let rem = (cr - midR * 2); + if (rem < minR) { + return [ cr / 3, cr / 3, cr / 3 ]; } + return [ midR, rem, midR ]; } else { - return [ minR, ...div(cr - minR * 2), minR ]; + return [ midR, cr - midR * 2, midR ]; } } - // filter chains that are too short from consideration - chains = chains.filter(c => c.total >= minR); - chains.sort((c1,c2) => c2.total - c1.total); + // medial axis segments for comparison + let segs = [...lineMap.values()]; - function merge1chain() { - let clen = chains.length; - for (let i=0; i chain); - - // filter dup points from connecting ends - chains = chains.map(chain => chain.filter((v, i) => i === 0 || v !== chain[i - 1])); - - // mark closed chains - // todo use exact point match by eliminating slice above - // then later removing repeated points in the resulting chain from merges - for (let chain of chains) { - chain.closed = (chain[0] === chain.peek()); - console.log(chain.closed ? 'closed' : 'open'); - if (chain.closed) chain.pop(); - } - - // shorten chain length by minR distance by popping or moving points - function shorten(chain) { - for (let rem=0, i=chain.length-1; i>=1 && rem < minR; i--) { - let c1 = chain[i]; - let c2 = chain[i-1]; - let d = pointDist(c1,c2); - if (rem + d <= minR) { - chain.pop(); - rem += d; - } else { - let diff = minR - rem; - // move c1 toward c2 - let pct = diff / d; - let dx = c1.x - c2.x; - let dy = c1.y - c2.y; - chain[i] = { - x: c1.x - dx * pct, - y: c1.y - dy * pct, - r: c1.r - }; - rem = 0; - break; - } - } - } - - // shorten chains terminating at nexus by midR - for (let rec of Object.values(nexus)) { - let { point } = rec; - rec.shorts = 0; - if (rec.chains.length < 3) continue; - for (let chain of chains.filter(c => !c.closed)) { - if (pointDist(chain[0], point) < minR) { - chain.reverse(); - shorten(chain); - chain.reverse(); - rec.shorts++; - } else if (chain.peek() === point) { - shorten(chain); - rec.shorts++; - } - } - } - - if (showNexuses) - for (let rec of Object.values(nexus)) { - let { point } = rec; - top.shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); - } - - function renderPointNormal(pt, ndx, ndy) { - if (showMidNormals) { - let xo = ndx * pt.r; - let yo = ndy * pt.r; - thin.push(new Point(pt.x + yo, pt.y - xo)); - thin.push(new Point(pt.x - yo, pt.y + xo)); - } - if (showMidRadii) { - const Sx = pt.x + ndy * pt.r; - const Sy = pt.y - ndx * pt.r; - const circ = top.shells; - const pop = div(pt.r); - let acc = 0; - for (let r of pop) { - const t = acc + r; - const px = Sx + -ndy * t; - const py = Sy + ndx * t; - circ.push(newPolygon().centerCircle({x:px, y:py}, r, 10)); - acc += r * 2; - } - } -} - - // gather point offsets into shells - let zi = z; - for (let chain of chains) { - // zi += 0.1; - if (showChainRawPoints) - for (let pt of chain) { - top.shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10)); - } - // draw medial axis chain - let { closed, length } = chain; - let term = closed ? length : length - 1; - let segs = []; - for (let i=0; i { + let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; + let len = pointDist(p1, p2); + let dx = (p1.x - p2.x) / len; + let dy = (p1.y - p2.y) / len; + let min = { dist: Infinity }; + for (let seg of segs) { + let { p0, p1 } = seg; + let int = intersectRayLine(np1, { dx: dy, dy: -dx }, p0, p1); + if (int?.dist < min.dist && int?.dist < noodleWidth) { + min = int; + min.seg = seg; } } + if (min.p1) { + let mp1 = min.seg; + if (showChainIntersect) { + lines.push(new Point(np1.x, np1.y)); + lines.push(new Point(min.x, min.y)); + } + let mr = min.dist + dstep; + if (mr < minR) { + nupoly.push(new Point(min.x, min.y)); + return; + } + // use intersection off and divide into extrusion lanes + let pop = div(mr); + let odd = (pop.length % 2 === 1); + let len = Math.ceil(pop.length / 2); + // check if segment was claimed by a different originating segment + // for odd wall counts so that they're not emitted twice + if (odd && mp1.claimed && mp1.claimed !== p1.segment) len--; + if (len === 0) { + nupoly.push(new Point(min.x, min.y)); + return; + } + let npo; + if (pop.length === 1) { + // let rad_step = ((pop[0] * sstep) | 0) / sstep; + // for single wall place it exactly on medial axis + nupoly.push(new Point(min.x, min.y)); + npo = { x: min.x, y: min.y, r: mr, s: p1.segment }; + } else { + // radius rounded to nearest step + let rad_step = ((pop[0] * sstep) | 0) / sstep; + // otherwise use first division + // compensate for minimal inset + let off = -dstep + rad_step; + npo = { + x: np1.x + dy * off, + y: np1.y - dx * off, + r: rad_step, + s: p1.segment + }; + // trace inset by full diameter + nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); + } + let lpo = trace.peek(); + // when outputting the same segment at the same radius + // it's safe to drop the last point since path output will connect them + if (trace.length > 1 && lpo.s === npo.s && lpo.r === npo.r) { + trace.pop(); + } + trace.push(npo); + if (odd && !mp1.claimed) { + mp1.claimed = p1.segment; + } + // detect trace jump and start new trace + if (ltpo && pointDist(ltpo, trace.peek()) > maxR) { + // start new trace removing jump point + // and adding that to the new trace + traces.push(trace); + let nutrace = [ trace.pop() ]; + nutrace.shell = trace.shell; + // close last trace if endpoints near enough + if (pointDist(ltpo, trace[0] <= maxR)) { + trace.push(trace[0]); + } + trace = nutrace; + } + // update last trace point out + ltpo = trace.peek(); + } + }); + // repeat first point of trace (if close enough to be closed) + if (trace[0] && pointDist(trace[0], trace.peek()) <= maxR) { + trace.push(trace[0]); } - { - // todo: handle closed - if (first) { - renderPointNormal(p0, ndx, ndy); - } - if (mid) { - renderPointNormal(p0, ((ndx + segp.ndx)/2), ((ndy + segp.ndy)/2)); - } - if (last) { - renderPointNormal(p1, ndx, ndy); - } - } - // interpolate across the length of the chain segment - if (showChainInterpPoints) - for (let indx = 1; indx < offs.length; indx++) { - const inc = indx * step; - const cr = p0.r + dr * inc; - const cx = p0.x + dx * inc; - const cy = p0.y + dy * inc; - const xo = ndx * cr; - const yo = ndy * cr; - if (showMidPoints) { - top.shells.push(newPolygon().centerCircle({ x:cx, y:cy }, 0.05, 10)); - } - renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy); + if (trace.length) { + traces.push(trace); } } + + // subtract inner extrusion offsets from parent extrusion offset + // this can result in more top level polys and must be processed + // iteratively. + let setA = [ parent ]; + for (let setB of inner) { + setA = POLY.subtract(setA, [ setB ], []); + } + + // clean and re-nest the resulting polygon soup + // subtraction cleans pre-subtract but not post and these + // outlines can have sharp unprintable interiors + let ret = POLY.nest( + POLY.flatten(setA, [], true).map(p => { + return p.clean().simplify(); + }).flat() + ).filter(p => p.area() >= minArea); + + remain.push(...ret); + } + + // remove point.segment markers for codec encoding between minion and worker + for (let trace of traces) { + for (let pt of trace) { + delete pt.s; + } } - POLY.setZ([...thin, ...top.shells], z); - // POLY.setZ([...top.shells], z); - - return { last, gaps }; + return { lines, polys, remain, traces }; } /** @@ -639,21 +601,21 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = { } }); let ret = { last, gaps }; switch (opt.thinType) { - case "type 1": + case "legacy 1": ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset }); break; - case "type 2": + case "legacy 2": ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm }); break; - case "type 3": - ret = thin_type_3({ z, top, count, top_poly, offsetN }); + case "adaptive": + ret = thin_type_3({ z, top, count, offsetN }); break; default: ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm }); break; } - last = ret.last || last; - gaps = ret.gaps || gaps; + last = ret.last ?? last; + gaps = ret.gaps ?? gaps; } } else { // no shells, just infill, is permitted diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index c7a90638..55808023 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -3,7 +3,7 @@ import { base, util } from '../../../geo/base.js'; import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js'; import { newBounds } from '../../../geo/bounds.js'; -import { newPoint } from '../../../geo/point.js'; +import { newPoint, Point } from '../../../geo/point.js'; import { newPolygon, Polygon } from '../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { newPrint } from '../../core/print.js'; @@ -834,7 +834,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { z = slice.z, lastPoly; - // support alternating shell order + // support alternating shell order if (Math.abs(shellOrder) > 1 && slice.index % 2 === 1) { shellOrder = -shellOrder; } @@ -1089,6 +1089,59 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } + function minThinDist(np, trace) { + return Math.min( + Math.hypot(np.x - trace[0].x, np.y - trace[0].y), + Math.hypot(np.x - trace.peek().x, np.y - trace.peek().y), + ); + } + + function outputAdaptiveWalls(thin, sort) { + // restore thin shell order annotation and sort on it + thin.forEach((a,i) => a.shell = sort[i] * shellOrder); + thin.forEach(t => t.shell = t.shell < 0 ? Math.abs(t.shell) + 0.5 : t.shell); + thin.sort((a,b) => a.shell - b.shell); + let np = print.lastPoint; + // perform nearest poly to poly algorithm on shells + for (;;) { + let min = { dist: Infinity }; + for (let trace of thin) { + if (!min.trace) { + min.trace = trace; + } else if (np && min.trace.shell === trace.shell) { + let dist = minThinDist(np, trace); + if (dist < min.dist) min = { dist, trace }; + } + } + if (!min.trace) { + startPoint = np; + return; + } + let { trace } = min; + let fp = trace[0]; + let lp = trace.peek(); + // reverse trace if far end closer + if (np && Math.hypot(np.x - lp.x, np.y - lp.y) < Math.hypot(np.x - fp.x, np.y - fp.y)) { + trace.reverse(); + } + // remove emitted trace + thin = thin.filter(t => t !== trace); + // retract if moving > retractDist + if (np && np.distTo2D(new Point(trace[0].x, trace[0].y)) > retractDist) { + retract(); + } + let outputSpeed = trace.shell === 1 || trace.shell === 1.5 ? finishSpeed : printSpeed; + for (let pt of trace) { + np = new Point(pt.x, pt.y, z); + if (pt === trace[0]) { + print.addOutput(preout, np, 0, moveSpeed); + } else { + print.addOutput(preout, np, ((pt.r * 2) / nozzleSize) * shellMult, outputSpeed); + } + } + } + } + /** * output sparse infill * @param {Polygon[]} polys @@ -1185,10 +1238,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { start = 0, skip = false, lastIndex = -1, - raft = opt.raft || false, + raft = opt.raft ?? false, flow = opt.flow || 1, - near = opt.near || (antiBacklash ? false : true), - fast = opt.fast || false, // support infill only! + near = opt.near ?? (antiBacklash ? false : true), + fast = opt.fast ?? false, // support infill only! fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow, thinDist = near ? thinWall : thinWall; @@ -1444,6 +1497,9 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // raft if (top.traces) outputTraces(top.traces); + // thin wall v3 + if (top.thin_wall) outputAdaptiveWalls(top.thin_wall, top.thin_sort); + // innermost shells let inner = next.innerShells() || []; let shells = next.shells || []; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index cec07284..291dcf24 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -997,31 +997,45 @@ function doRender(slice, isSynth, params, opt = {}) { .setLayer('part', Color.part) .addPolys([top.poly]); - output + if (top.shells?.length) output .setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) .addPolys(top.shells || [], vopt({ offset, height, clean: true })); - output + if (isThin && top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(a => a.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z))).flat(), vopt({ offset, height })); + + if (!isThin && top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(a => + a.length === 1 ? + newPolygon().centerCircle(a[0], a[0].r, 6) : + newPolygon(a.map(p => newPoint(p.x, p.y))) + .setZ(slice.z) + .closeIf(height) + ), vopt({ offset, height })); + + if (top.fill_lines?.length) output .setLayer("solid fill", isSynth ? Color.support : Color.fill) .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); - if (!(slice.belt && slice.belt.anchor)) output + if (!(slice.belt?.anchor)) output .setLayer("sparse fill", Color.infill) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - if (slice.belt && slice.belt.anchor) output + if (slice.belt?.anchor) output .setLayer("anchor", Color.anchor) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - if (top.thin_fill) output + if (top.thin_fill?.length) output .setLayer("thin fill", Color.thin) .addLines(top.thin_fill, vopt({ offset, height })); - if (top.gaps && devel) output + if (top.gaps?.length && devel) output .setLayer("gaps", Color.gaps) .addPolys(top.gaps, vopt({ offset, height, thin: true })); - if (isThin && devel && top.fill_off && top.fill_off.length) { + if (isThin && devel && top.fill_off?.length) { slice.output() .setLayer('fill inset', Color.inset) .addPolys(top.fill_off);