grid-apps-cmms/src/kiri/app/function.js

590 lines
18 KiB
JavaScript
Raw Normal View History

2022-02-18 14:56:59 -05:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from './api.js';
2025-12-25 10:35:49 -05:00
import { client } from './workers.js';
2025-12-24 22:38:04 -05:00
import { codec } from '../core/codec.js';
import { space } from '../../moto/space.js';
import { PMODES } from '../core/consts.js';
import { colorSchemeRegistry } from './color/schemes.js';
2025-12-24 22:38:04 -05:00
import { exportFile } from './export.js';
2022-02-18 14:56:59 -05:00
/**
* Get current color scheme
*/
function getColorScheme() {
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
/**
* Tracks completion state of operations to prevent redundant work.
* Properties: slice, preview, export
*/
2022-02-18 14:56:59 -05:00
let complete = {};
let order;
2022-02-18 14:56:59 -05:00
/**
* Prepare and execute slicing for all widgets on the platform.
* Main slicing function that:
* - Takes screenshots for export/preview
* - Handles belt mode layout
* - Slices each widget sequentially via worker
* - Tracks progress across all widgets
* - Renders sliced layers to stacks
* - Emits slice.begin, slice, slice.end, slice.error events
*
* @param {function} [callback] - Called when slicing completes
* @param {number} [scale=1] - Progress bar scale factor (for chaining operations)
* @param {number} [offset=0] - Progress bar offset (for chaining operations)
*/
2022-02-18 14:56:59 -05:00
function prepareSlices(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
2022-02-18 14:56:59 -05:00
if (view.is_arrange()) {
2024-07-06 15:40:58 -04:00
// in arrange mode, create a screenshot at the start slicing
// this can be used later by exports and rendered on some devices
2022-02-18 14:56:59 -05:00
let snap = space.screenshot();
view.snapshot = snap.substring(snap.indexOf(",") + 1);
2024-12-10 11:59:33 -05:00
client.snap(space.screenshot2({ width: 640 }));
let bambu = view.bambu = { };
space.screenshot3({ width: 512, out(png) { bambu.s512 = png } });
space.screenshot3({ width: 128, out(png) { bambu.s128 = png } });
2022-02-18 14:56:59 -05:00
}
if (mode.is_sla() && !callback) {
2024-07-06 15:40:58 -04:00
// in SLA mode, slice and preview are the same thing
2022-02-18 14:56:59 -05:00
callback = preparePreview;
}
order = api.selection.count() ?
Object.assign({}, ...api.selection.widgets().map((w,i) => ({[w.id]:i}))) :
undefined;
2022-02-18 14:56:59 -05:00
const widgets = api.widgets.all();
const settings = conf.get();
const { device, process, controller } = settings;
const isBelt = device.bedBelt || false;
const mark = Date.now();
// force layout in belt mode when widget exceeds bed length
2024-07-06 15:40:58 -04:00
// which has the side-effect (intended) of increasing the bed size
2022-02-18 14:56:59 -05:00
if (widgets.length && isBelt) {
let doLayout = false;
for (let w of widgets) {
let bb = w.getBoundingBox();
let yspan = bb.max.y - bb.min.y;
if (yspan > device.bedDepth) {
doLayout = true;
}
}
if (doLayout) {
platform.layout();
}
}
2022-02-18 18:42:08 -05:00
functions.clear_progress();
2022-02-18 14:56:59 -05:00
platform.deselect();
hide.slider();
view.set_slice();
conf.save();
// allow client.js to generate support widgets
2022-02-18 14:56:59 -05:00
event.emit('slice.begin', settings.mode);
2022-02-19 20:01:10 -05:00
// refresh widget list because 'slice.begin' may generate support widgets
const slicing = api.widgets.all()
2022-02-18 14:56:59 -05:00
.filter(w => !w.track.ignore && !w.meta.disabled);
if (slicing.length === 0) {
return api.show.alert('nothing to slice');
}
2022-02-18 14:56:59 -05:00
const totalv = slicing.map(w => w.getVertices().count).reduce((a,v) => a + v);
const defvert = totalv / slicing.length;
const track = {};
let totalProgress;
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.SLICE.slicing_opacity);
2022-02-18 14:56:59 -05:00
let segtimes = {},
segNumber = 0,
errored = false,
startTime = Date.now(),
toSlice = slicing.slice(),
camOrLaser = mode.is_cam() || mode.is_laser(),
extruders = {},
lastMsg;
for (let widget of toSlice) {
widget.belt = null;
2022-02-18 14:56:59 -05:00
widget.stats.progress = 0;
const scheme = getColorScheme();
widget.setColor(0xffaaaa); // slicing color (from old COLOR.slicing)
let { extruder } = widget.anno;
if (extruder >= 0) {
extruders[extruder] = extruder;
}
2022-02-18 14:56:59 -05:00
}
// in multi-material belt mode, the anchor needs to be extended
// to allow room for the purge tower to be built. calculate here
extruders = Object.values(extruders);
if (isBelt && extruders.length > 1 && process.outputPurgeTower) {
process.beltAnchor = Math.max(
process.firstLayerBeltLead,
Math.sqrt(process.outputPurgeTower) * extruders.length * (1/Math.sqrt(2)));
} else if (isBelt) {
2022-02-18 14:56:59 -05:00
process.beltAnchor = process.firstLayerBeltLead;
}
stacks.clear(); // clear rendered stacks
client.clear(); // clear worker cache
client.sync(); // send fresh widget data to worker
2022-02-18 14:56:59 -05:00
if (isBelt) {
2024-07-06 15:40:58 -04:00
// belt op required to rotate meshes for slicing
client.rotate(settings);
2022-02-18 14:56:59 -05:00
}
function sliceSetup() {
client.slicePre(settings, sliceNext);
}
2022-02-18 14:56:59 -05:00
function sliceNext() {
if (toSlice.length) {
// while more widgets to slice
sliceWidget(toSlice.shift())
} else {
// once all slicing done, run slicePost() once for all widgets
client.slicePost(settings, sliceDone);
2022-02-18 14:56:59 -05:00
}
}
function sliceWidget(widget) {
function onupdate(update, msg, alert) {
if (alert) {
api.show.alert(alert);
}
if (msg && msg !== lastMsg) {
let mark = Date.now();
if (lastMsg) {
let key = slicing.length > 1 ?
2025-12-25 22:27:26 -05:00
`${widget.id} ${segNumber++} ${lastMsg}` :
`${segNumber++} ${lastMsg}`
segtimes[key] = mark - startTime;
}
lastMsg = msg;
startTime = mark;
}
// on update
if (update >= 0) {
track[widget.id] = (update || 0) * factor;
totalProgress = 0;
for (let w of slicing) {
totalProgress += (track[w.id] || 0);
}
show.progress(offset + (totalProgress / slicing.length) * scale, msg);
}
}
function ondone(sliced, error) {
2022-02-18 14:56:59 -05:00
let mark = Date.now();
// update UI info
if (sliced) {
// update segment time
if (lastMsg) {
let key = slicing.length > 1 ?
2025-12-25 22:27:26 -05:00
`${widget.id} ${segNumber++} ${lastMsg}` :
`${segNumber++} ${lastMsg}`
segtimes[`${key}`] = mark - startTime;
2022-02-18 14:56:59 -05:00
}
event.emit('slice', settings.mode);
}
// handle slicing errors
if (error && !errored) {
errored = true;
view.set_arrange();
show.alert(error, 5);
show.progress(0);
client.restart();
event.emit('slice.error', error);
setTimeout(view.set_arrange, 1);
2022-02-18 14:56:59 -05:00
}
if (errored) {
// terminate slicing
sliceDone();
} else {
// start next widget slice
sliceNext();
}
}
// weight each widget progress % by their # vertices
let factor = (widget.getVertices().count / defvert);
widget.settings = settings;
widget.clearSlices();
onupdate(0.0001, "slicing");
// store slicing visuals
widget.stack = api.stacks.create(widget.id, widget.mesh);
// compensate for zcut (widget moved through floor)
widget.stack.obj.view.position.z = widget.track.zcut || 0;
// in case result of slice is nothing, do not preserve previous
widget.slices = []
// executed from kiri.js
client.slice(settings, widget, (reply) => {
if (reply.alert) {
onupdate(null, null, reply.alert);
}
if (reply.update) {
onupdate(reply.update, reply.updateStatus);
2022-02-18 14:56:59 -05:00
}
if (reply.send_start) {
widget.xfer = {start: reply.send_start};
}
if (reply.stats) {
widget.stats = reply.stats;
}
if (reply.send_end) {
widget.stats.load_time = widget.xfer.start - reply.send_end;
}
if (reply.slice) {
widget.slices.push(codec.decode(reply.slice, {mesh:widget.mesh}));
}
if (reply.done) {
ondone(true);
}
if (reply.error) {
ondone(false, reply.error);
2022-02-18 14:56:59 -05:00
}
});
// discard point cache
widget.points = undefined;
2022-02-18 14:56:59 -05:00
}
function sliceDone() {
let alert = null;
if (scale === 1 && feature.work_alerts && slicing.length) {
alert = show.alert("Rendering");
};
for (let widget of slicing) {
// on done
let key = slicing.length > 1 ?
`${widget.id}_${segNumber++} draw` :
`${segNumber++} draw`
segtimes[`${key}`] = widget.render(widget.stack);
// rotate stack for belt beds
if (widget.belt) {
widget.stack.obj.rotate(widget.belt);
2022-02-18 14:56:59 -05:00
}
if (scale === 1) {
// clear wireframe
const scheme = getColorScheme();
widget.setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity);
widget.setOpacity(camOrLaser ? scheme.views.SLICE.sliced_opacity ?? 0.2 : scheme.views.PREVIEW?.sliced_opacity ?? 0.0);
widget.setColor(scheme.widget.deselected);
api.hide.alert(alert);
2022-02-18 14:56:59 -05:00
}
}
view.update_slider_max(true);
show.layer(-1, 0);
if (scale === 1) {
view.update_stack_labels();
}
// if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) {
// $('render-ghost').onclick();
// }
2022-02-18 14:56:59 -05:00
if (scale === 1) {
show.progress(0);
}
// mark slicing complete for prep/preview
complete.slice = true;
event.emit('slice.end', settings.mode);
// print stats
segtimes.total = Date.now() - mark;
console.log(segtimes);
api.visuals.update_stats(segtimes);
2022-02-18 14:56:59 -05:00
if (callback && typeof callback === 'function') {
callback();
}
if (isBelt) {
// required to render supports properly (in belt mode)
// because this updates widget top z which is affected by rotation
api.platform.update_top_z();
}
// refresh / repaint workspace
space.refresh();
2022-02-18 14:56:59 -05:00
}
// do any global setup / state management
// then kick off slicing chain
sliceSetup();
2022-02-18 14:56:59 -05:00
}
/**
* Prepare preview/print visualization.
* Generates toolpaths and renders them as 3D lines.
* Handles multiple preview modes (speed, filament, layer) via feature.pmode.
* Auto-runs slicing first if not already complete.
* Emits preview.begin, print, preview.end, preview.error events.
*
* @param {function} [callback] - Called when preview completes
* @param {number} [scale=1] - Progress bar scale factor (for chaining operations)
* @param {number} [offset=0] - Progress bar offset (for chaining operations)
*/
2022-02-18 14:56:59 -05:00
function preparePreview(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
2022-02-18 14:56:59 -05:00
const widgets = api.widgets.all();
const settings = conf.get();
const { device, process, controller } = settings;
if (complete.preview === feature.pmode) {
if (device.extruders && device.extruders.length > 1) {
2022-02-18 14:56:59 -05:00
if (++feature.pmode > 2) {
feature.pmode = 1;
}
} else {
if (callback) {
callback();
}
return;
}
}
if (!complete.slice) {
settings.render = false;
prepareSlices(() => { preparePreview(callback, 0.25, 0.75) }, 0.75);
return;
}
hide.slider(true);
const isCam = mode.is_cam(), pMode = settings.mode;
const isDark = api.space.is_dark();
2022-02-18 14:56:59 -05:00
const isBelt = device.bedBelt || false;
view.set_preview();
conf.save();
event.emit('preview.begin', pMode);
if (isCam) {
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
api.widgets.setColor(scheme.views.PREVIEW.preview);
2022-02-18 14:56:59 -05:00
} else if (offset === 0) {
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
2022-02-18 14:56:59 -05:00
}
let mark = Date.now(),
segNumber = 0,
segtimes = {},
startTime,
lastMsg,
output = [];
// pass preview mode to worker
settings.pmode = feature.pmode;
settings.render = true;
settings.order = order;
2022-02-18 14:56:59 -05:00
client.prepare(settings, (progress, message, layer) => {
if (layer) {
output.push(codec.decode(layer));
2022-02-18 14:56:59 -05:00
}
if (message && message !== lastMsg) {
const mark = Date.now();
if (lastMsg) {
segtimes[`${segNumber++} ${lastMsg}`] = mark - startTime;
2022-02-18 14:56:59 -05:00
}
lastMsg = message;
startTime = mark;
}
show.progress(offset + progress * scale, message);
}, (reply, maxSpeed, minSpeed) => {
// handle worker errors
if (reply && reply.error) {
show.alert(reply.error, 5);
view.set_arrange();
show.progress(0);
space.update();
event.emit('preview.error', reply.error);
return;
}
if (lastMsg) {
segtimes[`${segNumber++} ${lastMsg}`] = Date.now() - startTime;
2022-02-18 14:56:59 -05:00
}
show.progress(0);
if (!isCam) {
2025-12-12 22:26:58 -05:00
api.widgets.setOpacity(0);
2022-02-18 14:56:59 -05:00
}
if (output.length) {
let alert = feature.work_alerts ? show.alert("Rendering") : null;
startTime = Date.now();
stacks.clear();
const stack = stacks.create('print', space.world)
const view = stack.obj.view;
2022-02-18 14:56:59 -05:00
output.forEach(layer => {
stack.add(layer);
});
event.emit('preview.view', view);
2022-02-18 14:56:59 -05:00
// rotate stack for belt beds
if (isBelt && widgets[0].belt) {
let ri = widgets[0].belt;
2022-02-18 14:56:59 -05:00
ri.dz = 0;
ri.dy = settings.device.bedDepth / 2;
stack.obj.rotate(widgets[0].belt);
2022-02-18 14:56:59 -05:00
}
api.hide.alert(alert);
segtimes[`${segNumber} draw`] = Date.now() - startTime;
2022-02-18 14:56:59 -05:00
}
// print stats
segtimes.total = Date.now() - mark;
console.log(segtimes);
api.visuals.update_stats(segtimes);
2022-02-18 14:56:59 -05:00
event.emit('print', pMode);
event.emit('preview.end', pMode);
space.update();
view.update_slider_max(true);
show.layer(-1, 0);
if (feature.pmode === PMODES.SPEED) {
view.update_speeds(maxSpeed, minSpeed);
} else {
view.update_speeds();
}
2022-02-19 14:13:35 -05:00
view.update_stack_labels();
2022-02-18 14:56:59 -05:00
let { controller, process } = settings;
// if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) {
// $('render-ghost').onclick();
// }
2022-02-18 14:56:59 -05:00
// mark preview complete for export
complete.preview = feature.pmode;
if (typeof(callback) === 'function') {
callback();
}
});
}
/**
* Prepare animation (requires SharedArrayBuffer support).
* Checks for browser support and emits function.animate event.
*/
2022-02-18 14:56:59 -05:00
function prepareAnimation() {
if (!window.SharedArrayBuffer) {
api.alerts.show("The security context of this");
2024-06-13 22:17:20 -04:00
api.alerts.show("window prevents animations.");
api.alerts.show("Try a Chromium-based browser");
return;
}
api.event.emit("function.animate", {mode: api.conf.get().mode});
2022-02-18 14:56:59 -05:00
}
/**
* Prepare and trigger export.
* Auto-runs preview first if not already complete.
* Delegates to exportFile() for mode-specific export.
* @param {...*} args - Arguments passed through to exportFile()
*/
2022-02-18 14:56:59 -05:00
function prepareExport() {
2022-02-19 14:13:35 -05:00
const settings = api.conf.get();
2022-02-18 14:56:59 -05:00
const argsave = arguments;
if (!complete.preview) {
preparePreview(() => { prepareExport(...argsave) });
return;
}
api.event.emit("function.export", {mode: settings.mode});
complete.export = true;
exportFile(...argsave);
2022-02-18 14:56:59 -05:00
}
/**
* Cancel running worker operation by restarting the worker.
*/
2022-02-18 14:56:59 -05:00
function cancelWorker() {
if (client.isBusy()) {
client.restart();
}
}
/**
* Parse and visualize gcode or other toolpath code.
* Sends code to worker for parsing, renders result as 3D preview.
* Emits code.load and code.loaded events.
*
* @param {string} code - Gcode or toolpath text
* @param {string} type - Code type identifier (gcode, etc.)
*/
2022-02-19 14:13:35 -05:00
function parseCode(code, type) {
const { conf, event, show, stacks, widgets, view } = api;
2022-02-19 14:13:35 -05:00
const settings = conf.get();
2022-02-18 14:56:59 -05:00
event.emit("code.load", {code, type});
view.set_preview();
2025-12-12 22:26:58 -05:00
widgets.setOpacity(0);
2022-02-19 14:13:35 -05:00
2022-02-18 14:56:59 -05:00
client.parse({code, type, settings}, progress => {
show.progress(progress, "parsing");
}, (layers, maxSpeed, minSpeed) => {
show.progress(0);
stacks.clear();
const stack = stacks.create('parse', space.world);
layers.forEach(layer => stack.add(layer));
view.update_slider_max(true);
view.update_speeds(maxSpeed, minSpeed);
show.slices();
2022-02-19 14:13:35 -05:00
view.update_stack_labels();
2022-02-18 14:56:59 -05:00
space.update();
event.emit("code.loaded", {code, type});
});
}
/**
* Clear operation completion tracking.
* Resets complete state so operations can run again.
*/
function clear_progress() {
complete = {};
}
2024-05-19 22:31:25 -04:00
// extend API (api.function)
export const functions = {
2022-02-18 14:56:59 -05:00
slice: prepareSlices,
print: preparePreview,
prepare: preparePreview,
animate: prepareAnimation,
export: prepareExport,
cancel: cancelWorker,
2022-02-19 14:13:35 -05:00
parse: parseCode,
2022-02-18 14:56:59 -05:00
clear: client.clear,
clear_progress,
};
export {
prepareSlices as slice,
preparePreview as print,
preparePreview as prepare,
prepareAnimation as animate,
prepareExport as export,
cancelWorker as cancel,
parseCode as parse,
clear_progress,
};