diff --git a/app.js b/app.js index fa1b22ef..f2e0d625 100644 --- a/app.js +++ b/app.js @@ -410,6 +410,7 @@ const script = { "geo/slicer", "geo/mesh", // "moto/broker", + "kiri/consts", "kiri/pack", "kiri/utils", "kiri/slice", @@ -459,6 +460,7 @@ const script = { "geo/polygon", "geo/gyroid", "geo/slicer", + "kiri/consts", "kiri-mode/fdm/driver", "kiri-mode/fdm/slice", "kiri/utils", diff --git a/src/kiri-mode/fdm/client.js b/src/kiri-mode/fdm/client.js index 75e8ced7..d7991531 100644 --- a/src/kiri-mode/fdm/client.js +++ b/src/kiri-mode/fdm/client.js @@ -4,683 +4,682 @@ (function() { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - FDM = KIRI.driver.FDM, - SPACE, API, VIEWS, LANG, PROC, UI, UC, - nextID = Date.now(), - p1, p2, iw, - lastBox, - lastMode, lastView, - lastPillar, fromPillar, - isFdmMode = false, - addingSupports = false, - alert = [], - boxes = {}, - func = {}; +const { Quaternion, Matrix4, Vector3, BufferAttribute, BufferGeometryUtils } = THREE; +const { Mesh, BoxGeometry, MeshPhongMaterial, Raycaster, Euler } = THREE; +const { base, kiri, moto } = self; +const { util } = base; +const { api, consts, lang } = kiri; +const { FDM } = kiri.driver; +const { VIEWS } = consts; +const { space } = moto; +const LANG = lang.current; - FDM.init = function(kiri, api) { - UI = api.ui; - UC = api.uc; - API = api; - SPACE = kiri.space; - VIEWS = kiri.consts.VIEWS; - LANG = KIRI.lang.current; - PROC = Object.keys(kiri.conf.defaults.fdm.p); +let lastBox, lastMode, lastView, lastPillar, fromPillar, + addingSupports = false, + isFdmMode = false, + nextID = Date.now(), + p1, p2, iw, + alert = [], + boxes = {}, + func = {}; - let rangeVars = { - // slice - "sliceShells": LANG.sl_shel_s, - "sliceFillType": LANG.fi_type, - "sliceFillWidth": LANG.fi_wdth_s, - "sliceFillSparse": LANG.fi_pcnt_s, - "sliceSolidMinArea": LANG.ad_msol_s, - "firstLayerRate": LANG.fl_rate_s, - "firstLayerPrintMult": LANG.fl_mult_s, - // prepare - "sliceShellOrder": LANG.sl_ordr_s, - "sliceFillOverlap": LANG.fi_over_s, - "outputFeedrate": LANG.ou_feed_s, - "outputFinishrate": LANG.ou_fini_s, - "outputShellMult": LANG.ou_shml_s, - "outputFillMult": LANG.ou_flml_s, - "outputSparseMult": LANG.ou_spml_s, - "outputRetractWipe": LANG.ad_wpln_s, - "outputShortPoly": LANG.ad_spol_s, - "outputMinSpeed": LANG.ad_mins_s, - "outputCoastDist": LANG.ad_scst_s, - "outputLoops": LANG.ag_loop_s, - "sliceSupportDensity": LANG.sp_dens_s, - "sliceSupportOffset": LANG.sp_offs_s, - "sliceLayerStart": LANG.sl_strt_s, - "firstLayerBrim": LANG.fl_brim_s, - "firstLayerBrimIn": LANG.fl_brin_s, - "firstLayerBrimTrig": LANG.fl_brmn_s, - "firstLayerBrimGap": LANG.fl_brgp_s, - // export - "zHopDistance": LANG.ad_zhop_s, - "arcTolerance": LANG.ad_zhop_s, - "antiBacklash": LANG.ad_abkl_s, - "outputTemp": LANG.ou_nozl_s, - "outputBedTemp": LANG.ou_bedd_s, - "outputFanSpeed": LANG.ou_fans_s, - "outputRetractDist": LANG.ad_rdst_s, - "outputRetractSpeed": LANG.outputRetractSpeed, - "outputRetractDwell": LANG.ad_rdwl_s, - }; +FDM.init = function(kiri, api) { + const proc_keys = Object.keys(kiri.conf.defaults.fdm.p); + const { ui, uc } = api; + const rangeVars = { + // slice + "sliceShells": LANG.sl_shel_s, + "sliceFillType": LANG.fi_type, + "sliceFillWidth": LANG.fi_wdth_s, + "sliceFillSparse": LANG.fi_pcnt_s, + "sliceSolidMinArea": LANG.ad_msol_s, + "firstLayerRate": LANG.fl_rate_s, + "firstLayerPrintMult": LANG.fl_mult_s, + // prepare + "sliceShellOrder": LANG.sl_ordr_s, + "sliceFillOverlap": LANG.fi_over_s, + "outputFeedrate": LANG.ou_feed_s, + "outputFinishrate": LANG.ou_fini_s, + "outputShellMult": LANG.ou_shml_s, + "outputFillMult": LANG.ou_flml_s, + "outputSparseMult": LANG.ou_spml_s, + "outputRetractWipe": LANG.ad_wpln_s, + "outputShortPoly": LANG.ad_spol_s, + "outputMinSpeed": LANG.ad_mins_s, + "outputCoastDist": LANG.ad_scst_s, + "outputLoops": LANG.ag_loop_s, + "sliceSupportDensity": LANG.sp_dens_s, + "sliceSupportOffset": LANG.sp_offs_s, + "sliceLayerStart": LANG.sl_strt_s, + "firstLayerBrim": LANG.fl_brim_s, + "firstLayerBrimIn": LANG.fl_brin_s, + "firstLayerBrimTrig": LANG.fl_brmn_s, + "firstLayerBrimGap": LANG.fl_brgp_s, + // export + "zHopDistance": LANG.ad_zhop_s, + "arcTolerance": LANG.ad_zhop_s, + "antiBacklash": LANG.ad_abkl_s, + "outputTemp": LANG.ou_nozl_s, + "outputBedTemp": LANG.ou_bedd_s, + "outputFanSpeed": LANG.ou_fans_s, + "outputRetractDist": LANG.ad_rdst_s, + "outputRetractSpeed": LANG.outputRetractSpeed, + "outputRetractDwell": LANG.ad_rdwl_s, + }; - for (let key of Object.keys(rangeVars)) { - if (UI[key]) { - UI[key].range = true; + for (let key of Object.keys(rangeVars)) { + if (ui[key]) { + ui[key].range = true; + } + } + + function filterSynth() { + api.widgets.filter((widget) => { + if (widget.track.synth) { + kiri.space.world.remove(widget.mesh); + kiri.Widget.Groups.remove(widget); + } + return !widget.track.synth + }); + } + + function clearRanges() { + api.conf.get().process.ranges = []; + ui.rangeGroup.firstElementChild.innerHTML = ''; + } + + function updateRanges(ranges = []) { + ui.rangeGroup.style.display = isFdmMode && ranges && ranges.length ? 'flex' : 'none'; + let html = []; + let bind = []; + let now = Date.now(); + let sorted = ranges.sort((a,b) => b.lo - a.lo); + for (let range of sorted) { + let id = (now++).toString(36); + let rows = Object.entries(range.fields).map(a => `
`).join(''); + let hover = `
${rows}
`; + let info = ``; + html.appendAll([ + `
${hover}${info}
` + ]); + bind.push({id, range}); + } + ui.rangeGroup.firstElementChild.innerHTML = html.join(''); + for (let rec of bind) { + $(`sel_${rec.id}`).onclick = () => { + api.show.layer(rec.range.hi, rec.range.lo); + }; + $(`del_${rec.id}`).onclick = () => { + let io = ranges.indexOf(rec.range); + ranges.splice(io,1); + updateRanges(ranges); + }; + } + } + + api.event.on("boolean.click", func.updateSupportButtons = () => { + if (!isFdmMode) { + return; + } + for (let btn of [ + ui.ssaGen, + ui.ssmAdd, + ui.ssmDun, + ui.ssmClr + ]) { + btn.disabled = ui.sliceSupportEnable.checked; + } + if (ui.sliceSupportEnable.checked) { + func.sclear(); + } + }); + + api.event.on("function.animate", (mode) => { + if (!isFdmMode) { + return; + } + if (!api.conf.get().controller.danger) { + return; + } + let pct = 0; + let int = setInterval(() => { + if (pct <= 1) { + api.const.STACKS.setFraction(pct); + pct += 0.05; + } else { + api.const.STACKS.setFraction(1); + clearTimeout(int); + } + }, 50); + // let slider = $('top-slider'); + // slider.style.display = 'flex'; + // slider.oninput = slider.onchange = (ev) => { + // api.const.STACKS.setFraction(parseInt(ev.target.value)/100); + // }; + }); + api.event.on("mode.set", mode => { + isFdmMode = mode === 'FDM'; + lastMode = mode; + updateVisiblity(); + }); + api.event.on("view.set", view => { + lastView = view; + updateVisiblity(); + filterSynth(); + func.sdone(); + // let ranges = api.conf.get().process.ranges; + if (isFdmMode) { + if (lastView === VIEWS.SLICE) { + for (let key of proc_keys) { + if (ui[key] && !ui[key].range) { + ui[key].disabled = true; + } + } + } else { + for (let key of proc_keys) { + if (ui[key]) ui[key].disabled = false; + } } } - - function filterSynth() { - api.widgets.filter((widget) => { - if (widget.track.synth) { - kiri.space.world.remove(widget.mesh); - kiri.Widget.Groups.remove(widget); + // ui.rangeGroup.style.display = ranges && ranges.length ? 'flex' : 'none'; + }); + api.event.on("range.updates", updateRanges); + api.event.on("settings.load", (settings) => { + if (settings.mode !== 'FDM') return; + settings.process.outputOriginCenter = (settings.device.originCenter || false); + restoreSupports(api.widgets.all()); + updateRanges(settings.process.ranges); + }); + api.event.on("settings.saved", (settings) => { + updateRanges(settings.process.ranges); + func.updateSupportButtons(); + // let ranges = settings.process.ranges; + // ui.rangeGroup.style.display = isFdmMode && ranges && ranges.length ? 'flex' : 'none'; + }); + api.event.on("button.click", target => { + switch (target) { + case api.ui.ssaGen: return func.sgen(); + case api.ui.ssmAdd: return func.sadd(); + case api.ui.ssmDun: return func.sdone(); + case api.ui.ssmClr: + // return api.uc.confirm("clear supports?").then(ok => { + // if (ok) func.sclear(); + // }); + func.sclear(); + } + }); + api.event.on("fdm.supports.detect", func.sgen = () => { + let alerts = []; + let { process, device } = api.conf.get(); + if (!device.bedBelt) { + if (process.sliceSupportAngle < 10) { + alerts.push(api.show.alert("angles below 10 degrees may fail")); + } + } + alerts.push(api.show.alert("analyzing part(s)...", 1000)); + FDM.support_generate(array => { + func.sclear(); + api.hide.alert(undefined,alerts); + for (let rec of array) { + let { widget, supports } = rec; + let wa = api.widgets.annotate(widget.id); + let ws = wa.support || []; + for (let support of supports) { + let { from, to, mid } = support; + let dw = api.conf.get().process.sliceSupportSize / 2; + let dh = from.z - to.z; + let rec = { + x: mid.x, + y: mid.y, + z: mid.z, + dw, + dh, + id: Math.random() * 0xffffffffff + }; + addWidgetSupport(widget, rec); + ws.push(Object.clone(rec)); } - return !widget.track.synth + wa.support = ws; + } + api.event.emit("fdm.supports.detected"); + }); + }); + api.event.on("fdm.supports.add", func.sadd = () => { + alert = api.show.alert("[esc] key cancels support editing"); + api.feature.hover = addingSupports = true; + }); + api.event.on("fdm.supports.done", func.sdone = () => { + delbox('intZ'); + delbox('intW'); + delbox('supp'); + api.hide.alert(alert); + api.feature.hover = addingSupports = false; + fromPillar = undefined; + }); + api.event.on("fdm.supports.clear", func.sclear = () => { + func.sdone(); + if (clearAllWidgetSupports()) { + api.conf.save(); + } + }); + api.event.on("slice.begin", () => { + if (!isFdmMode) { + return; + } + func.sdone(); + updateVisiblity(); + + // synth support widget for each widget group + let synth = []; + for (let group of kiri.Widget.Groups.list()) { + let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); + if (!merge.length) { + continue; + } + let boxen = merge.map(m => { + let geo = m.box.geometry.clone(); + if (geo.index) geo = geo.toNonIndexed(); + return geo.translate(m.x, m.y, m.z); }); + for (let box of boxen) { + // eliminate / normalize uv to allow other widget merge + box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3)); + } + let bbg = BufferGeometryUtils.mergeBufferGeometries(boxen); + let sw = kiri.newWidget(null, group); + let fwp = group[0].track.pos; + sw.loadGeometry(bbg); + sw._move(fwp.x, fwp.y, fwp.z); + api.widgets.add(sw); + sw.track.synth = true; + kiri.space.world.add(sw.mesh); } - - function clearRanges() { - API.conf.get().process.ranges = []; - UI.rangeGroup.firstElementChild.innerHTML = ''; + }); + api.event.on("slice.end", () => { + if (!isFdmMode) { + return; } - - function updateRanges(ranges = []) { - UI.rangeGroup.style.display = isFdmMode && ranges && ranges.length ? 'flex' : 'none'; - let html = []; - let bind = []; - let now = Date.now(); - let sorted = ranges.sort((a,b) => b.lo - a.lo); - for (let range of sorted) { - let id = (now++).toString(36); - let rows = Object.entries(range.fields).map(a => `
`).join(''); - let hover = `
${rows}
`; - let info = ``; - html.appendAll([ - `
${hover}${info}
` - ]); - bind.push({id, range}); - } - UI.rangeGroup.firstElementChild.innerHTML = html.join(''); - for (let rec of bind) { - $(`sel_${rec.id}`).onclick = () => { - api.show.layer(rec.range.hi, rec.range.lo); - }; - $(`del_${rec.id}`).onclick = () => { - let io = ranges.indexOf(rec.range); - ranges.splice(io,1); - updateRanges(ranges); - }; - } + }); + api.event.on("key.esc", () => { + if (!isFdmMode) { + return; } - - api.event.on("boolean.click", func.updateSupportButtons = () => { - if (!isFdmMode) { - return; - } - for (let btn of [ - UI.ssaGen, - UI.ssmAdd, - UI.ssmDun, - UI.ssmClr - ]) { - btn.disabled = UI.sliceSupportEnable.checked; - } - if (UI.sliceSupportEnable.checked) { - func.sclear(); + func.sdone() + }); + api.event.on("selection.scale", () => { + if (isFdmMode) { + clearRanges(); + func.sclear(); + } + }); + api.event.on("widget.delete", widget => { + if (isFdmMode) { + clearRanges(); + return; + } + }); + api.event.on("widget.duplicate", (widget, oldwidget) => { + if (!isFdmMode) { + return; + } + let ann = api.widgets.annotate(widget.id); + if (ann.support) { + for (let supp of Object.values(ann.support)) { + supp.id = Math.abs(Math.random() * 0xffffffffffff); } + restoreSupports([widget]); + } + }); + api.event.on("widget.mirror", widget => { + if (!isFdmMode) { + return; + } + let ann = api.widgets.annotate(widget.id); + let sups = ann.support || []; + sups.forEach(sup => { + let wsup = widget.sups[sup.id]; + wsup.box.position.x = wsup.x = sup.x = -sup.x; }); - - api.event.on("function.animate", (mode) => { - if (!isFdmMode) { - return; - } - if (!API.conf.get().controller.danger) { - return; - } - let pct = 0; - let int = setInterval(function() { - if (pct <= 1) { - api.const.STACKS.setFraction(pct); - pct += 0.05; - } else { - api.const.STACKS.setFraction(1); - clearTimeout(int); - } - }, 50); - // let slider = $('top-slider'); - // slider.style.display = 'flex'; - // slider.oninput = slider.onchange = (ev) => { - // api.const.STACKS.setFraction(parseInt(ev.target.value)/100); - // }; - }); - api.event.on("mode.set", mode => { - isFdmMode = mode === 'FDM'; - lastMode = mode; - updateVisiblity(); - }); - api.event.on("view.set", view => { - lastView = view; - updateVisiblity(); - filterSynth(); - func.sdone(); - // let ranges = API.conf.get().process.ranges; - if (isFdmMode) { - if (lastView === VIEWS.SLICE) { - for (let key of PROC) { - if (UI[key] && !UI[key].range) { - UI[key].disabled = true; - } - } - } else { - for (let key of PROC) { - if (UI[key]) UI[key].disabled = false; - } - } - } - // UI.rangeGroup.style.display = ranges && ranges.length ? 'flex' : 'none'; - }); - api.event.on("range.updates", updateRanges); - api.event.on("settings.load", (settings) => { - if (settings.mode !== 'FDM') return; - settings.process.outputOriginCenter = (settings.device.originCenter || false); - restoreSupports(api.widgets.all()); - updateRanges(settings.process.ranges); - }); - api.event.on("settings.saved", (settings) => { - updateRanges(settings.process.ranges); - func.updateSupportButtons(); - // let ranges = settings.process.ranges; - // UI.rangeGroup.style.display = isFdmMode && ranges && ranges.length ? 'flex' : 'none'; - }); - api.event.on("button.click", target => { - switch (target) { - case api.ui.ssaGen: return func.sgen(); - case api.ui.ssmAdd: return func.sadd(); - case api.ui.ssmDun: return func.sdone(); - case api.ui.ssmClr: - // return api.uc.confirm("clear supports?").then(ok => { - // if (ok) func.sclear(); - // }); - func.sclear(); - } - }); - api.event.on("fdm.supports.detect", func.sgen = () => { - let alerts = []; - let { process, device } = api.conf.get(); - if (!device.bedBelt) { - if (process.sliceSupportAngle < 10) { - alerts.push(api.show.alert("angles below 10 degrees may fail")); - } - } - alerts.push(api.show.alert("analyzing part(s)...", 1000)); - FDM.support_generate(array => { - func.sclear(); - api.hide.alert(undefined,alerts); - for (let rec of array) { - let { widget, supports } = rec; - let wa = API.widgets.annotate(widget.id); - let ws = wa.support || []; - for (let support of supports) { - let { from, to, mid } = support; - let dw = api.conf.get().process.sliceSupportSize / 2; - let dh = from.z - to.z; - let rec = { - x: mid.x, - y: mid.y, - z: mid.z, - dw, - dh, - id: Math.random() * 0xffffffffff - }; - addWidgetSupport(widget, rec); - ws.push(Object.clone(rec)); - } - wa.support = ws; - } - api.event.emit("fdm.supports.detected"); - }); - }); - api.event.on("fdm.supports.add", func.sadd = () => { - alert = api.show.alert("[esc] key cancels support editing"); - api.feature.hover = addingSupports = true; - }); - api.event.on("fdm.supports.done", func.sdone = () => { - delbox('intZ'); - delbox('intW'); - delbox('supp'); - api.hide.alert(alert); - api.feature.hover = addingSupports = false; - fromPillar = undefined; - }); - api.event.on("fdm.supports.clear", func.sclear = () => { - func.sdone(); - if (clearAllWidgetSupports()) { - API.conf.save(); - } - }); - api.event.on("slice.begin", () => { - if (!isFdmMode) { - return; - } - func.sdone(); - updateVisiblity(); - - // synth support widget for each widget group - let synth = []; - for (let group of kiri.Widget.Groups.list()) { - let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); - if (!merge.length) { - continue; - } - let boxen = merge.map(m => { - let geo = m.box.geometry.clone(); - if (geo.index) geo = geo.toNonIndexed(); - return geo.translate(m.x, m.y, m.z); - }); - for (let box of boxen) { - // eliminate / normalize uv to allow other widget merge - box.setAttribute('uv',new THREE.BufferAttribute(new Float32Array(0), 3)); - } - let bbg = THREE.BufferGeometryUtils.mergeBufferGeometries(boxen); - let sw = kiri.newWidget(null, group); - let fwp = group[0].track.pos; - sw.loadGeometry(bbg); - sw._move(fwp.x, fwp.y, fwp.z); - api.widgets.add(sw); - sw.track.synth = true; - KIRI.space.world.add(sw.mesh); - } - }); - api.event.on("slice.end", () => { - if (!isFdmMode) { - return; - } - }); - api.event.on("key.esc", () => { - if (!isFdmMode) { - return; - } - func.sdone() - }); - api.event.on("selection.scale", () => { - if (isFdmMode) { - clearRanges(); - func.sclear(); - } - }); - api.event.on("widget.delete", widget => { - if (isFdmMode) { - clearRanges(); - return; - } - }); - api.event.on("widget.duplicate", (widget, oldwidget) => { - if (!isFdmMode) { - return; - } - let ann = API.widgets.annotate(widget.id); - if (ann.support) { - for (let supp of Object.values(ann.support)) { - supp.id = Math.abs(Math.random() * 0xffffffffffff); - } - restoreSupports([widget]); - } - }); - api.event.on("widget.mirror", widget => { - if (!isFdmMode) { - return; - } - let ann = API.widgets.annotate(widget.id); + }); + api.event.on("widget.rotate", rot => { + if (!isFdmMode) { + return; + } + let {widget, x, y, z} = rot; + if (x || y) { + clearWidgetSupports(widget); + } else { + let ann = api.widgets.annotate(widget.id); let sups = ann.support || []; sups.forEach(sup => { let wsup = widget.sups[sup.id]; - wsup.box.position.x = wsup.x = sup.x = -sup.x; + let vc = new Vector3(sup.x, sup.y, sup.z); + let m4 = new Matrix4(); + m4 = m4.makeRotationFromEuler(new Euler(x || 0, y || 0, z || 0)); + vc.applyMatrix4(m4); + wsup.box.position.x = wsup.x = sup.x = vc.x; + wsup.box.position.y = wsup.y = sup.y = vc.y; + wsup.box.position.z = wsup.z = sup.z = vc.z; }); - }); - api.event.on("widget.rotate", rot => { - if (!isFdmMode) { - return; + } + }); + api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { + let { event } = on; + if (!isFdmMode) { + return; + } + if (!addingSupports) { + return; + } + delbox('supp'); + if (lastPillar) { + removeWidgetSupport(lastPillar.widget, lastPillar); + return; + } + if (!iw) return; + let { point, dim } = lastBox; + p1.y = Math.max(0, p1.y); + p2.y = Math.max(0, p2.y); + let rz = dim.rz; + let dh = dim.z; + let dw = api.conf.get().process.sliceSupportSize / 2; + let ip = iw.track.pos; + let wa = api.widgets.annotate(iw.id); + let ws = (wa.support = wa.support || []); + let id = ++nextID; + let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id}; + if (fromPillar && event && event.shiftKey) { + let targets = api.widgets.meshes().append(space.internals().platform); + let from = fromPillar, + d = { + x: rec.x - from.x, + y: rec.y - from.y, + z: rec.z - from.z + }; + let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z); + let lerp = util.lerp(0,dist,dw).map(v => v/dist); + let angle = rec.rz = Math.atan2(d.y,d.x); + let fromrec = ws.filter(r => r.id === from.id)[0]; + if (fromrec) { + from.rz = fromrec.rz = angle; + removeWidgetSupport(iw, fromPillar); + ws.push(Object.clone(fromrec)); + addWidgetSupport(iw, fromrec); } - let {widget, x, y, z} = rot; - if (x || y) { - clearWidgetSupports(widget); - } else { - let ann = API.widgets.annotate(widget.id); - let sups = ann.support || []; - sups.forEach(sup => { - let wsup = widget.sups[sup.id]; - let vc = new THREE.Vector3(sup.x, sup.y, sup.z); - let m4 = new THREE.Matrix4(); - m4 = m4.makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); - vc.applyMatrix4(m4); - wsup.box.position.x = wsup.x = sup.x = vc.x; - wsup.box.position.y = wsup.y = sup.y = vc.y; - wsup.box.position.z = wsup.z = sup.z = vc.z; - }); + lerp.pop(); + let seed = Math.random(); + for (let pct of lerp) { + let point = new Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct); + addSupportAtPoint(targets, point, ip, angle, seed++); } - }); - api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { - let { event } = on; - if (!isFdmMode) { - return; - } - if (!addingSupports) { - return; - } - delbox('supp'); - if (lastPillar) { - removeWidgetSupport(lastPillar.widget, lastPillar); - return; - } - if (!iw) return; - let { point, dim } = lastBox; - p1.y = Math.max(0, p1.y); - p2.y = Math.max(0, p2.y); - let rz = dim.rz; - let dh = dim.z; - let dw = api.conf.get().process.sliceSupportSize / 2; - let ip = iw.track.pos; - let wa = api.widgets.annotate(iw.id); - let ws = (wa.support = wa.support || []); - let id = ++nextID; - let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id}; - if (fromPillar && event && event.shiftKey) { - let targets = api.widgets.meshes().append(SPACE.internals().platform); - let from = fromPillar, - d = { - x: rec.x - from.x, - y: rec.y - from.y, - z: rec.z - from.z - }; - let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z); - let lerp = UTIL.lerp(0,dist,dw).map(v => v/dist); - let angle = rec.rz = Math.atan2(d.y,d.x); - let fromrec = ws.filter(r => r.id === from.id)[0]; - if (fromrec) { - from.rz = fromrec.rz = angle; - removeWidgetSupport(iw, fromPillar); - ws.push(Object.clone(fromrec)); - addWidgetSupport(iw, fromrec); - } - lerp.pop(); - let seed = Math.random(); - for (let pct of lerp) { - let point = new THREE.Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct); - addSupportAtPoint(targets, point, ip, angle, seed++); - } - } - ws.push(Object.clone(rec)); - fromPillar = addWidgetSupport(iw, rec); - API.conf.save(); - }); - function addSupportAtPoint(targets, point, ip, angle, id) { - let up = new THREE.Vector3(0,1,0) - let dn = new THREE.Vector3(0,-1,0) - let rp = new THREE.Vector3(point.x + ip.x, point.y, -point.z - ip.y); - let iup = new THREE.Raycaster(rp, up) + } + ws.push(Object.clone(rec)); + fromPillar = addWidgetSupport(iw, rec); + api.conf.save(); + }); + function addSupportAtPoint(targets, point, ip, angle, id) { + let up = new Vector3(0,1,0) + let dn = new Vector3(0,-1,0) + let rp = new Vector3(point.x + ip.x, point.y, -point.z - ip.y); + let iup = new Raycaster(rp, up) + .intersectObjects(targets, false) + .filter(t => !t.object.pillar); + if (iup.length && iup.length % 2 === 0) { + let idn = new Raycaster(rp, dn) .intersectObjects(targets, false) .filter(t => !t.object.pillar); - if (iup.length && iup.length % 2 === 0) { - let idn = new THREE.Raycaster(rp, dn) - .intersectObjects(targets, false) - .filter(t => !t.object.pillar); - if (idn.length && idn.length % 2 === 0) { - iw = iup[0].object.widget || iw; - let wa = api.widgets.annotate(iw.id); - let ws = (wa.support = wa.support || []); - let phi = iup[0].point; - let plo = idn[0].point; - let ply = Math.max(0, plo.y); - let mp = (phi.y + ply) / 2; - let dy = Math.abs(phi.y - ply); - let dw = api.conf.get().process.sliceSupportSize / 2; - let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id }; - ws.push(Object.clone(rec)); - fromPillar = addWidgetSupport(iw, rec); - } + if (idn.length && idn.length % 2 === 0) { + iw = iup[0].object.widget || iw; + let wa = api.widgets.annotate(iw.id); + let ws = (wa.support = wa.support || []); + let phi = iup[0].point; + let plo = idn[0].point; + let ply = Math.max(0, plo.y); + let mp = (phi.y + ply) / 2; + let dy = Math.abs(phi.y - ply); + let dw = api.conf.get().process.sliceSupportSize / 2; + let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id }; + ws.push(Object.clone(rec)); + fromPillar = addWidgetSupport(iw, rec); } } - api.event.on("mouse.hover", data => { - if (!isFdmMode) { - return; - } - if (!addingSupports) { - return; - } - delbox('supp'); - const { int, type, point, event } = data; - const pillar = int ? int.object.pillar : undefined; - if (lastPillar) { - lastPillar.box.material.color.r = 0; - lastPillar = null; - } - if (pillar) { - pillar.box.material.color.r = 0.5; - lastPillar = pillar; - if (event && (event.metaKey || event.ctrlKey)) { - return func.hoverup(); - } - return; - } - if (int && type === 'widget') { - iw = int.object.widget || iw; - } else { - iw = null; - } - p1 = point; - let dir = new THREE.Vector3(0,1,0) - let ray = new THREE.Raycaster(point, dir); - let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; - // when on object, project down on downward faces - if (int && int.face && int.face.normal.z < -0.1) { - dir.y = -1; - } - let targets = api.widgets.meshes().append(SPACE.internals().platform); - let i2 = ray.intersectObjects(targets, false) - .filter(t => !t.object.pillar) // eliminate other pillars - .filter(i => i.distance > 0.1); // false matches close to origin of ray - if (i2.length && i2.length % 2 === 0) { - p2 = i2[0].point; - iw = i2[0].object.widget || iw; - let p1y = Math.max(0, p1.y); - let p2y = Math.max(0, p2.y); - let hy = (p1y + p2y) / 2; - let dy = Math.abs(p1y - p2y); - let dw = api.conf.get().process.sliceSupportSize / 2; - addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', { - x:dw, y:dw, z:dy, rz - }); - } - if (event && event.altKey) { + } + api.event.on("mouse.hover", data => { + if (!isFdmMode) { + return; + } + if (!addingSupports) { + return; + } + delbox('supp'); + const { int, type, point, event } = data; + const pillar = int ? int.object.pillar : undefined; + if (lastPillar) { + lastPillar.box.material.color.r = 0; + lastPillar = null; + } + if (pillar) { + pillar.box.material.color.r = 0.5; + lastPillar = pillar; + if (event && (event.metaKey || event.ctrlKey)) { return func.hoverup(); } - }); - api.event.on("export.debug", msg => { - if (msg.arcQ && msg.arcQ.length > 2) { - if (xpdebug) { - xpdebug.destroy(); - xpdebug = undefined; - } - let poly = BASE.newPolygon().setOpen(); - for (let rec of msg.arcQ) { - rec.z += 1; - poly.addObj(rec); - } - - let layers = new KIRI.Layers(); - let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); - layer.addPoly(poly, {thin:true}); - - layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 }); - for (let center of msg.arcQ.center) { - center.z += 0.5; - layer.addPoly(BASE.newPolygon().centerCircle( - center, center.r, 20 - )); - } - - xpdebug = new KIRI.Stack(SPACE.world, false); - xpdebug.addLayers(layers); - xpdebug.setVisible(0,Infinity); - xpdebug.show(); - - SPACE.refresh(); - - console.log(msg, poly); - } - }); - } - - let xpdebug; - let supsave = {}; - let suptimer; - - function scheduleSupportSave(widget) { - clearTimeout(suptimer); - supsave[widget.id] = widget; - suptimer = setTimeout(() => { - for (let w of Object.values(supsave)) { - w.saveState(); - } - }, 50); - } - - function activeSupports() { - const active = []; - API.widgets.all().forEach(widget => { - Object.values(widget.sups || {}).forEach(support => { - active.push(support.box); - support.box.support = true; - }); - }); - return active; - } - - function restoreSupports(widgets) { - widgets.forEach(widget => { - const supports = API.widgets.annotate(widget.id).support || []; - supports.forEach(pos => { - addWidgetSupport(widget, pos); - }); - }); - } - - function addWidgetSupport(widget, pos) { - const { x, y, z, rz, dw, dh, id } = pos; - const sups = widget.sups = (widget.sups || {}); - // prevent duplicate restore from repeated settings load calls - if (!sups[id]) { - pos.box = addbox( - { x, y, z }, 0x0000dd, id, - { x:dw, y:dw, z:dh, rz }, { group: widget.mesh } - ); - pos.box.pillar = Object.assign({widget}, pos); - sups[id] = pos; - widget.adds.push(pos.box); - scheduleSupportSave(widget); + return; } - return sups[id]; - } - - function removeWidgetSupport(widget, rec) { - const {box, id} = rec; - widget.adds.remove(box); - widget.mesh.remove(box); - delete widget.sups[id]; - let sa = API.widgets.annotate(widget.id).support; - let ix = 0; - sa.forEach((rec,i) => { - if (rec.id === id) { - ix = i; - } - }); - sa.splice(ix,1); - API.conf.save(); - scheduleSupportSave(widget); - fromPillar = undefined; - } - - function updateVisiblity() { - API.widgets.all().forEach(w => { - setSupportVisiblity(w, lastMode === 'FDM' && lastView === VIEWS.ARRANGE); - }); - } - - function setSupportVisiblity(widget, bool) { - Object.values(widget.sups || {}).forEach(support => { - support.box.visible = bool; - }); - } - - function clearAllWidgetSupports() { - let cleared = 0; - API.widgets.all().forEach(widget => { - cleared += clearWidgetSupports(widget); - }); - return cleared; - } - - function clearWidgetSupports(widget) { - let cleared = 0; - Object.values(widget.sups || {}).forEach(support => { - widget.adds.remove(support.box); - widget.mesh.remove(support.box); - cleared++; - }); - widget.sups = {}; - delete API.widgets.annotate(widget.id).support; - scheduleSupportSave(widget); - return cleared; - } - - function delbox(name) { - const old = boxes[name]; - if (old) { - old.groupTo.remove(old); + if (int && type === 'widget') { + iw = int.object.widget || iw; + } else { + iw = null; } - } + p1 = point; + let dir = new Vector3(0,1,0) + let ray = new Raycaster(point, dir); + let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; + // when on object, project down on downward faces + if (int && int.face && int.face.normal.z < -0.1) { + dir.y = -1; + } + let targets = api.widgets.meshes().append(space.internals().platform); + let i2 = ray.intersectObjects(targets, false) + .filter(t => !t.object.pillar) // eliminate other pillars + .filter(i => i.distance > 0.1); // false matches close to origin of ray + if (i2.length && i2.length % 2 === 0) { + p2 = i2[0].point; + iw = i2[0].object.widget || iw; + let p1y = Math.max(0, p1.y); + let p2y = Math.max(0, p2.y); + let hy = (p1y + p2y) / 2; + let dy = Math.abs(p1y - p2y); + let dw = api.conf.get().process.sliceSupportSize / 2; + addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', { + x:dw, y:dw, z:dy, rz + }); + } + if (event && event.altKey) { + return func.hoverup(); + } + }); + api.event.on("export.debug", msg => { + if (msg.arcQ && msg.arcQ.length > 2) { + if (xpdebug) { + xpdebug.destroy(); + xpdebug = undefined; + } + let poly = base.newPolygon().setOpen(); + for (let rec of msg.arcQ) { + rec.z += 1; + poly.addObj(rec); + } - function addbox(point, color, name, dim = {x:1,y:1,z:1,rz:0}, opt = {}) { - delbox(name); - const box = boxes[name] = new THREE.Mesh( - new THREE.BoxGeometry(dim.x, dim.y, dim.z), - new THREE.MeshPhongMaterial({ - transparent: true, - opacity: 0.5, - color - }) + let layers = new kiri.Layers(); + let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); + layer.addPoly(poly, {thin:true}); + + layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 }); + for (let center of msg.arcQ.center) { + center.z += 0.5; + layer.addPoly(base.newPolygon().centerCircle( + center, center.r, 20 + )); + } + + xpdebug = new kiri.Stack(space.world, false); + xpdebug.addLayers(layers); + xpdebug.setVisible(0,Infinity); + xpdebug.show(); + + space.refresh(); + + console.log(msg, poly); + } + }); +} + +let xpdebug; +let supsave = {}; +let suptimer; + +function scheduleSupportSave(widget) { + clearTimeout(suptimer); + supsave[widget.id] = widget; + suptimer = setTimeout(() => { + for (let w of Object.values(supsave)) { + w.saveState(); + } + }, 50); +} + +function activeSupports() { + const active = []; + api.widgets.all().forEach(widget => { + Object.values(widget.sups || {}).forEach(support => { + active.push(support.box); + support.box.support = true; + }); + }); + return active; +} + +function restoreSupports(widgets) { + widgets.forEach(widget => { + const supports = api.widgets.annotate(widget.id).support || []; + supports.forEach(pos => { + addWidgetSupport(widget, pos); + }); + }); +} + +function addWidgetSupport(widget, pos) { + const { x, y, z, rz, dw, dh, id } = pos; + const sups = widget.sups = (widget.sups || {}); + // prevent duplicate restore from repeated settings load calls + if (!sups[id]) { + pos.box = addbox( + { x, y, z }, 0x0000dd, id, + { x:dw, y:dw, z:dh, rz }, { group: widget.mesh } ); - box.position.x = point.x; - box.position.y = point.y; - box.position.z = point.z; + pos.box.pillar = Object.assign({widget}, pos); + sups[id] = pos; + widget.adds.push(pos.box); + scheduleSupportSave(widget); + } + return sups[id]; +} - lastBox = {point, dim}; +function removeWidgetSupport(widget, rec) { + const { box, id } = rec; + widget.adds.remove(box); + widget.mesh.remove(box); + delete widget.sups[id]; + let sa = api.widgets.annotate(widget.id).support; + let ix = 0; + sa.forEach((rec,i) => { + if (rec.id === id) { + ix = i; + } + }); + sa.splice(ix,1); + api.conf.save(); + scheduleSupportSave(widget); + fromPillar = undefined; +} - const group = opt.group || SPACE.scene - group.add(box); - box.groupTo = group; +function updateVisiblity() { + api.widgets.all().forEach(w => { + setSupportVisiblity(w, lastMode === 'FDM' && lastView === VIEWS.ARRANGE); + }); +} - if (dim.rz) { - opt.rotate = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), dim.rz); - } - if (opt.rotate) { - opt.matrix = new THREE.Matrix4().makeRotationFromQuaternion(opt.rotate); - } - if (opt.matrix) { - box.geometry.applyMatrix4(opt.matrix); - } - return box; +function setSupportVisiblity(widget, bool) { + Object.values(widget.sups || {}).forEach(support => { + support.box.visible = bool; + }); +} + +function clearAllWidgetSupports() { + let cleared = 0; + api.widgets.all().forEach(widget => { + cleared += clearWidgetSupports(widget); + }); + return cleared; +} + +function clearWidgetSupports(widget) { + let cleared = 0; + Object.values(widget.sups || {}).forEach(support => { + widget.adds.remove(support.box); + widget.mesh.remove(support.box); + cleared++; + }); + widget.sups = {}; + delete api.widgets.annotate(widget.id).support; + scheduleSupportSave(widget); + return cleared; +} + +function delbox(name) { + const old = boxes[name]; + if (old) { + old.groupTo.remove(old); + } +} + +function addbox(point, color, name, dim = {x:1,y:1,z:1,rz:0}, opt = {}) { + delbox(name); + + const box = boxes[name] = new Mesh( + new BoxGeometry(dim.x, dim.y, dim.z), + new MeshPhongMaterial({ + transparent: true, + opacity: 0.5, + color + }) + ); + box.position.x = point.x; + box.position.y = point.y; + box.position.z = point.z; + + lastBox = {point, dim}; + + const group = opt.group || space.scene + group.add(box); + box.groupTo = group; + + if (dim.rz) { + opt.rotate = new Quaternion().setFromAxisAngle(new Vector3(0,0,1), dim.rz); + } + if (opt.rotate) { + opt.matrix = new Matrix4().makeRotationFromQuaternion(opt.rotate); + } + if (opt.matrix) { + box.geometry.applyMatrix4(opt.matrix); } - FDM.delbox = delbox; - FDM.addbox = addbox; - FDM.restoreSupports = restoreSupports; + return box; +} + +FDM.delbox = delbox; +FDM.addbox = addbox; +FDM.restoreSupports = restoreSupports; })(); diff --git a/src/kiri-mode/fdm/driver.js b/src/kiri-mode/fdm/driver.js index e3eff5d5..d248b63f 100644 --- a/src/kiri-mode/fdm/driver.js +++ b/src/kiri-mode/fdm/driver.js @@ -4,69 +4,66 @@ (function() { - const KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - POLY = BASE.polygons, - FDM = KIRI.driver.FDM = { - // init, // src/mode/fdm/client.js - // slice, // src/mode/fdm/slice.js - // prepare, // src/mode/fdm/prepare.js - // export, // src/mode/fdm/export.js - getRangeParameters - }; +const { kiri } = self; +const FDM = kiri.driver.FDM = { getRangeParameters }; - function getRangeParameters(process, index) { - if (index === undefined || index === null || index < 0) { - return process; - } - let ranges = process.ranges; - if (!(ranges && ranges.length)) { - return process; - } - let params = Object.clone(process); - for (let range of ranges) { - if (index >= range.lo && index <= range.hi) { - for (let [key,value] of Object.entries(range.fields)) { - params[key] = value; - params._range = true; - } +// shared by client and worker contexts +function getRangeParameters(process, index) { + if (index === undefined || index === null || index < 0) { + return process; + } + let ranges = process.ranges; + if (!(ranges && ranges.length)) { + return process; + } + let params = Object.clone(process); + for (let range of ranges) { + if (index >= range.lo && index <= range.hi) { + for (let [key, value] of Object.entries(range.fields)) { + params[key] = value; + params._range = true; } } - return params; } + return params; +} - // defer loading until KIRI.client and KIRI.worker exist - KIRI.load(function(API) { +// defer loading until client and worker exist +kiri.load(function(api) { - if (KIRI.client) - // FDM.support_generate = KIRI.client.fdm_support_generate = function(ondone) { + const { client, worker } = kiri; + + if (client) { FDM.support_generate = function(ondone) { - KIRI.client.clear(); - KIRI.client.sync(); - let settings = API.conf.get(); - let widgets = API.widgets.map(); - KIRI.client.send("fdm_support_generate", { settings }, (gen) => { + client.clear(); + client.sync(); + const settings = api.conf.get(); + const widgets = api.widgets.map(); + client.send("fdm_support_generate", { settings }, (gen) => { if (gen && gen.error) { - API.show.alert('support generation canceled'); + api.show.alert('support generation canceled'); return ondone([]); } - for (let g of gen) g.widget = widgets[g.id]; + for (let g of gen) { + g.widget = widgets[g.id]; + } ondone(gen); }); }; + } - if (KIRI.worker) - KIRI.worker.fdm_support_generate = function(data, send) { + if (worker) { + worker.fdm_support_generate = function(data, send) { const { settings } = data; const widgets = Object.values(wcache); const fresh = widgets.filter(widget => FDM.supports(settings, widget)); - send.done(KIRI.codec.encode(fresh.map(widget => { return { + send.done(kiri.codec.encode(fresh.map(widget => { return { id: widget.id, supports: widget.supports, } } ))); }; + } - }); +}); })(); diff --git a/src/kiri-mode/fdm/export.js b/src/kiri-mode/fdm/export.js index 3539c265..80184290 100644 --- a/src/kiri-mode/fdm/export.js +++ b/src/kiri-mode/fdm/export.js @@ -4,945 +4,940 @@ (function() { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - FDM = KIRI.driver.FDM, - debug = false; +const { base, kiri } = self; +const { util } = base; +const { FDM } = kiri.driver; +const { getRangeParameters } = FDM; - /** - * @returns {Array} gcode lines - */ - FDM.export = function(print, online, ondone, ondebug) { - let layers = print.output, - settings = print.settings, - controller = settings.controller, - thumbnails = controller.exportThumb, - getRangeParameters = FDM.getRangeParameters, - device = settings.device, - extras = device.extras || {}, - extruders = device.extruders, - extused = Object.keys(print.extruders).map(v => parseInt(v)), - gcodeFan = device.gcodeFan, - gcodeLayer = device.gcodeLayer, - gcodeTrack = device.gcodeTrack, - zMoveMax = device.deviceZMax || 0, - tool = 0, - fwRetract = device.fwRetract, - isDanger = controller.danger, - isBelt = device.bedBelt, - bedType = isBelt ? "belt" : "fixed", - extruder = extruders[tool], - offset_x = extruder.extOffsetX, - offset_y = extruder.extOffsetY, - extrudeAbs = device.extrudeAbs || false, - extrudeSet = false, - time = 0, - layer = 0, - layerno = 0, - pause = [], - pauseCmd = device.gcodePause, - output = [], - outputLength = 0, // absolute extruder position - 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}, - lout = {x:0, y:0, z:0}, - last = null, - zpos = 0, - bmax = 0, - blastz = 0, - process = settings.process, - belt_add_y = (process.firstLayerYOffset || 0) - (print.belty || 0), - oloops = process.outputLoops || 0, - zhop = process.zHopDistance || 0, // range - lineWidth = process.sliceLineWidth || 0, - seekMMM = process.outputSeekrate * 60, - retDist = process.outputRetractDist || 0, // range - retSpeed = process.outputRetractSpeed * 60 || 1, // range - retDwell = process.outputRetractDwell || 0, // range - timeDwell = retDwell / 1000, - arcDist = isBelt || !isDanger ? 0 : (process.arcTolerance || 0), - arcMin = 1, - arcRes = 20, - arcDev = 0.5, - arcMax = 40, - originCenter = process.outputOriginCenter || device.bedRound, - offset = originCenter ? { - x: 0, - y: 0, - z: 0 - } : { - x: device.bedWidth/2, - y: isBelt ? 0 : device.bedDepth/2, - z: 0 - }, - nozzleTemp = process.firstLayerNozzleTemp || process.outputTemp, - bedTemp = process.firstLayerBedTemp || process.outputBedTemp, - fanSpeed = undefined, - lastType = undefined, - lastNozzleTemp = nozzleTemp, - lastBedTemp = bedTemp, - lastFanSpeed = fanSpeed, - bounds = settings.bounds, - subst = { - minx: bounds.min.x + offset.x, - maxx: bounds.max.x + offset.x, - miny: bounds.min.y + offset.y, - maxy: bounds.max.y + offset.y, - travel_speed: seekMMM, - retract_speed: retSpeed, - retract_distance: retDist, - temp: nozzleTemp, - temp_bed: bedTemp, - bed_temp: bedTemp, - fan_speed: fanSpeed, - speed: fanSpeed, // legacy - top: offset.y ? device.bedDepth : device.bedDepth/2, - left: offset.x ? 0 : -device.bedWidth/2, - right: offset.x ? device.bedWidth : device.bedWidth/2, - bottom: offset.y ? 0 : -device.bedDepth/2, - z_max: device.maxHeight, - layers: layers.length, - progress: 0, - nozzle: 0, - tool: 0 - }, - pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, - lines = 0, - bytes = 0, - bcos = Math.cos(Math.PI/4), - icos = 1 / bcos, - inloops = 0, - arcQ = [], - minz = { x: Infinity, y: Infinity, z: Infinity }, - // lenghts of each filament (by nozzle) consumed - segments = [], - // palette & ping data - isPalette = device.filamentSource === 'palette3', - paletteInfo = extras.palette || {}, - palettePingStart = Math.max(paletteInfo.ping, paletteInfo.feed, paletteInfo.push) || 500, - palettePingSpace = paletteInfo.ping || 0, - // track purges for palette3 pings - purgePos, - purgeOn = 0, - purgeOff = 0; +FDM.export = function(print, online, ondone, ondebug) { + const { settings, belty, tools } = print; + const { controller, device } = settings; + const { extruders } = device; - let tools_used = Object.keys(print.tools); - for (let tool of tools_used) { - subst[`tool_used_${tool}`] = true; - } - subst.tool_count = tools_used.length; + let layers = print.output, + thumbnails = controller.exportThumb, + extras = device.extras || {}, + extused = Object.keys(print.extruders).map(v => parseInt(v)), + gcodeFan = device.gcodeFan, + gcodeLayer = device.gcodeLayer, + gcodeTrack = device.gcodeTrack, + zMoveMax = device.deviceZMax || 0, + tool = 0, + fwRetract = device.fwRetract, + isDanger = controller.danger, + isBelt = device.bedBelt, + bedType = isBelt ? "belt" : "fixed", + extruder = extruders[tool], + offset_x = extruder.extOffsetX, + offset_y = extruder.extOffsetY, + extrudeAbs = device.extrudeAbs || false, + extrudeSet = false, + time = 0, + layer = 0, + layerno = 0, + pause = [], + pauseCmd = device.gcodePause, + output = [], + outputLength = 0, // absolute extruder position + 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}, + lout = {x:0, y:0, z:0}, + last = null, + zpos = 0, + bmax = 0, + blastz = 0, + process = settings.process, + belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0), + oloops = process.outputLoops || 0, + zhop = process.zHopDistance || 0, // range + lineWidth = process.sliceLineWidth || 0, + seekMMM = process.outputSeekrate * 60, + retDist = process.outputRetractDist || 0, // range + retSpeed = process.outputRetractSpeed * 60 || 1, // range + retDwell = process.outputRetractDwell || 0, // range + timeDwell = retDwell / 1000, + arcDist = isBelt || !isDanger ? 0 : (process.arcTolerance || 0), + arcMin = 1, + arcRes = 20, + arcDev = 0.5, + arcMax = 40, + originCenter = process.outputOriginCenter || device.bedRound, + offset = originCenter ? { + x: 0, + y: 0, + z: 0 + } : { + x: device.bedWidth/2, + y: isBelt ? 0 : device.bedDepth/2, + z: 0 + }, + nozzleTemp = process.firstLayerNozzleTemp || process.outputTemp, + bedTemp = process.firstLayerBedTemp || process.outputBedTemp, + fanSpeed = undefined, + lastType = undefined, + lastNozzleTemp = nozzleTemp, + lastBedTemp = bedTemp, + lastFanSpeed = fanSpeed, + bounds = settings.bounds, + subst = { + minx: bounds.min.x + offset.x, + maxx: bounds.max.x + offset.x, + miny: bounds.min.y + offset.y, + maxy: bounds.max.y + offset.y, + travel_speed: seekMMM, + retract_speed: retSpeed, + retract_distance: retDist, + temp: nozzleTemp, + temp_bed: bedTemp, + bed_temp: bedTemp, + fan_speed: fanSpeed, + speed: fanSpeed, // legacy + top: offset.y ? device.bedDepth : device.bedDepth/2, + left: offset.x ? 0 : -device.bedWidth/2, + right: offset.x ? device.bedWidth : device.bedWidth/2, + bottom: offset.y ? 0 : -device.bedDepth/2, + z_max: device.maxHeight, + layers: layers.length, + progress: 0, + nozzle: 0, + tool: 0 + }, + pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, + lines = 0, + bytes = 0, + bcos = Math.cos(Math.PI/4), + icos = 1 / bcos, + inloops = 0, + arcQ = [], + minz = { x: Infinity, y: Infinity, z: Infinity }, + // lenghts of each filament (by nozzle) consumed + segments = [], + // palette & ping data + isPalette = device.filamentSource === 'palette3', + paletteInfo = extras.palette || {}, + palettePingStart = Math.max(paletteInfo.ping, paletteInfo.feed, paletteInfo.push) || 500, + palettePingSpace = paletteInfo.ping || 0, + // track purges for palette3 pings + purgePos, + purgeOn = 0, + purgeOff = 0; - // smallish band-aid. refactor above to remove redundancy - function updateParams(layer) { - let params = getRangeParameters(process, layer); - zhop = params.zHopDistance || 0; // range - retDist = params.outputRetractDist || 0; // range - retSpeed = params.outputRetractSpeed * 60 || 1; // range - retDwell = params.outputRetractDwell || 0; // range - timeDwell = retDwell / 1000; - nozzleTemp = layer === 0 ? - params.firstLayerNozzleTemp || params.outputTemp : - params.outputTemp || params.firstLayerNozzleTemp; - bedTemp = layer === 0 ? - params.firstLayerBedTemp || params.outputBedTemp : - params.outputBedTemp || params.firstLayerBedTemp; - fanSpeed = layer === 0 ? - params.firstLayerFanSpeed || 0 : - params.outputFanSpeed || 0; - Object.assign(subst, { - temp_bed: bedTemp, - bed_temp: bedTemp, - fan_speed: fanSpeed, - speed: fanSpeed, // legacy - retract_speed: retSpeed, - retract_distance: retDist, - temp: params.outputTemp, // range - temp_bed: params.outputBedTemp, // range - bed_temp: params.outputBedTemp, // range - }); - } + let tools_used = Object.keys(tools); + for (let tool of tools_used) { + subst[`tool_used_${tool}`] = true; + } + subst.tool_count = tools_used.length; - let rloops = []; - if (oloops > 0) { - // if oloops set, loop entire part - rloops.push({ - start: layers[0].slice.index, - end: Infinity, - iter: oloops - 1 - }); - } - if (process.ranges) { - // collect loops from ranges and synth range array - for (let range of process.ranges) { - if (range.fields.outputLoops) { - rloops.push({ - start: range.lo, - end: range.hi, - iter: range.fields.outputLoops - 1 - }); - } - } - } - let loops = isBelt && rloops.length ? rloops : undefined; - - function append(line) { - if (line) { - lines++; - bytes += line.length; - output.append(line); - } - // batch gcode output to free memory in worker - // consider alternate transfer schemes like indexeddb - // since this transfers the burden to the main thread - if (!line || output.length > 1000) { - online(output.join("\n")); - output = []; - } - }; - - function appendSubPad(line, pad) { - appendSub(line, true); - } - - let subon = true; - let ifhit = false; - - // eval() based var substitution (from print) - // with logic flow IF / ELIF / ELSE / END - // IF / IF is valid but will not nest - function appendSub(line, pad) { - if (line.indexOf(';; IF ') === 0) { - line = line.substring(6).trim(); - let evil = print.constReplace(line, subst, 0, 666); - subon = evil; - ifhit = subon; - } else if (line.indexOf(';; ELIF ') === 0) { - line = line.substring(6).trim(); - let evil = print.constReplace(line, subst, 0, 666); - subon = !subon && evil; - ifhit = ifhit || subon; - } else if (line.indexOf(';; ELSE') === 0) { - subon = !ifhit && !subon; - } else if (line.indexOf(';; END') === 0) { - subon = true; - ifhit = false; - } else if (subon) { - append(print.constReplace(line, subst, 0, pad)); - } - } - - function appendAll(arr) { - if (!arr) return; - if (!Array.isArray(arr)) arr = [ arr ]; - arr.forEach(function(line) { append(line) }); - } - - function appendAllSub(arr, pad) { - if (!arr || arr.length === 0) return; - if (!Array.isArray(arr)) arr = [ arr ]; - arr.forEach(function(line) { appendSub(line, pad) }); - } - - function appendTok() { - let arg = [...arguments].map(v => { - return typeof(v) === 'object' ? JSON.stringify(v) : v; - }); - append(arg.join('')); - } - - append(`; Generated by Kiri:Moto ${KIRI.version}`); - append(`; ${new Date().toString()}`); - appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); - append(`; Bed type: ${bedType}`); - append(`; Target: ${settings.filter[settings.mode]}`); - // inject thumbnail preview - if (thumbnails && worker.snap) { - let { width, height, url } = worker.snap; - let data = url.substring(url.indexOf(',') + 1); - append(`; thumbnail begin ${width} ${height} ${data.length}`); - for (let i=0; i 0) { - line.split(";")[0].split(' ').forEach(function (tok) { - // use max E position from gcode-preamble - if (tok[0] == 'E') { - if (extrudeAbs) { - outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); - emitted = outputLength; - } else { - emitted += parseFloat(tok.substring(1) || 0); - } - } - }); - } - if (line.indexOf("{tool}") > 0 && extused.length > 0) { - for (let i of extused) { - subst.tool = i; - appendSubPad(line); - } - subst.tool = 0; - } else { - appendSubPad(line); - } - } - - function dwell(ms) { - time += ms/1000; - // break up dwell times over 4s because of - // limitations in some firmwares - while (ms > 0) { - let wait = Math.min(4000, ms); - append(`G4 P${wait}`); - ms -= wait; - if (ms) { - append('G1'); - } - } - } - - function retract(zhop) { - if (retracted) { - // console.log({double_retract: zhop}); - return; - } - retracted = retDist; - if (fwRetract) { - append('G10'); - } else { - 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 unretract() { - if (!retracted) { - return; - } - drainQ(); - // console.log({engage:zhop}); - // when enabled, resume previous Z - if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end"); - // re-engage retracted filament - if (fwRetract) { - append('G11'); - } else { - moveTo({e:retracted}, retSpeed, `e-engage ${retracted}`); - } - retracted = 0; - // optional dwell after re-engaging filament to allow pressure to build - if (retDwell) dwell(retDwell); - time += (retDist / retSpeed) * 60 * 2; // retraction time - } - - let taxis = new THREE.Vector3( 1, 0, 0 ); - let tcent = new THREE.Vector2( 0, 0 ); - let angle = -Math.PI / 4; - let savePos = pos; - - function pushPos(newpos, rate, comment) { - savePos = Object.clone(pos); - moveTo({ - x: newpos.x + offset.x, - y: newpos.y + offset.y, - z: newpos.z + offset.z - }, rate, comment); - } - - function popPos(rate, comment) { - moveTo(savePos, rate, comment); - } - - function moveTo(newpos, rate, comment) { - if (pingRemain) { - if (newpos.e) { - if (pingRemain - newpos.e < -0.4) { - // split move if ping over-extrudes? complicates emitted calc. - // console.log({over_ping: pingRemain - newpos.e}); - } - pingRemain -= newpos.e; - } - } - let o = [!rate && !newpos.e ? 'G0' : 'G1']; - let emit = { x: false, y: false, z: false }; - if (typeof newpos.x === 'number' && newpos.x !== pos.x) { - pos.x = newpos.x; - emit.x = true; - } - if (typeof newpos.y === 'number' && newpos.y !== pos.y) { - pos.y = newpos.y; - emit.y = true; - if (isBelt) emit.z = true; - } - if (typeof newpos.z === 'number' && newpos.z !== pos.z) { - pos.z = newpos.z; - emit.z = true; - if (isBelt) emit.y = true; - } - let epos = isBelt ? { x: pos.x, y: pos.y, z: pos.z } : pos; - if (isBelt) { - let zheight = path ? path.height || 0 : 0; - epos.x = originCenter ? -pos.x : device.bedWidth - pos.x; - epos.z = blastz = pos.z * icos; - epos.y = -pos.y + epos.z * bcos + belt_add_y; - lout = epos; - } - if (emit.x) o.append(" X").append(epos.x.toFixed(decimals)); - if (emit.y) o.append(" Y").append(epos.y.toFixed(decimals)); - if (emit.z) o.append(" Z").append(epos.z.toFixed(decimals)); - if (debug) { - if (emit.x) minz.x = Math.min(minz.x, epos.x); - if (emit.y) minz.y = Math.min(minz.y, epos.y); - if (emit.z) minz.z = Math.min(minz.z, epos.z); - } - if (typeof newpos.e === 'number') { - outputLength += newpos.e; - if (extrudeAbs) { - // for cumulative (absolute) extruder positions - o.append(" E").append(outputLength.toFixed(decimals)); - } else { - o.append(" E").append(newpos.e.toFixed(decimals)); - } - } - if (zMoveMax && emit.z) { - rate = Math.min(zMoveMax, rate) * 60; - } - if (rate && rate != pos.f) { - o.append(" F").append(Math.round(rate)); - pos.f = rate - } - if (comment) { - o.append(` ; ${comment}`); - } - if (o.length === 1) { - // console.trace({no_move: o, out, newpos, pos, lastp, emit}); - return; - } - 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 = []; - let totaldistance = 0; - layers.forEach(function(outs) { - allout.appendAll(outs); + // smallish band-aid. refactor above to remove redundancy + function updateParams(layer) { + let params = getRangeParameters(process, layer); + zhop = params.zHopDistance || 0; // range + retDist = params.outputRetractDist || 0; // range + retSpeed = params.outputRetractSpeed * 60 || 1; // range + retDwell = params.outputRetractDwell || 0; // range + timeDwell = retDwell / 1000; + nozzleTemp = layer === 0 ? + params.firstLayerNozzleTemp || params.outputTemp : + params.outputTemp || params.firstLayerNozzleTemp; + bedTemp = layer === 0 ? + params.firstLayerBedTemp || params.outputBedTemp : + params.outputBedTemp || params.firstLayerBedTemp; + fanSpeed = layer === 0 ? + params.firstLayerFanSpeed || 0 : + params.outputFanSpeed || 0; + Object.assign(subst, { + temp_bed: bedTemp, + bed_temp: bedTemp, + fan_speed: fanSpeed, + speed: fanSpeed, // legacy + retract_speed: retSpeed, + retract_distance: retDist, + temp: params.outputTemp, // range + temp_bed: params.outputBedTemp, // range + bed_temp: params.outputBedTemp, // range }); - allout.forEachPair(function (o1, o2) { - totaldistance += o1.point.distTo2D(o2.point); - }, 1); + } - // for palette pings, amount of extrusion left - let pingRemain = 0; - - // retract before first move - retract(); - - while (layer < layers.length) { - path = layers[layer]; - layerno = path.slice.index; - - // range overrides - if (path.layer >= 0) { - updateParams(path.layer); + let rloops = []; + if (oloops > 0) { + // if oloops set, loop entire part + rloops.push({ + start: layers[0].slice.index, + end: Infinity, + iter: oloops - 1 + }); + } + if (process.ranges) { + // collect loops from ranges and synth range array + for (let range of process.ranges) { + if (range.fields.outputLoops) { + rloops.push({ + start: range.lo, + end: range.hi, + iter: range.fields.outputLoops - 1 + }); } - - emitPerMM = print.extrudePerMM( - lineWidth || extruder.extNozzle, - extruder.extFilament, - path.layer === 0 ? - (process.firstSliceHeight || process.sliceHeight) : path.height); - - zpos = path.z || zpos; - bmax = Math.max(bmax, pos.z * icos); - subst.z = subst.Z = zpos.round(3); - subst.e = subst.E = outputLength; - subst.layer = layer; - subst.height = path.height; - - if (isBelt) { - pos.z = zpos; - } - - if (pauseCmd && pause.indexOf(layer) >= 0) { - appendAllSub(pauseCmd) - } - - let endloop = 0; - if (loops) { - for (let loop of loops) { - if (layerno === loop.start && !loop.started) { - loop.started = true; - append(`M808 L${loop.iter}`); - if (extrudeAbs) { - append(`G92 Z${lout.z.round(decimals)} E${outputLength.round(decimals)}`); - } else { - append(`G92 Z${lout.z.round(decimals)}`); - } - inloops++; - } - if (layerno === loop.end && !loop.ended) { - loop.ended = true; - endloop++; - inloops--; - } - } - } - - if (gcodeLayer && gcodeLayer.length) { - appendAllSub(gcodeLayer); - } else { - append(`;; --- layer ${layer} (${subst.height.toFixed(3)} @ ${subst.z.round(3)}) ---`); - } - - // layer temp and fan overrides at layer changes - if (fanSpeed !== lastFanSpeed) { - appendAllSub(gcodeFan); - lastFanSpeed = fanSpeed; - } - if (bedTemp !== lastBedTemp) { - append(`M140 S${bedTemp} T0`); - lastBedTemp = bedTemp; - } - if (nozzleTemp !== lastNozzleTemp) { - if (t0) append(`M104 S${nozzleTemp} T0`); - if (t1) append(`M104 S${nozzleTemp} T1`); - if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`); - lastNozzleTemp = nozzleTemp; - } - - // move Z to layer height - if (layer > 0 || !isBelt) { - moveTo({z:zpos}, seekMMM); - } - - // iterate through layer outputs - for (pidx=0; pidx= palettePingStart) { - if (purgeOn === 0 && out.point.purgeOn && emitted - purgeOff >= palettePingSpace) { - retract(); - pushPos(out.point.purgeOn); - // shorter pause accounts for retract/move - dwell(12750); - popPos(); - unretract(); - purgeOn = emitted; - purgePos = out.point.purgeOn; - pingRemain = 20; - } - if (purgeOn && pingRemain <= 0) { - retract(); - pushPos(purgePos); - // shorter pause accounts for retract/move - dwell(6750); - popPos(); - unretract(); - if (!print.purges) { - print.purges = []; - } - print.purges.push({ - length: purgeOn, - extrusion: emitted - purgeOn - }); - purgeOff = emitted; - purgeOn = 0; - pingRemain = 0; - } - } - - // emit comment on output type chage - if (last && out.type !== last.type) { - append(`; feature ${out.type}`); - lastType = out.type; - } - - // look for extruder change, run scripts, recalc emit factor - if (out.tool !== undefined && out.tool != tool) { - segments.push({emitted, tool}); - appendAllSub(extruder.extDeselect); - tool = out.tool; - subst.nozzle = subst.tool = tool; - extruder = extruders[tool]; - offset_x = extruder.extOffsetX; - offset_y = extruder.extOffsetY; - emitPerMM = print.extrudePerMM( - lineWidth || extruder.extNozzle, - extruder.extFilament, - path.layer === 0 ? - (process.firstSliceHeight || process.sliceHeight) : path.height); - // do not run extruder swapping code when source is Palette3 - if (!isPalette) { - appendAllSub(extruder.extSelect); - } - } - - // if no point in output, it's a dwell command - if (!out.point) { - dwell(out.speed); - continue; - } - - let x = out.point.x + offset_x, - y = out.point.y + offset_y, - z = out.point.z; - - // adjust for inversions and origin offsets - if (process.outputInvertX) x = -x; - if (process.outputInvertY) y = -y; - if (offset) { - x += offset.x; - y += offset.y; - } - - dist = lastp ? lastp.distTo2D(out.point) : 0; - - // re-engage post-retraction before new extrusion - if (out.emit && retracted) { - unretract(); - } - - if (lastp && out.emit) { - if (arcDist) { - let rec = {e:out.emit, x, y, z, dist, emitPerMM, speedMMM}; - arcQ.push(rec); - let deem = false; // do arcQ[0] and rec have differing emit values? - let depm = false; // do arcQ[0] and rec have differing emit speeds? - let desp = false; // do arcQ[0] and rec have differing move speeds? - if (arcQ.length > 1) { - let el = arcQ.length; - deem = arcQ[0].e !== rec.e; - depm = arcQ[0].emitPerMM !== rec.emitPerMM; - desp = arcQ[0].speedMMM !== rec.speedMMM; - } - // ondebug({arcQ}); - if (arcQ.length > 2) { - let el = arcQ.length; - let e1 = arcQ[0]; // first in arcQ - let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ - let e3 = arcQ[el-1]; // last in arcQ - let e4 = arcQ[el-2]; // second last in arcQ - let e5 = arcQ[el-3]; // third last in arcQ - let cc = BASE.util.center2d(e1, e2, e3, 1); // find center - let lr = BASE.util.center2d(e3, e4, e5, 1); // find local radius - let dc = 0; - - let radFault = false; - if (lr) { - let angle = 2 * Math.asin(dist/(2*lr.r)); - radFault = Math.abs(angle) > Math.PI * 2 / arcRes; // enforce arcRes(olution) - // if (arcQ.center) { - // arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - // let avg = arcQ.rSum / arcQ.center.length; - // radFault = radFault || Math.abs(avg - lr.r) / avg > arcDev; // eliminate sharps and flats when local rad is out of arcDev(iation) - // } - } else { - radFault = true; - } - - if (cc) { - if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { - console.log({cc, e1, e2, e3}); - } - if (arcQ.length === 3) { - arcQ.center = [ cc ]; - arcQ.xSum = cc.x; - arcQ.ySum = cc.y; - arcQ.rSum = cc.r; - } else { - // check center point delta - arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); - arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); - arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - let dx = cc.x - arcQ.xSum / arcQ.center.length; - let dy = cc.y - arcQ.ySum / arcQ.center.length; - dc = Math.sqrt(dx * dx + dy * dy); - } - - // if new point is off the arc - // if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { - if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) { - // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault]; - if (arcQ.length === 4) { - // not enough points for an arc, drop first point and recalc center - emitQrec(arcQ.shift()); - let tc = BASE.util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); - // the new center is invalid as well. drop the first point - if (!tc) { - emitQrec(arcQ.shift()); - } else { - arcQ.center = [ tc ]; - let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); - if (Math.abs(angle) > Math.PI * 2 / arcRes) { // enforce arcRes on initial angle - emitQrec(arcQ.shift()); - } - } - } else { - // enough to consider an arc, emit and start new arc - let defer = arcQ.pop(); - drainQ(); - // re-add point that was off the last arc - arcQ.push(defer); - } - } else { - // new point is on the arc - arcQ.center.push(cc); - } - } else { - // drainQ on invalid center - drainQ(); - } - } - } else { - emitMM = emitPerMM * out.emit * dist; - moveTo({x:x, y:y, e:emitMM}, speedMMM); - emitted += emitMM; - } - } else { - drainQ(); - moveTo({x:x, y:y}, seekMMM); - // TODO disabling out of plane z moves until a better mechanism - // can be built that doesn't rely on computed zpos from layer heights... - // when making z moves (like polishing) allow slowdown vs fast seek - // let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM; - // moveTo({x:x, y:y, z:z}, moveSpeed); - } - - // retract filament if point retract flag set - if (out.retract) { - drainQ(); - retract(zhop); - } - - // update time and distance (should calc in moveTo() instead) - time += (dist / speedMMM) * 60 * 1.5; - distance += dist; - subst.progress = progress = Math.round((distance / totaldistance) * 100); - - // emit tracked progress - if (gcodeTrack && progress != lastProgress) { - appendAllSub(gcodeTrack); - lastProgress = progress; - } - - lastp = out.point; - laste = out.emit; - } - layer++; - - // end open loop when detected - while (endloop-- > 0) { - append(`M808`); - } - drainQ(); } + } + let loops = isBelt && rloops.length ? rloops : undefined; - function emitQrec(rec) { - let {e, x, y, dist, emitPerMM, speedMMM} = rec; - emitMM = emitPerMM * e * dist; - moveTo({x:x, y:y, e:emitMM}, speedMMM); - emitted += emitMM; + function append(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); } - - function drainQ() { - if (!arcDist) { - return; - } - if (arcQ.length > 4) { - // ondebug({arcQ}); - let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); - let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); - let gc = vec1.cross(vec2) < 0 ? 'G2' : 'G3'; - let from = arcQ[0]; - let to = arcQ.peek(); - arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); - arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); - arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); - let cl = arcQ.center.length; - let cc; - - let angle = BASE.util.thetaDiff( - Math.atan2((from.y - arcQ.ySum / cl), (from.x - arcQ.xSum / cl)), - Math.atan2((to.y - arcQ.ySum / cl), (to.x - arcQ.xSum / cl)), - gc === "G2" - ); - - if (Math.abs(angle) <= 3 * Math.PI / 4) { - cc = BASE.util.center2pr(from, to, arcQ.rSum / cl, gc === "G3"); - } - - if (!cc) { - cc = {x:arcQ.xSum/cl, y:arcQ.ySum/cl, z:arcQ[0].z, r:arcQ.rSum/cl}; - } - - // first arc point - emitQrec(from); - // console.log(arcQ.slice(), arcQ.center); - // console.log({first: from, last: arcQ.peek(), center: cc}); - // rest of arc to final point - let dist = arcQ.slice(1).map(v => v.dist).reduce((a,v) => a+v); - let emit = from.e;//arcQ.slice(1).map(v => v.e).reduce((a,v) => a+v); - emit = (from.emitPerMM * emit * dist); - outputLength += emit; - emitted += emit; - if (extrudeAbs) { - emit = outputLength; - } - // XYR form - // let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} R${cc.r.toFixed(decimals)} E${emit.toFixed(decimals)}`; - // XYIJ form - let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} I${(cc.x - pos.x).toFixed(decimals)} J${(cc.y - pos.y).toFixed(decimals)} E${emit.toFixed(decimals)}`; - let add = pos.f !== from.speedMMM ? ` E${from.speedMMM}` : ''; - append(`${pre}${add} ; merged=${cl-1} len=${dist.toFixed(decimals)} cp=${cc.x.round(2)},${cc.y.round(2)}`); - pos.x = to.x; - pos.y = to.y; - pos.z = to.z; - } else { - for (let rec of arcQ) { - emitQrec(rec); - } - } - arcQ.length = 0; - arcQ.center = undefined; - } - - // comprehensive arc validator - function arcValid() { - if (arcQ.length < 3) { - return false; - } - - let globalCenters = []; // see how a point first the curve within the context of the arc's end points. - - for (let i = 0; i < arcQ.length - 2; i++) { - let cc = UTIL.center2d(arcQ[0], arcQ[i+1], arcQ[arcQ.length - 1], 1); - if (!cc) { - return false; - } - globalCenters.push(cc); - } - - let ac = { // average center - x:globalCenters.reduce( (t,v) => t + v.x , 0) / (globalCenters.length), - y:globalCenters.reduce( (t,v) => t + v.y , 0) / (globalCenters.length), - z:globalCenters.reduce( (t,v) => t + v.z , 0) / (globalCenters.length), - r:globalCenters.reduce( (t,v) => t + v.r , 0) / (globalCenters.length) - }; - - // make sure centers are within specified tolerance - for (let cc of globalCenters) { - let dc = Math.sqrt(Math.pow(cc.x - ac.x, 2) + Math.pow(cc.y - ac.y, 2)); - if (dc / ac.r > arcDist) { - return false; - } - } - - // make sure radii are within specified tolerance - for (let point of arcQ) { - let rad = Math.sqrt(Math.pow(point.x - ac.x, 2) + Math.pow(point.y - ac.y, 2)); - if (Math.abs(rad - ac.r) > ac.r * arcDist) { - return false; - } - } - - // enforce arcRes(olution) - for (let i = 1; i < arcQ.length; i++) { - let angle = 2 * Math.asin(arcQ[i].dist/(2*ac.r)); - if (Math.abs(angle) > Math.PI * 2 / arcRes) { - return false; - } - } - - // check points in the context of neighbors - for (let i = 0; i < arcQ.length - 2; i++) { - let cc = UTIL.center2d(arcQ[i], arcQ[i+1], arcQ[i+2], 1); - if (!cc || Math.abs((cc.r - ac.r) / cc.r) > arcDev) { - return false; - } - } - - return true; - - } - - if (inloops) { - append(`M808`); - } - - segments.push({emitted, tool}); - subst.material = UTIL.round(emitted,2); - subst.time = UTIL.round(time,2); - subst['print_time'] = subst.time; - - append("; --- shutdown ---"); - appendAllSub(device.gcodePost); - append(`; --- filament used: ${subst.material} mm ---`); - append(`; --- print time: ${time.toFixed(0)}s ---`); - - // force emit of buffer - append(); - // console.log({segments, emitted, outputLength}); - print.segments = isPalette ? segments : undefined; - print.distance = emitted; - print.lines = lines; - print.bytes = bytes + lines - 1; - print.time = time; - - if (debug) { - console.log('segments', segments); - console.log('minz', minz); + // batch gcode output to free memory in worker + // consider alternate transfer schemes like indexeddb + // since this transfers the burden to the main thread + if (!line || output.length > 1000) { + online(output.join("\n")); + output = []; } }; + function appendSubPad(line, pad) { + appendSub(line, true); + } + + let subon = true; + let ifhit = false; + + // eval() based var substitution (from print) + // with logic flow IF / ELIF / ELSE / END + // IF / IF is valid but will not nest + function appendSub(line, pad) { + if (line.indexOf(';; IF ') === 0) { + line = line.substring(6).trim(); + let evil = print.constReplace(line, subst, 0, 666); + subon = evil; + ifhit = subon; + } else if (line.indexOf(';; ELIF ') === 0) { + line = line.substring(6).trim(); + let evil = print.constReplace(line, subst, 0, 666); + subon = !subon && evil; + ifhit = ifhit || subon; + } else if (line.indexOf(';; ELSE') === 0) { + subon = !ifhit && !subon; + } else if (line.indexOf(';; END') === 0) { + subon = true; + ifhit = false; + } else if (subon) { + append(print.constReplace(line, subst, 0, pad)); + } + } + + function appendAll(arr) { + if (!arr) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { append(line) }); + } + + function appendAllSub(arr, pad) { + if (!arr || arr.length === 0) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { appendSub(line, pad) }); + } + + function appendTok() { + let arg = [...arguments].map(v => { + return typeof(v) === 'object' ? JSON.stringify(v) : v; + }); + append(arg.join('')); + } + + append(`; Generated by Kiri:Moto ${kiri.version}`); + append(`; ${new Date().toString()}`); + appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); + append(`; Bed type: ${bedType}`); + append(`; Target: ${settings.filter[settings.mode]}`); + // inject thumbnail preview + if (thumbnails && worker.snap) { + let { width, height, url } = worker.snap; + let data = url.substring(url.indexOf(',') + 1); + append(`; thumbnail begin ${width} ${height} ${data.length}`); + for (let i=0; i 0) { + line.split(";")[0].split(' ').forEach(function (tok) { + // use max E position from gcode-preamble + if (tok[0] == 'E') { + if (extrudeAbs) { + outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); + emitted = outputLength; + } else { + emitted += parseFloat(tok.substring(1) || 0); + } + } + }); + } + if (line.indexOf("{tool}") > 0 && extused.length > 0) { + for (let i of extused) { + subst.tool = i; + appendSubPad(line); + } + subst.tool = 0; + } else { + appendSubPad(line); + } + } + + function dwell(ms) { + time += ms/1000; + // break up dwell times over 4s because of + // limitations in some firmwares + while (ms > 0) { + let wait = Math.min(4000, ms); + append(`G4 P${wait}`); + ms -= wait; + if (ms) { + append('G1'); + } + } + } + + function retract(zhop) { + if (retracted) { + // console.log({double_retract: zhop}); + return; + } + retracted = retDist; + if (fwRetract) { + append('G10'); + } else { + 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 unretract() { + if (!retracted) { + return; + } + drainQ(); + // console.log({engage:zhop}); + // when enabled, resume previous Z + if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end"); + // re-engage retracted filament + if (fwRetract) { + append('G11'); + } else { + moveTo({e:retracted}, retSpeed, `e-engage ${retracted}`); + } + retracted = 0; + // optional dwell after re-engaging filament to allow pressure to build + if (retDwell) dwell(retDwell); + time += (retDist / retSpeed) * 60 * 2; // retraction time + } + + let taxis = new THREE.Vector3( 1, 0, 0 ); + let tcent = new THREE.Vector2( 0, 0 ); + let angle = -Math.PI / 4; + let savePos = pos; + + function pushPos(newpos, rate, comment) { + savePos = Object.clone(pos); + moveTo({ + x: newpos.x + offset.x, + y: newpos.y + offset.y, + z: newpos.z + offset.z + }, rate, comment); + } + + function popPos(rate, comment) { + moveTo(savePos, rate, comment); + } + + function moveTo(newpos, rate, comment) { + if (pingRemain) { + if (newpos.e) { + if (pingRemain - newpos.e < -0.4) { + // split move if ping over-extrudes? complicates emitted calc. + // console.log({over_ping: pingRemain - newpos.e}); + } + pingRemain -= newpos.e; + } + } + let o = [!rate && !newpos.e ? 'G0' : 'G1']; + let emit = { x: false, y: false, z: false }; + if (typeof newpos.x === 'number' && newpos.x !== pos.x) { + pos.x = newpos.x; + emit.x = true; + } + if (typeof newpos.y === 'number' && newpos.y !== pos.y) { + pos.y = newpos.y; + emit.y = true; + if (isBelt) emit.z = true; + } + if (typeof newpos.z === 'number' && newpos.z !== pos.z) { + pos.z = newpos.z; + emit.z = true; + if (isBelt) emit.y = true; + } + let epos = isBelt ? { x: pos.x, y: pos.y, z: pos.z } : pos; + if (isBelt) { + let zheight = path ? path.height || 0 : 0; + epos.x = originCenter ? -pos.x : device.bedWidth - pos.x; + epos.z = blastz = pos.z * icos; + epos.y = -pos.y + epos.z * bcos + belt_add_y; + lout = epos; + } + if (emit.x) o.append(" X").append(epos.x.toFixed(decimals)); + if (emit.y) o.append(" Y").append(epos.y.toFixed(decimals)); + if (emit.z) o.append(" Z").append(epos.z.toFixed(decimals)); + if (debug) { + if (emit.x) minz.x = Math.min(minz.x, epos.x); + if (emit.y) minz.y = Math.min(minz.y, epos.y); + if (emit.z) minz.z = Math.min(minz.z, epos.z); + } + if (typeof newpos.e === 'number') { + outputLength += newpos.e; + if (extrudeAbs) { + // for cumulative (absolute) extruder positions + o.append(" E").append(outputLength.toFixed(decimals)); + } else { + o.append(" E").append(newpos.e.toFixed(decimals)); + } + } + if (zMoveMax && emit.z) { + rate = Math.min(zMoveMax, rate) * 60; + } + if (rate && rate != pos.f) { + o.append(" F").append(Math.round(rate)); + pos.f = rate + } + if (comment) { + o.append(` ; ${comment}`); + } + if (o.length === 1) { + // console.trace({no_move: o, out, newpos, pos, lastp, emit}); + return; + } + 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 = []; + let totaldistance = 0; + layers.forEach(function(outs) { + allout.appendAll(outs); + }); + allout.forEachPair(function (o1, o2) { + totaldistance += o1.point.distTo2D(o2.point); + }, 1); + + // for palette pings, amount of extrusion left + let pingRemain = 0; + + // retract before first move + retract(); + + while (layer < layers.length) { + path = layers[layer]; + layerno = path.slice.index; + + // range overrides + if (path.layer >= 0) { + updateParams(path.layer); + } + + emitPerMM = print.extrudePerMM( + lineWidth || extruder.extNozzle, + extruder.extFilament, + path.layer === 0 ? + (process.firstSliceHeight || process.sliceHeight) : path.height); + + zpos = path.z || zpos; + bmax = Math.max(bmax, pos.z * icos); + subst.z = subst.Z = zpos.round(3); + subst.e = subst.E = outputLength; + subst.layer = layer; + subst.height = path.height; + + if (isBelt) { + pos.z = zpos; + } + + if (pauseCmd && pause.indexOf(layer) >= 0) { + appendAllSub(pauseCmd) + } + + let endloop = 0; + if (loops) { + for (let loop of loops) { + if (layerno === loop.start && !loop.started) { + loop.started = true; + append(`M808 L${loop.iter}`); + if (extrudeAbs) { + append(`G92 Z${lout.z.round(decimals)} E${outputLength.round(decimals)}`); + } else { + append(`G92 Z${lout.z.round(decimals)}`); + } + inloops++; + } + if (layerno === loop.end && !loop.ended) { + loop.ended = true; + endloop++; + inloops--; + } + } + } + + if (gcodeLayer && gcodeLayer.length) { + appendAllSub(gcodeLayer); + } else { + append(`;; --- layer ${layer} (${subst.height.toFixed(3)} @ ${subst.z.round(3)}) ---`); + } + + // layer temp and fan overrides at layer changes + if (fanSpeed !== lastFanSpeed) { + appendAllSub(gcodeFan); + lastFanSpeed = fanSpeed; + } + if (bedTemp !== lastBedTemp) { + append(`M140 S${bedTemp} T0`); + lastBedTemp = bedTemp; + } + if (nozzleTemp !== lastNozzleTemp) { + if (t0) append(`M104 S${nozzleTemp} T0`); + if (t1) append(`M104 S${nozzleTemp} T1`); + if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`); + lastNozzleTemp = nozzleTemp; + } + + // move Z to layer height + if (layer > 0 || !isBelt) { + moveTo({z:zpos}, seekMMM); + } + + // iterate through layer outputs + for (pidx=0; pidx= palettePingStart) { + if (purgeOn === 0 && out.point.purgeOn && emitted - purgeOff >= palettePingSpace) { + retract(); + pushPos(out.point.purgeOn); + // shorter pause accounts for retract/move + dwell(12750); + popPos(); + unretract(); + purgeOn = emitted; + purgePos = out.point.purgeOn; + pingRemain = 20; + } + if (purgeOn && pingRemain <= 0) { + retract(); + pushPos(purgePos); + // shorter pause accounts for retract/move + dwell(6750); + popPos(); + unretract(); + if (!print.purges) { + print.purges = []; + } + print.purges.push({ + length: purgeOn, + extrusion: emitted - purgeOn + }); + purgeOff = emitted; + purgeOn = 0; + pingRemain = 0; + } + } + + // emit comment on output type chage + if (last && out.type !== last.type) { + append(`; feature ${out.type}`); + lastType = out.type; + } + + // look for extruder change, run scripts, recalc emit factor + if (out.tool !== undefined && out.tool != tool) { + segments.push({emitted, tool}); + appendAllSub(extruder.extDeselect); + tool = out.tool; + subst.nozzle = subst.tool = tool; + extruder = extruders[tool]; + offset_x = extruder.extOffsetX; + offset_y = extruder.extOffsetY; + emitPerMM = print.extrudePerMM( + lineWidth || extruder.extNozzle, + extruder.extFilament, + path.layer === 0 ? + (process.firstSliceHeight || process.sliceHeight) : path.height); + // do not run extruder swapping code when source is Palette3 + if (!isPalette) { + appendAllSub(extruder.extSelect); + } + } + + // if no point in output, it's a dwell command + if (!out.point) { + dwell(out.speed); + continue; + } + + let x = out.point.x + offset_x, + y = out.point.y + offset_y, + z = out.point.z; + + // adjust for inversions and origin offsets + if (process.outputInvertX) x = -x; + if (process.outputInvertY) y = -y; + if (offset) { + x += offset.x; + y += offset.y; + } + + dist = lastp ? lastp.distTo2D(out.point) : 0; + + // re-engage post-retraction before new extrusion + if (out.emit && retracted) { + unretract(); + } + + if (lastp && out.emit) { + if (arcDist) { + let rec = {e:out.emit, x, y, z, dist, emitPerMM, speedMMM}; + arcQ.push(rec); + let deem = false; // do arcQ[0] and rec have differing emit values? + let depm = false; // do arcQ[0] and rec have differing emit speeds? + let desp = false; // do arcQ[0] and rec have differing move speeds? + if (arcQ.length > 1) { + let el = arcQ.length; + deem = arcQ[0].e !== rec.e; + depm = arcQ[0].emitPerMM !== rec.emitPerMM; + desp = arcQ[0].speedMMM !== rec.speedMMM; + } + // ondebug({arcQ}); + if (arcQ.length > 2) { + let el = arcQ.length; + let e1 = arcQ[0]; // first in arcQ + let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ + let e3 = arcQ[el-1]; // last in arcQ + let e4 = arcQ[el-2]; // second last in arcQ + let e5 = arcQ[el-3]; // third last in arcQ + let cc = util.center2d(e1, e2, e3, 1); // find center + let lr = util.center2d(e3, e4, e5, 1); // find local radius + let dc = 0; + + let radFault = false; + if (lr) { + let angle = 2 * Math.asin(dist/(2*lr.r)); + radFault = Math.abs(angle) > Math.PI * 2 / arcRes; // enforce arcRes(olution) + // if (arcQ.center) { + // arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + // let avg = arcQ.rSum / arcQ.center.length; + // radFault = radFault || Math.abs(avg - lr.r) / avg > arcDev; // eliminate sharps and flats when local rad is out of arcDev(iation) + // } + } else { + radFault = true; + } + + if (cc) { + if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { + console.log({cc, e1, e2, e3}); + } + if (arcQ.length === 3) { + arcQ.center = [ cc ]; + arcQ.xSum = cc.x; + arcQ.ySum = cc.y; + arcQ.rSum = cc.r; + } else { + // check center point delta + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + let dx = cc.x - arcQ.xSum / arcQ.center.length; + let dy = cc.y - arcQ.ySum / arcQ.center.length; + dc = Math.sqrt(dx * dx + dy * dy); + } + + // if new point is off the arc + // if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { + if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) { + // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault]; + if (arcQ.length === 4) { + // not enough points for an arc, drop first point and recalc center + emitQrec(arcQ.shift()); + let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); + // the new center is invalid as well. drop the first point + if (!tc) { + emitQrec(arcQ.shift()); + } else { + arcQ.center = [ tc ]; + let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); + if (Math.abs(angle) > Math.PI * 2 / arcRes) { // enforce arcRes on initial angle + emitQrec(arcQ.shift()); + } + } + } else { + // enough to consider an arc, emit and start new arc + let defer = arcQ.pop(); + drainQ(); + // re-add point that was off the last arc + arcQ.push(defer); + } + } else { + // new point is on the arc + arcQ.center.push(cc); + } + } else { + // drainQ on invalid center + drainQ(); + } + } + } else { + emitMM = emitPerMM * out.emit * dist; + moveTo({x:x, y:y, e:emitMM}, speedMMM); + emitted += emitMM; + } + } else { + drainQ(); + moveTo({x:x, y:y}, seekMMM); + // TODO disabling out of plane z moves until a better mechanism + // can be built that doesn't rely on computed zpos from layer heights... + // when making z moves (like polishing) allow slowdown vs fast seek + // let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM; + // moveTo({x:x, y:y, z:z}, moveSpeed); + } + + // retract filament if point retract flag set + if (out.retract) { + drainQ(); + retract(zhop); + } + + // update time and distance (should calc in moveTo() instead) + time += (dist / speedMMM) * 60 * 1.5; + distance += dist; + subst.progress = progress = Math.round((distance / totaldistance) * 100); + + // emit tracked progress + if (gcodeTrack && progress != lastProgress) { + appendAllSub(gcodeTrack); + lastProgress = progress; + } + + lastp = out.point; + laste = out.emit; + } + layer++; + + // end open loop when detected + while (endloop-- > 0) { + append(`M808`); + } + drainQ(); + } + + function emitQrec(rec) { + let {e, x, y, dist, emitPerMM, speedMMM} = rec; + emitMM = emitPerMM * e * dist; + moveTo({x:x, y:y, e:emitMM}, speedMMM); + emitted += emitMM; + } + + function drainQ() { + if (!arcDist) { + return; + } + if (arcQ.length > 4) { + // ondebug({arcQ}); + let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); + let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); + let gc = vec1.cross(vec2) < 0 ? 'G2' : 'G3'; + let from = arcQ[0]; + let to = arcQ.peek(); + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + let cl = arcQ.center.length; + let cc; + + let angle = util.thetaDiff( + Math.atan2((from.y - arcQ.ySum / cl), (from.x - arcQ.xSum / cl)), + Math.atan2((to.y - arcQ.ySum / cl), (to.x - arcQ.xSum / cl)), + gc === "G2" + ); + + if (Math.abs(angle) <= 3 * Math.PI / 4) { + cc = util.center2pr(from, to, arcQ.rSum / cl, gc === "G3"); + } + + if (!cc) { + cc = {x:arcQ.xSum/cl, y:arcQ.ySum/cl, z:arcQ[0].z, r:arcQ.rSum/cl}; + } + + // first arc point + emitQrec(from); + // console.log(arcQ.slice(), arcQ.center); + // console.log({first: from, last: arcQ.peek(), center: cc}); + // rest of arc to final point + let dist = arcQ.slice(1).map(v => v.dist).reduce((a,v) => a+v); + let emit = from.e;//arcQ.slice(1).map(v => v.e).reduce((a,v) => a+v); + emit = (from.emitPerMM * emit * dist); + outputLength += emit; + emitted += emit; + if (extrudeAbs) { + emit = outputLength; + } + // XYR form + // let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} R${cc.r.toFixed(decimals)} E${emit.toFixed(decimals)}`; + // XYIJ form + let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} I${(cc.x - pos.x).toFixed(decimals)} J${(cc.y - pos.y).toFixed(decimals)} E${emit.toFixed(decimals)}`; + let add = pos.f !== from.speedMMM ? ` E${from.speedMMM}` : ''; + append(`${pre}${add} ; merged=${cl-1} len=${dist.toFixed(decimals)} cp=${cc.x.round(2)},${cc.y.round(2)}`); + pos.x = to.x; + pos.y = to.y; + pos.z = to.z; + } else { + for (let rec of arcQ) { + emitQrec(rec); + } + } + arcQ.length = 0; + arcQ.center = undefined; + } + + // comprehensive arc validator + function arcValid() { + if (arcQ.length < 3) { + return false; + } + + let globalCenters = []; // see how a point first the curve within the context of the arc's end points. + + for (let i = 0; i < arcQ.length - 2; i++) { + let cc = util.center2d(arcQ[0], arcQ[i+1], arcQ[arcQ.length - 1], 1); + if (!cc) { + return false; + } + globalCenters.push(cc); + } + + let ac = { // average center + x:globalCenters.reduce( (t,v) => t + v.x , 0) / (globalCenters.length), + y:globalCenters.reduce( (t,v) => t + v.y , 0) / (globalCenters.length), + z:globalCenters.reduce( (t,v) => t + v.z , 0) / (globalCenters.length), + r:globalCenters.reduce( (t,v) => t + v.r , 0) / (globalCenters.length) + }; + + // make sure centers are within specified tolerance + for (let cc of globalCenters) { + let dc = Math.sqrt(Math.pow(cc.x - ac.x, 2) + Math.pow(cc.y - ac.y, 2)); + if (dc / ac.r > arcDist) { + return false; + } + } + + // make sure radii are within specified tolerance + for (let point of arcQ) { + let rad = Math.sqrt(Math.pow(point.x - ac.x, 2) + Math.pow(point.y - ac.y, 2)); + if (Math.abs(rad - ac.r) > ac.r * arcDist) { + return false; + } + } + + // enforce arcRes(olution) + for (let i = 1; i < arcQ.length; i++) { + let angle = 2 * Math.asin(arcQ[i].dist/(2*ac.r)); + if (Math.abs(angle) > Math.PI * 2 / arcRes) { + return false; + } + } + + // check points in the context of neighbors + for (let i = 0; i < arcQ.length - 2; i++) { + let cc = util.center2d(arcQ[i], arcQ[i+1], arcQ[i+2], 1); + if (!cc || Math.abs((cc.r - ac.r) / cc.r) > arcDev) { + return false; + } + } + + return true; + + } + + if (inloops) { + append(`M808`); + } + + segments.push({emitted, tool}); + subst.material = util.round(emitted,2); + subst.time = util.round(time,2); + subst['print_time'] = subst.time; + + append("; --- shutdown ---"); + appendAllSub(device.gcodePost); + append(`; --- filament used: ${subst.material} mm ---`); + append(`; --- print time: ${time.toFixed(0)}s ---`); + + // force emit of buffer + append(); + // console.log({segments, emitted, outputLength}); + print.segments = isPalette ? segments : undefined; + print.distance = emitted; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.time = time; + + if (debug) { + console.log('segments', segments); + console.log('minz', minz); + } +}; + })(); diff --git a/src/kiri-mode/fdm/fill.js b/src/kiri-mode/fdm/fill.js index 5c6bec19..5614f053 100644 --- a/src/kiri-mode/fdm/fill.js +++ b/src/kiri-mode/fdm/fill.js @@ -4,317 +4,317 @@ (function() { - if (self.kiri.fill) return; +if (self.kiri.fill) return; - const KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - ROUND = UTIL.round, - DEG2RAD = Math.PI / 180, - FILL = self.kiri.fill = { - hex: fillHexFull, - grid: fillGrid, - gyroid: fillGyroid, - triangle: fillTriangle, - linear: fillLinear, - cubic: fillCubic - }, - CACHE = self.kiri.fill_fixed = { - hex: fillHexFull, - grid: fillGrid, - triangle: fillTriangle - }; +const KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + ROUND = UTIL.round, + DEG2RAD = Math.PI / 180, + FILL = self.kiri.fill = { + hex: fillHexFull, + grid: fillGrid, + gyroid: fillGyroid, + triangle: fillTriangle, + linear: fillLinear, + cubic: fillCubic + }, + CACHE = self.kiri.fill_fixed = { + hex: fillHexFull, + grid: fillGrid, + triangle: fillTriangle + }; - function fillHexFull(target) { - fillHex(target, true); - } +function fillHexFull(target) { + fillHex(target, true); +} - /** - * emitter creates a hex infill pattern and sends to target - * - * @param {Object} target - * @param {boolean} full continuous walls - */ - function fillHex(target, full) { - // compute segment lengths (vert/horiz and 45) - let spacing = target.offset(); - // let vhlen = (1 / target.density()) * (target.lineWidth() + spacing); - let vhlen = (1 / target.density()) * target.lineWidth() * 0.5; - let anxlen = ROUND(Math.cos(30 * DEG2RAD) * vhlen, 7); - let anylen = ROUND(Math.sin(30 * DEG2RAD) * vhlen, 7); - let bounds = target.bounds(); - let even = true; - let evenZ = target.zIndex() % 2 === 0; - let maxy = bounds.max.y + (vhlen + anylen * 2); - let x, y; +/** + * emitter creates a hex infill pattern and sends to target + * + * @param {Object} target + * @param {boolean} full continuous walls + */ +function fillHex(target, full) { + // compute segment lengths (vert/horiz and 45) + let spacing = target.offset(); + // let vhlen = (1 / target.density()) * (target.lineWidth() + spacing); + let vhlen = (1 / target.density()) * target.lineWidth() * 0.5; + let anxlen = ROUND(Math.cos(30 * DEG2RAD) * vhlen, 7); + let anylen = ROUND(Math.sin(30 * DEG2RAD) * vhlen, 7); + let bounds = target.bounds(); + let even = true; + let evenZ = target.zIndex() % 2 === 0; + let maxy = bounds.max.y + (vhlen + anylen * 2); + let x, y; - if (full || evenZ) { - x = bounds.min.x; - for (;;) { - if (even && x > bounds.max.x) break; - if (!even && x > bounds.max.x + anxlen + spacing) break; - y = bounds.min.y; - target.newline(); - while (y <= maxy) { - target.emit(x,y); - y += vhlen; - target.emit(x,y); - if (even) x += anxlen; else x -= anxlen; - y += anylen; - target.emit(x,y); - y += vhlen; - target.emit(x,y); - if (even) x -= anxlen; else x += anxlen; - y += anylen; - } - x += spacing; - if (even) x += (anxlen * 2); - even = !even; - target.newline(); - } - } else { - y = bounds.min.y + vhlen; - for (;;) { - if (even && y > bounds.max.y) break; - if (!even && y > bounds.max.y + anylen) break; - x = bounds.min.x; - target.newline(); - while (x < bounds.max.x) { - target.emit(x,y); - if (even) y += anylen; else y -= anylen; - x += anxlen; - target.emit(x,y); - x += spacing; - target.emit(x,y); - if (even) y -= anylen; else y += anylen; - x += anxlen; - target.emit(x,y); - x += spacing; - } - y += vhlen; - if (even) y += (anylen * 2); - even = !even; - target.newline(); - } - } - } - - function fillGyroid(target) { - let bounds = target.bounds(); - let height = target.zHeight(); - let span_x = bounds.max.x - bounds.min.x; - let span_y = bounds.max.y - bounds.min.y; - let density = target.density(); - let tile = 1 + (1 - density) * 15; - let tile_x = span_x / tile; - let tile_y = span_y / tile; - let tile_z = 1 / tile; - let gyroid = BASE.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500); - - // gyroid.polys.forEach(poly => { - // for (let tx=0; tx<=tile_x; tx++) { - // for (let ty=0; ty<=tile_y; ty++) { - // target.newline(); - // let bx = tx * tile + bounds.min.x; - // let by = ty * tile + bounds.min.y; - // poly.forEach(point => { - // target.emit(bx + point.x * tile, by + point.y * tile); - // }); - // } - // } - // }); - - let polys = []; - for (let tx=0; tx<=tile_x; tx++) { - for (let ty=0; ty<=tile_y; ty++) { - for (let poly of gyroid.polys) { - target.newline(); - let points = poly.map(el => { - return { - x: el.x * tile + tx * tile + bounds.min.x, - y: el.y * tile + ty * tile + bounds.min.y, - z: 0 - } - }); - polys.push(BASE.newPolygon().setOpen(true).addObj(points)); - } - } - } - polys = connectOpenPolys(polys); - for (let poly of polys.filter(p => p.perimeter() > 2)) { + if (full || evenZ) { + x = bounds.min.x; + for (;;) { + if (even && x > bounds.max.x) break; + if (!even && x > bounds.max.x + anxlen + spacing) break; + y = bounds.min.y; target.newline(); - for (let point of poly.points) { - target.emit(point.x, point.y); + while (y <= maxy) { + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x += anxlen; else x -= anxlen; + y += anylen; + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x -= anxlen; else x += anxlen; + y += anylen; + } + x += spacing; + if (even) x += (anxlen * 2); + even = !even; + target.newline(); + } + } else { + y = bounds.min.y + vhlen; + for (;;) { + if (even && y > bounds.max.y) break; + if (!even && y > bounds.max.y + anylen) break; + x = bounds.min.x; + target.newline(); + while (x < bounds.max.x) { + target.emit(x,y); + if (even) y += anylen; else y -= anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + target.emit(x,y); + if (even) y -= anylen; else y += anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + } + y += vhlen; + if (even) y += (anylen * 2); + even = !even; + target.newline(); + } + } +} + +function fillGyroid(target) { + let bounds = target.bounds(); + let height = target.zHeight(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let density = target.density(); + let tile = 1 + (1 - density) * 15; + let tile_x = span_x / tile; + let tile_y = span_y / tile; + let tile_z = 1 / tile; + let gyroid = BASE.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500); + + // gyroid.polys.forEach(poly => { + // for (let tx=0; tx<=tile_x; tx++) { + // for (let ty=0; ty<=tile_y; ty++) { + // target.newline(); + // let bx = tx * tile + bounds.min.x; + // let by = ty * tile + bounds.min.y; + // poly.forEach(point => { + // target.emit(bx + point.x * tile, by + point.y * tile); + // }); + // } + // } + // }); + + let polys = []; + for (let tx=0; tx<=tile_x; tx++) { + for (let ty=0; ty<=tile_y; ty++) { + for (let poly of gyroid.polys) { + target.newline(); + let points = poly.map(el => { + return { + x: el.x * tile + tx * tile + bounds.min.x, + y: el.y * tile + ty * tile + bounds.min.y, + z: 0 + } + }); + polys.push(BASE.newPolygon().setOpen(true).addObj(points)); } } } + polys = connectOpenPolys(polys); + for (let poly of polys.filter(p => p.perimeter() > 2)) { + target.newline(); + for (let point of poly.points) { + target.emit(point.x, point.y); + } + } +} - function connectOpenPolys(noff, dist = 0.1) { - if (noff.length <= 1) { - return noff; - } - let heal = 0; - // heal/rejoin open segments that have close endpoints - outer: for(;; heal++) { - let ntmp = noff, tlen = ntmp.length; - for (let i=0; i 0) { - // cull nulls - noff = noff.filter(o => o); - } +function connectOpenPolys(noff, dist = 0.1) { + if (noff.length <= 1) { return noff; } - - function fillGrid(target) { - let bounds = target.bounds(); - let height = target.zHeight(); - let span_x = bounds.max.x - bounds.min.x; - let span_y = bounds.max.y - bounds.min.y; - let offset = target.offset() / 2; - let tile = (1 / target.density()) * target.lineWidth(); - let tile_x = tile + offset; - let tile_xc = span_x / tile_x; - let tile_yc = span_y / tile; - - for (let tx=0; tx<=tile_xc; tx++) { - target.newline(); - for (let ty=0; ty<=tile_yc; ty++) { - let bx = tx * tile_x + bounds.min.x; - let by = ty * tile + bounds.min.y; - if ((tx + ty) % 2) { - target.emit(bx, by); - target.emit(bx + tile_x - offset, by + tile); - } else { - target.emit(bx + tile_x - offset, by); - target.emit(bx, by + tile); + let heal = 0; + // heal/rejoin open segments that have close endpoints + outer: for(;; heal++) { + let ntmp = noff, tlen = ntmp.length; + for (let i=0; i 0) { + // cull nulls + noff = noff.filter(o => o); } + return noff; +} - function fillLinear(target) { - let bounds = target.bounds(); - let span = Math.max( - bounds.max.x - bounds.min.x, - bounds.max.y - bounds.min.y - ); - let steps = Math.floor((span / target.lineWidth()) * target.density()); - let step = span / steps; - let ztype = Math.floor(target.zIndex() / target.repeat()) % 2; - if (ztype === 1) { - for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { - target.newline(); - target.emit(tx, bounds.min.y); - target.emit(tx, bounds.max.y); - } - } else if (ztype === 0) { - for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { - target.newline(); - target.emit(bounds.min.x, ty); - target.emit(bounds.max.x, ty); - } - } - } +function fillGrid(target) { + let bounds = target.bounds(); + let height = target.zHeight(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let offset = target.offset() / 2; + let tile = (1 / target.density()) * target.lineWidth(); + let tile_x = tile + offset; + let tile_xc = span_x / tile_x; + let tile_yc = span_y / tile; - function fillTriangle(target) { - let bounds = target.bounds(); - let span_x = bounds.max.x - bounds.min.x; - let span_y = bounds.max.y - bounds.min.y; - let offset = target.offset(); - let line_w = target.lineWidth() / 2; - let tile = (1 / target.density()) * (target.lineWidth() * 1.25); - let tile_x = tile + offset*2 + line_w; - let tile_xc = span_x / tile_x; - let tile_yc = span_y / tile; - - for (let tx=0; tx<=tile_xc; tx++) { - target.newline(); - for (let ty=0; ty<=tile_yc; ty++) { - let bx = tx * tile_x + bounds.min.x; - let by = ty * tile + bounds.min.y; - if ((tx + ty) % 2) { - target.emit(bx, by); - target.emit(bx + tile_x - offset - line_w, by + tile); - } else { - target.emit(bx + tile_x - offset - line_w, by); - target.emit(bx, by + tile); - } - } - } - - for (let tx=0; tx<=tile_xc; tx++) { + for (let tx=0; tx<=tile_xc; tx++) { + target.newline(); + for (let ty=0; ty<=tile_yc; ty++) { let bx = tx * tile_x + bounds.min.x; - let xp = bx + tile_x - line_w/2 - offset/2; - target.newline(); - target.emit(xp, bounds.min.y); - target.emit(xp, bounds.max.y); + let by = ty * tile + bounds.min.y; + if ((tx + ty) % 2) { + target.emit(bx, by); + target.emit(bx + tile_x - offset, by + tile); + } else { + target.emit(bx + tile_x - offset, by); + target.emit(bx, by + tile); + } } } +} + +function fillCubic(target) { + let bounds = target.bounds(); + let span = Math.max( + bounds.max.x - bounds.min.x, + bounds.max.y - bounds.min.y + ); + let steps = Math.floor((span / target.lineWidth()) * target.density()); + let step = span / steps; + let ztype = Math.floor(target.zIndex() / target.repeat()) % 3; + if (ztype === 1) { + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx, bounds.max.y); + } + } else if (ztype === 0) { + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.max.x, ty); + } + } else { + step *- Math.sqrt(2); + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx + 1000, bounds.max.y + 1000); + } + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.min.x + 1000, ty + 1000); + } + } +} + +function fillLinear(target) { + let bounds = target.bounds(); + let span = Math.max( + bounds.max.x - bounds.min.x, + bounds.max.y - bounds.min.y + ); + let steps = Math.floor((span / target.lineWidth()) * target.density()); + let step = span / steps; + let ztype = Math.floor(target.zIndex() / target.repeat()) % 2; + if (ztype === 1) { + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx, bounds.max.y); + } + } else if (ztype === 0) { + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.max.x, ty); + } + } +} + +function fillTriangle(target) { + let bounds = target.bounds(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let offset = target.offset(); + let line_w = target.lineWidth() / 2; + let tile = (1 / target.density()) * (target.lineWidth() * 1.25); + let tile_x = tile + offset*2 + line_w; + let tile_xc = span_x / tile_x; + let tile_yc = span_y / tile; + + for (let tx=0; tx<=tile_xc; tx++) { + target.newline(); + for (let ty=0; ty<=tile_yc; ty++) { + let bx = tx * tile_x + bounds.min.x; + let by = ty * tile + bounds.min.y; + if ((tx + ty) % 2) { + target.emit(bx, by); + target.emit(bx + tile_x - offset - line_w, by + tile); + } else { + target.emit(bx + tile_x - offset - line_w, by); + target.emit(bx, by + tile); + } + } + } + + for (let tx=0; tx<=tile_xc; tx++) { + let bx = tx * tile_x + bounds.min.x; + let xp = bx + tile_x - line_w/2 - offset/2; + target.newline(); + target.emit(xp, bounds.min.y); + target.emit(xp, bounds.max.y); + } +} })(); diff --git a/src/kiri-mode/fdm/prepare.js b/src/kiri-mode/fdm/prepare.js index 0d0a98dc..55b466c5 100644 --- a/src/kiri-mode/fdm/prepare.js +++ b/src/kiri-mode/fdm/prepare.js @@ -4,1133 +4,1742 @@ (function() { - let KIRI = self.kiri, - BASE = self.base, - UTIL = BASE.util, - POLY = BASE.polygons, - PATH = BASE.paths, - FDM = KIRI.driver.FDM, - { newPoint, newPolygon } = BASE, - { getRangeParameters } = FDM, - { poly2polyEmit } = PATH, - debug = false; +const { base, kiri, noop } = self; +const { consts, driver, newSlice, utils } = kiri; +const { config, polygons, paths, util, newPoint, newPolygon, Polygon } = base; +const { poly2polyEmit, tip2tipEmit } = paths; +const { numOrDefault } = util; +const { fillArea } = polygons; +const { beltfact } = consts; +const { FDM } = driver; +const { getRangeParameters } = FDM; +const POLY = polygons; - /** - * DRIVER PRINT CONTRACT - * - * @param {Function} update progress callback - * @returns {Object[]} returns array of render objects - */ - FDM.prepare = function(widgets, settings, update) { - // filter ignored widgets - widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); +/** + * DRIVER PRINT CONTRACT + * + * @param {Function} update progress callback + * @returns {Object[]} returns array of render objects + */ +FDM.prepare = function(widgets, settings, update) { + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); - let render = settings.render !== false, - { device, process, controller, bounds, mode } = settings, - { bedWidth, bedDepth } = device, - output = [], - printPoint = newPoint(0,0,0), - nozzle = device.extruders[0].extNozzle, - isBelt = device.bedBelt, - isThin = controller.lineType === "line", - isFlat = controller.lineType === "flat", - isDanger = controller.danger || false, - isPalette = device.filamentSource === 'palette3', - firstLayerHeight = isBelt ? process.sliceHeight : process.firstSliceHeight || process.sliceHeight, - firstLayerSeek = process.outputSeekrate, - firstLayerRate = process.firstLayerRate, - firstLayerMult = process.firstLayerPrintMult, - purgeTower = process.outputPurgeTower || 0, - layerRetract = process.outputLayerRetract, - draftShield = process.outputDraftShield, - layerno = 0, - zoff = 0, - zmin = 0, - shield, - layerout = [], - print = self.worker.print = KIRI.newPrint(settings, widgets), - beltfact = Math.cos(Math.PI/4); + let { device, process, controller, bounds, mode } = settings, + { sliceHeight, firstSliceHeight, firstLayerRate } = process, + { outputSeekrate, outputLayerRetract, outputDraftShield, outputPurgeTower } = process, + { bedWidth, bedDepth, filamentSource } = device, + { lineType, danger } = controller, + printPoint = newPoint(0,0,0), + print = self.worker.print = kiri.newPrint(settings, widgets), + nozzle = device.extruders[0].extNozzle, + isBelt = device.bedBelt, + isThin = lineType === "line", + isFlat = lineType === "flat", + isDanger = danger || false, + isPalette = filamentSource === 'palette3', + firstLayerHeight = isBelt ? sliceHeight : firstSliceHeight || sliceHeight, + layerno = 0, + zoff = 0, + zmin = 0, + shield, + output = [], + layerout = []; - // compute bounds if missing - if (!bounds) { - bounds = new THREE.Box3(); - for (let widget of widgets) { - let wp = widget.track.pos; - let wb = widget.bounds.clone(); - wb.min.x += wp.x; - wb.max.x += wp.x; - wb.min.y += wp.y; - wb.max.y += wp.y; - bounds.union(wb); + // compute bounds if missing + if (!bounds) { + bounds = new THREE.Box3(); + for (let widget of widgets) { + let wp = widget.track.pos; + let wb = widget.bounds.clone(); + wb.min.x += wp.x; + wb.max.x += wp.x; + wb.min.y += wp.y; + wb.max.y += wp.y; + bounds.union(wb); + } + settings.bounds = bounds; + } + + // 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 (!isBelt && (process.outputBrimCount || process.outputRaft)) { + let brims = [], + offset = process.outputBrimOffset || (process.outputRaft ? 4 : 0); + + // compute first brim + widgets.filter(w => w.slices.length).forEach(function(widget) { + let tops = []; + let slices = []; + if (outputDraftShield) { + slices.appendAll(widget.slices); + } else { + slices.push(widget.slices[0]); } - settings.bounds = bounds; + for (let slice of slices) { + // collect top outer polygons + if (slice.tops) + slice.tops.forEach(function(top) { + tops.push(top.poly.clone()); + }); + // collect support polygons + if (slice.supports) + slice.supports.forEach(function(support) { + tops.push(support.clone()); + }); + } + if (outputDraftShield) { + tops = POLY.union(tops,1,true); + } + // nest and offset tops + POLY.nest(tops).forEach(function(poly) { + let off = poly.offset(-offset + nozzle / 2); + if (off) off.forEach(function(brim) { + brim.move(widget.track.pos); + brims.push(brim); + }); + }); + }); + + // merge brims + brims = POLY.union(brims, undefined, true); + + // if brim is offset, over-expand then shrink to induce brims to merge + if (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); } - // 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 (!isBelt && (process.outputBrimCount || process.outputRaft)) { - let brims = [], - offset = process.outputBrimOffset || (process.outputRaft ? 4 : 0); + // if raft is specified + if (process.outputRaft) { + let offset = newPoint(0,0,0), + height = nozzle, + rafts = process.outputBrimCount ? + POLY.expand(brims, nozzle * 4, 0, null, 1) : brims; - // compute first brim - widgets.filter(w => w.slices.length).forEach(function(widget) { - let tops = []; - let slices = []; - if (draftShield) { - slices.appendAll(widget.slices); - } else { - slices.push(widget.slices[0]); - } - for (let slice of slices) { - // collect top outer polygons - if (slice.tops) - slice.tops.forEach(function(top) { - tops.push(top.poly.clone()); - }); - // collect support polygons - if (slice.supports) - slice.supports.forEach(function(support) { - tops.push(support.clone()); - }); - } - if (draftShield) { - tops = POLY.union(tops,1,true); - } - // nest and offset tops - POLY.nest(tops).forEach(function(poly) { - let off = poly.offset(-offset + nozzle / 2); - if (off) off.forEach(function(brim) { - brim.move(widget.track.pos); - brims.push(brim); - }); + + // 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); + rafts.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 = slicePrintPath(print, slice, printPoint, offset, layerout, { + speed, + mult: extrude, + }); + layerout.z = zoff + height; + layerout.height = height; + output.append(layerout); + + layerout = []; + zoff += height; + }; + + raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 5.0, firstLayerRate / 3, 4); + raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 5.0, 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 + 90 , nozzle * 0.7, process.outputFeedrate, 0.75); + + // raise first layer off raft slightly to lessen adhesion + firstLayerHeight += process.outputRaftSpacing || 0; + zoff += process.outputRaftSpacing || 0; + + // retract after last raft layer + output.last().last().retract = true; + } + + // if using brim or skirt + if (process.outputBrimCount) { + let polys = [], + preout = []; + + // offset specified # of brims + POLY.offset(brims, nozzle, { + outs: polys, + flat: true, + count: process.outputBrimCount, + z: firstLayerHeight / 2 + }); + + print.setType('brim'); + shield = POLY.nest(polys.clone(), true).clone(); + + // output brim points + let brimStart = offset < nozzle * 2 ? newPoint(-bedWidth, -bedDepth, 0) : printPoint; + printPoint = poly2polyEmit(polys, brimStart, (poly, index, count, startPoint) => { + return print.polyPrintPath(poly, startPoint, preout, { + rate: firstLayerRate, + onfirst: function(point) { + if (preout.length && point.distTo2D(startPoint) > 2) { + // retract between brims + preout.last().retract = true; + } + } }); }); - // merge brims - brims = POLY.union(brims, undefined, true); + print.addPrintPoints(preout, layerout, null); - // if brim is offset, over-expand then shrink to induce brims to merge - if (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 (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 support widgets when needed + // so that they can use a separate extruder + // compute zmin for belt purge towers + for (let widget of widgets.slice()) { + let sslices = []; + if (!widget.slices) { + console.log('invalid widget', widget); + continue; + } + for (let slice of widget.slices) { + zmin = Math.min(zmin, slice.z); + if (!slice.supports) { + continue; + } + let sslice = kiri.newSlice(slice.z); + sslice.extruder = process.sliceSupportNozzle; + sslice.supports = slice.supports.slice(); + sslice.height = slice.height; + sslices.push(sslice); + } + if (sslices.length) { + let swidget = kiri.newWidget(null,widget.group); + swidget.slices = sslices; + swidget.support = true; + swidget.rotinfo = widget.rotinfo; + swidget.belt = widget.belt; + swidget.track = Object.clone(widget.track); + swidget.mesh = { widget: swidget, position: swidget.track.pos }; + widget.anno.extruder = process.sliceSupportNozzle; + widgets.push(swidget); + } + } + + let lastPoly; + let lastLayer; + let lastOffset; + let extruders = print.extruders = []; + let extcount = 0; + + // find max layers (for updates) + // generate list of used extruders for purge blocks + for (let widget of widgets) { + let extruder = widget.anno.extruder || 0; + if (!extruders[extruder]) { + extruders[extruder] = {}; + extcount++; + } + } + + // determine size/location of purge blocks + let blokw = Math.sqrt(outputPurgeTower || 0); + let blokh = blokw; + let blokpos, walkpos, blok; + + function mkblok(w,h) { + let count = extcount - 1; + let gap = nozzle; + if (isBelt) { + let step = w + gap; + let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint + let sp = (bounds.max.y - bounds.min.y); // part y span + let ts = (w * count) + (gap * (count - 1)); // tower y span + let ty = -ts + w/2 - gap/2; // tower start y + blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos + walkpos = { y:step, x:0 }; + blok = { x:h, y:w }; + } else if (bounds.min.x < bounds.min.y) { + let step = h + gap; + let mp = (bounds.max.x + bounds.min.x) / 2; // part y midpoint + let sp = (bounds.max.x - bounds.min.x); // part y span + let ts = (h * count) + (gap * (count - 1)); // tower y span + let tx = mp - ts / 2 + step / 2; // tower start y + blokpos = { x:tx, y: bounds.max.y + 2 + w / 2}; // first block pos + walkpos = { x:step, y:0 }; + blok = { x:w, y:h }; + } else { + let step = w + gap; + let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint + let sp = (bounds.max.y - bounds.min.y); // part y span + let ts = (w * count) + (gap * (count - 1)); // tower y span + let ty = mp - ts / 2 + step / 2; // tower start y + blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos + walkpos = { y:step, x:0 }; + blok = { x:w, y:h }; + } + } + + function mkrec(i, angle = 45, thin = 6) { + let exi = device.extruders[i], + noz = exi.extNozzle, + pos = {x:blokpos.x + walkpos.x * i, y:blokpos.y + walkpos.y * i, z:0}, + rect = newPolygon().centerRectangle(pos, blok.x, blok.y), + full = linesToPoly(POLY.fillArea([ + newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) + ], angle, noz)), + sparse = rect.area() > 10 ? linesToPoly(POLY.fillArea([ + newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) + ], angle + 90, noz * thin)) : newPolygon(), + rec = { + extruder: i, + diameter: exi.extNozzle, + rect, + full, + sparse, + pause: newPoint(pos.x + walkpos.y, pos.y + walkpos.x, pos.z) + }; + return rec; + } + + mkblok(blokw, blokh); + + // allocate tower space for extruders-1 locations + let towers = extruders.slice(1).map((ext,i) => { + return ext ? mkrec(i) : ext; + }); + + function linesToPoly(points) { + let poly = newPolygon().setOpen(); + let ping = 0; + for (let i=0; i= 0 || lastPurgeTool === nozzle; + let tool = using >= 0 ? using : nozzle; + let first = isBelt ? !purgedFirst && layer.slice.index >= 0 : layer.slice.index === 0; + let rate = first ? process.firstLayerRate : process.outputFeedrate; + let wipe = true; + lastPurgeTool = tool; + if (rec) { + print.setType('purge tower'); + if (layer.last()) { + layer.last().retract = true; + } + purgedFirst = purgedFirst || first; + let purgeOn = first || !thin; + if (isBelt && z < 0) { + let scale = 1 - ((z / zmin)); + mkblok(blokw * scale, blokh); + rec = mkrec(rec.extruder, 0, 10); + rate = (process.outputFeedrate - process.firstLayerRate) * scale + process.firstLayerRate; + wipe = false; + } + let box = rec.rect.clone().setZ(z); + let pause = rec.pause.clone().setZ(z); + let fill = (z >= 0 && purgeOn ? rec.full : rec.sparse).clone().setZ(z); + if (isBelt) { + let bmove = {x:0, y:z, z:0}; + box.move(bmove); + pause.move(bmove); + if (fill) { + fill.move(bmove); + } + if (offset) { + let bo = { x:0, y:offset.y, z:offset.z }; + box.move(bo); + pause.move(bo); + if (fill) fill.move(bo); + } + } + start = print.polyPrintPath(box, start, layer, { + tool, + rate, + simple: true, + open: false, + onfirstout: (out => out.overate = (isBelt ? rate : 0)) + }); + if (fill && fill.length) { + // for pings, split path at 20mm + start = print.polyPrintPath(fill, start, layer, { + tool, + rate, + simple: true, + open: true, + onfirst: (point) => { point.purgeOn = purgeOn ? pause : undefined } + }); + } + layer.last().retract = true; + // experimental post-retract wipe + if (wipe) start = print.polyPrintPath(box, start, layer, { + tool, + rate, + simple: true, + open: false, + extrude: 0 + }); + layer.last().overate = 0; + return start; + } else { + console.log({no_purge_tower_for: nozzle, using, track, layer}); + return start; + } + } + + // establish offsets + for (let widget of widgets) { + let { rotinfo, belt } = widget; + let offset = Object.clone(widget.track.pos); + if (isBelt) { + let o = rotinfo.ypos * beltfact; + offset = { + x: rotinfo.xpos, + y: o, + z: o + }; + } else { + // when rafts used this is non-zero + offset.z = zoff; + } + widget.offset = offset; + } + + // create shuffled slice cake by z offset (slice.z + offset.z) + let cake = []; + let zrec = {}; + for (let widget of widgets) { + // skip synthesized support widget(s) + if (!widget.mesh) { + continue; + } + for (let slice of widget.slices) { + slice.widget = widget; + let z = (slice.z + widget.offset.z).round(2); + let rec = zrec[z] = zrec[z] || {z, slices:[]}; + if (rec.slices.length === 0) { + cake.push(rec); + } + rec.slices.push(slice); + } + } + cake.sort((a, b) => { + return a.z - b.z; + }); + + let firstExt; + let lastWidget; + let lastExt; + let lastOut; + + // walk cake layers bottom up + for (let layer of cake) { + // track purge blocks generated for each layer + let track = towers.slice(); + + // iterate over layer slices, find closest widget, print, eliminate + for (;;) { + let order = []; + // select slices of the same extruder type first then distance + for (let slice of layer.slices) { + if (slice.prep) { + continue; + } + let offset = lastOffset = slice.widget.offset; + let find = slice.findClosestPointTo(printPoint.sub(offset)); + if (find) { + let ext = slice.extruder; + let lex = lastExt; + let dst = Math.abs(find.distance); + // penalize extruder swaps + if (ext !== lex) { + dst *= 10000; + } + // for first object, penalize extruders other than first + if (lastExt === undefined && ext > 0) { + dst *= 10000; + } + order.push({dst, slice, offset, z: layer.z}); + } + } + if (order.length === 0) { + break; + } + order.sort((a,b) => { + return a.dst - b.dst; + }); + let { z, slice, offset } = order[0]; + if (firstExt === undefined) { + firstExt = slice.extruder; } - // if raft is specified - if (process.outputRaft) { - let offset = newPoint(0,0,0), - height = nozzle, - rafts = process.outputBrimCount ? - POLY.expand(brims, nozzle * 4, 0, null, 1) : brims; - - - // 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); - rafts.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.z = zoff + height; - layerout.height = height; - output.append(layerout); - - layerout = []; - zoff += height; - }; - - raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 5.0, firstLayerRate / 3, 4); - raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 5.0, 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 + 90 , nozzle * 0.7, process.outputFeedrate, 0.75); - - // raise first layer off raft slightly to lessen adhesion - firstLayerHeight += process.outputRaftSpacing || 0; - zoff += process.outputRaftSpacing || 0; - - // retract after last raft layer + // when layers switch between widgets, force retraction + let forceRetract = lastOut && lastOut.widget !== slice.widget; + if (forceRetract && output.length) { + // selecet last output of last layer output.last().last().retract = true; } - // if using brim or skirt - if (process.outputBrimCount) { - let polys = [], - preout = []; - - // offset specified # of brims - POLY.offset(brims, nozzle, { - outs: polys, - flat: true, - count: process.outputBrimCount, - z: firstLayerHeight / 2 - }); - - print.setType('brim'); - shield = POLY.nest(polys.clone(), true).clone(); - - // output brim points - let brimStart = offset < nozzle * 2 ? newPoint(-bedWidth, -bedDepth, 0) : printPoint; - printPoint = poly2polyEmit(polys, brimStart, (poly, index, count, startPoint) => { - return print.polyPrintPath(poly, startPoint, preout, { - rate: firstLayerRate, - onfirst: function(point) { - if (preout.length && point.distTo2D(startPoint) > 2) { - // retract between brims - preout.last().retract = true; + let params = getRangeParameters(process, slice.index); + slice.prep = true; + // retract between widgets or layers (when set) + if (layerout.length && slice.widget !== lastWidget) { + layerout.last().retract = true; + } + lastWidget = slice.widget; + layerout.z = z + slice.height / 2; + layerout.height = layerout.height || slice.height; + layerout.slice = slice; + // mark layer as anchor if slice is belt and flag set + layerout.anchor = slice.belt && slice.belt.anchor; + // detect extruder change and print purge block + if (!lastOut || lastOut.extruder !== slice.extruder) { + if (slice.extruder >= 0) + printPoint = purge(slice.extruder, track, layerout, printPoint, slice.z, undefined, offset); + } + let wtb = slice.widget.track.box; + let beltStart = slice.belt && slice.belt.touch;// && (widgets.length === 1); + // output seek to start point between mesh slices if previous data + print.setType('layer'); + printPoint = slicePrintPath( + print, + slice, + beltStart ? newPoint(-5000, 5000, 0) : printPoint.sub(offset), + offset, + layerout, + { + routeAround: process.outputAvoidGaps, + seedPoint: printPoint.sub(offset), + danger: isDanger, + params, // range parameters + first: slice.index === 0, + support: slice.widget.support, + onBelt: beltStart, + pretract: (wipeDist) => { + if (lastLayer && lastLayer.length) { + let lastOut = lastLayer.last(); + lastOut.retract = true; + if (wipeDist && lastPoly && lastOut.point) { + let endpoint = lastOut.point.followTo(lastPoly.center(true).add(offset), wipeDist); + if (endpoint.inPolygon(lastPoly)) { + print.addOutput(lastLayer, endpoint); + } } } - }); - }); - - 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 support widgets when needed - // so that they can use a separate extruder - // compute zmin for belt purge towers - for (let widget of widgets.slice()) { - let sslices = []; - if (!widget.slices) { - console.log('invalid widget', widget); - continue; - } - for (let slice of widget.slices) { - zmin = Math.min(zmin, slice.z); - if (!slice.supports) { - continue; - } - let sslice = KIRI.newSlice(slice.z); - sslice.extruder = process.sliceSupportNozzle; - sslice.supports = slice.supports.slice(); - sslice.height = slice.height; - sslices.push(sslice); - } - if (sslices.length) { - let swidget = KIRI.newWidget(null,widget.group); - swidget.slices = sslices; - swidget.support = true; - swidget.rotinfo = widget.rotinfo; - swidget.belt = widget.belt; - swidget.track = Object.clone(widget.track); - swidget.mesh = { widget: swidget, position: swidget.track.pos }; - widget.anno.extruder = process.sliceSupportNozzle; - widgets.push(swidget); - } - } - - let lastPoly; - let lastLayer; - let lastOffset; - let extruders = print.extruders = []; - let extcount = 0; - - // find max layers (for updates) - // generate list of used extruders for purge blocks - for (let widget of widgets) { - let extruder = widget.anno.extruder || 0; - if (!extruders[extruder]) { - extruders[extruder] = {}; - extcount++; - } - } - - // determine size/location of purge blocks - let blokw = Math.sqrt(purgeTower); - let blokh = blokw; - let blokpos, walkpos, blok; - - function mkblok(w,h) { - let count = extcount - 1; - let gap = nozzle; - if (isBelt) { - let step = w + gap; - let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint - let sp = (bounds.max.y - bounds.min.y); // part y span - let ts = (w * count) + (gap * (count - 1)); // tower y span - let ty = -ts + w/2 - gap/2; // tower start y - blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos - walkpos = { y:step, x:0 }; - blok = { x:h, y:w }; - } else if (bounds.min.x < bounds.min.y) { - let step = h + gap; - let mp = (bounds.max.x + bounds.min.x) / 2; // part y midpoint - let sp = (bounds.max.x - bounds.min.x); // part y span - let ts = (h * count) + (gap * (count - 1)); // tower y span - let tx = mp - ts / 2 + step / 2; // tower start y - blokpos = { x:tx, y: bounds.max.y + 2 + w / 2}; // first block pos - walkpos = { x:step, y:0 }; - blok = { x:w, y:h }; - } else { - let step = w + gap; - let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint - let sp = (bounds.max.y - bounds.min.y); // part y span - let ts = (w * count) + (gap * (count - 1)); // tower y span - let ty = mp - ts / 2 + step / 2; // tower start y - blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos - walkpos = { y:step, x:0 }; - blok = { x:w, y:h }; - } - } - - function mkrec(i, angle = 45, thin = 6) { - let exi = device.extruders[i], - noz = exi.extNozzle, - pos = {x:blokpos.x + walkpos.x * i, y:blokpos.y + walkpos.y * i, z:0}, - rect = newPolygon().centerRectangle(pos, blok.x, blok.y), - full = linesToPoly(POLY.fillArea([ - newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) - ], angle, noz)), - sparse = rect.area() > 10 ? linesToPoly(POLY.fillArea([ - newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) - ], angle + 90, noz * thin)) : newPolygon(), - rec = { - extruder: i, - diameter: exi.extNozzle, - rect, - full, - sparse, - pause: newPoint(pos.x + walkpos.y, pos.y + walkpos.x, pos.z) - }; - return rec; - } - - mkblok(blokw, blokh); - - // allocate tower space for extruders-1 locations - let towers = extruders.slice(1).map((ext,i) => { - return ext ? mkrec(i) : ext; - }); - - function linesToPoly(points) { - let poly = newPolygon().setOpen(); - let ping = 0; - for (let i=0; i= 0 || lastPurgeTool === nozzle; - let tool = using >= 0 ? using : nozzle; - let first = isBelt ? !purgedFirst && layer.slice.index >= 0 : layer.slice.index === 0; - let rate = first ? process.firstLayerRate : process.outputFeedrate; - let wipe = true; - lastPurgeTool = tool; - if (rec) { - print.setType('purge tower'); - if (layer.last()) { - layer.last().retract = true; - } - purgedFirst = purgedFirst || first; - let purgeOn = first || !thin; - if (isBelt && z < 0) { - let scale = 1 - ((z / zmin)); - mkblok(blokw * scale, blokh); - rec = mkrec(rec.extruder, 0, 10); - rate = (process.outputFeedrate - process.firstLayerRate) * scale + process.firstLayerRate; - wipe = false; - } - let box = rec.rect.clone().setZ(z); - let pause = rec.pause.clone().setZ(z); - let fill = (z >= 0 && purgeOn ? rec.full : rec.sparse).clone().setZ(z); - if (isBelt) { - let bmove = {x:0, y:z, z:0}; - box.move(bmove); - pause.move(bmove); - if (fill) { - fill.move(bmove); - } - if (offset) { - let bo = { x:0, y:offset.y, z:offset.z }; - box.move(bo); - pause.move(bo); - if (fill) fill.move(bo); } } - start = print.polyPrintPath(box, start, layer, { - tool, - rate, - simple: true, - open: false, - onfirstout: (out => out.overate = (isBelt ? rate : 0)) - }); - if (fill && fill.length) { - // for pings, split path at 20mm - start = print.polyPrintPath(fill, start, layer, { - tool, - rate, - simple: true, - open: true, - onfirst: (point) => { point.purgeOn = purgeOn ? pause : undefined } - }); - } - layer.last().retract = true; - // experimental post-retract wipe - if (wipe) start = print.polyPrintPath(box, start, layer, { - tool, - rate, - simple: true, - open: false, - extrude: 0 - }); - layer.last().overate = 0; - return start; - } else { - console.log({no_purge_tower_for: nozzle, using, track, layer}); - return start; + ); + + lastOut = slice; + lastExt = lastOut.extruder; + lastPoly = slice.lastPoly; + lastLayer = layerout; + + if (outputLayerRetract && layerout.length) { + layerout.last().retract = true; } } - // establish offsets - for (let widget of widgets) { - let { rotinfo, belt } = widget; - let offset = Object.clone(widget.track.pos); - if (isBelt) { - let o = rotinfo.ypos * beltfact; - offset = { - x: rotinfo.xpos, - y: o, - z: o - }; - } else { - // when rafts used this is non-zero - offset.z = zoff; - } - widget.offset = offset; - } - - // create shuffled slice cake by z offset (slice.z + offset.z) - let cake = []; - let zrec = {}; + // clear slice.prep so it can be re-previewed in a different mode for (let widget of widgets) { // skip synthesized support widget(s) if (!widget.mesh) { continue; } for (let slice of widget.slices) { - slice.widget = widget; - let z = (slice.z + widget.offset.z).round(2); - let rec = zrec[z] = zrec[z] || {z, slices:[]}; - if (rec.slices.length === 0) { - cake.push(rec); - } - rec.slices.push(slice); + slice.prep = false; } } - cake.sort((a, b) => { - return a.z - b.z; + + // draft shield + if (layerno > 0 && shield && outputDraftShield) { + print.setType('shield'); + shield = POLY.setZ(shield.clone(), printPoint.z); + let preout = []; + printPoint = poly2polyEmit(shield, printPoint, (poly, index, count, startPoint) => { + return print.polyPrintPath(poly, startPoint, preout, { + onfirst: function(point) { + if (preout.length && point.distTo2D(startPoint) > 2) { + // retract between part and shield + preout.last().retract = true; + } + } + }); + }); + preout.last().retract = true; + layerout.appendAll(preout); + } + + // if a declared extruder isn't used in a layer, use selected + // extruder to fill the relevant purge blocks for later support + track.slice().forEach(ext => { + printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt, lastOffset); }); - let firstExt; - let lastWidget; - let lastExt; - let lastOut; + // if layer produced output, append to output array + if (layerout.length) { + output.append(layerout); + } - // walk cake layers bottom up - for (let layer of cake) { - // track purge blocks generated for each layer - let track = towers.slice(); + // retract after last layer + if (layerno === cake.length - 1 && layerout.length) { + layerout.last().retract = true; + } - // iterate over layer slices, find closest widget, print, eliminate - for (;;) { - let order = []; - // select slices of the same extruder type first then distance - for (let slice of layer.slices) { - if (slice.prep) { - continue; - } - let offset = lastOffset = slice.widget.offset; - let find = slice.findClosestPointTo(printPoint.sub(offset)); - if (find) { - let ext = slice.extruder; - let lex = lastExt; - let dst = Math.abs(find.distance); - // penalize extruder swaps - if (ext !== lex) { - dst *= 10000; - } - // for first object, penalize extruders other than first - if (lastExt === undefined && ext > 0) { - dst *= 10000; - } - order.push({dst, slice, offset, z: layer.z}); + // notify progress + layerout.layer = layerno++; + update((layerno / cake.length) * 0.5, "prepare"); + + layerout = []; + } + + print.output = output; + + // post-process for base extrusions (touching the bed) + if (isBelt) { + // tune base threshold + let thresh = Infinity; + for (let layer of output) { + for (let rec of layer) { + let point = rec.point; + thresh = Math.min(thresh, point.z - point.y); + } + } + // store this offset to be removed from Y values in export + print.belty = thresh; + thresh = thresh + firstLayerHeight * 0.25; + // iterate over layers, find extrusion on belt and + // apply corrections and add brim when specified + for (let layer of output) { + let params = getRangeParameters(process, layer.layer || 0); + let brimHalf = params.firstLayerBrim < 0; + let firstLayerBrim = Math.abs(params.firstLayerBrim); + let firstLayerBrimIn = params.firstLayerBrimIn || 0; + let firstLayerBrimTrig = params.firstLayerBrimTrig || 0; + let firstLayerBrimGap = params.firstLayerBrimGap || 0; + let lastout, first = false; + let minz = Infinity, maxy = -Infinity, minx = Infinity, maxx = -Infinity; + let mins = Infinity; + let miny = Infinity; + let pads = []; + let overate = 0; + + for (let rec of layer) { + overate = rec.overate >= 0 ? rec.overate : overate; + let brate = params.firstLayerRate || firstLayerRate; + let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult; + let point = rec.point; + let belty = rec.belty = -point.y + point.z; + let lowrate = belty <= thresh ? brate : overate || brate; + miny = Math.min(miny, belty); + if (layer.anchor) { + // apply base rate to entire anchor (including bump) + rec.speed = Math.min(rec.speed, lowrate); + } + if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.005) { + // apply base speed to segments touching belt + rec.speed = Math.min(rec.speed, lowrate); + rec.emit *= bmult; + minx = Math.min(minx, point.x, lastout.point.x); + maxx = Math.max(maxx, point.x, lastout.point.x); + maxy = Math.max(maxy, point.y); + minz = Math.min(minz, point.z); + first = rec; + // find length of shortest bed-facing segment + mins = Math.min(mins, lastout.point.distTo2D(rec.point)); + // add to pads list if > 1mm long + if (point.x - lastout.point.x > 1) { + pads.push([lastout.point.x, point.x]); } } - if (order.length === 0) { + lastout = rec; + } + // do not add brims to anchor layers + if (!first || layer.anchor) { + continue; + } + let tmpout = []; + let trigmet = firstLayerBrimTrig === 0 || (firstLayerBrimTrig && mins <= firstLayerBrimTrig); + let brimax = Math.max(firstLayerBrim, firstLayerBrimIn); + // add brim when all conditions met + if (brimax && trigmet) { + let { emit, tool } = first; + let y = maxy; + let z = minz; + let g = firstLayerBrimGap || 0; + let b = Math.max(firstLayerBrim, 1) + g; + let bi = Math.max(firstLayerBrimIn, 1) + g; + layer.last().retract = true; + // outside brim + if (firstLayerBrim && !brimHalf) { + print.addOutput(tmpout, newPoint(maxx + b, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(maxx + g, y, z), emit, firstLayerRate, tool).retract = true; + } + // inside brim + if (firstLayerBrimIn && pads.length > 1) { + let gaps = []; + let lpad = pads[0]; + for (let pad of pads.slice(1)) { + let x0 = lpad[1]; + let x1 = pad[0]; + lpad = pad; + if (x1 - x0 > bi * 2) { + // over 2x brim so emit two segments + print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x1 - bi, y, z), emit, firstLayerRate, tool).retract = true; + print.addOutput(tmpout, newPoint(x0 + bi, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool).retract = true; + } else if (x1 - x0 > bi / 3) { + // over 1/3rd brim length emit single segment + print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool).retract = true; + } + } + } + // outside brim + if (firstLayerBrim) { + print.addOutput(tmpout, newPoint(minx - b, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(minx - g, y, z), emit, firstLayerRate, tool).retract = false; + } + } else { + // for any layer touching belt, ensure start point is nearest origin + // print.addOutput(tmpout, newPoint(minx, maxy, minz), 0, outputSeekrate, first.tool); + // print.lastPoint = newPoint(minx, maxy, minz); + } + layer.splice(0,0,...tmpout); + } + } + + // render if not explicitly disabled + if (settings.render !== false) { + print.render = FDM.prepareRender(output, (progress, layer) => { + update(0.5 + progress * 0.5, "render", layer); + }, { + toolMode: settings.pmode === 2, + tools: device.extruders, + thin: isThin, + flat: isFlat, + fdm: true + }); + } + + return print.render; +}; + +// fdm only +function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { + const { settings } = print; + const { device } = settings; + + let preout = [], + process = opt.params || settings.process, + extruder = slice.extruder || 0, + nozzleSize = device.extruders[extruder].extNozzle, + firstLayer = opt.first || false, + thinWall = nozzleSize * (opt.thinWall || 1.75), + retractDist = opt.retractOver || 2, + solidWidth = process.sliceFillWidth || 1, + fillMult = opt.mult || process.outputFillMult, + shellMult = opt.mult || process.outputShellMult || (process.laserSliceHeight >= 0 ? 1 : 0), + shellOrder = {"out-in":-1,"in-out":1}[process.sliceShellOrder] || -1, + sparseMult = process.outputSparseMult, + coastDist = process.outputCoastDist || 0, + finishSpeed = opt.speed || process.outputFinishrate, + firstShellSpeed = process.firstLayerRate, + firstFillSpeed = process.firstLayerFillRate, + firstPrintMult = process.firstLayerPrintMult, + printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), + fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), + infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, + moveSpeed = process.outputSeekrate, + origin = startPoint.add(offset), + zhop = process.zHopDistance || 0, + antiBacklash = process.antiBacklash, + wipeDist = process.outputRetractWipe || 0, + isBelt = device.bedBelt, + beltFirst = process.outputBeltFirst || false, + startClone = startPoint.clone(), + seedPoint = opt.seedPoint || startPoint, + z = slice.z, + lastPoly; + + // apply first layer extrusion multipliers + if (firstLayer) { + fillMult *= firstPrintMult; + shellMult *= firstPrintMult; + sparseMult *= firstPrintMult; + } + + function retract() { + let array = preout.length ? preout : output; + if (array.length) { + let last = array.last(); + last.retract = true; + if (wipeDist && lastPoly && last.point) { + let endpoint = last.point.followTo(lastPoly.center(true), wipeDist); + if (endpoint.inPolygon(lastPoly)) { + print.addOutput(array, endpoint); + } + } + } else if (opt.pretract) { + opt.pretract(wipeDist); + } else { + console.log('unable to retract. no preout or output'); + } + } + + function intersectsTop(p1, p2) { + if (slice.index < 0) { + return false; + } + let int = false; + slice.topPolysFlat().forEach((poly) => { + if (!int) poly.forEachSegment((s1, s2) => { + if (util.intersect(p1,p2,s1,s2,base.key.SEGINT)) { + return int = true; + } + }); + }); + // if intersecting, look for a route around + if (int && opt.routeAround) { + return !routeAround(p1, p2); + } + return int; + } + + // returns true if routed around or no retract requried + function routeAround(p1, p2) { + const dbug = false; + if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)}); + + let ints = []; + let tops = slice.topRouteFlat(); + for (let poly of tops) { + poly.forEachSegment((s1, s2) => { + let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT); + if (ip) { + ints.push({ip, poly}); + } + }); + } + + // no intersections + if (ints.length === 0) { + if (dbug === slice.index) console.log(slice.index, 'no ints'); + return false; + } + + // odd # of intersections ?!? do retraction + if (ints.length && ints.length % 2 !== 0) { + if (dbug === slice.index) console.log(slice.index, {odd_intersects: ints}); + return false; + } + + // sort by distance + ints.sort((a, b) => { + return a.ip.dist - b.ip.dist; + }); + + if (dbug === slice.index) console.log(slice.index, {ints}); + + // check pairs. eliminate too close points. + // pairs must intersect same poly or retract. + for (let i=0; i i); + + if (ints.length > 2) { + if (dbug === slice.index) console.log(slice.index, {complex_route: ints.length}); + return false; + } + + if (ints.length === 2) { + // can route around intersected top polys + for (let i=0; ii.ip.dist), + i1, i2, same: i1.poly === i2.poly, + route, + p1, p2, dist: p1.distTo2D(p2), + r1, r1d, r1s, r1e, + r2, r2d, r2s, r2e, + isCW}); + + for (let p of route) { + print.addOutput(preout, p, 0, moveSpeed, extruder); + } + + // output last point + print.addOutput(preout, i2.ip, 0, moveSpeed, extruder); + } + return true; + } + + return false; + } + + function outputTraces(poly, opt = {}) { + if (!poly) return; + if (Array.isArray(poly)) { + if (opt.sort) { + let polys = poly.slice().sort((a,b) => { + return (a.perimeter() - b.perimeter()) * opt.sort; + }); + let debug = polys.length > 3; + let last; + while (polys.length) { + let next; + for (let p of polys) { + if (!last) { + next = p; + break; + } + if (opt.sort > 0) { + // in-out + if (last.isInside(p)) { + next = p; + break; + } + } else { + // out-in + if (p.isInside(last)) { + next = p; + break; + } + } + } + if (next) { + last = next; + polys.remove(next); + outputTraces(next, opt); + } else { + last = null; + } + } + } else { + outputOrderClosest(poly, function(next) { + outputTraces(next, opt); + }, null); + } + } else { + let finishShell = poly.depth === 0 && !firstLayer; + startPoint = print.polyPrintPath(poly, startPoint, preout, { + tool: extruder, + rate: finishShell ? finishSpeed : printSpeed, + accel: finishShell, + wipe: process.outputWipeDistance || 0, + coast: firstLayer ? 0 : coastDist, + extrude: numOrDefault(opt.extrude, shellMult), + onfirst: function(firstPoint) { + let from = seedPoint || startPoint; + if (from.distTo2D(firstPoint) > retractDist) { + if (intersectsTop(from, firstPoint)) { + retract(); + } + } + seedPoint = null; + } + }); + lastPoly = slice.lastPoly = poly; + } + } + + /** + * @param {Polygon[]} polys + */ + function outputSparse(polys, extrude, speed) { + if (!polys) return; + let proxy = polys.map((poly) => { + return {poly: poly, first: poly.first(), last: poly.last()}; + }); + let lp = startPoint; + startPoint = tip2tipEmit(proxy, startPoint, (el, point, count) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint((p, i) => { + let dist = lp.distTo2D(p); + let rdst = dist > retractDist; + let itop = rdst && intersectsTop(lp,p); + let emit = extrude; + // retract if dist trigger and crosses a slice top polygon + if (i === 0) { + if (itop) { + retract(); + emit = 0; + } else if (dist > nozzleSize) { + emit = 0; + } + } + // let emit = i === 0 ? 0 : extrude; + // handle shallow cloned infill + if (poly.z !== undefined) { + p = p.clone().setZ(poly.z); + } + print.addOutput(preout, p, emit, speed || printSpeed, extruder); + lp = p; + }, !poly.open); + return lp; + }); + } + + function outputThin(lines) { + if (!lines) { + return; + } + let points = lines.group(2).map(grp => { + let [ p1, p2 ] = grp; + return newPoint( + (p1.x + p2.x) / 2, + (p1.y + p2.y) / 2, + (p1.z + p2.z) / 2 + ) + }); + let order = util.orderClosest(points, (p1, p2) => p1.distTo2D(p2)); + if (order.length === 0) { + return; + } + let first = points[0]; + let last = first; + print.addOutput(preout, first, 0, moveSpeed, extruder); + for (let p of order) { + let dist = last ? last.distTo2D(p) : 0; + if (dist > thinWall) { + retract(); + print.addOutput(preout, p, 0, moveSpeed, extruder); + } + print.addOutput(preout, p, 1, fillSpeed, extruder); + last = p; + } + // close a circle + if (last && last.distTo2D(first) <= thinWall) { + print.addOutput(preout, first, 1, fillSpeed, extruder); + } + } + + function outputFills(lines, opt = {}) { + if (!lines || lines.length === 0) { + return; + } + let p, p1, p2, dist, len, found, group, mindist, t1, t2, + marked = 0, + start = 0, + skip = false, + lastIndex = -1, + flow = opt.flow || 1, + near = opt.near || false, + fast = opt.fast || false, + fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow, + thinDist = near ? thinWall : thinWall; + + while (lines && marked < lines.length) { + group = null; + found = false; + mindist = Infinity; + + // use next nearest line strategy + if (near) + for (i=0; i lastIndex) { + group = p.index; + } + if (group !== null) { + if (p.index !== group) { + break; + } + if (p.index % 2 === 0) { + t1 = lines[i]; + t2 = lines[i+1]; + } else { + t2 = lines[i]; + t1 = lines[i+1]; + } + dist = Math.min(t1.distTo2D(startPoint), t2.distTo2D(startPoint)); + if (dist < mindist) { + p1 = t1; + p2 = t2; + mindist = dist; + } + start = i; + found = true; + } + } + + // go back to start and try again + if (!near && !found) { + if (start === 0 && lastIndex === -1) { + console.log('infinite loop', lines, { + marked, i, group, start, lastIndex, + points: lines.map(p => p.index).join(', ') + }); break; } - order.sort((a,b) => { - return a.dst - b.dst; - }); - let { z, slice, offset } = order[0]; - if (firstExt === undefined) { - firstExt = slice.extruder; - } - - // when layers switch between widgets, force retraction - let forceRetract = lastOut && lastOut.widget !== slice.widget; - if (forceRetract && output.length) { - // selecet last output of last layer - output.last().last().retract = true; - } - - let params = getRangeParameters(process, slice.index); - slice.prep = true; - // retract between widgets or layers (when set) - if (layerout.length && slice.widget !== lastWidget) { - layerout.last().retract = true; - } - lastWidget = slice.widget; - layerout.z = z + slice.height / 2; - layerout.height = layerout.height || slice.height; - layerout.slice = slice; - // mark layer as anchor if slice is belt and flag set - layerout.anchor = slice.belt && slice.belt.anchor; - // detect extruder change and print purge block - if (!lastOut || lastOut.extruder !== slice.extruder) { - if (slice.extruder >= 0) - printPoint = purge(slice.extruder, track, layerout, printPoint, slice.z, undefined, offset); - } - let wtb = slice.widget.track.box; - let beltStart = slice.belt && slice.belt.touch;// && (widgets.length === 1); - // output seek to start point between mesh slices if previous data - print.setType('layer'); - printPoint = print.slicePrintPath( - slice, - beltStart ? newPoint(-5000, 5000, 0) : printPoint.sub(offset), - offset, - layerout, - { - routeAround: process.outputAvoidGaps, - seedPoint: printPoint.sub(offset), - danger: isDanger, - params, // range parameters - first: slice.index === 0, - support: slice.widget.support, - onBelt: beltStart, - pretract: (wipeDist) => { - if (lastLayer && lastLayer.length) { - let lastOut = lastLayer.last(); - lastOut.retract = true; - if (wipeDist && lastPoly && lastOut.point) { - let endpoint = lastOut.point.followTo(lastPoly.center(true).add(offset), wipeDist); - if (endpoint.inPolygon(lastPoly)) { - print.addOutput(lastLayer, endpoint); - } - } - } - } - } - ); - - lastOut = slice; - lastExt = lastOut.extruder; - lastPoly = slice.lastPoly; - lastLayer = layerout; - - if (layerRetract && layerout.length) { - layerout.last().retract = true; - } + start = 0; + lastIndex = -1; + continue; } - // clear slice.prep so it can be re-previewed in a different mode - for (let widget of widgets) { - // skip synthesized support widget(s) - if (!widget.mesh) { - continue; - } - for (let slice of widget.slices) { - slice.prep = false; - } + dist = startPoint.distToSq2D(p1); + len = p1.distToSq2D(p2); + + // go back to start when dist > retractDist + if (!near && !fast && !skip && dist > retractDist) { + skip = true; + start = 0; + lastIndex = -1; + continue; } + skip = false; - // draft shield - if (layerno > 0 && shield && draftShield) { - print.setType('shield'); - shield = POLY.setZ(shield.clone(), printPoint.z); - let preout = []; - printPoint = poly2polyEmit(shield, printPoint, (poly, index, count, startPoint) => { - return print.polyPrintPath(poly, startPoint, preout, { - onfirst: function(point) { - if (preout.length && point.distTo2D(startPoint) > 2) { - // retract between part and shield - preout.last().retract = true; - } - } - }); - }); - preout.last().retract = true; - layerout.appendAll(preout); - } + // mark as used (temporarily) + p1.del = true; + p2.del = true; + marked += 2; + lastIndex = p1.index; - // if a declared extruder isn't used in a layer, use selected - // extruder to fill the relevant purge blocks for later support - track.slice().forEach(ext => { - printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt, lastOffset); - }); - - // if layer produced output, append to output array - if (layerout.length) { - output.append(layerout); - } - - // retract after last layer - if (layerno === cake.length - 1 && layerout.length) { - layerout.last().retract = true; - } - - // notify progress - layerout.layer = layerno++; - update((layerno / cake.length) * 0.5, "prepare"); - - layerout = []; - } - - print.output = output; - - // post-process for base extrusions (touching the bed) - if (isBelt) { - // tune base threshold - let thresh = Infinity; - for (let layer of output) { - for (let rec of layer) { - let point = rec.point; - thresh = Math.min(thresh, point.z - point.y); + // if dist to new segment is less than thinWall + // and segment length is less than thinWall then + // just extrude to midpoint of next segment. this is + // to avoid shaking the printer to death. + if (dist <= thinDist && len <= thinDist) { + p2 = p1.midPointTo(p2); + // this.addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder); + print.addOutput(preout, p2, fill, fillSpeed, extruder); + } else { + // retract if dist trigger or crosses a slice top polygon + if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) { + retract(); } - } - // store this offset to be removed from Y values in export - print.belty = thresh; - thresh = thresh + firstLayerHeight * 0.25; - // iterate over layers, find extrusion on belt and - // apply corrections and add brim when specified - for (let layer of output) { - let params = getRangeParameters(process, layer.layer || 0); - let brimHalf = params.firstLayerBrim < 0; - let firstLayerBrim = Math.abs(params.firstLayerBrim); - let firstLayerBrimIn = params.firstLayerBrimIn || 0; - let firstLayerBrimTrig = params.firstLayerBrimTrig || 0; - let firstLayerBrimGap = params.firstLayerBrimGap || 0; - let lastout, first = false; - let minz = Infinity, maxy = -Infinity, minx = Infinity, maxx = -Infinity; - let mins = Infinity; - let miny = Infinity; - let pads = []; - let overate = 0; - for (let rec of layer) { - overate = rec.overate >= 0 ? rec.overate : overate; - let brate = params.firstLayerRate || firstLayerRate; - let bmult = params.firstLayerPrintMult || firstLayerMult; - let point = rec.point; - let belty = rec.belty = -point.y + point.z; - let lowrate = belty <= thresh ? brate : overate || brate; - miny = Math.min(miny, belty); - if (layer.anchor) { - // apply base rate to entire anchor (including bump) - rec.speed = Math.min(rec.speed, lowrate); - } - if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.005) { - // apply base speed to segments touching belt - rec.speed = Math.min(rec.speed, lowrate); - rec.emit *= bmult; - minx = Math.min(minx, point.x, lastout.point.x); - maxx = Math.max(maxx, point.x, lastout.point.x); - maxy = Math.max(maxy, point.y); - minz = Math.min(minz, point.z); - first = rec; - // find length of shortest bed-facing segment - mins = Math.min(mins, lastout.point.distTo2D(rec.point)); - // add to pads list if > 1mm long - if (point.x - lastout.point.x > 1) { - pads.push([lastout.point.x, point.x]); - } - } - lastout = rec; + // anti-backlash on longer move + if (!fast && antiBacklash && dist > retractDist) { + print.addOutput(preout, p1.add({x:antiBacklash,y:-antiBacklash,z:0}), 0, moveSpeed, extruder); } - // do not add brims to anchor layers - if (!first || layer.anchor) { - continue; - } - let tmpout = []; - let trigmet = firstLayerBrimTrig === 0 || (firstLayerBrimTrig && mins <= firstLayerBrimTrig); - let brimax = Math.max(firstLayerBrim, firstLayerBrimIn); - // add brim when all conditions met - if (brimax && trigmet) { - let { emit, tool } = first; - let y = maxy; - let z = minz; - let g = firstLayerBrimGap || 0; - let b = Math.max(firstLayerBrim, 1) + g; - let bi = Math.max(firstLayerBrimIn, 1) + g; - layer.last().retract = true; - // outside brim - if (firstLayerBrim && !brimHalf) { - print.addOutput(tmpout, newPoint(maxx + b, y, z), 0, firstLayerSeek, tool); - print.addOutput(tmpout, newPoint(maxx + g, y, z), emit, firstLayerRate, tool).retract = true; - } - // inside brim - if (firstLayerBrimIn && pads.length > 1) { - let gaps = []; - let lpad = pads[0]; - for (let pad of pads.slice(1)) { - let x0 = lpad[1]; - let x1 = pad[0]; - lpad = pad; - if (x1 - x0 > bi * 2) { - // over 2x brim so emit two segments - print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, firstLayerSeek, tool); - print.addOutput(tmpout, newPoint(x1 - bi, y, z), emit, firstLayerRate, tool).retract = true; - print.addOutput(tmpout, newPoint(x0 + bi, y, z), 0, firstLayerSeek, tool); - print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool).retract = true; - } else if (x1 - x0 > bi / 3) { - // over 1/3rd brim length emit single segment - print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, firstLayerSeek, tool); - print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool).retract = true; - } - } - } - // outside brim - if (firstLayerBrim) { - print.addOutput(tmpout, newPoint(minx - b, y, z), 0, firstLayerSeek, tool); - print.addOutput(tmpout, newPoint(minx - g, y, z), emit, firstLayerRate, tool).retract = false; - } + + // bridge ends of fill when they're close together + if (dist < thinDist) { + print.addOutput(preout, p1, fill, fillSpeed, extruder); } else { - // for any layer touching belt, ensure start point is nearest origin - // print.addOutput(tmpout, newPoint(minx, maxy, minz), 0, firstLayerSeek, first.tool); - // print.lastPoint = newPoint(minx, maxy, minz); + print.addOutput(preout, p1, 0, moveSpeed, extruder); } - layer.splice(0,0,...tmpout); + + print.addOutput(preout, p2, fill, fillSpeed, extruder); } + + startPoint = p2; } - // render if not explicitly disabled - if (render) { - print.render = FDM.prepareRender(output, (progress, layer) => { - update(0.5 + progress * 0.5, "render", layer); - }, { - toolMode: settings.pmode === 2, - tools: device.extruders, - thin: isThin, - flat: isFlat, - fdm: true + // clear delete marks so we can re-print later + if (lines) lines.forEach(p => { p.del = false }); + } + + /** + * given array of polygons, emit them in next closest order with + * the special exception that depth is considered into distance + * so that inner polygons are emitted first. + * + * @param {Array} array of Polygons or Polygon wrappers (tops) + * @param {Function} fn call to emit next candidate + * @param {Function} fnp convert 'next' object into a Polygon for closeness + */ + function outputOrderClosest(array, fn, fnp) { + if (array.length === 1) { + return fn(array[0]); + } + array = array.slice(); + let closest, find, next, order, poly, lastDepth = 0; + for (;;) { + order = []; + closest = null; + for (let i=0; i { + return a.d - b.d; }); - } - - return print.render; - }; - - class Counter { - constructor() { - this.map = {}; - this.total = 0; - } - put(key) { - const map = this.map; - const kp = key || 'bad'; - map[kp] = (map[kp] || 0) + 1; - this.total++; - } - get() { - return { map: this.map, total: this.total }; + array[order[0].i] = null; + fn(order[0].n); } } - FDM.isDark = function() { - return current.print.settings.controller.dark ? true : false; + let out = []; + if (slice.tops) { + out.appendAll(slice.tops); }; + if (opt.support && slice.supports) { + out.appendAll(slice.supports); + } - FDM.rateToColor = function(rate, max) { - return FDM.isDark() ? - darkColorFunction(rate/max, 1, 0.85) : - currentColorFunction(rate/max, 1, 0.85); - }; + let lastTop = null; + outputOrderClosest(out, function(next) { + if (next instanceof Polygon) { + print.setType('support'); + // support polygon + next.setZ(z); + outputTraces([next].appendAll(next.inner || [])); + if (next.fill) { + next.fill.forEach(p => { p.z = z }); + outputFills(next.fill, {fast: true}); + } + } else { + print.setType('shells'); + if (lastTop && lastTop !== next) { + retract(); + } - FDM.prepareRender = function(levels, update, opts = {}) { - levels = levels.filter(level => level.length); - if (levels.length === 0) { - self.worker.print.maxSpeed = 0; - return []; - } + // control of layer start point + switch (process.sliceLayerStart) { + case "center": + startPoint = newPoint(0,0,startPoint.z); + break; + case "origin": + startPoint = origin.clone(); + break; + } - const dark = FDM.isDark(); - const tools = opts.tools || {}; - const flat = opts.flat; - const thin = opts.thin && !flat; - const ckspeed = opts.speed !== false; - const headColor = 0x888888; - const moveColor = opts.move >= 0 ? opts.move : 0xaaaaaa; - const printColor = opts.print >= 0 ? opts.print : 0x777700; - const arrowAll = true; - const arrowSize = arrowAll ? 0.2 : 0.4; - const layers = []; - const toolMode = opts.toolMode; - - const moveOpt = { - face: moveColor, - line: flat ? 1 : moveColor, - opacity: flat ? 0.5 : 1 - }; - const printOpt = { - face: printColor, - line: flat ? 1 : printColor, - opacity: flat ? 0.5 : 1 - }; - - let minspd = Infinity; - let maxspd = opts.maxspeed || 0; - let maxtool = []; - - for (let level of levels) { - for (let o of level) { - if (o.speed) { - minspd = Math.min(minspd, o.speed); - maxspd = Math.max(maxspd, o.speed); + // optimize start point on belt for tops touching belt + // and enforce optimal shell order (outer first) + if (isBelt && opt.onBelt) { + startPoint = startClone; + if (beltFirst) { + shellOrder = -1; } - if (toolMode && o.tool !== undefined) { - if (maxtool.indexOf(o.tool) < 0) { - maxtool.push(o.tool); - } + } + + // innermost shells + let inner = next.innerShells() || []; + + // output inner polygons + if (shellOrder === 1) outputTraces(inner, { sort: shellOrder }); + + outputTraces(next.shells, { sort: shellOrder }); + + // output outer polygons + if (shellOrder === -1) outputTraces(inner, { sort: shellOrder }); + + // output thin fill + print.setType('thin fill'); + outputThin(next.thin_fill); + + // then output solid and sparse fill + print.setType('solid fill'); + outputFills(next.fill_lines, {flow: solidWidth}); + + print.setType('sparse infill'); + outputSparse(next.fill_sparse, sparseMult, infillSpeed); + + lastTop = next; + } + }, function(obj) { + // for tops + return obj instanceof Polygon ? obj : obj.poly; + }); + + // produce polishing paths when present + if (slice.tops.length && slice.tops[0].polish) { + let {x,y} = slice.tops[0].polish; + if (x) { + outputSparse(x, 0, process.polishSpeed); + } + if (y) { + outputSparse(y, 0, process.polishSpeed); + } + } + + // offset print points + for (let i=0; i level.length); + if (levels.length === 0) { + self.worker.print.maxSpeed = 0; + return []; + } + + const dark = FDM.isDark(); + const tools = opts.tools || {}; + const flat = opts.flat; + const thin = opts.thin && !flat; + const ckspeed = opts.speed !== false; + const headColor = 0x888888; + const moveColor = opts.move >= 0 ? opts.move : 0xaaaaaa; + const printColor = opts.print >= 0 ? opts.print : 0x777700; + const arrowAll = true; + const arrowSize = arrowAll ? 0.2 : 0.4; + const layers = []; + const toolMode = opts.toolMode; + + const moveOpt = { + face: moveColor, + line: flat ? 1 : moveColor, + opacity: flat ? 0.5 : 1 + }; + const printOpt = { + face: printColor, + line: flat ? 1 : printColor, + opacity: flat ? 0.5 : 1 + }; + + let minspd = Infinity; + let maxspd = opts.maxspeed || 0; + let maxtool = []; + + for (let level of levels) { + for (let o of level) { + if (o.speed) { + minspd = Math.min(minspd, o.speed); + maxspd = Math.max(maxspd, o.speed); + } + if (toolMode && o.tool !== undefined) { + if (maxtool.indexOf(o.tool) < 0) { + maxtool.push(o.tool); } } } + } - // const maxspd = levels.map(level => { - // return level.map(o => o.speed || 0).reduce((a, v) => Math.max(a,v)); - // }).reduce((a, v) => Math.max(a, v)) + 1; + // const maxspd = levels.map(level => { + // return level.map(o => o.speed || 0).reduce((a, v) => Math.max(a,v)); + // }).reduce((a, v) => Math.max(a, v)) + 1; - // for reporting - self.worker.print.minSpeed = minspd; - self.worker.print.maxSpeed = maxspd; - self.worker.print.thinColor = thin; - self.worker.print.flatColor = flat; + // for reporting + self.worker.print.minSpeed = minspd; + self.worker.print.maxSpeed = maxspd; + self.worker.print.thinColor = thin; + self.worker.print.flatColor = flat; - let lastTool = null; - let lastEnd = null; - let lastOut = null; - let current = null; - let retracted = false; - let retractz = 0; + let lastTool = null; + let lastEnd = null; + let lastOut = null; + let current = null; + let retracted = false; + let retractz = 0; - function color(point) { - if (toolMode) { - return FDM.rateToColor(maxtool.indexOf(point.tool), maxtool.length); - } else { - return FDM.rateToColor(point.speed, maxspd); - } + function color(point) { + if (toolMode) { + return FDM.rateToColor(maxtool.indexOf(point.tool), maxtool.length); + } else { + return FDM.rateToColor(point.speed, maxspd); } + } - levels.forEach((level, index) => { - const prints = {}; - const moves = []; - const heads = []; - const changes = []; - const retracts = []; - const engages = []; - const output = new KIRI.Layers(); - layers.push(output); + levels.forEach((level, index) => { + const prints = {}; + const moves = []; + const heads = []; + const changes = []; + const retracts = []; + const engages = []; + const output = new kiri.Layers(); + layers.push(output); - const pushPrint = (toolid, poly) => { - toolid = toolid || 0; - const array = prints[toolid] = prints[toolid] || []; - const tool = tools[toolid] || {}; - array.width = (tool.extNozzle || 1) / 2; - array.push(poly); - emits++; - }; + const pushPrint = (toolid, poly) => { + toolid = toolid || 0; + const array = prints[toolid] = prints[toolid] || []; + const tool = tools[toolid] || {}; + array.width = (tool.extNozzle || 1) / 2; + array.push(poly); + emits++; + }; - let height = level.height / 2; - let width = 1; - let emits = 0; + let height = level.height / 2; + let width = 1; + let emits = 0; - level.forEach((out,oi) => { - if (retracted && out.emit) { - retracted = false; - engages.push(lastOut.point); + level.forEach((out,oi) => { + if (retracted && out.emit) { + retracted = false; + engages.push(lastOut.point); + } + if (out.tool !== lastTool) { + lastTool = out.tool; + changes.push(out.point); + } + if (out.retract) { + retracts.push(out.point); + retracted = true; + retractz++; + } + if (!out.point) { + // in cam mode, these are drilling or dwell ops + return; + } + + if (lastOut) { + if (arrowAll || lastOut.emit !== out.emit) { + heads.push({p1: lastOut.point, p2: out.point}); } - if (out.tool !== lastTool) { - lastTool = out.tool; - changes.push(out.point); - } - if (out.retract) { - retracts.push(out.point); - retracted = true; - retractz++; - } - if (!out.point) { - // in cam mode, these are drilling or dwell ops - return; - } - - if (lastOut) { - if (arrowAll || lastOut.emit !== out.emit) { - heads.push({p1: lastOut.point, p2: out.point}); - } - const op = out.point, lp = lastOut.point; - // const moved = Math.max( - // Math.abs(op.x - lp.x), - // Math.abs(op.y - lp.y), - // Math.abs(op.z - lp.z)); - // if (moved < 0.0001) return; - if (out.emit) { - if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { - current = newPolygon().setOpen(); - current.push(lastOut.point); - current.color = color(out); - pushPrint(out.tool, current); - } - current.push(out.point); - } else { - if (lastOut.emit || lastEnd) { - current = newPolygon().setOpen(); - current.push(lastOut.point); - moves.push(current); - } - current.push(out.point); - } - lastEnd = null; - } else { - current = newPolygon().setOpen(); - current.push(out.point); - if (out.emit) { + const op = out.point, lp = lastOut.point; + // const moved = Math.max( + // Math.abs(op.x - lp.x), + // Math.abs(op.y - lp.y), + // Math.abs(op.z - lp.z)); + // if (moved < 0.0001) return; + if (out.emit) { + if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { + current = newPolygon().setOpen(); + current.push(lastOut.point); current.color = color(out); pushPrint(out.tool, current); - } else { + } + current.push(out.point); + } else { + if (lastOut.emit || lastEnd) { + current = newPolygon().setOpen(); + current.push(lastOut.point); moves.push(current); } + current.push(out.point); + } + lastEnd = null; + } else { + current = newPolygon().setOpen(); + current.push(out.point); + if (out.emit) { + current.color = color(out); + pushPrint(out.tool, current); + } else { + moves.push(current); } - lastOut = out; - }); - // all moves with an emit at the very end (common in contouring) - if (lastOut.emit && !emits) { - pushPrint(lastOut.tool, current) } - lastEnd = lastOut; - if (changes.length) { - output - .setLayer('tool', { line: 0x000055, face: 0x0000ff, opacity: 0.5 }, true) - .addAreas(changes.map(point => { - return newPolygon().centerCircle(point, 0.2, 4).setZ(point.z + 0.03); - }), { outline: true }); - } - if (retracts.length) { - output - .setLayer('retract', { line: 0x550000, face: 0xff0000, opacity: 0.5 }, true) - .addAreas(retracts.map(point => { - return newPolygon().centerCircle(point, 0.2, 5).setZ(point.z + 0.01); - }), { outline: true }); - } - if (engages.length) { - output - .setLayer('engage', { line: 0x005500, face: 0x00ff00, opacity: 0.5 }, true) - .addAreas(engages.map(point => { - return newPolygon().centerCircle(point, 0.2, 7).setZ(point.z + 0.02); - }), { outline: true }); - } - if (heads.length) { - let line = dark ? 0xffffff : 0x112233; - output - .setLayer('arrows', { face: headColor, line, opacity: 0.75 }, true) - .addAreas(heads.map(points => { - const {p1, p2} = points; - const slope = p2.slopeTo(p1); - const s1 = BASE.newSlopeFromAngle(slope.angle + 20); - const s2 = BASE.newSlopeFromAngle(slope.angle - 20); - const p3 = points.p2.projectOnSlope(s1, arrowSize); - const p4 = points.p2.projectOnSlope(s2, arrowSize); - return newPolygon().addPoints([p2,p3,p4]).setZ(p2.z + 0.01); - }), { thin: true, outline: true }); - } - output - .setLayer(opts.other || 'move', moveOpt, opts.moves !== true) - .addPolys(moves, { thin: true, z: opts.z }); - // force level when present - let pz = level.z ? level.z - height : opts.z; - Object.values(prints).forEach(array => { - array.forEach(poly => { - if (flat && poly.appearsClosed()) { - poly.setClosed(); - poly.points.pop(); - } - output - .setLayer(opts.action || 'print', printOpt) - .addPolys([ poly ], - thin ? { thin, z: opts.z, color: poly.color } : - flat ? { - flat, z: pz, color: poly.color, - outline: true, offset: array.width, open: poly.open } : - { - offset: array.width, height, z: pz, - color: { face: poly.color, line: poly.color } - }) - }); - }); - - update(index / levels.length, output); + lastOut = out; }); - // console.log({retractz}); - return layers; - } - - const colorFunctions = FDM.colorFunctions = { - default: hsv2rgb.bind({ seg: 4, fn: color4d }), - simple: hsv2rgb.bind({ seg: 3, fn: color4 }), - dark: hsv2rgb.bind({ seg: 4, fn: color4d }) - }; - - let currentColorFunction = colorFunctions.default; - let darkColorFunction = colorFunctions.dark; - - // hsv values all = 0 to 1 - function hsv2rgb(h, s, v) { - const div = this.seg; - const ss = 1 / div; - const seg = Math.floor(h / ss); - const rem = h - (seg * ss); - const inc = (rem / ss); - const dec = (1 - inc); - const rgb = {r: 0, g: 0, b: 0}; - this.fn(rgb, inc, seg); - rgb.r = ((rgb.r * 255 * v) & 0xff) << 16; - rgb.g = ((rgb.g * 255 * v) & 0xff) << 8; - rgb.b = ((rgb.b * 255 * v) & 0xff); - return rgb.r | rgb.g | rgb.b; - } - - function color5(rgb, inc, seg) { - const dec = 1 - inc; - switch (seg) { - case 0: - rgb.r = 1; - rgb.g = inc; - rgb.b = 0; - break; - case 1: - rgb.r = dec; - rgb.g = 1; - rgb.b = 0; - break; - case 2: - rgb.r = 0; - rgb.g = dec; - rgb.b = inc; - break; - case 3: - rgb.r = inc; - rgb.g = 0; - rgb.b = 1; - break; - case 4: - rgb.r = dec; - rgb.g = 0; - rgb.b = dec; - break; + // all moves with an emit at the very end (common in contouring) + if (lastOut.emit && !emits) { + pushPrint(lastOut.tool, current) } - } - - function color4d(rgb, inc, seg) { - const dec = 1 - inc; - switch (seg) { - case 0: - rgb.r = 1; - rgb.g = inc; - rgb.b = 0; - break; - case 1: - rgb.r = dec; - rgb.g = 1; - rgb.b = 0; - break; - case 2: - rgb.r = 0; - rgb.g = dec; - rgb.b = inc; - break; - case 3: - rgb.r = inc * 0.85; - rgb.g = 0; - rgb.b = 1; - break; - case 4: - rgb.r = 0.85; - rgb.g = 0; - rgb.b = 1; - break; + lastEnd = lastOut; + if (changes.length) { + output + .setLayer('tool', { line: 0x000055, face: 0x0000ff, opacity: 0.5 }, true) + .addAreas(changes.map(point => { + return newPolygon().centerCircle(point, 0.2, 4).setZ(point.z + 0.03); + }), { outline: true }); } - } - - - function color4(rgb, inc, seg) { - const dec = 1 - inc; - switch (seg) { - case 0: - rgb.r = 0; - rgb.g = inc; - rgb.b = 1; - break; - case 1: - rgb.r = inc; - rgb.g = 1; - rgb.b = 0; - break; - case 2: - rgb.r = 1; - rgb.g = dec; - rgb.b = 0; - break; - case 3: - rgb.r = dec; - rgb.g = 0; - rgb.b = 0; - break; + if (retracts.length) { + output + .setLayer('retract', { line: 0x550000, face: 0xff0000, opacity: 0.5 }, true) + .addAreas(retracts.map(point => { + return newPolygon().centerCircle(point, 0.2, 5).setZ(point.z + 0.01); + }), { outline: true }); } + if (engages.length) { + output + .setLayer('engage', { line: 0x005500, face: 0x00ff00, opacity: 0.5 }, true) + .addAreas(engages.map(point => { + return newPolygon().centerCircle(point, 0.2, 7).setZ(point.z + 0.02); + }), { outline: true }); + } + if (heads.length) { + let line = dark ? 0xffffff : 0x112233; + output + .setLayer('arrows', { face: headColor, line, opacity: 0.75 }, true) + .addAreas(heads.map(points => { + const {p1, p2} = points; + const slope = p2.slopeTo(p1); + const s1 = base.newSlopeFromAngle(slope.angle + 20); + const s2 = base.newSlopeFromAngle(slope.angle - 20); + const p3 = points.p2.projectOnSlope(s1, arrowSize); + const p4 = points.p2.projectOnSlope(s2, arrowSize); + return newPolygon().addPoints([p2,p3,p4]).setZ(p2.z + 0.01); + }), { thin: true, outline: true }); + } + output + .setLayer(opts.other || 'move', moveOpt, opts.moves !== true) + .addPolys(moves, { thin: true, z: opts.z }); + // force level when present + let pz = level.z ? level.z - height : opts.z; + Object.values(prints).forEach(array => { + array.forEach(poly => { + if (flat && poly.appearsClosed()) { + poly.setClosed(); + poly.points.pop(); + } + output + .setLayer(opts.action || 'print', printOpt) + .addPolys([ poly ], + thin ? { thin, z: opts.z, color: poly.color } : + flat ? { + flat, z: pz, color: poly.color, + outline: true, offset: array.width, open: poly.open } : + { + offset: array.width, height, z: pz, + color: { face: poly.color, line: poly.color } + }) + }); + }); + + update(index / levels.length, output); + }); + // console.log({retractz}); + return layers; +} + +const colorFunctions = FDM.colorFunctions = { + default: hsv2rgb.bind({ seg: 4, fn: color4d }), + simple: hsv2rgb.bind({ seg: 3, fn: color4 }), + dark: hsv2rgb.bind({ seg: 4, fn: color4d }) +}; + +let currentColorFunction = colorFunctions.default; +let darkColorFunction = colorFunctions.dark; + +// hsv values all = 0 to 1 +function hsv2rgb(h, s, v) { + const div = this.seg; + const ss = 1 / div; + const seg = Math.floor(h / ss); + const rem = h - (seg * ss); + const inc = (rem / ss); + const dec = (1 - inc); + const rgb = {r: 0, g: 0, b: 0}; + this.fn(rgb, inc, seg); + rgb.r = ((rgb.r * 255 * v) & 0xff) << 16; + rgb.g = ((rgb.g * 255 * v) & 0xff) << 8; + rgb.b = ((rgb.b * 255 * v) & 0xff); + return rgb.r | rgb.g | rgb.b; +} + +function color5(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 1; + rgb.g = inc; + rgb.b = 0; + break; + case 1: + rgb.r = dec; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec; + rgb.b = inc; + break; + case 3: + rgb.r = inc; + rgb.g = 0; + rgb.b = 1; + break; + case 4: + rgb.r = dec; + rgb.g = 0; + rgb.b = dec; + break; } +} + +function color4d(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 1; + rgb.g = inc; + rgb.b = 0; + break; + case 1: + rgb.r = dec; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec; + rgb.b = inc; + break; + case 3: + rgb.r = inc * 0.85; + rgb.g = 0; + rgb.b = 1; + break; + case 4: + rgb.r = 0.85; + rgb.g = 0; + rgb.b = 1; + break; + } +} + + +function color4(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 0; + rgb.g = inc; + rgb.b = 1; + break; + case 1: + rgb.r = inc; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 1; + rgb.g = dec; + rgb.b = 0; + break; + case 3: + rgb.r = dec; + rgb.g = 0; + rgb.b = 0; + break; + } +} })(); diff --git a/src/kiri-mode/fdm/slice.js b/src/kiri-mode/fdm/slice.js index 71687428..6a9ae320 100644 --- a/src/kiri-mode/fdm/slice.js +++ b/src/kiri-mode/fdm/slice.js @@ -4,1897 +4,1896 @@ (function() { - const { base, kiri, noop } = self; - const { driver, fill, fill_fixed, newSlice, utils } = kiri; - const { config, polygons, util, newPoint } = base; - const { fillArea } = polygons; - const { FDM } = driver; +const { base, kiri, noop } = self; +const { consts, driver, fill, fill_fixed, newSlice, utils } = kiri; +const { config, polygons, util, newPoint } = base; +const { fillArea } = polygons; +const { beltfact } = consts; +const { FDM } = driver; +const { getRangeParameters } = FDM; - const POLY = polygons, - tracker = util.pwait, - lopacity = 0.6, - opacity = 1, - COLOR = { - anchor: { check: 0x999933, face: 0x999933, line: 0x999933, opacity, lopacity }, - shell: { check: 0x0077bb, face: 0x0077bb, line: 0x0077bb, opacity, lopacity }, - fill: { check: 0x00bb77, face: 0x00bb77, line: 0x00bb77, opacity, lopacity }, - infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity }, - support: { check: 0xaa5533, face: 0xaa5533, line: 0xaa5533, opacity, lopacity }, - gaps: { check: 0xaa3366, face: 0xaa3366, line: 0xaa3366, opacity, lopacity } - }, - PROTO = Object.clone(COLOR), - getRangeParameters = FDM.getRangeParameters, - profile = false, - profileStart = profile ? console.profile : noop, - profileEnd = profile ? console.profileEnd : noop, - debug = false; +const POLY = polygons, + tracker = util.pwait, + lopacity = 0.6, + opacity = 1, + COLOR = { + anchor: { check: 0x999933, face: 0x999933, line: 0x999933, opacity, lopacity }, + shell: { check: 0x0077bb, face: 0x0077bb, line: 0x0077bb, opacity, lopacity }, + fill: { check: 0x00bb77, face: 0x00bb77, line: 0x00bb77, opacity, lopacity }, + infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity }, + support: { check: 0xaa5533, face: 0xaa5533, line: 0xaa5533, opacity, lopacity }, + gaps: { check: 0xaa3366, face: 0xaa3366, line: 0xaa3366, opacity, lopacity } + }, + PROTO = Object.clone(COLOR), + profile = false, + profileStart = profile ? console.profile : noop, + profileEnd = profile ? console.profileEnd : noop, + debug = false; - let isThin = false, // force line rendering - isFlat = false, // force flat rendering - offset = 0; // poly line generation offsets +let isThin = false, // force line rendering + isFlat = false, // force flat rendering + offset = 0; // poly line generation offsets - function vopt(opt) { - if (opt) { - if (isFlat) { - opt.flat = true; - opt.outline = true; - return opt; - } - if (isThin) return null; +function vopt(opt) { + if (opt) { + if (isFlat) { + opt.flat = true; + opt.outline = true; + return opt; } - return opt; + if (isThin) return null; + } + return opt; +} + +/** + * may run in minion or worker context. do not create objects + * that will not quickly encode in threaded mode. add to existing + * data object. return is ignored. + */ +base.slicePost.FDM = function(data, options) { + const { z, lines, groups } = data; + const { useAssembly, post_args, zIndexes } = options; + const { process, isSynth, isDanger, vaseMode } = post_args; + const { shellOffset, fillOffset, clipOffset } = post_args; + data.tops = POLY.nest(groups); + if (isSynth) { + // do not shell synth widgets because + // they will be clipped against peers later + // which requires shelling post-clip + // but we still need to generate tops + delete data.groups; + return; + } + const index = zIndexes.indexOf(z); + const range = getRangeParameters(process, index); + // calculate fractional shells + let shellFrac = (range.sliceShells - (range.sliceShells | 0)); + let sliceShells = range.sliceShells | 0; + if (shellFrac) { + let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; + let v2 = 1 - v1; + let parts = Math.round(v2/v1) + 1; + let rem = index % parts; + let trg = shellFrac > 0.5 ? 1 : parts - 1; + sliceShells += rem >= trg ? 1 : 0; + } + const isFirst = index === 0; + const height = process.sliceHeight; + const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1; + const count = isSynth ? 1 : sliceShells; + const offset = shellOffset * spaceMult; + const fillOff = fillOffset * spaceMult; + const nutops = []; + // co-locate shell processing with top generation in slicer + for (let top of data.tops) { + nutops.push(FDM.share.doTopShells(z, top, count, offset/2, offset, fillOff, { + vase: vaseMode, + thin: process.detectThinWalls && !isSynth, + wasm: useAssembly, + danger: isDanger + })); + } + data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined; + data.tops = nutops; + delete data.groups; +}; + +FDM.sliceAll = function(settings, onupdate) { + // future home of brim and anchor generation + let widgets = Object.values(kiri.worker.cache) + .filter(w => !w.meta.disabled) + .sort((a,b) => { + return a.slices[0].z - b.slices[0].z + }); + // ignore first widget + widgets.shift(); + // count extruders used + let ext = []; + for (let w of widgets) { + if (w.anno && w.anno.extruder >= 0) { + let e = w.anno.extruder; + if (ext.indexOf(e) < 0) { + ext.push(e); + } + } + } + // remove anchor slices from other widgets (only with multi-material) + if (ext.length > 1) + for (let w of widgets) { + w.slices = w.slices.filter(s => s.index >= 0); + } +}; + +/** + * 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) { + let render = settings.render !== false, + { process, device, controller } = settings, + isBelt = device.bedBelt, + isSynth = widget.track.synth, + isDanger = controller.danger, + useAssembly = controller.assembly, + isConcurrent = controller.threaded && kiri.minions.concurrent, + solidMinArea = process.sliceSolidMinArea, + solidLayers = process.sliceSolidLayers || 0, + vaseMode = process.sliceFillType === 'vase' && !isSynth, + metadata = widget.anno, + extruder = parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0), + sliceHeight = process.sliceHeight, + sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, + nozzleSize = device.extruders[extruder].extNozzle, + lineWidth = process.sliceLineWidth || nozzleSize, + fillOffsetMult = 1.0 - bound(process.sliceFillOverlap, 0, 0.8), + shellOffset = lineWidth, + fillSpacing = lineWidth, + fillOffset = lineWidth * fillOffsetMult, + clipOffset = process.sliceSupportOffset, + sliceFillAngle = process.sliceFillAngle, + supportDensity = process.sliceSupportDensity; + + // override globals used by vopt() + isFlat = controller.lineType === "flat"; + isThin = !isFlat && controller.lineType === "line"; + offset = lineWidth / 2; + + if (isFlat) { + Object.values(COLOR).forEach(color => { + color.flat = true; + color.line = 1 + color.opacity = 1; + }); + } else { + Object.keys(COLOR).forEach(key => { + const color = COLOR[key]; + const proto = PROTO[key] + color.flat = proto.flat; + color.line = proto.line; + color.opacity = proto.opacity; + }); } - /** - * may run in minion or worker context. do not create objects - * that will not quickly encode in threaded mode. add to existing - * data object. return is ignored. - */ - base.slicePost.FDM = function(data, options) { - const { z, lines, groups } = data; - const { useAssembly, post_args, zIndexes } = options; - const { process, isSynth, isDanger, vaseMode } = post_args; - const { shellOffset, fillOffset, clipOffset } = post_args; - const tops = POLY.nest(groups); - if (isSynth) { - // do not shell synth widgets because - // they will be clipped against peers later - // which requires shelling post-clip - // but we still need to generate tops - data.tops = tops; - delete data.groups; + if (!(sliceHeight > 0 && sliceHeight < 100)) { + return ondone("invalid slice height"); + } + if (!(nozzleSize >= 0.01 && nozzleSize < 100)) { + return ondone("invalid nozzle size"); + } + + const sliceMinHeight = process.sliceAdaptive && process.sliceMinHeight > 0 ? + Math.min(process.sliceMinHeight, sliceHeight) : 0; + + if (sliceHeightBase <= 0) { + console.log("invalid first layer height < slice height"); + console.log("reverting to min valid slice height"); + sliceHeightBase = sliceMinHeight || sliceHeight; + } + + let bounds = widget.getBoundingBox(); + let points = widget.getPoints(); + let indices = []; + let heights = []; + + // handle z cutting (floor method) and base flattening + let zPress = isBelt ? process.firstLayerFlatten || 0 : 0; + let zCut = widget.track.zcut || 0; + if (zCut || zPress) { + for (let p of points) { + if (!p._z) { + p._z = p.z; + if (zPress) { + if (isBelt) { + let zb = (p.z - p.y) * beltfact; + if (zb > 0 && zb <= zPress) { + p.y += zb * beltfact; + p.z -= zb * beltfact; + } + } else { + if (p.z <= zPress) p.z = 0; + } + } + if (zCut && !isBelt) { + p.z -= zCut; + } + } + } + } + + base.slice(points, { + debug: process.xray, + xray: process.xray, + zMin: bounds.min.z, + zMax: bounds.max.z - zCut, + // support/synth usually has overlapping boxes + union: controller.healMesh || isSynth, + indices: process.indices || process.xray, + useAssembly, + post: 'FDM', + post_args: { + shellOffset, + fillOffset, + clipOffset, + lineWidth, + vaseMode, + isSynth, + process, + isDanger, + }, + // z index generator + zGen(zopt) { + if (process.xray) { + return zopt.zIndexes; + } + let { zMin, zMax } = zopt; + let h1 = sliceHeight; + let h0 = sliceHeightBase || h1; + let hm = sliceMinHeight || 0; + let h = h0; + let z = h0; + let zi = indices; // indices + let zh = heights; // heights + if (hm) { + // adaptive increments based on z indices (var map to legacy code) + let zIncFirst = h0; + let zInc = h1; + let zIncMin = hm; + let zHeights = heights; + let zIndexes = indices; + let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); + // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); + let zPos = zIncFirst, + zOI = 0, + zDelta, + zDivMin, + zDivMax, + zStep, + nextZ, + lzp = zPos; + // adaptive slice height + // first slice/height is fixed from base + zHeights.push(zIncFirst); + zIndexes.push(zIncFirst); + // console.log({zIncFirst, zOrdered}) + while (zPos < zMax && zOI < zOrdered.length) { + nextZ = zOrdered[zOI++]; + if (zPos >= nextZ) { + // console.log('skip',{zPos},'>=',{nextZ}); + continue; + } + zDelta = nextZ - zPos; + if (zDelta < zIncMin) { + // console.log('skip',{zDelta},'<',{zIncMin}); + continue; + } + zDivMin = Math.floor(zDelta / zIncMin); + zDivMax = Math.floor(zDelta / zInc); + if (zDivMax && zDivMax <= zDivMin) { + if (zDelta % zInc > 0.01) zDivMax++; + zStep = zDelta / zDivMax; + // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) + } else { + zStep = zDelta; + } + // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) + while (zPos < nextZ) { + zHeights.push(zStep); + zIndexes.push(zPos + zStep); + zPos += zStep; + // console.log({D: zPos - lzp, zPos}) + // lzp = zPos; + } + } + // console.log({zIndexes, zHeights}); + } else { + // simple based + fixed increment + while (z <= zMax) { + zh.push(h); + zi.push(z); + h = h1; + z += h; + } + } + // reduce slice position by half height + for (let i=0; i { + // post process slices and re-incorporate missing meta-data + return output.slices.map(data => { + let { z, clip, lines, groups } = data; + if (!data.tops) return null; + let slice = newSlice(z).addTops(data.tops); + slice.index = indices.indexOf(z); + slice.height = heights[slice.index]; + slice.clips = clip; + if (process.xray) { + slice.lines = lines; + slice.groups = groups; + slice.xray = process.xray; + } + return slice; + }).filter(s => s); + }).then(slices => { + return onSliceDone(slices); + }).then(ondone); + + async function doShadow(slices) { + if (widget.shadow) { return; } - const index = zIndexes.indexOf(z); - const range = FDM.getRangeParameters(process, index); - // calculate fractional shells - let shellFrac = (range.sliceShells - (range.sliceShells | 0)); - let sliceShells = range.sliceShells | 0; - if (shellFrac) { - let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; - let v2 = 1 - v1; - let parts = Math.round(v2/v1) + 1; - let rem = index % parts; - let trg = shellFrac > 0.5 ? 1 : parts - 1; - sliceShells += rem >= trg ? 1 : 0; + let root = widget.group[0]; + if (root.shadow) { + widget.shadow = root.shadow; + return; } - const isFirst = index === 0; - const height = process.sliceHeight; - const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1; - const count = isSynth ? 1 : sliceShells; - const offset = shellOffset * spaceMult; - const fillOff = fillOffset * spaceMult; - const nutops = []; - // co-locate shell processing with top generation in slicer - for (let top of tops) { - nutops.push(FDM.share.doTopShells(z, top, count, offset/2, offset, fillOff, { - vase: vaseMode, - thin: process.detectThinWalls && !isSynth, - wasm: useAssembly, - danger: isDanger - })); + // create shadow for clipping supports + let alltops = widget.group + .filter(w => !w.track.synth) // no supports in shadow + .map(w => w.slices).flat() + .map(s => s.tops).flat().map(t => t.simple); + let shadow = isConcurrent ? + await kiri.minions.union(alltops, 0.1) : + POLY.union(alltops, 0.1, true); + // expand shadow when requested (support clipping) + if (process.sliceSupportExtra) { + shadow = POLY.offset(shadow, process.sliceSupportExtra); } - data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined; - data.tops = nutops; - delete data.groups; - }; - - FDM.sliceAll = function(settings, onupdate) { - // future home of brim and anchor generation - let widgets = Object.values(kiri.worker.cache) - .filter(w => !w.meta.disabled) - .sort((a,b) => { - return a.slices[0].z - b.slices[0].z - }); - // ignore first widget - widgets.shift(); - // count extruders used - let ext = []; - for (let w of widgets) { - if (w.anno && w.anno.extruder >= 0) { - let e = w.anno.extruder; - if (ext.indexOf(e) < 0) { - ext.push(e); - } - } - } - // remove anchor slices from other widgets (only with multi-material) - if (ext.length > 1) - for (let w of widgets) { - w.slices = w.slices.filter(s => s.index >= 0); - } - }; - - /** - * 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) { - let render = settings.render !== false, - { process, device, controller } = settings, - isBelt = device.bedBelt, - isSynth = widget.track.synth, - isDanger = controller.danger, - useAssembly = controller.assembly, - isConcurrent = controller.threaded && kiri.minions.concurrent, - solidMinArea = process.sliceSolidMinArea, - solidLayers = process.sliceSolidLayers || 0, - vaseMode = process.sliceFillType === 'vase' && !isSynth, - metadata = widget.anno, - extruder = parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0), - sliceHeight = process.sliceHeight, - sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, - nozzleSize = device.extruders[extruder].extNozzle, - lineWidth = process.sliceLineWidth || nozzleSize, - fillOffsetMult = 1.0 - bound(process.sliceFillOverlap, 0, 0.8), - shellOffset = lineWidth, - fillSpacing = lineWidth, - fillOffset = lineWidth * fillOffsetMult, - clipOffset = process.sliceSupportOffset, - sliceFillAngle = process.sliceFillAngle, - supportDensity = process.sliceSupportDensity, - beltfact = Math.cos(Math.PI/4); - - // override globals used by vopt() - isFlat = controller.lineType === "flat"; - isThin = !isFlat && controller.lineType === "line"; - offset = lineWidth / 2; - - if (isFlat) { - Object.values(COLOR).forEach(color => { - color.flat = true; - color.line = 1 - color.opacity = 1; - }); - } else { - Object.keys(COLOR).forEach(key => { - const color = COLOR[key]; - const proto = PROTO[key] - color.flat = proto.flat; - color.line = proto.line; - color.opacity = proto.opacity; - }); - } - - if (!(sliceHeight > 0 && sliceHeight < 100)) { - return ondone("invalid slice height"); - } - if (!(nozzleSize >= 0.01 && nozzleSize < 100)) { - return ondone("invalid nozzle size"); - } - - const sliceMinHeight = process.sliceAdaptive && process.sliceMinHeight > 0 ? - Math.min(process.sliceMinHeight, sliceHeight) : 0; - - if (sliceHeightBase <= 0) { - console.log("invalid first layer height < slice height"); - console.log("reverting to min valid slice height"); - sliceHeightBase = sliceMinHeight || sliceHeight; - } - - let bounds = widget.getBoundingBox(); - let points = widget.getPoints(); - let indices = []; - let heights = []; - - // handle z cutting (floor method) and base flattening - let zPress = isBelt ? process.firstLayerFlatten || 0 : 0; - let zCut = widget.track.zcut || 0; - if (zCut || zPress) { - for (let p of points) { - if (!p._z) { - p._z = p.z; - if (zPress) { - if (isBelt) { - let zb = (p.z - p.y) * beltfact; - if (zb > 0 && zb <= zPress) { - p.y += zb * beltfact; - p.z -= zb * beltfact; - } - } else { - if (p.z <= zPress) p.z = 0; - } - } - if (zCut && !isBelt) { - p.z -= zCut; - } - } - } - } - - base.slice(points, { - debug: process.xray, - xray: process.xray, - zMin: bounds.min.z, - zMax: bounds.max.z - zCut, - // support/synth usually has overlapping boxes - union: controller.healMesh || isSynth, - indices: process.indices || process.xray, - useAssembly, - post: 'FDM', - post_args: { - shellOffset, - fillOffset, - clipOffset, - lineWidth, - vaseMode, - isSynth, - process, - isDanger, - }, - // z index generator - zGen(zopt) { - if (process.xray) { - return zopt.zIndexes; - } - let { zMin, zMax } = zopt; - let h1 = sliceHeight; - let h0 = sliceHeightBase || h1; - let hm = sliceMinHeight || 0; - let h = h0; - let z = h0; - let zi = indices; // indices - let zh = heights; // heights - if (hm) { - // adaptive increments based on z indices (var map to legacy code) - let zIncFirst = h0; - let zInc = h1; - let zIncMin = hm; - let zHeights = heights; - let zIndexes = indices; - let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); - // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); - let zPos = zIncFirst, - zOI = 0, - zDelta, - zDivMin, - zDivMax, - zStep, - nextZ, - lzp = zPos; - // adaptive slice height - // first slice/height is fixed from base - zHeights.push(zIncFirst); - zIndexes.push(zIncFirst); - // console.log({zIncFirst, zOrdered}) - while (zPos < zMax && zOI < zOrdered.length) { - nextZ = zOrdered[zOI++]; - if (zPos >= nextZ) { - // console.log('skip',{zPos},'>=',{nextZ}); - continue; - } - zDelta = nextZ - zPos; - if (zDelta < zIncMin) { - // console.log('skip',{zDelta},'<',{zIncMin}); - continue; - } - zDivMin = Math.floor(zDelta / zIncMin); - zDivMax = Math.floor(zDelta / zInc); - if (zDivMax && zDivMax <= zDivMin) { - if (zDelta % zInc > 0.01) zDivMax++; - zStep = zDelta / zDivMax; - // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) - } else { - zStep = zDelta; - } - // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) - while (zPos < nextZ) { - zHeights.push(zStep); - zIndexes.push(zPos + zStep); - zPos += zStep; - // console.log({D: zPos - lzp, zPos}) - // lzp = zPos; - } - } - // console.log({zIndexes, zHeights}); - } else { - // simple based + fixed increment - while (z <= zMax) { - zh.push(h); - zi.push(z); - h = h1; - z += h; - } - } - // reduce slice position by half height - for (let i=0; i { - // post process slices and re-incorporate missing meta-data - return output.slices.map(data => { - let { z, clip, lines, groups } = data; - if (!data.tops) return null; - let slice = newSlice(z).addTops(data.tops); - slice.index = indices.indexOf(z); - slice.height = heights[slice.index]; - slice.clips = clip; - if (process.xray) { - slice.lines = lines; - slice.groups = groups; - slice.xray = process.xray; - } - return slice; - }).filter(s => s); - }).then(slices => { - return onSliceDone(slices); - }).then(ondone); - - async function doShadow(slices) { - if (widget.shadow) { - return; - } - let root = widget.group[0]; - if (root.shadow) { - widget.shadow = root.shadow; - return; - } - // create shadow for clipping supports - let alltops = widget.group - .filter(w => !w.track.synth) // no supports in shadow - .map(w => w.slices).flat() - .map(s => s.tops).flat().map(t => t.simple); - let shadow = isConcurrent ? - await kiri.minions.union(alltops, 0.1) : - POLY.union(alltops, 0.1, true); - // expand shadow when requested (support clipping) - if (process.sliceSupportExtra) { - shadow = POLY.offset(shadow, process.sliceSupportExtra); - } - widget.shadow = root.shadow = POLY.setZ(shadow, 0); - // slices[0].output() - // .setLayer('shadow', { line: 0xff0000, check: 0xff0000 }) - // .addPolys(shadow); - } - - async function onSliceDone(slices) { - // remove all empty slices above part but leave below - // for multi-part (multi-extruder) setups where the void is ok - // also reverse because slicing occurs bottom-up - let found = false; - slices = slices.reverse().filter(slice => { - if (slice.tops.length) { - return found = true; - } else { - return found; - } - }).reverse(); - - // connect slices into linked list for island/bridge projections - for (let i=1; i 0.1) { - continue; - } - let ntops = []; - POLY.subtract(tops, pslice.clips, ntops, null, slice.z, 0); - tops = ntops; - } - // trim to group's shadow if not in belt mode - if (!isBelt) { - tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z); - } - } - slice.tops = []; - for (let t of tops) { - slice.addTop(t); - } - doShells(slice, 1, shellOffset / 2); - } - } - - // calculate % complete and call onupdate() - function doupdate(index, from, to, msg) { - trackupdate(index / slices.length, from, to, msg); - } - - function trackupdate(pct, from, to, msg) { - onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); - } - - // for each slice, performe a function and call doupdate() - function forSlices(from, to, fn, msg) { - slices.forEach(slice => { - fn(slice); - doupdate(slice.index, from, to, msg) - }); - } - - // do not hint polygon fill longer than a max span length - config.hint_len_max = util.sqr(process.sliceBridgeMax); - - // reset for solids, support projections - // and other annotations - slices.forEach(slice => { - slice.widget = widget; - slice.extruder = extruder; - slice.solids = []; - }); - - // just the top/bottom special solid layers or range defined solid layers - forSlices(0.15, 0.2, slice => { - let range = slice.params; - let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; - let isBottom = slice.index < process.sliceBottomLayers; - let isTop = slice.index > slices.length - process.sliceTopLayers - 1; - let isDense = range.sliceFillSparse > 0.995; - let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; - let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; - if (solidWidth) { - let fillSpace = fillSpacing * spaceMult * solidWidth; - doSolidLayerFill(slice, fillSpace, sliceFillAngle); - } - sliceFillAngle += 90.0; - }, "solid layers"); - - // add lead in anchor when specified in belt mode (but not for synths) - if (isBelt && !isSynth) { - // find adjusted zero point from slices - let smin = Infinity; - for (let slice of slices) { - let miny = Infinity; - for (let poly of slice.topPolys()) { - let y = poly.bounds.maxy; - let z = slice.z; - let by = z - y; - if (by < miny) miny = by; - if (by < smin) smin = by; - } - slice.belt = { miny, touch: false }; - } - // mark slices with tops touching belt - // also find max width of first 5 layers - let start; - let minx = Infinity, maxx = -Infinity; - let peek = 0; - for (let slice of slices) { - if (slice.tops.length && peek++ < 5) { - for (let poly of slice.topPolys()) { - minx = Math.min(minx, poly.bounds.minx); - maxx = Math.max(maxx, poly.bounds.maxx); - } - } - // mark slice as touching belt if near miny - // if (Math.abs(slice.belt.miny - smin) < 0.01) { - if (Math.abs(slice.belt.miny) < 0.01) { - slice.belt.touch = true; - if (!start) start = slice; - } - } - // ensure we start against a layer with shells - while (start && start.up && start.topShells().length === 0) { - start = start.up; - } - // if a brim applies, add that width to anchor - let brim = getRangeParameters(process, 0).firstLayerBrim || 0; - if (brim) { - minx -= brim; - maxx += brim; - } - // array of added top.fill_sparse arrays - let adds = []; - let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * beltfact; - while (anchorlen && start && anchorlen >= sliceHeight) { - let addto = start.down; - if (!addto) { - addto = newSlice(start.z - sliceHeight); - addto.extruder = extruder; - addto.belt = { }; - addto.height = start.height; - addto.up = start; - start.down = addto; - slices.splice(0,0,addto); - } else if (!addto.belt) { - console.log({addto_missing_belt: addto}); - addto.belt = {}; - } - addto.index = -1; - addto.belt.anchor = true; - // this allows the anchor to print bi-directionally - // by removing the forced start-point in print.js - addto.belt.touch = false; - let z = addto.z; - let y = z - smin - (nozzleSize / 2); - let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); - let snew = addto.addTop(splat).fill_sparse = [ splat ]; - adds.push(snew); - start = addto; - anchorlen -= sliceHeight; - } - // add anchor bump - let bump = process.firstLayerBeltBump; - if (bump) { - adds = adds.reverse().slice(1, adds.length - 1); - let count = 1; - for (let add of adds) { - let poly = add[0]; - let y = count++ * -start.height * 2; - if (-y > bump) { - count--; - // break; - } - let first = poly.first(); - // add up/over/down to anchor line (close = down) - // which completes the bump perimeter - poly.push(poly.last().add({x:0, y, z:0})); - poly.push(poly.first().add({x:0, y, z:0})); - poly.setClosed(); - if (count > 2 && maxx - minx > 10) { - // add vertical hatch lines insibe bump shell - let mp = (maxx + minx) / 2; - let dx = (maxx - minx - 2); - dx = (Math.floor(dx / 3) * 3) / 2; - let fy = first.y; - let fz = first.z; - let n2 = nozzleSize / 2; - for (let x = mp - dx; x <= mp + dx ; x += 3) { - add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); - } - } - } - } - } - - // calculations only relevant when solid layers are used - if (solidLayers && !vaseMode && !isSynth) { - profileStart("delta"); - forSlices(0.2, 0.34, slice => { - if (slice.index > 0) doDiff(slice, solidMinArea); - }, "layer deltas"); - profileEnd(); - profileStart("delta-project"); - forSlices(0.34, 0.35, slice => { - projectFlats(slice, solidLayers); - projectBridges(slice, solidLayers); - }, "layer deltas"); - profileEnd(); - profileStart("solid-fill") - let promises = isConcurrent ? [] : undefined; - forSlices(0.35, promises ? 0.4 : 0.5, slice => { - let params = slice.params || process; - let first = slice.index === 0; - let solidWidth = params.sliceFillWidth || 1; - let spaceMult = first ? params.firstLayerLineMult || 1 : 1; - let fillSpace = fillSpacing * spaceMult * solidWidth; - doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); - sliceFillAngle += 90.0; - }, "fill solids"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.4, 0.5); - }); - } - profileEnd(); - } - - if (!isSynth && !vaseMode) { - // sparse layers only present when non-vase mose and sparse % > 0 - let lastType; - let promises = isConcurrent ? [] : undefined; - forSlices(0.5, promises ? 0.55 : 0.7, slice => { - let params = slice.params || process; - if (!params.sliceFillSparse) { - return; - } - let newType = params.sliceFillType; - doSparseLayerFill(slice, { - settings, - process, - device, - lineWidth, - spacing: fillOffset, - density: params.sliceFillSparse, - bounds: widget.getBoundingBox(), - height: sliceHeight, - type: newType, - cache: params._range !== true && lastType === newType, - promises - }); - lastType = newType; - }, "infill"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.55, 0.7); - }); - } - // back-fill slices marked for infill cloning - for (let slice of slices) { - if (slice._clone_sparse) { - let tops = slice.tops; - let down = slice.down.tops; - for (let i=0; i p.cloneZ(slice.z)); - } - } - } - } else if (isSynth) { - // fill manual supports differently - let outline = process.sliceSupportOutline || false; - let promises = isConcurrent ? [] : undefined; - let resolve = []; - forSlices(0.5, promises ? 0.6 : 0.7, slice => { - let params = slice.params || process; - let density = params.sliceSupportDensity; - if (density) - for (let top of slice.tops) { - if (!outline) { - let offset = top.shells; - fillSupportPolys(promises, offset, lineWidth, density, slice.z, isBelt); - resolve.push({top, offset}); - } else { - let offset = []; - POLY.expand(top.shells || [], -nozzleSize/4, slice.z, offset); - fillSupportPolys(promises, offset, lineWidth, density, slice.z, isBelt); - resolve.push({top, offset}); - } - } - }, "infill"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.6, 0.7); - }); - } - for (let rec of resolve) { - let lines = rec.top.fill_lines = rec.offset.map(o => o.fill).flat().filter(v => v); - // if copying simply's support type, eliminate shells - // and zig/zag lines connectd by shell segments - if (!outline) { - let newlines = []; - let op2; - let eo = 0; - let idx = 1; - for (let i=0; i { - promises.push(doSupport(slice, process, widget.shadow, { exp: isDanger })); - }, "support"); - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.75, 0.8); - }); - profileEnd(); - profileStart("support-fill"); - promises = false && isConcurrent ? [] : undefined; - forSlices(0.8, promises ? 0.88 : 0.9, slice => { - doSupportFill(promises, slice, lineWidth, supportDensity, process.sliceSupportArea, isBelt); - }, "support"); - if (promises) { - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.88, 0.9); - }); - } - profileEnd(); - } - - // render if not explicitly disabled - if (render) { - forSlices(0.9, 1.0, slice => { - let params = slice.params || process; - doRender(slice, isSynth, params, controller.devel); - }, "render"); - } - - if (isBelt) { - let bounds = base.newBounds(); - for (let top of slices[0].tops) { - bounds.merge(top.poly.bounds); - } - widget.belt.miny = -bounds.miny; - widget.belt.midy = (bounds.miny + bounds.maxy) / 2; - } - } - + widget.shadow = root.shadow = POLY.setZ(shadow, 0); + // slices[0].output() + // .setLayer('shadow', { line: 0xff0000, check: 0xff0000 }) + // .addPolys(shadow); } - function bound(v,min,max) { - return Math.max(min,Math.min(max,v)); - } - - function doRender(slice, isSynth, params, devel) { - const output = slice.output(); - const height = slice.height / 2; - const solidWidth = params.sliceFillWidth || 1; - - slice.tops.forEach(top => { - if (isThin) output - .setLayer('part', { line: 0x333333, check: 0x333333 }) - .addPolys(top.poly); - - output - .setLayer("shells", isSynth ? COLOR.support : COLOR.shell) - .addPolys(top.shells || [], vopt({ offset, height, clean: true })); - - output - .setLayer("solid fill", isSynth ? COLOR.support : COLOR.fill) - .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height })); - - if (!(slice.belt && slice.belt.anchor)) output - .setLayer("sparse fill", COLOR.infill) - .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - - if (slice.belt && slice.belt.anchor) output - .setLayer("anchor", COLOR.anchor) - .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - - if (top.thin_fill) output - .setLayer("thin fill", COLOR.fill) - .addLines(top.thin_fill, vopt({ offset, height })); - - if (top.gaps) output - .setLayer("gaps", COLOR.gaps) - .addPolys(top.gaps, vopt({ offset, height, thin: true })); - - if (isThin && devel && top.fill_off && top.fill_off.length) { - slice.output() - .setLayer('fill inset', { face: 0, line: 0xaaaaaa, check: 0xaaaaaa }) - .addPolys(top.fill_off); - // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) - // .addPolys(top.last); - } - }); - - if (isThin && devel) { - if (slice.solids && slice.solids.length) output - .setLayer("solids", { face: 0xbbbb00, check: 0xbbbb00 }) - .addAreas(slice.solids); - - if (slice.bridges && slice.bridges.length) output - .setLayer("bridges", { face: 0x00cccc, line: 0x00cccc, check: 0x00cccc }) - .addAreas(slice.bridges); - - if (slice.flats && slice.flats.length) output - .setLayer("flats", { face: 0xaa00aa, line: 0xaa00aa, check: 0xaa00aa }) - .addAreas(slice.flats); - } - - if (slice.supports) output - .setLayer("support", COLOR.support) - .addPolys(slice.supports, vopt({ offset, height })); - - if (slice.supports) slice.supports.forEach(poly => { - if (poly.fill) output - .setLayer("support", COLOR.support) - .addLines(poly.fill, vopt({ offset, height })); - }); - - if (slice.xray) { - const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff ]; - if (slice.lines) { - slice.lines.forEach((line, i) => { - const group = i % 5; - slice.output().setLayer(`l${group}`, color[group]).addLine(line.p1, line.p2); - }); - } - if (slice.groups) - POLY.nest(slice.groups).forEach((poly, i) => { - const group = i % 5; - slice.addTop(poly); - // slice.output().setLayer(`g${i}`, 0x888888).addPoly(poly); - slice.output().setLayer(`g${i}`, color[group]).addPoly(poly); - }); - } - - // console.log(slice.index, slice.render.stats); - } - - // shared with SLA driver and minions - FDM.share = { - doShells, - doTopShells, - doDiff, - projectFlats, - projectBridges - }; - - /** - * Compute offset shell polygons. For FDM, the first offset is usually half - * of the nozzle width. Each subsequent offset is a full nozzle width. User - * parameters control tweaks to these numbers to allow for better shell bonding. - * The last shell generated is a "fillOffset" shell. Fill lines are clipped to - * this polygon. Adjusting fillOffset controls bonding of infill to the shells. - * - * Most of this is done in slicePost() in FDM mode. now this is used by SLA, Laser - * - * @param {number} count - * @param {number} offsetN - * @param {number} fillOffset - * @param {Obejct} options - */ - function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { - for (let top of slice.tops) { - doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); - } - } - - function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { - // pretend we're a top object in minions - if (!top.poly) { - top = { poly: top }; - } - - // add simple (low rez poly) where less accuracy is OK - top.simple = top.poly.simple(); - - let wasm = opt.wasm; - let top_poly = [ top.poly ]; - - if (opt.vase) { - // remove top poly inners in vase mode - top.poly = top.poly.clone(false); - } - - top.shells = []; - top.fill_off = []; - top.fill_lines = []; - - let last = [], - gaps = []; - - if (count) { - // permit offset of 0 for laser and drag knife - if (offset1 === 0 && count === 1) { - last = top_poly.clone(true); - top.shells = last; + async function onSliceDone(slices) { + // remove all empty slices above part but leave below + // for multi-part (multi-extruder) setups where the void is ok + // also reverse because slicing occurs bottom-up + let found = false; + slices = slices.reverse().filter(slice => { + if (slice.tops.length) { + return found = true; } else { - // heal top open polygons if the ends are close (benchy tilt test) - top_poly.forEach(p => { if (p.open) { - let dist = p.first().distTo2D(p.last()); - if (dist < 1) p.open = false; - } }); - if (opt.danger && opt.thin) { - top.thin_fill = []; - top.fill_sparse = []; - let layers = POLY.inset(top_poly, offsetN, count, z, true); - last = layers.last().mid; - top.shells = layers.map(r => r.mid).flat(); - top.gaps = layers.map(r => r.gap).flat(); - let off = offsetN; - let min = off * 0.75; - let max = off * 4; - for (let poly of layers.map(r => r.gap).flat()) { - let centers = poly.centers(off/2, z, min, max, {lines:false}); - top.fill_sparse.appendAll(centers); - // top.fill_lines.appendAll(centers); + return found; + } + }).reverse(); + + // connect slices into linked list for island/bridge projections + for (let i=1; i 0.1) { + continue; + } + let ntops = []; + POLY.subtract(tops, pslice.clips, ntops, null, slice.z, 0); + tops = ntops; + } + // trim to group's shadow if not in belt mode + if (!isBelt) { + tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z); + } + } + slice.tops = []; + for (let t of tops) { + slice.addTop(t); + } + doShells(slice, 1, shellOffset / 2); + } + } - oso.outs.forEach((polys, i) => { - polys.forEach(p => { - p.depth = i; - if (p.fill_off) { - p.fill_off.forEach(pi => pi.depth = i); - } - if (p.inner) { - for (let pi of p.inner) { - pi.depth = p.depth; - } - } - top.shells.push(p); - }); - last = polys; - }); + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + trackupdate(index / slices.length, from, to, msg); + } - // slice.solids.trimmed = slice.solids.trimmed || []; - oso.gaps.forEach((polys, i) => { - let off = (i == 0 ? offset1 : offsetN); - polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0, wasm}); - top.thin_fill.appendAll(cullIntersections( - fillArea(polys, 45, off/2, [], 0.01, off*2), - fillArea(polys, 135, off/2, [], 0.01, off*2), - )); - gaps = polys; + function trackupdate(pct, from, to, msg) { + onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); + } + + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(slice => { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + // do not hint polygon fill longer than a max span length + config.hint_len_max = util.sqr(process.sliceBridgeMax); + + // reset for solids, support projections + // and other annotations + slices.forEach(slice => { + slice.widget = widget; + slice.extruder = extruder; + slice.solids = []; + }); + + // just the top/bottom special solid layers or range defined solid layers + forSlices(0.15, 0.2, slice => { + let range = slice.params; + let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; + let isBottom = slice.index < process.sliceBottomLayers; + let isTop = slice.index > slices.length - process.sliceTopLayers - 1; + let isDense = range.sliceFillSparse > 0.995; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; + let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; + if (solidWidth) { + let fillSpace = fillSpacing * spaceMult * solidWidth; + doSolidLayerFill(slice, fillSpace, sliceFillAngle); + } + sliceFillAngle += 90.0; + }, "solid layers"); + + // add lead in anchor when specified in belt mode (but not for synths) + if (isBelt && !isSynth) { + // find adjusted zero point from slices + let smin = Infinity; + for (let slice of slices) { + let miny = Infinity; + for (let poly of slice.topPolys()) { + let y = poly.bounds.maxy; + let z = slice.z; + let by = z - y; + if (by < miny) miny = by; + if (by < smin) smin = by; + } + slice.belt = { miny, touch: false }; + } + // mark slices with tops touching belt + // also find max width of first 5 layers + let start; + let minx = Infinity, maxx = -Infinity; + let peek = 0; + for (let slice of slices) { + if (slice.tops.length && peek++ < 5) { + for (let poly of slice.topPolys()) { + minx = Math.min(minx, poly.bounds.minx); + maxx = Math.max(maxx, poly.bounds.maxx); + } + } + // mark slice as touching belt if near miny + // if (Math.abs(slice.belt.miny - smin) < 0.01) { + if (Math.abs(slice.belt.miny) < 0.01) { + slice.belt.touch = true; + if (!start) start = slice; + } + } + // ensure we start against a layer with shells + while (start && start.up && start.topShells().length === 0) { + start = start.up; + } + // if a brim applies, add that width to anchor + let brim = getRangeParameters(process, 0).firstLayerBrim || 0; + if (brim) { + minx -= brim; + maxx += brim; + } + // array of added top.fill_sparse arrays + let adds = []; + let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * beltfact; + while (anchorlen && start && anchorlen >= sliceHeight) { + let addto = start.down; + if (!addto) { + addto = newSlice(start.z - sliceHeight); + addto.extruder = extruder; + addto.belt = { }; + addto.height = start.height; + addto.up = start; + start.down = addto; + slices.splice(0,0,addto); + } else if (!addto.belt) { + console.log({addto_missing_belt: addto}); + addto.belt = {}; + } + addto.index = -1; + addto.belt.anchor = true; + // this allows the anchor to print bi-directionally + // by removing the forced start-point in print.js + addto.belt.touch = false; + let z = addto.z; + let y = z - smin - (nozzleSize / 2); + let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); + let snew = addto.addTop(splat).fill_sparse = [ splat ]; + adds.push(snew); + start = addto; + anchorlen -= sliceHeight; + } + // add anchor bump + let bump = process.firstLayerBeltBump; + if (bump) { + adds = adds.reverse().slice(1, adds.length - 1); + let count = 1; + for (let add of adds) { + let poly = add[0]; + let y = count++ * -start.height * 2; + if (-y > bump) { + count--; + // break; + } + let first = poly.first(); + // add up/over/down to anchor line (close = down) + // which completes the bump perimeter + poly.push(poly.last().add({x:0, y, z:0})); + poly.push(poly.first().add({x:0, y, z:0})); + poly.setClosed(); + if (count > 2 && maxx - minx > 10) { + // add vertical hatch lines insibe bump shell + let mp = (maxx + minx) / 2; + let dx = (maxx - minx - 2); + dx = (Math.floor(dx / 3) * 3) / 2; + let fy = first.y; + let fz = first.z; + let n2 = nozzleSize / 2; + for (let x = mp - dx; x <= mp + dx ; x += 3) { + add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); + } + } + } + } + } + + // calculations only relevant when solid layers are used + if (solidLayers && !vaseMode && !isSynth) { + profileStart("delta"); + forSlices(0.2, 0.34, slice => { + if (slice.index > 0) doDiff(slice, solidMinArea); + }, "layer deltas"); + profileEnd(); + profileStart("delta-project"); + forSlices(0.34, 0.35, slice => { + projectFlats(slice, solidLayers); + projectBridges(slice, solidLayers); + }, "layer deltas"); + profileEnd(); + profileStart("solid-fill") + let promises = isConcurrent ? [] : undefined; + forSlices(0.35, promises ? 0.4 : 0.5, slice => { + let params = slice.params || process; + let first = slice.index === 0; + let solidWidth = params.sliceFillWidth || 1; + let spaceMult = first ? params.firstLayerLineMult || 1 : 1; + let fillSpace = fillSpacing * spaceMult * solidWidth; + doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); + sliceFillAngle += 90.0; + }, "fill solids"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.4, 0.5); + }); + } + profileEnd(); + } + + if (!isSynth && !vaseMode) { + // sparse layers only present when non-vase mose and sparse % > 0 + let lastType; + let promises = isConcurrent ? [] : undefined; + forSlices(0.5, promises ? 0.55 : 0.7, slice => { + let params = slice.params || process; + if (!params.sliceFillSparse) { + return; + } + let newType = params.sliceFillType; + doSparseLayerFill(slice, { + settings, + process, + device, + lineWidth, + spacing: fillOffset, + density: params.sliceFillSparse, + bounds: widget.getBoundingBox(), + height: sliceHeight, + type: newType, + cache: params._range !== true && lastType === newType, + promises + }); + lastType = newType; + }, "infill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.55, 0.7); + }); + } + // back-fill slices marked for infill cloning + for (let slice of slices) { + if (slice._clone_sparse) { + let tops = slice.tops; + let down = slice.down.tops; + for (let i=0; i p.cloneZ(slice.z)); + } + } + } + } else if (isSynth) { + // fill manual supports differently + let outline = process.sliceSupportOutline || false; + let promises = isConcurrent ? [] : undefined; + let resolve = []; + forSlices(0.5, promises ? 0.6 : 0.7, slice => { + let params = slice.params || process; + let density = params.sliceSupportDensity; + if (density) + for (let top of slice.tops) { + if (!outline) { + let offset = top.shells; + fillSupportPolys(promises, offset, lineWidth, density, slice.z, isBelt); + resolve.push({top, offset}); + } else { + let offset = []; + POLY.expand(top.shells || [], -nozzleSize/4, slice.z, offset); + fillSupportPolys(promises, offset, lineWidth, density, slice.z, isBelt); + resolve.push({top, offset}); + } + } + }, "infill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.6, 0.7); + }); + } + for (let rec of resolve) { + let lines = rec.top.fill_lines = rec.offset.map(o => o.fill).flat().filter(v => v); + // if copying simply's support type, eliminate shells + // and zig/zag lines connectd by shell segments + if (!outline) { + let newlines = []; + let op2; + let eo = 0; + let idx = 1; + for (let i=0; i { + promises.push(doSupport(slice, process, widget.shadow, { exp: isDanger })); + }, "support"); + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.75, 0.8); + }); + profileEnd(); + profileStart("support-fill"); + promises = false && isConcurrent ? [] : undefined; + forSlices(0.8, promises ? 0.88 : 0.9, slice => { + doSupportFill(promises, slice, lineWidth, supportDensity, process.sliceSupportArea, isBelt); + }, "support"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.88, 0.9); + }); + } + profileEnd(); + } + + // render if not explicitly disabled + if (render) { + forSlices(0.9, 1.0, slice => { + let params = slice.params || process; + doRender(slice, isSynth, params, controller.devel); + }, "render"); + } + + if (isBelt) { + let bounds = base.newBounds(); + for (let top of slices[0].tops) { + bounds.merge(top.poly.bounds); + } + widget.belt.miny = -bounds.miny; + widget.belt.midy = (bounds.miny + bounds.maxy) / 2; + } + } + +} + +function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); +} + +function doRender(slice, isSynth, params, devel) { + const output = slice.output(); + const height = slice.height / 2; + const solidWidth = params.sliceFillWidth || 1; + + slice.tops.forEach(top => { + if (isThin) output + .setLayer('part', { line: 0x333333, check: 0x333333 }) + .addPolys(top.poly); + + output + .setLayer("shells", isSynth ? COLOR.support : COLOR.shell) + .addPolys(top.shells || [], vopt({ offset, height, clean: true })); + + output + .setLayer("solid fill", isSynth ? COLOR.support : COLOR.fill) + .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height })); + + if (!(slice.belt && slice.belt.anchor)) output + .setLayer("sparse fill", COLOR.infill) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + + if (slice.belt && slice.belt.anchor) output + .setLayer("anchor", COLOR.anchor) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + + if (top.thin_fill) output + .setLayer("thin fill", COLOR.fill) + .addLines(top.thin_fill, vopt({ offset, height })); + + if (top.gaps) output + .setLayer("gaps", COLOR.gaps) + .addPolys(top.gaps, vopt({ offset, height, thin: true })); + + if (isThin && devel && top.fill_off && top.fill_off.length) { + slice.output() + .setLayer('fill inset', { face: 0, line: 0xaaaaaa, check: 0xaaaaaa }) + .addPolys(top.fill_off); + // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) + // .addPolys(top.last); + } + }); + + if (isThin && devel) { + if (slice.solids && slice.solids.length) output + .setLayer("solids", { face: 0xbbbb00, check: 0xbbbb00 }) + .addAreas(slice.solids); + + if (slice.bridges && slice.bridges.length) output + .setLayer("bridges", { face: 0x00cccc, line: 0x00cccc, check: 0x00cccc }) + .addAreas(slice.bridges); + + if (slice.flats && slice.flats.length) output + .setLayer("flats", { face: 0xaa00aa, line: 0xaa00aa, check: 0xaa00aa }) + .addAreas(slice.flats); + } + + if (slice.supports) output + .setLayer("support", COLOR.support) + .addPolys(slice.supports, vopt({ offset, height })); + + if (slice.supports) slice.supports.forEach(poly => { + if (poly.fill) output + .setLayer("support", COLOR.support) + .addLines(poly.fill, vopt({ offset, height })); + }); + + if (slice.xray) { + const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff ]; + if (slice.lines) { + slice.lines.forEach((line, i) => { + const group = i % 5; + slice.output().setLayer(`l${group}`, color[group]).addLine(line.p1, line.p2); + }); + } + if (slice.groups) + POLY.nest(slice.groups).forEach((poly, i) => { + const group = i % 5; + slice.addTop(poly); + // slice.output().setLayer(`g${i}`, 0x888888).addPoly(poly); + slice.output().setLayer(`g${i}`, color[group]).addPoly(poly); + }); + } + + // console.log(slice.index, slice.render.stats); +} + +// shared with SLA driver and minions +FDM.share = { + doShells, + doTopShells, + doDiff, + projectFlats, + projectBridges +}; + +/** + * Compute offset shell polygons. For FDM, the first offset is usually half + * of the nozzle width. Each subsequent offset is a full nozzle width. User + * parameters control tweaks to these numbers to allow for better shell bonding. + * The last shell generated is a "fillOffset" shell. Fill lines are clipped to + * this polygon. Adjusting fillOffset controls bonding of infill to the shells. + * + * Most of this is done in slicePost() in FDM mode. now this is used by SLA, Laser + * + * @param {number} count + * @param {number} offsetN + * @param {number} fillOffset + * @param {Obejct} options + */ +function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { + for (let top of slice.tops) { + doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); + } +} + +function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { + // pretend we're a top object in minions + if (!top.poly) { + top = { poly: top }; + } + + // add simple (low rez poly) where less accuracy is OK + top.simple = top.poly.simple(); + + let wasm = opt.wasm; + let top_poly = [ top.poly ]; + + if (opt.vase) { + // remove top poly inners in vase mode + top.poly = top.poly.clone(false); + } + + top.shells = []; + top.fill_off = []; + top.fill_lines = []; + + let last = [], + gaps = []; + + if (count) { + // permit offset of 0 for laser and drag knife + if (offset1 === 0 && count === 1) { + last = top_poly.clone(true); + top.shells = last; + } else { + // heal top open polygons if the ends are close (benchy tilt test) + top_poly.forEach(p => { if (p.open) { + let dist = p.first().distTo2D(p.last()); + if (dist < 1) p.open = false; + } }); + if (opt.danger && opt.thin) { + top.thin_fill = []; + top.fill_sparse = []; + let layers = POLY.inset(top_poly, offsetN, count, z, true); + last = layers.last().mid; + top.shells = layers.map(r => r.mid).flat(); + top.gaps = layers.map(r => r.gap).flat(); + let off = offsetN; + let min = off * 0.75; + let max = off * 4; + for (let poly of layers.map(r => r.gap).flat()) { + let centers = poly.centers(off/2, z, min, max, {lines:false}); + top.fill_sparse.appendAll(centers); + // top.fill_lines.appendAll(centers); + } + } else if (opt.thin) { + top.thin_fill = []; + let oso = {z, count, gaps: [], outs: [], minArea: 0.05, wasm}; + POLY.offset(top_poly, [-offset1, -offsetN], oso); + + oso.outs.forEach((polys, i) => { + polys.forEach(p => { + p.depth = i; + if (p.fill_off) { + p.fill_off.forEach(pi => pi.depth = i); + } + if (p.inner) { + for (let pi of p.inner) { + pi.depth = p.depth; + } + } + top.shells.push(p); }); - } else { - // standard wall offsetting strategy - POLY.offset( - top_poly, - [-offset1, -offsetN], - { - z, - wasm, - count, - outs: top.shells, - flat: true, - call: (polys, onCount) => { - last = polys; - // mark each poly with depth (offset #) starting at 0 - for (let p of polys) { - p.depth = count - onCount; - if (p.fill_off) p.fill_off.forEach(function(pi) { - // use negative offset for inners - pi.depth = -(count - onCount); - }); - // mark inner depth to match parent - if (p.inner) { - for (let pi of p.inner) { - pi.depth = p.depth; - } + last = polys; + }); + + // slice.solids.trimmed = slice.solids.trimmed || []; + oso.gaps.forEach((polys, i) => { + let off = (i == 0 ? offset1 : offsetN); + polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0, wasm}); + top.thin_fill.appendAll(cullIntersections( + fillArea(polys, 45, off/2, [], 0.01, off*2), + fillArea(polys, 135, off/2, [], 0.01, off*2), + )); + gaps = polys; + }); + } else { + // standard wall offsetting strategy + POLY.offset( + top_poly, + [-offset1, -offsetN], + { + z, + wasm, + count, + outs: top.shells, + flat: true, + call: (polys, onCount) => { + last = polys; + // mark each poly with depth (offset #) starting at 0 + for (let p of polys) { + p.depth = count - onCount; + if (p.fill_off) p.fill_off.forEach(function(pi) { + // use negative offset for inners + pi.depth = -(count - onCount); + }); + // mark inner depth to match parent + if (p.inner) { + for (let pi of p.inner) { + pi.depth = p.depth; } } } } - ); - } + } + ); } - } else { - // no shells, just infill, is permitted - last = [top.poly]; } - - // generate fill offset poly set from last offset to top.fill_off - if (fillOffset && last.length > 0) { - // if gaps present, remove that area from fill inset - if (gaps.length) { - let nulast = []; - POLY.subtract(last, gaps, nulast, null, z); - last = nulast; - } - last.forEach(function(inner) { - POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z}); - }); - } - - // for diffing - top.last = last; - // top.last_simple = last.map(p => p.clean(true, undefined, config.clipper / 10)); - - return top; + } else { + // no shells, just infill, is permitted + last = [top.poly]; } - /** - * Create an entirely solid layer by filling all top polygons - * with an alternating pattern. - * - * @param {number} linewidth - * @param {number} angle - * @param {number} density - */ - function doSolidLayerFill(slice, spacing, angle) { - if (slice.tops.length === 0 || typeof(angle) != 'number') { - slice.isSolidLayer = false; - return; + // generate fill offset poly set from last offset to top.fill_off + if (fillOffset && last.length > 0) { + // if gaps present, remove that area from fill inset + if (gaps.length) { + let nulast = []; + POLY.subtract(last, gaps, nulast, null, z); + last = nulast; } - - slice.tops.forEach(function(top) { - let lines = fillArea(top.fill_off, angle, spacing, null); - top.fill_lines.appendAll(lines); - }); - - slice.isSolidLayer = true; - }; - - /** - * Take output from pluggable sparse infill algorithm and clip to - * the bounds of the top polygons and their inner solid areas. - */ - function doSparseLayerFill(slice, options = {}) { - let process = options.process, - spacing = options.spacing, // spacing space between fill lines - density = options.density, // density of infill 0.0 - 1.0 - bounds = options.bounds, // bounding box of widget - height = options.height, // z layer height - cache = !(options.cache === false), - type = options.type || 'hex'; - - if (slice.tops.length === 0 || density === 0.0 || slice.isSolidLayer || slice.index < 0) { - slice.isSparseFill = false; - return; - } - - let tops = slice.tops, - down = slice.down, - clib = self.ClipperLib, - ctyp = clib.ClipType, - ptyp = clib.PolyType, - cfil = clib.PolyFillType, - clip = new clib.Clipper(), - ctre = new clib.PolyTree(), - poly, - polys = [], - lines = [], - line = [], - solids = [], - // callback passed to pluggable infill algorithm - target = { - // slice and slice property access - slice: function() { return slice }, - zIndex: function() { return slice.index }, - zValue: function() { return slice.z }, - // various option map access - options: function() { return options }, - lineWidth: function() { return options.lineWidth }, - bounds: function() { return bounds }, - zHeight: function() { return height }, - offset: function() { return spacing }, - density: function() { return density }, - repeat: function() { return process.sliceFillRepeat }, - // output functions - emit: function(x,y) { - if (isNaN(x)) { - solids.push(x); - } else { - line.push(newPoint(x, y, slice.z)); - slice.isSparseFill = true; - } - }, - newline: function() { - if (line.length > 0) { - lines.push(line); - line = []; - } - } - }; - - // use specified fill type - if (type && fill[type]) { - fill[type](target); - } else { - console.log({missing_infill: type}); - return; - } - - // force emit of last line - target.newline(); - - // prepare top infill structure - for (let top of tops) { - top.fill_sparse = top.fill_sparse || []; - polys.appendAll(top.fill_off); - polys.appendAll(top.solids); - } - - // update fill fingerprint for this slice - slice._fill_finger = POLY.fingerprint(polys); - - let skippable = cache && fill_fixed[type] ? true : false; - let miss = false; - // if the layer below has the same fingerprint, - // we may be able to clone the infill instead of regenerating it - if (skippable && slice.fingerprintSame(down)) { - // the fill fingerprint can slightly different because of solid projections - if (down._fill_finger && POLY.fingerprintCompare(slice._fill_finger, down._fill_finger)) { - for (let i=0; i { - top.fill_lines.push(p1, p2); - }); - } - } - if (fillable.length) { - let lines = POLY.fillArea(fillable, angl, options.lineWidth); - top.fill_lines.appendAll(lines); - } - } - } - } - - // if only solids were added and no lines to clip - if (!sparse_clip) { - return; - } - - if (options.promises) { - options.promises.push(kiri.minions.clip(slice, polys, lines)); - return; - } - - lines = lines.map(a => a.map(p => p.toClipper())); - clip.AddPaths(lines, ptyp.ptSubject, false); - clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); - - if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { - for (let node of ctre.m_AllPolys) { - poly = POLY.fromClipperNode(node, slice.z); - for (let top of tops) { - // use only polygons inside this top - if (poly.isInside(top.poly)) { - top.fill_sparse.push(poly); - } - } - } - } - }; - - /** - * Find difference between fill inset poly on two adjacent layers. - * Used to calculate bridges, flats and then solid projections. - * 'expand' is used for top offsets in SLA mode - */ - function doDiff(slice, minArea, options = {}) { - const { sla, fakedown } = options; - if (slice.index === 0 && !fakedown) { - return; - } - const top = slice, - down = slice.down || (fakedown ? newSlice(-1) : null), - topInner = sla ? top.topPolys() : top.topInners(), - downInner = sla ? down.topPolys() : down.topInners(), - bridges = top.bridges = [], - flats = down.flats = []; - - // skip diffing layers that are identical - if (slice.fingerprintSame(down)) { - top.bridges = bridges; - down.flats = flats; - return; - } - - POLY.subtract(topInner, downInner, bridges, flats, slice.z, minArea, { - wasm: true - }); - }; - - /** - * - * - * @param {Polygon[]} polys - */ - function addSolidFills(slice, polys) { - if (slice.solids) { - slice.solids.appendAll(polys); - } else if (polys && polys.length) { - console.log({no_solids_in: slice, for: polys}) - } - }; - - /** - * project bottom flats down - */ - function projectFlats(slice, count) { - if (slice.isSolidLayer || !slice.down || !slice.flats) return; - projectSolid(slice, slice.flats, count, false, true); - }; - - /** - * project top bridges up - */ - function projectBridges(slice, count) { - if (slice.isSolidLayer || !slice.up || !slice.bridges) return; - projectSolid(slice, slice.bridges, count, true, true); - }; - - /** - * fill projected areas and store line data - * @return {boolean} true if filled, false if not - */ - function doSolidsFill(slice, spacing, angle, minArea, fillQ) { - let minarea = minArea || 1, - tops = slice.tops, - solids = slice.solids; - - if (!(tops && solids)) { - return; - } - - let unioned = POLY.union(solids, undefined, true, { wasm: true }).flat(), - isSLA = (spacing === undefined && angle === undefined); - - if (solids.length === 0) return false; - if (unioned.length === 0) return false; - - let trims = [], - inner = isSLA ? slice.topPolys() : slice.topFillOff(); - - // trim each solid to the inner bounds - for (let p of unioned) { - p.setZ(slice.z); - for (let i of inner) { - let masks = p.mask(i); - if (masks && masks.length > 0) { - trims.appendAll(masks); - } - } - } - - // clear old solids and make array for new - tops.forEach(top => { top.solids = [] }); - - // replace solids with merged and trimmed solids - slice.solids = solids = trims; - - // parent each solid polygon inside the smallest bounding top - for (let solid of solids) { - for (let top of tops) { - if (top.poly.overlaps(solid)) { - if (!solid.parent || solid.parent.area() > top.poly.area()) { - if (solid.areaDeep() < minarea) { - // console.log({i:slice.index,cull_solid:solid,area:solid.areaDeep()}); - continue; - } - solid.parent = top.poly; - top.solids.push(solid); - } - } - } - } - - // for SLA to bypass line infill - if (isSLA) { - return true; - } - - // create empty filled line array for each top - for (let top of tops) { - // synth belt anchor tops don't want fill - if (!top.fill_lines) { - continue; - } - const tofill = []; - const angfill = []; - const newfill = top.fill_lines = []; - // determine fill orientation from top - for (let solid of solids) { - if (solid.parent === top.poly) { - if (solid.fillang) { - angfill.push(solid); - } else { - tofill.push(solid); - } - } - } - if (tofill.length > 0) { - doFillArea(fillQ, tofill, angle, spacing, newfill); - // top.fill_lines_norm = {angle:angle,spacing:spacing}; - } - if (angfill.length > 0) { - top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; - for (let af of angfill) { - doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); - // top.fill_lines_ang.list.push(af.fillang.angle + 45); - // top.fill_lines_ang.poly.push(af.clone()); - } - } - } - } - - function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { - if (fillQ) { - fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); - } else { - POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); - } - } - - /** - * calculate external overhangs requiring support - */ - async function doSupport(slice, proc, shadow, opt = {}) { - let maxBridge = proc.sliceSupportSpan || 5, - minArea = proc.supportMinArea || 0.1, - pillarSize = proc.sliceSupportSize, - offset = proc.sliceSupportOffset || 0, - gap = proc.sliceSupportGap, - size = (pillarSize || 1), - tops = slice.topPolys(), - trimTo = tops; - - let traces = POLY.flatten(slice.topShells().clone(true)), - fill = slice.topFill(), - points = [], - down = slice.down, - down_tops = down ? down.topPolys() : null, - down_traces = down ? POLY.flatten(down.topShells().clone(true)) : null; - - if (opt.exp && down_tops) { - let points = down_tops.map(p => p.deepLength).reduce((a,v)=>a+v); - if (points > 200) { - // use de-rez'd top shadow instead - down_tops = down.topSimples(); - // de-rez trace polys because it's not that important for supports - down_traces = down_traces.map(p => p.clean(true, undefined, config.clipper / 10)); - } - } - - // DEBUG code - let SDBG = false; - let cks = SDBG ? [] : undefined; - let pip = SDBG ? [] : undefined; - let pcl = SDBG ? [] : undefined; - - // check if point is supported by layer below - function checkPointSupport(point) { - if (SDBG) cks.push(point); // DEBUG - // skip points close to other support points - for (let i=0; i= maxBridge) { - let slope = p1.slopeTo(p2).factor(1/dist), - segs = Math.floor(dist / maxBridge) + 1, - seglen = dist / segs; - while (i < segs) { - checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); - } - } - if (poly) checkPointSupport(p2); - } - - let supports = []; - - // generate support polys from unsupported points - if (slice.down) (function() { - // check trace line support needs - traces.forEach(function(trace) { - trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); - }); - - // add offset solids to supports (or fill depending) - fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); - - // skip the rest if no points or supports - if (!(points.length || supports.length)) return; - - let pillars = []; - - // for each point, create a bounding rectangle - points.forEach(function(point) { - pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); - }); - - supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); - // merge pillars and replace with convex hull of outer points (aka smoothing) - pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { - supports.push(base.newPolygon().createConvexHull(pillar.points)); - }); - })(); - - // DEBUG code - if (SDBG && down_traces) slice.output() - .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) - .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) - .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) - .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) - .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) - .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) - .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) - .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) - .setLayer('dtr', { line: 0x0, check: 0x0 }) - .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); - ; - - if (supports.length === 0) { - return; - } - - // then union supports - if (supports.length > 10) { - supports = await kiri.minions.union(supports); - } else { - supports = POLY.union(supports, null, true, { wasm: false }); - } - - // clip to top polys - supports = POLY.trimTo(supports, shadow); - - let depth = 0; - while (down && supports.length > 0) { - down.supports = down.supports || []; - - let trimmed = [], culled = []; - - // culled = supports; - // clip supports to shell offsets - POLY.subtract(supports, down.topSimples(), trimmed, null, slice.z, minArea, { wasm: false }); - - // set depth hint on support polys for infill density - trimmed.forEach(function(trim) { - if (trim.area() < minArea) return; - culled.push(trim.setZ(down.z)); - }); - - // exit when no more support polys exist - if (culled.length === 0) break; - - // new bridge polys for next pass (skip first layer below) - if (depth >= gap) { - down.supports.appendAll(culled); - } - - supports = culled; - down = down.down; - depth++; - } - - } - - function doSupportFill(promises, slice, linewidth, density, minArea, isBelt) { - let supports = slice.supports, - nsB = [], - nsC = [], - min = minArea || 0.1; - - if (!supports) return; - - // union supports - supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); - - // clip supports to slice clip offset (or shell if none) - POLY.subtract(supports, slice.clips, nsB, null, slice.z, min, { wasm: false }); - supports = nsB; - - // also trim to lower offsets, if they exist - if (slice.down && slice.down.clips) { - POLY.subtract(nsB, slice.down.clips, nsC, null, slice.z, min, { wasm: false }); - supports = nsC; - } - - if (supports) { - fillSupportPolys(promises, supports, linewidth, density, slice.z, isBelt); - } - - // re-assign new supports back to slice - slice.supports = supports; - }; - - function fillSupportPolys(promises, polys, linewidth, density, z, isBelt) { - // calculate fill density - let spacing = linewidth * (1 / density); - polys.forEach(function (poly) { - // angle based on width/height ratio - let angle = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? 90 : 0; - // inset support poly for fill lines 33% of nozzle width - let inset = POLY.offset([poly], -linewidth/3, {flat: true, z, wasm: true}); - // do the fill - if (inset && inset.length > 0) { - doFillArea(promises, inset, angle, spacing, poly.fill = []); - } - return true; + last.forEach(function(inner) { + POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z}); }); } - /** - * - * @param {Slice} slice - * @param {Polygon[]} polys - * @param {number} count - * @param {boolean} up - * @param {boolean} first - * @returns {*} - */ - function projectSolid(slice, polys, count, up, first) { - if (!slice || slice.isSolidLayer || count <= 0) { - return; - } - let clones = polys.clone(true); - if (first) { - clones.forEach(function(p) { - p.hintFillAngle(); - }); - } - addSolidFills(slice, clones); - if (count > 0) { - if (up) projectSolid(slice.up, polys, count-1, true, false); - else projectSolid(slice.down, polys, count-1, false, false); - } + // for diffing + top.last = last; + // top.last_simple = last.map(p => p.clean(true, undefined, config.clipper / 10)); + + return top; +} + +/** + * Create an entirely solid layer by filling all top polygons + * with an alternating pattern. + * + * @param {number} linewidth + * @param {number} angle + * @param {number} density + */ + function doSolidLayerFill(slice, spacing, angle) { + if (slice.tops.length === 0 || typeof(angle) != 'number') { + slice.isSolidLayer = false; + return; } - /** - * given an array of arrays of points (lines), eliminate intersections - * between groups, then return a unified array of shortest non-intersects. - * - * @returns {Point[]} - */ - function cullIntersections() { - function toLines(pts) { - let lns = []; - for (let i=0, il=pts.length; i t); - if (aOa.length < 1) return; - let aa = toLines(aOa.shift()); - while (aOa.length) { - let bb = toLines(aOa.shift()); - loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); - } - let good = []; - for (let i=0, il=aa.length; i 2 ? good : []; + slice.tops.forEach(function(top) { + let lines = fillArea(top.fill_off, angle, spacing, null); + top.fill_lines.appendAll(lines); + }); + + slice.isSolidLayer = true; +}; + +/** + * Take output from pluggable sparse infill algorithm and clip to + * the bounds of the top polygons and their inner solid areas. + */ +function doSparseLayerFill(slice, options = {}) { + let process = options.process, + spacing = options.spacing, // spacing space between fill lines + density = options.density, // density of infill 0.0 - 1.0 + bounds = options.bounds, // bounding box of widget + height = options.height, // z layer height + cache = !(options.cache === false), + type = options.type || 'hex'; + + if (slice.tops.length === 0 || density === 0.0 || slice.isSolidLayer || slice.index < 0) { + slice.isSparseFill = false; + return; } - FDM.supports = function(settings, widget) { - let isBelt = settings.device.bedBelt; - let process = settings.process; - let size = process.sliceSupportSize; - let s4 = size / 4; - let s2 = size * 0.45; - let min = 0.01; - let geo = new THREE.BufferGeometry(); - geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); - let mat = new THREE.MeshBasicMaterial(); - let rad = (Math.PI / 180); - let deg = (180 / Math.PI); - let angle = rad * settings.process.sliceSupportAngle; - let thresh = -Math.sin(angle); - let dir = new THREE.Vector3(0,0,-1) - let add = []; - let mesh = new THREE.Mesh(geo, mat); - let platform = new THREE.Mesh( - new THREE.PlaneGeometry(1000,1000,1), mat - ); - function pointIn(x, y, p1, p2, p3) { - let det = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) - return det * ((p2.x - p1.x) * (y - p1.y) - (p2.y - p1.y) * (x - p1.x)) > 0 && - det * ((p3.x - p2.x) * (y - p2.y) - (p3.y - p2.y) * (x - p2.x)) > 0 && - det * ((p1.x - p3.x) * (y - p3.y) - (p1.y - p3.y) * (x - p3.x)) > 0 - } - // first, last, distance - function fld(arr, key) { - let first = arr[0]; - let last = arr.last(); - let dist = last[key] - first[key]; - return { first, last, dist } - } - // sorted range distance from key - function rdist(range, key) { - return range.last[key] - range.first[key]; - } - // test area - function ta(p1, p2, p3) { - let sortx = [p1,p2,p3].sort((a,b) => { return a.x - b.x }); - let sorty = [p1,p2,p3].sort((a,b) => { return a.y - b.y }); - let sortz = [p1,p2,p3].sort((a,b) => { return a.z - b.z }); - let xv = fld(sortx, 'x'); - let yv = fld(sorty, 'y'); - let xa = base.util.lerp(xv.first.x + s4, xv.last.x - s4, s2, true); - let ya = base.util.lerp(yv.first.y + s4, yv.last.y - s4, s2, true); - for (let x of xa) { - for (let y of ya) { - if (pointIn(x, y, p1, p2, p3)) { - let z = base.util.zInPlane(p1, p2, p3, x, y); - tp(new THREE.Vector3(x, y, z)); - } + let tops = slice.tops, + down = slice.down, + clib = self.ClipperLib, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + poly, + polys = [], + lines = [], + line = [], + solids = [], + // callback passed to pluggable infill algorithm + target = { + // slice and slice property access + slice: function() { return slice }, + zIndex: function() { return slice.index }, + zValue: function() { return slice.z }, + // various option map access + options: function() { return options }, + lineWidth: function() { return options.lineWidth }, + bounds: function() { return bounds }, + zHeight: function() { return height }, + offset: function() { return spacing }, + density: function() { return density }, + repeat: function() { return process.sliceFillRepeat }, + // output functions + emit: function(x,y) { + if (isNaN(x)) { + solids.push(x); + } else { + line.push(newPoint(x, y, slice.z)); + slice.isSparseFill = true; + } + }, + newline: function() { + if (line.length > 0) { + lines.push(line); + line = []; } } - } - // test poly - function tP(poly, face) { - let bounds = poly.bounds; - let xa = base.util.lerp(bounds.minx + s4, bounds.maxx - s4, s2, true); - let ya = base.util.lerp(bounds.miny + s4, bounds.maxy - s4, s2, true); - for (let x of xa) { - for (let y of ya) { - if (base.newPoint(x, y, 0).isInPolygon(poly)) { - let z = base.util.zInPlane(face[0], face[1], face[2], x, y); - tp(new THREE.Vector3(x, y, z)); - } + }; + + // use specified fill type + if (type && fill[type]) { + fill[type](target); + } else { + console.log({missing_infill: type}); + return; + } + + // force emit of last line + target.newline(); + + // prepare top infill structure + for (let top of tops) { + top.fill_sparse = top.fill_sparse || []; + polys.appendAll(top.fill_off); + polys.appendAll(top.solids); + } + + // update fill fingerprint for this slice + slice._fill_finger = POLY.fingerprint(polys); + + let skippable = cache && fill_fixed[type] ? true : false; + let miss = false; + // if the layer below has the same fingerprint, + // we may be able to clone the infill instead of regenerating it + if (skippable && slice.fingerprintSame(down)) { + // the fill fingerprint can slightly different because of solid projections + if (down._fill_finger && POLY.fingerprintCompare(slice._fill_finger, down._fill_finger)) { + for (let i=0; i 0.5) { - let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2); - add.push({from: point, to: int[0].point, mid}); - point.added = true; - } - } - let filter = isBelt ? (norm) => { - return norm.z <= thresh && norm.y < 0; - } : (norm) => { - return norm.z < thresh; - }; - let { position } = geo.attributes; - let { itemSize, count, array } = position; - let v3cache = new Vector3Cache(); - let coplane = new Coplanars(); - for (let i = 0; i base.newPoint(v.x, v.y, v.z))); - if (poly.area() < min && poly.perimeter() < size) { - continue; - } - // skip faces on bed - if (a.z + b.z + c.z < 0.01) { - continue; - } - // match with other attached, coplanar faces - coplane.put(a, b, c, norm.z); - } - let groups = coplane.group(true); - // console.log({v3cache, coplane, groups}); - // let ptotl = Object.values(groups).flat().flat().length; - // console.log({ptotl}); - // let pdone = 0; - for (let group of Object.values(groups)) { - for (let polys of group) { - for (let poly of polys) { - if (poly.area() >= process.sliceSupportArea) - tP(poly, polys.face); - // console.log(++pdone / ptotl); - } - } - } - - widget.supports = add; - return add.length > 0; - }; - - class Vector3Cache { - constructor() { - this.cache = {}; - } - - get(x, y, z) { - let key = [x.round(4),y.round(4),z.round(4)].join(','); - let val = this.cache[key]; - if (!val) { - val = new THREE.Vector3(x, y, z); - this.cache[key] = val; - } - return val; } } - class Coplanars { - constructor() { - this.cache = {}; - } + let sparse_clip = slice.isSparseFill; - put(a, b, c, norm) { - let key = norm.round(7).toString(); - let arr = this.cache[key]; - if (!arr) { - arr = []; - this.cache[key] = arr; + // solid fill areas + if (solids.length) { + for (let top of tops) { + if (!top.fill_off) return; + let masks = top.fill_off.slice(); + if (top.solids) { + masks = POLY.subtract(masks, top.solids, [], null, slice.z); } - arr.push([a,b,c]); + let angl = process.sliceFillAngle * ((slice.index % 2) + 1); + for (let solid of solids) { + let inter = [], + fillable = []; + for (let mask of masks) { + let p = solid.mask(mask); + if (p && p.length) inter.appendAll(p); + } + // offset fill area to accommodate trace + if (inter.length) { + POLY.expand(inter, -options.lineWidth/2, slice.z, fillable); + } + // fill intersected areas + if (inter.length) { + slice.isSparseFill = true; + for (let p of inter) { + p.forEachSegment((p1, p2) => { + top.fill_lines.push(p1, p2); + }); + } + } + if (fillable.length) { + let lines = POLY.fillArea(fillable, angl, options.lineWidth); + top.fill_lines.appendAll(lines); + } + } + } + } + + // if only solids were added and no lines to clip + if (!sparse_clip) { + return; + } + + if (options.promises) { + options.promises.push(kiri.minions.clip(slice, polys, lines)); + return; + } + + lines = lines.map(a => a.map(p => p.toClipper())); + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + for (let node of ctre.m_AllPolys) { + poly = POLY.fromClipperNode(node, slice.z); + for (let top of tops) { + // use only polygons inside this top + if (poly.isInside(top.poly)) { + top.fill_sparse.push(poly); + } + } + } + } +}; + +/** + * Find difference between fill inset poly on two adjacent layers. + * Used to calculate bridges, flats and then solid projections. + * 'expand' is used for top offsets in SLA mode + */ +function doDiff(slice, minArea, options = {}) { + const { sla, fakedown } = options; + if (slice.index === 0 && !fakedown) { + return; + } + const top = slice, + down = slice.down || (fakedown ? newSlice(-1) : null), + topInner = sla ? top.topPolys() : top.topInners(), + downInner = sla ? down.topPolys() : down.topInners(), + bridges = top.bridges = [], + flats = down.flats = []; + + // skip diffing layers that are identical + if (slice.fingerprintSame(down)) { + top.bridges = bridges; + down.flats = flats; + return; + } + + POLY.subtract(topInner, downInner, bridges, flats, slice.z, minArea, { + wasm: true + }); +}; + +/** + * + * + * @param {Polygon[]} polys + */ +function addSolidFills(slice, polys) { + if (slice.solids) { + slice.solids.appendAll(polys); + } else if (polys && polys.length) { + console.log({no_solids_in: slice, for: polys}) + } +}; + +/** + * project bottom flats down + */ +function projectFlats(slice, count) { + if (slice.isSolidLayer || !slice.down || !slice.flats) return; + projectSolid(slice, slice.flats, count, false, true); +}; + +/** + * project top bridges up + */ +function projectBridges(slice, count) { + if (slice.isSolidLayer || !slice.up || !slice.bridges) return; + projectSolid(slice, slice.bridges, count, true, true); +}; + +/** + * fill projected areas and store line data + * @return {boolean} true if filled, false if not + */ +function doSolidsFill(slice, spacing, angle, minArea, fillQ) { + let minarea = minArea || 1, + tops = slice.tops, + solids = slice.solids; + + if (!(tops && solids)) { + return; + } + + let unioned = POLY.union(solids, undefined, true, { wasm: true }).flat(), + isSLA = (spacing === undefined && angle === undefined); + + if (solids.length === 0) return false; + if (unioned.length === 0) return false; + + let trims = [], + inner = isSLA ? slice.topPolys() : slice.topFillOff(); + + // trim each solid to the inner bounds + for (let p of unioned) { + p.setZ(slice.z); + for (let i of inner) { + let masks = p.mask(i); + if (masks && masks.length > 0) { + trims.appendAll(masks); + } + } + } + + // clear old solids and make array for new + tops.forEach(top => { top.solids = [] }); + + // replace solids with merged and trimmed solids + slice.solids = solids = trims; + + // parent each solid polygon inside the smallest bounding top + for (let solid of solids) { + for (let top of tops) { + if (top.poly.overlaps(solid)) { + if (!solid.parent || solid.parent.area() > top.poly.area()) { + if (solid.areaDeep() < minarea) { + // console.log({i:slice.index,cull_solid:solid,area:solid.areaDeep()}); + continue; + } + solid.parent = top.poly; + top.solids.push(solid); + } + } + } + } + + // for SLA to bypass line infill + if (isSLA) { + return true; + } + + // create empty filled line array for each top + for (let top of tops) { + // synth belt anchor tops don't want fill + if (!top.fill_lines) { + continue; + } + const tofill = []; + const angfill = []; + const newfill = top.fill_lines = []; + // determine fill orientation from top + for (let solid of solids) { + if (solid.parent === top.poly) { + if (solid.fillang) { + angfill.push(solid); + } else { + tofill.push(solid); + } + } + } + if (tofill.length > 0) { + doFillArea(fillQ, tofill, angle, spacing, newfill); + // top.fill_lines_norm = {angle:angle,spacing:spacing}; + } + if (angfill.length > 0) { + top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; + for (let af of angfill) { + doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); + // top.fill_lines_ang.list.push(af.fillang.angle + 45); + // top.fill_lines_ang.poly.push(af.clone()); + } + } + } +} + +function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { + if (fillQ) { + fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); + } else { + POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); + } +} + +/** + * calculate external overhangs requiring support + */ +async function doSupport(slice, proc, shadow, opt = {}) { + let maxBridge = proc.sliceSupportSpan || 5, + minArea = proc.supportMinArea || 0.1, + pillarSize = proc.sliceSupportSize, + offset = proc.sliceSupportOffset || 0, + gap = proc.sliceSupportGap, + size = (pillarSize || 1), + tops = slice.topPolys(), + trimTo = tops; + + let traces = POLY.flatten(slice.topShells().clone(true)), + fill = slice.topFill(), + points = [], + down = slice.down, + down_tops = down ? down.topPolys() : null, + down_traces = down ? POLY.flatten(down.topShells().clone(true)) : null; + + if (opt.exp && down_tops) { + let points = down_tops.map(p => p.deepLength).reduce((a,v)=>a+v); + if (points > 200) { + // use de-rez'd top shadow instead + down_tops = down.topSimples(); + // de-rez trace polys because it's not that important for supports + down_traces = down_traces.map(p => p.clean(true, undefined, config.clipper / 10)); + } + } + + // DEBUG code + let SDBG = false; + let cks = SDBG ? [] : undefined; + let pip = SDBG ? [] : undefined; + let pcl = SDBG ? [] : undefined; + + // check if point is supported by layer below + function checkPointSupport(point) { + if (SDBG) cks.push(point); // DEBUG + // skip points close to other support points + for (let i=0; i= maxBridge) { + let slope = p1.slopeTo(p2).factor(1/dist), + segs = Math.floor(dist / maxBridge) + 1, + seglen = dist / segs; + while (i < segs) { + checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); + } + } + if (poly) checkPointSupport(p2); + } + + let supports = []; + + // generate support polys from unsupported points + if (slice.down) (function() { + // check trace line support needs + traces.forEach(function(trace) { + trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); + }); + + // add offset solids to supports (or fill depending) + fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); + + // skip the rest if no points or supports + if (!(points.length || supports.length)) return; + + let pillars = []; + + // for each point, create a bounding rectangle + points.forEach(function(point) { + pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); + }); + + supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); + // merge pillars and replace with convex hull of outer points (aka smoothing) + pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { + supports.push(base.newPolygon().createConvexHull(pillar.points)); + }); + })(); + + // DEBUG code + if (SDBG && down_traces) slice.output() + .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) + .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) + .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) + .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) + .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) + .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) + .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) + .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) + .setLayer('dtr', { line: 0x0, check: 0x0 }) + .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); + ; + + if (supports.length === 0) { + return; + } + + // then union supports + if (supports.length > 10) { + supports = await kiri.minions.union(supports); + } else { + supports = POLY.union(supports, null, true, { wasm: false }); + } + + // clip to top polys + supports = POLY.trimTo(supports, shadow); + + let depth = 0; + while (down && supports.length > 0) { + down.supports = down.supports || []; + + let trimmed = [], culled = []; + + // culled = supports; + // clip supports to shell offsets + POLY.subtract(supports, down.topSimples(), trimmed, null, slice.z, minArea, { wasm: false }); + + // set depth hint on support polys for infill density + trimmed.forEach(function(trim) { + if (trim.area() < minArea) return; + culled.push(trim.setZ(down.z)); + }); + + // exit when no more support polys exist + if (culled.length === 0) break; + + // new bridge polys for next pass (skip first layer below) + if (depth >= gap) { + down.supports.appendAll(culled); } - group(union) { - let out = {}; - for (let norm in this.cache) { - let arr = this.cache[norm]; - let groups = []; - for (let face of arr) { - let match = undefined; - // see if face matches vertices in any group - outer: for (let group of groups) { - for (let el of group) { - if ( - el.indexOf(face[0]) >= 0 || - el.indexOf(face[1]) >= 0 || - el.indexOf(face[2]) >= 0 - ) { - match = group; - break outer; - } + supports = culled; + down = down.down; + depth++; + } + +} + +function doSupportFill(promises, slice, linewidth, density, minArea, isBelt) { + let supports = slice.supports, + nsB = [], + nsC = [], + min = minArea || 0.1; + + if (!supports) return; + + // union supports + supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); + + // clip supports to slice clip offset (or shell if none) + POLY.subtract(supports, slice.clips, nsB, null, slice.z, min, { wasm: false }); + supports = nsB; + + // also trim to lower offsets, if they exist + if (slice.down && slice.down.clips) { + POLY.subtract(nsB, slice.down.clips, nsC, null, slice.z, min, { wasm: false }); + supports = nsC; + } + + if (supports) { + fillSupportPolys(promises, supports, linewidth, density, slice.z, isBelt); + } + + // re-assign new supports back to slice + slice.supports = supports; +}; + +function fillSupportPolys(promises, polys, linewidth, density, z, isBelt) { + // calculate fill density + let spacing = linewidth * (1 / density); + polys.forEach(function (poly) { + // angle based on width/height ratio + let angle = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? 90 : 0; + // inset support poly for fill lines 33% of nozzle width + let inset = POLY.offset([poly], -linewidth/3, {flat: true, z, wasm: true}); + // do the fill + if (inset && inset.length > 0) { + doFillArea(promises, inset, angle, spacing, poly.fill = []); + } + return true; + }); +} + +/** + * + * @param {Slice} slice + * @param {Polygon[]} polys + * @param {number} count + * @param {boolean} up + * @param {boolean} first + * @returns {*} + */ +function projectSolid(slice, polys, count, up, first) { + if (!slice || slice.isSolidLayer || count <= 0) { + return; + } + let clones = polys.clone(true); + if (first) { + clones.forEach(function(p) { + p.hintFillAngle(); + }); + } + addSolidFills(slice, clones); + if (count > 0) { + if (up) projectSolid(slice.up, polys, count-1, true, false); + else projectSolid(slice.down, polys, count-1, false, false); + } +} + +/** + * given an array of arrays of points (lines), eliminate intersections + * between groups, then return a unified array of shortest non-intersects. + * + * @returns {Point[]} + */ +function cullIntersections() { + function toLines(pts) { + let lns = []; + for (let i=0, il=pts.length; i t); + if (aOa.length < 1) return; + let aa = toLines(aOa.shift()); + while (aOa.length) { + let bb = toLines(aOa.shift()); + loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); + } + let good = []; + for (let i=0, il=aa.length; i 2 ? good : []; +} + +FDM.supports = function(settings, widget) { + let isBelt = settings.device.bedBelt; + let process = settings.process; + let size = process.sliceSupportSize; + let s4 = size / 4; + let s2 = size * 0.45; + let min = 0.01; + let geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); + let mat = new THREE.MeshBasicMaterial(); + let rad = (Math.PI / 180); + let deg = (180 / Math.PI); + let angle = rad * settings.process.sliceSupportAngle; + let thresh = -Math.sin(angle); + let dir = new THREE.Vector3(0,0,-1) + let add = []; + let mesh = new THREE.Mesh(geo, mat); + let platform = new THREE.Mesh( + new THREE.PlaneGeometry(1000,1000,1), mat + ); + function pointIn(x, y, p1, p2, p3) { + let det = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) + return det * ((p2.x - p1.x) * (y - p1.y) - (p2.y - p1.y) * (x - p1.x)) > 0 && + det * ((p3.x - p2.x) * (y - p2.y) - (p3.y - p2.y) * (x - p2.x)) > 0 && + det * ((p1.x - p3.x) * (y - p3.y) - (p1.y - p3.y) * (x - p3.x)) > 0 + } + // first, last, distance + function fld(arr, key) { + let first = arr[0]; + let last = arr.last(); + let dist = last[key] - first[key]; + return { first, last, dist } + } + // sorted range distance from key + function rdist(range, key) { + return range.last[key] - range.first[key]; + } + // test area + function ta(p1, p2, p3) { + let sortx = [p1,p2,p3].sort((a,b) => { return a.x - b.x }); + let sorty = [p1,p2,p3].sort((a,b) => { return a.y - b.y }); + let sortz = [p1,p2,p3].sort((a,b) => { return a.z - b.z }); + let xv = fld(sortx, 'x'); + let yv = fld(sorty, 'y'); + let xa = base.util.lerp(xv.first.x + s4, xv.last.x - s4, s2, true); + let ya = base.util.lerp(yv.first.y + s4, yv.last.y - s4, s2, true); + for (let x of xa) { + for (let y of ya) { + if (pointIn(x, y, p1, p2, p3)) { + let z = base.util.zInPlane(p1, p2, p3, x, y); + tp(new THREE.Vector3(x, y, z)); + } + } + } + } + // test poly + function tP(poly, face) { + let bounds = poly.bounds; + let xa = base.util.lerp(bounds.minx + s4, bounds.maxx - s4, s2, true); + let ya = base.util.lerp(bounds.miny + s4, bounds.maxy - s4, s2, true); + for (let x of xa) { + for (let y of ya) { + if (base.newPoint(x, y, 0).isInPolygon(poly)) { + let z = base.util.zInPlane(face[0], face[1], face[2], x, y); + tp(new THREE.Vector3(x, y, z)); + } + } + } + } + // test point + function tp(point) { + if (point.added) { + return; + } + // omit pillars close to existing pillars + for (let added of add) { + let p2 = new THREE.Vector2(point.x, point.y); + let pm = new THREE.Vector2(added.mid.x, added.mid.y); + if (Math.abs(point.z - added.from.z) < s2 && p2.distanceTo(pm) < s4) { + return; + } + } + let ray = new THREE.Raycaster(point, dir); + let int = ray.intersectObjects([ mesh, platform ], false); + if (int && int.length && int[0].distance > 0.5) { + let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2); + add.push({from: point, to: int[0].point, mid}); + point.added = true; + } + } + let filter = isBelt ? (norm) => { + return norm.z <= thresh && norm.y < 0; + } : (norm) => { + return norm.z < thresh; + }; + let { position } = geo.attributes; + let { itemSize, count, array } = position; + let v3cache = new Vector3Cache(); + let coplane = new Coplanars(); + for (let i = 0; i base.newPoint(v.x, v.y, v.z))); + if (poly.area() < min && poly.perimeter() < size) { + continue; + } + // skip faces on bed + if (a.z + b.z + c.z < 0.01) { + continue; + } + // match with other attached, coplanar faces + coplane.put(a, b, c, norm.z); + } + let groups = coplane.group(true); + // console.log({v3cache, coplane, groups}); + // let ptotl = Object.values(groups).flat().flat().length; + // console.log({ptotl}); + // let pdone = 0; + for (let group of Object.values(groups)) { + for (let polys of group) { + for (let poly of polys) { + if (poly.area() >= process.sliceSupportArea) + tP(poly, polys.face); + // console.log(++pdone / ptotl); + } + } + } + + widget.supports = add; + return add.length > 0; +}; + +class Vector3Cache { + constructor() { + this.cache = {}; + } + + get(x, y, z) { + let key = [x.round(4),y.round(4),z.round(4)].join(','); + let val = this.cache[key]; + if (!val) { + val = new THREE.Vector3(x, y, z); + this.cache[key] = val; + } + return val; + } +} + +class Coplanars { + constructor() { + this.cache = {}; + } + + put(a, b, c, norm) { + let key = norm.round(7).toString(); + let arr = this.cache[key]; + if (!arr) { + arr = []; + this.cache[key] = arr; + } + arr.push([a,b,c]); + } + + group(union) { + let out = {}; + for (let norm in this.cache) { + let arr = this.cache[norm]; + let groups = []; + for (let face of arr) { + let match = undefined; + // see if face matches vertices in any group + outer: for (let group of groups) { + for (let el of group) { + if ( + el.indexOf(face[0]) >= 0 || + el.indexOf(face[1]) >= 0 || + el.indexOf(face[2]) >= 0 + ) { + match = group; + break outer; } } - if (match) { - match.push(face); - } else { - groups.push([face]); - } } - if (union) { - // convert groups of faces to contiguous polygon groups - groups = groups.map(group => { - let parr = group.map(arr => { - return base.newPolygon() - .add(arr[0].x, arr[0].y, arr[0].z) - .add(arr[1].x, arr[1].y, arr[1].z) - .add(arr[2].x, arr[2].y, arr[2].z); - }); - let union = parr.length === 1 ? parr : - POLY.union(parr, 0, true, {wasm:false}); - union.merged = parr.length; - union.face = group[0]; - return union; - }); + if (match) { + match.push(face); + } else { + groups.push([face]); } - out[norm] = groups; } - // console.log(out); - return out; + if (union) { + // convert groups of faces to contiguous polygon groups + groups = groups.map(group => { + let parr = group.map(arr => { + return base.newPolygon() + .add(arr[0].x, arr[0].y, arr[0].z) + .add(arr[1].x, arr[1].y, arr[1].z) + .add(arr[2].x, arr[2].y, arr[2].z); + }); + let union = parr.length === 1 ? parr : + POLY.union(parr, 0, true, {wasm:false}); + union.merged = parr.length; + union.face = group[0]; + return union; + }); + } + out[norm] = groups; } + // console.log(out); + return out; } +} })(); diff --git a/src/kiri/consts.js b/src/kiri/consts.js index 7a5c2d37..6b36c070 100644 --- a/src/kiri/consts.js +++ b/src/kiri/consts.js @@ -131,6 +131,7 @@ kiri.consts = { MODES, VIEWS, SEED, + beltfact: Math.cos(Math.PI / 4) }; })(); diff --git a/src/kiri/main.js b/src/kiri/main.js index aa1ac760..053412c8 100644 --- a/src/kiri/main.js +++ b/src/kiri/main.js @@ -10,7 +10,7 @@ const { feature, platform, selection, settings } = api; const { COLOR, MODES, PMODES, VIEWS } = consts; - const LANG = lang.current, + const LANG = lang.current, WIN = self.window, DOC = self.document, LOC = self.location, diff --git a/src/kiri/print.js b/src/kiri/print.js index 2d1c3700..ce7bb9ef 100644 --- a/src/kiri/print.js +++ b/src/kiri/print.js @@ -7,6 +7,8 @@ const { base, kiri } = self; const { paths, util, newPoint, Polygon } = base; const { tip2tipEmit } = paths; +const { numOrDefault } = util; +const { beltfact } = kiri.consts; kiri.newPrint = function(settings, widgets, id) { return new Print(settings, widgets, id); @@ -70,7 +72,7 @@ class Print { const { process } = settings; let shortDist = process.outputShortDistance, - shellMult = pref(options.extrude, process.outputShellMult), + shellMult = numOrDefault(options.extrude, process.outputShellMult), printSpeed = options.rate || process.outputFeedrate, moveSpeed = process.outputSeekrate, minSpeed = process.outputMinSpeed, @@ -169,638 +171,6 @@ class Print { } } - // fdm only - slicePrintPath(slice, startPoint, offset, output, opt = {}) { - const scope = this; - const { settings } = scope; - const { device } = settings; - - // console.log({slicePrintPath: slice.index, ext:slice.extruder}); - let i, - preout = [], - process = opt.params || settings.process, - extruder = slice.extruder || 0, - nozzleSize = device.extruders[extruder].extNozzle, - firstLayer = opt.first || false, - thinWall = nozzleSize * (opt.thinWall || 1.75), - retractDist = opt.retractOver || 2, - solidWidth = process.sliceFillWidth || 1, - fillMult = opt.mult || process.outputFillMult, - shellMult = opt.mult || process.outputShellMult || (process.laserSliceHeight >= 0 ? 1 : 0), - shellOrder = {"out-in":-1,"in-out":1}[process.sliceShellOrder] || -1, - sparseMult = process.outputSparseMult, - coastDist = process.outputCoastDist || 0, - finishSpeed = opt.speed || process.outputFinishrate, - firstShellSpeed = process.firstLayerRate, - firstFillSpeed = process.firstLayerFillRate, - firstPrintMult = process.firstLayerPrintMult, - printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), - fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), - infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, - moveSpeed = process.outputSeekrate, - origin = startPoint.add(offset), - zhop = process.zHopDistance || 0, - antiBacklash = process.antiBacklash, - wipeDist = process.outputRetractWipe || 0, - isBelt = device.bedBelt, - beltFirst = process.outputBeltFirst || false, - startClone = startPoint.clone(), - seedPoint = opt.seedPoint || startPoint, - z = slice.z, - lastPoly; - - // apply first layer extrusion multipliers - if (firstLayer) { - fillMult *= firstPrintMult; - shellMult *= firstPrintMult; - sparseMult *= firstPrintMult; - } - - function retract() { - let array = preout.length ? preout : output; - if (array.length) { - let last = array.last(); - last.retract = true; - if (wipeDist && lastPoly && last.point) { - let endpoint = last.point.followTo(lastPoly.center(true), wipeDist); - if (endpoint.inPolygon(lastPoly)) { - scope.addOutput(array, endpoint); - } - } - } else if (opt.pretract) { - opt.pretract(wipeDist); - } else { - console.log('unable to retract. no preout or output'); - } - } - - function intersectsTop(p1, p2) { - if (slice.index < 0) { - return false; - } - let int = false; - slice.topPolysFlat().forEach((poly) => { - if (!int) poly.forEachSegment((s1, s2) => { - if (util.intersect(p1,p2,s1,s2,base.key.SEGINT)) { - return int = true; - } - }); - }); - // if intersecting, look for a route around - if (int && opt.routeAround) { - return !routeAround(p1, p2); - } - return int; - } - - // returns true if routed around or no retract requried - function routeAround(p1, p2) { - const dbug = false; - if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)}); - - let ints = []; - let tops = slice.topRouteFlat(); - for (let poly of tops) { - poly.forEachSegment((s1, s2) => { - let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT); - if (ip) { - ints.push({ip, poly}); - } - }); - } - - // no intersections - if (ints.length === 0) { - if (dbug === slice.index) console.log(slice.index, 'no ints'); - return false; - } - - // odd # of intersections ?!? do retraction - if (ints.length && ints.length % 2 !== 0) { - if (dbug === slice.index) console.log(slice.index, {odd_intersects: ints}); - return false; - } - - // sort by distance - ints.sort((a, b) => { - return a.ip.dist - b.ip.dist; - }); - - if (dbug === slice.index) console.log(slice.index, {ints}); - - // check pairs. eliminate too close points. - // pairs must intersect same poly or retract. - for (let i=0; i i); - - if (ints.length > 2) { - if (dbug === slice.index) console.log(slice.index, {complex_route: ints.length}); - return false; - } - - if (ints.length === 2) { - // can route around intersected top polys - for (let i=0; ii.ip.dist), - i1, i2, same: i1.poly === i2.poly, - route, - p1, p2, dist: p1.distTo2D(p2), - r1, r1d, r1s, r1e, - r2, r2d, r2s, r2e, - isCW}); - - for (let p of route) { - scope.addOutput(preout, p, 0, moveSpeed, extruder); - } - - // output last point - scope.addOutput(preout, i2.ip, 0, moveSpeed, extruder); - } - return true; - } - - return false; - } - - function outputTraces(poly, opt = {}) { - if (!poly) return; - if (Array.isArray(poly)) { - if (opt.sort) { - let polys = poly.slice().sort((a,b) => { - return (a.perimeter() - b.perimeter()) * opt.sort; - }); - let debug = polys.length > 3; - let last; - while (polys.length) { - let next; - for (let p of polys) { - if (!last) { - next = p; - break; - } - if (opt.sort > 0) { - // in-out - if (last.isInside(p)) { - next = p; - break; - } - } else { - // out-in - if (p.isInside(last)) { - next = p; - break; - } - } - } - if (next) { - last = next; - polys.remove(next); - outputTraces(next, opt); - } else { - last = null; - } - } - } else { - outputOrderClosest(poly, function(next) { - outputTraces(next, opt); - }, null); - } - } else { - let finishShell = poly.depth === 0 && !firstLayer; - startPoint = scope.polyPrintPath(poly, startPoint, preout, { - tool: extruder, - rate: finishShell ? finishSpeed : printSpeed, - accel: finishShell, - wipe: process.outputWipeDistance || 0, - coast: firstLayer ? 0 : coastDist, - extrude: pref(opt.extrude, shellMult), - onfirst: function(firstPoint) { - let from = seedPoint || startPoint; - if (from.distTo2D(firstPoint) > retractDist) { - if (intersectsTop(from, firstPoint)) { - retract(); - } - } - seedPoint = null; - } - }); - lastPoly = slice.lastPoly = poly; - } - } - - /** - * @param {Polygon[]} polys - */ - function outputSparse(polys, extrude, speed) { - if (!polys) return; - let proxy = polys.map((poly) => { - return {poly: poly, first: poly.first(), last: poly.last()}; - }); - let lp = startPoint; - startPoint = tip2tipEmit(proxy, startPoint, (el, point, count) => { - let poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint((p, i) => { - let dist = lp.distTo2D(p); - let rdst = dist > retractDist; - let itop = rdst && intersectsTop(lp,p); - let emit = extrude; - // retract if dist trigger and crosses a slice top polygon - if (i === 0) { - if (itop) { - retract(); - emit = 0; - } else if (dist > nozzleSize) { - emit = 0; - } - } - // let emit = i === 0 ? 0 : extrude; - // handle shallow cloned infill - if (poly.z !== undefined) { - p = p.clone().setZ(poly.z); - } - scope.addOutput(preout, p, emit, speed || printSpeed, extruder); - lp = p; - }, !poly.open); - return lp; - }); - } - - function outputThin(lines) { - if (!lines) { - return; - } - let points = lines.group(2).map(grp => { - let [ p1, p2 ] = grp; - return newPoint( - (p1.x + p2.x) / 2, - (p1.y + p2.y) / 2, - (p1.z + p2.z) / 2 - ) - }); - let order = util.orderClosest(points, (p1, p2) => p1.distTo2D(p2)); - if (order.length === 0) { - return; - } - let first = points[0]; - let last = first; - scope.addOutput(preout, first, 0, moveSpeed, extruder); - for (let p of order) { - let dist = last ? last.distTo2D(p) : 0; - if (dist > thinWall) { - retract(); - scope.addOutput(preout, p, 0, moveSpeed, extruder); - } - scope.addOutput(preout, p, 1, fillSpeed, extruder); - last = p; - } - // close a circle - if (last && last.distTo2D(first) <= thinWall) { - scope.addOutput(preout, first, 1, fillSpeed, extruder); - } - } - - function outputFills(lines, opt = {}) { - if (!lines || lines.length === 0) { - return; - } - let p, p1, p2, dist, len, found, group, mindist, t1, t2, - marked = 0, - start = 0, - skip = false, - lastIndex = -1, - flow = opt.flow || 1, - near = opt.near || false, - fast = opt.fast || false, - fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow, - thinDist = near ? thinWall : thinWall; - - while (lines && marked < lines.length) { - group = null; - found = false; - mindist = Infinity; - - // use next nearest line strategy - if (near) - for (i=0; i lastIndex) { - group = p.index; - } - if (group !== null) { - if (p.index !== group) { - break; - } - if (p.index % 2 === 0) { - t1 = lines[i]; - t2 = lines[i+1]; - } else { - t2 = lines[i]; - t1 = lines[i+1]; - } - dist = Math.min(t1.distTo2D(startPoint), t2.distTo2D(startPoint)); - if (dist < mindist) { - p1 = t1; - p2 = t2; - mindist = dist; - } - start = i; - found = true; - } - } - - // go back to start and try again - if (!near && !found) { - if (start === 0 && lastIndex === -1) { - console.log('infinite loop', lines, { - marked, i, group, start, lastIndex, - points: lines.map(p => p.index).join(', ') - }); - break; - } - start = 0; - lastIndex = -1; - continue; - } - - dist = startPoint.distToSq2D(p1); - len = p1.distToSq2D(p2); - - // go back to start when dist > retractDist - if (!near && !fast && !skip && dist > retractDist) { - skip = true; - start = 0; - lastIndex = -1; - continue; - } - skip = false; - - // mark as used (temporarily) - p1.del = true; - p2.del = true; - marked += 2; - lastIndex = p1.index; - - // if dist to new segment is less than thinWall - // and segment length is less than thinWall then - // just extrude to midpoint of next segment. this is - // to avoid shaking the printer to death. - if (dist <= thinDist && len <= thinDist) { - p2 = p1.midPointTo(p2); - // this.addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder); - scope.addOutput(preout, p2, fill, fillSpeed, extruder); - } else { - // retract if dist trigger or crosses a slice top polygon - if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) { - retract(); - } - - // anti-backlash on longer move - if (!fast && antiBacklash && dist > retractDist) { - scope.addOutput(preout, p1.add({x:antiBacklash,y:-antiBacklash,z:0}), 0, moveSpeed, extruder); - } - - // bridge ends of fill when they're close together - if (dist < thinDist) { - scope.addOutput(preout, p1, fill, fillSpeed, extruder); - } else { - scope.addOutput(preout, p1, 0, moveSpeed, extruder); - } - - scope.addOutput(preout, p2, fill, fillSpeed, extruder); - } - - startPoint = p2; - } - - // clear delete marks so we can re-print later - if (lines) lines.forEach(p => { p.del = false }); - } - - /** - * given array of polygons, emit them in next closest order with - * the special exception that depth is considered into distance - * so that inner polygons are emitted first. - * - * @param {Array} array of Polygons or Polygon wrappers (tops) - * @param {Function} fn call to emit next candidate - * @param {Function} fnp convert 'next' object into a Polygon for closeness - */ - function outputOrderClosest(array, fn, fnp) { - if (array.length === 1) { - return fn(array[0]); - } - array = array.slice(); - let closest, find, next, order, poly, lastDepth = 0; - for (;;) { - order = []; - closest = null; - for (i=0; i { - return a.d - b.d; - }); - array[order[0].i] = null; - fn(order[0].n); - } - } - - let out = []; - if (slice.tops) { - out.appendAll(slice.tops); - }; - if (opt.support && slice.supports) { - out.appendAll(slice.supports); - } - - let lastTop = null; - outputOrderClosest(out, function(next) { - if (next instanceof Polygon) { - scope.setType('support'); - // support polygon - next.setZ(z); - outputTraces([next].appendAll(next.inner || [])); - if (next.fill) { - next.fill.forEach(p => { p.z = z }); - outputFills(next.fill, {fast: true}); - } - } else { - scope.setType('shells'); - if (lastTop && lastTop !== next) { - retract(); - } - - // control of layer start point - switch (process.sliceLayerStart) { - case "center": - startPoint = newPoint(0,0,startPoint.z); - break; - case "origin": - startPoint = origin.clone(); - break; - } - - // optimize start point on belt for tops touching belt - // and enforce optimal shell order (outer first) - if (isBelt && opt.onBelt) { - startPoint = startClone; - if (beltFirst) { - shellOrder = -1; - } - } - - // innermost shells - let inner = next.innerShells() || []; - - // output inner polygons - if (shellOrder === 1) outputTraces(inner, { sort: shellOrder }); - - outputTraces(next.shells, { sort: shellOrder }); - - // output outer polygons - if (shellOrder === -1) outputTraces(inner, { sort: shellOrder }); - - // output thin fill - scope.setType('thin fill'); - outputThin(next.thin_fill); - - // then output solid and sparse fill - scope.setType('solid fill'); - outputFills(next.fill_lines, {flow: solidWidth}); - - scope.setType('sparse infill'); - outputSparse(next.fill_sparse, sparseMult, infillSpeed); - - lastTop = next; - } - }, function(obj) { - // for tops - return obj instanceof Polygon ? obj : obj.poly; - }); - - // produce polishing paths when present - if (slice.tops.length && slice.tops[0].polish) { - let {x,y} = slice.tops[0].polish; - if (x) { - outputSparse(x, 0, process.polishSpeed); - } - if (y) { - outputSparse(y, 0, process.polishSpeed); - } - } - - // offset print points - for (i=0; i Object.clone(o))); @@ -1181,10 +550,6 @@ class Output { } } -function pref(a,b) { - return a !== undefined ? a : b; -} - kiri.Print = Print; })();