From 6ede24eebb06fd52c783b04797fce615b9cee6e0 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 16 Sep 2025 11:13:30 -0400 Subject: [PATCH 01/60] clean up radii terms --- src/kiri/mode/fdm/post.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index a2222402..6b926c8f 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -169,16 +169,17 @@ function thin_type_3(params) { // produce trace from outside of poly inward no more than max inset let { noodle, remain } = top.poly.noodle(offsetN * count); - // top.shells = noodle; - top.gaps = last = remain; - let thin = top.thin_fill = []; let sparse = top.fill_sparse = []; let scale = 1000; - let minR = offsetN / 2; - let maxR = minR * 1.5; + let midR = offsetN * 0.5; + let minR = midR * 0.75; + let maxR = midR * 1.5; let minSpur = offsetN * 2; + // top.shells = noodle; + top.gaps = last = remain; + let showNoodle = false; let mergeChains = true; let interpolateShortSpur = true; @@ -341,14 +342,14 @@ function thin_type_3(params) { } else if (cr <= maxR * 2) { return [ cr / 2, cr / 2 ]; } else if (cr <= maxR * 3) { - let rem = cr - minR * 2; - if (rem < maxR) { - return [ minR, rem, minR ]; + if (cr - maxR * 2 >= minR) { + return [ maxR, cr - maxR * 2, maxR ]; } else { - return [ minR, rem / 2, rem / 2, minR ]; + return [ cr / 3, cr / 3, cr / 3 ]; } } else { - return [ minR, ...div(cr - minR * 2), minR ]; + let divs = (cr / midR) | 0; + return new Array(divs).fill(cr / divs); } } @@ -524,7 +525,7 @@ function thin_type_3(params) { thin.push(new Point(p1.x, p1.y, zi)); } // compute medial axis segment normal - let offs = len <= offsetN ? [ 0 ] : base.util.lerp(0, len, offsetN, true); + let offs = len <= midR ? [ 0 ] : base.util.lerp(0, len, midR, true); let step = 1 / offs.length; let dr = (p1.r - p0.r); let dx = (p1.x - p0.x); @@ -533,7 +534,7 @@ function thin_type_3(params) { let ndy = dy / len; segs.push({ p0, p1, offs, step, dr, dx, dy, ndx, ndy }); } - console.log({ segs }); + // compute chain subdivisions let slen = segs.length; for (let si=0; si Date: Tue, 16 Sep 2025 14:49:30 -0400 Subject: [PATCH 02/60] add micro offsetting for visualization --- src/kiri/mode/fdm/post.js | 65 +++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 6b926c8f..f91f6b14 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -169,19 +169,46 @@ function thin_type_3(params) { // produce trace from outside of poly inward no more than max inset let { noodle, remain } = top.poly.noodle(offsetN * count); + top.gaps = remain; + let thin = top.thin_fill = []; + let shells = top.shells; let sparse = top.fill_sparse = []; let scale = 1000; let midR = offsetN * 0.5; let minR = midR * 0.75; let maxR = midR * 1.5; - let minSpur = offsetN * 2; + let minSpur = offsetN * 1; - // top.shells = noodle; - top.gaps = last = remain; + if (false) { + let dtotl = 0; + let dstep = minR / 2; + let inset = POLY.expand(noodle, -dstep); + while (inset && inset.length) { + dtotl += dstep; + let ps = POLY.flatten(inset); + for (let poly of ps) { + for (let p of poly.segment(minR).points) { + let d = dtotl * 0+0.05; + // let z = dtotl * 10; + shells.append( newPolygon().centerCircle(p, d, 10).setZ(z) ); + } + } + POLY.setZ(inset, z); + shells.appendAll(inset); + if (dtotl >= maxR) { + break; + } + inset = POLY.expand(inset, -dstep); + } + } + + // shells.appendAll(noodle); + // return { last, gaps }; let showNoodle = false; let mergeChains = true; + let shortenAtNexus = false; let interpolateShortSpur = true; let showChainRawPoints = false; let showChainInterpPoints = true; @@ -332,7 +359,7 @@ function thin_type_3(params) { // render inset "noodle" if (showNoodle) { - top.shells.appendAll(noodle); + shells.appendAll(noodle); } // divide a segment/redius into 1 or more equal subsegments @@ -457,6 +484,7 @@ function thin_type_3(params) { } // shorten chains terminating at nexus by midR + if (shortenAtNexus) for (let rec of Object.values(nexus)) { let { point } = rec; rec.shorts = 0; @@ -477,7 +505,7 @@ function thin_type_3(params) { if (showNexuses) for (let rec of Object.values(nexus)) { let { point } = rec; - top.shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); + shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); } function renderPointNormal(pt, ndx, ndy) { @@ -490,14 +518,13 @@ function thin_type_3(params) { 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)); + shells.push(newPolygon().centerCircle({x:px, y:py}, r, 10)); acc += r * 2; } } @@ -509,7 +536,7 @@ function thin_type_3(params) { // zi += 0.1; if (showChainRawPoints) for (let pt of chain) { - top.shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10)); + shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10)); } // draw medial axis chain let { closed, length } = chain; @@ -525,14 +552,14 @@ function thin_type_3(params) { thin.push(new Point(p1.x, p1.y, zi)); } // compute medial axis segment normal - let offs = len <= midR ? [ 0 ] : base.util.lerp(0, len, midR, true); - let step = 1 / offs.length; + let steps = Math.ceil(len / midR) + 1; + let step = 1 / steps; let dr = (p1.r - p0.r); let dx = (p1.x - p0.x); let dy = (p1.y - p0.y); let ndx = dx / len; let ndy = dy / len; - segs.push({ p0, p1, offs, step, dr, dx, dy, ndx, ndy }); + segs.push({ p0, p1, steps, step, dr, dx, dy, ndx, ndy }); } // compute chain subdivisions @@ -540,7 +567,7 @@ function thin_type_3(params) { for (let si=0; si Date: Wed, 17 Sep 2025 00:55:09 -0400 Subject: [PATCH 03/60] add more splitting / intersection visualizations --- src/geo/polygons.js | 2 +- src/kiri/mode/fdm/post.js | 158 ++++++++++++++++++++++++++++++-------- 2 files changed, 129 insertions(+), 31 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 6d4d8128..fcbd1303 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); } diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index f91f6b14..96be3140 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -171,6 +171,23 @@ function thin_type_3(params) { top.gaps = remain; + let mergeChains = false; + let simplifyChain = false; + let shortenAtNexus = false; + let shortenEdgeSpur = false; + let intersectChains = true; + + let showNoodle = false; + let showInsetPoints = false; + let showChainRawPoints = false; + let showChainInterpPoints = true; + let showNuMedLine = false; + let showMedLine = true; + let showMedPoints = false; + let showMedNormals = false; + let showMedRadii = false; + let showNexuses = false; + let thin = top.thin_fill = []; let shells = top.shells; let sparse = top.fill_sparse = []; @@ -180,10 +197,16 @@ function thin_type_3(params) { let maxR = midR * 1.5; let minSpur = offsetN * 1; - if (false) { + let inset; + let dstep = minR / 4; + top.gaps = remain; + + if (true) { let dtotl = 0; - let dstep = minR / 2; - let inset = POLY.expand(noodle, -dstep); + inset = POLY.offset(noodle, -dstep, { + join: ClipperLib.JoinType.jtRound, + arc: (1/dstep) * 4 + }); while (inset && inset.length) { dtotl += dstep; let ps = POLY.flatten(inset); @@ -191,12 +214,13 @@ function thin_type_3(params) { for (let p of poly.segment(minR).points) { let d = dtotl * 0+0.05; // let z = dtotl * 10; + if (showInsetPoints) shells.append( newPolygon().centerCircle(p, d, 10).setZ(z) ); } } POLY.setZ(inset, z); shells.appendAll(inset); - if (dtotl >= maxR) { + if (true || dtotl >= maxR) { break; } inset = POLY.expand(inset, -dstep); @@ -206,18 +230,6 @@ function thin_type_3(params) { // shells.appendAll(noodle); // return { last, gaps }; - let showNoodle = false; - let mergeChains = true; - let shortenAtNexus = false; - 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 pointMap = new Map(); let lineMap = new Map(); @@ -294,7 +306,7 @@ function thin_type_3(params) { // 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 (shortenEdgeSpur) if (p0.r < minR || p1.r < minR) { if (p0.r > minR && len > minSpur) { // move p1 toward p0 until minR met @@ -451,9 +463,9 @@ function thin_type_3(params) { // mark closed chains // todo use exact point match by eliminating slice above // then later removing repeated points in the resulting chain from merges + if (mergeChains) for (let chain of chains) { chain.closed = (chain[0] === chain.peek()); - console.log(chain.closed ? 'closed' : 'open'); if (chain.closed) chain.pop(); } @@ -508,14 +520,39 @@ function thin_type_3(params) { shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); } + // Keep endpoints; drop interior points closer than R from the last kept point. + function dropClosePoints(chain, R) { + if (!chain || chain.length <= 2 || R <= 0) { + return chain; + } + let last = chain[0]; + let out = [ last ]; + let R2 = R * R; + for (let i = 1; i < chain.length - 1; i++) { + let dx = chain[i].x - last.x, + dy = chain[i].y - last.y; + if (dx * dx + dy * dy >= R2) { + out.push(chain[i]); + last = chain[i]; + } + } + out.push(chain[chain.length - 1]); + return out; + } + + // drop chain points < minR apart + if (simplifyChain) { + chains = chains.map(chain => dropClosePoints(chain, minR)); + } + function renderPointNormal(pt, ndx, ndy) { - if (showMidNormals) { + if (showMedNormals) { 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) { + if (showMedRadii) { const Sx = pt.x + ndy * pt.r; const Sy = pt.y - ndx * pt.r; const pop = div(pt.r); @@ -532,7 +569,10 @@ function thin_type_3(params) { // gather point offsets into shells let zi = z; + let nuchains = []; for (let chain of chains) { + let nuchain = []; + nuchains.push(nuchain); // zi += 0.1; if (showChainRawPoints) for (let pt of chain) { @@ -547,7 +587,7 @@ function thin_type_3(params) { let p1 = chain[(i+1) % length]; let len = pointDist(p0, p1); // medial axis segment - if (showMidline) { + if (showMedLine) { thin.push(new Point(p0.x, p0.y, zi)); thin.push(new Point(p1.x, p1.y, zi)); } @@ -571,23 +611,26 @@ function thin_type_3(params) { let first = si === 0; let last = si === slen - 1; let mid = !(first || last); + if (first || mid) { + nuchain.push(p0); + } // for length 2, first and last segment are the same - if (showMidPoints) { + if (showMedPoints) { if (first) { // todo: handle closed - shells.push(newPolygon().centerCircle(p0, 0.08, 10)); + shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); } if (mid) { // mid segments - shells.push(newPolygon().centerCircle(p0, 0.08, 10)); + shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); } if (last) { // todo: handle closed - shells.push(newPolygon().centerCircle(p0, 0.08, 10)); + shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); if (closed) { console.log('closed'); } else { - shells.push(newPolygon().centerCircle(p1, 0.08, 10)); + shells.push(newPolygon().centerCircle(p1, p1.r/2, 10)); } } } @@ -604,7 +647,6 @@ function thin_type_3(params) { } } // interpolate across the length of the chain segment - if (showChainInterpPoints) for (let indx = 1; indx < steps; indx++) { const inc = indx * step; const cr = p0.r + dr * inc; @@ -612,11 +654,67 @@ function thin_type_3(params) { 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)); + if (showChainInterpPoints) { + if (showMedPoints) { + shells.push(newPolygon().centerCircle({ x:cx, y:cy }, cr/2, 10)); + } + renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy); } - renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy); + nuchain.push({ x:cx, y:cy, r:cr }); } + if (last) { + nuchain.push(p1); + } + } + if (showNuMedLine) { + 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 np2 = { x: np1.x + dy * 10, y: np1.y - dx * 10 }; + let min = { dist: Infinity }; + for (let chain of nuchains) { + for (let i=0; i Date: Wed, 17 Sep 2025 01:21:31 -0400 Subject: [PATCH 04/60] add single (odd) wall claim system for median intercepts --- src/kiri/mode/fdm/post.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 96be3140..e1990c43 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -181,6 +181,7 @@ function thin_type_3(params) { let showInsetPoints = false; let showChainRawPoints = false; let showChainInterpPoints = true; + let showChainIntersect = false; let showNuMedLine = false; let showMedLine = true; let showMedPoints = false; @@ -192,9 +193,10 @@ function thin_type_3(params) { let shells = top.shells; let sparse = top.fill_sparse = []; let scale = 1000; - let midR = offsetN * 0.5; - let minR = midR * 0.75; - let maxR = midR * 1.5; + 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 minSpur = offsetN * 1; let inset; @@ -678,7 +680,6 @@ function thin_type_3(params) { // project inset segment normals onto closest chain if (intersectChains) { - console.log({ chains, nuchains }); let { intersect } = base.util; let { SEGINT } = base.key; inset = POLY.flatten(inset, []); @@ -701,14 +702,21 @@ function thin_type_3(params) { } } if (min.p1) { - thin.push(new Point(np1.x, np1.y)); - thin.push(new Point(min.x, min.y)); - let mr = Math.min(min.p1.r, min.p2.r); + let { p1, p2 } = min; + if (showChainIntersect) { + thin.push(new Point(np1.x, np1.y)); + thin.push(new Point(min.x, min.y)); + } + let mr = Math.min(p1.r, p2.r); if (mr < minR) return; let pop = div(mr); - // todo decide which side has priority to emit single wall - // same is true for odd midlines: 3, 5, etc - if (pop.length === 1) return; + // implement first-to-intersect claim system + // for odd wall counts so that they're not emitted twice + // todo extend to all odd counts + if (pop.length === 1 && p1.claimed) { + return; + } + p1.claimed = true; shells.push(newPolygon().centerCircle({ x: np1.x + dy * (pop[0] - dstep), y: np1.y - dx * (pop[0] - dstep) From ee2d6753798eeff2139ecad2d1d03caf2f51bee7 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 17 Sep 2025 01:33:59 -0400 Subject: [PATCH 05/60] maintain better nominal outer wall width --- src/kiri/mode/fdm/post.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index e1990c43..21fae9a5 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -383,10 +383,11 @@ function thin_type_3(params) { } else if (cr <= maxR * 2) { return [ cr / 2, cr / 2 ]; } else if (cr <= maxR * 3) { - if (cr - maxR * 2 >= minR) { - return [ maxR, cr - maxR * 2, maxR ]; - } else { + let rem = (cr - midR * 2); + if (rem < minR) { return [ cr / 3, cr / 3, cr / 3 ]; + } else { + return [ midR, rem, midR ]; } } else { let divs = (cr / midR) | 0; From 4b64c4b8b0bd355c40a355504c7381a1593bc27f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 17 Sep 2025 10:21:00 -0400 Subject: [PATCH 06/60] place single wall exactly on medial axis vs inter --- src/kiri/mode/fdm/post.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 21fae9a5..4dbb5117 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -718,10 +718,19 @@ function thin_type_3(params) { return; } p1.claimed = true; - shells.push(newPolygon().centerCircle({ - x: np1.x + dy * (pop[0] - dstep), - y: np1.y - dx * (pop[0] - dstep) - }, pop[0], 10)); + if (pop.length === 1) { + // for single wall place it exactly on medial axis + shells.push(newPolygon().centerCircle({ + x: min.x, + y: min.y + }, mr, 12)); + } else { + // otherwise use divisions + shells.push(newPolygon().centerCircle({ + x: np1.x + dy * (pop[0] - dstep), + y: np1.y - dx * (pop[0] - dstep) + }, pop[0], 12)); + } } }); } From 2c8925002b85a741ae736b66d5789db1053102c6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 17 Sep 2025 17:13:34 -0400 Subject: [PATCH 07/60] calculate new inset after first pass extrusion. possible to use new inset for another pass vs recycling medial axis --- src/kiri/mode/fdm/post.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 4dbb5117..95da06cb 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -178,6 +178,8 @@ function thin_type_3(params) { let intersectChains = true; let showNoodle = false; + let showExtrusion = true; + let showExtrudeInset = false; let showInsetPoints = false; let showChainRawPoints = false; let showChainInterpPoints = true; @@ -685,6 +687,7 @@ function thin_type_3(params) { let { SEGINT } = base.key; inset = POLY.flatten(inset, []); for (let poly of inset) { + let nupoly = new Polygon(); 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); @@ -711,28 +714,41 @@ function thin_type_3(params) { let mr = Math.min(p1.r, p2.r); if (mr < minR) return; let pop = div(mr); + let odd = (pop.length % 2 === 1); + let len = Math.ceil(pop.length / 2); + if (odd && p1.claimed) len--; // implement first-to-intersect claim system // for odd wall counts so that they're not emitted twice // todo extend to all odd counts - if (pop.length === 1 && p1.claimed) { + if (len === 0) { return; } - p1.claimed = true; if (pop.length === 1) { + nupoly.push(new Point(min.x, min.y)); // for single wall place it exactly on medial axis + if (showExtrusion) shells.push(newPolygon().centerCircle({ x: min.x, y: min.y }, mr, 12)); } else { + let off = -dstep; // otherwise use divisions - shells.push(newPolygon().centerCircle({ - x: np1.x + dy * (pop[0] - dstep), - y: np1.y - dx * (pop[0] - dstep) - }, pop[0], 12)); + for (let i=0; i<1; i++) { + off += (pop[i] * (i ? 2 : 1)); + if (showExtrusion) + shells.push(newPolygon().centerCircle({ + x: np1.x + dy * off, + y: np1.y - dx * off + }, pop[0], 12)); + } + off += dstep / 2; + nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); } + p1.claimed = true; } }); + if (showExtrudeInset) shells.push(nupoly); } } From a2a5e4d457b0877f618eb43ef28764beb1859077 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 17 Sep 2025 19:05:30 -0400 Subject: [PATCH 08/60] another optimization to maintain nominal wall thickness --- src/kiri/mode/fdm/post.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 95da06cb..52631439 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -383,17 +383,21 @@ function thin_type_3(params) { 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 - midR * 2); if (rem < minR) { return [ cr / 3, cr / 3, cr / 3 ]; - } else { - return [ midR, rem, midR ]; } + return [ midR, rem, midR ]; } else { - let divs = (cr / midR) | 0; - return new Array(divs).fill(cr / divs); + return [ midR, cr - midR * 2, midR ]; + // let divs = (cr / midR) | 0; + // return new Array(divs).fill(cr / divs); } } From 7b2e723fbaeb3ebb19f89021aa745974b65f5a6e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 18 Sep 2025 09:59:12 -0400 Subject: [PATCH 09/60] clean and simplify inset extrude poly --- src/kiri/mode/fdm/post.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 52631439..415e9633 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -185,7 +185,7 @@ function thin_type_3(params) { let showChainInterpPoints = true; let showChainIntersect = false; let showNuMedLine = false; - let showMedLine = true; + let showMedLine = false; let showMedPoints = false; let showMedNormals = false; let showMedRadii = false; @@ -752,7 +752,9 @@ function thin_type_3(params) { p1.claimed = true; } }); - if (showExtrudeInset) shells.push(nupoly); + if (showExtrudeInset) { + shells.push(...nupoly.clean().simplify()); + } } } From 43a083a93a38f412403292951d91229b2e6f70b8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 18 Sep 2025 14:42:41 -0400 Subject: [PATCH 10/60] fix ray/line. add max inset visalization --- src/geo/base.js | 12 +++--- src/geo/polygon.js | 8 +--- src/geo/polygons.js | 1 + src/kiri/mode/fdm/post.js | 85 +++++++++++++++++++++++++++++++-------- 4 files changed, 78 insertions(+), 28 deletions(-) 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/polygon.js b/src/geo/polygon.js index 846515f9..e5a3d9a6 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -2042,13 +2042,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; diff --git a/src/geo/polygons.js b/src/geo/polygons.js index fcbd1303..1d5c5212 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -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/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 415e9633..650eb3c6 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -175,7 +175,8 @@ function thin_type_3(params) { let simplifyChain = false; let shortenAtNexus = false; let shortenEdgeSpur = false; - let intersectChains = true; + let intersectChains = false; + let maxInsetBypass = true; let showNoodle = false; let showExtrusion = true; @@ -185,7 +186,7 @@ function thin_type_3(params) { let showChainInterpPoints = true; let showChainIntersect = false; let showNuMedLine = false; - let showMedLine = false; + let showMedLine = true; let showMedPoints = false; let showMedNormals = false; let showMedRadii = false; @@ -201,38 +202,90 @@ function thin_type_3(params) { let maxF = midR * 2; // max interior extrusion width let minSpur = offsetN * 1; + let insets = []; let inset; let dstep = minR / 4; + let dtotl = 0; top.gaps = remain; - if (true) { - let dtotl = 0; + if (maxInsetBypass || intersectChains) { inset = POLY.offset(noodle, -dstep, { join: ClipperLib.JoinType.jtRound, arc: (1/dstep) * 4 }); while (inset && inset.length) { dtotl += dstep; - let ps = POLY.flatten(inset); - for (let poly of ps) { - for (let p of poly.segment(minR).points) { - let d = dtotl * 0+0.05; - // let z = dtotl * 10; - if (showInsetPoints) - shells.append( newPolygon().centerCircle(p, d, 10).setZ(z) ); - } + for (let p of POLY.flatten(inset, [])) { + p.dtotl = dtotl; } + insets.push(inset); + let ps = POLY.flatten(inset); POLY.setZ(inset, z); shells.appendAll(inset); - if (true || dtotl >= maxR) { + if (intersectChains) { break; } - inset = POLY.expand(inset, -dstep); + // if (dtotl >= maxR * 3) { + // break; + // } + inset = POLY.offset(inset, -dstep, { + // join: ClipperLib.JoinType.jtRound, + // arc: (1/dstep) * 4 + minArea: 0 + }); } } - // shells.appendAll(noodle); - // return { last, gaps }; + // project from first inset and find greated offset intersection + if (maxInsetBypass) { + // shells.appendAll(noodle); + // return { last, gaps }; + let { intersectRayLine } = base.util; + let tests = insets.slice(1); + let source = POLY.flatten(insets[0], []); + console.log({ source, tests }); + 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) { + thin.push(new Point(np1.x, np1.y, z)); + thin.push(new Point(max.int.x, max.int.y, z)); + } + }); + } + + shells.appendAll(noodle); + return { last, gaps }; + } let pointMap = new Map(); let lineMap = new Map(); From afcacc89b3c88f1188cb9ff2a34822af9f1f50aa Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 18 Sep 2025 16:39:28 -0400 Subject: [PATCH 11/60] use intersect offset instead of approximate medial axis radius --- src/kiri/mode/fdm/post.js | 52 ++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 650eb3c6..44f40217 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -175,18 +175,18 @@ function thin_type_3(params) { let simplifyChain = false; let shortenAtNexus = false; let shortenEdgeSpur = false; - let intersectChains = false; - let maxInsetBypass = true; + let intersectChains = true; + let maxInsetBypass = false; let showNoodle = false; + let showMedLine = false; + let showInset = false; let showExtrusion = true; let showExtrudeInset = false; - let showInsetPoints = false; let showChainRawPoints = false; let showChainInterpPoints = true; let showChainIntersect = false; let showNuMedLine = false; - let showMedLine = true; let showMedPoints = false; let showMedNormals = false; let showMedRadii = false; @@ -221,16 +221,13 @@ function thin_type_3(params) { insets.push(inset); let ps = POLY.flatten(inset); POLY.setZ(inset, z); - shells.appendAll(inset); + if (showInset) { + shells.appendAll(inset); + } if (intersectChains) { break; } - // if (dtotl >= maxR * 3) { - // break; - // } inset = POLY.offset(inset, -dstep, { - // join: ClipperLib.JoinType.jtRound, - // arc: (1/dstep) * 4 minArea: 0 }); } @@ -238,8 +235,6 @@ function thin_type_3(params) { // project from first inset and find greated offset intersection if (maxInsetBypass) { - // shells.appendAll(noodle); - // return { last, gaps }; let { intersectRayLine } = base.util; let tests = insets.slice(1); let source = POLY.flatten(insets[0], []); @@ -739,12 +734,15 @@ function thin_type_3(params) { } // project inset segment normals onto closest chain - if (intersectChains) { - let { intersect } = base.util; - let { SEGINT } = base.key; - inset = POLY.flatten(inset, []); - for (let poly of inset) { - let nupoly = new Polygon(); + if (intersectChains) for (let insetp of inset) { + let { intersectRayLine } = base.util; + let parent = new Polygon(); + let inner = []; + for (let poly of insetp.flattenTo([])) { + let nupoly = poly.parent ? new Polygon() : parent; + if (poly.parent) { + inner.push(nupoly); + } 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); @@ -756,8 +754,8 @@ function thin_type_3(params) { for (let i=0; i Date: Thu, 18 Sep 2025 18:28:53 -0400 Subject: [PATCH 12/60] skip chains and use medial axis segments directly --- src/kiri/mode/fdm/post.js | 40 ++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 44f40217..06ca29c1 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -186,7 +186,7 @@ function thin_type_3(params) { let showChainRawPoints = false; let showChainInterpPoints = true; let showChainIntersect = false; - let showNuMedLine = false; + let showNuMedLine = true; let showMedPoints = false; let showMedNormals = false; let showMedRadii = false; @@ -627,6 +627,7 @@ function thin_type_3(params) { // gather point offsets into shells let zi = z; let nuchains = []; + let nusegs = []; for (let chain of chains) { let nuchain = []; nuchains.push(nuchain); @@ -727,6 +728,7 @@ function thin_type_3(params) { for (let i=0; i Date: Thu, 18 Sep 2025 19:49:06 -0400 Subject: [PATCH 13/60] add alt seg generator. prepare to drop chains --- src/kiri/mode/fdm/post.js | 295 ++++---------------------------------- 1 file changed, 24 insertions(+), 271 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 06ca29c1..3555c3c9 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -171,12 +171,9 @@ function thin_type_3(params) { top.gaps = remain; - let mergeChains = false; - let simplifyChain = false; - let shortenAtNexus = false; - let shortenEdgeSpur = false; let intersectChains = true; let maxInsetBypass = false; + let altNuSegs = true; let showNoodle = false; let showMedLine = false; @@ -184,13 +181,8 @@ function thin_type_3(params) { let showExtrusion = true; let showExtrudeInset = false; let showChainRawPoints = false; - let showChainInterpPoints = true; let showChainIntersect = false; - let showNuMedLine = true; - let showMedPoints = false; - let showMedNormals = false; - let showMedRadii = false; - let showNexuses = false; + let showNuMedLine = false; let thin = top.thin_fill = []; let shells = top.shells; @@ -200,7 +192,6 @@ function thin_type_3(params) { 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 minSpur = offsetN * 1; let insets = []; let inset; @@ -333,6 +324,7 @@ function thin_type_3(params) { } let chains = []; + let nusegs = []; // for each standalone noodle, construct medial axis // map medial axis points into de-duplicated segments @@ -355,37 +347,22 @@ function thin_type_3(params) { 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 (shortenEdgeSpur) - 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); + if (altNuSegs) { + // add to nusegs + if (len <= minR) { + nusegs.push({ p0, p1 }); } else { - // both points eliminated - pointMap.delete(p0.key); - pointMap.delete(p1.key); - lineMap.delete(seg.key); - continue; + let steps = Math.ceil(len / minR) + 1; + let dx = (p1.x - p0.x) / steps; + let dy = (p1.y - p0.y) / steps; + for (let i=0; i c.total >= minR); - chains.sort((c1,c2) => c2.total - c1.total); - - 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 - if (mergeChains) - for (let chain of chains) { - chain.closed = (chain[0] === chain.peek()); - 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 - if (shortenAtNexus) - 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; - shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts)); - } - - // Keep endpoints; drop interior points closer than R from the last kept point. - function dropClosePoints(chain, R) { - if (!chain || chain.length <= 2 || R <= 0) { - return chain; - } - let last = chain[0]; - let out = [ last ]; - let R2 = R * R; - for (let i = 1; i < chain.length - 1; i++) { - let dx = chain[i].x - last.x, - dy = chain[i].y - last.y; - if (dx * dx + dy * dy >= R2) { - out.push(chain[i]); - last = chain[i]; - } - } - out.push(chain[chain.length - 1]); - return out; - } - - // drop chain points < minR apart - if (simplifyChain) { - chains = chains.map(chain => dropClosePoints(chain, minR)); - } - - function renderPointNormal(pt, ndx, ndy) { - if (showMedNormals) { - 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 (showMedRadii) { - const Sx = pt.x + ndy * pt.r; - const Sy = pt.y - ndx * pt.r; - 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; - shells.push(newPolygon().centerCircle({x:px, y:py}, r, 10)); - acc += r * 2; - } - } -} + // chains = chains.filter(c => c.total >= minR); // gather point offsets into shells let zi = z; let nuchains = []; - let nusegs = []; + if (!altNuSegs) for (let chain of chains) { let nuchain = []; nuchains.push(nuchain); @@ -672,63 +473,25 @@ function thin_type_3(params) { if (first || mid) { nuchain.push(p0); } - // for length 2, first and last segment are the same - if (showMedPoints) { - if (first) { - // todo: handle closed - shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); - } - if (mid) { - // mid segments - shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); - } - if (last) { - // todo: handle closed - shells.push(newPolygon().centerCircle(p0, p0.r/2, 10)); - if (closed) { - console.log('closed'); - } else { - shells.push(newPolygon().centerCircle(p1, p1.r/2, 10)); - } - } - } - { - // 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 for (let indx = 1; indx < steps; 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 (showChainInterpPoints) { - if (showMedPoints) { - shells.push(newPolygon().centerCircle({ x:cx, y:cy }, cr/2, 10)); - } - renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy); - } nuchain.push({ x:cx, y:cy, r:cr }); } if (last) { nuchain.push(p1); } } - if (showNuMedLine) { - for (let i=0; i Date: Thu, 18 Sep 2025 21:34:44 -0400 Subject: [PATCH 14/60] extract and somewhat generalize trace_noodle --- src/kiri/mode/fdm/post.js | 263 +++++++++++--------------------------- 1 file changed, 75 insertions(+), 188 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 3555c3c9..1f3cbb0d 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -167,69 +167,78 @@ function thin_type_3(params) { let { z, top, count, offsetN, last, gaps } = 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.gaps = remain; - let intersectChains = true; - let maxInsetBypass = false; - let altNuSegs = true; - - let showNoodle = false; - let showMedLine = false; - let showInset = false; - let showExtrusion = true; - let showExtrudeInset = false; - let showChainRawPoints = false; - let showChainIntersect = false; - let showNuMedLine = false; + // render inset "noodle" + // shells.appendAll(noodle); let thin = top.thin_fill = []; let shells = top.shells; let sparse = top.fill_sparse = []; - let scale = 1000; + 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, polys } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { + brute: false, + showExtrusion: true, + showExtrudeInset: true, + showChainIntersect: false + }); + + console.log({ lines, polys }); + + POLY.setZ([...lines, ...polys], z); + thin.push(...lines); + shells.push(...polys); + + return { last, gaps }; +} + +// trace a single extrusion line around the inside of the noodle +function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { + brute, + showExtrusion, + showExtrudeInset, + showChainIntersect, +}) { + let scale = 1000; + + let polys = []; + let lines = []; let insets = []; - let inset; let dstep = minR / 4; let dtotl = 0; - top.gaps = remain; - if (maxInsetBypass || intersectChains) { - 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); - let ps = POLY.flatten(inset); - POLY.setZ(inset, z); - if (showInset) { - shells.appendAll(inset); - } - if (intersectChains) { - break; - } - inset = POLY.offset(inset, -dstep, { - minArea: 0 - }); + 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); + polys.appendAll(inset); + if (!brute) { + break; + } + inset = POLY.offset(inset, -dstep, { + minArea: 0 + }); } // project from first inset and find greated offset intersection - if (maxInsetBypass) { + if (brute) { let { intersectRayLine } = base.util; let tests = insets.slice(1); let source = POLY.flatten(insets[0], []); - console.log({ source, tests }); for (let poly of source) { // segment the smallest inset and test against all other insets poly.segment(minR).forEachSegment((p1, p2) => { @@ -263,14 +272,12 @@ function thin_type_3(params) { } } if (max.int) { - thin.push(new Point(np1.x, np1.y, z)); - thin.push(new Point(max.int.x, max.int.y, z)); + lines.push(new Point(np1.x, np1.y)); + lines.push(new Point(max.int.x, max.int.y)); } }); } - - shells.appendAll(noodle); - return { last, gaps }; + return { lines, polys }; } let pointMap = new Map(); @@ -323,12 +330,10 @@ function thin_type_3(params) { return rec; } - let chains = []; let nusegs = []; // 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 }); @@ -343,60 +348,23 @@ function thin_type_3(params) { } } - // connect segments into chains + // break down long segments for (let seg of lineMap.values()) { let { p0, p1 } = seg; let len = seg.len = pointDist(p0, p1); - if (altNuSegs) { - // add to nusegs - if (len <= minR) { + // add to nusegs + if (len <= minR) { + nusegs.push({ p0, p1 }); + } else { + let steps = Math.ceil(len / minR) + 1; + let dx = (p1.x - p0.x) / steps; + let dy = (p1.y - p0.y) / steps; + for (let i=0; i c.total >= minR); - - // gather point offsets into shells - let zi = z; - let nuchains = []; - if (!altNuSegs) - for (let chain of chains) { - let nuchain = []; - nuchains.push(nuchain); - // zi += 0.1; - if (showChainRawPoints) - for (let pt of chain) { - 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 Date: Thu, 18 Sep 2025 22:07:08 -0400 Subject: [PATCH 15/60] further abstract trace_noodle --- src/kiri/mode/fdm/post.js | 43 ++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 1f3cbb0d..ba17e491 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -168,9 +168,7 @@ function thin_type_3(params) { // produce trace from outside of poly inward no more than max inset let noodleWidth = offsetN * count; - let { noodle, remain } = top.poly.noodle(noodleWidth); - - top.gaps = remain; + let { noodle,} = top.poly.noodle(noodleWidth); // render inset "noodle" // shells.appendAll(noodle); @@ -184,10 +182,8 @@ function thin_type_3(params) { let maxR = midR * 1.5; // max double wall extrusion width let maxF = midR * 2; // max interior extrusion width - let { lines, polys } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { + let { lines, polys, remain, trace } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { brute: false, - showExtrusion: true, - showExtrudeInset: true, showChainIntersect: false }); @@ -197,18 +193,26 @@ function thin_type_3(params) { thin.push(...lines); shells.push(...polys); + // show extrusion + for (let point of trace) { + shells.push(newPolygon().centerCircle(point, point.r, 12)); + } + + // show remaining noodle after single trace + shells.push(...remain); + return { last, gaps }; } // trace a single extrusion line around the inside of the noodle function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { brute, - showExtrusion, - showExtrudeInset, showChainIntersect, }) { let scale = 1000; + let remain = []; + let trace = []; let polys = []; let lines = []; let insets = []; @@ -385,8 +389,6 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { return [ midR, rem, midR ]; } else { return [ midR, cr - midR * 2, midR ]; - // let divs = (cr / midR) | 0; - // return new Array(divs).fill(cr / divs); } } @@ -436,24 +438,21 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { nupoly.push(new Point(min.x, min.y)); return; } + let op; if (pop.length === 1) { nupoly.push(new Point(min.x, min.y)); // for single wall place it exactly on medial axis - if (showExtrusion) - polys.push(newPolygon().centerCircle({ - x: min.x, - y: min.y - }, mr, 12)); + trace.push(op = { x: min.x, y: min.y, r: mr }) } else { let off = -dstep; // otherwise use divisions for (let i=0; i<1; i++) { off += (pop[i] * (i ? 2 : 1)); - if (showExtrusion) - polys.push(newPolygon().centerCircle({ + trace.push(op = { x: np1.x + dy * off, - y: np1.y - dx * off - }, pop[0], 12)); + y: np1.y - dx * off, + r: pop[0] + }) } off += dstep / 2; nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); @@ -465,12 +464,10 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { }); } // subtract inner extrusion offset from parent - if (showExtrudeInset) { - polys.push(...POLY.subtract([ parent ], inner, [])); - } + remain.push(...POLY.subtract([ parent ], inner, [])); } - return { lines, polys }; + return { lines, polys, remain, trace }; } /** From a78ec87fb72cf12505dfaa9256b5ef45e7824f0b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 19 Sep 2025 10:36:27 -0400 Subject: [PATCH 16/60] fix post-noodle-trace subtraction --- src/kiri/mode/fdm/post.js | 71 +++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index ba17e491..dba0aa69 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -182,31 +182,42 @@ function thin_type_3(params) { let maxR = midR * 1.5; // max double wall extrusion width let maxF = midR * 2; // max interior extrusion width - let { lines, polys, remain, trace } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { - brute: false, - showChainIntersect: false - }); + for (let i=0; i<1; i++) { + console.log({ noodle }); - console.log({ lines, polys }); + let { lines, polys, remain, trace } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { + brute: false, + showMedialAxis: i > 0, + showChainIntersect: i > 0 + }); - POLY.setZ([...lines, ...polys], z); - thin.push(...lines); - shells.push(...polys); + console.log({ lines, polys, remain, trace }); - // show extrusion - for (let point of trace) { - shells.push(newPolygon().centerCircle(point, point.r, 12)); + POLY.setZ([...lines, ...polys], z); + thin.push(...lines); + shells.push(...polys); + + // show extrusion + if (i === 0) + for (let point of trace) { + shells.push(newPolygon().centerCircle(point, point.r, 12)); + } + + // show remaining noodle after single trace + shells.push(...POLY.flatten(remain)); + + noodle = [ POLY.nest(remain)[3] ]; + // break; } - // show remaining noodle after single trace - shells.push(...remain); - return { last, gaps }; } -// trace a single extrusion line around the inside of the noodle +// trace a single extrusion line around the inside of the noodle poly function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { brute, + showInset, + showMedialAxis, showChainIntersect, }) { let scale = 1000; @@ -229,7 +240,9 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { p.dtotl = dtotl; } insets.push(inset); - polys.appendAll(inset); + if (showInset) { + polys.appendAll(inset); + } if (!brute) { break; } @@ -359,6 +372,9 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { // add to nusegs if (len <= minR) { nusegs.push({ p0, p1 }); + if (showMedialAxis) { + lines.push(new Point(p0.x, p0.y), new Point(p1.x, p1.y)); + } } else { let steps = Math.ceil(len / minR) + 1; let dx = (p1.x - p0.x) / steps; @@ -366,6 +382,9 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { for (let i=0; i { + return p.clean().simplify(); + }).flat() + ); + remain.push(...ret); } return { lines, polys, remain, trace }; From c39576ce37ffc4bcbec6ace0e27b612a5f79f5a4 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 19 Sep 2025 11:06:58 -0400 Subject: [PATCH 17/60] filter remaining trace polygons to >= minArea --- src/kiri/mode/fdm/post.js | 78 +++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index dba0aa69..33b176d1 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -182,51 +182,67 @@ function thin_type_3(params) { let maxR = midR * 1.5; // max double wall extrusion width let maxF = midR * 2; // max interior extrusion width - for (let i=0; i<1; i++) { - console.log({ noodle }); + let lines = []; + let polys = []; + let trace = []; - let { lines, polys, remain, trace } = trace_noodle(noodle, noodleWidth, minR, midR, maxR, { + for (let i=0; i 0, - showChainIntersect: i > 0 + showMedialAxis: false, + showChainIntersect: false }); - console.log({ lines, polys, remain, trace }); - - POLY.setZ([...lines, ...polys], z); - thin.push(...lines); - shells.push(...polys); - - // show extrusion - if (i === 0) - for (let point of trace) { - shells.push(newPolygon().centerCircle(point, point.r, 12)); - } + console.log({ remain }); // show remaining noodle after single trace shells.push(...POLY.flatten(remain)); - noodle = [ POLY.nest(remain)[3] ]; - // break; + noodle = remain; } + console.log({ lines, polys, trace }); + + // show extrusion + for (let point of trace) { + shells.push(newPolygon().centerCircle(point, point.r, 12)); + } + + POLY.setZ([...lines, ...polys], z); + thin.push(...lines); + shells.push(...polys); + return { last, gaps }; } // trace a single extrusion line around the inside of the noodle poly -function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { - brute, - showInset, - showMedialAxis, - showChainIntersect, -}) { - let scale = 1000; - - let remain = []; - let trace = []; - let polys = []; - let lines = []; +function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { + let { + brute, + showInset, + showMedialAxis, + showChainIntersect, + minArea = midR * midR, + lines = [], + polys = [], + remain = [], + trace = [], + } = opt; let insets = []; + let scale = 1000; let dstep = minR / 4; let dtotl = 0; @@ -498,7 +514,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, { POLY.flatten(setA, [], true).map(p => { return p.clean().simplify(); }).flat() - ); + ).filter(p => p.area() >= minArea); remain.push(...ret); } From e55a8328e34d37585ae011884e2a027fd32df1bc Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 19 Sep 2025 12:13:02 -0400 Subject: [PATCH 18/60] re-add gaps visualization. remove unused inset iteration for single intersections --- src/kiri/mode/fdm/post.js | 48 ++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 33b176d1..80955d23 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -164,11 +164,14 @@ 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 noodleWidth = offsetN * count; - let { noodle,} = top.poly.noodle(noodleWidth); + let { noodle, remain } = top.poly.noodle(noodleWidth); + + // remaining area for infill after shells are extruded + let gaps = top.gaps = remain; // render inset "noodle" // shells.appendAll(noodle); @@ -189,6 +192,7 @@ function thin_type_3(params) { for (let i=0; i Date: Fri, 19 Sep 2025 12:50:42 -0400 Subject: [PATCH 19/60] start passing back thin wall data to slice --- src/kiri/core/slice.js | 3 ++- src/kiri/mode/fdm/post.js | 28 ++++++++++++++-------------- src/kiri/mode/fdm/slice.js | 16 ++++++++++------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 934f7394..6982d2cf 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -144,7 +144,8 @@ 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.fill_lines = data.fill_lines; top.fill_sparse = data.fill_sparse; top.fill_off = data.fill_off; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 80955d23..e26bbc4f 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -170,15 +170,14 @@ function thin_type_3(params) { let noodleWidth = offsetN * count; let { noodle, remain } = top.poly.noodle(noodleWidth); - // remaining area for infill after shells are extruded - let gaps = top.gaps = remain; - // render inset "noodle" // shells.appendAll(noodle); - let thin = top.thin_fill = []; - let shells = top.shells; - let sparse = top.fill_sparse = []; + let debugRemain = false; + let debugExtrusion = false; + + let shells = top.shells; // only used for debug + let trace = top.thin_wall = []; let midR = offsetN * 0.5; // nominal extrusion width let minR = midR * 0.75; // smallest interior fill or single wall width @@ -187,7 +186,6 @@ function thin_type_3(params) { let lines = []; let polys = []; - let trace = []; for (let i=0; i newPolygon().centerCircle(p, p.r, 12).setZ(slice.z)), vopt({ offset, height })); + + 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); From b1eee58c1e74e8a339897b6f270cff39c75f59bd Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 19 Sep 2025 20:26:34 -0400 Subject: [PATCH 20/60] annotate thinwall points with shell number --- src/kiri/mode/fdm/post.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index e26bbc4f..e3ae6d93 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -199,6 +199,7 @@ function thin_type_3(params) { i === 0 ? midR : midR * 1.25, i === 0 ? maxR : maxF, { + shell: 0, lines, polys, remain, @@ -242,6 +243,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { polys = [], remain = [], trace = [], + shell = 0 } = opt; let insets = []; let scale = 1000; @@ -437,6 +439,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { let parent = new Polygon(); let inner = []; for (let poly of insetp.flattenTo([])) { + let sno = poly.parent ? shell : -shell; // shell number (- for inner/hole) let nupoly = poly.parent ? new Polygon() : parent; if (poly.parent) { inner.push(nupoly); @@ -481,7 +484,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (pop.length === 1) { // for single wall place it exactly on medial axis nupoly.push(new Point(min.x, min.y)); - trace.push(op = { x: min.x, y: min.y, r: mr }) + trace.push(op = { x: min.x, y: min.y, r: mr, sno }) } else { // otherwise use first division // compensate for minimal inset @@ -489,7 +492,8 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { trace.push(op = { x: np1.x + dy * off, y: np1.y - dx * off, - r: pop[0] + r: pop[0], + sno }) // trace inset by full diameter nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); From d5b9d5d8f731fc4b9ae3701c1035d42702c50ae1 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 01:13:13 -0400 Subject: [PATCH 21/60] test output using point radius for extrusion width --- src/kiri/mode/fdm/prepare.js | 21 +++++++++++++++++++-- src/kiri/mode/fdm/slice.js | 10 +++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index c7a90638..455f5250 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,20 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } + function outputAdaptiveWalls(thin, opt = {}) { + let np; + for (let pt of thin) { + let { lastPoint, lastEmit } = print; + let { x, y, r } = pt; + np = new Point(x, y, z); + if (lastPoint && lastPoint.distTo2D(np) > retractDist) { + print.addOutput(preout, np, 0, moveSpeed); + } else { + print.addOutput(preout, np, (r * 2) / nozzleSize, printSpeed); + } + } + } + /** * output sparse infill * @param {Polygon[]} polys @@ -1444,6 +1458,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); + // 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 fb854b9b..32cb2f52 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -997,10 +997,14 @@ 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 })); + if (top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z)), 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 })); @@ -1017,10 +1021,6 @@ function doRender(slice, isSynth, params, opt = {}) { .setLayer("thin fill", Color.thin) .addLines(top.thin_fill, vopt({ offset, height })); - if (top.thin_wall?.length) output - .setLayer("shells", Color.thin) - .addPolys(top.thin_wall.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z)), vopt({ offset, height })); - if (top.gaps?.length && devel) output .setLayer("gaps", Color.gaps) .addPolys(top.gaps, vopt({ offset, height, thin: true })); From 8dfd40bf692f5a5ad830757888f7a15d947acd5b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 10:57:11 -0400 Subject: [PATCH 22/60] apply thin wall shell extrusion multiplier --- src/kiri/mode/fdm/prepare.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 455f5250..4069e7a0 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1098,7 +1098,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (lastPoint && lastPoint.distTo2D(np) > retractDist) { print.addOutput(preout, np, 0, moveSpeed); } else { - print.addOutput(preout, np, (r * 2) / nozzleSize, printSpeed); + print.addOutput(preout, np, ((r * 2) / nozzleSize) * shellMult, printSpeed); } } } From 82a429c13ab05649c7c476544009d5e62033f15f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 14:56:56 -0400 Subject: [PATCH 23/60] add shell width stepping --- src/kiri/mode/fdm/post.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index e3ae6d93..ecd892ca 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -239,12 +239,15 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { showMedialAxis, showChainIntersect, minArea = midR * midR, + shellStep = 1 / 30, // diameter step as % of nozzle lines = [], polys = [], remain = [], trace = [], shell = 0 } = opt; + + let sstep = 1 / shellStep; let insets = []; let scale = 1000; let dstep = minR / 4; @@ -482,17 +485,20 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { } let op; 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)); trace.push(op = { x: min.x, y: min.y, r: mr, sno }) } 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 + pop[0]; + let off = -dstep + rad_step; trace.push(op = { x: np1.x + dy * off, y: np1.y - dx * off, - r: pop[0], + r: rad_step, sno }) // trace inset by full diameter From e2c5fc4a988d46d28deb6968095acc1b507ffa9d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 15:20:26 -0400 Subject: [PATCH 24/60] repeeat last point of trace if close enough to close --- src/kiri/core/render.js | 3 +-- src/kiri/mode/fdm/post.js | 7 +++++++ src/kiri/mode/fdm/slice.js | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) 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/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index ecd892ca..742df723 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -447,6 +447,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (poly.parent) { inner.push(nupoly); } + let fpo; 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); @@ -507,8 +508,14 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (odd && !mp1.claimed) { mp1.claimed = true; } + // store first trace out for this poly to repeat at the end + fpo = fpo ?? trace.peek(); } }); + // repeat first point of trace (if close enough to be closed) + if (fpo && pointDist(fpo, trace.peek()) <= maxR) { + trace.push(fpo); + } } // subtract inner extrusion offsets from parent extrusion offset diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 32cb2f52..62c5d7ca 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -1001,10 +1001,14 @@ function doRender(slice, isSynth, params, opt = {}) { .setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) .addPolys(top.shells || [], vopt({ offset, height, clean: true })); - if (top.thin_wall?.length) output + if (isThin && top.thin_wall?.length) output .setLayer("walls", Color.thin) .addPolys(top.thin_wall.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z)), vopt({ offset, height })); + if (!isThin && top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(p => newPolygon().centerCircle(p, p.r, 3).setZ(slice.z)), 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 })); From 07213ddc790748f326c91098dcb81fc4b994c510 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 16:24:33 -0400 Subject: [PATCH 25/60] annotate thin shells for output ordering --- src/geo/polygon.js | 8 ++++++ src/kiri/core/slice.js | 1 + src/kiri/mode/fdm/post.js | 51 ++++++++++++++++++++++++++---------- src/kiri/mode/fdm/prepare.js | 11 +++++--- src/kiri/mode/fdm/slice.js | 10 +++++-- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index e5a3d9a6..f81ebbb6 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(); diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 6982d2cf..d684d046 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -146,6 +146,7 @@ class Slice { let top = new Top(data.poly); 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/post.js b/src/kiri/mode/fdm/post.js index 742df723..40d3ebcc 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -177,7 +177,7 @@ function thin_type_3(params) { let debugExtrusion = false; let shells = top.shells; // only used for debug - let trace = top.thin_wall = []; + 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 @@ -199,11 +199,11 @@ function thin_type_3(params) { i === 0 ? midR : midR * 1.25, i === 0 ? maxR : maxF, { - shell: 0, + shell: i + 1, lines, polys, remain, - trace, + traces, brute: false, showMedialAxis: false, showChainIntersect: false @@ -228,7 +228,12 @@ function thin_type_3(params) { if (lines.length) top.thin_fill = lines; if (polys.length) shells.push(...polys); - return { trace, last: remain }; + // create shell depth annotation that will survive serialization + top.thin_sort = traces.map(a => 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 @@ -243,7 +248,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { lines = [], polys = [], remain = [], - trace = [], + traces = [], shell = 0 } = opt; @@ -447,7 +452,9 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (poly.parent) { inner.push(nupoly); } - let fpo; + let ltpo; + let trace = []; + trace.shell = sno; 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); @@ -489,7 +496,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { // 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)); - trace.push(op = { x: min.x, y: min.y, r: mr, sno }) + trace.push(op = { x: min.x, y: min.y, r: mr }) } else { // radius rounded to nearest step let rad_step = ((pop[0] * sstep) | 0) / sstep; @@ -499,8 +506,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { trace.push(op = { x: np1.x + dy * off, y: np1.y - dx * off, - r: rad_step, - sno + r: rad_step }) // trace inset by full diameter nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); @@ -508,13 +514,29 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (odd && !mp1.claimed) { mp1.claimed = true; } - // store first trace out for this poly to repeat at the end - fpo = fpo ?? trace.peek(); + // 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 = sno; + // 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 (fpo && pointDist(fpo, trace.peek()) <= maxR) { - trace.push(fpo); + if (trace[0] && pointDist(trace[0], trace.peek()) <= maxR) { + trace.push(trace[0]); + } + if (trace.length) { + traces.push(trace); } } @@ -534,10 +556,11 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { return p.clean().simplify(); }).flat() ).filter(p => p.area() >= minArea); + remain.push(...ret); } - return { lines, polys, remain, trace }; + return { lines, polys, remain, traces }; } /** diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 4069e7a0..03f5f6cd 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1089,9 +1089,14 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } - function outputAdaptiveWalls(thin, opt = {}) { + function outputAdaptiveWalls(thin, sort, opt = {}) { + // restore thin shell order annotation and sort on it + thin.forEach((a,i) => a.shell = sort[i]); + thin.sort((a,b) => (a.shell - b.shell) * shellOrder); + // perform nearest poly to poly algorithm on shells let np; - for (let pt of thin) { + for (let trace of thin) + for (let pt of trace) { let { lastPoint, lastEmit } = print; let { x, y, r } = pt; np = new Point(x, y, z); @@ -1459,7 +1464,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (top.traces) outputTraces(top.traces); // thin wall v3 - if (top.thin_wall) outputAdaptiveWalls(top.thin_wall); + if (top.thin_wall) outputAdaptiveWalls(top.thin_wall, top.thin_sort); // innermost shells let inner = next.innerShells() || []; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 62c5d7ca..291dcf24 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -1003,11 +1003,17 @@ function doRender(slice, isSynth, params, opt = {}) { if (isThin && top.thin_wall?.length) output .setLayer("walls", Color.thin) - .addPolys(top.thin_wall.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z)), vopt({ offset, height })); + .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(p => newPolygon().centerCircle(p, p.r, 3).setZ(slice.z)), vopt({ offset, height })); + .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) From 6a44654714e8026d7bf5260753aabb2e2ebc90ee Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 18:17:56 -0400 Subject: [PATCH 26/60] update single width extrusion claim system. does not require splitting medial axis segments --- src/geo/point.js | 7 ++++- src/geo/polygon.js | 7 +++-- src/kiri/mode/fdm/post.js | 54 +++++++++++++-------------------------- 3 files changed, 29 insertions(+), 39 deletions(-) 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 f81ebbb6..7eee6ad0 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -2322,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; @@ -2334,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; } @@ -2348,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/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 40d3ebcc..d27f27dd 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -218,11 +218,14 @@ function thin_type_3(params) { // console.log({ lines, polys, trace }); // show extrusion + let zo = z + 0.1; if (debugExtrusion) + for (let trace of traces) for (let point of trace) { - polys.push(newPolygon().centerCircle(point, point.r, 12).setZ(z)); + polys.push(newPolygon().centerCircle(point, point.r, 12).setZ(zo = zo + 0.005)); } + if (!debugExtrusion) POLY.setZ([...lines, ...polys], z); if (lines.length) top.thin_fill = lines; @@ -279,7 +282,11 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { }); } - // project from first inset and find greated offset intersection + // 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); @@ -375,8 +382,6 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { return rec; } - let nusegs = []; - // for each standalone noodle, construct medial axis // map medial axis points into de-duplicated segments for (let poly of noodle) { @@ -393,31 +398,6 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { } } - // break down long segments - for (let seg of lineMap.values()) { - let { p0, p1 } = seg; - let len = seg.len = pointDist(p0, p1); - // add to nusegs - if (len <= minR) { - nusegs.push({ p0, p1 }); - if (showMedialAxis) { - lines.push(new Point(p0.x, p0.y), new Point(p1.x, p1.y)); - } - } else { - let steps = Math.ceil(len / minR) + 1; - let dx = (p1.x - p0.x) / steps; - let dy = (p1.y - p0.y) / steps; - for (let i=0; i { + poly.segment(minR, true).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 min = { dist: Infinity }; - for (let seg of nusegs) { + 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) { @@ -486,24 +469,23 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { 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) len--; + if (odd && mp1.claimed && mp1.claimed !== p1.segment) len--; if (len === 0) { nupoly.push(new Point(min.x, min.y)); return; } - let op; 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)); - trace.push(op = { x: min.x, y: min.y, r: mr }) + trace.push({ x: min.x, y: min.y, r: mr }) } 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; - trace.push(op = { + trace.push({ x: np1.x + dy * off, y: np1.y - dx * off, r: rad_step @@ -512,7 +494,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); } if (odd && !mp1.claimed) { - mp1.claimed = true; + mp1.claimed = p1.segment; } // detect trace jump and start new trace if (ltpo && pointDist(ltpo, trace.peek()) > maxR) { From 7277eed1118c24836bd393b44a7048ad6f393b71 Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Sun, 21 Sep 2025 17:38:45 -0600 Subject: [PATCH 27/60] helical bugfixes --- src/kiri/mode/cam/op-helical.js | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/op-helical.js index 82fc7dbc..3c3aed6a 100644 --- a/src/kiri/mode/cam/op-helical.js +++ b/src/kiri/mode/cam/op-helical.js @@ -138,14 +138,17 @@ export class OpHelical extends CamOp { ); // console.log({down,toBottom,ofFull,ofCircle,startAngle,finalAngle,finalEnd}); - //circle down to final point - poly.addPoints( - arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , { - clockwise, - radius, - center, - }) - ); + //if not already very close to final point + if( ofCircle >= 0.001 ){ + //circle down to final point + poly.addPoints( + arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , { + clockwise, + radius, + center, + }) + ); + } //if starting at the bottom, reverse poly if ( reverse ){ @@ -169,9 +172,9 @@ export class OpHelical extends CamOp { let lineOut = radius - entryOffset; let centerOffset = (entryOffset**2 + 2*entryOffset*lineOut) / (2*(entryOffset + lineOut)); //add entry points - entry.push(... + entry.push( newPoint(0,-lineOut,0), - arcToPath( + ...arcToPath( newPoint(0,-lineOut,0), newPoint(radius,0,0), 128, @@ -186,12 +189,15 @@ export class OpHelical extends CamOp { } } //for each point + //rotate by start angle + // add to hole center + let invert = ((clockwise && !reverse ) || (!clockwise && reverse) ) ? -1 : 1 + let entryAngle = reverse ? finalAngle : startAngle entry = entry.map(p => { - //rotate by start angle - // add to hole center // TODO: there's totally a way to do this with a matrix - p.y *= clockwise ? 1 : -1 - p.rotate(startAngle) + // logical XOR + p.y *= invert + p.rotate(entryAngle) return p.add(entryCenter) }) //append to start of poly From 813ae0d9e67ba16b03b3b426e172bb11158c7724 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 21:20:36 -0400 Subject: [PATCH 28/60] add proper shell output ordering with next closest algorithm --- src/kiri/mode/fdm/post.js | 5 +--- src/kiri/mode/fdm/prepare.js | 54 +++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index d27f27dd..fbdbe115 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -205,7 +205,6 @@ function thin_type_3(params) { remain, traces, brute: false, - showMedialAxis: false, showChainIntersect: false }); @@ -234,8 +233,7 @@ function thin_type_3(params) { // create shell depth annotation that will survive serialization top.thin_sort = traces.map(a => a.shell); - console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces }); - + // console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces }); return { traces, last: remain }; } @@ -244,7 +242,6 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { let { brute, showInset, - showMedialAxis, showChainIntersect, minArea = midR * midR, shellStep = 1 / 30, // diameter step as % of nozzle diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 03f5f6cd..b5bdab6d 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1089,21 +1089,49 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } - function outputAdaptiveWalls(thin, sort, 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]); - thin.sort((a,b) => (a.shell - b.shell) * shellOrder); - // perform nearest poly to poly algorithm on shells + 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; - for (let trace of thin) - for (let pt of trace) { - let { lastPoint, lastEmit } = print; - let { x, y, r } = pt; - np = new Point(x, y, z); - if (lastPoint && lastPoint.distTo2D(np) > retractDist) { - print.addOutput(preout, np, 0, moveSpeed); - } else { - print.addOutput(preout, np, ((r * 2) / nozzleSize) * shellMult, printSpeed); + // 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) { + 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); + 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, printSpeed); + } } } } From cc2615db713b4e3471569b43ebccba843fea77b4 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 21 Sep 2025 23:00:14 -0400 Subject: [PATCH 29/60] fix infill offsetting. implement point reduction. fix move to start on layer up --- src/kiri/mode/fdm/post.js | 28 ++++++++++++++++++++-------- src/kiri/mode/fdm/prepare.js | 3 ++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index fbdbe115..21b7593c 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -170,6 +170,9 @@ function thin_type_3(params) { let noodleWidth = offsetN * count; let { noodle, remain } = top.poly.noodle(noodleWidth); + // re-expand inner offset so fill offsets align properly with expectation + remain = POLY.offset(remain, offsetN / 2); + // render inset "noodle" // shells.appendAll(noodle); @@ -188,7 +191,7 @@ function thin_type_3(params) { let polys = []; for (let i=0; i 1 && lpo.s === npo.s && lpo.r === npo.r) { + trace.pop(); + } + trace.push(npo); if (odd && !mp1.claimed) { mp1.claimed = p1.segment; } diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index b5bdab6d..8c1b7499 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1101,7 +1101,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { 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; + let np = print.lastPoint; // perform nearest poly to poly algorithm on shells for (;;) { let min = { dist: Infinity }; @@ -1114,6 +1114,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } if (!min.trace) { + startPoint = np; return; } let { trace } = min; From 9567a99fd9e4792d1cdf5b4daeca0fd93b4889f0 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 00:09:41 -0400 Subject: [PATCH 30/60] fix thin wall retraction. remove point markers to allow codec between minion n worker --- src/kiri/mode/fdm/post.js | 12 +++++++++--- src/kiri/mode/fdm/prepare.js | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 21b7593c..655a569d 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -430,14 +430,13 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { let parent = new Polygon(); let inner = []; for (let poly of insetp.flattenTo([])) { - let sno = poly.parent ? shell : -shell; // shell number (- for inner/hole) let nupoly = poly.parent ? new Polygon() : parent; if (poly.parent) { inner.push(nupoly); } let ltpo; let trace = []; - trace.shell = sno; + trace.shell = poly.parent ? shell : -shell; // shell number (- for inner/hole) poly.segment(minR, true).forEachSegment((p1, p2) => { let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; let len = pointDist(p1, p2); @@ -511,7 +510,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { // and adding that to the new trace traces.push(trace); let nutrace = [ trace.pop() ]; - nutrace.shell = sno; + nutrace.shell = trace.shell; // close last trace if endpoints near enough if (pointDist(ltpo, trace[0] <= maxR)) { trace.push(trace[0]); @@ -551,6 +550,13 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { 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; + } + } + return { lines, polys, remain, traces }; } diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 8c1b7499..d2bb8897 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1126,6 +1126,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } // 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(); + } for (let pt of trace) { np = new Point(pt.x, pt.y, z); if (pt === trace[0]) { From 6021fe0ffb40e8a69b5c635fba9855241d21239d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 01:57:56 -0400 Subject: [PATCH 31/60] move shell/fill overlap to shell menu. allow micro backlash. increase thin resolution stepping --- src/kiri/mode/fdm/init-menu.js | 5 +++-- src/kiri/mode/fdm/post.js | 2 +- src/kiri/mode/fdm/prepare.js | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) 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 655a569d..3ff454d7 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -247,7 +247,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { showInset, showChainIntersect, minArea = midR * midR, - shellStep = 1 / 30, // diameter step as % of nozzle + shellStep = 1 / 100, // diameter step as % of nozzle lines = [], polys = [], remain = [], diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index d2bb8897..55808023 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1130,12 +1130,13 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { 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, printSpeed); + print.addOutput(preout, np, ((pt.r * 2) / nozzleSize) * shellMult, outputSpeed); } } } @@ -1237,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; From cbfdde341a6a5db685eac4ca0c0f5e4bc626b7d4 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 02:19:31 -0400 Subject: [PATCH 32/60] rename thin wall implementations --- src/kiri/core/consts.js | 6 +++--- src/kiri/mode/fdm/post.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 3ff454d7..7c321bf6 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -601,13 +601,13 @@ 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": + case "adaptive": ret = thin_type_3({ z, top, count, offsetN }); break; default: From e8d5c9e743da31b6bdbca8b35060c4bead338a54 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 09:48:25 -0400 Subject: [PATCH 33/60] bump rev 4.3.1 and fix thinwall closed poly --- package.json | 2 +- src/geo/polygon.js | 6 ++- src/kiri/mode/fdm/post.js | 88 +++++++++++++++++++------------------- src/kiri/mode/fdm/slice.js | 2 +- src/moto/license.js | 4 +- web/kiri/manifest.json | 2 +- 6 files changed, 54 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 5e9bdcc9..82e76bb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.3.0", + "version": "4.3.1", "description": "grid.space 3d slicing & modeling tools", "author": "Stewart Allen ", "license": "MIT", diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 7eee6ad0..2a2be748 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1105,7 +1105,11 @@ export class Polygon { } closeIf(dist = 1) { - if (this.first().distTo2D(this.last()) <= dist) { + let closeDist = this.first().distTo2D(this.last()); + if (closeDist < 0.001) { + this.points.pop(); + return this.setClosed(); + } else if (closeDist <= dist) { return this.setClosed(); } else { return this.setOpen(); diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 7c321bf6..8bf789ff 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -287,50 +287,50 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { // 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 }; - } + // 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(); diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 291dcf24..41472f5d 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -1009,7 +1009,7 @@ function doRender(slice, isSynth, params, opt = {}) { .setLayer("walls", Color.thin) .addPolys(top.thin_wall.map(a => a.length === 1 ? - newPolygon().centerCircle(a[0], a[0].r, 6) : + newPolygon().centerCircle(a[0], a[0].r/2, 12) : newPolygon(a.map(p => newPoint(p.x, p.y))) .setZ(slice.z) .closeIf(height) diff --git a/src/moto/license.js b/src/moto/license.js index ae5ed98c..21ca7569 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -3,9 +3,9 @@ const terms = { COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "4.3.0" + VERSION: "4.3.1" }; -export const beta = 1; +export const beta = 0; export const license = terms; export const version = terms.VERSION; diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index ce4522be..051f5f4c 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.3.0", + "name": "Kiri:Moto 4.3.1", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/kiri/", From 3a4aa465da7ab2ac507e61fff227f3b66ed1430d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 12:09:49 -0400 Subject: [PATCH 34/60] fixup for origin point finder for high res meshes --- src/kiri/mode/cam/cl-origin.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 5b63f8ac..c694f078 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -25,6 +25,16 @@ export function originSelect() { env.hover = pointHover; env.hoverUp = pointHoverUp; + // less precise point key for hi-res meshes that leads to dups + function updateKey(pt) { + pt._key = [ + ((pt.x * 1000) | 0), + ((pt.y * 1000) | 0), + ((pt.z * 1000) | 0) + ].join(' '); + return pt; + } + CAM.traces((ids) => { api.hide.alert(alert); alert = api.show.alert("select origin
[esc] to cancel", 1000); @@ -53,14 +63,14 @@ export function originSelect() { }); for (let poly of polys) { if (poly.circularity() >= 0.99) { - let center = poly.calcCircleCenter(); + let center = updateKey(poly.calcCircleCenter()); if (points[center.key]) { continue; } addpoint(widget, center); } else if (poly.isOpen()) { - incpoint(widget, poly.first()); - incpoint(widget, poly.last()); + incpoint(widget, updateKey(poly.first())); + incpoint(widget, updateKey(poly.last())); } } }); From dfa2308224f4895f74195f9af10744fba9ca3981 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 12:27:56 -0400 Subject: [PATCH 35/60] add todo/note for single wall noodle and claim system --- src/kiri/mode/fdm/post.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 8bf789ff..6f002388 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -468,6 +468,10 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { 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 + // todo: failing for noodle width single wall on circular objects + // update claim system to eliminate overlapping close singles + // and/or extend segment definition to curve within angle deviation + // instead of strict point/point segments if (odd && mp1.claimed && mp1.claimed !== p1.segment) len--; if (len === 0) { nupoly.push(new Point(min.x, min.y)); From 6bf15373b6960303f229fd29a38d2f509f17d51b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 18:07:35 -0400 Subject: [PATCH 36/60] add slice bridge annotations to slow print. fix thin shell depth for identifying outside faces --- src/kiri/mode/fdm/post.js | 47 +++++++++++++++++++++++------------- src/kiri/mode/fdm/prepare.js | 10 ++++++-- src/kiri/mode/fdm/slice.js | 13 +++++++++- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 6f002388..9a9d43e4 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -169,13 +169,15 @@ function thin_type_3(params) { // produce trace from outside of poly inward no more than max inset let noodleWidth = offsetN * count; let { noodle, remain } = top.poly.noodle(noodleWidth); + for (let n of POLY.flatten(noodle).sort((a,b) => a.depth - b.depth)) { + n.shell = [1,-1][Math.floor(n.depth / 2)]; + // console.log('map', n.depth, n.shell, n.length, n.area().round(3)); + } // re-expand inner offset so fill offsets align properly with expectation remain = POLY.offset(remain, offsetN / 2); - // render inset "noodle" - // shells.appendAll(noodle); - + let debugNoodle = false; let debugRemain = false; let debugExtrusion = false; @@ -190,11 +192,14 @@ function thin_type_3(params) { let lines = []; let polys = []; - for (let i=0; i b.area() - a.area())) trace_noodle( [ n ], noodleWidth, @@ -202,7 +207,7 @@ function thin_type_3(params) { i === 0 ? midR : midR * 1.25, i === 0 ? maxR : maxF, { - shell: i + 1, + shell: n.shell, lines, polys, remain: next, @@ -234,9 +239,17 @@ function thin_type_3(params) { if (polys.length) shells.push(...polys); // create shell depth annotation that will survive serialization - top.thin_sort = traces.map(a => a.shell); + top.thin_sort = traces.map(t => t.shell); - // console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces }); + // remove point.segment markers for codec encoding between minion and worker + for (let trace of traces) { + for (let pt of trace) { + delete pt.s; + } + } + + // console.log({ traces: traces.map(t => [t.length, t.shell]) }); + // console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces, polys }); return { traces, last: remain }; } @@ -255,6 +268,8 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { shell = 0 } = opt; + // increment shell with direction (1=outside, -1=inside) + let shellAdd = shell > 0 ? 1 : -1; let sstep = 1 / shellStep; let insets = []; let scale = 1000; @@ -265,6 +280,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { join: ClipperLib.JoinType.jtRound, arc: (1/dstep) * 4 }); + while (inset && inset.length) { dtotl += dstep; for (let p of POLY.flatten(inset, [])) { @@ -434,9 +450,10 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { if (poly.parent) { inner.push(nupoly); } + let sadd = shellAdd > 0 ? (poly.parent ? shellAdd : 0) : (poly.parent ? 0 : shellAdd); let ltpo; let trace = []; - trace.shell = poly.parent ? shell : -shell; // shell number (- for inner/hole) + trace.shell = shell + sadd; poly.segment(minR, true).forEachSegment((p1, p2) => { let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; let len = pointDist(p1, p2); @@ -548,19 +565,15 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { let ret = POLY.nest( POLY.flatten(setA, [], true).map(p => { return p.clean().simplify(); - }).flat() + }).flat().map(p => { + p.shell = shell + shellAdd; + return p; + }) ).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; - } - } - return { lines, polys, remain, traces }; } diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 55808023..96e65cdb 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -805,10 +805,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1, sparseMult = process.outputSparseMult, coastDist = process.outputCoastDist || 0, - finishSpeed = opt.speed || process.outputFinishrate, firstShellSpeed = process.firstLayerRate, firstFillSpeed = process.firstLayerFillRate, firstPrintMult = process.firstLayerPrintMult, + finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate), printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, @@ -834,15 +834,21 @@ 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; } + // update fill speed when solids detected if (slice.finishSolids) { fillSpeed = process.sliceSolidRate || finishSpeed; } + // override: adapt bridge layer speed + if (slice.isBridgeLayer) { + fillSpeed /= 2; + } + // apply first layer extrusion multipliers if (firstLayer) { fillMult *= firstPrintMult; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 41472f5d..8fa865d5 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -498,6 +498,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; let isBottom = slice.index < bottomLayers; let isTop = topLayers && slice.index > slices.length - topLayers - 1; + let isTopBase = isTop && slice.index === slices.length - topLayers; let isDense = range.sliceFillSparse > 0.995; let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; @@ -505,6 +506,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let fillSpace = fillSpacing * spaceMult * solidWidth; doSolidLayerFill(slice, fillSpace, sliceFillAngle); } + if (isTopBase) { + // mark the first top solid supporting layer as a bridge + slice.isBridgeLayer = true; + } sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "solid layers"); @@ -1386,7 +1391,10 @@ export function projectFlats(slice, count, expand) { export function projectBridges(slice, count) { if (!slice.up || !slice.bridges) return; // these flats are marked for finishing print speed - if (slice.bridges.length) slice.finishSolids = true; + if (slice.bridges.length) { + slice.finishSolids = true; + slice.isBridgeLayer = true; + } projectSolid(slice, slice.bridges, count, true, true); }; @@ -1746,6 +1754,9 @@ function fillSupportPolys(args) { * @returns {*} */ function projectSolid(slice, polys, count, up, first) { + if (slice && !up && count === 1) { + slice.isBridgeLayer = true; + } if (!slice || count <= 0) { return; } From 92cfe7f2bd548c9384bc337c7f977f1e73a53b89 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 19:22:03 -0400 Subject: [PATCH 37/60] slow down shells on flats layers --- src/kiri/mode/fdm/prepare.js | 5 +++++ src/kiri/mode/fdm/slice.js | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 96e65cdb..bd676ec2 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -844,6 +844,11 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { fillSpeed = process.sliceSolidRate || finishSpeed; } + // on flats layers also slow down shell print speed + if (slice.isFlatsLayer) { + printSpeed = finishSpeed || printSpeed; + } + // override: adapt bridge layer speed if (slice.isBridgeLayer) { fillSpeed /= 2; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 8fa865d5..f0bc89e2 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -506,6 +506,9 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let fillSpace = fillSpacing * spaceMult * solidWidth; doSolidLayerFill(slice, fillSpace, sliceFillAngle); } + if (slice.index === slices.length - 1) { + slice.isFlatsLayer = true; + } if (isTopBase) { // mark the first top solid supporting layer as a bridge slice.isBridgeLayer = true; @@ -1378,8 +1381,9 @@ function addSolidFills(slice, polys) { export function projectFlats(slice, count, expand) { if (!slice.down || !slice.flats) return; // these flats are marked for finishing print speed - if (slice.flats.length) slice.finishSolids = true; - if (slice && slice.flats && slice.flats.length) { + if (slice.flats?.length) { + slice.finishSolids = true; + slice.isFlatsLayer = true; const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; projectSolid(slice, flats, count, false, true); } @@ -1391,7 +1395,7 @@ export function projectFlats(slice, count, expand) { export function projectBridges(slice, count) { if (!slice.up || !slice.bridges) return; // these flats are marked for finishing print speed - if (slice.bridges.length) { + if (slice.bridges?.length) { slice.finishSolids = true; slice.isBridgeLayer = true; } From d976461ed46cb15b6faed75599a985b94b30a880 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 21:38:02 -0400 Subject: [PATCH 38/60] code formatting --- src/kiri/mode/cam/cl-helical.js | 104 +++++++++++++-------------- src/kiri/mode/cam/slice.js | 124 ++++++++++++++++---------------- src/mesh/tool.js | 57 +++++++-------- 3 files changed, 142 insertions(+), 143 deletions(-) diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js index 6894f0a6..f8102e94 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/cl-helical.js @@ -5,59 +5,59 @@ import { clearPops, env } from "./client.js"; let alert, lastWidget; export let helicalOn = false; export function selectHelical() { - if (helicalOn) { - return helicalDone(); - } - clearPops(); - alert = api.show.alert("analyzing surfaces...", 1000); - let cylinders = env.poppedRec.cylinders; + if (helicalOn) { + return helicalDone(); + } + clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let cylinders = env.poppedRec.cylinders; - CAM.surface_prep(false, () => { - api.hide.alert(alert); - alert = api.show.alert("[esc] cancels surface selection"); - for (let [wid, arr] of Object.entries(cylinders)) { - let widget = api.widgets.forid(wid); - if ( widget && arr?.length ) - for (let faceid of arr) { - CAM.cylinderToggle(widget, faceid, (faceids) => { - // cylinders[widget.id] = faceids; - }); + CAM.surface_prep(false, () => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(cylinders)) { + let widget = api.widgets.forid(wid); + if (widget && arr?.length) + for (let faceid of arr) { + CAM.cylinderToggle(widget, faceid, (faceids) => { + // cylinders[widget.id] = faceids; + }); + } + } + }); + helicalOn = env.hoveredOp; + helicalOn.classList.add("editing"); + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = (lastWidget = obj.object.widget); + CAM.cylinderToggle(widget, faceid, ({ faces, error }) => { + if (error) { + api.show.alert(error, 3000); + console.log(error); + return; + } + cylinders[widget.id] = faces; + }); + }; +}; + +export function helicalDone() { + if (!(helicalOn && env.poppedRec && env.poppedRec.cylinders)) { + return; + } + let { cylinders } = env.poppedRec; + for (let wid of Object.keys(cylinders)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.cylinderClear(widget); + } else { + delete cylinders[wid]; } } - }); - helicalOn = env.hoveredOp; - helicalOn.classList.add("editing"); - api.feature.on_mouse_up = (obj, ev) => { - let { face } = obj; - let min = Math.min(face.a, face.b, face.c); - let faceid = min / 3; - let widget = (lastWidget = obj.object.widget); - CAM.cylinderToggle(widget, faceid, ({ faces, error }) => { - if ( error ) { - api.show.alert(error, 3000); - console.log( error ); - return; - } - cylinders[widget.id] = faces; - }); - }; -}; - -export function helicalDone(){ - if (!( helicalOn && env.poppedRec && env.poppedRec.cylinders )) { - return; - } - let { cylinders } = env.poppedRec; - for (let wid of Object.keys(cylinders)) { - let widget = api.widgets.forid(wid); - if (widget) { - CAM.cylinderClear(widget); - } else { - delete cylinders[wid]; - } - } - api.hide.alert(alert); - api.feature.on_mouse_up = undefined; - helicalOn.classList.remove("editing"); - helicalOn = false; + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + helicalOn.classList.remove("editing"); + helicalOn = false; }; diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index cf77b042..0d8f884d 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -510,8 +510,7 @@ export async function traces(settings, widget) { widget.traces = polylines; return true; -}; - +} /** * Returns an array of arrays of perpindicular triangles in the mesh. @@ -523,110 +522,109 @@ export async function traces(settings, widget) { * @param {object} opts - options object * @return {array} - array of arrays of triangle data */ -export async function cylinders(settings, widget, opts){ - - let {} = opts ?? {} - - let {array:verts} = widget.mesh.geometry.attributes.position - let perpTriangles = [] +export async function cylinders(settings, widget, opts) { + let { } = opts ?? {}; + let { array: verts } = widget.mesh.geometry.attributes.position; + let perpTriangles = []; //iterate over all triangles - for(let i = 0; i < verts.length; i+=9){ - let a = [verts[i], verts[i+1], verts[i+2]], - b = [verts[i+3], verts[i+4], verts[i+5]], - c = [verts[i+6], verts[i+7], verts[i+8]]; + for (let i = 0; i < verts.length; i += 9) { + let a = [verts[i], verts[i + 1], verts[i + 2]], + b = [verts[i + 3], verts[i + 4], verts[i + 5]], + c = [verts[i + 6], verts[i + 7], verts[i + 8]]; //calculate normal - let normal = new THREE.Vector3(a[0]-b[0], a[1]-b[1], a[2]-b[2]) - .cross(new THREE.Vector3(b[0]-c[0], b[1]-c[1], b[2]-c[2])) - .normalize() + let normal = new THREE.Vector3(a[0] - b[0], a[1] - b[1], a[2] - b[2]) + .cross(new THREE.Vector3(b[0] - c[0], b[1] - c[1], b[2] - c[2])) + .normalize(); // if perpindicular normal, and at least 2 Zs are the same - if(normal.z.round(5) == 0 && ! (a[2] != b[2] && b[2] != c[2])){ + if (normal.z.round(5) == 0 && !(a[2] != b[2] && b[2] != c[2])) { let minZ = Math.min(a[2], b[2], c[2]), maxZ = Math.max(a[2], b[2], c[2]); - if(minZ == maxZ){ // all Zs are the same indicated malformed geometry + if (minZ == maxZ) { // all Zs are the same indicated malformed geometry continue; } - perpTriangles.push({...[a,b,c], normal,minZ,maxZ,i:index}) + perpTriangles.push({ ...[a, b, c], normal, minZ, maxZ, i: index }); } } //map where zmax, zmin -> triangleData - let cylinderTriangles = new Map() - for(let t of perpTriangles){ - let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}` - if(!cylinderTriangles.has(hash)){ - cylinderTriangles.set(hash,[]) + let cylinderTriangles = new Map(); + for (let t of perpTriangles) { + let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`; + if (!cylinderTriangles.has(hash)) { + cylinderTriangles.set(hash, []); } - cylinderTriangles.get(hash).push(t) + cylinderTriangles.get(hash).push(t); } - - return Array.from(cylinderTriangles.values()) + + return Array.from(cylinderTriangles.values()); } - - /** - * Find all triangles that are part of the same cylinder as the given triangle - * @param {Widget} widget - widget object - * @param {number} face - index of face in triangle vertex data - * @return {array} - array of all triangle data that belong to the same cylinder - * @requires surface_prep must be called first - */ +/** + * Find all triangles that are part of the same cylinder as the given triangle + * @param {Widget} widget - widget object + * @param {number} face - index of face in triangle vertex data + * @return {array} - array of all triangle data that belong to the same cylinder + * @requires surface_prep must be called first + */ export function cylinder_find (widget, face){ CAM.surface_prep(widget,false); return widget.tool.findCylinderSurface(face); } -export function cylinder_poly_find(widget, face){ +export function cylinder_poly_find(widget, face) { let faces = cylinder_find(widget, face); let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - let slicer = new cam_slicer(widget,{}) - let opts = { - dedup:false, - edges: false, - over:true - } - let firstOffset = faces[0]*9 - let [x1,y1,z1,x2,y2,z2,x3,y3,z3]= Array.from(vert.subarray(firstOffset,firstOffset+9)) - let zs = [z1,z2,z3].map(z => z.round(5)) + let slicer = new cam_slicer(widget, {}); + let firstOffset = faces[0] * 9; + let [x1, y1, z1, x2, y2, z2, x3, y3, z3] = Array.from(vert.subarray(firstOffset, firstOffset + 9)); + let zs = [z1, z2, z3].map(z => z.round(5)); let zmin = Math.min(...zs); let zmax = Math.max(...zs); - let zmid = (zmin+zmax)/2 - let cylVerts = new Float32Array(faces.length*9) - + let zmid = (zmin + zmax) / 2; + let cylVerts = new Float32Array(faces.length * 9); + let opts = { + dedup: false, + edges: false, + over: true + }; + let i = 0 //for each cylinder face index - for(let index of faces){ - let off = index*9 + for (let index of faces) { + let off = index * 9; //copy the 9 triangle vertices floats in - for(let j = 0;j < 9;j++){ - cylVerts[i] = vert[off+j] - i++ + for (let j = 0; j < 9; j++) { + cylVerts[i] = vert[off + j]; + i++; } } + //slice the poly at the midpoint - let poly = slicer.sliceZ(zmid, cylVerts,opts)?.polys?.at(0) - if(!poly) throw "slicing returned no poly" + let poly = slicer.sliceZ(zmid, cylVerts, opts)?.polys?.at(0); + if (!poly) throw "slicing returned no poly"; + let circular = poly.circularity() > 0.98; + //throw error if not circular - if(!circular) throw "faces must be circular" - + if (!circular) throw "faces must be circular"; + //calculate circle properties let area = poly.area(), - diam = Math.sqrt(area / Math.PI)* 2, + diam = Math.sqrt(area / Math.PI) * 2, center = poly.calcCircleCenter(); - - //find direction using normal to center point + //find direction using normal to center point let point = newPoint(x1, y1, z1), delta = point.setZ(center.z).sub(center), - normal = new THREE.Vector3(x1-x2, y1-y2, z1-z2) - .cross(new THREE.Vector3(x2-x3, y2-y3, z2-z3)), + normal = new THREE.Vector3(x1 - x2, y1 - y2, z1 - z2) + .cross(new THREE.Vector3(x2 - x3, y2 - y3, z2 - z3)), dotProd = normal.dot(delta), - interior = dotProd < 0 + interior = dotProd < 0 - return {faces, zmin, zmax, poly, circular, area, diam, center,interior} + return { faces, zmin, zmax, poly, circular, area, diam, center, interior } } /** diff --git a/src/mesh/tool.js b/src/mesh/tool.js index 41db2f8b..bc80aa6a 100644 --- a/src/mesh/tool.js +++ b/src/mesh/tool.js @@ -241,13 +241,12 @@ class MeshTool { }; } -/** - * Retrieves the adjacent faces for a given face index. - * - * @param {number} face - The index of the face for which adjacent faces are to be found. - * @returns {number[]} An array of face indices that are adjacent to the given face. - */ - + /** + * Retrieves the adjacent faces for a given face index. + * + * @param {number} face - The index of the face for which adjacent faces are to be found. + * @returns {number[]} An array of face indices that are adjacent to the given face. + */ getAdjacentFaces(face) { const { faces, sides } = this.getIndex(); const foff = face * 6; @@ -319,31 +318,31 @@ class MeshTool { * 3. Filtering faces with normals perpendicular to Z axis * 4. Recursively adding adjacent faces with the same Z values and normal perpendicular to Z axis */ - findCylinderSurface(face){ - face = Number(face) - - const radianTolerance = 8 * (Math.PI/180); + findCylinderSurface(face) { + face = Number(face); + const radianTolerance = 8 * (Math.PI / 180); const vertexTolerance = 1e-4; - //helper function - const dissimilar = (a,b,c) => Math.abs(a-b) > vertexTolerance && - Math.abs(b-c) > vertexTolerance && - Math.abs(a-c) > vertexTolerance; + + // helper function + const dissimilar = (a, b, c) => Math.abs(a - b) > vertexTolerance && + Math.abs(b - c) > vertexTolerance && + Math.abs(a - c) > vertexTolerance; let found = {}, out = [face], - {vertices} = this, - {faces} = this.getIndex(), - faceOffset = face*9, - zs = [2,5,8].map(i => vertices[faceOffset+i].round(5)), //get zs from face vertices + { vertices } = this, + { faces } = this.getIndex(), + faceOffset = face * 9, + zs = [2, 5, 8].map(i => vertices[faceOffset + i].round(5)), //get zs from face vertices lowZ = Math.min(zs), highZ = Math.max(zs), checked = {}, check = [face]; - - //check for errors, and throw if found - if(dissimilar(...zs)){ + + // check for errors, and throw if found + if (dissimilar(...zs)) { throw `face must have only 2 Z values. found: ${zs.join(", ")}` - }else if(Math.abs( faces[face * 6 + 2] > 1e-6 )){ + } else if (Math.abs(faces[face * 6 + 2] > 1e-6)) { throw "face's normal must be perpendicular to Z axis" } @@ -352,11 +351,12 @@ class MeshTool { const face = check.shift(); const froot = face * 6; const vroot = face * 9 - + // filter faces to those perpendicular to +z axis - if ( Math.abs( faces[froot + 2] > 1e-6 )) continue; - //filter faces with verts don't share the z values of the first face - if( + if (Math.abs(faces[froot + 2] > 1e-6)) continue; + + // filter faces with verts don't share the z values of the first face + if ( !(vertices[vroot + 2] !== lowZ || vertices[vroot + 2] !== highZ) && !(vertices[vroot + 5] !== lowZ || vertices[vroot + 5] !== highZ) && !(vertices[vroot + 8] !== lowZ || vertices[vroot + 8] !== highZ) @@ -369,7 +369,7 @@ class MeshTool { } const aroot = f * 6; let sum = 0; - for (let i=0; i<3; i++) { + for (let i = 0; i < 3; i++) { sum += Math.pow(faces[froot + i] - faces[aroot + i], 2); } const fn = Math.sqrt(sum); @@ -381,6 +381,7 @@ class MeshTool { } } } + return out; } From cb0344fb7022e6db3a4ceb6b5a83ceab0be6a88a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 23:18:08 -0400 Subject: [PATCH 39/60] fix LANG api ref for extruders --- src/kiri/core/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/core/settings.js b/src/kiri/core/settings.js index 1a5b65b0..5efa6a30 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/core/settings.js @@ -286,7 +286,7 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) { function updateExtruderFields(device) { const { ui } = api; - const { LANG } = api.const; + const { current: LANG } = api.const.LANG; if (device.extruders && device.extruders[device.internal]) { updateFieldsFromSettings(device.extruders[device.internal]); ui.extruder.innerHTML = From bcadc9dfb9270019c64040c5778ef2d9bbc52a9a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 22 Sep 2025 23:39:45 -0400 Subject: [PATCH 40/60] reorder fdm menus, add solid fill sub-menu --- src/kiri/core/conf.js | 2 ++ src/kiri/mode/fdm/init-menu.js | 15 +++++++-------- web/kiri/index.html | 1 + web/kiri/lang/en.js | 21 ++++++++++++--------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index d8f38f30..3153c01f 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -949,7 +949,9 @@ export const conf = { hidden: { "fdm-base": true, "fdm-cool": true, + "fdm-heat": true, "fdm-fill": true, + "fdm-solid": true, "fdm-supp": true, "fdm-xprt": true }, diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 73b93a1f..5320c367 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -82,8 +82,11 @@ 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 }), + _____: newGroup(LANG.fs_menu, $('fdm-solid'), { modes:FDM, driven, hideable, separator, group:"fdm-solid" }), + sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}), + sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), + sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), _____: 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}), @@ -91,9 +94,6 @@ export function menu() { 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)}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), - sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), @@ -172,16 +172,15 @@ export function menu() { outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toFloat, bound:bound(0,3)}), + arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), - sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), 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: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)}), fdmRanges: $('fdm-ranges'), diff --git a/web/kiri/index.html b/web/kiri/index.html index 91431e16..a72a261a 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -337,6 +337,7 @@
+
diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index fdaa35b3..a9b61cf5 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -332,14 +332,14 @@ self.lang['en-us'] = { se_save: "save", // FDM LAYERS - sl_menu: "paths", + sl_menu: "layers", sl_angl_s: "slice angle", sl_angl_l: ["slice angle bias in degrees"], sl_lahi_s: "layer height", sl_lahi_l: ["height of each slice","layer in millimeters"], ad_minl_s: "layer minimum", ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], - ad_adap_s: "adaptive", + ad_adap_s: "adaptive height", ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], sl_ltop_s: "top layers", sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], @@ -363,6 +363,15 @@ self.lang['en-us'] = { sl_strt_s: "layer start", sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + // FDM SOLID + fs_menu: "solid fill", + fi_over_s: "shell overlap", + fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + fi_angl_s: "start angle", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_grow_s: "area expand", + fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], + // FDM FILL fi_menu: "sparse fill", fi_type: "fill type", @@ -370,12 +379,6 @@ self.lang['en-us'] = { fi_pcnt_l: ["fill density values","0.0 - 1.0"], fi_rept_s: "fill repeat", fi_rept_l: ["number of times to repeat","unchanged fill layers"], - fi_angl_s: "solid angle", - fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], - fi_grow_s: "solid expand", - fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], - fi_over_s: "fill overlap", - fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], // fi_rate_s: "print speed", fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], @@ -855,7 +858,7 @@ self.lang['en-us'] = { ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], ad_scst_s: "shell coast", ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], - ad_msol_s: "min solid", + ad_msol_s: "min area", ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], From 9138e219ea83ed227bf61776f1e992931ec2c67f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Sep 2025 18:33:03 -0400 Subject: [PATCH 41/60] fixes #429 outline refresh and stock size issues --- src/kiri/core/widget.js | 5 +++++ src/kiri/mode/cam/slice.js | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 3e340433..783598ca 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -360,6 +360,10 @@ class Widget { this.setOpacity(opacity ?? 1); } + refreshVisualState() { + this.setVisualState(this.getVisualState()); + } + restoreVisualState() { if (this.cache.vizstate) { this.setVisualState(this.cache.vizstate); @@ -613,6 +617,7 @@ class Widget { this.roto = []; this.center(); this.setModified(); + this.refreshVisualState(); } mirror() { diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 0d8f884d..2c9f454f 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -132,15 +132,17 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } if (stock.x && stock.y && stock.z && !isIndexed) { - if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { + let maxDelta = 1e-3; + + if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) { return error('stock X too small for part. resize stock or use offset stock'); } - if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) { + if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) { return error('stock Y too small for part. resize stock or use offset stock'); } - if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) { + if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) { return error('stock Z too small for part. resize stock or use offset stock'); } } From 90c29d1aa74f0e645865f6cb892fc3e20e60ba03 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Sep 2025 18:48:05 -0400 Subject: [PATCH 42/60] fixes #430 by providing an enclosing element height and aspect ratio --- web/kiri/index.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/kiri/index.css b/web/kiri/index.css index bb06d38e..5b39165b 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1636,8 +1636,10 @@ details[open] summary::after { background-color: rgba(255,255,255,0.5); position: absolute; z-index: 30; - bottom: -20px; + bottom: calc(-4px - 1em); right: 0; + height: 1em; + aspect-ratio: 1 / 1 !important; } .mod-x svg { aspect-ratio: 1 / 1 !important; From 457c03766c69ced8b58f5c329d0fe4436adb6eea Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 23 Sep 2025 20:20:34 -0400 Subject: [PATCH 43/60] fix adding supports from underneath platform looking up --- src/kiri/mode/fdm/client.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/client.js b/src/kiri/mode/fdm/client.js index a7471a5d..7bb6d5cf 100644 --- a/src/kiri/mode/fdm/client.js +++ b/src/kiri/mode/fdm/client.js @@ -426,7 +426,7 @@ export function init() { let idn = new Raycaster(rp, dn) .intersectObjects(targets, false) .filter(t => !t.object.pillar); - if (idn.length && idn.length % 2 === 0) { + if (idn.length) { iw = iup[0].object.widget || iw; let wa = api.widgets.annotate(iw.id); let ws = (wa.support = wa.support || []); @@ -471,10 +471,11 @@ export function init() { iw = null; } p1 = point; + // looking up by default (assumes platform intersect) let dir = new Vector3(0,1,0) let ray = new Raycaster(point, dir); let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; - // when on object, project down on downward faces + // when intersecting underside of object, project down if (int && int.face && int.face.normal.z < -0.1) { dir.y = -1; } @@ -482,7 +483,7 @@ export function init() { let i2 = ray.intersectObjects(targets, false) .filter(t => !t.object.pillar) // eliminate other pillars .filter(i => i.distance > 0.1); // false matches close to origin of ray - if (i2.length && i2.length % 2 === 0) { + if (i2.length) { p2 = i2[0].point; iw = i2[0].object.widget || iw; let p1y = Math.max(0, p1.y); From f8180fe79a6c5f15d44c988570fe60eb79dac13a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 24 Sep 2025 01:18:19 -0400 Subject: [PATCH 44/60] mock up basic thinwall w/out medial axis --- src/kiri/core/print.js | 10 ++++++---- src/kiri/mode/fdm/post.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index d0487b52..7b6c642c 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -138,16 +138,17 @@ class Print { } poly.forEachPoint((point, pos, points, count) => { - if (first) { - if (options.onfirst) { + if (first || point.skip) { + // if (point.skip) console.log({ skip: point }); + if (first && options.onfirst) { options.onfirst(point); } // move to first output point on poly let out = scope.addOutput(output, point, 0, moveSpeed, tool); - if (options.onfirstout) { + if (first && options.onfirstout) { options.onfirstout(out); } - first = false; + if (first) first = false; } else { let seglen = last.distTo2D(point); if (coastDist && shellMult && perimeter - seglen <= coastDist) { @@ -158,6 +159,7 @@ class Print { shellMult = 0; } perimeter -= seglen; + // to increase shellMult when point.inc set for collapsed points scope.addOutput(output, point, shellMult, printSpeed, tool); } last = point; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 9a9d43e4..7a86ea94 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -99,6 +99,37 @@ function offset_default(params) { } } ); + // look for close points on segmented polys + if (false) + for (let poly of POLY.flatten(last)) { + let ps = poly.segment(offset1, true); + let pp = ps.points; + let moved = false; + pp.push(pp[0]); + for (let i=0; i Date: Wed, 24 Sep 2025 20:34:11 -0400 Subject: [PATCH 45/60] extend approach. need to switch to segment comparison w/ angles --- src/geo/polygon.js | 5 +- src/kiri/mode/fdm/post.js | 106 ++++++++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 2a2be748..806fbd9b 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -2326,7 +2326,7 @@ export class Polygon { // split long straight lines into segments no longer than max // and return a new polygon - segment(max = 1, mark) { + segment(max = 1, mark, wrap) { const newp = []; const points = this.points; const length = points.length; @@ -2358,6 +2358,9 @@ export class Polygon { } } if (newp.length > length) { + if (wrap) { + newp.push(newp[0]); + } return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(this.open); } return this; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 7a86ea94..8da695bb 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -70,6 +70,9 @@ export function slicePost(data, options) { function offset_default(params) { let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params; + + let first; + // standard wall offsetting strategy POLY.offset( top_poly, @@ -81,6 +84,7 @@ function offset_default(params) { outs: top.shells, flat: true, call: (polys, onCount) => { + first = first || polys; last = polys; // mark each poly with depth (offset #) starting at 0 for (let p of polys) { @@ -99,37 +103,87 @@ function offset_default(params) { } } ); - // look for close points on segmented polys + + // look for close points on adjacent segmented polys and merge + let test_polys = first !== last ? [ ...first, ...last ] : last; + let thin_test = POLY.flatten(test_polys).map(poly => { + return { + poly, + points: poly.segment(offset1, true).points + } + }); + if (false) - for (let poly of POLY.flatten(last)) { - let ps = poly.segment(offset1, true); - let pp = ps.points; - let moved = false; - pp.push(pp[0]); - for (let i=0; i Date: Wed, 24 Sep 2025 23:59:31 -0400 Subject: [PATCH 46/60] add basic thin wall handling and default to on --- src/kiri/core/conf.js | 2 +- src/kiri/core/consts.js | 1 + src/kiri/core/print.js | 17 ++++++---- src/kiri/mode/fdm/post.js | 71 +++++++++++++++++---------------------- 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index 3153c01f..febd09c0 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -338,7 +338,7 @@ export const conf = { outputLoops: 0, outputInvertX: false, outputInvertY: false, - sliceDetectThin: "off", + sliceDetectThin: "basic", sliceMinHeight: 0, sliceAdaptive: false, zHopDistance: 0.2, diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index 748fa4e4..fa73787f 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -115,6 +115,7 @@ const LISTS = { ], thin: [ { name: "off" }, + { name: "basic" }, { name: "adaptive" }, { name: "legacy 1" }, { name: "legacy 2" } diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 7b6c642c..c343540b 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -137,18 +137,21 @@ class Print { printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly); } + let lpo; poly.forEachPoint((point, pos, points, count) => { - if (first || point.skip) { + if (point.skip && lpo?.skip) { + scope.addOutput(output, point, 0, moveSpeed, tool); + } else if (first) { // if (point.skip) console.log({ skip: point }); - if (first && options.onfirst) { + if (options.onfirst) { options.onfirst(point); } // move to first output point on poly let out = scope.addOutput(output, point, 0, moveSpeed, tool); - if (first && options.onfirstout) { + if (options.onfirstout) { options.onfirstout(out); } - if (first) first = false; + first = false; } else { let seglen = last.distTo2D(point); if (coastDist && shellMult && perimeter - seglen <= coastDist) { @@ -159,10 +162,12 @@ class Print { shellMult = 0; } perimeter -= seglen; + // increase mult by % of point moved relative to nozzle radius + let multOut = shellMult + (point.moved ?? 0); // to increase shellMult when point.inc set for collapsed points - scope.addOutput(output, point, shellMult, printSpeed, tool); + scope.addOutput(output, point, multOut, printSpeed, tool); } - last = point; + last = lpo = point; }, close, closest.index); this.lastPoly = poly; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 8da695bb..68da6933 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -7,6 +7,7 @@ import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { getRangeParameters } from './driver.js'; import { slicer } from '../../../geo/slicer.js'; import { Point } from '../../../geo/point.js'; +import { Slope } from '../../../geo/slope.js'; slicer.slicePost.FDM = slicePost; @@ -69,7 +70,7 @@ export function slicePost(data, options) { }; function offset_default(params) { - let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params; + let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps, thin } = params; let first; @@ -104,16 +105,31 @@ function offset_default(params) { } ); + if (!thin) { + return { last, gaps }; + } + + const deltaBig = 160; + const deltSmall = 20; + // look for close points on adjacent segmented polys and merge let test_polys = first !== last ? [ ...first, ...last ] : last; - let thin_test = POLY.flatten(test_polys).map(poly => { + let test_flats = POLY.flatten(test_polys); + + // pre-compute segment angles + for (let poly of test_flats) + poly.forEachSegment((p0, p1) => { + p0.angle = new Slope(p0, p1); + }); + + // segment each poly to be tested + let thin_test = test_flats.map(poly => { return { poly, points: poly.segment(offset1, true).points } }); - if (false) for (let pi=0, pil=thin_test.length; pi deltSmall && diff < deltaBig) continue; // merge points marking point offset and skip let mid = p0.midPointTo(p1); - let inc = p0.distTo2D(mid); + let inc = p0.distTo2D(mid) / offset1; moved = p0.moved = p1.moved = inc; p1.skip = true; p0.x = p1.x = mid.x; @@ -154,36 +171,6 @@ function offset_default(params) { } } - // for (let poly of poly.segment(offset1, true)) { - // let ps = poly.segment(offset1, true); - // let pp = ps.points; - // let moved = false; - // pp.push(pp[0]); - // for (let i=0; i Date: Thu, 25 Sep 2025 00:11:40 -0400 Subject: [PATCH 47/60] filter unmodified subsegments used for proximity testing --- src/kiri/mode/fdm/post.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 68da6933..826c2566 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -167,7 +167,12 @@ function offset_default(params) { for (let rec of thin_test) { if (rec.moved) { - rec.poly.points = rec.points; + let last_seg; + rec.poly.points = rec.points.filter((p,i) => { + let keep = (last_seg !== p.segment || p.moved || p.skip); + last_seg = p.segment; + return keep; + }); } } From 90a9f21ab3135c4a3eff397b3d3a62d4987378aa Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Thu, 25 Sep 2025 18:43:38 -0600 Subject: [PATCH 48/60] fix helical operator mistake --- src/kiri/mode/cam/op-helical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/op-helical.js index 3c3aed6a..a8f19e78 100644 --- a/src/kiri/mode/cam/op-helical.js +++ b/src/kiri/mode/cam/op-helical.js @@ -58,7 +58,7 @@ export class OpHelical extends CamOp { } let { zmin, zmax, diam, center, interior, faces } = res; // console.log({ zmin, zmax, diam, center, interior }); - let radius = diam >> 1; + let radius = diam /2; zmax = fromTop ? stock.z: zmax; let radAdd, From f5d234da3145fe166571a23dc66bb9015677d688 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 25 Sep 2025 21:31:54 -0400 Subject: [PATCH 49/60] revert new thin default until codec updated --- src/kiri/core/conf.js | 2 +- src/kiri/mode/fdm/post.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index febd09c0..3153c01f 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -338,7 +338,7 @@ export const conf = { outputLoops: 0, outputInvertX: false, outputInvertY: false, - sliceDetectThin: "basic", + sliceDetectThin: "off", sliceMinHeight: 0, sliceAdaptive: false, zHopDistance: 0.2, diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 826c2566..34458f49 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -114,7 +114,7 @@ function offset_default(params) { // look for close points on adjacent segmented polys and merge let test_polys = first !== last ? [ ...first, ...last ] : last; - let test_flats = POLY.flatten(test_polys); + let test_flats = POLY.flatten(test_polys ?? []); // pre-compute segment angles for (let poly of test_flats) From 39cc3db3d8b1607405219c74a309251e4371aa92 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Sep 2025 09:44:56 -0400 Subject: [PATCH 50/60] add codec optional extended point data encoding --- src/kiri/core/codec.js | 28 +++++++++++++++++++++++++++- src/kiri/mode/fdm/post.js | 6 ++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index 06c9f896..3547fb6f 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -312,7 +312,7 @@ Polygon.prototype.encode = function(state) { state.poly[this.id] = this; - return { + let enc = { type: TYPE.POLY, id: this.id, array: encodePointArray(this.points, state, this.z), @@ -322,6 +322,20 @@ Polygon.prototype.encode = function(state) { color: this.color, open: this.open }; + + // encode extended point info for tagged polys + if (this._epk) { + let epk = enc.epk = {}; + for (let key of this._epk) { + let arr = epk[key] = []; + let pts = this.points; + for (let i=0, l=pts.length; i { let keep = (last_seg !== p.segment || p.moved || p.skip); last_seg = p.segment; From 35b48273bd65b9901c1415562bbab02cc966aa12 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Sep 2025 19:48:56 -0400 Subject: [PATCH 51/60] move output layer menu items to layer menu. relabel for clarity --- src/kiri/mode/fdm/init-menu.js | 4 ++-- web/kiri/lang/en.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 5320c367..7eee5e85 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -74,6 +74,8 @@ export function menu() { sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), separator: newBlank({ class:"set-sep", driven }), sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), + outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), + sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), @@ -165,8 +167,6 @@ export function menu() { outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), separator: newBlank({ class:"set-sep", driven }), - sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), - outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), separator: newBlank({ class:"set-sep", driven, show:isBelt }), outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a9b61cf5..baa8b7eb 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -360,8 +360,8 @@ self.lang['en-us'] = { sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], sl_ordr_s: "shell order", sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], - sl_strt_s: "layer start", - sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + sl_strt_s: "start point", + sl_strt_l: ["layer starting point","last = where last layer ended","center = part center","origin = device origin","random = yolo"], // FDM SOLID fs_menu: "solid fill", @@ -876,8 +876,8 @@ self.lang['en-us'] = { ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], ad_zint_s: "interleave z", ad_zint_l: ["interleave Z heights with odd number shells"], - ad_lret_s: "layer retract", - ad_lret_l: ["force filament retraction","between layers"], + ad_lret_s: "force retract", + ad_lret_l: ["force filament retraction between layers"], ad_agap_s: "avoid gaps", ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], ad_play_s: "polish layers", From ac956a3cac8d591d7a2b22a452de25d49640ee84 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 26 Sep 2025 22:04:27 -0400 Subject: [PATCH 52/60] add spiral layer start point option --- src/kiri/core/consts.js | 1 + src/kiri/mode/fdm/prepare.js | 11 +++++++++++ web/kiri/lang/en.js | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index fa73787f..4efe0ba5 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -27,6 +27,7 @@ const LISTS = { { name: "center" }, { name: "origin" }, { name: "random" }, + { name: "spiral" }, ], infill: [ { name: "none" }, diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index bd676ec2..d3d2e601 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -41,6 +41,7 @@ export async function fdm_prepare(widgets, settings, update) { zneg = 0, zoff = 0, zmin = 0, + zmax = 0, shield, output = [], layerout = []; @@ -224,6 +225,7 @@ export async function fdm_prepare(widgets, settings, update) { } for (let slice of widget.slices) { zmin = Math.min(zmin, slice.z); + zmax = Math.max(zmax, slice.z); if (!slice.supports) { continue; } @@ -469,6 +471,8 @@ export async function fdm_prepare(widgets, settings, update) { let lastExt; let lastOut; + print.zmax = zmax; + // walk cake layers bottom up for (let layer of cake) { // track purge blocks generated for each layer @@ -1474,6 +1478,13 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // control of layer start point switch (process.sliceLayerStart) { + case "spiral": { + let center = top.poly.bounds.center(); + let angle = (Math.PI * 2) * (slice.z / print.zmax); + let x = center.x + Math.cos(angle)*10000; + let y = center.y + Math.sin(angle)*10000; + startPoint = newPoint(x,y,startPoint.z); + } break; case "last": break; case "random": diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index baa8b7eb..35819ccc 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -848,7 +848,7 @@ self.lang['en-us'] = { // FDM ADVANCED ad_menu: "expert", - ad_rdst_s: "retract dist", + ad_rdst_s: "retract distance", ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], ad_rrat_s: "retract rate", ad_rrat_l: ["speed of filament","retraction in mm/s"], @@ -866,7 +866,7 @@ self.lang['en-us'] = { ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], ad_arct_s: "arc tolerance", ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], - ad_zhop_s: "z hop dist", + ad_zhop_s: "z hop distance", ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], ad_abkl_s: "anti-backlash", ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], From 7660570f3a0a5b5dfaa3f29dcbe1346029cfa926 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Sep 2025 00:19:22 -0400 Subject: [PATCH 53/60] add oname and oid params for macro conditions and substitutions --- src/kiri/core/print.js | 11 +++++++++-- src/kiri/mode/fdm/export.js | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index c343540b..27c7cd32 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -176,6 +176,14 @@ class Print { } constReplace(str, consts, start, pad, short) { + function tryeval(str) { + try { + return eval(`{ ${str} }`) + } catch (e) { + console.log({ eval_error: e, str }); + return str; + } + } let cs = str.indexOf("{", start || 0), ce = str.indexOf("}", cs), tok, nutok, nustr; @@ -199,8 +207,7 @@ class Print { } eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`); eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`); - let scr = eva.join(''); - let evl = eval(`{ ${scr} }`); + let evl = tryeval(eva.join('')); nutok = evl; if (pad === 666) { return evl; diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 8481bdc0..2f693f08 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -111,6 +111,8 @@ export function fdm_export(print, online, ondone, ondebug) { progress: 0, nozzle: tool, tool: tool, + oid: '', + oname: '', model_labels: model_labels.sort().join(',') }, pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, @@ -620,6 +622,8 @@ export function fdm_export(print, online, ondone, ondebug) { let b64 = encodeBitOffset(off); append(`; start object id: ${out.widget.track.grid_id}`); isBambu && append(`M624 ${b64}`); + subst.oid = out.widget.track.grid_id; + subst.oname = out.widget.meta.file; } cwidget = out.widget; } From 79a227d7411fd0612bf2a0247706f3a2aba396e8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 27 Sep 2025 21:15:21 -0400 Subject: [PATCH 54/60] add fdm scarf seams and min layer time --- notes.md | 16 +++----- src/geo/polygon.js | 15 +++++-- src/kiri/core/conf.js | 2 + src/kiri/core/print.js | 71 ++++++++++++++++++++++++++++++++-- src/kiri/core/render.js | 7 +--- src/kiri/mode/fdm/init-menu.js | 2 + src/kiri/mode/fdm/prepare.js | 42 ++++++++++++++++---- web/kiri/lang/en.js | 6 ++- 8 files changed, 129 insertions(+), 32 deletions(-) diff --git a/notes.md b/notes.md index 5efbac1a..d45c1523 100644 --- a/notes.md +++ b/notes.md @@ -31,27 +31,21 @@ * `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder purge blocks fail to generate properly for rafts -* `F` min layer time -* `F` separate bridging speed / fan control -* `F` elephant foot compensation -* `F` inner / outer perimeter compensation -* `F` better thin-wall (like arachne) -* `F` add scarf seams -* `F` add 'random' and 'seam' start point options +* `F` new parameter to cap output flow rate (on printer device) +* `F` new parameters for bridging speed / bridge fan control +* `F` new base paremeter for hole and perimeter compensation +* `F` new parameters for inner / outer perimeter compensation * `F` convert ranges to z offsets while continuing to show layer # * `F` support pillar top/bottom should conform to part * `F` more explicit line width control with ranges and min/max adaptive * `F` test outlining solid projected areas (internally) * `F` gradient infill https://www.youtube.com/watch?v=hq53gsYREHU&feature=emb_logo -* `F` feather sharp tips by reducing extrusion in area of overlap * `F` segment large flat areas on first layer to mitigate peeling * `F` option to support interior bridges when 0% infill * `F` calculate filament use per extruder per print * `F` expand internal supporting flats / solids before projection (threshold) -* `P` reduce fan speed, feed rate, and extrusion factor for bridges * `P` auto purge pillars when quick layers are detected for extra cooling -* `P` extruder + filament max flow rate cap in planner * `P` solid fill the tops of supports for down facing flats # FDM - BELT @@ -91,8 +85,8 @@ * `F` ease-in and ease-out especially on tab cut-out start/stop * `F` maintain several part orientations + op chains in a single profile +* `P` outer outside corners as arc moves * `P` improve parser - do not require spaced tokens and support implied G0 / G1 -* `P` outer corner arc moves * `P` log Z interpolation for contour XYZ moves * `P` option to start with the smallest poly by area on layer change * `P` redo all path route / planning in prepare to account for terrain before camOut diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 806fbd9b..2ef4d2f6 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -2324,9 +2324,12 @@ export class Polygon { return obj; } - // split long straight lines into segments no longer than max - // and return a new polygon - segment(max = 1, mark, wrap) { + // split long straight lines into segments no longer than `max` + // and return a new polygon. optionally `mark` points with their + // derived segment start point (hinting for thin walls), `wrap` + // the poly first point by appending to the end (adaptive walls), + // or stopping segmentation at `maxoff` + segment(max = 1, mark = false, wrap = false, maxoff = Infinity) { const newp = []; const points = this.points; const length = points.length; @@ -2339,9 +2342,13 @@ export class Polygon { const dl = Math.sqrt(dx * dx + dy * dy); newp.push(p1); if (mark) p1.segment = p1; - if (dl < max) { + // if segment shorter than max (delta) or + // maxoff is exhausted (only segmenting up to some length) + // then skip sub-segmenting the current segment + if (dl < max || maxoff < 0) { continue; } + maxoff -= dl; const div = dl / max; const fit = div | 0; const add = fit - 1; diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index 3153c01f..4daf5556 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -327,8 +327,10 @@ export const conf = { outputRetractDwell: 20, outputBrimCount: 2, outputBrimOffset: 2, + outputScarfLength: 0, outputShortPoly: 100.0, outputMinSpeed: 10.0, + outputMinLayerTime: 0, outputCoastDist: 0, outputPurgeTower: 0, outputBeltFirst: false, diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 27c7cd32..d4e973b9 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -98,6 +98,7 @@ class Print { * @param {Array} output - the array to print to * @param {Object} [options] - optional parameters * @param {boolean} [options.ccw] - set the polygon to be counter-clockwise + * @param {boolean} [options.scarf] - scarf seam permitted * @param {number} [options.extrude] - extrude factor for the polygon * @param {number} [options.rate] - print speed in mm/s * @param {number} [options.coast] - distance to coast at the end of the polygon @@ -128,6 +129,7 @@ class Print { closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint), perimeter = poly.perimeter(), close = !options.open, + scarf = !poly.open ? (options.scarf ?? 0) : false, tool = options.tool, last = startPoint, first = true; @@ -137,8 +139,70 @@ class Print { printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly); } + // if not starting at first point in poly, rotate to move start to index = 0 + let pp = poly.points; + if (closest.index > 0) { + let cio = pp.indexOf(closest.point); + pp = poly.points = [ ...pp.slice(cio), ...pp.slice(0, cio) ]; + } + + // scarf sanity checks + if (scarf) { + // cancel scarf for thin wall polys + if (pp.filter(p => p.skip || p.moved).length) { + scarf = 0; + } else { + // cancel scarf if any point.z differs + let z0 = pp[0].z; + let zd = 0; + for (let p of pp) zd += Math.abs(p.z - z0); + if (zd) scarf = 0; + } + // console.log({ scarf }); + } + + // when creating scarf seams, segment poly up to seam length + // create array of step up points at start of poly with increasing z + // and increasing shellMult and then append the same points on the back + // end of the poly with fixed z and decreasing shellMult + if (scarf) { + let epz = Math.max(...poly.points.map(p => p.z)); + let spz = startPoint.z; + poly = poly.segment(options.nozzleSize ?? 0.4, false, false, scarf * 2); + pp = poly.points; + let lp, sp = []; + for (let p of pp) { + let d = lp?.distTo2D(p) ?? 0; + sp.push(lp = p); + scarf -= d; + if (scarf <= 0) break; + } + let scale = 0.25; // flow compensation on overlap + let mid = (1 / sp.length) * 0.75; // flow compensation (- half step) + let zd = (epz - spz) / sp.length; + let zi = 1; + for (let p of sp) { + p.z -= zd * (sp.length - zi); + p.moved = (((zi++) / sp.length) * scale) - 1 - mid; + } + let esp = sp.map(p => p.clone()); // ending scarf points + for (let p of esp) { + p.z = epz; + p.moved = (((--zi) / esp.length) * scale) - 1 - mid; + } + esp.pop(); // stop short to prevent overlap bulge + pp.push(...esp); + // console.log({ poly, spz, epz, zd, sp, esp }); + scarf = true; + } + + // scarf manages its own close point + if (close && !scarf) { + pp.push(pp[0]); + } + let lpo; - poly.forEachPoint((point, pos, points, count) => { + for (let point of pp) { if (point.skip && lpo?.skip) { scope.addOutput(output, point, 0, moveSpeed, tool); } else if (first) { @@ -154,7 +218,8 @@ class Print { first = false; } else { let seglen = last.distTo2D(point); - if (coastDist && shellMult && perimeter - seglen <= coastDist) { + // cancel coast when using scarf seam + if (!scarf && coastDist && shellMult && perimeter - seglen <= coastDist) { let delta = perimeter - coastDist; let offset = seglen - delta; let offPoint = last.offsetPointFrom(point, offset) @@ -168,7 +233,7 @@ class Print { scope.addOutput(output, point, multOut, printSpeed, tool); } last = lpo = point; - }, close, closest.index); + } this.lastPoly = poly; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index a322958d..1c92f077 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -181,8 +181,8 @@ async function path(levels, update, opts = {}) { return; } - if (lastOut) { // if last outpoint is defined - + // if last outpoint is defined + if (lastOut) { if (arrowAll || lastOut.emit !== out.emit) { heads.push({p1: lastOutPoint, p2: outPoint}); } @@ -200,13 +200,10 @@ async function path(levels, update, opts = {}) { current.color = color(out); pushPrint(out.tool, current); } - out.arcPoints.forEach(p => { current.push(p); }) - current.push(outPoint); - } else if (out.emit) { // explicity G1 in CAM mode // just a non-move in other modes diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 7eee5e85..fc0506c1 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -102,6 +102,7 @@ export function menu() { _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), + outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }), _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), @@ -177,6 +178,7 @@ export function menu() { outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), + outputScarfLength: newInput(LANG.ad_scar_s, {title:LANG.ad_scar_l, convert:toFloat, bound:bound(0,1000)}), outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), 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)}), diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index d3d2e601..c4b207b3 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -800,6 +800,8 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { process = opt.params || settings.process, originCenter = device.originCenter || bedRound, extruder = parseInt(slice.extruder || 0), + scarfLength = process.outputScarfLength || 0, + minLayerTime = process.outputMinLayerTime || 0, nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, firstLayer = (opt.first || false) && !opt.support, thinWall = nozzleSize * (opt.thinWall || 1.75), @@ -1036,8 +1038,8 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { return false; } - // solid infill - function outputTraces(poly, opt = {}) { + // output shell wall + function outputTraces(poly, opt = {}, traceNo = 0) { if (!poly) return; if (Array.isArray(poly)) { if (opt.sort) { @@ -1070,26 +1072,28 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (next) { last = next; polys.remove(next); - outputTraces(next, opt); + outputTraces(next, opt, traceNo++); } else { last = null; } } } else { outputOrderClosest(poly, function(next) { - outputTraces(next, opt); + outputTraces(next, opt, traceNo++); }, null); } } else { let finishShell = poly.depth === 0 && !firstLayer; startPoint = print.polyPrintPath(poly, startPoint, preout, { - ccw: opt.shell && process.outputAlternating && slice.index % 2, - tool: extruder, - rate: finishShell ? finishSpeed : printSpeed, accel: finishShell, - wipe: process.outputWipeDistance || 0, + ccw: opt.shell && process.outputAlternating && slice.index % 2, coast: firstLayer ? 0 : coastDist, extrude: numOrDefault(opt.extrude, shellMult), + nozzleSize, + rate: finishShell ? finishSpeed : printSpeed, + scarf: (traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0, + tool: extruder, + wipe: process.outputWipeDistance || 0, onfirst: function(firstPoint) { let from = seedPoint || startPoint; if (from.distTo2D(firstPoint) > retractDist) { @@ -1570,6 +1574,28 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } + // adjust layer output speeds (by slowing them down) + // if layer print time is less than minimum (required for cooling) + if (minLayerTime) { + let lp, td = 0, tt = 0; + for (let p of preout) { + if (!p.emit) continue; + let mms = p.speed ?? 1; + let dst = lp?.distTo2D(p.point) ?? 0; + if (dst) { + tt += (dst/mms); + td += dst; + } + lp = p.point; + } + if (tt < minLayerTime) { + let fact = tt / minLayerTime; + for (let p of preout) { + if (p.emit) p.speed *= fact; + } + } + } + // offset print points for (let i=0; i 0.1"], ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], + ad_scar_s: "scarf length", + ad_scar_l: ["ease up length for scarf joint","0 = disabled"], ad_spol_s: "short path", ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], ad_arct_s: "arc tolerance", From 77b4cce97ad20ec459b641d7fc3174ed6c2e741d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Sep 2025 02:08:32 -0400 Subject: [PATCH 55/60] fix scarf/arc conflict and scarf z on export. add print remain time macro var --- notes.md | 7 ++++- src/kiri/core/print.js | 20 +++++++------- src/kiri/mode/fdm/export.js | 27 ++++++++++++++----- src/kiri/mode/fdm/prepare.js | 51 ++++++++++++++++++++---------------- web/kiri/lang/en.js | 4 +-- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/notes.md b/notes.md index d45c1523..b7e7a9a1 100644 --- a/notes.md +++ b/notes.md @@ -100,11 +100,12 @@ * subfolder parameter for dropped files * auto-kick check box in preferences - # Mesh:Tool * add undo/redo * add TextGeometry +* https://threejs.org/docs/#examples/en/geometries/TextGeometry +* https://dustinpfister.github.io/2023/07/05/threejs-text-geometry/ * font to path with https://github.com/paulzi/svg-text-to-path * click to repair normals based on a known good (selected) face * send to Kiri:Moto workspace (or update model vertices in place) @@ -116,3 +117,7 @@ * fix mirror to work with groups (just models currently) * add analyze results dialog * remove/hide auto-repair function + +# Other + +* preferred icons https://icons.getbootstrap.com/ diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index d4e973b9..5fcfb198 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -80,11 +80,9 @@ class Print { return lastOut; } - addPrintPoints(input, output, startPoint, tool) { + addPrintPoints(input, output) { if (this.startPoint && input.length > 0) { this.lastPoint = this.startPoint; - // TODO: revisit seek to origin as the first move - // addOutput(output, startPoint, 0, undefined, tool); } output.appendAll(input); } @@ -120,11 +118,11 @@ class Print { const { settings } = scope; const { process } = settings; - let shortDist = process.outputShortDistance, - shellMult = numOrDefault(options.extrude, process.outputShellMult), + let shellMult = numOrDefault(options.extrude, process.outputShellMult), printSpeed = options.rate || process.outputFeedrate, moveSpeed = process.outputSeekrate, minSpeed = process.outputMinSpeed, + nozzleSize = options.nozzleSize, coastDist = options.coast || 0, closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint), perimeter = poly.perimeter(), @@ -177,20 +175,20 @@ class Print { scarf -= d; if (scarf <= 0) break; } - let scale = 0.25; // flow compensation on overlap - let mid = (1 / sp.length) * 0.75; // flow compensation (- half step) + let fcs = 1.0; // flow compensation seam + let fco = (1 / sp.length) * 0.0; // flow compensation offset (- half step) let zd = (epz - spz) / sp.length; let zi = 1; for (let p of sp) { p.z -= zd * (sp.length - zi); - p.moved = (((zi++) / sp.length) * scale) - 1 - mid; + p.moved = (((zi++) / sp.length) * fcs) - 1 - fco; } let esp = sp.map(p => p.clone()); // ending scarf points for (let p of esp) { p.z = epz; - p.moved = (((--zi) / esp.length) * scale) - 1 - mid; + p.moved = (((--zi) / esp.length) * fcs) - 1 - fco; } - esp.pop(); // stop short to prevent overlap bulge + // esp.pop(); // stop short to prevent overlap bulge pp.push(...esp); // console.log({ poly, spz, epz, zd, sp, esp }); scarf = true; @@ -208,7 +206,7 @@ class Print { } else if (first) { // if (point.skip) console.log({ skip: point }); if (options.onfirst) { - options.onfirst(point); + options.onfirst(point, output); } // move to first output point on poly let out = scope.addOutput(output, point, 0, moveSpeed, tool); diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 2f693f08..74328cfb 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -59,7 +59,8 @@ export function fdm_export(print, online, ondone, ondebug) { retSpeed = process.outputRetractSpeed * 60 || 1, // range retDwell = process.outputRetractDwell || 0, // range timeDwell = retDwell / 1000, - arcDist = isBelt ? 0 : (process.arcTolerance || 0), + scarfZ = process.outputScarfLength ? true : false, + arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0), arcMin = 1, arcRes = 20, arcDev = 0.5, @@ -113,7 +114,9 @@ export function fdm_export(print, online, ondone, ondebug) { tool: tool, oid: '', oname: '', - model_labels: model_labels.sort().join(',') + model_labels: model_labels.sort().join(','), + total_time: Math.ceil(print.total_time / 60), + remain_time: Math.ceil(print.total_time / 60), }, pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, lines = 0, @@ -527,6 +530,7 @@ export function fdm_export(print, online, ondone, ondebug) { // retract before first move retract(); + subst.remain_time = Math.ceil(print.total_time / 60); while (layer < layers.length) { path = layers[layer]; layerno = path.slice.index; @@ -600,7 +604,7 @@ export function fdm_export(print, online, ondone, ondebug) { } // move Z to layer height - if (layer > 0 || !isBelt) { + if (layer === 0 || ((layer > 0 || !isBelt) && !scarfZ)) { moveTo({z:zpos}, seekMMM); } @@ -783,12 +787,20 @@ export function fdm_export(print, online, ondone, ondebug) { } else { // emitMM = emitPerMM * out.emit * dist; emitMM = extrudeMM(dist, emitPerMM, out.emit); - moveTo({x:x, y:y, e:emitMM}, speedMMM); + if (scarfZ) { + moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); + } else { + moveTo({x, y, e:emitMM}, speedMMM); + } emitted += emitMM; } } else { drainQ(); - moveTo({x:x, y:y}, seekMMM); + if (scarfZ) { + moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); + } else { + moveTo({x, y}, seekMMM); + } // TODO disabling out of plane z moves until a better mechanism // can be built that doesn't rely on computed zpos from layer heights... // when making z moves (like polishing) allow slowdown vs fast seek @@ -834,11 +846,13 @@ export function fdm_export(print, online, ondone, ondebug) { append(`M808`); } drainQ(); + + print.total_time -= path.print_time; + subst.remain_time = Math.ceil(print.total_time / 60); } function emitQrec(rec) { let {e, x, y, dist, emitPerMM, speedMMM} = rec; - // emitMM = emitPerMM * e * dist; emitMM = extrudeMM(dist, emitPerMM, e); moveTo({x:x, y:y, e:emitMM}, speedMMM); emitted += emitMM; @@ -962,7 +976,6 @@ export function fdm_export(print, online, ondone, ondebug) { } return true; - } if (inloops) { diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index c4b207b3..5d18cba7 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -142,6 +142,7 @@ export async function fdm_prepare(widgets, settings, update) { }); layerout.z = zoff + height; layerout.height = height; + layerout.print_time = printPoint.layer_time; output.append(layerout); layerout = []; @@ -196,7 +197,7 @@ export async function fdm_prepare(widgets, settings, update) { }); }); - print.addPrintPoints(preout, layerout, null); + print.addPrintPoints(preout, layerout); if (preout.length) { // retract between brims and print @@ -472,6 +473,7 @@ export async function fdm_prepare(widgets, settings, update) { let lastOut; print.zmax = zmax; + print.total_time = 0; // walk cake layers bottom up for (let layer of cake) { @@ -574,6 +576,8 @@ export async function fdm_prepare(widgets, settings, update) { } } ); + layerout.print_time = printPoint.layer_time; + print.total_time += printPoint.layer_time; print.setWidget(null); lastOut = slice; @@ -1084,6 +1088,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } else { let finishShell = poly.depth === 0 && !firstLayer; + let scarf = (slice.index > 0 && traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0; startPoint = print.polyPrintPath(poly, startPoint, preout, { accel: finishShell, ccw: opt.shell && process.outputAlternating && slice.index % 2, @@ -1091,10 +1096,9 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { extrude: numOrDefault(opt.extrude, shellMult), nozzleSize, rate: finishShell ? finishSpeed : printSpeed, - scarf: (traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0, + scarf, tool: extruder, - wipe: process.outputWipeDistance || 0, - onfirst: function(firstPoint) { + onfirst: (firstPoint, preout) => { let from = seedPoint || startPoint; if (from.distTo2D(firstPoint) > retractDist) { if (intersectsTop(from, firstPoint)) { @@ -1576,23 +1580,21 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // adjust layer output speeds (by slowing them down) // if layer print time is less than minimum (required for cooling) - if (minLayerTime) { - let lp, td = 0, tt = 0; - for (let p of preout) { - if (!p.emit) continue; - let mms = p.speed ?? 1; - let dst = lp?.distTo2D(p.point) ?? 0; - if (dst) { - tt += (dst/mms); - td += dst; - } - lp = p.point; + let lp, td = 0, tt = 0; + for (let p of preout) { + if (!p.emit) continue; + let mms = p.speed ?? 1; + let dst = lp?.distTo2D(p.point) ?? 0; + if (dst) { + tt += (dst/mms); + td += dst; } - if (tt < minLayerTime) { - let fact = tt / minLayerTime; - for (let p of preout) { - if (p.emit) p.speed *= fact; - } + lp = p.point; + } + if (minLayerTime && tt < minLayerTime) { + let fact = tt / minLayerTime; + for (let p of preout) { + if (p.emit) p.speed *= fact; } } @@ -1602,7 +1604,12 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } // add offset points to total print - print.addPrintPoints(preout, output, origin, extruder); + print.addPrintPoints(preout, output); - return startPoint.add(offset); + // create next layer start point and annotate + // with print time for previous layer (hack to + // avoid changing return signature rn) + let newStart = startPoint.add(offset); + newStart.layer_time = Math.max(tt, minLayerTime); + return newStart; } diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index b0eb9348..451bcadb 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -865,11 +865,11 @@ self.lang['en-us'] = { ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], ad_scar_s: "scarf length", - ad_scar_l: ["ease up length for scarf joint","0 = disabled"], + ad_scar_l: ["ease up length for scarf joint. disables arc detection","0 = disabled"], ad_spol_s: "short path", ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], ad_arct_s: "arc tolerance", - ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","value in millimeters","0 or scarf disables this"], ad_zhop_s: "z hop distance", ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], ad_abkl_s: "anti-backlash", From 5aa5456f37c6974823dfa7d8b4a14ee74c022bcf Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Sep 2025 12:41:08 -0400 Subject: [PATCH 56/60] fix move extrusion with scarfz --- src/kiri/core/print.js | 2 -- src/kiri/mode/fdm/export.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 5fcfb198..86736811 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -188,9 +188,7 @@ class Print { p.z = epz; p.moved = (((--zi) / esp.length) * fcs) - 1 - fco; } - // esp.pop(); // stop short to prevent overlap bulge pp.push(...esp); - // console.log({ poly, spz, epz, zd, sp, esp }); scarf = true; } diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 74328cfb..b6768e4b 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -797,7 +797,7 @@ export function fdm_export(print, online, ondone, ondebug) { } else { drainQ(); if (scarfZ) { - moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); + moveTo({x, y, z:z + path.height/2}, speedMMM); } else { moveTo({x, y}, seekMMM); } From 6c26dd0d8174e8b0872f80c71ac5bbbf3484790a Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Sep 2025 21:22:19 -0400 Subject: [PATCH 57/60] only apply new starting point on first move of new layer --- src/kiri/mode/fdm/prepare.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 5d18cba7..d3a0bfd9 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1485,6 +1485,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } // control of layer start point + if (!lastTop) switch (process.sliceLayerStart) { case "spiral": { let center = top.poly.bounds.center(); From f53d6da12bbf24d685fd38482d85195ccad6c0bb Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 28 Sep 2025 22:39:56 -0400 Subject: [PATCH 58/60] improve gap routing. add adaptive shell fallback on failure --- src/geo/polygon.js | 23 ++++++++++++++++++++--- src/kiri/mode/fdm/post.js | 15 +++++++++++---- src/kiri/mode/fdm/prepare.js | 17 +++++++++++------ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 2ef4d2f6..ddce7094 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1323,15 +1323,15 @@ export class Polygon { pp2.pos = ip2; } }); - list.sort(function(p1, p2) { - return util.distSq(lp1, p1) - util.distSq(lp1, p2); - }); if (deep && this.inner) { this.inner.forEach(p => { let ints = p.intersections(lp1, lp2); if (ints) list.appendAll(ints); }); } + list.sort(function(p1, p2) { + return util.distSq(lp1, p1) - util.distSq(lp1, p2); + }); return list; } @@ -1392,6 +1392,23 @@ export class Polygon { return poly; } + /** + * emit the shortest poly that connects two points on a closed CW poly + * because traveling around the 'end point' may yield a shorter route. + * if the poly is open, use emitSegment() + */ + emitShortestSegment(i1, i2) { + if (this.open) { + return this.emitSegment(i1, i2); + } + let start = i1.p2.pos, + end = i2.p1.pos, + points = this.points, + seg0 = new Polygon([ i1, ...points.slice(start, end + 1), i2 ]).setOpen(), + seg1 = new Polygon([ i2, ...points.slice(end +1 ), ...points.slice(0, start), i1 ]).setOpen(); + return seg0.perimeter() < seg1.perimeter() ? seg0 : seg1; + } + /** * @param {Polygon} poly * @param {number} [tolerance] diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 55b335fd..58e4ca65 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -487,6 +487,7 @@ function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { // construct medial axis lines. this appears to produce duplicate // reverse segments in a subset of cases let ma = JSPoly.construct_medial_axis(out, inr); + if (ma.length === 0) throw('cannot construct medial axis'); // dedup the points so we can track them uniquely for (let { point0, point1 } of ma) { pointsToLine(point0, point1); @@ -696,7 +697,8 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = { if (dist < 1) p.open = false; } }); let ret = { last, gaps }; - switch (opt.thinType) { + let ttype = opt.thinType; + switch (ttype) { case "legacy 1": ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset }); break; @@ -704,13 +706,18 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = { ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm }); break; case "adaptive": - ret = thin_type_3({ z, top, count, offsetN }); - break; + try { + ret = thin_type_3({ z, top, count, offsetN }); + break; + } catch (e) { + console.log('adaptive layer failed. falling back to basic.', e); + ttype = "basic"; + } case "basic": default: ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm, - thin: opt.thinType === 'basic' + thin: ttype === 'basic' }); break; } diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index d3a0bfd9..8d560b43 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -894,7 +894,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // return true if move path from p1 to p2 intersects a // top and a path around the top (inside print) was not found - function intersectsTop(p1, p2) { + function requiresRetract(p1, p2) { if (slice.index < 0) { return false; } @@ -919,15 +919,20 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)}); let ints = []; + let intk = new Set(); let tops = slice.topRouteFlat(); for (let poly of tops) { poly.forEachSegment((s1, s2) => { let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT); - if (ip) { + if (ip) ip._key = undefined; + if (ip && !intk.has(ip.key)) { ints.push({ip, poly}); + intk.add(ip.key); + ip.p1.poly.hits = (ip.p1.poly.hits ?? 0) + 1; } }); } + ints = ints.filter(i => i.poly.hits === 2); // no intersections if (ints.length === 0) { @@ -1101,7 +1106,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { onfirst: (firstPoint, preout) => { let from = seedPoint || startPoint; if (from.distTo2D(firstPoint) > retractDist) { - if (intersectsTop(from, firstPoint)) { + if (requiresRetract(from, firstPoint)) { retract(); } } @@ -1183,7 +1188,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { poly.forEachPoint((p, i) => { let dist = lp.distTo2D(p); let rdst = dist > retractDist; - let itop = rdst && intersectsTop(lp,p); + let itop = rdst && requiresRetract(lp,p); let emit = extrude; // retract if dist trigger and crosses a slice top polygon if (i === 0) { @@ -1227,7 +1232,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } let first = points[0]; let last = first; - if (startPoint && startPoint.distTo2D(first) > thinWall && intersectsTop(startPoint, first)) { + if (startPoint && startPoint.distTo2D(first) > thinWall && requiresRetract(startPoint, first)) { retract(); } print.addOutput(preout, first, 0, moveSpeed, extruder); @@ -1376,7 +1381,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { print.addOutput(preout, p2, fill, fillSpeed, extruder); } else { // retract if dist trigger or crosses a slice top polygon - if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) { + if (!fast && dist > retractDist && (zhop || requiresRetract(startPoint, p1))) { retract(); } From 8e75ad718f53b3fc24aa1c34bc14ecc2b5065061 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 29 Sep 2025 11:56:07 -0400 Subject: [PATCH 59/60] add test slicing poly pumping (rounding) --- src/kiri/mode/fdm/post.js | 21 ++++++++++++++++++++- src/kiri/mode/fdm/slice.js | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 58e4ca65..5a44e57d 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -21,7 +21,26 @@ export function slicePost(data, options) { const { useAssembly, post_args, zIndexes } = options; const { process, isSynth, vaseMode } = post_args; const { shellOffset, fillOffset, clipOffset } = post_args; - data.tops = POLY.nest(groups); + + if (options.pump) { + let nugroups = []; + let pump = shellOffset / 10; + for (let p of groups) { + let retop = POLY.offset([ p ], -pump, { + join: ClipperLib.JoinType.jtRound, + arc: (1/pump) * 4 + }); + retop = POLY.offset(retop, pump, { + join: ClipperLib.JoinType.jtRound, + arc: (1/pump) * 4 + }); + nugroups.push(...retop); + } + data.tops = POLY.nest(nugroups); + } else { + data.tops = POLY.nest(groups); + } + if (isSynth) { const process = post_args.process; if (process.sliceSupportGrow > 0) { diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index f0bc89e2..bc91bb0e 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -345,7 +345,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } } if (process.xray) { - slice.index = process.xrayi.shift(); + slice.index = process.xray.shift(); slice.lines = lines; slice.groups = groups; slice.xray = process.xray; From 8eaa0a33a837e266208338e357cdada70ea810d8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 29 Sep 2025 12:28:51 -0400 Subject: [PATCH 60/60] fix angle diff calc for basic thin wall merge test --- src/kiri/mode/fdm/post.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 5a44e57d..25cd368e 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -165,9 +165,8 @@ function offset_default(params) { if (p0 === p1 || p0.segment === p1.segment) continue; let dist = p0.distTo2D(p1); if (dist < offset1) { - let diff = p0.segment.angle.angleDiff(p1.segment.angle,false); - if (p0.segment === p1.segment && diff < deltaBig) continue; - if (p0.segment !== p1.segment && diff > deltSmall && diff < deltaBig) continue; + let diff = p0.segment.angle.angleDiff(p1.segment.angle,true); + if (diff < deltaBig) continue; // merge points marking point offset and skip let mid = p0.midPointTo(p1); let inc = p0.distTo2D(mid) / offset1;