diff --git a/app.js b/app.js index 9f5ac835..de03774a 100644 --- a/app.js +++ b/app.js @@ -308,6 +308,7 @@ const script = { "mode/cam/tool", "mode/cam/topo", "mode/cam/prepare", + "mode/cam/export", "mode/laser/driver", "kiri/widget", "kiri/print", diff --git a/package.json b/package.json index 761075e1..a8bcc266 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "2.3.D0", + "version": "2.3.D1", "description": "grid.space 3d slice & modeling tools", "author": "Stewart Allen ", "license": "MIT", diff --git a/src/license.js b/src/license.js index 3f8cee55..805392ff 100644 --- a/src/license.js +++ b/src/license.js @@ -3,7 +3,7 @@ let terms = { COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "2.3.D0" + VERSION: "2.3.D1" }; if (typeof(module) === 'object') { diff --git a/src/mode/cam/driver.js b/src/mode/cam/driver.js index ee13c01e..52f73a89 100644 --- a/src/mode/cam/driver.js +++ b/src/mode/cam/driver.js @@ -15,8 +15,8 @@ init, slice, sliceRender, - // printSetup, // src/mode/cam/prepare.js - printExport, + // printSetup, // src/mode/cam/prepare.js + // printExport, // src/mode/cam/export.js printRender }, CPRO = CAM.process = { @@ -28,16 +28,6 @@ TRACE: 6, DRILL: 7 }, - MODES = [ - "unset", - "level", - "rough", - "outline", - "contour-x", - "contour-y", - "trace", - "drill" - ], SLICER = KIRI.slicer, newLine = BASE.newLine, newSlice = KIRI.newSlice, @@ -63,150 +53,6 @@ }); } - /** - * Find top paths to trace when using ball and taper mills - * in waterline outlining and tracing modes. - */ - function findTracingPaths(widget, slice, tool, profile, partial) { - // for now, only emit completed polys and not segments - // TODO consider limiting to nup path lengths that are >= tool radius - let only_whole = !partial; - // check for ball and taper mills paths and add to top[0].inner - let polys = []; - let nups = []; - let cull = []; - slice.gatherTopPolys([]).forEach(poly => poly.flattenTo(polys)); - polys.forEach(poly => { - let pz = poly.first().z; - let mz = -Infinity; - let np = newPolygon().setOpen(); - let mp = 0; - // find top poly segments that are not significantly offset - // from tool profile and add to new polygons which accumulate - // to the top inner array - poly.forEachSegment((p1,p2) => { - let nz = getTopoZPathMax(widget, profile, p1.x, p1.y, p2.x, p2.y); - if (nz > mz) { - mz = nz; - } - // this # should be computed from topo resolution - if (nz - pz < 0.01) { - mp++ - if (np.length) { - if (!np.first().isEqual(p2)) { - np.append(p2); - } else { - np.setClosed(); - } - } else { - np.append(p1).append(p2); - } - } else if (np.length) { - if (!only_whole) { - nups.append(np); - } - np = newPolygon().setOpen(); - } - }); - if (np.length) { - if (np.length === poly.length) { - np.setClosed(); - } - // if a trace poly has no interruptions for an endmill - // and it's an inner poly, eliminate it from the parent - // so it won't be offset. - let parent = poly.parent; - if (np.isClosed() && parent) { - // console.log(slice.z,'cull',poly); - if (parent.inner) { - parent.inner = parent.inner.filter(p => p !== poly); - } - if (only_whole) { - nups.append(np); - } - } - if (!only_whole) { - nups.append(np); - } - } - }); - if (nups.length) { - // console.log(slice.z,'nups',nups.length); - slice.tops[0].inner = nups; - } - } - - // cut outside traces at the right points - function addCutoutTabs(slice, toolDiam, tabWidth, tabCount, tabAngle) { - // skip if no tops | traces - if (slice.tops.length === 0) return; - - let notabs = 0; - let nutrace = []; - - // find trace with greatest area - slice.tops[0].traces.forEach(function(trace, index) { - - // required to match computed order of cutouts - trace.setClockwise(); - - let count = tabCount, - angle = tabAngle, - angle_inc = 360 / count, - center = trace.bounds.center(slice.z), - offset = (tabWidth + toolDiam) / 2, - ints = [], - segs = []; - - while (count-- > 0) { - let slope = BASE.newSlopeFromAngle(angle), - normal = BASE.newSlopeFromAngle(angle + 90), - c1 = center.projectOnSlope(normal, offset), - c2 = center.projectOnSlope(normal, -offset), - o1 = c1.projectOnSlope(slope, 10000), - o2 = c2.projectOnSlope(slope, 10000), - int1 = trace.intersections(c1, o1).pop(), - int2 = trace.intersections(c2, o2).pop(); - if (int1 && int2) { - ints.push(int1); - ints.push(int2); - } - angle -= angle_inc; - } - - if (ints.length) { - ints.push(ints.shift()); - for (let i=0; i si.perimeter()) { - sj._overlap = true; - } - } - } - } - // replace intersected trace with non-overlapping segments - nutrace.appendAll(segs.filter(seg => !seg._overlap)); - } else { - nutrace.push(trace); - notabs++; - } - - }); - - if (notabs) { - console.log(`unable to compute tabs for ${notabs} traces @ z=${slice.z}`); - } - - slice.tops[0].traces = nutrace; - } - /** * DRIVER SLICE CONTRACT * @@ -279,8 +125,7 @@ return ondone(`invalid z bottom >= bounds z max ${bounds.max.z}`); } - // TODO cache terrain slicer info in widget - // TODO pass widget.isModified() on slice and re-use if false + // TODO pass widget.isModified() on slice and re-use cache if false // TODO pre-slice in background from client signal // TODO same applies to topo map generation let slicer = new KIRI.slicer2(widget.getPoints(), { @@ -291,7 +136,6 @@ let tshadow = []; let tzindex = slicer.interval(minStepDown, { fit: true, off: 0.01, down: true }); let terrain = slicer.slice(tzindex, { each: (data, index, total) => { - console.log('terrain', index, total, data); tshadow = POLY.union(tshadow.appendAll(data.tops), 0.01, true); tslices.push(data.slice); // let slice = data.slice; @@ -303,7 +147,6 @@ onupdate(0.0 + (index/total) * 0.1, "mapping"); }, genso: true }); let shadowTop = terrain[terrain.length - 1]; - console.log({slicer, tzindex, tshadow, terrain}); if (procDrillReg) { maxToolDiam = Math.max(maxToolDiam, drillToolDiam); @@ -317,7 +160,6 @@ // identify through holes thruHoles = tshadow.map(p => p.inner || []).flat(); - console.log({tshadow, thruHoles: thruHoles.clone(true)}); // create facing slices if (procFacing) { @@ -593,6 +435,77 @@ } } + // cut outside traces at the right points + function addCutoutTabs(slice, toolDiam, tabWidth, tabCount, tabAngle) { + // skip if no tops | traces + if (slice.tops.length === 0) return; + + let notabs = 0; + let nutrace = []; + + // find trace with greatest area + slice.tops[0].traces.forEach(function(trace, index) { + + // required to match computed order of cutouts + trace.setClockwise(); + + let count = tabCount, + angle = tabAngle, + angle_inc = 360 / count, + center = trace.bounds.center(slice.z), + offset = (tabWidth + toolDiam) / 2, + ints = [], + segs = []; + + while (count-- > 0) { + let slope = BASE.newSlopeFromAngle(angle), + normal = BASE.newSlopeFromAngle(angle + 90), + c1 = center.projectOnSlope(normal, offset), + c2 = center.projectOnSlope(normal, -offset), + o1 = c1.projectOnSlope(slope, 10000), + o2 = c2.projectOnSlope(slope, 10000), + int1 = trace.intersections(c1, o1).pop(), + int2 = trace.intersections(c2, o2).pop(); + if (int1 && int2) { + ints.push(int1); + ints.push(int2); + } + angle -= angle_inc; + } + + if (ints.length) { + ints.push(ints.shift()); + for (let i=0; i si.perimeter()) { + sj._overlap = true; + } + } + } + } + // replace intersected trace with non-overlapping segments + nutrace.appendAll(segs.filter(seg => !seg._overlap)); + } else { + nutrace.push(trace); + notabs++; + } + + }); + + if (notabs) { + console.log(`unable to compute tabs for ${notabs} traces @ z=${slice.z}`); + } + + slice.tops[0].traces = nutrace; + } + // runs in browser main function sliceRender(widget) { let slices = widget.slices; @@ -656,318 +569,81 @@ }); } - /** - * DRIVER PRINT CONTRACT - * - * @param {Object} print state object - * @param {Function} update incremental callback - * @param {Number} [index] into widget array - * @param {Object} [firstPoint] starting point - */ - - /** - * @returns {Array} gcode lines - */ - function printExport(print, online) { - let widget = print.widgets[0]; - - if (!widget) return; - - let i, - time = 0, - lines = 0, - bytes = 0, - factor = 1, - output = [], - spindle = 0, - modes = CPRO, - settings = print.settings, - device = settings.device, - gcodes = settings.device || {}, - tools = settings.tools, - space = gcodes.gcodeSpace ? ' ' : '', - stripComments = gcodes.gcodeStrip || false, - cmdToolChange = gcodes.gcodeChange || [ "M6 T{tool}" ], - cmdSpindle = gcodes.gcodeSpindle || [ "M3 S{speed}" ], - cmdDwell = gcodes.gcodeDwell || [ "G4 P{time}" ], - bounds = widget.getCamBounds(settings), - dev = settings.device, - spro = settings.process, - maxZd = spro.camFastFeedZ, - maxXYd = spro.camFastFeed, - decimals = BASE.config.gcode_decimals || 4, - pos = { x:null, y:null, z:null, f:null, t:null }, - line, - cidx, - mode = 0, - point, - points = 0, - hasStock = spro.camStockOffset || (spro.camStockZ && spro.camStockX && spro.camStockY), - zmax = hasStock ? settings.stock.z : bounds.max.z, - runbox = { - max: { x:-Infinity, y:-Infinity, z:-Infinity}, - min: { x:Infinity, y:Infinity, z:Infinity} - }, - offset = { - x: -settings.origin.x, - y: settings.origin.y - }, - consts = { - tool: 0, - tool_name: "unknown", - top: (offset ? dev.bedDepth : dev.bedDepth/2), - left: (offset ? 0 : -dev.bedWidth/2), - right: (offset ? dev.bedWidth : dev.bedWidth/2), - bottom: (offset ? 0 : -dev.bedDepth/2), - time_sec: 0, - time_ms: 0, - time: 0 - }, - append; - - if (online) { - append = function(line) { - if (line) { - lines++; - bytes += line.length; - output.append(line); - } - if (!line || output.length > 1000) { - online(output.join("\n")); - output = []; - } - }; - } else { - append = function(line) { - if (!line) return; - output.append(line); - lines++; - bytes += line.length; - } - } - - function filterEmit(array, consts) { - if (!array) return; - for (i=0; i= 0) { - line = line.substring(0, cidx).trim(); - if (line.length === 0) continue; - } - if (line.indexOf('G20') === 0) { - factor = 1/25.4; - consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; - consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; - consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; - consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; - } else if (line.indexOf('G21') === 0) { - factor = 1; - } - append(line); - } - } - - function add0(val, opt) { - let s = val.toString(), - d = s.indexOf("."); - if (d < 0) { - return opt ? s : s + '.0'; - } else { - return val.toFixed(decimals); - } - } - - function toolByNumber(number) { - for (let i=0; i { - let tool = toolz[tn]; - append(`; tool=${tn} flute=${tool.flute_diam} len=${tool.flute_len} metric=${tool.metric}`); - }); - } - - // emit gcode preamble - filterEmit(gcodes.gcodePre, consts); - - // emit all points in layer/point order - print.output.forEach(function (layerout) { - if (mode !== layerout.mode) { - if (mode && !stripComments) append("; ending " + MODES[mode] + " pass after " + Math.round(time/60) + " seconds"); - mode = layerout.mode; - if (!stripComments) append("; starting " + MODES[mode] + " pass"); - } - if (layerout.spindle && layerout.spindle !== spindle) { - spindle = layerout.spindle; - if (spindle > 0) { - filterEmit(cmdSpindle, {speed: Math.abs(spindle)}); - } else { - append("M4"); - } - // append((spindle > 0 ? "M3" : "M4") + " S" + Math.abs(spindle)); - } - layerout.forEach(function(out) { - moveTo(out); - }); - }); - if (mode && !stripComments) append("; ending " + MODES[mode] + " pass after " + Math.round(time/60) + " seconds"); - - // emit gcode post - filterEmit(gcodes.gcodePost, consts); - - // flush buffered gcode - append(); - - print.time = time; - print.lines = lines; - print.bytes = bytes + lines - 1; - print.bounds = runbox; - - return online ? null : output.join("\n"); - }; - function printRender(print) { return KIRI.driver.FDM.printRender(print, {aslines: true, color: 0x010101, move_color: 0xcc3333}); } + /** + * Find top paths to trace when using ball and taper mills + * in waterline outlining and tracing modes. + */ + function findTracingPaths(widget, slice, tool, profile, partial) { + // for now, only emit completed polys and not segments + // TODO consider limiting to nup path lengths that are >= tool radius + let only_whole = !partial; + // check for ball and taper mills paths and add to top[0].inner + let polys = []; + let nups = []; + let cull = []; + slice.gatherTopPolys([]).forEach(poly => poly.flattenTo(polys)); + polys.forEach(poly => { + let pz = poly.first().z; + let mz = -Infinity; + let np = newPolygon().setOpen(); + let mp = 0; + // find top poly segments that are not significantly offset + // from tool profile and add to new polygons which accumulate + // to the top inner array + poly.forEachSegment((p1,p2) => { + let nz = getTopoZPathMax(widget, profile, p1.x, p1.y, p2.x, p2.y); + if (nz > mz) { + mz = nz; + } + // this # should be computed from topo resolution + if (nz - pz < 0.01) { + mp++ + if (np.length) { + if (!np.first().isEqual(p2)) { + np.append(p2); + } else { + np.setClosed(); + } + } else { + np.append(p1).append(p2); + } + } else if (np.length) { + if (!only_whole) { + nups.append(np); + } + np = newPolygon().setOpen(); + } + }); + if (np.length) { + if (np.length === poly.length) { + np.setClosed(); + } + // if a trace poly has no interruptions for an endmill + // and it's an inner poly, eliminate it from the parent + // so it won't be offset. + let parent = poly.parent; + if (np.isClosed() && parent) { + // console.log(slice.z,'cull',poly); + if (parent.inner) { + parent.inner = parent.inner.filter(p => p !== poly); + } + if (only_whole) { + nups.append(np); + } + } + if (!only_whole) { + nups.append(np); + } + } + }); + if (nups.length) { + // console.log(slice.z,'nups',nups.length); + slice.tops[0].inner = nups; + } + } + })(); diff --git a/src/mode/cam/export.js b/src/mode/cam/export.js new file mode 100644 index 00000000..2c00b98f --- /dev/null +++ b/src/mode/cam/export.js @@ -0,0 +1,324 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + POLY = BASE.polygons, + UTIL = BASE.util, + CAM = KIRI.driver.CAM, + PRO = CAM.process, + MODES = [ + "unset", + "level", + "rough", + "outline", + "contour-x", + "contour-y", + "trace", + "drill" + ]; + + /** + * @returns {Array} gcode lines + */ + CAM.printExport = function printExport(print, online) { + let widget = print.widgets[0]; + + if (!widget) return; + + let i, + time = 0, + lines = 0, + bytes = 0, + factor = 1, + output = [], + spindle = 0, + settings = print.settings, + device = settings.device, + gcodes = settings.device || {}, + tools = settings.tools, + space = gcodes.gcodeSpace ? ' ' : '', + stripComments = gcodes.gcodeStrip || false, + cmdToolChange = gcodes.gcodeChange || [ "M6 T{tool}" ], + cmdSpindle = gcodes.gcodeSpindle || [ "M3 S{speed}" ], + cmdDwell = gcodes.gcodeDwell || [ "G4 P{time}" ], + bounds = widget.getCamBounds(settings), + dev = settings.device, + spro = settings.process, + maxZd = spro.camFastFeedZ, + maxXYd = spro.camFastFeed, + decimals = BASE.config.gcode_decimals || 4, + pos = { x:null, y:null, z:null, f:null, t:null }, + line, + cidx, + mode = 0, + point, + points = 0, + hasStock = spro.camStockOffset || (spro.camStockZ && spro.camStockX && spro.camStockY), + zmax = hasStock ? settings.stock.z : bounds.max.z, + runbox = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + offset = { + x: -settings.origin.x, + y: settings.origin.y + }, + consts = { + tool: 0, + tool_name: "unknown", + top: (offset ? dev.bedDepth : dev.bedDepth/2), + left: (offset ? 0 : -dev.bedWidth/2), + right: (offset ? dev.bedWidth : dev.bedWidth/2), + bottom: (offset ? 0 : -dev.bedDepth/2), + time_sec: 0, + time_ms: 0, + time: 0 + }, + append; + + if (online) { + append = function(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); + } + if (!line || output.length > 1000) { + online(output.join("\n")); + output = []; + } + }; + } else { + append = function(line) { + if (!line) return; + output.append(line); + lines++; + bytes += line.length; + } + } + + function filterEmit(array, consts) { + if (!array) return; + for (i=0; i= 0) { + line = line.substring(0, cidx).trim(); + if (line.length === 0) continue; + } + if (line.indexOf('G20') === 0) { + factor = 1/25.4; + consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; + consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; + consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; + consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; + } else if (line.indexOf('G21') === 0) { + factor = 1; + } + append(line); + } + } + + function add0(val, opt) { + let s = val.toString(), + d = s.indexOf("."); + if (d < 0) { + return opt ? s : s + '.0'; + } else { + return val.toFixed(decimals); + } + } + + function toolByNumber(number) { + for (let i=0; i { + let tool = toolz[tn]; + append(`; tool=${tn} flute=${tool.flute_diam} len=${tool.flute_len} metric=${tool.metric}`); + }); + } + + // emit gcode preamble + filterEmit(gcodes.gcodePre, consts); + + // emit all points in layer/point order + print.output.forEach(function (layerout) { + if (mode !== layerout.mode) { + if (mode && !stripComments) append("; ending " + MODES[mode] + " pass after " + Math.round(time/60) + " seconds"); + mode = layerout.mode; + if (!stripComments) append("; starting " + MODES[mode] + " pass"); + } + if (layerout.spindle && layerout.spindle !== spindle) { + spindle = layerout.spindle; + if (spindle > 0) { + filterEmit(cmdSpindle, {speed: Math.abs(spindle)}); + } else { + append("M4"); + } + // append((spindle > 0 ? "M3" : "M4") + " S" + Math.abs(spindle)); + } + layerout.forEach(function(out) { + moveTo(out); + }); + }); + if (mode && !stripComments) append("; ending " + MODES[mode] + " pass after " + Math.round(time/60) + " seconds"); + + // emit gcode post + filterEmit(gcodes.gcodePost, consts); + + // flush buffered gcode + append(); + + print.time = time; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.bounds = runbox; + + return online ? null : output.join("\n"); + }; + +})(); diff --git a/src/mode/cam/prepare.js b/src/mode/cam/prepare.js index aaa712b4..b54336b4 100644 --- a/src/mode/cam/prepare.js +++ b/src/mode/cam/prepare.js @@ -12,6 +12,14 @@ PRO = CAM.process, newPoint = BASE.newPoint; + /** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + * @param {Number} [index] into widget array + * @param {Object} [firstPoint] starting point + */ CAM.printSetup = function(print, update, index, firstPoint) { let widgetIndex = index || 0, widgetArray = print.widgets,