diff --git a/notes.md b/notes.md index 5efbac1a..7cf2186c 100644 --- a/notes.md +++ b/notes.md @@ -31,27 +31,19 @@ * `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` 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 +83,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 @@ -106,11 +98,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) @@ -122,3 +115,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/package.json b/package.json index a9ee6147..8ac0748a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.3.1", + "version": "4.3.2", "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 806fbd9b..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] @@ -2324,9 +2341,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 +2359,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/api.js b/src/kiri/core/api.js index c66292a3..42ec035c 100644 --- a/src/kiri/core/api.js +++ b/src/kiri/core/api.js @@ -96,6 +96,7 @@ let UC = UI.prefix('kiri').inputAction(settings.conf.update), put: (key, val) => localSet(key, val), set: (key, val) => localSet(key, val), setItem: (key, val) => localSet(key, val), + removeItem: (key) => localRemove(key) }, tweak = { line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, @@ -119,6 +120,11 @@ function localSet(key, val) { return val; } +function localRemove(key) { + let sloc = api.conf.get().local; + return delete sloc[key]; +} + export const api = { ajax, beta, 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 0) { this.lastPoint = this.startPoint; - // TODO: revisit seek to origin as the first move - // addOutput(output, startPoint, 0, undefined, tool); } output.appendAll(input); } @@ -98,6 +96,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 @@ -119,15 +118,16 @@ 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(), close = !options.open, + scarf = !poly.open ? (options.scarf ?? 0) : false, tool = options.tool, last = startPoint, first = true; @@ -137,14 +137,74 @@ 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 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) * 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) * fcs) - 1 - fco; + } + pp.push(...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) { // 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); @@ -154,7 +214,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 +229,7 @@ class Print { scope.addOutput(output, point, multOut, printSpeed, tool); } last = lpo = point; - }, close, closest.index); + } this.lastPoly = poly; @@ -176,6 +237,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 +268,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/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/core/widget.js b/src/kiri/core/widget.js index 783598ca..47222eb7 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -782,9 +782,11 @@ class Widget { } if (set) { let dark = this.api.space.is_dark(); + let cam = this.api.mode.is_cam(); + let color = dark && !cam ? 0xffffff : 0; let angle = this.api.conf.get().controller.edgeangle || 20; let edges = new THREE.EdgesGeometry(mesh.geometry, angle); - let material = new THREE.LineBasicMaterial({ color: 0 }); + let material = new THREE.LineBasicMaterial({ color }); this.outline = new THREE.LineSegments(edges, material); this.outline.renderOrder = -20; mesh.add(this.outline); diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 8481bdc0..b6768e4b 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, @@ -111,7 +112,11 @@ export function fdm_export(print, online, ondone, ondebug) { progress: 0, nozzle: tool, tool: tool, - model_labels: model_labels.sort().join(',') + oid: '', + oname: '', + 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, @@ -525,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; @@ -598,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); } @@ -620,6 +626,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; } @@ -779,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}, 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 @@ -830,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; @@ -958,7 +976,6 @@ export function fdm_export(print, online, ondone, ondebug) { } return true; - } if (inloops) { diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 5320c367..07571f0b 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) }), @@ -100,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)}), @@ -165,8 +168,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}), @@ -177,8 +178,11 @@ 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)}), + sliceCompInner: newInput(LANG.sl_ofin_s, {title:LANG.sl_ofin_l, convert:toFloat, bound:bound(-10.0,10.0)}), + sliceCompOuter: newInput(LANG.sl_ofot_s, {title:LANG.sl_ofot_l, convert:toFloat, bound:bound(-10.0,10.0)}), zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index 34458f49..b910958a 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -17,11 +17,44 @@ slicer.slicePost.FDM = slicePost; * data object. return is ignored. */ export function slicePost(data, options) { - const { z, lines, groups } = data; - const { useAssembly, post_args, zIndexes } = options; - const { process, isSynth, vaseMode } = post_args; - const { shellOffset, fillOffset, clipOffset } = post_args; - data.tops = POLY.nest(groups); + const { groups, z } = data; + const { post_args, useAssembly, zIndexes } = options; + const { isSynth, process, vaseMode } = post_args; + const { compInner, compOuter, pump } = post_args; + const { clipOffset, fillOffset, shellOffset } = post_args; + + if (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); + } + + // perimeter and inner hole compensation offsets + if (compInner || compOuter) { + let inner = POLY.inner(data.tops).flat(); + let outer = POLY.outer(data.tops); + if (compInner) { + inner = POLY.flatten(POLY.offset(inner, compInner/2)); + } + if (compOuter) { + outer = POLY.flatten(POLY.offset(outer, -compOuter/2)); + } + data.tops = POLY.nest([...inner, ...outer]); + } + if (isSynth) { const process = post_args.process; if (process.sliceSupportGrow > 0) { @@ -35,6 +68,7 @@ export function slicePost(data, options) { delete data.groups; return; } + const index = zIndexes.indexOf(z); const range = getRangeParameters(process, index); // calculate fractional shells @@ -110,7 +144,7 @@ function offset_default(params) { } const deltaBig = 160; - const deltSmall = 20; + const deltaSmall = 20; // look for close points on adjacent segmented polys and merge let test_polys = first !== last ? [ ...first, ...last ] : last; @@ -120,6 +154,8 @@ function offset_default(params) { for (let poly of test_flats) poly.forEachSegment((p0, p1) => { p0.angle = new Slope(p0, p1); + // todo calc vertex angle + // exclude segment vertex from merge when angle < deltaSmall }); // segment each poly to be tested @@ -146,14 +182,13 @@ 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; - moved = p0.moved = p1.moved = inc; - p1.skip = true; + moved = p0.moved = p1.moved = inc.round(4); + p1.skip = 1; p0.x = p1.x = mid.x; p0.y = p1.y = mid.y; } @@ -168,6 +203,8 @@ function offset_default(params) { for (let rec of thin_test) { if (rec.moved) { let last_seg; + // tag for codec extended point encoding + rec.poly._epk = ["moved", "skip"]; rec.poly.points = rec.points.filter((p,i) => { let keep = (last_seg !== p.segment || p.moved || p.skip); last_seg = p.segment; @@ -485,6 +522,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); @@ -694,7 +732,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; @@ -702,13 +741,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 bd676ec2..8d560b43 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 = []; @@ -141,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 = []; @@ -195,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 @@ -224,6 +226,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 +472,9 @@ export async function fdm_prepare(widgets, settings, update) { let lastExt; let lastOut; + print.zmax = zmax; + print.total_time = 0; + // walk cake layers bottom up for (let layer of cake) { // track purge blocks generated for each layer @@ -570,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; @@ -796,6 +804,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), @@ -884,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; } @@ -909,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) { @@ -1032,8 +1047,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) { @@ -1066,30 +1081,32 @@ 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; + let scarf = (slice.index > 0 && traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0; 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), - onfirst: function(firstPoint) { + nozzleSize, + rate: finishShell ? finishSpeed : printSpeed, + scarf, + tool: extruder, + onfirst: (firstPoint, preout) => { let from = seedPoint || startPoint; if (from.distTo2D(firstPoint) > retractDist) { - if (intersectsTop(from, firstPoint)) { + if (requiresRetract(from, firstPoint)) { retract(); } } @@ -1171,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) { @@ -1215,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); @@ -1364,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(); } @@ -1473,7 +1490,15 @@ 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(); + 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": @@ -1559,13 +1584,38 @@ 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) + 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 (minLayerTime && 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 - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "4.3.1" + VERSION: "4.3.2" }; export const beta = 0; diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a9b61cf5..39eb200d 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -360,8 +360,12 @@ 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"], + sl_ofin_s: "shell inner offset", + sl_ofin_l: ["inner hole perimeter offset","in millimeters"], + sl_ofot_s: "shell outer offset", + sl_ofot_l: ["outer wall perimeter offset","in millimeters"], // FDM SOLID fs_menu: "solid fill", @@ -765,8 +769,10 @@ self.lang['en-us'] = { ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], ou_fanl_s: "fan on layer", ou_fanl_l: ["layer # to turn on fan","layers start at 0"], - ou_fans_s: "fan speed", + ou_fans_s: "fan on speed", ou_fans_l: "0 - 255", + ou_layt_s: "min layer time", + ou_layt_l: ["minimum allowable layer print time in seconds. printing will be slowed if the layer would outerwise complete in fewer seconds.","0 = disabled"], // OUTPUT CAM ou_toll_s: "precision", @@ -848,7 +854,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"], @@ -862,11 +868,13 @@ self.lang['en-us'] = { 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"], + ad_scar_s: "scarf length", + 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_zhop_s: "z hop dist", + 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", 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"], @@ -876,8 +884,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", diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index 051f5f4c..80b2d2f8 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.3.1", + "name": "Kiri:Moto 4.3.2", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/kiri/",