add laser stacking stl reconstruction option

This commit is contained in:
Stewart Allen 2025-12-15 21:06:19 -05:00
commit 84d6cdcf2c
7 changed files with 98 additions and 6 deletions

View file

@ -2403,6 +2403,7 @@ export class Polygon {
}
// extrude poly (with inner voids) into 3d mesh
// todo: zadd broken when poly Z is not 0
extrude(z = 1, opt = {}) {
let earcut = this.earcut(); // array of 3-point polygons

View file

@ -647,7 +647,11 @@ export const conf = {
ctSliceHeight: 1,
ctSliceHeightMin: 0,
ctSliceSingle: true,
ctOmitInner: false,
ctOutTileSpacing: 1,
ctOutClean: false,
ctOutFilter: 1,
ctOutSmooth: 0.1,
ctOutPower: 100,
ctOutSpeed: 1000,
ctOutGroup: true,
@ -690,6 +694,7 @@ export const conf = {
ctSliceHeight: 1,
ctSliceHeightMin: 0,
ctSliceSingle: true,
ctOmitInner: false,
ctOutTileSpacing: 1,
ctOutPower: 100,
ctOutSpeed: 1000,
@ -734,6 +739,7 @@ export const conf = {
ctSliceHeight: 1,
ctSliceHeightMin: 0,
ctSliceSingle: true,
ctOmitInner: false,
ctOutTileSpacing: 1,
ctOutPower: 100,
ctOutSpeed: 1000,

View file

@ -99,6 +99,12 @@ function exportLaserDialog(data, names) {
const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`;
const settings = api.conf.get();
const driver = laser_driver;
const { process } = settings;
if (process.ctOutStack) {
$('print-stl').classList.remove('hide');
} else {
$('print-stl').classList.add('hide');
}
function download_svg() {
api.util.download(
@ -114,6 +120,13 @@ function exportLaserDialog(data, names) {
);
}
function download_stl() {
api.util.download(
driver.exportSTL(settings, data),
$('print-filename-laser').value + ".stl"
);
}
function download_gcode() {
api.util.download(
driver.exportGCode(settings, data),
@ -130,6 +143,7 @@ function exportLaserDialog(data, names) {
$('print-lines').value = util.comma(segments);
$('print-svg').onclick = download_svg;
$('print-dxf').onclick = download_dxf;
$('print-stl').onclick = download_stl;
$('print-lg').onclick = download_gcode;
}

View file

@ -872,8 +872,9 @@ class Widget {
return shadows[z] = POLY.setZ(shadow, z);
}
// create a stack of faces in 1mm increments
// the stacks are then used to produce shadowlines
#ensureShadowCache() {
// cache faces with normals up
if (this.cache.shadow) {
return;
}
@ -881,7 +882,6 @@ class Widget {
const length = geo.length;
const bounds = this.getBoundingBox();
const stack = {};
// const faces = [];
for (let i = Math.floor(bounds.min.z); i <= Math.ceil(bounds.max.z); i++) {
stack[i] = [];
}
@ -892,7 +892,6 @@ class Widget {
const n = THREE.computeFaceNormal(a, b, c);
if (n.z < 0.001) {
continue;
// faces.push(a, b, c);
}
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
const maxZ = Math.ceil(Math.max(a.z, b.z, c.z));
@ -904,6 +903,7 @@ class Widget {
}
// union triangles > z (opt cap < ztop) into polygon(s)
// slice the triangle stack matchingg z then union the results
#computeShadowAt(z, ztop) {
this.#ensureShadowCache();
const found = [];

View file

@ -10,6 +10,8 @@ import { polygons as POLY } from '../../../geo/polygons.js';
import { render } from '../../core/render.js';
import { slicer } from '../../../geo/slicer.js';
import { newSlice } from '../../core/slice.js';
import { newPolygon } from '../../../geo/polygon.js';
import { STL } from '../../../load/stl.js';
export const TYPE = {
LASER: 0,
@ -228,9 +230,28 @@ function laser_slice(settings, widget, onupdate, ondone) {
widget.slices = output.slices.map(data => {
let { z, lines, groups } = data;
let tops = POLY.nest(groups);
let { ctOutClean, ctOutFilter, ctOutSmooth } = proc;
// optional cleaning, smoothing, filtering steps
// if we're going to re-export an stl
if (ctOutClean) {
tops.map(top => top.clean(true));
POLY.setZ(tops, z);
}
// running this here vs at output time seems to cause manifold issues ?!?
// if (ctOutSmooth) {
// console.log({ ctOutSmooth });
// tops = POLY.offset(tops, ctOutSmooth);
// tops = POLY.offset(tops, -ctOutSmooth);
// POLY.setZ(tops, z);
// }
if (ctOutFilter) {
tops = tops.filter(top => top.area() > ctOutFilter);
}
// for WireEDM, we commonly omit inner polys because we can't
// navigate to cut them separately unlike laser, water, drag
if (ctOmitInner) tops.forEach(top => delete top.inner);
if (ctOmitInner) {
tops.forEach(top => delete top.inner);
}
let slice = newSlice(z).addTops(tops);
slice.index = indices.indexOf(z);
// create top offsets (even if 0)
@ -269,6 +290,7 @@ async function laser_prepare(widgets, settings, update) {
{ ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process;
ctOutStack = ctOutStack && isLaser;
print.widgets = widgets;
// shim to adapt older code -- should be refactored
// let pppo = [];
@ -588,6 +610,49 @@ function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlaye
onpost();
};
function exportSTL(settings, data) {
let { ctSliceHeight, ctOutSmooth } = settings.process;
let polys = [];
for (let array of data) {
let poly;
for (let el of array) {
let { emit, point } = el;
if (emit === 0) {
poly = newPolygon().addObj(point);
polys.push(poly);
} else {
poly.addObj(point);
}
}
}
if (ctOutSmooth) {
polys = polys.map(poly => {
let pump = POLY.offset([ poly ], ctOutSmooth);
pump = POLY.offset(pump, -ctOutSmooth);
return POLY.setZ(pump, poly.getZ());
}).flat().filter(v => v);
}
let layers = {};
let mesh = [];
for (let poly of polys) {
let z = poly.getZ();
let layer = layers[z];
if (!layer) {
layer = layers[z] = [];
}
layer.push(poly);
}
for (let rec of Object.entries(layers)) {
let [ z, polys ] = rec;
polys = POLY.nest(polys);
for (let poly of polys) {
let ext = poly.setZ(0).extrude(ctSliceHeight, { zadd: parseFloat(z) });
mesh = mesh.concat(ext);
}
}
return new STL().encode(mesh);
}
/**
*
*/
@ -867,5 +932,6 @@ export const LASER = {
export: laser_export,
exportGCode,
exportDXF,
exportSVG
exportSVG,
exportSTL
};

View file

@ -35,7 +35,7 @@ export function menu() {
ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }),
separator: newBlank({ class:"set-sep", driven }),
ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}),
ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}),
ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l}),
_____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }),
ctSurfaces: newRow([
(ui.faceAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})),
@ -68,6 +68,10 @@ export function menu() {
ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }),
ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }),
ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }),
separator: newBlank({ class:"set-sep", driven, modes:LASER, show: () => ui.ctOutStack.checked }),
ctOutClean: newBoolean('clean', onBooleanClick, { title:'clean', modes:LASER, show:() => ui.ctOutStack.checked }),
ctOutFilter: newInput('filter', { title:'filter', modes:LASER, convert:toFloat, show:() => ui.ctOutStack.checked }),
ctOutSmooth: newInput('smooth', { title:'smooth', modes:LASER, convert:toFloat, show:() => ui.ctOutStack.checked }),
};

View file

@ -691,6 +691,7 @@
<div class="f-row box">
<button id="print-svg" class="grow">svg</button>
<button id="print-dxf" class="grow">dxf</button>
<button id="print-stl" class="grow">stl</button>
<button id="print-lg" class="grow">gcode</button>
</div>
</div>