/** Copyright Stewart Allen -- All Rights Reserved */ import { base } from '../../../geo/base.js'; import { Packer } from '../../app/pack.js'; import { newBounds } from '../../../geo/bounds.js'; import { newPoint } from '../../../geo/point.js'; import { newPrint } from '../../core/print.js'; import { newSlopeFromAngle } from '../../../geo/slope.js'; 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 { encode as encodeOBJ } from '../../../load/obj.js'; export const TYPE = { LASER: 0, DRAG: 1, WJET: 2, WEDM: 3 }; function init(worker) { // console.log({ LASER_INIT: worker }); } function polyLabel(poly, label) { console.log('label', label); } function sliceEmitObjects(print, slice, groups, opt = { }) { let process = print.settings.process, { ctOutMark, ctOutStack, ctOutGroup, ctOutShaper, outputLaserLabel } = process, grouped = ctOutMark || ctOutGroup, label = false && outputLaserLabel, simple = opt.simple || false, emit = { in: [], out: [], mark: [] }, lastEmit = opt.lastEmit, zcolor = print.settings.process.ctOutZColor; function polyOut(poly, group, type, indexed) { if (!poly) { return; } if (Array.isArray(poly)) { poly.forEach((pi, index) => { polyOut(pi, group, type, indexed ? index : undefined); }); } else { let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1, rate: 1}; if (type === "mark") { pathOpt.rate = 0.001; pathOpt.extrude = 2; } else if (label && type === "out") { let lbl = indexed !== undefined ? `${slice.index}-${indexed}` : `${slice.index}`; polyLabel(poly, lbl); } if (simple) { pathOpt.simple = true; } if (poly.open) { pathOpt.open = true; } emit[type].push(poly); print.PPP(poly, group, pathOpt); // when stacking (top down widget slices), if the last layer is fully contained // by the current layer, then it is "marked" onto the current layer in a different color if (ctOutMark && type === "out" && lastEmit) { for (let out of lastEmit.out) { if (out.isInside(poly)) { polyOut(out.clone().setZ(poly.getZ()), group, "mark"); } } } } } if (grouped) { let group = []; let outer = slice.offset; let inner = outer.map(poly => poly.inner || []).flat(); // preserve slice thickness in group group.thick = slice.thick; // cut inside before outside polyOut(inner, group, "in", outer.length > 1); polyOut(outer, group, "out", outer.length > 1); groups.push(group); } else { for (let top of slice.offset) { let group = []; group.thick = slice.thick; polyOut([ top ], group, "in"); polyOut(top.inner || [], group, "out"); groups.push(group); } } return emit; }; // convert arc into line segments function arc(center, rad, s1, s2, out) { let a1 = s1.angle; let step = 5; let diff = s1.angleDiff(s2, true); let ticks = Math.abs(Math.floor(diff / step)); let dir = Math.sign(diff); let off = (diff % step) / 2; if (off == 0) { ticks++; } else { out.push( center.projectOnSlope(s1, rad) ); } while (ticks-- > 0) { out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) ); a1 += step * dir; } out.push( center.projectOnSlope(s2, rad) ); } // start to the "left" of the first point function addKnifeRadii(poly, tipoff) { poly.setClockwise(); let oldpts = poly.points.slice(); // find leftpoint and make that the first point let start = oldpts[0]; let startI = 0; let inner = tipoff < 0; tipoff = Math.abs(tipoff); for (let i=1; i start.x || (pt.x == start.x && pt.y < start.y))) { start = pt; startI = i; } else if (!inner && (pt.x < start.x || (pt.x == start.x && pt.y > start.y))) { start = pt; startI = i; } } if (startI > 0) { oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); } let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0}); let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); let newpts = [ lastpt, lastpt = oldpts[0].clone() ]; let tmp; for (let i=1; i= 10) { if (lastpt.distTo2D(nextpt) >= tipoff) { arc(lastpt, tipoff, lastsl, nextsl, newpts); } else { // todo handle short segments // newpts.push(lastpt.projectOnSlope(lastsl, tipoff) ); // newpts.push( lastpt.projectOnSlope(nextsl, tipoff) ); } } newpts.push(nextpt); lastsl = nextsl; lastpt = nextpt; } newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) ); // newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) ); poly.open = true; poly.points = newpts; } /** * DRIVER SLICE CONTRACT * * @param {Object} settings * @param {Widget} Widget * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ function laser_slice(settings, widget, onupdate, ondone) { let proc = settings.process; let offset = proc.ctSliceKerf || 0; let color = settings.controller.dark ? 0xbbbbbb : 0; if (proc.ctSliceHeight < 0) { return ondone("invalid slice height"); } let { ctSliceSingle, ctSliceHeight, ctSliceHeightMin, ctOmitInner } = proc; let bounds = widget.getBoundingBox(); let points = widget.getPoints(); let indices = []; slicer.slice(points, { zMin: bounds.min.z, zMax: bounds.max.z, zGen(zopt) { let { zMin, zMax, zIndexes } = zopt; if (ctSliceSingle) { indices = [ ctSliceHeight ]; } else if (ctSliceHeight) { for (let z = zMin + ctSliceHeight / 2; z < zMax; z += ctSliceHeight) { indices.push(z); } indices; } else { for (let i = 1; i < zIndexes.length; i++) { indices.push((zIndexes[i-1] + zIndexes[i]) / 2); } // discard layers too if (ctSliceHeightMin) { let last; indices = indices.filter(v => { let ok = true; if (last !== undefined && Math.abs(v - last) < ctSliceHeightMin) { ok = false; } else { last = v; } return ok; }); } } return indices; }, onupdate(v) { onupdate(v); } }).then(output => { // ensure slice order is high Z to low Z for stacking 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); } let slice = newSlice(z).addTops(tops); slice.index = indices.indexOf(z); // create top offsets (even if 0) let offsets = slice.offset = offset ? POLY.offset(tops, offset, {z: slice.z, miter: 2 / offset}) : tops; slice.output().setLayer("object", { line: 0x888800 }).addPolys(tops); slice.output().setLayer("cut", { line: color }).addPolys(offsets); return slice; }).sort((a,b) => b.z - a.z); ondone(); }); }; /** * DRIVER PRINT CONTRACT * * @param {Object} print state object * @param {Function} update incremental callback */ async function laser_prepare(widgets, settings, update) { let worker = self.kiri_worker.current, device = settings.device, process = settings.process, print = worker.print = newPrint(settings, widgets), isWire = worker.mode === 'WEDM', isKnife = worker.mode === 'DRAG', isLaser = worker.mode === 'LASER', output = print.output = [], totalSlices = 0, slices = 0, maxZ = 0, { bedWidth, bedDepth } = device, { ctOutLayer, ctOutTileSpacing, ctSliceSingle } = process, { ctOriginCenter, ctOriginBounds } = process, { ctOriginOffX, ctOriginOffY } = process, { ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process; ctOutStack = ctOutStack && isLaser; print.widgets = widgets; // shim to adapt older code -- should be refactored // let pppo = []; let pppg = []; let tile = []; let tiles = []; print.PPP = function(poly, group, options) { if (!pppg.contains(group)){ pppg.push(group); tiles.push(tile = []); tile.num = tiles.length; } // pppo.push({ poly, group, options }); tile.push({ poly, group, options }); }; // filter ignored widgets widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); // find max layers (for updates) and Z (for shaper) for (let widget of widgets) { totalSlices += widget.slices.length; maxZ = Math.max(maxZ, widget.bounds.max.z); } // emit objects from each slice into output array let layers = []; for (let widget of widgets) { if (ctOutStack) { // 3d stack output, no merging or layout for (let slice of widget.slices) { let group = []; let polys = []; for (let poly of slice.offset) { if (poly.inner) { polys.push(...poly.inner); } polys.push(poly); } for (let poly of polys) { print.PPP(poly, group, { extrude: 1, rate: 1, }); } } } else if (ctOutMerged) { // slice stack merging, no layout // there are no inner vs outer polys // merged polys "increase" their color / weight let merged = []; for (let slice of widget.slices) { let polys = []; slice.offset.clone(true).forEach(p => p.flattenTo(polys)); for (let poly of polys) { let match = false; for (let i=0; i { addKnifeRadii(ip, -ctOutKnifeTip); }); } } } let lastEmit; let slices = ctOutStack ? widget.slices.reverse() : widget.slices; for (let slice of slices) { lastEmit = sliceEmitObjects(print, slice, layers, {simple: isKnife, lastEmit}); update((slices++ / totalSlices) * 0.5, "prepare"); } } } // for tile layout packing let dw = device.bedWidth / 2, dh = device.bedDepth / 2, sort = !ctOutLayer, spacing = ctOutTileSpacing; // compute tile bounds for (let tile of tiles) { let bounds = newBounds(); tile.forEach(rec => bounds.merge(rec.poly.bounds)); tile.w = bounds.width(); tile.h = bounds.height(); tile.bounds = bounds; if (ctSliceSingle && isKnife) { for (let { poly } of tile) { poly.setZ(0); } } } // pack tiles let tp = new Packer(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { return packer.rescale(1.1, 1.1); }); // reposition tiles into their packed locations (unless 3d stack) if (!(ctOutStack || ctSliceSingle)) for (let tile of tiles) { let { fit, bounds } = tile; for (let { poly } of tile) { poly.setZ(0); poly.move({ x: fit.x - (tp.max.w / 2) - bounds.minx, y: fit.y - (tp.max.h / 2) - bounds.miny, z: 0}, true); } tile.bounds = newBounds(); for (let { poly } of tile) { tile.bounds.merge(poly.bounds); } } // set starting point based on origin type let currentPos = ctOriginCenter ? newPoint(0,0,0) : ctOriginBounds ? newPoint( - tp.max.w/2 - (ctOriginOffX || 0), - tp.max.h/2 - (ctOriginOffY || 0), 0 ) : newPoint(-dw, -dh, 0); // output tiles starting with closest to origin let remain = tiles.slice(); while (remain.length) { let closest = ctOutStack ? { dist: 0, tile: remain[0] } : { dist: Infinity }; for (let tile of remain) { for (let { poly } of tile) { for (let p of poly.points) { let pd = currentPos.distTo2D(p); if (pd < closest.dist) { closest.dist = pd; closest.tile = tile; } } } } if (!closest.tile) throw "no closest found"; for (let { poly, group, options } of closest.tile) { currentPos = print.polyPrintPath(poly, currentPos, group, options); if (!output.contains(group)) { output.push(group); } } remain.remove(closest.tile); } // clone output points to prevent double offsetting during export for (let layer of output) { for (let rec of layer) { rec.point = rec.point.clone(); } } return render.path(output, (progress, layer) => { if (ctOutShaper) { for (let layer of output) { for (let out of layer) { out.point.zi = maxZ - out.point.z; } } } update(0.5 + progress * 0.5, "render", layer); }, { thin: true, z: ctOutStack ? undefined : 0, action: "cut", moves: process.isKnife }); }; function laser_export(print, online, ondone) { ondone(print.output); } /** * */ function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlayer) { let { process, device } = settings, { bedWidth, bedDepth } = device, { ctOriginCenter, ctOriginBounds } = process, { outputInvertX, outputInvertY } = process, { ctOriginOffX, ctOriginOffY } = process, { ctOutPower, ctOutSpeed, maxZ } = process, last, point, poly = [], off = ctOriginBounds ? {x:ctOriginOffX||0, y:ctOriginOffY||0} : {x:0, y:0}, min = {x:0, y:0}, max = {x:0, y:0}, size = {w:0, h:0}; // compute bounds for entire output output.forEach(layer => { layer.forEach(out => { point = out.point; if (outputInvertX) point.x = -point.x; if (outputInvertY) point.y = -point.y; min.x = Math.min(min.x, point.x); max.x = Math.max(max.x, point.x); min.y = Math.min(min.y, point.y); max.y = Math.max(max.y, point.y); }); }); size.w = max.x - min.x; size.h = max.y - min.y; let bounds = newBounds(); if (ctOriginCenter) { // place origin at geometric center of all points // regardless of workspace size for (let layer of output) { for (let out of layer) { point = out.point; point.x = point.x - min.x - size.w / 2 + off.x; point.y = point.y - min.y - size.h / 2 + off.y; bounds.update(point); } } max.x = max.x - min.x + off.x; max.y = max.y - min.y + off.y; min.x = 0; min.y = 0; } else if (ctOriginBounds) { // place origin at min x,y of all points for (let layer of output) { for (let out of layer) { point = out.point; point.x = point.x - min.x + off.x; point.y = point.y - min.y + off.y; bounds.update(point); } } max.x = max.x - min.x + off.x; max.y = max.y - min.y + off.y; min.x = 0; min.y = 0; } else { // place origin at min x,y taking into account // each layer's relative position let w = bedWidth; let h = bedDepth; for (let layer of output) { for (let out of layer) { point = out.point; point.x += w / 2 + off.x; point.y += h / 2 + off.y; bounds.update(point); } } max.x = w + off.x; max.y = h + off.y; min.x = 0; min.y = 0; } onpre(min, max, ctOutPower, ctOutSpeed); // output each layer with moves between them output.forEach((layer, index) => { let thick = 0; let color = 0; layer.forEach((out, li) => { thick = out.thick; point = out.point; if (onlayer) { onlayer(index, point.z, out.thick, output.length); } if (out.emit) { color = out.emit; if (last && poly.length === 0) { poly.push(onpoint(last)); } poly.push(onpoint(point)); } else if (poly.length > 0) { onpoly(poly, color, thick); poly = []; } last = point; }); if (poly.length > 0) { onpoly(poly, color, thick); poly = []; } }); onpost(); }; function exportOBJ(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 obj = []; 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; let layer = []; polys = POLY.nest(polys); for (let poly of polys) { let ext = poly.setZ(0).extrude(ctSliceHeight, { zadd: parseFloat(z) }); layer = layer.concat(ext); } obj.push({ file:`z-${z}`, varr: layer }); } return encodeOBJ(obj); } /** * */ function exportGCode(settings, data) { let lines = [], dx = 0, dy = 0, z = 0, feedrate; let dev = settings.device; let proc = settings.process; let space = dev.gcodeSpace ? ' ' : ''; let max_power = dev.laserMaxPower || 255; let power = max_power; let cut_on = dev.gcodeLaserOn || dev.gcodeWaterOn || dev.gcodeKnifeDn || []; let cut_off = dev.gcodeLaserOff || dev.gcodeWaterOff || dev.gcodeKnifeUp || []; let isKnife = proc.isKnife; let knifeDepth = proc.ctOutKnifeDepth; let passes = isKnife ? proc.ctOutKnifePasses : 1; exportElements( settings, data, function(min, max, pct, speed) { let width = (max.x - min.x), height = (max.y - min.y); dx = min.x; dy = min.y; feedrate = `${space}F${speed}`; power = (max_power * (pct / 100)).toFixed(3); (dev.gcodePre || []).forEach(line => { lines.push(line); }); }, function(poly, color, thick) { if (isKnife) { lines.push(`; start new poly id=${poly.id} len=${poly.length}`); } for (let i=1; i { line = line.replace('{power}', power); line = line.replace('{color}', color); line = line.replace('{thick}', thick); line = line.replace('{z}', z); lines.push(line); }); lines.push(`G1${space}${point}${feedrate}`); } else { lines.push(`G1${space}${point}`); } }); cut_off.forEach(line => { lines.push(line); }); } if (isKnife) { lines.appendAll([`G0${space}Z5`]); } }, function() { (dev.gcodePost || []).forEach(line => { lines.push(line); }); }, function(point) { z = point.z; return `X${(point.x - dx).toFixed(3)}${space}Y${(point.y - dy).toFixed(3)}`; }, function(layer, z, thick, layers) { // ununsed } ); return lines.join('\n'); }; /** * */ function exportSVG(settings, data, cut_color) { let { process } = settings; let { ctOutInches, ctOutStack, ctOutShaper } = process; let swidth = "0.1mm"; let zcolor = process.ctOutZColor ? 1 : 0; let lines = [], dx = 0, dy = 0, my, z = 0; let colors = [ "black", "purple", "blue", "red", "orange", "yellow", "green", "brown", "gray" ]; exportElements( settings, data, function(min, max) { let width = (max.x - min.x), height = (max.y - min.y); dx = min.x; dy = min.y; my = max.y; lines.push(''); lines.push(''); let svg = [''); }, function(poly, color, thick) { let cout = zcolor || colors[((color-1) % colors.length)]; if (ctOutStack) { let def = ["path"]; if (z !== undefined && ctOutShaper) { if (ctOutInches) { def.push(`shaper:cutDepth="${(z / 25.4).toFixed(2)}in"`); } else { def.push(`shaper:cutDepth="${(z).toFixed(2)}mm"`); } def.push(`shaper:cutType="online"`); // def.push(`shaper:cutOffset="0in"`); // def.push(`shaper:toolDia="0in"`); } else if (z !== undefined) { def.push(`z="${z}"`); } let path = poly.map((xy, i) => i > 0 ? `L${xy}` : `M${xy}`); let std = ctOutShaper ? '' : `fill="none" stroke="${cout}" stroke-width="${swidth}" `; lines.push(`<${def.join(' ')} d="${path.join(' ')}" ${std}/>`); } else { let def = ["polyline"]; if (z !== undefined) def.push(`z="${z}"`); if (thick !== undefined) def.push(`h="${thick}"`); lines.push(`<${def.join(' ')} points="${poly.join(' ')}" fill="none" stroke="${cout}" stroke-width="${swidth}" />`); } }, function() { lines.push(""); }, function(point) { z = ctOutStack ? (point.zi ?? point.z) : undefined; let px = point.x - dx; let py = my - point.y - dy; // convert to imperial for stacked if (ctOutInches) { px /= 25.4; py /= 25.4; } return base.util.round(px, 3) + "," + base.util.round(py, 3); }, function(layer, z, thick, layers) { if (zcolor) { zcolor = Math.round(((layer + 1) / layers) * 0xffffff).toString(16); zcolor = `#${zcolor.padStart(6,0)}`; } } ); return lines.join('\n'); }; /** * */ function exportDXF(settings, data) { let lines = []; exportElements( settings, data, function(min, max) { lines.appendAll([ " 0", "SECTION", " 2", "HEADER", " 0", "ENDSEC", " 0", "SECTION", " 2", "TABLES", " 0", "ENDSEC", " 0", "SECTION", " 2", "BLOCKS", " 0", "ENDSEC", " 0", "SECTION", " 2", "ENTITIES", ]); }, function(poly) { lines.appendAll([ " 0", "POLYLINE", " 8", "0", " 66", "1", " 70", "1", ]); poly.forEach(point => { lines.appendAll([ ' 0', 'VERTEX', ' 8', '0', ' 10', point.x, ' 20', point.y ]); }); lines.appendAll([ ' 0', 'SEQEND', ]); }, function() { lines.appendAll([ ' 0', 'ENDSEC', ' 0', 'SECTION', ' 2', 'OBJECTS', ' 0', 'ENDSEC', ' 0', 'EOF', ]); }, function(point) { return {x:point.x,y:point.y}; } ); return lines.join('\n'); }; export const LASER = { TYPE, type: TYPE.LASER, name: 'Laser', init, slice: laser_slice, prepare: laser_prepare, export: laser_export, exportGCode, exportDXF, exportSVG, exportOBJ };