diff --git a/app.js b/app.js index fdab04b3..e7ac6cab 100644 --- a/app.js +++ b/app.js @@ -263,8 +263,11 @@ const script = { "kiri/slice", // "kiri/slicer", "mode/fdm/driver", + "mode/fdm/client", "mode/sla/driver", + "mode/sla/client", "mode/cam/driver", + "mode/cam/client", "mode/laser/driver", "kiri/layer", "kiri/widget", @@ -303,7 +306,12 @@ const script = { "kiri/slicer", "kiri/slicer2", "mode/fdm/driver", + "mode/fdm/slice", + "mode/fdm/prepare", + "mode/fdm/export", "mode/sla/driver", + "mode/sla/slice", + "mode/sla/export", "mode/cam/driver", "mode/cam/tool", "mode/cam/topo", diff --git a/src/kiri.js b/src/kiri.js index b29ac2d8..5b0228a6 100644 --- a/src/kiri.js +++ b/src/kiri.js @@ -6,7 +6,8 @@ if (!self.kiri) { self.kiri = { - loader: [] // module loading: array of functions + driver: {}, // driver modules + loader: [] // module loading: array of functions }; } diff --git a/src/kiri/print.js b/src/kiri/print.js index 969ddb04..8aca82f1 100644 --- a/src/kiri/print.js +++ b/src/kiri/print.js @@ -248,18 +248,18 @@ // more generic. only used by SLA at the moment PRO.export = function(remote, online, ondone) { - let driver = KIRI.driver[this.settings.mode]; - - if (!(driver && driver.printExport)) { - console.log({missing_export_driver: mode}); - ondone(null); - return; - } + let mode = this.settings.mode; + let driver = KIRI.driver[mode]; if (remote) { // executed from kiri.js KIRI.work.printExport(this.settings, online, ondone); } else { + if (!(driver && driver.printExport)) { + console.log({missing_export_driver: mode}); + ondone(null); + return; + } // executed from kiri-worker.js driver.printExport(this, online, ondone); } diff --git a/src/mode/cam/client.js b/src/mode/cam/client.js new file mode 100644 index 00000000..58677e25 --- /dev/null +++ b/src/mode/cam/client.js @@ -0,0 +1,99 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + CAM = KIRI.driver.CAM, + PRO = CAM.process; + + CAM.init = function(kiri, api) { + api.event.on("mode.set", (mode) => { + let isCAM = mode === 'CAM'; + $('set-tools').style.display = isCAM ? '' : 'none'; + kiri.space.platform.setColor(isCAM ? 0xeeeeee : 0xcccccc); + }); + api.event.on("settings.saved", (settings) => { + let proc = settings.process; + api.ui.camTabs.marker.style.display = proc.camTabsOn ? 'flex' : 'none'; + api.ui.camRough.marker.style.display = proc.camRoughOn ? 'flex' : 'none'; + api.ui.camDrill.marker.style.display = + proc.camDrillingOn || proc.camDrillReg !== 'none' ? 'flex' : 'none'; + api.ui.camOutline.marker.style.display = proc.camOutlineOn ? 'flex' : 'none'; + api.ui.camContour.marker.style.display = + proc.camContourXOn || proc.camContourYOn ? 'flex' : 'none'; + }); + } + + CAM.sliceRender = function(widget) { + let slices = widget.slices; + if (!slices) return; + + slices.forEach(function(slice) { + let tops = slice.tops, + layers = slice.layers, + outline = layers.outline, + open = (slice.camMode === PRO.CONTOUR_X || slice.camMode === PRO.CONTOUR_Y); + + layers.outline.clear(); // slice raw edges + layers.trace.clear(); // roughing + layers.solid.clear(); // outline + layers.bridge.clear(); // outline x + layers.flat.clear(); // outline y + layers.fill.clear(); // facing + + tops.forEach(function(top) { + outline.poly(top.poly, 0x999900, true, open); + // if (top.inner) outline.poly(top.inner, 0xdddddd, true); + if (top.inner) outline.poly(top.inner, 0xff0000, true); + }); + + // various outlining + let layer; + slice.tops.forEach(function(top) { + switch (slice.camMode) { + case PRO.OUTLINE: + layer = layers.solid; + break; + case PRO.CONTOUR_X: + layer = layers.bridge; + break; + case PRO.CONTOUR_Y: + layer = layers.flat; + break; + default: // roughing + layer = layers.trace; + break; + } + if (top.traces) { + layer.poly(top.traces, 0x010101, true, null); + } + }); + + // facing (previously separate. now part of roughing) + layer = slice.layers.fill; + slice.tops.forEach(function(top) { + if (top.fill_lines) { + layer.lines(top.fill_lines, fill_color); + } + }); + + outline.render(); + layers.trace.render(); + layers.solid.render(); + layers.bridge.render(); + layers.flat.render(); + layers.fill.render(); + }); + } + + CAM.printRender = function(print) { + return KIRI.driver.FDM.printRender(print, { + aslines: true, + color: 0x010101, + move_color: 0xcc3333 + }); + } + +})(); diff --git a/src/mode/cam/driver.js b/src/mode/cam/driver.js index 70a4a423..2bc09c84 100644 --- a/src/mode/cam/driver.js +++ b/src/mode/cam/driver.js @@ -4,20 +4,14 @@ (function() { - if (!self.kiri.driver) self.kiri.driver = { }; - if (self.kiri.driver.CAM) return; - - const KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - POLY = BASE.polygons, + let KIRI = self.kiri, CAM = KIRI.driver.CAM = { - init, + // init, // src/mode/cam/client.js + // sliceRender, // src/mode/cam/client.js + // printRender // src/mode/cam/client.js // slice, // src/mode/cam/slice.js - sliceRender, // printSetup, // src/mode/cam/prepare.js // printExport, // src/mode/cam/export.js - printRender }, CPRO = CAM.process = { LEVEL: 1, @@ -27,96 +21,7 @@ CONTOUR_Y: 5, TRACE: 6, DRILL: 7 - }, - newLine = BASE.newLine, - newSlice = KIRI.newSlice, - newPoint = BASE.newPoint, - newPolygon = BASE.newPolygon; - - function init(kiri, api) { - api.event.on("mode.set", (mode) => { - let isCAM = mode === 'CAM'; - $('set-tools').style.display = isCAM ? '' : 'none'; - kiri.space.platform.setColor(isCAM ? 0xeeeeee : 0xcccccc); - }); - api.event.on("settings.saved", (settings) => { - let proc = settings.process; - api.ui.camTabs.marker.style.display = proc.camTabsOn ? 'flex' : 'none'; - api.ui.camRough.marker.style.display = proc.camRoughOn ? 'flex' : 'none'; - api.ui.camDrill.marker.style.display = - proc.camDrillingOn || proc.camDrillReg !== 'none' ? 'flex' : 'none'; - api.ui.camOutline.marker.style.display = proc.camOutlineOn ? 'flex' : 'none'; - api.ui.camContour.marker.style.display = - proc.camContourXOn || proc.camContourYOn ? 'flex' : 'none'; - }); - } - - // runs in browser main - function sliceRender(widget) { - let slices = widget.slices; - if (!slices) return; - - slices.forEach(function(slice) { - let tops = slice.tops, - layers = slice.layers, - outline = layers.outline, - open = (slice.camMode === CPRO.CONTOUR_X || slice.camMode === CPRO.CONTOUR_Y); - - layers.outline.clear(); // slice raw edges - layers.trace.clear(); // roughing - layers.solid.clear(); // outline - layers.bridge.clear(); // outline x - layers.flat.clear(); // outline y - layers.fill.clear(); // facing - - tops.forEach(function(top) { - outline.poly(top.poly, 0x999900, true, open); - // if (top.inner) outline.poly(top.inner, 0xdddddd, true); - if (top.inner) outline.poly(top.inner, 0xff0000, true); - }); - - // various outlining - let layer; - slice.tops.forEach(function(top) { - switch (slice.camMode) { - case CPRO.OUTLINE: - layer = layers.solid; - break; - case CPRO.CONTOUR_X: - layer = layers.bridge; - break; - case CPRO.CONTOUR_Y: - layer = layers.flat; - break; - default: // roughing - layer = layers.trace; - break; - } - if (top.traces) { - layer.poly(top.traces, 0x010101, true, null); - } - }); - - // facing (previously separate. now part of roughing) - layer = slice.layers.fill; - slice.tops.forEach(function(top) { - if (top.fill_lines) { - layer.lines(top.fill_lines, fill_color); - } - }); - - outline.render(); - layers.trace.render(); - layers.solid.render(); - layers.bridge.render(); - layers.flat.render(); - layers.fill.render(); - }); - } - - 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 diff --git a/src/mode/fdm/client.js b/src/mode/fdm/client.js new file mode 100644 index 00000000..cede8c01 --- /dev/null +++ b/src/mode/fdm/client.js @@ -0,0 +1,270 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + FDM = KIRI.driver.FDM; + + FDM.init = function(kiri, api) { + api.event.on("settings.load", (settings) => { + if (settings.mode !== 'FDM') return; + settings.process.outputOriginCenter = (settings.device.originCenter || false); + }); + api.event.on("settings.saved", (settings) => { + let proc = settings.process; + api.ui.fdmSupport.marker.style.display = proc.sliceSupportEnable ? 'flex' : 'none'; + api.ui.fdmInfill.marker.style.display = proc.sliceFillSparse > 0 ? 'flex' : 'none'; + api.ui.fdmRaft.marker.style.display = proc.outputRaft ? 'flex' : 'none'; + }); + } + + /** + * DRIVER SLICE RENDER CONTRACT + * + * @param {Object} widget to render + * @param {number} mode to use to select colors + */ + FDM.sliceRender = function(widget) { + let slices = widget.slices; + let settings = widget.settings; + let extnum = widget.getExtruder(settings); + let extarr = settings.device.extruders || []; + let extinfo = extarr[extnum || 0]; + let extoff = (extinfo.extNozzle || 0.4) / 2; + let thin = settings.controller.thinRender && settings.mode === 'FDM'; + + if (!slices) return; + + // render outline + slices.forEach(function(s) { + let layers = s.layers; + let tops = s.tops; + let outline = layers.outline; + let shells = layers.trace; + let solids = layers.fill; + let sparse = layers.sparse; + let support = layers.support; + let outsolid = layers.solid; + let outbridge = layers.bridge; + let outflat = layers.flat; + + outline.clear(); + shells.clear(); + solids.clear(); + sparse.clear(); + support.clear(); + outsolid.clear(); + outbridge.clear(); + outflat.clear(); + + outline.setTransparent(true); + shells.setTransparent(false); + solids.setTransparent(false); + sparse.setTransparent(false); + support.setTransparent(false); + outsolid.setTransparent(true); + outbridge.setTransparent(true); + outflat.setTransparent(true); + + tops.forEach(function(top) { + // outline + outline.poly(top.poly, 0xdddddd, true, false); + outline.solid(top.poly, thin ? 0xdddddd : 0xcccccc); + if (top.inner) outline.poly(top.inner, 0xdddddd, true); + // if (top.thinner) outline.poly(top.thinner, 0x559999, true, null); + + // shells + if (thin) { + shells.poly(top.traces, 0x77bbcc, true); + } else { + shells.noodle(top.traces, extoff, 0x88aadd, 0x77bbcc); + } + if (top.polish) { + shells.poly(top.polish.x, 0x880000, true); + shells.poly(top.polish.y, 0x880000, true); + } + + // solid fill + if (thin) { + solids.lines(top.thin_fill, 0x77bbcc); + solids.lines(top.fill_lines, 0x77bbcc); + } else { + solids.noodle_lines(top.thin_fill, extoff, 0x88aadd, 0x77bbcc, s.z); + solids.noodle_lines(top.fill_lines, extoff, 0x88aadd, 0x77bbcc, s.z); + } + + // sparse fill + if (top.fill_sparse) { + top.fill_sparse.forEach(function(poly) { + if (thin) { + // todo cull polys with single point before this + if (poly.length > 1) poly.render(sparse, 0x0, false, true); + } else { + sparse.noodle_open(poly, extoff, 0x88aadd, 0x77bbcc, s.z); + } + }); + } + + // support + if (s.supports) { + if (thin) { + s.supports.forEach(function(poly) { + support.poly(poly, 0xffaadd, true); + support.lines(poly.fills, 0xffaadd); + }); + } else { + support.noodle(s.supports, extoff, 0xeeaadd, 0x77bbcc); + s.supports.forEach(function(poly) { + support.noodle_lines(poly.fills, extoff, 0xeeaadd, 0x77bbcc, s.z); + }); + } + } + }); + + // solid outlines + let trimmed = s.solids.trimmed; + if (trimmed) trimmed.forEach(function(poly) { + poly.setZ(s.z + 0.025); + outsolid.poly(poly, 0x00cc00, true, false); + outsolid.solid(poly, 0x00dd00); + }); + + // diff bridges + if (s.bridges) s.bridges.forEach(function (poly) { + poly.setZ(s.z + 0.05); + outbridge.poly(poly, 0x0099ee, true, false); + outbridge.solid(poly, 0x00aaff); + }); + + // diff flats + if (s.flats) s.flats.forEach(function (poly) { + poly.setZ(s.z + 0.05); + outflat.poly(poly, 0xee0099, true, false); + outflat.solid(poly, 0xff00aa); + }); + + outline.renderAll(); + shells.renderAll(); + solids.renderAll(); + sparse.renderAll(); + support.renderAll(); + outsolid.renderAll(); + outbridge.renderAll(); + outflat.renderAll(); + }); + } + + + FDM.printRender = function(print, options) { + let debug = KIRI.api.const.LOCAL; + let scope = print, emits, last, + moves, moving = true, + opt = options || {}, + tools = opt.tools || [], + showmoves = !opt.nomoves, + maxspeed = 0; + // find max speed + scope.output.forEach(function(layerout) { + layerout.forEach(function(out, index) { + if (out.emit && out.speed) { + maxspeed = Math.max(maxspeed, out.speed); + } + }); + }); + if (maxspeed === 0) { + maxspeed = 4000; + } + maxspeed *= 1.001; + // render layered output + scope.lines = 0; + scope.output.forEach(function(layerout) { + let move = [], print = [], cprint; + layerout.forEach(function(out, index) { + let point = toPoint(out.point); + if (last) { + // drop short segments + // if (point.emit === last.emit && UTIL.distSq(last, point) < 0.001 && point.z === last.z) { + // return; + // } + if (out.emit > 0) { + if (moving || !cprint) { + cprint = base.newPolygon().setOpen(true).append(last); + print.push({ + poly: cprint, + speed: out.speed || maxspeed || 4000, + tool:tools[out.tool] + }); + } + try { + cprint.append(point); + } catch (e) { + console.log(e, {cprint}); + } + moving = false; + } else { + cprint = null; + move.push(last); + move.push(point); + moving = true; + } + // move direction arrow heads + if (debug && last.z == point.z) { + let rs = BASE.newSlope( + {x: point.x, y: point.y}, + {x: last.x, y: last.y} + ); + let ao1 = BASE.newSlopeFromAngle(rs.angle + 25); + let ao2 = BASE.newSlopeFromAngle(rs.angle - 25); + let sp = BASE.newPoint(point.x, point.y, point.z); + move.push(sp); + move.push(sp.projectOnSlope(ao1, BASE.config.debug_arrow)); + move.push(sp); + move.push(sp.projectOnSlope(ao2, BASE.config.debug_arrow)); + } + } + last = point; + }); + emits = KIRI.newLayer(scope.group); + if (showmoves) { + moves = KIRI.newLayer(scope.group); + moves.lines(move, opt.move_color || 0x888888); + } + emits.setTransparent(false); + // emit printing shapes + print.forEach(segment => { + let {poly, speed, tool} = segment; + let off = tool ? (tool.extNozzle || 0.4) / 2 : 0.2; + let sint = Math.min(maxspeed, parseInt(speed)); + let rgb = scope.hsv2rgb({h:sint/maxspeed, s:1, v:1}); + let color = ((rgb.r * 0xff) << 16) | + ((rgb.g * 0xff) << 8) | + ((rgb.b * 0xff) << 0); + if (opt.flat) { + poly = poly.clone().setZ(0); + } + if (opt.aslines) { + emits.poly(poly, opt.color || color, false, true); + } else { + // first point may be from the layer below, so use second point + emits.noodle_open(poly, off - 0.02, color, 0x0, poly.getZ(1)); + } + }); + emits.renderAll(); + if (showmoves) { + moves.render(); + scope.movesView.push(moves); + } + scope.printView.push(emits); + scope.lines += print.length; + }); + } + + function toPoint(obj) { + return base.newPoint(obj.x, obj.y, obj.z); + } + +})(); diff --git a/src/mode/fdm/driver.js b/src/mode/fdm/driver.js index 2af26b8b..095bef26 100644 --- a/src/mode/fdm/driver.js +++ b/src/mode/fdm/driver.js @@ -4,34 +4,20 @@ (function() { - if (!self.kiri.driver) self.kiri.driver = { }; - if (self.kiri.driver.FDM) return; - const KIRI = self.kiri, BASE = self.base, DBUG = BASE.debug, UTIL = BASE.util, - CONF = BASE.config, POLY = BASE.polygons, FDM = KIRI.driver.FDM = { - init, - slice, - sliceRender, - printSetup, - printExport, - printRender - }, - SLICER = KIRI.slicer, - newPoint = BASE.newPoint; - - // customer gcode post function for XYZ daVinci Mini W - self.kiri_fdm_xyz_mini_w = function(gcode, options) { - return btoa("; filename = kirimoto.gcode\n; machine = dv1MW0A000\n" + gcode); - }; - - function bound(v,min,max) { - return Math.max(min,Math.min(max,v)); - } + // init, // src/mode/fdm/client.js + // sliceRender, // src/mode/fdm/client.js + // printRender // src/mode/fdm/client.js + // slice, // src/mode/fdm/slice.js + // printSetup, // src/mode/fdm/prepare.js + // printExport, // src/mode/fdm/export.js + fixExtruders + }; function fixExtruders(settings) { Object.entries(settings.widget).forEach(arr => { @@ -44,1345 +30,9 @@ return settings; } - function init(kiri, api) { - api.event.on("settings.load", (settings) => { - if (settings.mode !== 'FDM') return; - settings.process.outputOriginCenter = (settings.device.originCenter || false); - }); - api.event.on("settings.saved", (settings) => { - let proc = settings.process; - api.ui.fdmSupport.marker.style.display = proc.sliceSupportEnable ? 'flex' : 'none'; - api.ui.fdmInfill.marker.style.display = proc.sliceFillSparse > 0 ? 'flex' : 'none'; - api.ui.fdmRaft.marker.style.display = proc.outputRaft ? 'flex' : 'none'; - }); - } - - /** - * DRIVER SLICE CONTRACT - * - * Given a widget and settings object, call functions necessary to produce - * slices and then the computations using those slices. This function is - * designed to run client or server-side and provides all output via - * callback functions. - * - * @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 slice(settings, widget, onupdate, ondone) { - fixExtruders(settings); - let spro = settings.process, - sdev = settings.device, - update_start = Date.now(), - minSolid = spro.sliceSolidMinArea, - solidLayers = spro.sliceSolidLayers, - vaseMode = spro.sliceFillType === 'vase', - doSolidLayers = solidLayers && !vaseMode, - metadata = settings.widget[widget.id] || {}, - extruder = metadata.extruder || 0, - sliceHeight = spro.sliceHeight, - nozzleSize = sdev.extruders[extruder].extNozzle, - firstOffset = nozzleSize / 2, - shellOffset = nozzleSize, - fillOffsetMult = 1.0 - bound(spro.sliceFillOverlap, 0, 0.8), - fillSpacing = nozzleSize, - fillOffset = nozzleSize * fillOffsetMult, - sliceFillAngle = spro.sliceFillAngle, - view = widget.mesh && widget.mesh.newGroup ? widget.mesh.newGroup() : null; - - if (!(sliceHeight > 0 && sliceHeight < 100)) { - return ondone("invalid slice height"); - } - if (!(nozzleSize >= 0.01 && nozzleSize < 100)) { - return ondone("invalid nozzle size"); - } - - if (spro.firstSliceHeight === 0) { - spro.firstSliceHeight = sliceHeight; - } - - if (spro.firstSliceHeight < sliceHeight) { - DBUG.log("invalid first layer height < slice height"); - DBUG.log("reverting to slice height"); - spro.firstSliceHeight = sliceHeight; - } - - SLICER.sliceWidget(widget, { - height: sliceHeight, - minHeight: sliceHeight > spro.sliceMinHeight ? spro.sliceMinHeight : 0, - firstHeight: spro.firstSliceHeight, - view: view - }, onSliceDone, onSliceUpdate); - - function onSliceUpdate(update) { - return onupdate(0.0 + update * 0.5); - } - - function onSliceDone(slices) { - // slices = slices.filter(slice => slice.tops.length); - // remove all empty slices above part but leave below - // for multi-part (multi-extruder) setups where the void is ok - let found = false; - slices = slices.reverse().filter(slice => { - if (slice.tops.length) { - return found = true; - } else { - return found; - } - }).reverse(); - - widget.slices = slices; - - if (!slices) return; - - // calculate % complete and call onupdate() - function doupdate(index, from, to, msg) { - onupdate(0.5 + (from + ((index/slices.length) * (to-from))) * 0.5, msg); - } - - // for each slice, performe a function and call doupdate() - function forSlices(from, to, fn, msg) { - slices.forEach(function(slice) { - fn(slice); - doupdate(slice.index, from, to, msg) - }); - } - - // do not hint polygin fill longer than a max span length - CONF.hint_len_max = UTIL.sqr(spro.sliceBridgeMax); - - // reset (if necessary) for solids and support projections - slices.forEach(function(slice) { - slice.extruder = extruder; - slice.invalidateFill(); - slice.invalidateSolids(); - slice.invalidateSupports(); - }); - - let supportEnabled = spro.sliceSupportEnable && spro.sliceSupportDensity > 0.0, - supportMinArea = spro.sliceSupportArea; - - // create shells and diff inner fillable areas - forSlices(0.0, 0.2, function(slice) { - let solid = ( - slice.index < spro.sliceBottomLayers || - slice.index > slices.length - spro.sliceTopLayers-1 || - spro.sliceFillSparse > 0.95 - ) && !vaseMode; - slice.doShells(spro.sliceShells, firstOffset, shellOffset, fillOffset, { - vase: vaseMode, - thin: spro.detectThinWalls - }); - if (solid) slice.doSolidLayerFill(fillSpacing, sliceFillAngle); - sliceFillAngle += 90.0; - }, "offsets"); - - // calculations only relevant when solid layers are used - if (doSolidLayers) { - forSlices(0.2, 0.34, function(slice) { - slice.doDiff(minSolid); - }, "diff"); - forSlices(0.34, 0.35, function(slice) { - slice.projectFlats(solidLayers); - slice.projectBridges(solidLayers); - }, "solids"); - forSlices(0.35, 0.5, function(slice) { - slice.doSolidsFill(fillSpacing, sliceFillAngle, minSolid); - sliceFillAngle += 90.0; - }, "solids"); - } - - // calculations only relevant when supports are enabled - if (supportEnabled) { - forSlices(0.5, 0.7, function(slice) { - slice.doSupport(spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset, spro.sliceSupportGap); - }, "support"); - forSlices(0.7, 0.8, function(slice) { - slice.doSupportFill(nozzleSize, spro.sliceSupportDensity, supportMinArea); - }, "support"); - } - - // sparse layers only present when non-vase mose and sparse % > 0 - if (!vaseMode && spro.sliceFillSparse > 0.0) { - forSlices(0.8, 1.0, function(slice) { - slice.doSparseLayerFill({ - settings: settings, - process: spro, - device: sdev, - lineWidth: nozzleSize, - spacing: fillOffset, - density: spro.sliceFillSparse, - bounds: widget.getBoundingBox(), - height: sliceHeight, - type: spro.sliceFillType - }); - }, "infill"); - } - - // let polish = spro.polishLayers; - // // experimental polishing - // if (polish) { - // let polish_layer = Math.floor(polish); - // let polish_step = Math.max(polish - polish_layer || 1, 0.25); - // widget.polish = {}; - // let px = []; - // let py = []; - // // compute x polishing slices - // SLICER.sliceWidget(widget, { - // height: nozzleSize * polish_step, - // swapX: true, - // swapY: false, - // simple: true - // }, (polish_done => { - // widget.polish.x = polish_done - // .filter(s => s.groups.length) - // .map(s => s.groups) - // .forEach(p => px.appendAll(p)); - // }), (polish_update) => { - // // console.log({polish_update}); - // }); - // // compute y polishing slices - // SLICER.sliceWidget(widget, { - // height: nozzleSize * polish_step, - // swapX: false, - // swapY: true, - // simple: true - // }, (polish_done => { - // widget.polish.y = polish_done - // .filter(s => s.groups.length) - // .map(s => s.groups) - // .forEach(p => py.appendAll(p)); - // }), (polish_update) => { - // // console.log({polish_update}); - // }); - // // apply polishing finishes to layers - // forSlices(1.0, 1.0, (slice) => { - // if (polish_layer >= 2) { - // let sai = (slice.index - polish_layer); - // if (sai % (polish_layer-1) !== 0) { - // return; - // } - // } - // if (slice.index >= polish_layer) { - // let sd = slice; - // for (let i=0; i { - // - // let polys = []; - // pa.forEach(p => { - // // rotate and test for self-intersection (points under model) - // p.ensureXY(); - // let ox = p._aligned == 'yz' ? 0.1 : 0; - // let oy = p._aligned == 'yz' ? 0 : 0.1; - // p.forEachPoint(pt => { - // let int = p.intersections( - // pt.offset(ox,oy,0), - // pt.offset(ox*10000,oy*10000,0)); - // if (int.length > 0) { - // pt._under = true; - // } - // }); - // p.restoreXY(); - // - // let lastp = undefined; - // let poly = []; - // // look for qualifying segments - // p.forEachSegment((p1, p2) => { - // // eliminate segments that projected up - // // intersect with the polygon (bottom faces) - // if (p1._under || p2._under) { - // return; - // } - // // skip when both below layer range - // if (p1.z < zb && p2.z < zb) { - // return; - // } - // // skip when both above layer range - // if (p1.z > zt && p2.z > zt) { - // return; - // } - // // skip vertical - // if (p1.x === p2.x && p1.y === p2.y) { - // return; - // } - // // skip horizontal - // // if (p1.z === p2.z) { - // // return; - // // } - // // order points lowest to highest - // let swap = false; - // if (p1.z > p2.z) { - // let t = p2; - // p2 = p1; - // p1 = t; - // swap = true; - // } - // let trimlo = false; - // let trimhi = false; - // if (p1.z < zb) { - // trimlo = true; - // } - // if (p2.z > zt) { - // trimhi = true; - // } - // let xaxis = p1.x === p2.x; - // if (xaxis) { - // p1 = BASE.newPoint(p1.y,p1.z,p1.x); - // p2 = BASE.newPoint(p2.y,p2.z,p2.x); - // } else { - // p1 = BASE.newPoint(p1.x,p1.z,p1.y); - // p2 = BASE.newPoint(p2.x,p2.z,p2.y); - // } - // let slope = BASE.newSlope(p1, p2); - // let angle = slope.angle; - // if (angle > 80 && angle < 100) { - // return; - // } - // let len = p1.distTo2D(p2); - // let np1 = p1; - // if (trimlo) { - // let zunder = zb - p1.y; - // let zover = p2.y - zb; - // let zdelt = p2.y - p1.y; - // let pct = zunder / zdelt; - // np1 = p1.follow(slope, len * pct); - // } - // if (trimhi) { - // let zunder = zt - p1.y; - // let zover = p2.y - zt; - // let zdelt = p2.y - p1.y; - // let pct = zover / zdelt; - // p2 = p2.follow(slope.invert(), len * pct); - // } - // p1 = np1; - // if (xaxis) { - // p1 = BASE.newPoint(p1.z,p1.x,p1.y); - // p2 = BASE.newPoint(p2.z,p2.x,p2.y); - // } else { - // p1 = BASE.newPoint(p1.x,p1.z,p1.y); - // p2 = BASE.newPoint(p2.x,p2.z,p2.y); - // } - // if (!lastp) { - // poly.push(p1); - // poly.push(p2); - // } else if (p1.isMergable2D(lastp)) { - // // } else if (p1.isEqual(lastp)) { - // poly.push(p2); - // cont++; - // } else if (poly.length) { - // polys.push(poly); - // poly = [p1, p2]; - // } - // lastp = p2; - // }); - // if (poly.length) { - // polys.push(poly); - // } - // }); - // pout.push(polys); - // polys = []; - // }); - // - // if (pout.length && slice.tops.length) { - // slice.tops[0].polish = { - // x: pout[0] - // .map(a => BASE.newPolygon(a).setOpen()) - // // .filter(p => p.perimeter() > nozzleSize) - // , - // y: pout[1] - // .map(a => BASE.newPolygon(a).setOpen()) - // // .filter(p => p.perimeter() > nozzleSize) - // }; - // } - // } - // }); - // } - - // report slicing complete - ondone(); - } - - } - - /** - * DRIVER SLICE RENDER CONTRACT - * - * @param {Object} widget to render - * @param {number} mode to use to select colors - */ - function sliceRender(widget) { - let slices = widget.slices; - let settings = widget.settings; - let extnum = widget.getExtruder(settings); - let extarr = settings.device.extruders || []; - let extinfo = extarr[extnum || 0]; - let extoff = (extinfo.extNozzle || 0.4) / 2; - let thin = settings.controller.thinRender && settings.mode === 'FDM'; - - if (!slices) return; - - // render outline - slices.forEach(function(s) { - let layers = s.layers; - let tops = s.tops; - let outline = layers.outline; - let shells = layers.trace; - let solids = layers.fill; - let sparse = layers.sparse; - let support = layers.support; - let outsolid = layers.solid; - let outbridge = layers.bridge; - let outflat = layers.flat; - - outline.clear(); - shells.clear(); - solids.clear(); - sparse.clear(); - support.clear(); - outsolid.clear(); - outbridge.clear(); - outflat.clear(); - - outline.setTransparent(true); - shells.setTransparent(false); - solids.setTransparent(false); - sparse.setTransparent(false); - support.setTransparent(false); - outsolid.setTransparent(true); - outbridge.setTransparent(true); - outflat.setTransparent(true); - - tops.forEach(function(top) { - // outline - outline.poly(top.poly, 0xdddddd, true, false); - outline.solid(top.poly, thin ? 0xdddddd : 0xcccccc); - if (top.inner) outline.poly(top.inner, 0xdddddd, true); - // if (top.thinner) outline.poly(top.thinner, 0x559999, true, null); - - // shells - if (thin) { - shells.poly(top.traces, 0x77bbcc, true); - } else { - shells.noodle(top.traces, extoff, 0x88aadd, 0x77bbcc); - } - if (top.polish) { - shells.poly(top.polish.x, 0x880000, true); - shells.poly(top.polish.y, 0x880000, true); - } - - // solid fill - if (thin) { - solids.lines(top.thin_fill, 0x77bbcc); - solids.lines(top.fill_lines, 0x77bbcc); - } else { - solids.noodle_lines(top.thin_fill, extoff, 0x88aadd, 0x77bbcc, s.z); - solids.noodle_lines(top.fill_lines, extoff, 0x88aadd, 0x77bbcc, s.z); - } - - // sparse fill - if (top.fill_sparse) { - top.fill_sparse.forEach(function(poly) { - if (thin) { - // todo cull polys with single point before this - if (poly.length > 1) poly.render(sparse, 0x0, false, true); - } else { - sparse.noodle_open(poly, extoff, 0x88aadd, 0x77bbcc, s.z); - } - }); - } - - // support - if (s.supports) { - if (thin) { - s.supports.forEach(function(poly) { - support.poly(poly, 0xffaadd, true); - support.lines(poly.fills, 0xffaadd); - }); - } else { - support.noodle(s.supports, extoff, 0xeeaadd, 0x77bbcc); - s.supports.forEach(function(poly) { - support.noodle_lines(poly.fills, extoff, 0xeeaadd, 0x77bbcc, s.z); - }); - } - } - }); - - // solid outlines - let trimmed = s.solids.trimmed; - if (trimmed) trimmed.forEach(function(poly) { - poly.setZ(s.z + 0.025); - outsolid.poly(poly, 0x00cc00, true, false); - outsolid.solid(poly, 0x00dd00); - }); - - // diff bridges - if (s.bridges) s.bridges.forEach(function (poly) { - poly.setZ(s.z + 0.05); - outbridge.poly(poly, 0x0099ee, true, false); - outbridge.solid(poly, 0x00aaff); - }); - - // diff flats - if (s.flats) s.flats.forEach(function (poly) { - poly.setZ(s.z + 0.05); - outflat.poly(poly, 0xee0099, true, false); - outflat.solid(poly, 0xff00aa); - }); - - outline.renderAll(); - shells.renderAll(); - solids.renderAll(); - sparse.renderAll(); - support.renderAll(); - outsolid.renderAll(); - outbridge.renderAll(); - outflat.renderAll(); - }); - } - - /** - * DRIVER PRINT CONTRACT - * - * @param {Object} print state object - * @param {Function} update incremental callback - */ - function printSetup(print, update) { - let widgets = print.widgets.slice(), - settings = fixExtruders(print.settings), - device = settings.device, - nozzle = device.extruders[0].extNozzle, - process = settings.process, - mode = settings.mode, - output = print.output, - bounds = settings.bounds, - printPoint = newPoint(0,0,0), - firstLayerHeight = process.firstSliceHeight || process.sliceHeight, - maxLayers = 0, - layer = 0, - zoff = 0, - meshIndex, - lastIndex, - closest, - mindist, - minidx, - find, - layerout = [], - slices = [], - sliceEntry; - - // TODO pick a widget with a slice on the first layer and use that nozzle - // create brim, skirt, raft if specificed in FDM mode (code shared by laser) - if (process.outputBrimCount || process.outputRaft) { - let brims = [], - offset = process.outputBrimOffset || (process.outputRaft ? 4 : 0); - - // compute first brim - widgets.forEach(function(widget) { - let tops = []; - // collect top outer polygons - widget.slices[0].tops.forEach(function(top) { - tops.push(top.poly.clone()); - }); - // collect support polygons - if (widget.slices[0].supports) - widget.slices[0].supports.forEach(function(support) { - tops.push(support.clone()); - }); - // nest and offset tops - POLY.nest(tops).forEach(function(poly) { - poly.offset(-offset + nozzle / 2).forEach(function(brim) { - brim.move(widget.mesh.position); - brims.push(brim); - }); - }); - }); - - // merge brims - brims = POLY.union(brims); - - // if brim is offset, over-expand then shrink to induce brims to merge - if (offset && brims.length) { - let extra = process.sliceSupportExtra + 2; - let zheight = brims[0].getZ(); - brims = POLY.expand(brims, extra, zheight, null, 1); - brims = POLY.expand(brims, -extra, zheight, null, 1); - } - - // if raft is specified - if (process.outputRaft) { - let offset = newPoint(0,0,0), - height = nozzle; - - // cause first point of raft to be used - printPoint = null; - - let raft = function(height, angle, spacing, speed, extrude) { - let slice = kiri.newSlice(zoff + height / 2); - brims.forEach(function(brim) { - // use first point of first brim as start point - if (printPoint === null) printPoint = brim.first(); - let t = slice.addTop(brim); - t.traces = [ brim ]; - t.inner = POLY.expand(t.traces, -nozzle * 0.5, 0, null, 1); - // tweak bounds for fill to induce an offset - t.inner[0].bounds.minx -= nozzle/2; - t.inner[0].bounds.maxx += nozzle/2; - t.fill_lines = POLY.fillArea(t.inner, angle, spacing, []); - }) - offset.z = slice.z; - printPoint = print.slicePrintPath(slice, printPoint, offset, layerout, { - speed: speed, - mult: extrude, - }); - layerout.height = height; - output.append(layerout); - - layerout = []; - zoff += height; - }; - - raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 3, 4); - raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 2, 4); - raft(nozzle/2, process.sliceFillAngle + 90, nozzle * 3.0, process.outputFeedrate, 2.5); - raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.5); - raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.0); - - // raise first layer off raft slightly to lessen adhesion - firstLayerHeight += process.outputRaftSpacing || 0; - - // retract after last raft layer - output.last().last().retract = true; - } - // raft excludes brims - else - // if using brim vs raft - if (process.outputBrimCount) { - let polys = [], - preout = []; - - // expand specified # of brims - brims.forEach(function(brim) { - POLY.offset([brim], nozzle, {outs: polys, flat: true, count: process.outputBrimCount}); - }); - - // output brim points - printPoint = print.poly2polyEmit(polys, printPoint, function(poly, index, count, startPoint) { - return print.polyPrintPath(poly, startPoint, preout, { - rate: process.firstLayerRate, - onfirst: function(point) { - if (preout.length && point.distTo2D(startPoint) > 2) { - // retract between brims - preout.last().retract = true; - } - } - }); - }, null, true); - - print.addPrintPoints(preout, layerout, null); - - if (preout.length) { - // retract between brims and print - preout.last().retract = true; - } - } - // recompute bounds for purge block offsets - let bbounds = BASE.newBounds(); - brims.forEach(brim => { - bbounds.merge(brim.bounds); - }); - bounds.min.x = Math.min(bounds.min.x, bbounds.minx); - bounds.min.y = Math.min(bounds.min.y, bbounds.miny); - bounds.max.x = Math.max(bounds.max.x, bbounds.maxx); - bounds.max.y = Math.max(bounds.max.y, bbounds.maxy); - } - - // synthesize a support widget, if needed - if (process.sliceSupportEnable) { - let swidget = KIRI.newWidget(); - let sslices = swidget.slices = []; - widgets.forEach(function(widget) { - let slices = widget.slices; - while (sslices.length < slices.length) { - let sslice = KIRI.newSlice(slices[sslices.length].z); - sslice.extruder = process.sliceSupportNozzle; - sslices.push(sslice); - } - slices.forEach((slice,index) => { - if (!slice.supports) return; - if (!sslices[index].supports) { - sslices[index].supports = []; - } - sslices[index].supports.appendAll(slice.supports.map(p => { - if (p.fills) p.fills.forEach(p => p.move(widget.track.pos)); - return p.move(widget.track.pos); - })); - }); - }); - swidget.support = true; - swidget.mesh = { widget: swidget }; // fake for lookup - settings.widget[swidget.id] = { extruder: process.sliceSupportNozzle }; - widgets.push(swidget); - } - - let extruders = []; - let extcount = 0; - - // find max layers (for updates) - // generate list of used extruders for purge blocks - widgets.forEach(function(widget) { - maxLayers = Math.max(maxLayers, widget.slices.length); - let extruder = (settings.widget[widget.id] || {}).extruder || 0; - if (!extruders[extruder]) { - extruders[extruder] = {}; - extcount++; - } - }); - - let blokpos, walkpos, blok; - if (bounds.min.x < bounds.min.y) { - let dx = ((bounds.max.x - bounds.min.x) - (extcount * 10)) / 2 + 5; - blokpos = { x:bounds.min.x + dx, y: bounds.max.y + 5}; - walkpos = { x:10, y:0 }; - blok = { x:9, y:4 }; - } else { - let dy = ((bounds.max.y - bounds.min.y) - (extcount * 10)) / 2 + 5; - blokpos = { x:bounds.max.x + 5, y: bounds.min.y + dy}; - walkpos = { x:0, y:10 }; - blok = { x:4, y:9 }; - } - - // compute purge blocks - extruders = extruders.map((ext,i) => { - if (!ext) return ext; - let noz = device.extruders[i].extNozzle, - pos = {x:blokpos.x, y:blokpos.y, z:0}, - rec = { - extruder: i, - poly: BASE.newPolygon().centerSpiral(pos, blok.x, blok.y, noz*2, 3) - }; - blokpos.x += walkpos.x; - blokpos.y += walkpos.y; - return rec; - }); - - // generate purge block for given nozzle - function purge(nozzle, track, layer, start, z, using) { - if (extcount < 2) { - return start; - } - let rec = track[nozzle]; - if (rec) { - track[nozzle] = null; - if (layer.last()) layer.last().retract = true; - start = print.polyPrintPath(rec.poly.clone().setZ(z), start, layer, { - tool: using || nozzle, - open: true, - simple: true - }); - layer.last().retract = true; - return start; - } else { - console.log({already_purged: nozzle, from: track, layer}); - return start; - } - } - - // increment layer count until no widget has remaining slices - for (;;) { - // create list of mesh slice arrays with their platform offsets - for (meshIndex = 0; meshIndex < widgets.length; meshIndex++) { - let mesh = widgets[meshIndex].mesh; - if (!mesh.widget) { - continue; - } - let mslices = mesh.widget.slices; - if (mslices && mslices[layer]) { - slices.push({ - slice: mslices[layer], - offset: mesh.position || {x:0, y:0, z:0}, - widget: mesh.widget - }); - } - } - - // exit if no slices - if (slices.length === 0) { - break; - } - - // track purge blocks generated for each layer - let track = extruders.slice(); - let lastOut; - let lastExt; - - // iterate over layer slices, find closest widget, print, eliminate - for (;;) { - closest = null; - mindist = Infinity; - let order = []; - // select slices of the same extruder type first then distance - for (meshIndex = 0; meshIndex < slices.length; meshIndex++) { - sliceEntry = slices[meshIndex]; - if (sliceEntry) { - find = sliceEntry.slice.findClosestPointTo(printPoint.sub(sliceEntry.offset)); - if (find) { - let ext = sliceEntry.slice.extruder; - let lex = lastOut ? lastOut.extruder : ext; - let dst = Math.abs(find.distance); - if (ext !== lex) dst *= 10000; - order.push({dst,sliceEntry,meshIndex}); - } - } - } - order.sort((a,b) => { - return a.dst - b.dst; - }); - if (order.length) { - let find = order.shift(); - closest = find.sliceEntry; - minidx = find.meshIndex; - } - - if (!closest) { - if (sliceEntry) lastOut = sliceEntry.slice; - break; - } - // retract between widgets - if (layerout.length && minidx !== lastIndex) { - layerout.last().retract = true; - } - layerout.height = layerout.height || closest.slice.height; - slices[minidx] = null; - closest.offset.z = zoff; - // detect extruder change and print purge block - if (!lastOut || lastOut.extruder !== closest.slice.extruder) { - printPoint = purge(closest.slice.extruder, track, layerout, printPoint, closest.slice.z); - } - // output seek to start point between mesh slices if previous data - printPoint = print.slicePrintPath( - closest.slice, - printPoint.sub(closest.offset), - closest.offset, - layerout, - { - first: closest.slice.index === 0, - support: closest.widget.support - } - ); - lastOut = closest.slice; - lastExt = lastOut.ext - lastIndex = minidx; - } - - // if a declared extruder isn't used in a layer, use selected - // extruder to fill the relevant purge blocks for later support - track.forEach(ext => { - if (ext) { - printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt); - } - }); - - // if layer produced output, append to output array - if (layerout.length) output.append(layerout); - - // notify progress - layerout.layer = layer++; - update(layer / maxLayers); - - // retract after last layer - if (layer === maxLayers && layerout.length) { - layerout.last().retract = true; - } - - slices = []; - layerout = []; - lastOut = undefined; - } + // customer gcode post function for XYZ daVinci Mini W + self.kiri_fdm_xyz_mini_w = function(gcode, options) { + return btoa("; filename = kirimoto.gcode\n; machine = dv1MW0A000\n" + gcode); }; - /** - * @returns {Array} gcode lines - */ - function printExport(print, online) { - let layers = print.output, - settings = fixExtruders(print.settings), - device = settings.device, - extruders = device.extruders, - process = settings.process, - gcodeFan = device.gcodeFan, - gcodeLayer = device.gcodeLayer, - gcodeTrack = device.gcodeTrack, - tool = 0, - extruder = extruders[tool], - offset_x = extruder.extOffsetX, - offset_y = extruder.extOffsetY, - extrudeAbs = device.extrudeAbs, - time = 0, - layer = 0, - pause = [], - pauseCmd = device.gcodePause, - output = [], - outputLength = 0, - lastProgress = 0, - decimals = BASE.config.gcode_decimals || 4, - progress = 0, - distance = 0, - emitted = 0, - retracted = 0, - pos = {x:0, y:0, z:0, f:0}, - last = null, - zpos = 0, - zhop = process.zHopDistance || 0, - seekMMM = process.outputSeekrate * 60, - retDist = process.outputRetractDist, - retSpeed = process.outputRetractSpeed * 60, - retDwell = process.outputRetractDwell || 0, - timeDwell = retDwell / 1000, - offset = process.outputOriginCenter ? null : { - x: device.bedWidth/2, - y: device.bedDepth/2 - }, - subst = { - travel_speed: seekMMM, - retract_speed: retSpeed, - retract_distance: retDist, - temp: process.firstLayerNozzleTemp || process.outputTemp, - temp_bed: process.firstLayerBedTemp || process.outputBedTemp, - bed_temp: process.firstLayerBedTemp || process.outputBedTemp, - fan_speed: process.outputFanMax, - speed: process.outputFanMax, // legacy - top: offset ? device.bedDepth : device.bedDepth/2, - left: offset ? 0 : -device.bedWidth/2, - right: offset ? device.bedWidth : device.bedWidth/2, - bottom: offset ? 0 : -device.bedDepth/2, - z_max: device.maxHeight, - layers: layers.length, - nozzle: 0, - tool: 0 - }, - pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, - append, - lines = 0, - bytes = 0; - - (process.gcodePauseLayers || "").split(",").forEach(function(lv) { - let v = parseInt(lv); - if (v >= 0) pause.push(v); - }); - - 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 appendSub(line) { - append(print.constReplace(line, subst)); - } - - function appendAll(arr) { - if (!arr) return; - if (!Array.isArray(arr)) arr = [ arr ]; - arr.forEach(function(line) { append(line) }); - } - - function appendAllSub(arr) { - if (!arr) return; - if (!Array.isArray(arr)) arr = [ arr ]; - arr.forEach(function(line) { appendSub(line) }); - } - - append(`; Generated by Kiri:Moto ${KIRI.version}`); - append(`; ${new Date().toString()}`); - appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); - append(`; Target: ${settings.filter[settings.mode]}`); - append("; --- process ---"); - for (let pk in process) { - append("; " + pk + " = " + process[pk]); - } - append("; --- startup ---"); - let t0 = false; - let t1 = false; - for (let i=0; i= 0) t0 = true; else - if (line.indexOf('T1') >= 0) t1 = true; else - if (line.indexOf('M82') >= 0) extrudeAbs = true; else - if (line.indexOf('M83') >= 0) extrudeAbs = false; else - if (line.indexOf('G90') >= 0) extrudeAbs = true; else - if (line.indexOf('G91') >= 0) extrudeAbs = false; else - if (line.indexOf('G92') === 0) { - line.split(";")[0].split(' ').forEach(function (tok) { - let val = parseFloat(tok.substring(1) || 0) || 0; - switch (tok[0]) { - case 'X': pos.x = val; break; - case 'Y': pos.y = val; break; - case 'Z': pos.z = val; break; - case 'E': outputLength = val; break; - } - }); - } - if (extrudeAbs && line.indexOf('E') > 0) { - line.split(";")[0].split(' ').forEach(function (tok) { - // use max E position from gcode-preamble - if (tok[0] == 'E') { - outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); - } - }); - } - appendSub(line); - } - - function dwell(ms) { - append(`G4 P${ms}`); - time += timeDwell; - } - - function retract(zhop) { - retracted = retDist; - moveTo({e:-retracted}, retSpeed, `e-retract ${retDist}`); - if (zhop) moveTo({z:zpos + zhop}, seekMMM, "z-hop start"); - time += (retDist / retSpeed) * 60 * 2; // retraction time - } - - function moveTo(newpos, rate, comment) { - if (comment) { - append(";; " + comment); - } - let o = [!rate && !newpos.e ? 'G0' : 'G1']; - if (typeof newpos.x === 'number') { - pos.x = newpos.x;//UTIL.round(newpos.x,decimals); - o.append(" X").append(pos.x.toFixed(decimals)); - } - if (typeof newpos.y === 'number') { - pos.y = newpos.y;//UTIL.round(newpos.y,decimals); - o.append(" Y").append(pos.y.toFixed(decimals)); - } - if (typeof newpos.z === 'number') { - pos.z = newpos.z;//UTIL.round(newpos.z,decimals); - o.append(" Z").append(pos.z.toFixed(decimals)); - } - if (typeof newpos.e === 'number') { - outputLength += newpos.e; - if (extrudeAbs) { - // for cumulative (absolute) extruder positions - // o.append(" E").append(UTIL.round(outputLength, decimals)); - o.append(" E").append(outputLength.toFixed(decimals)); - } else { - // o.append(" E").append(UTIL.round(newpos.e, decimals)); - o.append(" E").append(newpos.e.toFixed(decimals)); - } - } - if (rate && rate != pos.f) { - o.append(" F").append(Math.round(rate)); - pos.f = rate - } - let line = o.join(''); - if (last == line) { - // console.log({dup:line}); - return; - } - last = line; - append(line); - } - - // calc total distance traveled by head as proxy for progress - let allout = [], totaldistance = 0; - layers.forEach(function(outs) { - allout.appendAll(outs); - }); - allout.forEachPair(function (o1, o2) { - totaldistance += o1.point.distTo2D(o2.point); - }, 1); - - // retract before first move - retract(); - - while (layer < layers.length) { - path = layers[layer]; - emitPerMM = print.extrudePerMM( - extruder.extNozzle, - extruder.extFilament, - path.layer === 0 ? - (process.firstSliceHeight || process.sliceHeight) : path.height); - - subst.z = (zpos + path.height).toFixed(3); - subst.Z = subst.z; - subst.layer = layer; - subst.height = path.height.toFixed(3); - - if (pauseCmd && pause.indexOf(layer) >= 0) { - appendAllSub(pauseCmd) - } - - if (gcodeLayer && gcodeLayer.length) { - appendAllSub(gcodeLayer); - } else { - append(`;; --- layer ${layer} (${subst.height} @ ${subst.z}) ---`); - } - - if (layer > 0 && process.outputLayerRetract) { - retract(); - } - - // enable fan at fan layer - if (gcodeFan && layer === process.outputFanLayer) { - appendAllSub(gcodeFan); - } - - // second layer transitions - if (layer === 1) { - // update temps when first layer overrides are present - if (process.firstLayerNozzleTemp) { - subst.temp = process.outputTemp; - if (t0) appendSub("M104 S{temp} T0"); - if (t1) appendSub("M104 S{temp} T1"); - if (!(t0 || t1)) appendSub("M104 S{temp} T{tool}"); - } - if (process.firstLayerBedTemp) { - subst.bed_temp = subst.temp_bed = process.outputBedTemp; - appendSub("M140 S{temp_bed} T0"); - } - } - - // move Z to layer height - zpos += path.height; - moveTo({z:zpos}, seekMMM); - - // iterate through layer outputs - for (pidx=0; pidx 0) { - if (moving || !cprint) { - cprint = base.newPolygon().setOpen(true).append(last); - print.push({ - poly: cprint, - speed: out.speed || maxspeed || 4000, - tool:tools[out.tool] - }); - } - try { - cprint.append(point); - } catch (e) { - console.log(e, {cprint}); - } - moving = false; - } else { - cprint = null; - move.push(last); - move.push(point); - moving = true; - } - // move direction arrow heads - if (debug && last.z == point.z) { - let rs = BASE.newSlope( - {x: point.x, y: point.y}, - {x: last.x, y: last.y} - ); - let ao1 = BASE.newSlopeFromAngle(rs.angle + 25); - let ao2 = BASE.newSlopeFromAngle(rs.angle - 25); - let sp = BASE.newPoint(point.x, point.y, point.z); - move.push(sp); - move.push(sp.projectOnSlope(ao1, BASE.config.debug_arrow)); - move.push(sp); - move.push(sp.projectOnSlope(ao2, BASE.config.debug_arrow)); - } - } - last = point; - }); - emits = KIRI.newLayer(scope.group); - if (showmoves) { - moves = KIRI.newLayer(scope.group); - moves.lines(move, opt.move_color || 0x888888); - } - emits.setTransparent(false); - // emit printing shapes - print.forEach(segment => { - let {poly, speed, tool} = segment; - let off = tool ? (tool.extNozzle || 0.4) / 2 : 0.2; - let sint = Math.min(maxspeed, parseInt(speed)); - let rgb = scope.hsv2rgb({h:sint/maxspeed, s:1, v:1}); - let color = ((rgb.r * 0xff) << 16) | - ((rgb.g * 0xff) << 8) | - ((rgb.b * 0xff) << 0); - if (opt.flat) { - poly = poly.clone().setZ(0); - } - if (opt.aslines) { - emits.poly(poly, opt.color || color, false, true); - } else { - // first point may be from the layer below, so use second point - emits.noodle_open(poly, off - 0.02, color, 0x0, poly.getZ(1)); - } - }); - emits.renderAll(); - if (showmoves) { - moves.render(); - scope.movesView.push(moves); - } - scope.printView.push(emits); - scope.lines += print.length; - }); - } - })(); diff --git a/src/mode/fdm/export.js b/src/mode/fdm/export.js new file mode 100644 index 00000000..51868938 --- /dev/null +++ b/src/mode/fdm/export.js @@ -0,0 +1,382 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + FDM = KIRI.driver.FDM; + + /** + * @returns {Array} gcode lines + */ + FDM.printExport = function(print, online) { + let layers = print.output, + settings = FDM.fixExtruders(print.settings), + device = settings.device, + extruders = device.extruders, + process = settings.process, + gcodeFan = device.gcodeFan, + gcodeLayer = device.gcodeLayer, + gcodeTrack = device.gcodeTrack, + tool = 0, + extruder = extruders[tool], + offset_x = extruder.extOffsetX, + offset_y = extruder.extOffsetY, + extrudeAbs = device.extrudeAbs, + time = 0, + layer = 0, + pause = [], + pauseCmd = device.gcodePause, + output = [], + outputLength = 0, + lastProgress = 0, + decimals = BASE.config.gcode_decimals || 4, + progress = 0, + distance = 0, + emitted = 0, + retracted = 0, + pos = {x:0, y:0, z:0, f:0}, + last = null, + zpos = 0, + zhop = process.zHopDistance || 0, + seekMMM = process.outputSeekrate * 60, + retDist = process.outputRetractDist, + retSpeed = process.outputRetractSpeed * 60, + retDwell = process.outputRetractDwell || 0, + timeDwell = retDwell / 1000, + offset = process.outputOriginCenter ? null : { + x: device.bedWidth/2, + y: device.bedDepth/2 + }, + subst = { + travel_speed: seekMMM, + retract_speed: retSpeed, + retract_distance: retDist, + temp: process.firstLayerNozzleTemp || process.outputTemp, + temp_bed: process.firstLayerBedTemp || process.outputBedTemp, + bed_temp: process.firstLayerBedTemp || process.outputBedTemp, + fan_speed: process.outputFanMax, + speed: process.outputFanMax, // legacy + top: offset ? device.bedDepth : device.bedDepth/2, + left: offset ? 0 : -device.bedWidth/2, + right: offset ? device.bedWidth : device.bedWidth/2, + bottom: offset ? 0 : -device.bedDepth/2, + z_max: device.maxHeight, + layers: layers.length, + nozzle: 0, + tool: 0 + }, + pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, + append, + lines = 0, + bytes = 0; + + (process.gcodePauseLayers || "").split(",").forEach(function(lv) { + let v = parseInt(lv); + if (v >= 0) pause.push(v); + }); + + 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 appendSub(line) { + append(print.constReplace(line, subst)); + } + + function appendAll(arr) { + if (!arr) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { append(line) }); + } + + function appendAllSub(arr) { + if (!arr) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { appendSub(line) }); + } + + append(`; Generated by Kiri:Moto ${KIRI.version}`); + append(`; ${new Date().toString()}`); + appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); + append(`; Target: ${settings.filter[settings.mode]}`); + append("; --- process ---"); + for (let pk in process) { + append("; " + pk + " = " + process[pk]); + } + append("; --- startup ---"); + let t0 = false; + let t1 = false; + for (let i=0; i= 0) t0 = true; else + if (line.indexOf('T1') >= 0) t1 = true; else + if (line.indexOf('M82') >= 0) extrudeAbs = true; else + if (line.indexOf('M83') >= 0) extrudeAbs = false; else + if (line.indexOf('G90') >= 0) extrudeAbs = true; else + if (line.indexOf('G91') >= 0) extrudeAbs = false; else + if (line.indexOf('G92') === 0) { + line.split(";")[0].split(' ').forEach(function (tok) { + let val = parseFloat(tok.substring(1) || 0) || 0; + switch (tok[0]) { + case 'X': pos.x = val; break; + case 'Y': pos.y = val; break; + case 'Z': pos.z = val; break; + case 'E': outputLength = val; break; + } + }); + } + if (extrudeAbs && line.indexOf('E') > 0) { + line.split(";")[0].split(' ').forEach(function (tok) { + // use max E position from gcode-preamble + if (tok[0] == 'E') { + outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); + } + }); + } + appendSub(line); + } + + function dwell(ms) { + append(`G4 P${ms}`); + time += timeDwell; + } + + function retract(zhop) { + retracted = retDist; + moveTo({e:-retracted}, retSpeed, `e-retract ${retDist}`); + if (zhop) moveTo({z:zpos + zhop}, seekMMM, "z-hop start"); + time += (retDist / retSpeed) * 60 * 2; // retraction time + } + + function moveTo(newpos, rate, comment) { + if (comment) { + append(";; " + comment); + } + let o = [!rate && !newpos.e ? 'G0' : 'G1']; + if (typeof newpos.x === 'number') { + pos.x = newpos.x;//UTIL.round(newpos.x,decimals); + o.append(" X").append(pos.x.toFixed(decimals)); + } + if (typeof newpos.y === 'number') { + pos.y = newpos.y;//UTIL.round(newpos.y,decimals); + o.append(" Y").append(pos.y.toFixed(decimals)); + } + if (typeof newpos.z === 'number') { + pos.z = newpos.z;//UTIL.round(newpos.z,decimals); + o.append(" Z").append(pos.z.toFixed(decimals)); + } + if (typeof newpos.e === 'number') { + outputLength += newpos.e; + if (extrudeAbs) { + // for cumulative (absolute) extruder positions + // o.append(" E").append(UTIL.round(outputLength, decimals)); + o.append(" E").append(outputLength.toFixed(decimals)); + } else { + // o.append(" E").append(UTIL.round(newpos.e, decimals)); + o.append(" E").append(newpos.e.toFixed(decimals)); + } + } + if (rate && rate != pos.f) { + o.append(" F").append(Math.round(rate)); + pos.f = rate + } + let line = o.join(''); + if (last == line) { + // console.log({dup:line}); + return; + } + last = line; + append(line); + } + + // calc total distance traveled by head as proxy for progress + let allout = [], totaldistance = 0; + layers.forEach(function(outs) { + allout.appendAll(outs); + }); + allout.forEachPair(function (o1, o2) { + totaldistance += o1.point.distTo2D(o2.point); + }, 1); + + // retract before first move + retract(); + + while (layer < layers.length) { + path = layers[layer]; + emitPerMM = print.extrudePerMM( + extruder.extNozzle, + extruder.extFilament, + path.layer === 0 ? + (process.firstSliceHeight || process.sliceHeight) : path.height); + + subst.z = (zpos + path.height).toFixed(3); + subst.Z = subst.z; + subst.layer = layer; + subst.height = path.height.toFixed(3); + + if (pauseCmd && pause.indexOf(layer) >= 0) { + appendAllSub(pauseCmd) + } + + if (gcodeLayer && gcodeLayer.length) { + appendAllSub(gcodeLayer); + } else { + append(`;; --- layer ${layer} (${subst.height} @ ${subst.z}) ---`); + } + + if (layer > 0 && process.outputLayerRetract) { + retract(); + } + + // enable fan at fan layer + if (gcodeFan && layer === process.outputFanLayer) { + appendAllSub(gcodeFan); + } + + // second layer transitions + if (layer === 1) { + // update temps when first layer overrides are present + if (process.firstLayerNozzleTemp) { + subst.temp = process.outputTemp; + if (t0) appendSub("M104 S{temp} T0"); + if (t1) appendSub("M104 S{temp} T1"); + if (!(t0 || t1)) appendSub("M104 S{temp} T{tool}"); + } + if (process.firstLayerBedTemp) { + subst.bed_temp = subst.temp_bed = process.outputBedTemp; + appendSub("M140 S{temp_bed} T0"); + } + } + + // move Z to layer height + zpos += path.height; + moveTo({z:zpos}, seekMMM); + + // iterate through layer outputs + for (pidx=0; pidx -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + FDM = KIRI.driver.FDM, + newPoint = BASE.newPoint; + + /** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + */ + FDM.printSetup = function(print, update) { + let widgets = print.widgets.slice(), + settings = FDM.fixExtruders(print.settings), + device = settings.device, + nozzle = device.extruders[0].extNozzle, + process = settings.process, + mode = settings.mode, + output = print.output, + bounds = settings.bounds, + printPoint = newPoint(0,0,0), + firstLayerHeight = process.firstSliceHeight || process.sliceHeight, + maxLayers = 0, + layer = 0, + zoff = 0, + meshIndex, + lastIndex, + closest, + mindist, + minidx, + find, + layerout = [], + slices = [], + sliceEntry; + + // TODO pick a widget with a slice on the first layer and use that nozzle + // create brim, skirt, raft if specificed in FDM mode (code shared by laser) + if (process.outputBrimCount || process.outputRaft) { + let brims = [], + offset = process.outputBrimOffset || (process.outputRaft ? 4 : 0); + + // compute first brim + widgets.forEach(function(widget) { + let tops = []; + // collect top outer polygons + widget.slices[0].tops.forEach(function(top) { + tops.push(top.poly.clone()); + }); + // collect support polygons + if (widget.slices[0].supports) + widget.slices[0].supports.forEach(function(support) { + tops.push(support.clone()); + }); + // nest and offset tops + POLY.nest(tops).forEach(function(poly) { + poly.offset(-offset + nozzle / 2).forEach(function(brim) { + brim.move(widget.mesh.position); + brims.push(brim); + }); + }); + }); + + // merge brims + brims = POLY.union(brims); + + // if brim is offset, over-expand then shrink to induce brims to merge + if (offset && brims.length) { + let extra = process.sliceSupportExtra + 2; + let zheight = brims[0].getZ(); + brims = POLY.expand(brims, extra, zheight, null, 1); + brims = POLY.expand(brims, -extra, zheight, null, 1); + } + + // if raft is specified + if (process.outputRaft) { + let offset = newPoint(0,0,0), + height = nozzle; + + // cause first point of raft to be used + printPoint = null; + + let raft = function(height, angle, spacing, speed, extrude) { + let slice = kiri.newSlice(zoff + height / 2); + brims.forEach(function(brim) { + // use first point of first brim as start point + if (printPoint === null) printPoint = brim.first(); + let t = slice.addTop(brim); + t.traces = [ brim ]; + t.inner = POLY.expand(t.traces, -nozzle * 0.5, 0, null, 1); + // tweak bounds for fill to induce an offset + t.inner[0].bounds.minx -= nozzle/2; + t.inner[0].bounds.maxx += nozzle/2; + t.fill_lines = POLY.fillArea(t.inner, angle, spacing, []); + }) + offset.z = slice.z; + printPoint = print.slicePrintPath(slice, printPoint, offset, layerout, { + speed: speed, + mult: extrude, + }); + layerout.height = height; + output.append(layerout); + + layerout = []; + zoff += height; + }; + + raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 3, 4); + raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 2, 4); + raft(nozzle/2, process.sliceFillAngle + 90, nozzle * 3.0, process.outputFeedrate, 2.5); + raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.5); + raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.0); + + // raise first layer off raft slightly to lessen adhesion + firstLayerHeight += process.outputRaftSpacing || 0; + + // retract after last raft layer + output.last().last().retract = true; + } + // raft excludes brims + else + // if using brim vs raft + if (process.outputBrimCount) { + let polys = [], + preout = []; + + // expand specified # of brims + brims.forEach(function(brim) { + POLY.offset([brim], nozzle, {outs: polys, flat: true, count: process.outputBrimCount}); + }); + + // output brim points + printPoint = print.poly2polyEmit(polys, printPoint, function(poly, index, count, startPoint) { + return print.polyPrintPath(poly, startPoint, preout, { + rate: process.firstLayerRate, + onfirst: function(point) { + if (preout.length && point.distTo2D(startPoint) > 2) { + // retract between brims + preout.last().retract = true; + } + } + }); + }, null, true); + + print.addPrintPoints(preout, layerout, null); + + if (preout.length) { + // retract between brims and print + preout.last().retract = true; + } + } + // recompute bounds for purge block offsets + let bbounds = BASE.newBounds(); + brims.forEach(brim => { + bbounds.merge(brim.bounds); + }); + bounds.min.x = Math.min(bounds.min.x, bbounds.minx); + bounds.min.y = Math.min(bounds.min.y, bbounds.miny); + bounds.max.x = Math.max(bounds.max.x, bbounds.maxx); + bounds.max.y = Math.max(bounds.max.y, bbounds.maxy); + } + + // synthesize a support widget, if needed + if (process.sliceSupportEnable) { + let swidget = KIRI.newWidget(); + let sslices = swidget.slices = []; + widgets.forEach(function(widget) { + let slices = widget.slices; + while (sslices.length < slices.length) { + let sslice = KIRI.newSlice(slices[sslices.length].z); + sslice.extruder = process.sliceSupportNozzle; + sslices.push(sslice); + } + slices.forEach((slice,index) => { + if (!slice.supports) return; + if (!sslices[index].supports) { + sslices[index].supports = []; + } + sslices[index].supports.appendAll(slice.supports.map(p => { + if (p.fills) p.fills.forEach(p => p.move(widget.track.pos)); + return p.move(widget.track.pos); + })); + }); + }); + swidget.support = true; + swidget.mesh = { widget: swidget }; // fake for lookup + settings.widget[swidget.id] = { extruder: process.sliceSupportNozzle }; + widgets.push(swidget); + } + + let extruders = []; + let extcount = 0; + + // find max layers (for updates) + // generate list of used extruders for purge blocks + widgets.forEach(function(widget) { + maxLayers = Math.max(maxLayers, widget.slices.length); + let extruder = (settings.widget[widget.id] || {}).extruder || 0; + if (!extruders[extruder]) { + extruders[extruder] = {}; + extcount++; + } + }); + + let blokpos, walkpos, blok; + if (bounds.min.x < bounds.min.y) { + let dx = ((bounds.max.x - bounds.min.x) - (extcount * 10)) / 2 + 5; + blokpos = { x:bounds.min.x + dx, y: bounds.max.y + 5}; + walkpos = { x:10, y:0 }; + blok = { x:9, y:4 }; + } else { + let dy = ((bounds.max.y - bounds.min.y) - (extcount * 10)) / 2 + 5; + blokpos = { x:bounds.max.x + 5, y: bounds.min.y + dy}; + walkpos = { x:0, y:10 }; + blok = { x:4, y:9 }; + } + + // compute purge blocks + extruders = extruders.map((ext,i) => { + if (!ext) return ext; + let noz = device.extruders[i].extNozzle, + pos = {x:blokpos.x, y:blokpos.y, z:0}, + rec = { + extruder: i, + poly: BASE.newPolygon().centerSpiral(pos, blok.x, blok.y, noz*2, 3) + }; + blokpos.x += walkpos.x; + blokpos.y += walkpos.y; + return rec; + }); + + // generate purge block for given nozzle + function purge(nozzle, track, layer, start, z, using) { + if (extcount < 2) { + return start; + } + let rec = track[nozzle]; + if (rec) { + track[nozzle] = null; + if (layer.last()) layer.last().retract = true; + start = print.polyPrintPath(rec.poly.clone().setZ(z), start, layer, { + tool: using || nozzle, + open: true, + simple: true + }); + layer.last().retract = true; + return start; + } else { + console.log({already_purged: nozzle, from: track, layer}); + return start; + } + } + + // increment layer count until no widget has remaining slices + for (;;) { + // create list of mesh slice arrays with their platform offsets + for (meshIndex = 0; meshIndex < widgets.length; meshIndex++) { + let mesh = widgets[meshIndex].mesh; + if (!mesh.widget) { + continue; + } + let mslices = mesh.widget.slices; + if (mslices && mslices[layer]) { + slices.push({ + slice: mslices[layer], + offset: mesh.position || {x:0, y:0, z:0}, + widget: mesh.widget + }); + } + } + + // exit if no slices + if (slices.length === 0) { + break; + } + + // track purge blocks generated for each layer + let track = extruders.slice(); + let lastOut; + let lastExt; + + // iterate over layer slices, find closest widget, print, eliminate + for (;;) { + closest = null; + mindist = Infinity; + let order = []; + // select slices of the same extruder type first then distance + for (meshIndex = 0; meshIndex < slices.length; meshIndex++) { + sliceEntry = slices[meshIndex]; + if (sliceEntry) { + find = sliceEntry.slice.findClosestPointTo(printPoint.sub(sliceEntry.offset)); + if (find) { + let ext = sliceEntry.slice.extruder; + let lex = lastOut ? lastOut.extruder : ext; + let dst = Math.abs(find.distance); + if (ext !== lex) dst *= 10000; + order.push({dst,sliceEntry,meshIndex}); + } + } + } + order.sort((a,b) => { + return a.dst - b.dst; + }); + if (order.length) { + let find = order.shift(); + closest = find.sliceEntry; + minidx = find.meshIndex; + } + + if (!closest) { + if (sliceEntry) lastOut = sliceEntry.slice; + break; + } + // retract between widgets + if (layerout.length && minidx !== lastIndex) { + layerout.last().retract = true; + } + layerout.height = layerout.height || closest.slice.height; + slices[minidx] = null; + closest.offset.z = zoff; + // detect extruder change and print purge block + if (!lastOut || lastOut.extruder !== closest.slice.extruder) { + printPoint = purge(closest.slice.extruder, track, layerout, printPoint, closest.slice.z); + } + // output seek to start point between mesh slices if previous data + printPoint = print.slicePrintPath( + closest.slice, + printPoint.sub(closest.offset), + closest.offset, + layerout, + { + first: closest.slice.index === 0, + support: closest.widget.support + } + ); + lastOut = closest.slice; + lastExt = lastOut.ext + lastIndex = minidx; + } + + // if a declared extruder isn't used in a layer, use selected + // extruder to fill the relevant purge blocks for later support + track.forEach(ext => { + if (ext) { + printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt); + } + }); + + // if layer produced output, append to output array + if (layerout.length) output.append(layerout); + + // notify progress + layerout.layer = layer++; + update(layer / maxLayers); + + // retract after last layer + if (layer === maxLayers && layerout.length) { + layerout.last().retract = true; + } + + slices = []; + layerout = []; + lastOut = undefined; + } + }; + +})(); diff --git a/src/mode/fdm/slice.js b/src/mode/fdm/slice.js new file mode 100644 index 00000000..0a320d69 --- /dev/null +++ b/src/mode/fdm/slice.js @@ -0,0 +1,372 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + CONF = BASE.config, + FDM = KIRI.driver.FDM, + SLICER = KIRI.slicer; + + /** + * DRIVER SLICE CONTRACT + * + * Given a widget and settings object, call functions necessary to produce + * slices and then the computations using those slices. This function is + * designed to run client or server-side and provides all output via + * callback functions. + * + * @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) + */ + FDM.slice = function(settings, widget, onupdate, ondone) { + FDM.fixExtruders(settings); + let spro = settings.process, + sdev = settings.device, + update_start = Date.now(), + minSolid = spro.sliceSolidMinArea, + solidLayers = spro.sliceSolidLayers, + vaseMode = spro.sliceFillType === 'vase', + doSolidLayers = solidLayers && !vaseMode, + metadata = settings.widget[widget.id] || {}, + extruder = metadata.extruder || 0, + sliceHeight = spro.sliceHeight, + nozzleSize = sdev.extruders[extruder].extNozzle, + firstOffset = nozzleSize / 2, + shellOffset = nozzleSize, + fillOffsetMult = 1.0 - bound(spro.sliceFillOverlap, 0, 0.8), + fillSpacing = nozzleSize, + fillOffset = nozzleSize * fillOffsetMult, + sliceFillAngle = spro.sliceFillAngle, + view = widget.mesh && widget.mesh.newGroup ? widget.mesh.newGroup() : null; + + if (!(sliceHeight > 0 && sliceHeight < 100)) { + return ondone("invalid slice height"); + } + if (!(nozzleSize >= 0.01 && nozzleSize < 100)) { + return ondone("invalid nozzle size"); + } + + if (spro.firstSliceHeight === 0) { + spro.firstSliceHeight = sliceHeight; + } + + if (spro.firstSliceHeight < sliceHeight) { + DBUG.log("invalid first layer height < slice height"); + DBUG.log("reverting to slice height"); + spro.firstSliceHeight = sliceHeight; + } + + SLICER.sliceWidget(widget, { + height: sliceHeight, + minHeight: sliceHeight > spro.sliceMinHeight ? spro.sliceMinHeight : 0, + firstHeight: spro.firstSliceHeight, + view: view + }, onSliceDone, onSliceUpdate); + + function onSliceUpdate(update) { + return onupdate(0.0 + update * 0.5); + } + + function onSliceDone(slices) { + // slices = slices.filter(slice => slice.tops.length); + // remove all empty slices above part but leave below + // for multi-part (multi-extruder) setups where the void is ok + let found = false; + slices = slices.reverse().filter(slice => { + if (slice.tops.length) { + return found = true; + } else { + return found; + } + }).reverse(); + + widget.slices = slices; + + if (!slices) return; + + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + onupdate(0.5 + (from + ((index/slices.length) * (to-from))) * 0.5, msg); + } + + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(function(slice) { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + // do not hint polygin fill longer than a max span length + CONF.hint_len_max = UTIL.sqr(spro.sliceBridgeMax); + + // reset (if necessary) for solids and support projections + slices.forEach(function(slice) { + slice.extruder = extruder; + slice.invalidateFill(); + slice.invalidateSolids(); + slice.invalidateSupports(); + }); + + let supportEnabled = spro.sliceSupportEnable && spro.sliceSupportDensity > 0.0, + supportMinArea = spro.sliceSupportArea; + + // create shells and diff inner fillable areas + forSlices(0.0, 0.2, function(slice) { + let solid = ( + slice.index < spro.sliceBottomLayers || + slice.index > slices.length - spro.sliceTopLayers-1 || + spro.sliceFillSparse > 0.95 + ) && !vaseMode; + slice.doShells(spro.sliceShells, firstOffset, shellOffset, fillOffset, { + vase: vaseMode, + thin: spro.detectThinWalls + }); + if (solid) slice.doSolidLayerFill(fillSpacing, sliceFillAngle); + sliceFillAngle += 90.0; + }, "offsets"); + + // calculations only relevant when solid layers are used + if (doSolidLayers) { + forSlices(0.2, 0.34, function(slice) { + slice.doDiff(minSolid); + }, "diff"); + forSlices(0.34, 0.35, function(slice) { + slice.projectFlats(solidLayers); + slice.projectBridges(solidLayers); + }, "solids"); + forSlices(0.35, 0.5, function(slice) { + slice.doSolidsFill(fillSpacing, sliceFillAngle, minSolid); + sliceFillAngle += 90.0; + }, "solids"); + } + + // calculations only relevant when supports are enabled + if (supportEnabled) { + forSlices(0.5, 0.7, function(slice) { + slice.doSupport(spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset, spro.sliceSupportGap); + }, "support"); + forSlices(0.7, 0.8, function(slice) { + slice.doSupportFill(nozzleSize, spro.sliceSupportDensity, supportMinArea); + }, "support"); + } + + // sparse layers only present when non-vase mose and sparse % > 0 + if (!vaseMode && spro.sliceFillSparse > 0.0) { + forSlices(0.8, 1.0, function(slice) { + slice.doSparseLayerFill({ + settings: settings, + process: spro, + device: sdev, + lineWidth: nozzleSize, + spacing: fillOffset, + density: spro.sliceFillSparse, + bounds: widget.getBoundingBox(), + height: sliceHeight, + type: spro.sliceFillType + }); + }, "infill"); + } + + // let polish = spro.polishLayers; + // // experimental polishing + // if (polish) { + // let polish_layer = Math.floor(polish); + // let polish_step = Math.max(polish - polish_layer || 1, 0.25); + // widget.polish = {}; + // let px = []; + // let py = []; + // // compute x polishing slices + // SLICER.sliceWidget(widget, { + // height: nozzleSize * polish_step, + // swapX: true, + // swapY: false, + // simple: true + // }, (polish_done => { + // widget.polish.x = polish_done + // .filter(s => s.groups.length) + // .map(s => s.groups) + // .forEach(p => px.appendAll(p)); + // }), (polish_update) => { + // // console.log({polish_update}); + // }); + // // compute y polishing slices + // SLICER.sliceWidget(widget, { + // height: nozzleSize * polish_step, + // swapX: false, + // swapY: true, + // simple: true + // }, (polish_done => { + // widget.polish.y = polish_done + // .filter(s => s.groups.length) + // .map(s => s.groups) + // .forEach(p => py.appendAll(p)); + // }), (polish_update) => { + // // console.log({polish_update}); + // }); + // // apply polishing finishes to layers + // forSlices(1.0, 1.0, (slice) => { + // if (polish_layer >= 2) { + // let sai = (slice.index - polish_layer); + // if (sai % (polish_layer-1) !== 0) { + // return; + // } + // } + // if (slice.index >= polish_layer) { + // let sd = slice; + // for (let i=0; i { + // + // let polys = []; + // pa.forEach(p => { + // // rotate and test for self-intersection (points under model) + // p.ensureXY(); + // let ox = p._aligned == 'yz' ? 0.1 : 0; + // let oy = p._aligned == 'yz' ? 0 : 0.1; + // p.forEachPoint(pt => { + // let int = p.intersections( + // pt.offset(ox,oy,0), + // pt.offset(ox*10000,oy*10000,0)); + // if (int.length > 0) { + // pt._under = true; + // } + // }); + // p.restoreXY(); + // + // let lastp = undefined; + // let poly = []; + // // look for qualifying segments + // p.forEachSegment((p1, p2) => { + // // eliminate segments that projected up + // // intersect with the polygon (bottom faces) + // if (p1._under || p2._under) { + // return; + // } + // // skip when both below layer range + // if (p1.z < zb && p2.z < zb) { + // return; + // } + // // skip when both above layer range + // if (p1.z > zt && p2.z > zt) { + // return; + // } + // // skip vertical + // if (p1.x === p2.x && p1.y === p2.y) { + // return; + // } + // // skip horizontal + // // if (p1.z === p2.z) { + // // return; + // // } + // // order points lowest to highest + // let swap = false; + // if (p1.z > p2.z) { + // let t = p2; + // p2 = p1; + // p1 = t; + // swap = true; + // } + // let trimlo = false; + // let trimhi = false; + // if (p1.z < zb) { + // trimlo = true; + // } + // if (p2.z > zt) { + // trimhi = true; + // } + // let xaxis = p1.x === p2.x; + // if (xaxis) { + // p1 = BASE.newPoint(p1.y,p1.z,p1.x); + // p2 = BASE.newPoint(p2.y,p2.z,p2.x); + // } else { + // p1 = BASE.newPoint(p1.x,p1.z,p1.y); + // p2 = BASE.newPoint(p2.x,p2.z,p2.y); + // } + // let slope = BASE.newSlope(p1, p2); + // let angle = slope.angle; + // if (angle > 80 && angle < 100) { + // return; + // } + // let len = p1.distTo2D(p2); + // let np1 = p1; + // if (trimlo) { + // let zunder = zb - p1.y; + // let zover = p2.y - zb; + // let zdelt = p2.y - p1.y; + // let pct = zunder / zdelt; + // np1 = p1.follow(slope, len * pct); + // } + // if (trimhi) { + // let zunder = zt - p1.y; + // let zover = p2.y - zt; + // let zdelt = p2.y - p1.y; + // let pct = zover / zdelt; + // p2 = p2.follow(slope.invert(), len * pct); + // } + // p1 = np1; + // if (xaxis) { + // p1 = BASE.newPoint(p1.z,p1.x,p1.y); + // p2 = BASE.newPoint(p2.z,p2.x,p2.y); + // } else { + // p1 = BASE.newPoint(p1.x,p1.z,p1.y); + // p2 = BASE.newPoint(p2.x,p2.z,p2.y); + // } + // if (!lastp) { + // poly.push(p1); + // poly.push(p2); + // } else if (p1.isMergable2D(lastp)) { + // // } else if (p1.isEqual(lastp)) { + // poly.push(p2); + // cont++; + // } else if (poly.length) { + // polys.push(poly); + // poly = [p1, p2]; + // } + // lastp = p2; + // }); + // if (poly.length) { + // polys.push(poly); + // } + // }); + // pout.push(polys); + // polys = []; + // }); + // + // if (pout.length && slice.tops.length) { + // slice.tops[0].polish = { + // x: pout[0] + // .map(a => BASE.newPolygon(a).setOpen()) + // // .filter(p => p.perimeter() > nozzleSize) + // , + // y: pout[1] + // .map(a => BASE.newPolygon(a).setOpen()) + // // .filter(p => p.perimeter() > nozzleSize) + // }; + // } + // } + // }); + // } + + // report slicing complete + ondone(); + } + + } + + function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); + } + +})(); diff --git a/src/mode/sla/client.js b/src/mode/sla/client.js new file mode 100644 index 00000000..7a9f01fc --- /dev/null +++ b/src/mode/sla/client.js @@ -0,0 +1,188 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + SLA = KIRI.driver.SLA; + + SLA.init = function(kiri, api) { + api.event.on("mode.set", (mode) => { + if (mode === 'SLA') { + api.ui.act.preview.classList.add('hide'); + } else { + api.ui.act.preview.classList.remove('hide'); + } + }); + }; + + SLA.sliceRender = function(widget) { + // legacy debug + return; + + widget.slices.forEach(slice => { + let layers = slice.layers, + outline = layers.outline, + support = layers.support; + + if (slice.solids.unioned) { + // console.log('solid', slice.index) + slice.solids.unioned.forEach(poly => { + poly = poly.clone(true);//.move(widget.track.pos); + outline.poly(poly, 0x010101, true); + outline.solid(poly, 0x0099cc); + }); + } else if (slice.tops) { + // console.log('top', slice.index) + slice.tops.forEach(top => { + let poly = top.poly;//.clone(true).move(widget.track.pos); + outline.poly(poly, 0x010101, true, false); + outline.solid(poly, 0xfcba03); + }); + } + + if (slice.supports) { + // console.log('support', slice.index) + slice.supports.forEach(poly => { + //poly = poly.clone(true).move(widget.track.pos); + support.poly(poly, 0x010101, true, false); + support.solid(poly, 0xfcba03); + }); + } + + slice.renderDiff(); + slice.renderSolidOutlines(); + + outline.renderAll(); + support.renderAll(); + }); + } + + SLA.printRender = function(print) { + let widgets = print.widgets, + settings = print.settings, + device = settings.device, + process = settings.process; + + for (let index=0; ; index++) { + let layer = KIRI.newLayer(print.group); + let count = 0; + + widgets.forEach(widget => { + let slice = widget.slices[index]; + if (!slice) { + return; + } + count++; + let polys = slice.solids.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + polys.forEach(poly => { + poly = poly.clone(true).move(widget.track.pos); + layer.poly(poly, 0x777777, true); + layer.solid(poly, 0x0099cc); + }); + }); + + layer.renderSolid(); + layer.render(); + + if (count === 0) { + // TODO fix with contract for exposing layer count + // hack uses expected gcode output array in print object + print.output = print.printView; + return; + } + + print.printView.push(layer); + } + } + + SLA.printDownload = function(print) { + let { API, lines, done } = print.sla; + let filename = `print-${new Date().getTime().toString(36)}`; + + API.ajax("/kiri/output-sla.html", html => { + API.ui.print.innerHTML = html; + + let printset = print.settings, + process = printset.process, + device = printset.device, + print_sec = (process.slaBaseLayers * process.slaBaseOn) + + (lines.length - process.slaBaseLayers) * process.slaLayerOn; + + // add peel lift/drop times to total print time + for (let i=0; i { saveFile(API, done.file, ".photon") }; + break; + case 'Anycubic.Photon.S': + download.innerText += " .photons"; + download.onclick = () => { saveFile(API, done.file, ".photons") }; + break; + } + + let canvas = $('print-canvas'); + let ctx = canvas.getContext('2d'); + let img = ctx.createImageData(done.height, done.width); + let imgDV = new DataView(img.data.buffer); + let range = $('print-range'); + range.value = 0; + range.min = 0; + range.max = lines.length - 1; + range.oninput = function() { + let lineDV = new DataView(lines[range.value]); + for (let i=0; i { - if (mode === 'SLA') { - api.ui.act.preview.classList.add('hide'); - } else { - api.ui.act.preview.classList.remove('hide'); - } - }); - } - - /** - * DRIVER SLICE CONTRACT - runs in worker - * - * @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 slice(settings, widget, onupdate, ondone) { - let process = settings.process, - device = settings.device, - work_total, - work_remain; - - if (legacy && !self.OffscreenCanvas) { - return ondone("browser lacks support for OffscreenCanvas",true); - } - - // calculate % complete and call onupdate() - function doupdate(work, msg) { - onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg); - work_remain -= work; - } - - // for each slice, perform a function and call doupdate() - function forSlices(slices, work, fn, msg) { - slices.forEach(function(slice,index) { - fn(slice,index); - doupdate(work / slices.length, msg) - }); - } - - let b64 = atob(self.worker.snap); - let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); - let img = new png.PNG(); - img.parse(bin, (err, data) => { - preview = img; - previewSmall = samplePNG(img, 200, 125); - previewLarge = samplePNG(img, 400, 300); - }); - let height = process.slaSlice || 0.05; - - SLICER.sliceWidget(widget, { - height: height, - add: !process.slaOpenTop - }, function(slices) { - // hold onto last (empty) slice - let last = slices[slices.length-1]; - // remove empty slices - slices = widget.slices = slices.filter(slice => slice.tops.length); - if (!process.slaOpenTop) { - // re-add last empty slice for open top - slices.push(last); - } - // prepend raft layers to slices array - if (process.slaSupportEnable && process.slaSupportLayers) { - let layers = process.slaSupportLayers, - zoff = height / 2, - snew = [], - polys = [], - gap = process.slaSupportGap, // gap layers above raft - grow = height, // union per layer expand - off = 1 - (layers * grow); // starting union offset from part - let outer = slices.forEach(slice => { - // poly.clone prevents inner voids from forming - polys.appendAll(slice.tops.map(t => t.poly.clone())); - }); - // p.clone prevents inner voids from forming - let union = POLY.union(polys).map(p => p.clone()); - let expand = POLY.expand(union, off, zoff, [], 1); - let lastraft; - for (let s=0; s { - slice.tops.push(newTop(u.clone(true).setZ(zoff))); - }); - expand = POLY.expand(expand, grow, zoff, [], 1); - lastraft = slice; - } - snew.push(slice); - zoff += height; - } - // compensate for midline start - zoff -= height / 2; - // replace slices with new appended array - slices = widget.slices = snew.concat(slices.map(s => { - s.tops.forEach(t => t.poly.setZ(s.z + zoff)); - s.index += snew.length; - s.z += zoff; - return s; - })); - // annotate widget for support generation - widget.union = union; - widget.lastraft = lastraft; - } - // re-connect slices into linked list for island/bridge projections - for (let i=1; i { return t+v }); - work_remain = work_total; - forSlices(slices, 5, (slice,index) => { - if (process.slaShell) { - slice.doShells(2, 0, process.slaShell); - } else { - slice.doShells(1, 0); - } - }, "slice"); - forSlices(slices, 10, (slice) => { - if (slice.synth) return; - slice.doDiff(0.000001, 0.005, !process.slaOpenBase); - }, "delta"); - if (solidLayers) { - forSlices(slices, 10, (slice) => { - if (slice.synth) return; - slice.projectFlats(solidLayers); - slice.projectBridges(solidLayers); - }, "project"); - forSlices(slices, 10, (slice) => { - if (slice.synth) return; - slice.doSolidsFill(undefined, undefined, 0.001); - let traces = POLY.nest(POLY.flatten(slice.gatherTraces([]))); - let trims = slice.solids.trimmed || []; - traces.appendAll(trims); - let union = POLY.union(traces); - slice.solids.unioned = union; - }, "solid"); - } else { - forSlices(slices, 10, (slice) => { - if (slice.synth) return; - slice.solids.unioned = slice.gatherTopPolys([]); - }, "solid"); - } - if (process.slaFillDensity && process.slaShell) { - fill_cache = []; - forSlices(slices, 60, (slice) => { - if (slice.synth) return; - fillPolys(slice, settings); - }, "infill"); - } - if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) { - computeSupports(widget, process, progress => { - doupdate(100 * progress, "support"); - }); - } - ondone(); - }, function(update) { - return onupdate(0.0 + update * 0.25); - }); - }; - - function computeSupports(widget, process, progress) { - let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0), - perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0), - slices = widget.slices, - length = slices.length, - tot_mass = 0, // total widget "mass" - tot_bear = 0; // total "mass-bearing" area - - let ops = slices.filter(slice => !slice.synth); - - // compute total "mass" by slice - ops.forEach(slice => { - slice.mass = slice.solids.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0); - if (slice.up && slice.up.bridges) { - slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0); - slice.bear_up = slice.up.bridges; - tot_bear += slice.bear; - } else { - slice.bear = 0; - } - tot_mass += slice.mass; - }); - - let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity); - - // console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear}); - - let first; - ops.slice().map((s,i) => { - if (!first && s.bear) { - s.ord_first = first = true; - } - // 30x lowest to 1x highest - s.ord_weight = Math.pow(30,((ops.length - i) / ops.length)); - return s; - }).sort((a,b) => { - if (a.ord_first) return -1; - if (b.ord_first) return 1; - return (b.bear * b.ord_weight) - (a.bear * a.ord_weight); - }).forEach((slice, index) => { - slice.ord_bear = index; - }) - - let rem_mass = tot_mass, - rem_bear = tot_bear; - - // compute remaining mass, bearing surface, for each slice - let run, runLast, runCount = 0, runList = []; - ops.forEach(slice => { - slice.rem_mass = rem_mass; - slice.rem_bear = rem_bear; - // slice.can_bear = slice.mass * mass_per_bear; - slice.can_bear = slice.bear * mass_per_bear; - rem_mass -= slice.mass; - rem_bear -= slice.bear; - }); - - let ord = ops.sort((a,b) => { - return a.ord_bear - b.ord_bear; - }); - - rem_mass = tot_mass; - rem_bear = tot_bear; - - // in order of load bearing capability, select layer - // and recompute the requirements on the slices below - for (let i=0; iops[0].index; j--) { - slices[j].rem_mass -= bearing; - } - - // remove total mass left to bear - rem_mass -= bearing; - slice.can_emit = true; - if (rem_mass <= 0) { - // console.log({break: i, of: ord.length}); - break; - } - } - - let seq = 0, - seqLast = 0, - spacing = (1 - process.slaSupportDensity) * 10, - size = Math.bound(process.slaSupportSize / 2, 0.25, 1); - - // compute and project support pillars - slices.forEach((slice,index) => { - if (slice.can_emit) { - if (seq === 0 || slice.index - seqLast > 5) { - slice.bear_up.map(p => { - return p.clone(true).setZ(slice.z); - }).forEach(p => { - projectSupport(process, slice, p, size, spacing); - }); - seq++; - } else if (seq > 5) { - seq = 0; - } else { - seq++; - } - seqLast = slice.index; - } - progress(1 / slices.length); - }); - - // union support pillars - slices.forEach(slice => { - if (slice.supports) { - slice.supports = POLY.union(slice.supports,0); - } - }); - } - - function projectSupport(process, slice, poly, size, spacing) { - let flat = poly.circularityDeep() > 0.1, - arr = [ poly ]; - - // insetting polys produces arrays. consume til gone - while (arr.length) { - poly = arr.shift(); - let out = [], - seg = [], - per = poly.perimeter(), - crit = 0, - polys = poly.clone(true).flattenTo([]); - - polys.forEach(p => p.forEachSegment((p1, p2) => { - let rec = {dist: p1.distTo2D(p2), p1, p2}; - if (rec.dist >= spacing || slice.ord_first) crit++; - seg.push(rec); - })); - - seg.sort((a,b) => { - return b.dist - a.dist; - }); - - // emit all critical (long) segments. min 3 - while (seg.length && (crit > 0 || out.length < 3)) { - let {dist, p1, p2} = seg.shift(); - if (dist >= spacing) { - // spaced along line - let num = Math.ceil(dist / spacing) + 1, - step = dist / num, - pt = p1; - while (num-- >= 0) { - out.push(pt); - pt = pt.offsetPointTo(p2, step); - } - crit--; - } else { - // line midpoint - out.push(p1.offsetPointTo(p2, dist / 2)); - } - } - - // drop points too close to other pillars - if (!slice.ord_first) - drop: for (let i=0; i { - if (out[i] && out[i].distTo2D(rec.point) <= size) { - out[i] = null; - } - }); - } - - // mark support pillar for each point - out.filter(p => p !== null) - .forEach(p => { - let track = projectPillar(process, slice, p, size, []); - // find stunted pillars terminating on a face (not raft or merged) - if (track.length && !(track.max || track.synth || track.merged)) { - // remove stunted pillar - track.forEach(rec => { - let sp = rec.slice.supports; - let rp = sp.indexOf(rec.pillar); - if (rp >= 0) sp.splice(rp,1); - }); - } - }); - - // inset polygon for flat area support - if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr); - } - } - - function projectPillar(process, slice, point, size, track) { - if (!slice.supports) slice.supports = []; - if (!slice.pillars) slice.pillars = []; - - let points = process.slaSupportPoints, - pillar = newPolygon() - .centerCircle(point, size/2, points, true) - .setZ(slice.z), - max = process.slaSupportSize, - inc = process.slaSlice/2, - end = false, // center intersects - over = [], // overlapping points - safe = [], // non-overlapping points - low = slice.index < process.slaSupportGap + process.slaSupportLayers; - - track.min = track.min ? Math.min(track.min, size) : size; - track.max = (track.max ? true : false) || size >= max; - track.synth = track.synth || slice.synth; - - slice.tops.forEach(t => { - if (track.length > 3 && point.isInPolygon(t.poly)) { - end = true; - } - pillar.points.forEach(p => { - let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001); - if (isin) { - over.push(p); - } else { - safe.push(p); - } - }); - }); - - if (end) { - // backtrack shrinking pillars to point if - // not landing on the base and size is max'd - if (!slice.synth && track.max) { - let nusize = track.min; - while (track.length) { - let prec = track.pop(), - npil = newPolygon() - .centerCircle(prec.point, nusize/2, points, true) - .setZ(prec.slice.z), - spos = prec.slice.supports.indexOf(prec.pillar); - // if we find our old pillar, replace - if (spos >= 0) { - prec.slice.supports[spos] = npil; - nusize += inc; - } - if (nusize > prec.size) { - break; - } - } - } - return track; - } - - let nextpoint = point; - - if (over.length) { - // move toward average of safe (non-overlapping) points - if (safe.length) { - let x = 0, y = 0; - safe.forEach(p => { x += p.x; y += p.y }); - x /= safe.length; - y /= safe.length; - nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc); - } - // once max'ed out, can only shrink in size - if (track.max) { - size -= inc; - } - } else if (!track.max || low) { - size += inc; - if (low) max += (process.slaSupportGap * process.slaSlice * 2); - } - - if (size < track.min) { - return track; - } - - // let close = []; - // for (let i=0; i { - // newp.x += p.x; - // newp.y += p.y; - // }); - // newp.x /= (close.length + 1); - // newp.y /= (close.length + 1); - // nextpoint = point.offsetPointTo(newp, inc); - // } - - slice.supports.push(pillar); - slice.pillars.push({point, pillar, size}); - track.push({slice, point, pillar, size}); - if (slice.down) { - projectPillar(process, slice.down, nextpoint, Math.min(size, max), track); - } - return track; - } - - function fillPolys(slice, settings) { - let process = settings.process, - device = settings.device, - polys = slice.solids.unioned, - bounds = settings.bounds, - width = bounds.max.x - bounds.min.x, - depth = bounds.max.y - bounds.min.y, - max = Math.max(width,depth), - seq = Math.round(process.slaFillLine / process.slaSlice), - linew = process.slaFillLine, - units_w = (width / linew) * process.slaFillDensity, - units_d = (depth / linew) * process.slaFillDensity, - step_x = width / units_w, - step_y = depth / units_d, - start_x = -(width / 2), - start_y = -(depth / 2), - end_x = width / 2, - end_y = depth / 2, - fill = []; - - let seq_i = Math.floor(slice.index / seq), - seq_c = seq_i % 4, - cached = fill_cache[seq_c]; - - if (!cached && seq_c !== 1) - for (let x=start_x; x p.clone(true).setZ(slice.z)); - } - - fill = POLY.trimTo(fill, slice.tops.map(t => t.poly)); - fill = POLY.union(slice.solids.unioned.appendAll(fill)); - - slice.solids.unioned = fill; - } + if (SLA.legacy) console.log("SLA Driver in Legacy Mode"); /** * DRIVER PRINT CONTRACT - runs in worker @@ -596,855 +30,4 @@ update(1); }; - /** - * DRIVER PRINT CONTRACT - runs in worker - * @param {Object} print state object - * @param {Function} online streaming reply - * @param {Function} ondone last reply - */ - function printExport(print, online, ondone) { - let widgets = print.widgets, - settings = print.settings, - device = settings.device, - process = settings.process, - output = print.output, - layermax = 0, - width = 2560, - height = 1440, - width2 = width/2, - height2 = height/2, - scaleX = width / device.bedWidth, - scaleY = height / device.bedDepth, - mark = Date.now(), - layers = process.slaAntiAlias || 1, - masks = [], - images = [], - slices = [], - legacyMode = legacy || layers > 1, - part1 = legacyMode ? 0.25 : 0.85, - part2 = legacyMode ? 0.75 : 0.15; - - let d = 8 / layers; - for (let i=0; i { - layermax = Math.max(widget.slices.length); - }); - - let render = legacyMode ? plugin.renderLayer : plugin.renderLayerWasm; - - // generate layer bitmaps - // in wasm mode, rle layers generated here, too - for (let index=0; index < layermax; index++) { - let param = { index, width, height, widgets, scaleX, scaleY, masks }; - let {image, layers, end} = render(param); - images.push(image); - slices.push(layers); - // transfer images to browser main - image = image.buffer; - online({ - progress: (index / layermax) * part1, - message: "image_gen", - data: image - },[image]); - if (end) break; - } - - let exp_func; - - switch (device.deviceName) { - case 'Anycubic.Photon': - exp_func = generatePhoton; - break; - case 'Anycubic.Photon.S': - exp_func = generatePhotons; - break; - } - - let file = exp_func(print, { - width: width, - height: height, - small: previewSmall.data, - large: previewLarge.data, - lines: images, - slices: slices - }, (progress, message) => { - online({progress: progress * part2 + part1, message}); - }); - ondone({ - width: width, - height: height, - file: file - },[file]); - - console.log('print.export', Date.now() - mark); - }; - - // runs in browser main - function sliceRender(widget) { - // legacy debug - return; - - widget.slices.forEach(slice => { - let layers = slice.layers, - outline = layers.outline, - support = layers.support; - - if (slice.solids.unioned) { - // console.log('solid', slice.index) - slice.solids.unioned.forEach(poly => { - poly = poly.clone(true);//.move(widget.track.pos); - outline.poly(poly, 0x010101, true); - outline.solid(poly, 0x0099cc); - }); - } else if (slice.tops) { - // console.log('top', slice.index) - slice.tops.forEach(top => { - let poly = top.poly;//.clone(true).move(widget.track.pos); - outline.poly(poly, 0x010101, true, false); - outline.solid(poly, 0xfcba03); - }); - } - - if (slice.supports) { - // console.log('support', slice.index) - slice.supports.forEach(poly => { - //poly = poly.clone(true).move(widget.track.pos); - support.poly(poly, 0x010101, true, false); - support.solid(poly, 0xfcba03); - }); - } - - slice.renderDiff(); - slice.renderSolidOutlines(); - - outline.renderAll(); - support.renderAll(); - }); - } - - // runs in browser main - function printRender(print) { - let widgets = print.widgets, - settings = print.settings, - device = settings.device, - process = settings.process; - - for (let index=0; ; index++) { - let layer = KIRI.newLayer(print.group); - let count = 0; - - widgets.forEach(widget => { - let slice = widget.slices[index]; - if (!slice) { - return; - } - count++; - let polys = slice.solids.unioned; - if (!polys) polys = slice.tops.map(t => t.poly); - if (slice.supports) polys.appendAll(slice.supports); - polys.forEach(poly => { - poly = poly.clone(true).move(widget.track.pos); - layer.poly(poly, 0x777777, true); - layer.solid(poly, 0x0099cc); - }); - }); - - layer.renderSolid(); - layer.render(); - - if (count === 0) { - // TODO fix with contract for exposing layer count - // hack uses expected gcode output array in print object - print.output = print.printView; - return; - } - - print.printView.push(layer); - } - } - - // runs in browser main - function printDownload(print) { - let { API, lines, done } = print.sla; - let filename = `print-${new Date().getTime().toString(36)}`; - - API.ajax("/kiri/output-sla.html", html => { - API.ui.print.innerHTML = html; - - let printset = print.settings, - process = printset.process, - device = printset.device, - print_sec = (process.slaBaseLayers * process.slaBaseOn) + - (lines.length - process.slaBaseLayers) * process.slaLayerOn; - - // add peel lift/drop times to total print time - for (let i=0; i { saveFile(API, done.file, ".photon") }; - break; - case 'Anycubic.Photon.S': - download.innerText += " .photons"; - download.onclick = () => { saveFile(API, done.file, ".photons") }; - break; - } - - let canvas = $('print-canvas'); - let ctx = canvas.getContext('2d'); - let img = ctx.createImageData(done.height, done.width); - let imgDV = new DataView(img.data.buffer); - let range = $('print-range'); - range.value = 0; - range.min = 0; - range.max = lines.length - 1; - range.oninput = function() { - let lineDV = new DataView(lines[range.value]); - for (let i=0; i 1) { - let d = 8 / subcount; - for (let i=0; i { - let count = line.length; - let lineDV = new DataView(line.buffer); - let bits = new Uint8Array(line.length); - let bitsDV = new DataView(bits.buffer); - let subs = [{ data: bits, view: bitsDV }]; - for (let sl=1; sl { - progress(pro * 0.4 + 0.4, "layer_encode"); - })); - } else { - let codedlen = slices.reduce((t,l) => { - return t + l.reduce((t,a) => { - return t + a.length - }, 0); - }, 0); - coded = { - layers: slices.map(slice => { return { sublayers: slice }}), - length: codedlen - }; - } - - let codelen = coded.layers.length; - let buflen = 3000 + coded.length + (codelen * subcount * 28) + small.byteLength + large.byteLength; - let filebuf = new ArrayBuffer(buflen); - let filedat = new self.DataWriter(new DataView(filebuf)); - let printtime = (process.slaBaseLayers * process.slaBaseOn) + - (coded.layers.length - process.slaBaseLayers) * process.slaLayerOn; - - filedat.writeU32(0x1900fd12); // header - filedat.writeU32(2,true); // version - filedat.writeF32(68.04, true); // bed x - filedat.writeF32(120.96, true); // bed y - filedat.writeF32(150.0, true); // bed z - filedat.skip(12); // padding - filedat.writeF32(process.slaSlice, true); // layer height - filedat.writeF32(process.slaLayerOn, true); // default lamp on - filedat.writeF32(process.slaBaseOn, true); // base lamp on - filedat.writeF32(process.slaLayerOff, true); // lamp off - filedat.writeU32(process.slaBaseLayers, true); // base layers - filedat.writeU32(1440, true); // device x - filedat.writeU32(2560, true); // device y - let hirez = filedat.skip(4); // hirez preview address filled pater - let layerpos = filedat.skip(4); // layer data address filled later - filedat.writeU32(codelen, true); - let lorez = filedat.skip(4); // hirez preview address filled later - filedat.writeU32(printtime, true); // print time seconds - filedat.writeU32(1, true); // projection type (1=lcd, 0=cast) - let proppos = filedat.skip(4); // print properties address filled later - let proplen = filedat.skip(4); // print properties length filled later - filedat.writeU32(subcount, true); // AA level (sub layers) - filedat.writeU16(0x00ff, true); // light pwm (TODO); - filedat.writeU16(0x00ff, true); // light pwm bottom (TODO); - - let propstart = filedat.pos; - filedat.view.setUint32(proppos, filedat.pos, true); - // write print properties - filedat.writeF32(process.slaBasePeelDist, true); - filedat.writeF32(process.slaBasePeelLiftRate * 60 , true); - filedat.writeF32(process.slaPeelDist, true); - filedat.writeF32(process.slaPeelLiftRate * 60 , true); - filedat.writeF32(process.slaPeelDropRate * 60, true); - filedat.writeF32(0, true); // volume of used - filedat.writeF32(0, true); // weight of used - filedat.writeF32(0, true); // cost of used - filedat.writeF32(0, true); // bottom off delay time - filedat.writeF32(0, true); // light off delay time - filedat.writeU32(process.slaBaseLayers, true); - filedat.writeF32(0, true); // p1 ? - filedat.writeF32(0, true); // p2 ? - filedat.writeF32(0, true); // p3 ? - filedat.writeF32(0, true); // p4 ? - filedat.view.setUint32(proplen, filedat.pos - propstart, true); - - filedat.view.setUint32(layerpos, filedat.pos, true); - // write layer headers - let layers = coded.layers; - let layerat = []; - - for (let sc=0; sc { - let count = line.length / 4; - let bits = new Uint8Array(line.length / 4); - let bitsDV = new DataView(bits.buffer); - let lineDV = new DataView(line.buffer); - // reduce RGB to R = 0||1 - for (let i = 0; i < count; i++) { - // defeat anti-aliasing for the moment - bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0); - } - progress(index / conf.lines.length); - return { subs: [{ - exposureTime: process.slaLayerOn, - data: bits - }] }; - }); - coded = encodeLayers(converted, "photons"); - } else { - let codedlen = slices.reduce((t,l) => { - return t + l.reduce((t,a) => { - return t + a.length - }, 0); - }, 0); - coded = { - layers: slices.map(slice => { return { sublayers: slice }}), - length: codedlen - }; - } - - let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount); - let filedat = new DataView(filebuf); - let filePos = 0; - - filedat.setUint32 (0, 2, false); - filedat.setUint32 (4, 3227560, false); - filedat.setUint32 (8, 824633720, false); - filedat.setUint16 (12, 10, false); - filedat.setFloat64(14, process.slaSlice, false); - filedat.setFloat64(22, process.slaLayerOn, false); - filedat.setFloat64(30, process.slaLayerOff, false); - filedat.setFloat64(38, process.slaBaseOn, false); - filedat.setUint32 (46, process.slaBaseLayers, false); - filedat.setFloat64(50, process.slaPeelDist, false); - filedat.setFloat64(58, process.slaPeelLift, false); - filedat.setFloat64(66, process.slaPeelDrop, false); - filedat.setFloat64(74, 69420, false); - filedat.setUint32 (82, 224, false); - filedat.setUint32 (86, 42, false); - filedat.setUint32 (90, 168, false); - filedat.setUint32 (94, 10, false); - filedat.setUint32 (75362, layerCount, false); - - filePos = 75366; - for (let i = 0; i < layerCount; i++) { - let layer = coded.layers[i], - sublayer = layer.sublayers[0], - numbytes = sublayer.length; - - filedat.setUint32 (filePos + 0, 69420, false); - filedat.setFloat64(filePos + 4, 0); - filedat.setUint32 (filePos + 12, height, false); - filedat.setUint32 (filePos + 16, width, false); - filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false); - filedat.setUint32 (filePos + 24, 2684702720, false); - filePos += 28; - for (let j = 0; j < numbytes; j++) { - filedat.setUint8(filePos + j, sublayer[j], false); - } - filePos += numbytes; - progress((i / layerCount) / 2 + 0.5); - } - - return filebuf; - } - - function encodeLayers(input, type, progress) { - let layers = [], length = 0, total = 0, count = 0; - input.forEach(layer => { - layer.subs.forEach(sub => total++); - }); - for (let index = 0; index < input.length; index++) { - let subs = input[index].subs, - sublayers = [], - sublength = 0; - for (let subindex = 0; subindex < subs.length; subindex++) { - let data = subs[subindex].data; - let encoded = rleEncode(data, type); - sublength += encoded.length; - sublayers.push(encoded); - if (progress) progress(count++/total); - if (type == "photons") break; - } - length += sublength; - layers.push({ - sublength, - sublayers - }); - } - return { length, layers }; - } - - function rleEncode(data, type) { - let maxlen = (type === 'photons') ? 128 : 125, - color = data[0], - runlen = 1, - output = []; - for (let index = 1; index < data.length; index++) { - let newColor = data[index]; - if (newColor !== color) { - output.push(rleByte(color, runlen, type)); - color = newColor; - runlen = 1; - } else { - if (runlen === maxlen) { - output.push(rleByte(color, runlen, type)); - runlen = 1; - } else { - runlen++; - } - } - } - if (runlen > 0) { - output.push(rleByte(color, runlen, type)); - } - return output; - } - - function rleByte(color, length, type) { - switch (type) { - case 'photon': - return (length & 0x7f) | ((color << 7) & 0x80); - case 'photons': - length--; - return (length & 1 ? 128 : 0) | - (length & 2 ? 64 : 0) | - (length & 4 ? 32 : 0) | - (length & 8 ? 16 : 0) | - (length & 16 ? 8 : 0) | - (length & 32 ? 4 : 0) | - (length & 64 ? 2 : 0) | color; - } - } - - function rleDecode(data, type) { - let bytes = []; - if (type === 'photon') { - for (let i = 0; i < data.length; i++) { - let val = data[i], - color = val >> 7, - count = val & 0x7f; - for (let j = 0; j < count; j++) { - bytes.push(color); - } - } - } else { - for (let i = 0; i < data.length; i++) { - let val = data[i], - color = val & 1, - count = - ((val & 128 ? 1 : 0) | - (val & 64 ? 2 : 0) | - (val & 32 ? 4 : 0) | - (val & 16 ? 8 : 0) | - (val & 8 ? 16 : 0) | - (val & 4 ? 32 : 0) | - (val & 2 ? 64 : 0)) + 1; - for (let j = 0; j < count; j++) { - bytes.push(color); - } - } - } - return bytes; - } - - function pixAt(png,x,y) { - let idx = (x + png.width * y) * 4; - let dat = png.data; - return [ - dat[idx++], - dat[idx++], - dat[idx++], - dat[idx++] - ]; - } - - function averageBlock(png,x1,y1,x2,y2) { - let val = [0, 0, 0, 0], count = 0, x, y, z, v2; - for (x=x1; x 4/3) { - div = png.height / tw; - xoff = Math.round((png.width - (th * div)) / 2); - yoff = 0; - } else { - div = png.width / th; - xoff = 0; - yoff = Math.round((png.height - (tw * div)) / 2); - } - - for (let y=0; y png.height) continue; - ey = Math.round((y+1) * div + yoff); - for (let x=0; x png.width) continue; - ex = Math.round((x+1) * div + xoff); - bidx = (y * th + x) * 4; - pixval = averageBlock(png,dx,dy,ex,ey); - buf[bidx+0] = pixval[0]; - buf[bidx+1] = pixval[1]; - buf[bidx+2] = pixval[2]; - buf[bidx+3] = pixval[3]; - } - } - - return {width, height, data:buf, png}; - } - - // write out a thumbnail image - function writePhotonImage(preview, writer) { - let data = new Uint8Array(preview.data), len = data.byteLength; - writer.writeU32(preview.width, true); - writer.writeU32(preview.height, true); - let hpos = writer.skip(4); - writer.writeU32(len/2, true); - writer.view.setUint32(hpos, writer.pos, true); - let pos = 0; - while (pos < len) { - let r = data[pos++], - g = data[pos++], - b = data[pos++], - a = data[pos++], - v = (((r/4)&0x1f) << 11) | - (((g/4)&0x1f) << 6) | - (((b/4)&0x1f) << 0) ; - writer.writeU16(v, true); - } - } - - // load renderer code in worker context only - if (!self.window) { - fetch('/wasm/kiri-sla.wasm') - .then(response => response.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes, { - env: { - reportf: (a,b) => { console.log('[f]',a,b) }, - reporti: (a,b) => { console.log('[i]',a,b) } - } - })) - .then(results => { - let {module, instance} = results; - let {exports} = instance; - let heap = new Uint8Array(exports.memory.buffer); - self.wasm = { - heap, - memory: exports.memory, - render: exports.render, - rle_encode: exports.rle_encode - }; - }); - - // new WebAssembly rasterizer - plugin.renderLayerWasm = function renderLayer(params) { - let { width, height, index, widgets, scaleX, scaleY, masks } = params; - let width2 = width / 2, height2 = height / 2; - let array = []; - let count = 0; - let wasm = self.wasm; - - function scaleMovePoly(poly) { - let points = poly.points; - let bounds = poly.bounds = BASE.newBounds(); - for (let i=0, il=points.length; i { - let slice = widget.slices[index]; - if (slice) { - if (slice.synth) count++; - let polys = slice.solids.unioned; - if (!polys) polys = slice.tops.map(t => t.poly); - if (slice.supports) polys.appendAll(slice.supports); - array.appendAll(polys.map(poly => { - return poly.clone(true).move(widget.track.pos); - })); - count += polys.length; - } - }); - - let imagelen = width * height; - let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen); - writer.writeU16(width, true); - writer.writeU16(height, true); - writer.writeU16(array.length, true); - - // scale and move all polys to fit in rendered platform coordinates - for (let i=0, il=array.length; i { - let slice = widget.slices[index]; - if (slice) { - // prevent premature exit on empty synth slice - if (slice.synth) count++; - let polys = slice.solids.unioned; - if (!polys) polys = slice.tops.map(t => t.poly); - if (slice.supports) polys.appendAll(slice.supports); - polys.forEach(poly => { - poly.move(widget.track.pos); - ctx.beginPath(); - polyout(poly.setClockwise(), ctx, opt); - if (poly.inner) { - poly.inner.forEach(inner => { - polyout(inner.setCounterClockwise(), ctx, opt); - }); - } - ctx.fill(); - count++; - }); - } else { - // console.log({no_slice_at: index}) - } - }); - let data = ctx.getImageData(0,0,height,width).data; - // reduce RGBA to R - let red = new Uint8ClampedArray(data.length / 4); - for (let i=0; i { - if (i === 0) { - ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); - } else { - ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); - } - }, true); - ctx.closePath(); - } - - } - })(); diff --git a/src/mode/sla/export.js b/src/mode/sla/export.js new file mode 100644 index 00000000..f0b0a104 --- /dev/null +++ b/src/mode/sla/export.js @@ -0,0 +1,627 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + SLA = KIRI.driver.SLA; + + /** + * DRIVER PRINT CONTRACT - runs in worker + * @param {Object} print state object + * @param {Function} online streaming reply + * @param {Function} ondone last reply + */ + SLA.printExport = function(print, online, ondone) { + let widgets = print.widgets, + settings = print.settings, + device = settings.device, + process = settings.process, + output = print.output, + layermax = 0, + width = 2560, + height = 1440, + width2 = width/2, + height2 = height/2, + scaleX = width / device.bedWidth, + scaleY = height / device.bedDepth, + mark = Date.now(), + layers = process.slaAntiAlias || 1, + masks = [], + images = [], + slices = [], + legacyMode = SLA.legacy || layers > 1, + part1 = legacyMode ? 0.25 : 0.85, + part2 = legacyMode ? 0.75 : 0.15; + + let d = 8 / layers; + for (let i=0; i { + layermax = Math.max(widget.slices.length); + }); + + let render = legacyMode ? renderLayer : renderLayerWasm; + + // generate layer bitmaps + // in wasm mode, rle layers generated here, too + for (let index=0; index < layermax; index++) { + let param = { index, width, height, widgets, scaleX, scaleY, masks }; + let {image, layers, end} = render(param); + images.push(image); + slices.push(layers); + // transfer images to browser main + image = image.buffer; + online({ + progress: (index / layermax) * part1, + message: "image_gen", + data: image + },[image]); + if (end) break; + } + + let exp_func; + + switch (device.deviceName) { + case 'Anycubic.Photon': + exp_func = generatePhoton; + break; + case 'Anycubic.Photon.S': + exp_func = generatePhotons; + break; + } + + let file = exp_func(print, { + width: width, + height: height, + small: SLA.previewSmall.data, + large: SLA.previewLarge.data, + lines: images, + slices: slices + }, (progress, message) => { + online({progress: progress * part2 + part1, message}); + }); + ondone({ + width: width, + height: height, + file: file + },[file]); + + console.log('print.export', Date.now() - mark); + }; + + function generatePhoton(print, conf, progress) { + let printset = print.settings, + process = printset.process, + device = printset.device, + width = conf.width, + height = conf.height, + layerCount = conf.lines.length, + layerBytes = width * height, + small = conf.small, + large = conf.large, + slices = conf.slices, + subcount = process.slaAntiAlias || 1, + masks = [], + coded; + + if (SLA.legacy || subcount > 1) { + let d = 8 / subcount; + for (let i=0; i { + let count = line.length; + let lineDV = new DataView(line.buffer); + let bits = new Uint8Array(line.length); + let bitsDV = new DataView(bits.buffer); + let subs = [{ data: bits, view: bitsDV }]; + for (let sl=1; sl { + progress(pro * 0.4 + 0.4, "layer_encode"); + })); + } else { + let codedlen = slices.reduce((t,l) => { + return t + l.reduce((t,a) => { + return t + a.length + }, 0); + }, 0); + coded = { + layers: slices.map(slice => { return { sublayers: slice }}), + length: codedlen + }; + } + + let codelen = coded.layers.length; + let buflen = 3000 + coded.length + (codelen * subcount * 28) + small.byteLength + large.byteLength; + let filebuf = new ArrayBuffer(buflen); + let filedat = new self.DataWriter(new DataView(filebuf)); + let printtime = (process.slaBaseLayers * process.slaBaseOn) + + (coded.layers.length - process.slaBaseLayers) * process.slaLayerOn; + + filedat.writeU32(0x1900fd12); // header + filedat.writeU32(2,true); // version + filedat.writeF32(68.04, true); // bed x + filedat.writeF32(120.96, true); // bed y + filedat.writeF32(150.0, true); // bed z + filedat.skip(12); // padding + filedat.writeF32(process.slaSlice, true); // layer height + filedat.writeF32(process.slaLayerOn, true); // default lamp on + filedat.writeF32(process.slaBaseOn, true); // base lamp on + filedat.writeF32(process.slaLayerOff, true); // lamp off + filedat.writeU32(process.slaBaseLayers, true); // base layers + filedat.writeU32(1440, true); // device x + filedat.writeU32(2560, true); // device y + let hirez = filedat.skip(4); // hirez preview address filled pater + let layerpos = filedat.skip(4); // layer data address filled later + filedat.writeU32(codelen, true); + let lorez = filedat.skip(4); // hirez preview address filled later + filedat.writeU32(printtime, true); // print time seconds + filedat.writeU32(1, true); // projection type (1=lcd, 0=cast) + let proppos = filedat.skip(4); // print properties address filled later + let proplen = filedat.skip(4); // print properties length filled later + filedat.writeU32(subcount, true); // AA level (sub layers) + filedat.writeU16(0x00ff, true); // light pwm (TODO); + filedat.writeU16(0x00ff, true); // light pwm bottom (TODO); + + let propstart = filedat.pos; + filedat.view.setUint32(proppos, filedat.pos, true); + // write print properties + filedat.writeF32(process.slaBasePeelDist, true); + filedat.writeF32(process.slaBasePeelLiftRate * 60 , true); + filedat.writeF32(process.slaPeelDist, true); + filedat.writeF32(process.slaPeelLiftRate * 60 , true); + filedat.writeF32(process.slaPeelDropRate * 60, true); + filedat.writeF32(0, true); // volume of used + filedat.writeF32(0, true); // weight of used + filedat.writeF32(0, true); // cost of used + filedat.writeF32(0, true); // bottom off delay time + filedat.writeF32(0, true); // light off delay time + filedat.writeU32(process.slaBaseLayers, true); + filedat.writeF32(0, true); // p1 ? + filedat.writeF32(0, true); // p2 ? + filedat.writeF32(0, true); // p3 ? + filedat.writeF32(0, true); // p4 ? + filedat.view.setUint32(proplen, filedat.pos - propstart, true); + + filedat.view.setUint32(layerpos, filedat.pos, true); + // write layer headers + let layers = coded.layers; + let layerat = []; + + for (let sc=0; sc { + let count = line.length / 4; + let bits = new Uint8Array(line.length / 4); + let bitsDV = new DataView(bits.buffer); + let lineDV = new DataView(line.buffer); + // reduce RGB to R = 0||1 + for (let i = 0; i < count; i++) { + // defeat anti-aliasing for the moment + bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0); + } + progress(index / conf.lines.length); + return { subs: [{ + exposureTime: process.slaLayerOn, + data: bits + }] }; + }); + coded = encodeLayers(converted, "photons"); + } else { + let codedlen = slices.reduce((t,l) => { + return t + l.reduce((t,a) => { + return t + a.length + }, 0); + }, 0); + coded = { + layers: slices.map(slice => { return { sublayers: slice }}), + length: codedlen + }; + } + + let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount); + let filedat = new DataView(filebuf); + let filePos = 0; + + filedat.setUint32 (0, 2, false); + filedat.setUint32 (4, 3227560, false); + filedat.setUint32 (8, 824633720, false); + filedat.setUint16 (12, 10, false); + filedat.setFloat64(14, process.slaSlice, false); + filedat.setFloat64(22, process.slaLayerOn, false); + filedat.setFloat64(30, process.slaLayerOff, false); + filedat.setFloat64(38, process.slaBaseOn, false); + filedat.setUint32 (46, process.slaBaseLayers, false); + filedat.setFloat64(50, process.slaPeelDist, false); + filedat.setFloat64(58, process.slaPeelLift, false); + filedat.setFloat64(66, process.slaPeelDrop, false); + filedat.setFloat64(74, 69420, false); + filedat.setUint32 (82, 224, false); + filedat.setUint32 (86, 42, false); + filedat.setUint32 (90, 168, false); + filedat.setUint32 (94, 10, false); + filedat.setUint32 (75362, layerCount, false); + + filePos = 75366; + for (let i = 0; i < layerCount; i++) { + let layer = coded.layers[i], + sublayer = layer.sublayers[0], + numbytes = sublayer.length; + + filedat.setUint32 (filePos + 0, 69420, false); + filedat.setFloat64(filePos + 4, 0); + filedat.setUint32 (filePos + 12, height, false); + filedat.setUint32 (filePos + 16, width, false); + filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false); + filedat.setUint32 (filePos + 24, 2684702720, false); + filePos += 28; + for (let j = 0; j < numbytes; j++) { + filedat.setUint8(filePos + j, sublayer[j], false); + } + filePos += numbytes; + progress((i / layerCount) / 2 + 0.5); + } + + return filebuf; + } + + function encodeLayers(input, type, progress) { + let layers = [], length = 0, total = 0, count = 0; + input.forEach(layer => { + layer.subs.forEach(sub => total++); + }); + for (let index = 0; index < input.length; index++) { + let subs = input[index].subs, + sublayers = [], + sublength = 0; + for (let subindex = 0; subindex < subs.length; subindex++) { + let data = subs[subindex].data; + let encoded = rleEncode(data, type); + sublength += encoded.length; + sublayers.push(encoded); + if (progress) progress(count++/total); + if (type == "photons") break; + } + length += sublength; + layers.push({ + sublength, + sublayers + }); + } + return { length, layers }; + } + + function rleEncode(data, type) { + let maxlen = (type === 'photons') ? 128 : 125, + color = data[0], + runlen = 1, + output = []; + for (let index = 1; index < data.length; index++) { + let newColor = data[index]; + if (newColor !== color) { + output.push(rleByte(color, runlen, type)); + color = newColor; + runlen = 1; + } else { + if (runlen === maxlen) { + output.push(rleByte(color, runlen, type)); + runlen = 1; + } else { + runlen++; + } + } + } + if (runlen > 0) { + output.push(rleByte(color, runlen, type)); + } + return output; + } + + function rleByte(color, length, type) { + switch (type) { + case 'photon': + return (length & 0x7f) | ((color << 7) & 0x80); + case 'photons': + length--; + return (length & 1 ? 128 : 0) | + (length & 2 ? 64 : 0) | + (length & 4 ? 32 : 0) | + (length & 8 ? 16 : 0) | + (length & 16 ? 8 : 0) | + (length & 32 ? 4 : 0) | + (length & 64 ? 2 : 0) | color; + } + } + + function rleDecode(data, type) { + let bytes = []; + if (type === 'photon') { + for (let i = 0; i < data.length; i++) { + let val = data[i], + color = val >> 7, + count = val & 0x7f; + for (let j = 0; j < count; j++) { + bytes.push(color); + } + } + } else { + for (let i = 0; i < data.length; i++) { + let val = data[i], + color = val & 1, + count = + ((val & 128 ? 1 : 0) | + (val & 64 ? 2 : 0) | + (val & 32 ? 4 : 0) | + (val & 16 ? 8 : 0) | + (val & 8 ? 16 : 0) | + (val & 4 ? 32 : 0) | + (val & 2 ? 64 : 0)) + 1; + for (let j = 0; j < count; j++) { + bytes.push(color); + } + } + } + return bytes; + } + + // write out a thumbnail image + function writePhotonImage(preview, writer) { + let data = new Uint8Array(preview.data), len = data.byteLength; + writer.writeU32(preview.width, true); + writer.writeU32(preview.height, true); + let hpos = writer.skip(4); + writer.writeU32(len/2, true); + writer.view.setUint32(hpos, writer.pos, true); + let pos = 0; + while (pos < len) { + let r = data[pos++], + g = data[pos++], + b = data[pos++], + a = data[pos++], + v = (((r/4)&0x1f) << 11) | + (((g/4)&0x1f) << 6) | + (((b/4)&0x1f) << 0) ; + writer.writeU16(v, true); + } + } + + // load renderer code in worker context only + fetch('/wasm/kiri-sla.wasm') + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, { + env: { + reportf: (a,b) => { console.log('[f]',a,b) }, + reporti: (a,b) => { console.log('[i]',a,b) } + } + })) + .then(results => { + let {module, instance} = results; + let {exports} = instance; + let heap = new Uint8Array(exports.memory.buffer); + self.wasm = { + heap, + memory: exports.memory, + render: exports.render, + rle_encode: exports.rle_encode + }; + }); + + // new WebAssembly rasterizer + function renderLayerWasm(params) { + let { width, height, index, widgets, scaleX, scaleY, masks } = params; + let width2 = width / 2, height2 = height / 2; + let array = []; + let count = 0; + let wasm = self.wasm; + + function scaleMovePoly(poly) { + let points = poly.points; + let bounds = poly.bounds = BASE.newBounds(); + for (let i=0, il=points.length; i { + let slice = widget.slices[index]; + if (slice) { + if (slice.synth) count++; + let polys = slice.solids.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + array.appendAll(polys.map(poly => { + return poly.clone(true).move(widget.track.pos); + })); + count += polys.length; + } + }); + + let imagelen = width * height; + let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen); + writer.writeU16(width, true); + writer.writeU16(height, true); + writer.writeU16(array.length, true); + + // scale and move all polys to fit in rendered platform coordinates + for (let i=0, il=array.length; i { + let slice = widget.slices[index]; + if (slice) { + // prevent premature exit on empty synth slice + if (slice.synth) count++; + let polys = slice.solids.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + polys.forEach(poly => { + poly.move(widget.track.pos); + ctx.beginPath(); + polyout(poly.setClockwise(), ctx, opt); + if (poly.inner) { + poly.inner.forEach(inner => { + polyout(inner.setCounterClockwise(), ctx, opt); + }); + } + ctx.fill(); + count++; + }); + } else { + // console.log({no_slice_at: index}) + } + }); + let data = ctx.getImageData(0,0,height,width).data; + // reduce RGBA to R + let red = new Uint8ClampedArray(data.length / 4); + for (let i=0; i { + if (i === 0) { + ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); + } else { + ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); + } + }, true); + ctx.closePath(); + } + +})(); diff --git a/src/mode/sla/slice.js b/src/mode/sla/slice.js new file mode 100644 index 00000000..ded4fb44 --- /dev/null +++ b/src/mode/sla/slice.js @@ -0,0 +1,622 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + + let KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + SLA = KIRI.driver.SLA, + SLICER = KIRI.slicer, + newTop = KIRI.newTop, + newSlice = KIRI.newSlice, + newPoint = BASE.newPoint, + newPolygon = BASE.newPolygon, + fill_cache; + + /** + * DRIVER SLICE CONTRACT - runs in worker + * + * @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) + */ + SLA.slice = function(settings, widget, onupdate, ondone) { + let process = settings.process, + device = settings.device, + work_total, + work_remain; + + if (SLA.legacy && !self.OffscreenCanvas) { + return ondone("browser lacks support for OffscreenCanvas",true); + } + + // calculate % complete and call onupdate() + function doupdate(work, msg) { + onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg); + work_remain -= work; + } + + // for each slice, perform a function and call doupdate() + function forSlices(slices, work, fn, msg) { + slices.forEach(function(slice,index) { + fn(slice,index); + doupdate(work / slices.length, msg) + }); + } + + let b64 = atob(self.worker.snap); + let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); + let img = new png.PNG(); + img.parse(bin, (err, data) => { + SLA.preview = img; + SLA.previewSmall = samplePNG(img, 200, 125); + SLA.previewLarge = samplePNG(img, 400, 300); + }); + let height = process.slaSlice || 0.05; + + SLICER.sliceWidget(widget, { + height: height, + add: !process.slaOpenTop + }, function(slices) { + // hold onto last (empty) slice + let last = slices[slices.length-1]; + // remove empty slices + slices = widget.slices = slices.filter(slice => slice.tops.length); + if (!process.slaOpenTop) { + // re-add last empty slice for open top + slices.push(last); + } + // prepend raft layers to slices array + if (process.slaSupportEnable && process.slaSupportLayers) { + let layers = process.slaSupportLayers, + zoff = height / 2, + snew = [], + polys = [], + gap = process.slaSupportGap, // gap layers above raft + grow = height, // union per layer expand + off = 1 - (layers * grow); // starting union offset from part + let outer = slices.forEach(slice => { + // poly.clone prevents inner voids from forming + polys.appendAll(slice.tops.map(t => t.poly.clone())); + }); + // p.clone prevents inner voids from forming + let union = POLY.union(polys).map(p => p.clone()); + let expand = POLY.expand(union, off, zoff, [], 1); + let lastraft; + for (let s=0; s { + slice.tops.push(newTop(u.clone(true).setZ(zoff))); + }); + expand = POLY.expand(expand, grow, zoff, [], 1); + lastraft = slice; + } + snew.push(slice); + zoff += height; + } + // compensate for midline start + zoff -= height / 2; + // replace slices with new appended array + slices = widget.slices = snew.concat(slices.map(s => { + s.tops.forEach(t => t.poly.setZ(s.z + zoff)); + s.index += snew.length; + s.z += zoff; + return s; + })); + // annotate widget for support generation + widget.union = union; + widget.lastraft = lastraft; + } + // re-connect slices into linked list for island/bridge projections + for (let i=1; i { return t+v }); + work_remain = work_total; + forSlices(slices, 5, (slice,index) => { + if (process.slaShell) { + slice.doShells(2, 0, process.slaShell); + } else { + slice.doShells(1, 0); + } + }, "slice"); + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + slice.doDiff(0.000001, 0.005, !process.slaOpenBase); + }, "delta"); + if (solidLayers) { + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + slice.projectFlats(solidLayers); + slice.projectBridges(solidLayers); + }, "project"); + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + slice.doSolidsFill(undefined, undefined, 0.001); + let traces = POLY.nest(POLY.flatten(slice.gatherTraces([]))); + let trims = slice.solids.trimmed || []; + traces.appendAll(trims); + let union = POLY.union(traces); + slice.solids.unioned = union; + }, "solid"); + } else { + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + slice.solids.unioned = slice.gatherTopPolys([]); + }, "solid"); + } + if (process.slaFillDensity && process.slaShell) { + fill_cache = []; + forSlices(slices, 60, (slice) => { + if (slice.synth) return; + fillPolys(slice, settings); + }, "infill"); + } + if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) { + computeSupports(widget, process, progress => { + doupdate(100 * progress, "support"); + }); + } + ondone(); + }, function(update) { + return onupdate(0.0 + update * 0.25); + }); + }; + + function computeSupports(widget, process, progress) { + let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0), + perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0), + slices = widget.slices, + length = slices.length, + tot_mass = 0, // total widget "mass" + tot_bear = 0; // total "mass-bearing" area + + let ops = slices.filter(slice => !slice.synth); + + // compute total "mass" by slice + ops.forEach(slice => { + slice.mass = slice.solids.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0); + if (slice.up && slice.up.bridges) { + slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0); + slice.bear_up = slice.up.bridges; + tot_bear += slice.bear; + } else { + slice.bear = 0; + } + tot_mass += slice.mass; + }); + + let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity); + + // console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear}); + + let first; + ops.slice().map((s,i) => { + if (!first && s.bear) { + s.ord_first = first = true; + } + // 30x lowest to 1x highest + s.ord_weight = Math.pow(30,((ops.length - i) / ops.length)); + return s; + }).sort((a,b) => { + if (a.ord_first) return -1; + if (b.ord_first) return 1; + return (b.bear * b.ord_weight) - (a.bear * a.ord_weight); + }).forEach((slice, index) => { + slice.ord_bear = index; + }) + + let rem_mass = tot_mass, + rem_bear = tot_bear; + + // compute remaining mass, bearing surface, for each slice + let run, runLast, runCount = 0, runList = []; + ops.forEach(slice => { + slice.rem_mass = rem_mass; + slice.rem_bear = rem_bear; + // slice.can_bear = slice.mass * mass_per_bear; + slice.can_bear = slice.bear * mass_per_bear; + rem_mass -= slice.mass; + rem_bear -= slice.bear; + }); + + let ord = ops.sort((a,b) => { + return a.ord_bear - b.ord_bear; + }); + + rem_mass = tot_mass; + rem_bear = tot_bear; + + // in order of load bearing capability, select layer + // and recompute the requirements on the slices below + for (let i=0; iops[0].index; j--) { + slices[j].rem_mass -= bearing; + } + + // remove total mass left to bear + rem_mass -= bearing; + slice.can_emit = true; + if (rem_mass <= 0) { + // console.log({break: i, of: ord.length}); + break; + } + } + + let seq = 0, + seqLast = 0, + spacing = (1 - process.slaSupportDensity) * 10, + size = Math.bound(process.slaSupportSize / 2, 0.25, 1); + + // compute and project support pillars + slices.forEach((slice,index) => { + if (slice.can_emit) { + if (seq === 0 || slice.index - seqLast > 5) { + slice.bear_up.map(p => { + return p.clone(true).setZ(slice.z); + }).forEach(p => { + projectSupport(process, slice, p, size, spacing); + }); + seq++; + } else if (seq > 5) { + seq = 0; + } else { + seq++; + } + seqLast = slice.index; + } + progress(1 / slices.length); + }); + + // union support pillars + slices.forEach(slice => { + if (slice.supports) { + slice.supports = POLY.union(slice.supports,0); + } + }); + } + + function projectSupport(process, slice, poly, size, spacing) { + let flat = poly.circularityDeep() > 0.1, + arr = [ poly ]; + + // insetting polys produces arrays. consume til gone + while (arr.length) { + poly = arr.shift(); + let out = [], + seg = [], + per = poly.perimeter(), + crit = 0, + polys = poly.clone(true).flattenTo([]); + + polys.forEach(p => p.forEachSegment((p1, p2) => { + let rec = {dist: p1.distTo2D(p2), p1, p2}; + if (rec.dist >= spacing || slice.ord_first) crit++; + seg.push(rec); + })); + + seg.sort((a,b) => { + return b.dist - a.dist; + }); + + // emit all critical (long) segments. min 3 + while (seg.length && (crit > 0 || out.length < 3)) { + let {dist, p1, p2} = seg.shift(); + if (dist >= spacing) { + // spaced along line + let num = Math.ceil(dist / spacing) + 1, + step = dist / num, + pt = p1; + while (num-- >= 0) { + out.push(pt); + pt = pt.offsetPointTo(p2, step); + } + crit--; + } else { + // line midpoint + out.push(p1.offsetPointTo(p2, dist / 2)); + } + } + + // drop points too close to other pillars + if (!slice.ord_first) + drop: for (let i=0; i { + if (out[i] && out[i].distTo2D(rec.point) <= size) { + out[i] = null; + } + }); + } + + // mark support pillar for each point + out.filter(p => p !== null) + .forEach(p => { + let track = projectPillar(process, slice, p, size, []); + // find stunted pillars terminating on a face (not raft or merged) + if (track.length && !(track.max || track.synth || track.merged)) { + // remove stunted pillar + track.forEach(rec => { + let sp = rec.slice.supports; + let rp = sp.indexOf(rec.pillar); + if (rp >= 0) sp.splice(rp,1); + }); + } + }); + + // inset polygon for flat area support + if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr); + } + } + + function projectPillar(process, slice, point, size, track) { + if (!slice.supports) slice.supports = []; + if (!slice.pillars) slice.pillars = []; + + let points = process.slaSupportPoints, + pillar = newPolygon() + .centerCircle(point, size/2, points, true) + .setZ(slice.z), + max = process.slaSupportSize, + inc = process.slaSlice/2, + end = false, // center intersects + over = [], // overlapping points + safe = [], // non-overlapping points + low = slice.index < process.slaSupportGap + process.slaSupportLayers; + + track.min = track.min ? Math.min(track.min, size) : size; + track.max = (track.max ? true : false) || size >= max; + track.synth = track.synth || slice.synth; + + slice.tops.forEach(t => { + if (track.length > 3 && point.isInPolygon(t.poly)) { + end = true; + } + pillar.points.forEach(p => { + let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001); + if (isin) { + over.push(p); + } else { + safe.push(p); + } + }); + }); + + if (end) { + // backtrack shrinking pillars to point if + // not landing on the base and size is max'd + if (!slice.synth && track.max) { + let nusize = track.min; + while (track.length) { + let prec = track.pop(), + npil = newPolygon() + .centerCircle(prec.point, nusize/2, points, true) + .setZ(prec.slice.z), + spos = prec.slice.supports.indexOf(prec.pillar); + // if we find our old pillar, replace + if (spos >= 0) { + prec.slice.supports[spos] = npil; + nusize += inc; + } + if (nusize > prec.size) { + break; + } + } + } + return track; + } + + let nextpoint = point; + + if (over.length) { + // move toward average of safe (non-overlapping) points + if (safe.length) { + let x = 0, y = 0; + safe.forEach(p => { x += p.x; y += p.y }); + x /= safe.length; + y /= safe.length; + nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc); + } + // once max'ed out, can only shrink in size + if (track.max) { + size -= inc; + } + } else if (!track.max || low) { + size += inc; + if (low) max += (process.slaSupportGap * process.slaSlice * 2); + } + + if (size < track.min) { + return track; + } + + // let close = []; + // for (let i=0; i { + // newp.x += p.x; + // newp.y += p.y; + // }); + // newp.x /= (close.length + 1); + // newp.y /= (close.length + 1); + // nextpoint = point.offsetPointTo(newp, inc); + // } + + slice.supports.push(pillar); + slice.pillars.push({point, pillar, size}); + track.push({slice, point, pillar, size}); + if (slice.down) { + projectPillar(process, slice.down, nextpoint, Math.min(size, max), track); + } + return track; + } + + function fillPolys(slice, settings) { + let process = settings.process, + device = settings.device, + polys = slice.solids.unioned, + bounds = settings.bounds, + width = bounds.max.x - bounds.min.x, + depth = bounds.max.y - bounds.min.y, + max = Math.max(width,depth), + seq = Math.round(process.slaFillLine / process.slaSlice), + linew = process.slaFillLine, + units_w = (width / linew) * process.slaFillDensity, + units_d = (depth / linew) * process.slaFillDensity, + step_x = width / units_w, + step_y = depth / units_d, + start_x = -(width / 2), + start_y = -(depth / 2), + end_x = width / 2, + end_y = depth / 2, + fill = []; + + let seq_i = Math.floor(slice.index / seq), + seq_c = seq_i % 4, + cached = fill_cache[seq_c]; + + if (!cached && seq_c !== 1) + for (let x=start_x; x p.clone(true).setZ(slice.z)); + } + + fill = POLY.trimTo(fill, slice.tops.map(t => t.poly)); + fill = POLY.union(slice.solids.unioned.appendAll(fill)); + + slice.solids.unioned = fill; + } + + function pixAt(png,x,y) { + let idx = (x + png.width * y) * 4; + let dat = png.data; + return [ + dat[idx++], + dat[idx++], + dat[idx++], + dat[idx++] + ]; + } + + function averageBlock(png,x1,y1,x2,y2) { + let val = [0, 0, 0, 0], count = 0, x, y, z, v2; + for (x=x1; x 4/3) { + div = png.height / tw; + xoff = Math.round((png.width - (th * div)) / 2); + yoff = 0; + } else { + div = png.width / th; + xoff = 0; + yoff = Math.round((png.height - (tw * div)) / 2); + } + + for (let y=0; y png.height) continue; + ey = Math.round((y+1) * div + yoff); + for (let x=0; x png.width) continue; + ex = Math.round((x+1) * div + xoff); + bidx = (y * th + x) * 4; + pixval = averageBlock(png,dx,dy,ex,ey); + buf[bidx+0] = pixval[0]; + buf[bidx+1] = pixval[1]; + buf[bidx+2] = pixval[2]; + buf[bidx+3] = pixval[3]; + } + } + + return {width, height, data:buf, png}; + } + +})();