fix arc detect radian / degree

add polygon thickness() ratio helper
add fdm slice delta thickness filter parameter
add fdm print to moonraker auto-start option
fix fdm finish speed ratio for flats and bridges
reflow fdm slice.js code for better readability
This commit is contained in:
Stewart Allen 2026-01-09 15:18:44 -05:00
commit 7e92845662
11 changed files with 429 additions and 389 deletions

View file

@ -115,19 +115,20 @@ export function findArcCenters(poly, opt = {}) {
* @param {Polygon} poly - polygon to analyze (will be mutated with arc annotations)
* @param {Object} opts - detection options
* @param {number} opts.tolerance - arc detection tolerance (default 0.1)
* @param {number} opts.arcRes - arc resolution in radians (default 0.1)
* @param {number} opts.arcRes - arc resolution in degrees (default 1)
* @param {number} opts.minPoints - minimum points to consider an arc (default 4)
* @returns {Polygon} this polygon with arc annotations
*/
export function detectArcs(poly, opts = {}) {
const {
tolerance = 0.1,
arcRes = 0.1,
arcRes = 1,
minPoints = 4
} = opts;
const points = poly.points;
const length = points.length;
const arcResRadians = arcRes * (Math.PI / 180);
if (length < minPoints) {
return poly;
@ -143,7 +144,7 @@ export function detectArcs(poly, opts = {}) {
// because we need at least minPoints to form an arc
while (i < length - minPoints + 1) {
// Try to detect arc starting at position i
const arcData = detectArcAt(poly, i, points, length, tolerance, arcRes, minPoints);
const arcData = detectArcAt(poly, i, points, length, tolerance, arcResRadians, minPoints);
if (arcData) {
// Annotate the starting point
@ -172,7 +173,7 @@ export function detectArcs(poly, opts = {}) {
* @param {number} minPoints - minimum points for arc
* @returns {Object|null} arc data or null if no arc detected
*/
export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) {
function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) {
// Greedy approach: collect as many points as possible that might form an arc
let candidates = [];
@ -222,7 +223,7 @@ export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, m
* @param {number} minPoints - minimum points for arc
* @returns {Object|null} arc data or null if invalid
*/
export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) {
function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) {
if (arcPoints.length < minPoints) {
return null;
}
@ -295,7 +296,7 @@ export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) {
* @param {number} tolerance - detection tolerance
* @returns {Object|null} center with x, y, r properties or null
*/
export function findBestCenter(arcPoints, tolerance) {
function findBestCenter(arcPoints, tolerance) {
const len = arcPoints.length;
// Use 3 well-spaced points for initial center calculation

View file

@ -1221,6 +1221,21 @@ export class Polygon {
return a;
}
/**
* find the ratio of area to perimeter which can give provide
* guidance if this polygon is likely to have a thin cross-section
*
* @param {*} deep whether to include inner holes in the calculation
* @returns ratio
*/
thickness(deep) {
if (deep) {
return 2 * this.areaDeep() / this.perimeterDeep();
} else {
return 2 * this.area() / this.perimeter();
}
}
/**
* @param {Polygon} poly
* @returns {boolean}

View file

@ -337,6 +337,7 @@ export const conf = {
sliceShells: 3,
sliceSolidify: false,
sliceSolidMinArea: 1,
sliceSolidMinThick: 0.4,
sliceSupportAngle: 50,
sliceSupportDensity: 0.1,
sliceSupportExtra: 0,

View file

@ -262,12 +262,8 @@ function exportGCodeDialog(gcode, sections, info, names) {
localSet('octo-apik', apik.trim());
localSet('octo-type', type.trim());
filename = $('print-filename').value;
form.append("file", getBlob(), filename+"."+fileext);
// moonraker upload params
// form.append("root", "gcodes"); // optional but recommended
// form.append("path", ""); // optional subfolder
if (start) form.append("print", "true"); // <-- auto-start
filename = $('print-filename').value + "." + fileext;
form.append("file", getBlob(), filename);
ajax.onreadystatechange = function() {
console.log({ ajax_readyState: ajax.readyState });
if (ajax.readyState === 4) {
@ -279,6 +275,16 @@ function exportGCodeDialog(gcode, sections, info, names) {
api.show.alert("send error\nstatus: "+status+"\nmessage: "+ajax.responseText);
}
api.show.progress(0);
if (start && type === 'moonraker') {
fetch(`${host}/printer/print/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
})
.then(r => r.json())
.then(data => console.log('print start', data))
.catch(err => console.error('print start error', err))
}
}
api.show.progress(0);
};

View file

@ -133,7 +133,7 @@ class Print {
scarf = !poly.open ? (options.scarf ?? 0) : false,
last = startPoint,
first = true,
{ arcOpts, tool, zmax } = options;
{ arcOpts, tool, zmax, onfirst, onfirstout } = options;
// if short, use calculated print speed based on sliding scale
if (perimeter < process.outputShortPoly) {
@ -169,7 +169,7 @@ class Print {
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);
poly = poly.segment(nozzleSize ?? 0.4, false, false, scarf * 2);
pp = poly.points;
let lp, sp = [];
for (let p of pp) {
@ -196,6 +196,7 @@ class Print {
} else if (arcOpts) {
// annotate arc points
pp = newPolygon(pp).detectArcs(arcOpts).points;
// console.log(arcOpts, pp.map(p => p.arc?.skip).filter(v => v));
}
// scarf manages its own close point
@ -210,10 +211,13 @@ class Print {
let { arc } = point;
if (arc) {
arcInfo = arc;
scope.addOutput(output, point, shellMult, moveSpeed, tool);
if (first && onfirst) onfirst(point, output);
let out = scope.addOutput(output, point, shellMult, moveSpeed, tool);
if (first && onfirstout) onfirstout(out);
// arc center is relative to first point
arcInfo.center.move({ x: -point.x, y: -point.y, z: 0 });
last = point;
if (first) first = false;
continue;
} else if (arcInfo) {
// accumulate and pass to last point to calc FDM E value
@ -235,14 +239,10 @@ class Print {
scope.addOutput(output, point, 0, moveSpeed, tool);
} else if (first) {
// if (point.skip) console.log({ skip: point });
if (options.onfirst) {
options.onfirst(point, output);
}
onfirst && onfirst(point, output);
// move to first output point on poly
let out = scope.addOutput(output, point, 0, moveSpeed, tool);
if (options.onfirstout) {
options.onfirstout(out);
}
onfirstout && onfirstout(out);
first = false;
} else {
let seglen = last.distTo2D(point);

View file

@ -101,6 +101,7 @@ export function menu() {
sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}),
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}),
sliceSolidMinThick: newInput(LANG.ad_mthk_s, {title:LANG.ad_mthk_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}),

View file

@ -30,6 +30,7 @@ export function init() {
"sliceFillSparse": LANG.fi_pcnt_s,
"sliceFillGrow": LANG.fi_grow_s,
"sliceSolidMinArea": LANG.ad_msol_s,
"sliceSolidMinThick": LANG.ad_mthk_s,
"sliceTopLayers": LANG.sl_ltop_s,
"sliceBottomLayers": LANG.sl_lbot_s,
"firstLayerRate": LANG.fl_rate_s,

View file

@ -541,7 +541,6 @@ export async function fdm_prepare(widgets, settings, update) {
},
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
support: slice.supports,
zmax,
}
);
@ -780,7 +779,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
const { settings } = print;
const { device } = settings;
const { bedWidth, bedDepth, bedRound } = device;
const { extrudePerMM } = FDM;
let preout = [],
process = opt.params || settings.process,
@ -790,7 +788,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
minLayerTime = process.outputMinLayerTime || 0,
nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle,
nozzles = device.extruders.map(e => e.extNozzle),
firstLayer = (opt.first || false) && !opt.support,
firstLayer = (opt.first || false),
thinWall = nozzleSize * (opt.thinWall || 1.75),
retractDist = opt.retractOver || (nozzleSize * 5),
fillMult = opt.mult || process.outputFillMult,
@ -845,10 +843,12 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
printSpeed = finishSpeed || printSpeed;
}
// override: adapt bridge layer speed
if (slice.isBridgeLayer) {
// override: adapt bridge layer speed and flats
// but not on the first layer which has it's own settings
if (slice.index > 0 && (slice.isBridgeLayer || slice.isFlatsLayer)) {
fillSpeed /= 2;
printSpeed /= 2;
finishSpeed /= 2;
}
function retract() {
@ -1444,7 +1444,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
if (slice.tops) {
out.appendAll(slice.tops);
}
if (opt.support && slice.supports) {
if (slice.supports) {
out.appendAll(slice.supports);
}

View file

@ -63,6 +63,7 @@ const CONSTANTS = {
* @property {number} sliceFillGrow - Fill boundary expansion in mm
* @property {number} sliceFillOverlap - Fill overlap with shells (0.0-0.8)
* @property {number} sliceSolidMinArea - Minimum area in mm² for solid regions
* @property {number} sliceSolidMinThick - Miminum "thickness" ratio for culling thin solids
* @property {number} sliceSupportDensity - Support fill density (0.0-1.0)
* @property {number} sliceSupportFill - Support fill angle (or auto if >= 1000)
* @property {number} sliceSupportGap - Gap between support and part in mm
@ -266,6 +267,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
let indices = [];
let heights = [];
let healed = false;
let slices; // set by decodeSlices()
// handle z cutting (floor method) and base flattening
let zPress = isBelt ? process.firstLayerFlatten || 0 : 0;
@ -328,35 +330,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
.then(processSlices)
.then(ondone);
// turn slicer raw output into Slice objects
function decodeSlices(output) {
// post process slices and re-incorporate missing meta-data
return output.slices.map(data => {
let { z, clip, lines, groups, changes } = data;
if (!data.tops) return null;
let slice = newSlice(z).addTops(data.tops, { minArea: CONSTANTS.MIN_POLY_AREA });
slice.index = indices.indexOf(z);
slice.height = heights[slice.index];
slice.clips = clip;
// do not warn on merging supports
if (changes) {
healed = true;
slice.changes = changes;
if (devel) {
console.log('slice healed', slice.index, slice.z, changes);
}
}
if (process.xray) {
slice.index = process.xray.shift();
slice.lines = lines;
slice.groups = groups;
slice.xray = slice.index;
}
return slice;
}).filter(s => s);
}
// z index generator
// z index generator (bottom up)
function zGen(zopt) {
if (process.xray) {
return zopt.zIndexes;
@ -438,321 +412,365 @@ export function sliceOne(settings, widget, onupdate, ondone) {
return zi;
}
async function processSlices(slices) {
/**
* Process automatic and manual shadow-based support generation
*/
async function processSupports() {
if (process.sliceSupportType === 'disabled') {
return;
// turn slicer raw output into Slice objects
// z index order should be bottom up at this point
async function decodeSlices(output) {
// post process slices and re-incorporate missing meta-data
slices = output.slices.map(data => {
let { z, clip, lines, groups, changes } = data;
if (!data.tops) return null;
let slice = newSlice(z).addTops(data.tops, { minArea: CONSTANTS.MIN_POLY_AREA });
slice.index = indices.indexOf(z);
slice.height = heights[slice.index];
slice.clips = clip;
// do not warn on merging supports
if (changes) {
healed = true;
slice.changes = changes;
if (devel) {
console.log('slice healed', slice.index, slice.z, changes);
}
}
if (process.xray) {
slice.index = process.xray.shift();
slice.lines = lines;
slice.groups = groups;
slice.xray = slice.index;
}
return slice;
}).filter(s => s);
}
let stack = slices.slice();
let indices = stack.map(s => s.z);
let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180);
let manual = process.sliceSupportType === 'manual';
// calculate % complete and call onupdate()
function doupdate(index, from, to, msg) {
trackupdate(index / slices.length, from, to, msg);
}
// sort bottom up so shadows do not accumulate
// since that is done later and clipped to slice.clips
stack.sort((a,b) => a.z - b.z);
// slicing is the first 50% of the update "time"
function trackupdate(pct, from, to, msg) {
onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg);
}
// process manual supports if they exist
// convert paint points to circles on matching slices
let { paint } = widget.anno;
if (manual && paint?.length) {
let hpi = Math.PI/2;
for (let slice of stack) {
let polys = [];
for (let rec of paint) {
let { point, radius } = rec;
let dz = Math.abs(slice.z - point.z);
if (dz < radius) {
// scale radius by distance from point.z
radius = (Math.acos(dz/radius) / hpi) * radius;
polys.push(newPolygon().centerCircle(point, radius, 10));
}
// for each slice, performe a function and call doupdate()
function forSlices(from, to, fn, msg) {
slices.forEach(slice => {
fn(slice);
doupdate(slice.index, from, to, msg)
});
}
/**
* Process automatic and manual shadow-based support generation
*/
async function processSupports() {
if (process.sliceSupportType === 'disabled') {
return;
}
let stack = slices.slice();
let indices = stack.map(s => s.z);
let zAngNorm = Math.sin(process.sliceSupportAngle * Math.PI / 180);
let manual = process.sliceSupportType === 'manual';
// sort bottom up so shadows do not accumulate
// since that is done later and clipped to slice.clips
stack.sort((a,b) => a.z - b.z);
// process manual supports if they exist
// convert paint points to circles on matching slices
let { paint } = widget.anno;
if (manual && paint?.length) {
let hpi = Math.PI/2;
for (let slice of stack) {
let polys = [];
for (let rec of paint) {
let { point, radius } = rec;
let dz = Math.abs(slice.z - point.z);
if (dz < radius) {
// scale radius by distance from point.z
radius = (Math.acos(dz/radius) / hpi) * radius;
polys.push(newPolygon().centerCircle(point, radius, 10));
}
slice.shadow = POLY.union(polys, 0, true);
}
slice.shadow = POLY.union(polys, 0, true);
}
// create automatic shadows supports when on manual paint
if (!manual) {
await widget.computeShadowStack(indices, progress => {
trackupdate(progress, 0.05, 0.10, "shadow");
}, zAngNorm);
for (let slice of stack) {
slice.shadow = await widget.shadowAt(slice.z, true);
}
}
// 1. accumulate / union shadow coverage top down
// 2. trim to area outside slice.clips
let minArea = lineWidth;
let shadowSum;
let length = stack.length;
let count = 0;
// perform accumulation top down
for (let slice of stack.reverse()) {
let shadow = slice.shadow ?? [];
if (process.sliceSupportExtra) {
shadow = POLY.offset(shadow, process.sliceSupportExtra);
}
// shadows accumulate down
if (shadowSum) {
shadow = POLY.union([...shadow, ...shadowSum], minArea, true);
}
// trim to slice.clips
if (true) {
let rem = [];
let clips = [
slice.up?.clips,
slice.clips,
slice.down?.clips
].filter(v => v).flat();
clips = POLY.union(clips, minArea, true);
POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false });
shadow = rem;
}
// pump shadow to clean to clean it up
if (true) {
let bump = lineWidth * 2;
shadow = POLY.offset(shadow, bump, { z: slice.z });
shadow = POLY.offset(shadow, -bump, { z: slice.z });
}
shadowSum = shadow;
slice.supports = shadow;
slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
trackupdate((++count/length), 0.10, 0.15, "support");
}
}
/**
* Process top and bottom layers or any other
* layers detected and marked for solid fill
*/
function processSolidLayers() {
forSlices(0.15, 0.2, slice => {
let range = slice.params;
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 > CONSTANTS.DENSE_INFILL_THRESHOLD;
let isSolid = (isBottom || ((isTop || isDense) && !vaseMode));
let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0;
if (solidWidth) {
let fillSpace = fillSpacing * solidWidth;
layerMakeSolid({ slice, spacing: fillSpace, angle: 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;
}
sliceFillAngle = (sliceFillAngle + 90.0) % 360;
}, "solid layers");
// create automatic shadows supports when on manual paint
if (!manual) {
await widget.computeShadowStack(indices, progress => {
trackupdate(progress, 0.05, 0.10, "shadow");
}, zAngNorm);
for (let slice of stack) {
slice.shadow = await widget.shadowAt(slice.z, true);
}
}
/**
* Process layer diffs and project solid areas
*/
function processLayerDiffs() {
// boolean diff layers to detect bridges and flats
profileStart("delta");
forSlices(0.2, 0.34, slice => {
let params = slice.params || process;
let solidMinArea = params.sliceSolidMinArea;
let sliceFillGrow = params.sliceFillGrow;
layerDiff(slice, { min: solidMinArea, grow: sliceFillGrow });
}, "layer deltas");
profileEnd();
// project bridges and flats up and down into part
profileStart("delta-project");
forSlices(0.34, 0.35, slice => {
let params = slice.params || process;
topLayers = params.sliceTopLayers || 0;
bottomLayers = params.sliceBottomLayers || 0;
if (topLayers) projectFlats(slice, topLayers);
if (bottomLayers) projectBridges(slice, bottomLayers);
if (slice.flats) POLY.setZ(slice.flats, slice.z);
}, "layer deltas");
profileEnd();
}
// 1. accumulate / union shadow coverage top down
// 2. trim to area outside slice.clips
let minArea = lineWidth;
let shadowSum;
let length = stack.length;
let count = 0;
/**
* Process solid fill patterns
*/
async function processSolidFills() {
profileStart("solid-fill")
let promises = isConcurrent ? [] : undefined;
forSlices(0.35, promises ? 0.4 : 0.5, slice => {
let params = slice.params || process;
let solidWidth = params.sliceFillWidth || 1;
// perform accumulation top down
for (let slice of stack.reverse()) {
let shadow = slice.shadow ?? [];
if (process.sliceSupportExtra) {
shadow = POLY.offset(shadow, process.sliceSupportExtra);
}
// shadows accumulate down
if (shadowSum) {
shadow = POLY.union([...shadow, ...shadowSum], minArea, true);
}
// trim to slice.clips
if (true) {
let rem = [];
let clips = [
slice.up?.clips,
slice.clips,
slice.down?.clips
].filter(v => v).flat();
clips = POLY.union(clips, minArea, true);
POLY.subtract(shadow, clips, rem, null, slice.z, minArea, { wasm: false });
shadow = rem;
}
// pump shadow to clean to clean it up
if (true) {
let bump = lineWidth * 2;
shadow = POLY.offset(shadow, bump, { z: slice.z });
shadow = POLY.offset(shadow, -bump, { z: slice.z });
}
shadowSum = shadow;
slice.supports = shadow;
slice.output().setLayer("shadow", 0xff0000).addPolys(shadow);
trackupdate((++count/length), 0.10, 0.15, "support");
}
}
/**
* Process top and bottom layers or any other
* layers detected and marked for solid fill
*/
async function processSolidLayers() {
forSlices(0.15, 0.2, slice => {
let range = slice.params;
let isBottom = slice.index < bottomLayers;
let isTop = topLayers && slice.index > slices.length - topLayers - 1;
let isDense = range.sliceFillSparse > CONSTANTS.DENSE_INFILL_THRESHOLD;
let isSolid = (isBottom || ((isTop || isDense) && !vaseMode));
let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0;
if (solidWidth) {
let fillSpace = fillSpacing * solidWidth;
let solidMinArea = params.sliceSolidMinArea;
layerFillSolids({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises });
sliceFillAngle = (sliceFillAngle + 90.0) % 360;
}, "fill solids");
// very last layer (top) is set to finish solid rate
slices.last().finishSolids = true
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.4, 0.5);
});
layerMakeSolid({ slice, spacing: fillSpace, angle: sliceFillAngle });
}
profileEnd();
}
/**
* Process sparse infill patterns
*/
async function processSparseInfill() {
let lastType;
let promises = isConcurrent ? [] : undefined;
forSlices(0.5, promises ? 0.55 : 0.7, slice => {
let params = slice.params || process;
if (!params.sliceFillSparse) {
return;
}
let newType = params.sliceFillType;
layerSparseFill(slice, {
settings,
process,
device,
lineWidth,
spacing: fillOffset,
density: params.sliceFillSparse,
bounds: widget.getBoundingBox(),
height: sliceHeight,
type: newType,
cache: params._range !== true && lastType === newType,
promises
});
lastType = newType;
}, "infill");
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.55, 0.7);
});
if (slice.index === slices.length - 1) {
slice.isFlatsLayer = true;
}
// back-fill slices marked for infill cloning
for (let slice of slices) {
if (slice._clone_sparse) {
let tops = slice.tops;
let down = slice.down.tops;
for (let i=0; i<tops.length; i++) {
tops[i].fill_sparse = down[i].fill_sparse.map(p => p.cloneZ(slice.z));
}
}
sliceFillAngle = (sliceFillAngle + 90.0) % 360;
}, "solid layers");
}
/**
* Process layer diffs and project solid areas
*/
async function processLayerDiffs() {
// boolean diff layers to detect bridges and flats
profileStart("delta");
forSlices(0.2, 0.33, slice => {
let params = slice.params || process;
let solidMinArea = params.sliceSolidMinArea;
let sliceMinThick = params.sliceSolidMinThick;
let sliceFillGrow = params.sliceFillGrow;
layerDiff(slice, { area: solidMinArea, grow: sliceFillGrow, thick: sliceMinThick });
}, "layer deltas");
profileEnd();
// project bridges and flats up and down into part
profileStart("delta-project");
forSlices(0.33, 0.34, slice => {
let params = slice.params || process;
topLayers = params.sliceTopLayers || 0;
bottomLayers = params.sliceBottomLayers || 0;
if (topLayers) projectFlats(slice, topLayers);
if (bottomLayers) projectBridges(slice, bottomLayers);
if (slice.flats) POLY.setZ(slice.flats, slice.z);
}, "layer deltas");
profileEnd();
// union solid areas
profileStart("solid-union");
forSlices(0.34, 0.35, slice => {
if (slice.solids) {
slice.solids = POLY.union(slice.solids, 0, true);
}
}
});
profileEnd();
}
/**
* Process support structure fills
*/
async function processSupportFills() {
profileStart("support-fill");
let promises = false && isConcurrent ? [] : undefined;
forSlices(0.8, promises ? 0.88 : 0.9, slice => {
let params = slice.params || process;
let density = params.sliceSupportDensity;
layerSupportFill({
angle: process.sliceSupportFill,
density,
isBelt,
lineWidth,
outline: process.sliceSupportOutline !== false,
promises,
slice,
});
}, "support fill");
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.88, 0.9);
});
}
profileEnd();
}
/**
* Process brick/interleave mode slicing
*/
function processBrickMode() {
let indices = slices.map(s => s.index);
let first = indices[1];
let last = indices[indices.length - 2];
let nu = [];
for (let slice of slices) {
if (slice.index < first || slice.index > last) {
continue;
}
let nuSlice = slice.clone();
nuSlice.z -= slice.height / 2;
if (slice.index === first) {
nuSlice.z = slice.z - slice.height / 4;
nuSlice.height = slice.height / 2;
} else {
nuSlice.height = slice.height;
}
nu.push(nuSlice);
let ti = 0;
for (let top of slice.tops || []) {
let nuTop = nuSlice.tops[ti++];
nuTop.shells = [];
top.shells = top.shells.filter((s,i) => {
if (i % 2 === 0) {
return true;
} else {
nuTop.shells.push(s);
return false;
}
});
}
if (slice.index === last) {
let cap = nuSlice.clone();
cap.z += (slice.height * 0.75);
cap.height = (slice.height / 2);
nu.push(cap);
cap.tops?.forEach((top, i) => {
top.shells = nuSlice.tops[i].shells.clone();
});
}
}
slices.appendAll(nu);
slices.sort((a,b) => a.z - b.z);
slices.forEach((s,i) => s.index = i);
}
// calculate % complete and call onupdate()
function doupdate(index, from, to, msg) {
trackupdate(index / slices.length, from, to, msg);
}
function trackupdate(pct, from, to, msg) {
onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg);
}
// for each slice, performe a function and call doupdate()
function forSlices(from, to, fn, msg) {
slices.forEach(slice => {
fn(slice);
doupdate(slice.index, from, to, msg)
/**
* Process solid fill patterns
*/
async function processSolidFills() {
profileStart("solid-fill")
let promises = isConcurrent ? [] : undefined;
forSlices(0.35, promises ? 0.4 : 0.5, slice => {
let params = slice.params || process;
let solidWidth = params.sliceFillWidth || 1;
let fillSpace = fillSpacing * solidWidth;
let solidMinArea = params.sliceSolidMinArea;
layerFillSolids({ slice, spacing: fillSpace, angle: sliceFillAngle, minArea: solidMinArea, promises });
sliceFillAngle = (sliceFillAngle + 90.0) % 360;
}, "fill solids");
// very last layer (top) is set to finish solid rate
slices.last().finishSolids = true
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.4, 0.5);
});
}
profileEnd();
}
/**
* Process sparse infill patterns
*/
async function processSparseInfill() {
let lastType;
let promises = isConcurrent ? [] : undefined;
forSlices(0.5, promises ? 0.55 : 0.7, slice => {
let params = slice.params || process;
if (!params.sliceFillSparse) {
return;
}
let newType = params.sliceFillType;
layerSparseFill(slice, {
settings,
process,
device,
lineWidth,
spacing: fillOffset,
density: params.sliceFillSparse,
bounds: widget.getBoundingBox(),
height: sliceHeight,
type: newType,
cache: params._range !== true && lastType === newType,
promises
});
lastType = newType;
}, "infill");
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.55, 0.7);
});
}
// back-fill slices marked for infill cloning
for (let slice of slices) {
if (slice._clone_sparse) {
let tops = slice.tops;
let down = slice.down.tops;
for (let i=0; i<tops.length; i++) {
tops[i].fill_sparse = down[i].fill_sparse.map(p => p.cloneZ(slice.z));
}
}
}
}
/**
* Process support structure fills
*/
async function processSupportFills() {
profileStart("support-fill");
let promises = false && isConcurrent ? [] : undefined;
forSlices(0.8, promises ? 0.88 : 0.9, slice => {
let params = slice.params || process;
let density = params.sliceSupportDensity;
layerSupportFill({
angle: process.sliceSupportFill,
density,
isBelt,
lineWidth,
outline: process.sliceSupportOutline !== false,
promises,
slice,
});
}, "support fill");
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.88, 0.9);
});
}
profileEnd();
}
/**
* Process brick/interleave mode slicing
*/
async function processBrickMode() {
let indices = slices.map(s => s.index);
let first = indices[1];
let last = indices[indices.length - 2];
let nu = [];
for (let slice of slices) {
if (slice.index < first || slice.index > last) {
continue;
}
let nuSlice = slice.clone();
nuSlice.z -= slice.height / 2;
if (slice.index === first) {
nuSlice.z = slice.z - slice.height / 4;
nuSlice.height = slice.height / 2;
} else {
nuSlice.height = slice.height;
}
nu.push(nuSlice);
let ti = 0;
for (let top of slice.tops || []) {
let nuTop = nuSlice.tops[ti++];
nuTop.shells = [];
top.shells = top.shells.filter((s,i) => {
if (i % 2 === 0) {
return true;
} else {
nuTop.shells.push(s);
return false;
}
});
}
if (slice.index === last) {
let cap = nuSlice.clone();
cap.z += (slice.height * 0.75);
cap.height = (slice.height / 2);
nu.push(cap);
cap.tops?.forEach((top, i) => {
top.shells = nuSlice.tops[i].shells.clone();
});
}
}
slices.appendAll(nu);
slices.sort((a,b) => a.z - b.z);
slices.forEach((s,i) => s.index = i);
}
async function renderSlices() {
forSlices(0.9, 1.0, slice => {
let params = slice.params || process;
layerRender(slice, params, {
dark: controller.dark,
devel: controller.devel,
renderContext
});
}, "render");
}
async function processSlices() {
// alert non-manifold parts
if (healed) {
onupdate(null, null, "part may not be manifold");
}
// reverse slices to perform these ops top-down
// remove all empty slices above part but leave below
// for multi-part (multi-extruder) setups where the void is ok
// also reverse because slicing occurs bottom-up
let found = false;
slices = slices.reverse().filter(slice => {
if (slice.tops.length) {
@ -762,40 +780,48 @@ export function sliceOne(settings, widget, onupdate, ondone) {
}
}).reverse();
// inject one empty slice at the top so that
// top layer flats are detected properly
slices.push(newSlice(slices.peek().index + 1));
// attach slices to widget since the
// variable will not be replaced after this
widget.slices = slices;
// exit if no slices detected with tops
if (!slices || slices.length === 0) {
return;
}
// connect slices into linked list for island/bridge projections
for (let i=1; i<slices.length; i++) {
slices[i-1].up = slices[i];
slices[i].down = slices[i-1];
}
widget.slices = slices;
if (!slices || slices.length === 0) {
return;
}
// attach range params to each slice
// should slice.index be recalculated after filtering?
for (let slice of slices) {
slice.params = getRangeParameters(process, slice.index);
}
// reset for solids, support projections
// and other annotations
// reset solids, support projections, and other annotations
for (let slice of slices) {
slice.widget = widget;
slice.extruder = extruder;
slice.solids = [];
slice.widget = widget;
}
// new auto support demonstrator using cast shadow
// support generation using either
// enclosed shadow or manual painted supports
await processSupports();
// process solid layers (top/bottom)
processSolidLayers();
await processSolidLayers();
// add lead in anchor when specified in belt mode (but not for synths)
if (isBelt) {
// Generate belt anchor
// generate belt anchor
generateBeltAnchor({
slices,
widget,
@ -806,7 +832,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
extruder
});
// Calculate smin for pooch text embossing
// calculate smin for pooch text embossing
let smin = Infinity;
for (let slice of slices) {
if (slice.belt && slice.belt.miny < smin) {
@ -814,7 +840,7 @@ export function sliceOne(settings, widget, onupdate, ondone) {
}
}
// Experimental emboss text on flat underside
// experimental emboss text on flat underside
embossBeltPooch({
slices,
widget,
@ -828,14 +854,11 @@ export function sliceOne(settings, widget, onupdate, ondone) {
// layer boolean diffs need to be computed to find flat areas to fill
// and overhangs that need to be supported. these are stored in flats
// and bridges, projected up/down, and merged into an array of solids
if (!vaseMode) {
processLayerDiffs();
await processSolidFills();
}
// for "real" objects, fill the remaining voids with sparse fill
// sparse layers only present when non-vase mode and sparse % > 0
if (!vaseMode) {
await processLayerDiffs();
await processSolidFills();
await processSparseInfill();
}
@ -846,19 +869,12 @@ export function sliceOne(settings, widget, onupdate, ondone) {
// brick/interleave mode processing
if (isBrick) {
processBrickMode();
await processBrickMode();
}
// render if not explicitly disabled
if (render) {
forSlices(0.9, 1.0, slice => {
let params = slice.params || process;
layerRender(slice, params, {
dark: controller.dark,
devel: controller.devel,
renderContext
});
}, "render");
await renderSlices();
}
if (isBelt) {
@ -913,14 +929,15 @@ function bound(v,min,max) {
*/
function layerRender(slice, params, opt = {}) {
const { dark, devel, renderContext: ctx } = opt;
const { offset, isThin, isFlat } = ctx;
const Color = dark ? COLOR_DARK : COLOR;
const output = slice.output();
const height = slice.height / 2;
const solidWidth = params.sliceFillWidth || 1;
const { offset, isThin, isFlat } = ctx;
if (slice.tops) // missing for supports
slice.tops.forEach(top => {
if (slice.tops?.length)
for (let top of slice.tops) {
if (isThin) output
.setLayer('part', Color.part)
.addPolys([top.poly]);
@ -970,7 +987,7 @@ function layerRender(slice, params, opt = {}) {
// .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 })
// .addPolys(top.last);
}
});
}
if (isThin && devel) {
if (slice.solids?.length) output
@ -1088,13 +1105,7 @@ export function layerProcessTops(slice, count, offset1, offsetN, fillOffset, opt
/**
* Create an entirely solid layer by filling all top polygons
* with an alternating pattern.
*
* @param {number} linewidth
* @param {number} angle
* @param {number} density
*/
/**
* Create solid fill layer by filling all top polygons with alternating pattern
*
* @param {Object} args - Options object
* @param {Slice} args.slice - Slice to fill
* @param {number} args.spacing - Fill line spacing in mm
@ -1299,33 +1310,38 @@ function layerSparseFill(slice, options = {}) {
* 'expand' is used for top offsets in SLA mode
*/
export function layerDiff(slice, options = {}) {
const { sla, fakedown, grow, min } = options;
if ((slice.index <= 0 && !fakedown) || slice.xray) {
const { sla, grow, area, thick } = options;
if (slice.index < 0 || slice.xray) {
return;
}
const top = slice,
down = slice.down || (fakedown ? newSlice(-1) : null),
let boundary = !(slice.up && slice.down),
top = slice,
down = slice.down || newSlice(-1),
topInner = sla ? top.topPolys() : top.topInners(),
downInner = sla ? down.topPolys() : down.topInners(),
bridges = top.bridges = [],
flats = down.flats = [];
// skip diffing layers that are identical
if (slice.fingerprintSame(down)) {
if (!boundary && slice.fingerprintSame(down)) {
top.bridges = bridges;
down.flats = flats;
// console.log(slice.z, 'layer fingerprint match');
return;
}
let newBridges = [];
let newFlats = [];
POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, min, {
POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, area, {
wasm: true
});
newBridges = newBridges.filter(p => p.areaDeep() >= min);
newFlats = newFlats.filter(p => p.areaDeep() >= min);
// console.log(slice.z, { newBridges, newFlats });
newBridges = newBridges.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
newFlats = newFlats.filter(p => p.areaDeep() >= area && p.thickness(true) >= thick);
if (grow > 0 && newBridges.length) {
newBridges = POLY.offset(newBridges, grow);
@ -1652,20 +1668,21 @@ export function projectFlats(slice, count, expand) {
if (slice.flats?.length) {
slice.finishSolids = true;
slice.isFlatsLayer = true;
const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats;
projectSolid({ slice, polys: flats, count, up: false, first: true });
const polys = expand ? POLY.expand(slice.flats, expand) : slice.flats;
projectSolid({ slice, polys, count, up: false, first: true });
}
}
/**
* project top bridges up into part
*/
export function projectBridges(slice, count) {
export function projectBridges(slice, count, expand) {
if (!slice.up || !slice.bridges) return;
// these flats are marked for finishing print speed
// these bridges are marked for finishing print speed
if (slice.bridges?.length) {
slice.finishSolids = true;
slice.isBridgeLayer = true;
const polys = expand ? POLY.expand(slice.bridges, expand) : slice.bridges;
projectSolid({ slice, polys, count, up: true, first: true });
}
projectSolid({ slice, polys: slice.bridges, count, up: true, first: true });
}

View file

@ -777,7 +777,3 @@
</div>
</body>
</html>
<div id="opapik">
<label lk="ex_akey">key</label>
<input id="octo-apik" size="20" />
</div>

View file

@ -396,7 +396,7 @@ self.lang['en-us'] = {
fi_over_l: ["overlap between shells and solid or sparse infill. can improve bonding to shells","as fraction of line width","0.0 - 1.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_s: "expand",
fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"],
// FDM FILL
@ -891,8 +891,10 @@ 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: "area filter",
ad_msol_s: "min area",
ad_msol_l: ["minimum area (mm^2)","bridges and flats less than this are culled"],
ad_mthk_s: "min thick",
ad_mthk_l: ["minimum area thickness","which is a ratio of area to perimeter","for layer differences which are used","to find bridges and flat areas","values from 0.0 - 0.5 work best"],
ad_mins_s: "min speed",
ad_mins_l: ["minimum speed","for short segments"],
ad_maxf_s: "max flowrate",