/** Copyright Stewart Allen -- All Rights Reserved */ import { $ } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; import { conf } from '../../core/conf.js'; import { util } from '../../../geo/base.js'; import { client } from '../../core/client.js'; import { addbox, delbox, getlastbox } from '../../core/boxes.js'; import { Layers } from '../../core/layers.js'; import { Stack } from '../../core/stack.js'; import { Widget, newWidget } from '../../core/widget.js'; import { space } from '../../../moto/space.js'; const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; const { VIEWS } = api.const; const LANG = api.language.current; let lastBox, lastMode, lastView, lastPillar, fromPillar, addingSupports = false, isFdmMode = false, nextID = Date.now(), p1, p2, iw, alert = [], func = {}; export function init() { const { ui } = api; const proc_keys = Object.keys(conf.defaults.fdm.p); const rangeVars = { // slice // "sliceHeight": LANG.sl_lahi_s, "sliceShells": LANG.sl_shel_s, "sliceFillType": LANG.fi_type, "sliceFillWidth": LANG.fi_wdth_s, "sliceFillSparse": LANG.fi_pcnt_s, "sliceFillRate": LANG.ou_feed_s, "sliceFillGrow": LANG.fi_grow_s, "sliceSolidRate": LANG.ou_fini_s, "sliceSolidMinArea": LANG.ad_msol_s, "sliceTopLayers": LANG.sl_ltop_s, "sliceBottomLayers": LANG.sl_lbot_s, "firstLayerRate": LANG.fl_rate_s, "firstLayerPrintMult": LANG.fl_mult_s, "firstLayerFanSpeed": LANG.ou_fans_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, "outputAlternating": LANG.ad_altr_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.ad_rrat_s, "outputRetractDwell": LANG.ad_rdwl_s, "outputLayerRetract": LANG.ad_lret_s }; for (let key of Object.keys(rangeVars)) { if (ui[key]) { ui[key].range = true; } } function filterSynth() { api.widgets.filter((widget) => { if (widget.track.synth) { space.world.remove(widget.mesh); Widget.Groups.remove(widget); } return !widget.track.synth }); } function clearRanges() { api.conf.get().process.ranges = []; ui.fdmRanges.innerHTML = ''; } // re-render ranges menu function updateRanges(ranges = []) { 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.fdmRanges.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("function.animate", (mode) => { if (!(isFdmMode && lastView === VIEWS.PREVIEW)) { return; } let slider = $('top-slider'); slider.style.display = 'flex'; slider.oninput = slider.onchange = (ev) => { api.const.STACKS.setFraction(parseInt(ev.target.value)/1000); api.space.update(); }; $('anim-slider').value = 500; api.const.STACKS.setFraction(0.5); }); api.event.on("mode.set", mode => { isFdmMode = mode === 'FDM'; lastMode = mode; // hide/show fdm mode elements for (let el of [...document.getElementsByClassName('mode-fdm')]) { api.uc.setClass(el, 'hide', !isFdmMode); } updateSupportVisiblity(); }); api.event.on("view.set", view => { $('top-slider').style.display = 'none'; lastView = view; updateSupportVisiblity(); 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; } } } }); api.event.on("range.updates", updateRanges); api.event.on("settings.load", (settings) => { if (settings.mode !== 'FDM') return; restoreSupports(api.widgets.all()); updateRanges(settings.process.ranges); }); api.event.on("settings.saved", (settings) => { updateRanges(settings.process.ranges); }); 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 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)); 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(); updateSupportVisiblity(); // synth support widget for each widget group // iow, create support meshes based on support widget annotation for (let group of 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.mergeGeometries(boxen); let sw = 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; sw.track.support = true; space.world.add(sw.mesh); } }); api.event.on("slice.end", () => { if (!isFdmMode) { return; } }); api.event.on("key.esc", () => { if (isFdmMode) { func.sdone() } }); api.event.on("selection.scale", () => { if (isFdmMode) { clearRanges(); func.sclear(); } }); api.event.on("widget.delete", widget => { if (isFdmMode) { clearRanges(); } }); 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("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]; 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("mouse.hover.up", func.hoverup = (on = {}) => { let { event } = on; if (!isFdmMode) { return; } if (!addingSupports) { return; } delbox('supp'); if (lastPillar) { removeWidgetSupport(lastPillar.widget, lastPillar); return; } lastBox = getlastbox(); if (!(iw && lastBox)) 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 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 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 (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 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 }); } else { lastBox = null; } 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); } let layers = new 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 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 support_generate(ondone) { 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'); return ondone([]); } for (let g of gen) { g.widget = widgets[g.id]; } ondone(gen); }); }; function scheduleSupportSave(widget) { clearTimeout(suptimer); supsave[widget.id] = widget; suptimer = setTimeout(() => { for (let w of Object.values(supsave)) { w.saveState(); } }, 50); } export 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 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 updateSupportVisiblity() { const visible = (lastMode === 'FDM' && lastView === VIEWS.ARRANGE); for (let w of api.widgets.all()) { setSupportVisiblity(w, visible); } } 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; }