add fdm scarf seams and min layer time

This commit is contained in:
Stewart Allen 2025-09-27 21:15:21 -04:00
commit 79a227d741
8 changed files with 129 additions and 32 deletions

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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)}),

View file

@ -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<preout.length; i++) {
preout[i].point = preout[i].point.add(offset);

View file

@ -765,8 +765,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",
@ -862,6 +864,8 @@ 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","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",