grid-apps-cmms/src2/kiri-mode/fdm/client.js
2025-07-05 00:19:41 -04:00

645 lines
21 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../moto/webui.js';
import { api } from '../../kiri/api.js';
import { conf } from '../../kiri/conf.js';
import { client } from '../../kiri/client.js';
import { addbox, delbox } from '../../kiri/boxes.js';
import { Layers } from '../../kiri/layers.js';
import { Stack } from '../../kiri/stack.js';
import { Widget, newWidget } from '../../kiri/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 => `<div><label class="pad">${rangeVars[a[0]]}</label><span></span><label class="val">${a[1]}</label></div>`).join('');
let hover = `<div id="hov_${id}" class="range-detail">${rows}</div>`;
let info = `<button id="sel_${id}" class="j-center grow">${range.lo} - ${range.hi}</button><button id="del_${id}"><i class="fa fa-trash"></i></button>`;
html.appendAll([
`<div id="rng_${id}" class="range-info">${hover}${info}</div>`
]);
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;
}
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;
}