fold tiny util into api, clean up modal corner cases, add more jsdoc

This commit is contained in:
Stewart Allen 2025-12-25 16:17:44 -05:00
commit bcd2449b89
11 changed files with 339 additions and 90 deletions

View file

@ -82,14 +82,36 @@ function update(clear) {
}
}
/**
* Update progress bar display.
* In debug mode, also shows progress status message.
* @param {number} [value=0] - Progress value (0.0 to 1.0)
* @param {string} [msg] - Optional status message to display
*/
function progress(value = 0, msg) {
value = (value * 100).round(4);
api.ui.progress.width = value+'%';
if (self.debug) {
// console.log(msg, value.round(2));
api.ui.prostatus.style.display = 'flex';
if (msg) {
api.ui.prostatus.innerHTML = msg;
} else {
api.ui.prostatus.innerHTML = '';
}
}
}
export {
hide,
show,
update
update,
progress
};
export default {
hide,
show,
update
update,
progress
};

View file

@ -35,7 +35,6 @@ import { stats } from './stats.js';
import { types as load } from '../../load/file.js';
import { UI } from './inputs.js';
import { updateTool } from '../mode/cam/tools.js';
import { util as utilModule } from './util.js';
import { view as viewModule } from './viewmode.js';
import { visuals } from './visuals.js';
import { widgets } from '../core/widgets.js';
@ -43,11 +42,11 @@ import { workspace } from './workspace.js';
// environment setup
let LOC = self.location,
SETUP = utils.parseOpt(LOC.search.substring(1)),
SECURE = utilModule.isSecure(LOC.protocol),
LOCAL = self.debug && !SETUP.remote,
EVENT = broker,
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri'));
SETUP = utils.parseOpt(LOC.search.substring(1)),
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
LOCAL = self.debug && !SETUP.remote,
SECURE = isSecure(LOC.protocol);
// todo: fix in widget.js b/c front-end and back-end do not share api
self.kiri_catalog = FILES;
@ -61,6 +60,27 @@ EVENT.on = (topic, listener) => {
return EVENT;
};
/**
* Check if protocol is secure (HTTPS).
* @param {string} proto - Protocol string (e.g., "https:", "http:")
* @returns {boolean} True if protocol starts with "https"
*/
function isSecure(proto) {
return proto.toLowerCase().indexOf("https") === 0;
}
/**
* Download data as a file using browser download mechanism.
* Creates temporary object URL and triggers download via hidden link click.
* @param {Blob|ArrayBuffer|string} data - Data to download
* @param {string} filename - Filename for download
*/
function download(data, filename) {
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
$('_dexport_').click();
}
/** Busy state counter for tracking active operations */
let busyVal = 0,
/** Hover feature flag */
@ -68,7 +88,10 @@ let busyVal = 0,
/** Explicit undefined for object defaults */
undef = undefined;
// the big kahuna
/**
* organize modules for easy use in other modules
* without having to know the paths to all of them
*/
export const api = {
ajax,
beta,
@ -132,7 +155,7 @@ export const api = {
emit(t,m,o) { return EVENT.publish(t,m,o) },
bind(t,m,o) { return EVENT.bind(t,m,o) },
alerts(clr) { alerts.update(clr) },
import: utilModule.loadFile,
import() { api.ui.load.click() },
settings: settingsUI.trigger_event
},
electron: navigator.userAgent.includes('Electron'),
@ -205,7 +228,7 @@ export const api = {
import() { api.ui.import.style.display = '' },
layer: visuals.set_visible_layer,
local() { console.trace('deprecated') },
progress: utilModule.setProgress,
progress: alerts.progress,
slices: visuals.show_slices,
tools: showTools
},
@ -222,8 +245,8 @@ export const api = {
uc: UI.prefix('kiri').inputAction(settings.conf.update),
ui: {},
util: {
isSecure: utilModule.isSecure,
download: utilModule.download,
isSecure,
download,
ui2rec() { api.conf.update_from(...arguments) },
rec2ui() { api.conf.update_fields(...arguments) },
/**

View file

@ -255,10 +255,10 @@ function setup_keybd_nav() {
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
// rendering options
$('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() };
$('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); };
$('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); };
$('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); };
$('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() };
$('render-ghost').onclick = () => { api.view.set_wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); };
$('render-wire').onclick = () => { api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); };
$('render-solid').onclick = () => { api.view.set_wireframe(false, 0, 1); };
$('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') };
$('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') };
$('mesh-merge').onclick = selection.merge;

View file

@ -9,6 +9,7 @@ class ModalControl {
#ui;
#onShow;
#onHide;
#showing;
#initialized = false;
/**
@ -38,7 +39,15 @@ class ModalControl {
* @returns {boolean}
*/
visible() {
return this.#ui.modal.style.display === 'flex';
return modal.#ui.modal.style.display === 'flex';
}
/**
* Check if a specific modal is currently visible
* @returns {boolean}
*/
is(name) {
return modal.#showing === name;
}
/**
@ -46,18 +55,21 @@ class ModalControl {
* @param {string} which - Name of the modal to show
*/
show(which) {
let mod = this.#ui.modal,
let mod = modal.#ui.modal,
style = mod.style,
visible = this.visible(),
visible = modal.visible(),
info = { pct: 0 };
// hide all modals before showing another
Object.keys(this.#ui.modals).forEach(name => {
this.#ui.modals[name].style.display = name === which ? 'flex' : '';
Object.keys(modal.#ui.modals).forEach(name => {
console.log({ name, which });
modal.#ui.modals[name].style.display = name === which ? 'flex' : '';
});
modal.#showing = which;
const ondone = () => {
this.#onShow?.(which);
modal.#onShow?.(which);
};
if (visible) {
@ -79,10 +91,13 @@ class ModalControl {
* Hide the modal with animation
*/
hide() {
if (!this.visible()) {
if (!modal.visible()) {
return;
}
let mod = this.#ui.modal,
modal.#showing = undefined;
let mod = modal.#ui.modal,
style = mod.style,
info = { pct: 100 };
@ -92,7 +107,7 @@ class ModalControl {
onUpdate(() => { style.height = `${info.pct}%` }).
onComplete(() => {
style.display = '';
this.#onHide?.();
modal.#onHide?.();
}).
start();
}

View file

@ -93,8 +93,8 @@ function setMode(mode, lock, then) {
platform.update_selected();
selection.update_bounds(api.widgets.all());
api.conf.update_fields();
// because device dialog, if showing, needs to be updated
if (modal.visible()) {
// if device dialog showing, needs to be refreshed
if (modal.is('setup')) {
api.show.devices();
}
api.space.restore(null, true);

View file

@ -1,20 +1,39 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Face selection tools for mesh manipulation.
* Provides interactive tools for:
* - Lay flat: Rotate selected face to be parallel with build plate
* - Left align: Rotate to align face normal with -X axis
* - Face up: Select face normal for custom operations
* - Camera focus: Select point for camera focus
*
* Tools use hover mode to display visual indicator on face under cursor.
* User clicks face to apply operation. ESC to cancel.
*/
import { api } from './api.js';
import { THREE } from '../../ext/three.js';
import { space } from '../../moto/space.js';
const { Vector3, Quaternion } = THREE;
/** X-axis unit vector for rotation calculations */
const XAXIS = new THREE.Vector3(1,0,0);
/** Z-axis unit vector for rotation calculations */
const ZAXIS = new THREE.Vector3(0,0,1);
// circle
/** Circle mesh geometry for face indicator */
let pgeo = new THREE.CircleGeometry(8, 30);
/** Semi-transparent material for face indicator */
let pmat = new THREE.MeshBasicMaterial({color: 0xddeeff, opacity: 0.6, transparent: true});
/** Combined mesh for face indicator (circle + outline) */
let pmesh = new THREE.Mesh(pgeo, pmat);
// circle outline
// Build circle outline from geometry indices
let pi = pgeo.index.array;
let pp = pgeo.attributes.position.array;
let np = [];
@ -36,19 +55,39 @@ let wmat = new THREE.LineBasicMaterial({ color: 0x6699cc });
let wmesh = new THREE.LineSegments(wgeo, wmat);
pmesh.add(wmesh);
/** Alert handle for "ESC to end" message */
let alert;
/** Last object (widget mesh) the indicator was attached to */
let lastobj;
/** Last face that was hovered over */
let lastface;
/** Whether a tool is currently active */
let enabled = false;
/** Name of current operation for display */
let opName = 'lay flat';
/** Callback to execute when face is selected */
let onDone = onLayFlatSelect;
/**
* Lay flat operation: Rotate widget so selected face is parallel to build plate.
* Calculates quaternion to align face normal with -Z axis (down).
*/
function onLayFlatSelect() {
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
api.selection.rotate(q);
// endIt();
}
/**
* Left align operation: Rotate widget so selected face normal aligns with -X axis.
* Projects face normal onto XY plane and calculates Z-axis rotation.
* Used for orienting parts for belt printers or side printing.
*/
function onLeftySelect() {
// Project the face normal onto the XY plane (ignore Z component)
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
@ -67,11 +106,21 @@ function onLeftySelect() {
api.selection.rotate(q);
}
/**
* Face select operation: Emit face normal for custom handling.
* Emits 'tool.mesh.face-normal' event with selected face normal.
*/
function onFaceUpSelect() {
api.event.emit('tool.mesh.face-normal', lastface.normal);
// endIt();
}
/**
* Start interactive face selection tool.
* Enables hover mode, displays alert, and begins tracking mouse.
* If already enabled, ends the current operation.
* Cannot start if another hover mode is active.
*/
function startIt() {
if (enabled) {
endIt();
@ -86,6 +135,10 @@ function startIt() {
alert = api.show.alert(`[ESC] to end ${opName}`, 600000);
}
/**
* End current face selection tool.
* Disables hover mode, hides alert, and cleans up indicator mesh.
*/
function endIt() {
if (enabled) {
api.hide.alert(alert);
@ -96,6 +149,9 @@ function endIt() {
}
}
/**
* Remove face indicator mesh from scene.
*/
function cleanup() {
if (lastobj) {
lastobj.remove(pmesh);
@ -105,30 +161,52 @@ function cleanup() {
api.event.on('key.esc', endIt);
/**
* Start camera focus tool.
* User clicks face to set camera focus point.
* @param {Function} fn - Callback to execute with focus point
*/
function startFocus(fn) {
opName = 'camera focus';
onDone = fn;
startIt();
}
/**
* Start face selection tool.
* User clicks face to select its normal vector.
*/
function startFaceUp() {
opName = 'face select';
onDone = onFaceUpSelect;
startIt();
}
/**
* Start lay flat tool.
* User clicks face to rotate widget so that face is parallel to build plate.
*/
function startLayFlat() {
opName = 'lay flat';
onDone = onLayFlatSelect;
startIt();
}
/**
* Start left align tool.
* User clicks face to align its normal with -X axis.
*/
function startLeftAlign() {
opName = 'align y axis';
onDone = onLeftySelect;
startIt();
}
/**
* Scale face indicator based on camera distance.
* For camera focus, scales inversely (closer = smaller).
* For other tools, scales with distance for consistent visual size.
*/
function scale() {
let cam = space.internals().camera;
let dist = cam.position.distanceTo(pmesh.position);

View file

@ -1,52 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
const WIN = self.window;
function isSecure(proto) {
return proto.toLowerCase().indexOf("https") === 0;
}
function setProgress(value = 0, msg) {
value = (value * 100).round(4);
api.ui.progress.width = value+'%';
if (self.debug) {
// console.log(msg, value.round(2));
api.ui.prostatus.style.display = 'flex';
if (msg) {
api.ui.prostatus.innerHTML = msg;
} else {
api.ui.prostatus.innerHTML = '';
}
}
}
function loadFile(ev) {
// use modern Filesystem api when available
if (false && window.showOpenFilePicker) {
window.showOpenFilePicker().then(files => {
return Promise.all(files.map(fh => fh.getFile()))
}).then(files => {
if (files.length) {
api.platform.load_files(files);
}
}).catch(e => { /* ignore cancel */ });
return;
}
api.ui.load.click();
}
function downloadBlob(data, filename) {
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
$('_dexport_').click();
}
export const util = {
isSecure,
download: downloadBlob,
setProgress,
loadFile
};

View file

@ -11,8 +11,26 @@ import STACKS from './stacks.js';
const { VIEWS } = consts;
const DOC = self.document;
/** Current view mode (ARRANGE, SLICE, PREVIEW, or ANIMATE) */
let viewMode = VIEWS.ARRANGE;
/**
* Set application view mode and update UI accordingly.
* Modes:
* - ARRANGE: Widget positioning and arrangement
* - SLICE: Sliced layer visualization
* - PREVIEW: Toolpath/gcode preview
* - ANIMATE: Animated toolpath playback
*
* Each mode:
* - Updates UI button states
* - Clears selections and bounds
* - Shows/hides relevant tools and sliders
* - Configures widget visibility and rendering
* - Emits 'view.set' event
*
* @param {number} mode - View mode constant from VIEWS
*/
function setViewMode(mode) {
const isCAM = settings.mode() === 'CAM';
viewMode = mode;
@ -37,23 +55,23 @@ function setViewMode(mode) {
api.visuals.set_visible_layer();
api.visuals.set_widget_visibility(true);
api.widgets.setOpacity(1);
api.view.edges(api.local.getBoolean('model.edges'));
api.view.set_edges(api.local.getBoolean('model.edges'));
break;
case VIEWS.SLICE:
$('act-slice').classList.add('selected');
api.visuals.update_speeds();
api.visuals.update_slider_max();
api.visuals.set_widget_visibility(true);
!isCAM && api.view.edges(false);
!isCAM && api.view.set_edges(false);
break;
case VIEWS.PREVIEW:
$('act-preview').classList.add('selected');
api.visuals.set_widget_visibility(true);
!isCAM && api.view.edges(false);
!isCAM && api.view.set_edges(false);
break;
case VIEWS.ANIMATE:
$('act-animate').classList.add('selected');
!isCAM && api.view.edges(false);
!isCAM && api.view.set_edges(false);
break;
default:
console.log("invalid view mode: "+mode);
@ -63,15 +81,29 @@ function setViewMode(mode) {
DOC.activeElement.blur();
}
/**
* View mode management API.
* Provides getters/setters and mode checking utilities.
*/
export const view = {
/** Get current view mode */
get() { return viewMode },
/** Set view mode */
set() { setViewMode(...arguments) },
/** Switch to arrange mode */
set_arrange() { setViewMode(VIEWS.ARRANGE) },
/** Switch to slice mode */
set_slice() { setViewMode(VIEWS.SLICE) },
/** Switch to preview mode */
set_preview() { setViewMode(VIEWS.PREVIEW) },
/** Switch to animate mode */
set_animate() { setViewMode(VIEWS.ANIMATE) },
/** Check if in arrange mode */
is_arrange() { return viewMode === VIEWS.ARRANGE },
/** Check if in slice mode */
is_slice() { return viewMode === VIEWS.SLICE },
/** Check if in preview mode */
is_preview() { return viewMode === VIEWS.PREVIEW },
/** Check if in animate mode */
is_animate() { return viewMode === VIEWS.ANIMATE }
};

View file

@ -8,15 +8,35 @@ import STACKS from './stacks.js';
const { COLOR, VIEWS } = consts;
// Note: viewMode is imported dynamically via api.view.get()
/**
* Constrain value to min/max bounds.
* @param {number} v - Value to constrain
* @param {number} min - Minimum value
* @param {number} max - Maximum value
* @returns {number} Constrained value
*/
function bound(v,min,max) {
return Math.max(min,Math.min(max,v));
}
/**
* Get unit scale factor for coordinate conversion.
* Returns 25.4 for CAM mode with inches, 1.0 otherwise (mm).
* @returns {number} Unit scale factor
*/
function unitScale() {
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
}
/**
* Update speed color legend for preview mode.
* Shows color-coded speed visualization in preview mode for FDM.
* Generates 20 color swatches from worker and displays in UI.
* Only visible in PREVIEW mode for FDM (not SLA/LASER) when enabled.
* Emits 'preview.speeds' event with min/max speeds.
* @param {number} [maxSpeed] - Maximum speed value
* @param {number} [minSpeed] - Minimum speed value
*/
function updateSpeeds(maxSpeed, minSpeed) {
const { ui } = api;
const viewMode = api.view.get();
@ -51,6 +71,11 @@ function updateSpeeds(maxSpeed, minSpeed) {
}
}
/**
* Update slider visual position and sync settings.
* Updates slider thumb positions based on current range.
* Synchronizes configuration fields from range values.
*/
function updateSlider() {
const { lo, hi, max } = api.slider.getRange();
if (max > 0) {
@ -59,6 +84,13 @@ function updateSlider() {
api.conf.update_fields_from_range();
}
/**
* Set visible layer range and update visualization.
* Bounds values to valid range and updates slider.
* Triggers slice visualization update.
* @param {number} [h] - High layer (defaults to current hi)
* @param {number} [l] - Low layer (defaults to current lo)
*/
function setVisibleLayer(h, l) {
const { lo, hi, max } = api.slider.getRange();
h = h >= 0 ? h : hi;
@ -70,11 +102,25 @@ function setVisibleLayer(h, l) {
showSlices();
}
/**
* Set wireframe rendering mode for all widgets.
* Updates 3D scene after applying to all widgets.
* @param {boolean} bool - Enable/disable wireframe
* @param {number} [color] - Optional wireframe color
* @param {number} [opacity] - Optional wireframe opacity
*/
function setWireframe(bool, color, opacity) {
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
SPACE.update();
}
/**
* Toggle or set edge rendering for all widgets.
* If bool.toggle is truthy, toggles the current state.
* Otherwise sets edge visibility to bool value.
* Persists state to local storage and updates scene.
* @param {boolean|object} bool - Enable/disable edges, or {toggle: true} to toggle
*/
function setEdges(bool) {
if (bool && bool.toggle) {
api.local.toggle('model.edges');
@ -86,6 +132,12 @@ function setEdges(bool) {
SPACE.update();
}
/**
* Update slider maximum from stack height.
* Sets slider max to tallest stack layer - 1.
* If set=true or current hi exceeds new max, resets range to show top layer.
* @param {boolean} [set] - Force reset range to max
*/
function updateSliderMax(set) {
let max = STACKS.getRange().tallest - 1;
api.slider.setMax(max);
@ -96,6 +148,10 @@ function updateSliderMax(set) {
}
}
/**
* Hide slice visualization and restore widget rendering.
* Clears stack display, restores model opacity, disables wireframe.
*/
function hideSlices() {
STACKS.clear();
api.widgets.setOpacity(COLOR.model_opacity);
@ -104,6 +160,10 @@ function hideSlices() {
});
}
/**
* Show or hide all widgets in 3D scene.
* @param {boolean} bool - True to show, false to hide
*/
function setWidgetVisibility(bool) {
api.widgets.each(w => {
if (bool) {
@ -115,9 +175,13 @@ function setWidgetVisibility(bool) {
}
/**
* hide or show slice-layers and their sub-elements
*
* @param {number} [layer]
* Show slice layer visualization.
* Only works in SLICE/PREVIEW/ANIMATE views (not ARRANGE).
* Updates slider range to show specified layer(s).
* If no layer specified, shows current hi layer.
* Expands range downward if needed (newLo = min(layer, lo)).
* Updates slider position and triggers stack visualization.
* @param {number|string} [layer] - Layer to show (defaults to current hi)
*/
function showSlices(layer) {
const viewMode = api.view.get();
@ -146,15 +210,26 @@ function showSlices(layer) {
SPACE.update();
}
/**
* Show layer slider UI element.
*/
function showSlider() {
api.slider.show();
}
/**
* Hide layer slider and speed legend UI elements.
*/
function hideSlider() {
api.slider.hide();
api.ui.speeds.style.display = 'none';
}
/**
* Synchronize stack label visibility with saved preferences.
* Reads visibility state from settings.labels for current mode/view combination.
* Applies visibility to all stack labels.
*/
function updateStackLabelState() {
const settings = api.conf.get();
// match label checkboxes to preference
@ -165,8 +240,8 @@ function updateStackLabelState() {
}
export const visuals = {
wireframe: setWireframe,
edges: setEdges,
set_wireframe: setWireframe,
set_edges: setEdges,
unit_scale: unitScale,
update_speeds: updateSpeeds,
update_slider: updateSlider,

View file

@ -17,13 +17,25 @@ const LOC = self.location;
const SETUP = parseOpt(LOC.search.substring(1));
const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri'));
/** Auto-save timer handle */
let autoSaveTimer = null;
/**
* Reload the application page.
* Emits 'reload' event and defers reload until busy count reaches zero.
*/
function reload() {
api.event.emit('reload');
do_reload(100);
}
/**
* Perform page reload after ensuring async operations complete.
* Checks busy state and defers reload until idle.
* Retries with exponential backoff (100ms -> 250ms).
* @param {number} [time=100] - Initial delay in milliseconds
* @private
*/
function do_reload(time) {
// allow time for async saves to complete and busy to to to zero
setTimeout(() => {
@ -36,6 +48,11 @@ function do_reload(time) {
}, time || 100);
}
/**
* Trigger delayed auto-save if enabled.
* Debounces saves with 1-second delay.
* Only saves if controller.autoSave is enabled.
*/
function auto_save() {
if (!settings.ctrl().autoSave) {
return;
@ -46,6 +63,14 @@ function auto_save() {
}, 1000);
}
/**
* Set camera focus on widgets or point.
* If point provided, focuses on that point.
* Otherwise calculates centroid of selection (or all widgets if sel undefined/empty).
* For CAM indexed mode, uses Z=0 focus height. Otherwise uses half of top_z.
* @param {Widget|Array<Widget>} [sel] - Widget(s) to focus on (defaults to all)
* @param {object} [point] - Optional point {x, y, z} to focus on directly
*/
function setFocus(sel, point) {
if (point) {
SPACE.platform.setCenter(point.x, point.z, point.y);
@ -76,6 +101,16 @@ function setFocus(sel, point) {
SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y));
}
/**
* Save workspace state to local storage.
* Saves:
* - Configuration settings
* - Widget list and metadata
* - Widget positions and states
* Removes stale widget data for deleted widgets.
* Shows alert unless quiet=true.
* @param {boolean} [quiet] - Suppress "workspace saved" alert
*/
function saveWorkspace(quiet) {
api.conf.save();
const newWidgets = [];
@ -100,6 +135,18 @@ function saveWorkspace(quiet) {
}
}
/**
* Restore workspace from local storage.
* Restores:
* - Configuration settings
* - Camera view position
* - All saved widgets from storage
* Clears existing platform widgets unless skip_widget_load=true.
* Calls ondone callback when complete.
* @param {Function} [ondone] - Callback when restore completes
* @param {boolean} [skip_widget_load] - Skip loading widgets (config only)
* @returns {boolean} True if widgets were queued for loading
*/
function restoreWorkspace(ondone, skip_widget_load) {
let newset = api.conf.restore(false),
camera = newset.controller.view,
@ -154,6 +201,10 @@ function restoreWorkspace(ondone, skip_widget_load) {
return toload.length > 0;
}
/**
* Clear all widgets from workspace.
* Clears worker cache/memory, selects all widgets, and deletes them.
*/
function clearWorkspace() {
// free up worker cache/mem
api.client.clear();
@ -161,6 +212,10 @@ function clearWorkspace() {
platform.delete(selection.meshes());
}
/**
* Check if dark mode is enabled.
* @returns {boolean} True if dark mode active
*/
function is_dark() {
return settings.ctrl().dark;
}

View file

@ -23,6 +23,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
// get tab widgets
let tabW = widget.group.filter(w => w != widget);
// merge overlapping tabs
for (let i=0; i<tabW.length; i++) {
for (let j=i+1; j<tabW.length; j++) {