From 84d6cdcf2cca6c85934853375c131af25b4001de Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 15 Dec 2025 21:06:19 -0500 Subject: [PATCH] add laser stacking stl reconstruction option --- src/geo/polygon.js | 1 + src/kiri/core/conf.js | 6 +++ src/kiri/core/export.js | 14 +++++++ src/kiri/core/widget.js | 6 +-- src/kiri/mode/laser/driver.js | 70 +++++++++++++++++++++++++++++++- src/kiri/mode/laser/init-menu.js | 6 ++- web/kiri/index.html | 1 + 7 files changed, 98 insertions(+), 6 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 1bb4bbbe..1349371f 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -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 diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index 162b23d7..19fa135f 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -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, diff --git a/src/kiri/core/export.js b/src/kiri/core/export.js index dbabe999..28ba7927 100644 --- a/src/kiri/core/export.js +++ b/src/kiri/core/export.js @@ -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; } diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 29886e18..3b225a52 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -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 = []; diff --git a/src/kiri/mode/laser/driver.js b/src/kiri/mode/laser/driver.js index 8dfbb45c..c07e4007 100644 --- a/src/kiri/mode/laser/driver.js +++ b/src/kiri/mode/laser/driver.js @@ -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 }; diff --git a/src/kiri/mode/laser/init-menu.js b/src/kiri/mode/laser/init-menu.js index 14d40eee..824fe6aa 100644 --- a/src/kiri/mode/laser/init-menu.js +++ b/src/kiri/mode/laser/init-menu.js @@ -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:''})), @@ -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 }), }; diff --git a/web/kiri/index.html b/web/kiri/index.html index df7431f9..91e37723 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -691,6 +691,7 @@
+