diff --git a/notes.md b/notes.md index dd1079a8..0963e46d 100644 --- a/notes.md +++ b/notes.md @@ -1,9 +1,5 @@ # Kiri:Moto todo and notes -## 2.4 roadmap -* `laser` -* - drag knife support - ## 2.5 roadmap # `cnc` # - lathe, A-axis @@ -64,6 +60,7 @@ # CAM +* `B` starting export during animation unleashes chaos * `B` crossing interior open spaces doesn't lift high enough * https://forum.grid.space/t/g0-movement-hits-the-stock-using-custom-tabs-positioning/171/2 * `B` climb vs conventional not fully consistent after refactor diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 28a27a5c..b8dd2277 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -221,9 +221,10 @@ return ret; }; + // use Slope.angleDiff() then re-test path mitering / rendering function slopeDiff(s1, s2) { - const n1 = s1.angle;//(s1.angle + 360) % 360; - const n2 = s2.angle;//(s2.angle + 360) % 360; + const n1 = s1.angle; + const n2 = s2.angle; let diff = n2 - n1; while (diff < -180) diff += 360; while (diff > 180) diff -= 360; @@ -781,8 +782,6 @@ */ PRO.isNested = function(parent) { if (parent.bounds.contains(this.bounds)) { - //let int = POLY().rayIntersect(this.bounds.leftMost, nest_test_slope, [parent], false); - //return int.length % 2 === 1 || this.isInside(parent, CONF.precision_nested_sq); return this.isInside(parent, CONF.precision_nested_sq); } return false; @@ -1546,37 +1545,37 @@ * rotate such that first and last points are the points * furthest apart and lowest when there is a tie breaker */ - PRO.spread = function() { - let poly = this, - points = poly.points, - plen = points.length, - i, pp, np, p, mdelta, newmax, max, shift, maxd; - - max = {d:0}; - maxd = 0; - // find two most distance points - for (i=0; i maxd; - maxd = MAX(maxd,p); - // select lowest points (corner case = square) - if (newmax || (mdelta < 0.001 && max.p1 && MIN(pp.z,np.z) < MIN(max.p1.z, max.p2.z))) { - max = {p1:pp, p2:np, i:(i+1)%plen, d:p}; - } - } - - if (max.i > 0) { - // shift array to start at "leftmost" Point - shift = points.slice(max.i); - shift.appendAll(points.slice(0,max.i)); - return newPolygon(shift); - } - - return poly; - } + // PRO.spread = function() { + // let poly = this, + // points = poly.points, + // plen = points.length, + // i, pp, np, p, mdelta, newmax, max, shift, maxd; + // + // max = {d:0}; + // maxd = 0; + // // find two most distance points + // for (i=0; i maxd; + // maxd = MAX(maxd,p); + // // select lowest points (corner case = square) + // if (newmax || (mdelta < 0.001 && max.p1 && MIN(pp.z,np.z) < MIN(max.p1.z, max.p2.z))) { + // max = {p1:pp, p2:np, i:(i+1)%plen, d:p}; + // } + // } + // + // if (max.i > 0) { + // // shift array to start at "leftmost" Point + // shift = points.slice(max.i); + // shift.appendAll(points.slice(0,max.i)); + // return newPolygon(shift); + // } + // + // return poly; + // } /** ****************************************************************** * Connect to base and Helpers diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 993fe0b0..d1279995 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -462,7 +462,7 @@ * and return resulting gaps from offsets for thin wall detection in * in FDM mode and uncleared areas in CAM mode. */ - function offset(polys, dist, options) { + function offset(polys, dist, opts = {}) { // do not offset open lines polys = polys.filter(p => !p.open); @@ -475,7 +475,6 @@ }); let orig = polys, - opts = options || {}, count = numOrDefault(opts.count, 1), depth = numOrDefault(opts.depth, 0), clean = opts.clean !== false, @@ -483,7 +482,7 @@ fill = numOrDefault(opts.fill, ClipperLib.PolyFillType.pftNonZero), join = numOrDefault(opts.join, ClipperLib.JoinType.jtMiter), type = numOrDefault(opts.type, ClipperLib.EndType.etClosedPolygon), - coff = new ClipperLib.ClipperOffset(), + coff = new ClipperLib.ClipperOffset(opts.miter, opts.arc), ctre = new ClipperLib.PolyTree(), // if dist is array with values, shift out next offset offs = Array.isArray(dist) ? (dist.length > 1 ? dist.shift() : dist[0]) : dist, diff --git a/src/geo/slope.js b/src/geo/slope.js index 8f6ee07f..517008d9 100644 --- a/src/geo/slope.js +++ b/src/geo/slope.js @@ -95,6 +95,15 @@ return this; }; + PRO.angleDiff = function(s2,sign) { + const n1 = this.angle; + const n2 = s2.angle; + let diff = n2 - n1; + while (diff < -180) diff += 360; + while (diff > 180) diff -= 360; + return sign ? diff : Math.abs(diff); + }; + /** ****************************************************************** * Connect to base and Helpers ******************************************************************* */ diff --git a/src/kiri/conf.js b/src/kiri/conf.js index b2b691ad..07d7e698 100644 --- a/src/kiri/conf.js +++ b/src/kiri/conf.js @@ -497,7 +497,6 @@ laserSliceHeight: 1, laserSliceHeightMin: 0, laserSliceSingle: false, - laserKnife: false, outputTileSpacing: 1, outputLaserPower: 100, outputLaserSpeed: 1000, @@ -510,7 +509,8 @@ outputInvertY: false, outputKnifeDepth: 1, outputKnifePasses: 1, - outputTipOffset: 2 + outputKnifeTip: 2, + knifeOn: false } } }, diff --git a/src/kiri/init.js b/src/kiri/init.js index b77aa5a7..36d6dde2 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -1666,10 +1666,12 @@ outputLaserMerged: UC.newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:LASER}), outputLaserGroup: UC.newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, modes:LASER}), - knife: UC.newGroup(LANG.dk_menu, null, {modes:LASER, show:showLaserKnife }), - outputKnifeDepth: UC.newInput(LANG.dk_dpth_s, {title:LANG.ou_exml_l, convert:UC.toFloat, bound:UC.bound(0.0,5.0), modes:LASER, show:showLaserKnife}), - outputKnifePasses: UC.newInput(LANG.dk_pass_s, {title:LANG.dk_pass_l, convert:UC.toInt, bound:UC.bound(0,5), modes:LASER, show:showLaserKnife}), - outputTipOffset: UC.newInput(LANG.dk_offs_s, {title:LANG.dk_offs_l, convert:UC.toFloat, bound:UC.bound(0.0,10.0), modes:LASER, show:showLaserKnife}), + knife: UC.newGroup(LANG.dk_menu, null, {modes:LASER, marker:true}), + outputKnifeDepth: UC.newInput(LANG.dk_dpth_s, {title:LANG.dk_dpth_l, convert:UC.toFloat, bound:UC.bound(0.0,5.0), modes:LASER}), + outputKnifePasses: UC.newInput(LANG.dk_pass_s, {title:LANG.dk_pass_l, convert:UC.toInt, bound:UC.bound(0,5), modes:LASER}), + outputKnifeTip: UC.newInput(LANG.dk_offs_s, {title:LANG.dk_offs_l, convert:UC.toFloat, bound:UC.bound(0.0,10.0), modes:LASER}), + knifeSep: UC.newBlank({class:"pop-sep", modes:LASER}), + knifeOn: UC.newBoolean(LANG.enable, onBooleanClick, {title:LANG.ou_drkn_l, modes:LASER}), output: UC.newGroup(LANG.ou_menu, null, {modes:GCODE}), outputLaserPower: UC.newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:UC.toInt, bound:UC.bound(1,100), modes:LASER}), @@ -1696,7 +1698,6 @@ outputOriginBounds: UC.newBoolean(LANG.or_bnds_s, onBooleanClick, {title:LANG.or_bnds_l, modes:LASER}), outputOriginCenter: UC.newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l, modes:CAM_LASER}), camOriginTop: UC.newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l, modes:CAM}), - laserKnife: UC.newBoolean(LANG.ou_drkn_s, onBooleanClick, {title:LANG.ou_drkn_l, modes:[]}), advanced: UC.newGroup(LANG.ad_menu, null, {modes:FDM, expert:true}), outputRetractDist: UC.newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:UC.toFloat, modes:FDM, expert:true}), @@ -1767,10 +1768,6 @@ layers: UC.setGroup($("layers")), }); - function showLaserKnife() { - return UI.laserKnife.checked; - } - function spindleShow() { return settings().device.spindleMax > 0; } diff --git a/src/kiri/print.js b/src/kiri/print.js index c49c1140..e3b1e48f 100644 --- a/src/kiri/print.js +++ b/src/kiri/print.js @@ -321,11 +321,10 @@ * @param {Function} [onfirst] optional fn to call on first point * @return {Point} last output point */ - PRO.polyPrintPath = function(poly, startPoint, output, opt) { + PRO.polyPrintPath = function(poly, startPoint, output, options = {}) { poly.setClockwise(); - let options = opt || {}, - process = this.settings.process, + let process = this.settings.process, shortDist = process.outputShortDistance, shellMult = pref(options.extrude, process.outputShellMult), printSpeed = options.rate || process.outputFeedrate, diff --git a/src/kiri/worker.js b/src/kiri/worker.js index c2bfbf25..f02b07e5 100644 --- a/src/kiri/worker.js +++ b/src/kiri/worker.js @@ -220,6 +220,7 @@ KIRI.worker = { out.point = BASE.newPoint(x,y,z || 0); }); }); + const print = current.print = KIRI.newPrint(null, Object.values(cache)); const layers = KIRI.driver.FDM.prepareRender(parsed, progress => { send.data({ progress }); }, { thin: true }); diff --git a/src/mode/cam/prepare.js b/src/mode/cam/prepare.js index 9ec6425c..53ce03d6 100644 --- a/src/mode/cam/prepare.js +++ b/src/mode/cam/prepare.js @@ -550,10 +550,11 @@ spindle = Math.min(spindleMax, process.camOutlineSpindle); let flatLevels = depthData.outline.map(level => { return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); - }); - // flatLevels.xout('flat'); + }).filter(l => l.length > 0); + flatLevels.xout('flat'); // start with the smallest polygon on the top printPoint = flatLevels[0] + // .filter(l => l.length > 0)[0] .sort((a,b) => { return a.area() - b.area() })[0] .average(); printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false); diff --git a/src/mode/laser/driver.js b/src/mode/laser/driver.js index 1dbd8fc4..3557eafe 100644 --- a/src/mode/laser/driver.js +++ b/src/mode/laser/driver.js @@ -14,6 +14,7 @@ FDM = KIRI.driver.FDM, CAM = KIRI.driver.CAM, LASER = KIRI.driver.LASER = { + init, slice, prepare, export: exportLaser, @@ -24,6 +25,13 @@ SLICER = KIRI.slicer, newPoint = BASE.newPoint; + function init(kiri, api) { + api.event.on("settings.saved", (settings) => { + let UI = kiri.api.ui; + UI.knife.marker.style.display = UI.knifeOn.checked ? '' : 'none'; + }); + } + /** * DRIVER SLICE CONTRACT * @@ -34,6 +42,7 @@ */ function slice(settings, widget, onupdate, ondone) { let proc = settings.process; + let offset = proc.laserOffset; if (proc.laserSliceHeight < 0) { return ondone("invalid slice height"); @@ -46,8 +55,11 @@ }, function(slices) { widget.slices = slices; slices.forEach(function(slice, index) { - FDM.share.doShells(slice, 1, -proc.laserOffset); - slice.output().setLayer("slice").addPolys(slice.topPolys()); + let tops = slice.tops.map(t => t.poly); + let offsets = slice.offset = offset ? + POLY.offset(tops, offset, {z: slice.z, miter: 2 / offset}) : tops; + slice.output().setLayer("layer", { line: 0x888800 }).addPolys(tops); + slice.output().setLayer("cut").addPolys(offsets); onupdate(0.80 + (index/slices.length) * 0.20); }); ondone(); @@ -56,7 +68,7 @@ }); }; - function sliceEmitObjects(print, slice, groups) { + function sliceEmitObjects(print, slice, groups, simple) { let start = newPoint(0,0,0); let process = print.settings.process; let grouped = process.outputLaserGroup; @@ -74,14 +86,19 @@ laserOut(pi, group); }); } else { - print.polyPrintPath(poly, start, group, - zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1} - ); + let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1}; + if (simple) pathOpt.simple = true; + if (poly.open) pathOpt.open = true; + print.polyPrintPath(poly, start, group, pathOpt); } } - laserOut(slice.topPolyInners(), group); - laserOut(slice.topShells(), group); + let offset = slice.offset; + let inner = offset.map(poly => poly.inner || []).flat(); + + laserOut(offset, group); + laserOut(inner, group); + if (!grouped) { groups.push(group); group = []; @@ -103,10 +120,76 @@ let device = settings.device, process = settings.process, print = self.worker.print = KIRI.newPrint(settings, widgets), + knifeOn = process.knifeOn, + knifeDepth = process.outputKnifeDepth, + knifePasses = process.outputKnifePasses, + knifeTipOff = process.outputKnifeTip, output = print.output = [], totalSlices = 0, slices = 0; + function arc(center, s1, s2, out) { + let a1 = s1.angle; + let a2 = s2.angle; + let diff = s1.angleDiff(s2,true); + let step = 5; + 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, knifeTipOff) ); + } + while (ticks-- > 0) { + out.push( center.projectOnSlope(BASE.newSlopeFromAngle(a1 + off), knifeTipOff) ); + a1 += step * dir; + } + out.push( center.projectOnSlope(s2, knifeTipOff) ); + } + + // start to the "left" of the first point + function addKnifeRadii(poly) { + poly.setClockwise(); + let oldpts = poly.points.slice(); + // find leftpoint and make that the first point + let start = oldpts[0]; + let startI = 0; + for (let i=1; i 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:-knifeTipOff,y:0,z:0}); + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let newpts = [ lastpt, lastpt = oldpts[0] ]; + let tmp; + for (let i=1; i= 10 && lastpt.distTo2D(nextpt) >= knifeTipOff) { + arc(lastpt, lastsl, nextsl, newpts); + // let tmp; + // newpts.push( tmp = lastpt.projectOnSlope(lastsl, knifeTipOff) ); + // newpts.push( lastpt.projectOnSlope(nextsl, knifeTipOff) ); + } + newpts.push(nextpt); + lastsl = nextsl; + lastpt = nextpt; + } + newpts.push( tmp = lastpt.projectOnSlope(lastsl, knifeTipOff) ); + newpts.push( tmp.clone().move({x:knifeTipOff, y:0, z: 0}) ); + poly.open = true; + poly.points = newpts; + poly.length = newpts.length; + } + // find max layers (for updates) widgets.forEach(function(widget) { totalSlices += widget.slices.length; @@ -119,7 +202,7 @@ let merged = []; widget.slices.forEach(function(slice) { let polys = []; - slice.topPolys().clone(true).forEach(p => p.flattenTo(polys)); + slice.offsets.clone(true).forEach(p => p.flattenTo(polys)); polys.forEach(p => { let match = false; for (let i=0; i { + slice.offset.forEach(poly => { + addKnifeRadii(poly); + if (poly.inner) poly.inner.forEach(ip => { + addKnifeRadii(ip); + }); + }); + }); + } widget.slices.forEach(function(slice) { - sliceEmitObjects(print, slice, output); + sliceEmitObjects(print, slice, output, knifeOn); update((slices++ / totalSlices) * 0.5, "prepare"); }); } @@ -205,13 +298,9 @@ }); } - // if (process.laserKnife) { - // console.log({laser_it: output}); - // } - return print.render = FDM.prepareRender(output, (progress, layer) => { update(0.5 + progress * 0.5, "render", layer); - }, { thin: true, z: 0, action: "cut" }); + }, { thin: true, z: 0, action: "cut", moves: process.knifeOn }); }; function exportLaser(print, online, ondone) { @@ -310,10 +399,14 @@ 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 power = 255; let laser_on = dev.gcodeLaserOn || []; let laser_off = dev.gcodeLaserOff || []; + let knifeOn = proc.knifeOn; + let knifeDepth = proc.outputKnifeDepth; + let passes = knifeOn ? proc.outputKnifePasses : 1; exportElements( settings, @@ -332,25 +425,41 @@ }); }, function(poly, color, thick) { - poly.forEach(function(point, index) { - if (index === 0) { - lines.push(`G0${space}${point}`); - } else if (index === 1) { - laser_on.forEach(line => { - 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}`); - } - }); - laser_off.forEach(line => { - lines.push(line); - }); + if (knifeOn) { + lines.push(`; start new poly id=${poly.id} len=${poly.length}`); + } + for (let i=0; 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}`); + } + }); + laser_off.forEach(line => { + lines.push(line); + }); + } + if (knifeOn) { + lines.appendAll([`G0${space}Z5`]); + } }, function() { (dev.gcodePost || []).forEach(line => { diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 7c5d2f4e..9c92f342 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -348,9 +348,9 @@ kiri.lang['en-us'] = { // LASER KNIFE dk_menu: "knife", dk_dpth_s: "cut depth", - dk_dpth_l: "cut depth per pass\nin millimeters", + dk_dpth_l: "final cut depth\nin millimeters", dk_pass_s: "cut passes", - dk_pass_l: "number of passes\neach pass is deeper", + dk_pass_l: "number of passes\ndown to cut depth", dk_offs_s: "tip offset", dk_offs_l: "distance from blade tip\nto center of tool\nin millimeters", @@ -372,7 +372,7 @@ kiri.lang['en-us'] = { ou_layo_s: "layer color", ou_layo_l: "output layer colors\nfor each z index\noverridden by merged", ou_drkn_s: "drag knife", - ou_drkn_l: "enable drag knife\noutput menu", + ou_drkn_l: "enable drag knife\noutput in gcode\ncut radii are added\nto corners with\ncut down passes", // OUTPUT FDM ou_nozl_s: "nozzle temp",