clean up and document fdm slicing

This commit is contained in:
Stewart Allen 2026-01-06 14:47:14 -05:00
commit 97fbcb109e
3 changed files with 928 additions and 465 deletions

309
src/kiri/mode/fdm/belt.js Normal file
View file

@ -0,0 +1,309 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Belt printer specific functions for FDM slicing
* Handles anchor generation, text embossing, and belt-specific bounds
*/
import { newPolygon } from '../../../geo/polygon.js';
import { newBounds } from '../../../geo/bounds.js';
import { newSlice } from '../../core/slice.js';
import { getRangeParameters } from './driver.js';
// Import constants from slice.js
// These would ideally be shared, but for now we'll define locally
const BELT = {
PRECISION_ROUNDING: 3,
PEEK_LAYERS: 5,
ANCHOR_BUMP_SPACING_MULT: 2,
MIN_WIDTH_FOR_HATCH: 10,
HATCH_SPACING: 3,
POOCH_Y_RANGE_MIN: 0,
POOCH_Y_RANGE_MAX: 3,
POOCH_FLATNESS_TOLERANCE: 0.01,
POOCH_WIDTH_TOLERANCE: 1,
POOCH_DY_TOLERANCE: 0.1,
POOCH_DZ_TOLERANCE: 1,
POOCH_TEXT_SCALE_X: 1.2,
POOCH_TEXT_SCALE_Y: 1,
POOCH_FONT_SIZE: 24,
POOCH_PIXEL_THRESHOLD: 30,
};
/**
* Generate belt anchor for lead-in adhesion
*
* Creates a lead-in anchor section before the actual part begins printing.
* The anchor helps adhesion on belt printers by providing a larger contact area.
* Optionally adds "bumps" to the anchor for improved adhesion.
*
* @param {Object} options - Configuration options
* @param {Array} options.slices - Array of slices to process
* @param {Object} options.widget - Widget being sliced
* @param {Object} options.process - Process parameters
* @param {number} options.lineWidth - Extrusion width in mm
* @param {number} options.sliceHeight - Layer height in mm
* @param {number} options.sliceHeightBase - First layer height in mm
* @param {number} options.extruder - Extruder index
* @returns {void} Modifies slices array in place
*/
export function generateBeltAnchor({ slices, widget, process, lineWidth, sliceHeight, sliceHeightBase, extruder }) {
let { cosf, slope } = widget.belt;
// find adjusted zero point from slices
let smin = Infinity;
for (let slice of slices) {
let miny = Infinity;
for (let poly of slice.topPolys()) {
let y = poly.bounds.maxy;
let z = slice.z;
// at 45 degrees, 1mm in Z is 1mm in Y
let by = (slope * z) - y;
if (by < miny) miny = by;
if (by < smin) smin = by;
}
// mark slices with tops touching belt
slice.belt = { miny, touch: miny.round(BELT.PRECISION_ROUNDING) < sliceHeightBase };
}
// find max width of first N layers for brim additions
let start;
let minx = Infinity, maxx = -Infinity;
let peek = 0;
for (let slice of slices) {
if (slice.tops.length && peek++ < BELT.PEEK_LAYERS) {
for (let poly of slice.topPolys()) {
minx = Math.min(minx, poly.bounds.minx);
maxx = Math.max(maxx, poly.bounds.maxx);
}
}
// find first slice touching belt for start of anchor
if (!start && slice.belt.touch) {
start = slice;
}
}
// ensure we start against a layer with shells
while (start && start.up && start.topShells().length === 0) {
start = start.up;
}
// if a brim applies, add that width to anchor
let brim = getRangeParameters(process, 0).firstLayerBrim || 0;
if (brim) {
minx -= brim;
maxx += brim;
}
// array of added top.fill_sparse arrays
let adds = [];
let step = sliceHeight;
let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf;
while (anchorlen && start && anchorlen >= sliceHeight) {
let addto = start.down;
if (!addto) {
addto = newSlice(start.z - step);
addto.extruder = extruder;
addto.belt = { };
addto.height = start.height;
addto.up = start;
start.down = addto;
slices.splice(0,0,addto);
} else if (!addto.belt) {
console.log({addto_missing_belt: addto});
addto.belt = {};
}
addto.index = -1;
addto.belt.anchor = true;
// this allows the anchor to print bi-directionally
// by removing the forced start-point in print.js
addto.belt.touch = false;
let z = addto.z;
let y = (slope * z) - smin - (lineWidth / 2);
let splat = newPolygon().add(minx, y, z).add(maxx, y, z).setOpen();
let snew = addto.addTop(splat).fill_sparse = [ splat ];
adds.push(snew);
start = addto;
anchorlen -= (step * slope);
}
// add anchor bump
let bump = process.firstLayerBeltBump;
if (bump) {
adds = adds.reverse().slice(1, adds.length - 1);
let count = 1;
for (let add of adds) {
let poly = add[0];
let y = count++ * -start.height * BELT.ANCHOR_BUMP_SPACING_MULT;
if (-y > bump) {
count--;
}
let first = poly.first();
// add up/over/down to anchor line (close = down)
// which completes the bump perimeter
poly.push(poly.last().add({x:0, y, z:0}));
poly.push(poly.first().add({x:0, y, z:0}));
poly.setClosed();
if (count > 2 && maxx - minx > BELT.MIN_WIDTH_FOR_HATCH) {
// add vertical hatch lines inside bump shell
let mp = (maxx + minx) / 2;
let dx = (maxx - minx - 2);
dx = (Math.floor(dx / BELT.HATCH_SPACING) * BELT.HATCH_SPACING) / 2;
let fy = first.y;
let fz = first.z;
let n2 = lineWidth / 2;
for (let x = mp - dx; x <= mp + dx ; x += BELT.HATCH_SPACING) {
add.push(newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() );
}
}
}
}
}
/**
* Experimental: Emboss text on flat underside of belt-printed object
*
* Scans for flat underside surfaces and embosses specified text using
* canvas rendering and pixel detection. The text is converted to support
* patterns that create raised or recessed text on the bottom surface.
*
* @param {Object} options - Configuration options
* @param {Array} options.slices - Array of slices to process
* @param {Object} options.widget - Widget being sliced
* @param {Object} options.process - Process parameters
* @param {number} options.lineWidth - Extrusion width in mm
* @param {number} options.smin - Minimum belt Y offset
* @returns {void} Modifies slice.supports arrays
*/
export function embossBeltPooch({ slices, widget, process, lineWidth, smin }) {
if (!process.pooch || !self.OffscreenCanvas) {
return;
}
const { length, width, height, text } = process.pooch;
const { slope } = widget.belt;
let firstZ, firstI, lastZ, lastI, minX = 0, maxX = 0, maxY = 0;
// locate suitable flat spot
for (let slice of slices) {
let { belt } = slice;
if (!belt.touch) {
continue;
}
let index = slice.index;
let z = slice.z;
outer: for (let poly of slice.topPolys()) {
for (let i=0, p=poly.points, l=p.length; i<l; i++) {
let p0 = p[i];
let p1 = p[(i + 1) % l];
let p0y = z - p0.y;
let p1y = z - p1.y;
let i_ok = lastI ? index - lastI === 1 : true
let y_ok = p0y > BELT.POOCH_Y_RANGE_MIN && p0y < BELT.POOCH_Y_RANGE_MAX &&
Math.abs(p0y - p1y) < BELT.POOCH_FLATNESS_TOLERANCE;
let x_ok = Math.abs(width - Math.abs(p1.x - p0.x)) < BELT.POOCH_WIDTH_TOLERANCE
if (y_ok && x_ok) {
if (i_ok) {
firstZ = firstZ || z;
firstI = firstI || index;
lastZ = z;
lastI = index;
minX = Math.min(p0.x, p1.x);
maxX = Math.max(p0.x, p1.x);
maxY = p0y;
break outer;
} else {
firstZ = lastI = 0;
}
}
}
}
}
let dy = Math.abs(height - maxY * (1 / Math.sqrt(2)));
let dz = Math.abs(length - ((lastZ - firstZ) * Math.sqrt(2)));
if (dy < BELT.POOCH_DY_TOLERANCE && dz < BELT.POOCH_DZ_TOLERANCE) {
// console.log('FOUND', { firstI, lastI, minX, maxX, maxY });
let span = lastI - firstI - 2; // x = down the belt
let tall = width * 2;
let can = new self.OffscreenCanvas(span, tall);
let ctx = can.getContext("2d");
ctx.scale(BELT.POOCH_TEXT_SCALE_X, BELT.POOCH_TEXT_SCALE_Y);
ctx.font = `${BELT.POOCH_FONT_SIZE}px sans-serif`;
ctx.textBaseline = "bottom";
ctx.fillText(text, 1, tall - 1);
let img = ctx.getImageData(0, 0, span, tall).data.buffer;
let rgb = new Uint32Array(img);
for (let x=0; x<span; x++) {
let str = '';
let maxp = 0;
let lines = [];
let start, end;
for (let y=tall-1; y>=0; y--) {
let pix = rgb[y * span + x];
pix = (
((pix >> 24) & 0xff) +
((pix >> 16) & 0xff) +
((pix >> 8) & 0xff)
) / 3;
str += pix > BELT.POOCH_PIXEL_THRESHOLD ? '*' : '-';
maxp = Math.max(maxp, pix);
if (pix > BELT.POOCH_PIXEL_THRESHOLD) {
if (start >= 0) {
end = tall - y;
} else {
start = tall - y;
}
} else {
if (start >= 0 && end > start) {
lines.push({ start, end });
}
start = end = undefined;
}
}
console.log((x).toString().padStart(2,0),str,maxp | 0,lines);
if (lines.length) {
let slice = slices[firstI + x + 1];
let supps = slice.supports = slice.supports || [];
let z = slice.z;
let y = z - smin - (lineWidth / 2);
for (let line of lines) {
supps.push(newPolygon()
.add(minX + line.start / 2, y, z)
.add(minX + line.end / 2, y, z)
.setOpen()
);
}
}
}
}
}
/**
* Finalize belt-specific bounds for widget tracking
*
* Calculates final Y-axis bounds for belt-printed parts,
* used for positioning and collision detection.
*
* @param {Object} options - Configuration options
* @param {Array} options.slices - Array of processed slices
* @param {Object} options.widget - Widget being sliced
* @returns {void} Modifies widget.belt properties
*/
export function finalizeBeltBounds({ slices, widget }) {
let bounds = newBounds();
let slice = slices[0];
if (slice.tops) {
for (let top of slice.tops) {
bounds.merge(top.poly.bounds);
}
} else if (slice.supports) {
for (let poly of slice.supports) {
bounds.merge(poly);
}
}
widget.belt.miny = -bounds.miny;
widget.belt.midy = (bounds.miny + bounds.maxy) / 2;
}

View file

@ -1,7 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js';
import { fdm_slice, slicePost } from './slice.js';
import { sliceOne, slicePost } from './slice.js';
import { fdm_prepare } from './prepare.js';
import { fdm_export } from './export.js';
import { codec } from '../../core/codec.js';
@ -41,7 +41,7 @@ export const FDM = {
extrudePerMM,
extrudeMM,
slicePre: undefined,
slice: fdm_slice,
slice: sliceOne,
slicePost: slicePost,
prepare: fdm_prepare,
export: fdm_export

File diff suppressed because it is too large Load diff