fold tiny util into api, clean up modal corner cases, add more jsdoc
This commit is contained in:
parent
dbde596903
commit
bcd2449b89
11 changed files with 339 additions and 90 deletions
|
|
@ -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 {
|
export {
|
||||||
hide,
|
hide,
|
||||||
show,
|
show,
|
||||||
update
|
update,
|
||||||
|
progress
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
hide,
|
hide,
|
||||||
show,
|
show,
|
||||||
update
|
update,
|
||||||
|
progress
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import { stats } from './stats.js';
|
||||||
import { types as load } from '../../load/file.js';
|
import { types as load } from '../../load/file.js';
|
||||||
import { UI } from './inputs.js';
|
import { UI } from './inputs.js';
|
||||||
import { updateTool } from '../mode/cam/tools.js';
|
import { updateTool } from '../mode/cam/tools.js';
|
||||||
import { util as utilModule } from './util.js';
|
|
||||||
import { view as viewModule } from './viewmode.js';
|
import { view as viewModule } from './viewmode.js';
|
||||||
import { visuals } from './visuals.js';
|
import { visuals } from './visuals.js';
|
||||||
import { widgets } from '../core/widgets.js';
|
import { widgets } from '../core/widgets.js';
|
||||||
|
|
@ -43,11 +42,11 @@ import { workspace } from './workspace.js';
|
||||||
|
|
||||||
// environment setup
|
// environment setup
|
||||||
let LOC = self.location,
|
let LOC = self.location,
|
||||||
SETUP = utils.parseOpt(LOC.search.substring(1)),
|
|
||||||
SECURE = utilModule.isSecure(LOC.protocol),
|
|
||||||
LOCAL = self.debug && !SETUP.remote,
|
|
||||||
EVENT = broker,
|
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
|
// todo: fix in widget.js b/c front-end and back-end do not share api
|
||||||
self.kiri_catalog = FILES;
|
self.kiri_catalog = FILES;
|
||||||
|
|
@ -61,6 +60,27 @@ EVENT.on = (topic, listener) => {
|
||||||
return EVENT;
|
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 */
|
/** Busy state counter for tracking active operations */
|
||||||
let busyVal = 0,
|
let busyVal = 0,
|
||||||
/** Hover feature flag */
|
/** Hover feature flag */
|
||||||
|
|
@ -68,7 +88,10 @@ let busyVal = 0,
|
||||||
/** Explicit undefined for object defaults */
|
/** Explicit undefined for object defaults */
|
||||||
undef = undefined;
|
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 = {
|
export const api = {
|
||||||
ajax,
|
ajax,
|
||||||
beta,
|
beta,
|
||||||
|
|
@ -132,7 +155,7 @@ export const api = {
|
||||||
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
||||||
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
||||||
alerts(clr) { alerts.update(clr) },
|
alerts(clr) { alerts.update(clr) },
|
||||||
import: utilModule.loadFile,
|
import() { api.ui.load.click() },
|
||||||
settings: settingsUI.trigger_event
|
settings: settingsUI.trigger_event
|
||||||
},
|
},
|
||||||
electron: navigator.userAgent.includes('Electron'),
|
electron: navigator.userAgent.includes('Electron'),
|
||||||
|
|
@ -205,7 +228,7 @@ export const api = {
|
||||||
import() { api.ui.import.style.display = '' },
|
import() { api.ui.import.style.display = '' },
|
||||||
layer: visuals.set_visible_layer,
|
layer: visuals.set_visible_layer,
|
||||||
local() { console.trace('deprecated') },
|
local() { console.trace('deprecated') },
|
||||||
progress: utilModule.setProgress,
|
progress: alerts.progress,
|
||||||
slices: visuals.show_slices,
|
slices: visuals.show_slices,
|
||||||
tools: showTools
|
tools: showTools
|
||||||
},
|
},
|
||||||
|
|
@ -222,8 +245,8 @@ export const api = {
|
||||||
uc: UI.prefix('kiri').inputAction(settings.conf.update),
|
uc: UI.prefix('kiri').inputAction(settings.conf.update),
|
||||||
ui: {},
|
ui: {},
|
||||||
util: {
|
util: {
|
||||||
isSecure: utilModule.isSecure,
|
isSecure,
|
||||||
download: utilModule.download,
|
download,
|
||||||
ui2rec() { api.conf.update_from(...arguments) },
|
ui2rec() { api.conf.update_from(...arguments) },
|
||||||
rec2ui() { api.conf.update_fields(...arguments) },
|
rec2ui() { api.conf.update_fields(...arguments) },
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -255,10 +255,10 @@ function setup_keybd_nav() {
|
||||||
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
$('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) };
|
||||||
|
|
||||||
// rendering options
|
// rendering options
|
||||||
$('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() };
|
$('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() };
|
||||||
$('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); };
|
$('render-ghost').onclick = () => { api.view.set_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-wire').onclick = () => { api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); };
|
||||||
$('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); };
|
$('render-solid').onclick = () => { api.view.set_wireframe(false, 0, 1); };
|
||||||
$('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') };
|
$('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') };
|
||||||
$('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') };
|
$('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') };
|
||||||
$('mesh-merge').onclick = selection.merge;
|
$('mesh-merge').onclick = selection.merge;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class ModalControl {
|
||||||
#ui;
|
#ui;
|
||||||
#onShow;
|
#onShow;
|
||||||
#onHide;
|
#onHide;
|
||||||
|
#showing;
|
||||||
#initialized = false;
|
#initialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,7 +39,15 @@ class ModalControl {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
visible() {
|
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
|
* @param {string} which - Name of the modal to show
|
||||||
*/
|
*/
|
||||||
show(which) {
|
show(which) {
|
||||||
let mod = this.#ui.modal,
|
let mod = modal.#ui.modal,
|
||||||
style = mod.style,
|
style = mod.style,
|
||||||
visible = this.visible(),
|
visible = modal.visible(),
|
||||||
info = { pct: 0 };
|
info = { pct: 0 };
|
||||||
|
|
||||||
// hide all modals before showing another
|
// hide all modals before showing another
|
||||||
Object.keys(this.#ui.modals).forEach(name => {
|
Object.keys(modal.#ui.modals).forEach(name => {
|
||||||
this.#ui.modals[name].style.display = name === which ? 'flex' : '';
|
console.log({ name, which });
|
||||||
|
modal.#ui.modals[name].style.display = name === which ? 'flex' : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modal.#showing = which;
|
||||||
|
|
||||||
const ondone = () => {
|
const ondone = () => {
|
||||||
this.#onShow?.(which);
|
modal.#onShow?.(which);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
|
@ -79,10 +91,13 @@ class ModalControl {
|
||||||
* Hide the modal with animation
|
* Hide the modal with animation
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
if (!this.visible()) {
|
if (!modal.visible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mod = this.#ui.modal,
|
|
||||||
|
modal.#showing = undefined;
|
||||||
|
|
||||||
|
let mod = modal.#ui.modal,
|
||||||
style = mod.style,
|
style = mod.style,
|
||||||
info = { pct: 100 };
|
info = { pct: 100 };
|
||||||
|
|
||||||
|
|
@ -92,7 +107,7 @@ class ModalControl {
|
||||||
onUpdate(() => { style.height = `${info.pct}%` }).
|
onUpdate(() => { style.height = `${info.pct}%` }).
|
||||||
onComplete(() => {
|
onComplete(() => {
|
||||||
style.display = '';
|
style.display = '';
|
||||||
this.#onHide?.();
|
modal.#onHide?.();
|
||||||
}).
|
}).
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ function setMode(mode, lock, then) {
|
||||||
platform.update_selected();
|
platform.update_selected();
|
||||||
selection.update_bounds(api.widgets.all());
|
selection.update_bounds(api.widgets.all());
|
||||||
api.conf.update_fields();
|
api.conf.update_fields();
|
||||||
// because device dialog, if showing, needs to be updated
|
// if device dialog showing, needs to be refreshed
|
||||||
if (modal.visible()) {
|
if (modal.is('setup')) {
|
||||||
api.show.devices();
|
api.show.devices();
|
||||||
}
|
}
|
||||||
api.space.restore(null, true);
|
api.space.restore(null, true);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,39 @@
|
||||||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
/** 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 { api } from './api.js';
|
||||||
import { THREE } from '../../ext/three.js';
|
import { THREE } from '../../ext/three.js';
|
||||||
import { space } from '../../moto/space.js';
|
import { space } from '../../moto/space.js';
|
||||||
|
|
||||||
const { Vector3, Quaternion } = THREE;
|
const { Vector3, Quaternion } = THREE;
|
||||||
|
|
||||||
|
/** X-axis unit vector for rotation calculations */
|
||||||
const XAXIS = new THREE.Vector3(1,0,0);
|
const XAXIS = new THREE.Vector3(1,0,0);
|
||||||
|
|
||||||
|
/** Z-axis unit vector for rotation calculations */
|
||||||
const ZAXIS = new THREE.Vector3(0,0,1);
|
const ZAXIS = new THREE.Vector3(0,0,1);
|
||||||
|
|
||||||
// circle
|
/** Circle mesh geometry for face indicator */
|
||||||
let pgeo = new THREE.CircleGeometry(8, 30);
|
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});
|
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);
|
let pmesh = new THREE.Mesh(pgeo, pmat);
|
||||||
|
|
||||||
// circle outline
|
// Build circle outline from geometry indices
|
||||||
let pi = pgeo.index.array;
|
let pi = pgeo.index.array;
|
||||||
let pp = pgeo.attributes.position.array;
|
let pp = pgeo.attributes.position.array;
|
||||||
let np = [];
|
let np = [];
|
||||||
|
|
@ -36,19 +55,39 @@ let wmat = new THREE.LineBasicMaterial({ color: 0x6699cc });
|
||||||
let wmesh = new THREE.LineSegments(wgeo, wmat);
|
let wmesh = new THREE.LineSegments(wgeo, wmat);
|
||||||
pmesh.add(wmesh);
|
pmesh.add(wmesh);
|
||||||
|
|
||||||
|
/** Alert handle for "ESC to end" message */
|
||||||
let alert;
|
let alert;
|
||||||
|
|
||||||
|
/** Last object (widget mesh) the indicator was attached to */
|
||||||
let lastobj;
|
let lastobj;
|
||||||
|
|
||||||
|
/** Last face that was hovered over */
|
||||||
let lastface;
|
let lastface;
|
||||||
|
|
||||||
|
/** Whether a tool is currently active */
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
|
|
||||||
|
/** Name of current operation for display */
|
||||||
let opName = 'lay flat';
|
let opName = 'lay flat';
|
||||||
|
|
||||||
|
/** Callback to execute when face is selected */
|
||||||
let onDone = onLayFlatSelect;
|
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() {
|
function onLayFlatSelect() {
|
||||||
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
|
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
|
||||||
api.selection.rotate(q);
|
api.selection.rotate(q);
|
||||||
// endIt();
|
// 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() {
|
function onLeftySelect() {
|
||||||
// Project the face normal onto the XY plane (ignore Z component)
|
// Project the face normal onto the XY plane (ignore Z component)
|
||||||
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
|
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
|
||||||
|
|
@ -67,11 +106,21 @@ function onLeftySelect() {
|
||||||
api.selection.rotate(q);
|
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() {
|
function onFaceUpSelect() {
|
||||||
api.event.emit('tool.mesh.face-normal', lastface.normal);
|
api.event.emit('tool.mesh.face-normal', lastface.normal);
|
||||||
// endIt();
|
// 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() {
|
function startIt() {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
endIt();
|
endIt();
|
||||||
|
|
@ -86,6 +135,10 @@ function startIt() {
|
||||||
alert = api.show.alert(`[ESC] to end ${opName}`, 600000);
|
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() {
|
function endIt() {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
api.hide.alert(alert);
|
api.hide.alert(alert);
|
||||||
|
|
@ -96,6 +149,9 @@ function endIt() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove face indicator mesh from scene.
|
||||||
|
*/
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
if (lastobj) {
|
if (lastobj) {
|
||||||
lastobj.remove(pmesh);
|
lastobj.remove(pmesh);
|
||||||
|
|
@ -105,30 +161,52 @@ function cleanup() {
|
||||||
|
|
||||||
api.event.on('key.esc', endIt);
|
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) {
|
function startFocus(fn) {
|
||||||
opName = 'camera focus';
|
opName = 'camera focus';
|
||||||
onDone = fn;
|
onDone = fn;
|
||||||
startIt();
|
startIt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start face selection tool.
|
||||||
|
* User clicks face to select its normal vector.
|
||||||
|
*/
|
||||||
function startFaceUp() {
|
function startFaceUp() {
|
||||||
opName = 'face select';
|
opName = 'face select';
|
||||||
onDone = onFaceUpSelect;
|
onDone = onFaceUpSelect;
|
||||||
startIt();
|
startIt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start lay flat tool.
|
||||||
|
* User clicks face to rotate widget so that face is parallel to build plate.
|
||||||
|
*/
|
||||||
function startLayFlat() {
|
function startLayFlat() {
|
||||||
opName = 'lay flat';
|
opName = 'lay flat';
|
||||||
onDone = onLayFlatSelect;
|
onDone = onLayFlatSelect;
|
||||||
startIt();
|
startIt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start left align tool.
|
||||||
|
* User clicks face to align its normal with -X axis.
|
||||||
|
*/
|
||||||
function startLeftAlign() {
|
function startLeftAlign() {
|
||||||
opName = 'align y axis';
|
opName = 'align y axis';
|
||||||
onDone = onLeftySelect;
|
onDone = onLeftySelect;
|
||||||
startIt();
|
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() {
|
function scale() {
|
||||||
let cam = space.internals().camera;
|
let cam = space.internals().camera;
|
||||||
let dist = cam.position.distanceTo(pmesh.position);
|
let dist = cam.position.distanceTo(pmesh.position);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
@ -11,8 +11,26 @@ import STACKS from './stacks.js';
|
||||||
const { VIEWS } = consts;
|
const { VIEWS } = consts;
|
||||||
const DOC = self.document;
|
const DOC = self.document;
|
||||||
|
|
||||||
|
/** Current view mode (ARRANGE, SLICE, PREVIEW, or ANIMATE) */
|
||||||
let viewMode = VIEWS.ARRANGE;
|
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) {
|
function setViewMode(mode) {
|
||||||
const isCAM = settings.mode() === 'CAM';
|
const isCAM = settings.mode() === 'CAM';
|
||||||
viewMode = mode;
|
viewMode = mode;
|
||||||
|
|
@ -37,23 +55,23 @@ function setViewMode(mode) {
|
||||||
api.visuals.set_visible_layer();
|
api.visuals.set_visible_layer();
|
||||||
api.visuals.set_widget_visibility(true);
|
api.visuals.set_widget_visibility(true);
|
||||||
api.widgets.setOpacity(1);
|
api.widgets.setOpacity(1);
|
||||||
api.view.edges(api.local.getBoolean('model.edges'));
|
api.view.set_edges(api.local.getBoolean('model.edges'));
|
||||||
break;
|
break;
|
||||||
case VIEWS.SLICE:
|
case VIEWS.SLICE:
|
||||||
$('act-slice').classList.add('selected');
|
$('act-slice').classList.add('selected');
|
||||||
api.visuals.update_speeds();
|
api.visuals.update_speeds();
|
||||||
api.visuals.update_slider_max();
|
api.visuals.update_slider_max();
|
||||||
api.visuals.set_widget_visibility(true);
|
api.visuals.set_widget_visibility(true);
|
||||||
!isCAM && api.view.edges(false);
|
!isCAM && api.view.set_edges(false);
|
||||||
break;
|
break;
|
||||||
case VIEWS.PREVIEW:
|
case VIEWS.PREVIEW:
|
||||||
$('act-preview').classList.add('selected');
|
$('act-preview').classList.add('selected');
|
||||||
api.visuals.set_widget_visibility(true);
|
api.visuals.set_widget_visibility(true);
|
||||||
!isCAM && api.view.edges(false);
|
!isCAM && api.view.set_edges(false);
|
||||||
break;
|
break;
|
||||||
case VIEWS.ANIMATE:
|
case VIEWS.ANIMATE:
|
||||||
$('act-animate').classList.add('selected');
|
$('act-animate').classList.add('selected');
|
||||||
!isCAM && api.view.edges(false);
|
!isCAM && api.view.set_edges(false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("invalid view mode: "+mode);
|
console.log("invalid view mode: "+mode);
|
||||||
|
|
@ -63,15 +81,29 @@ function setViewMode(mode) {
|
||||||
DOC.activeElement.blur();
|
DOC.activeElement.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View mode management API.
|
||||||
|
* Provides getters/setters and mode checking utilities.
|
||||||
|
*/
|
||||||
export const view = {
|
export const view = {
|
||||||
|
/** Get current view mode */
|
||||||
get() { return viewMode },
|
get() { return viewMode },
|
||||||
|
/** Set view mode */
|
||||||
set() { setViewMode(...arguments) },
|
set() { setViewMode(...arguments) },
|
||||||
|
/** Switch to arrange mode */
|
||||||
set_arrange() { setViewMode(VIEWS.ARRANGE) },
|
set_arrange() { setViewMode(VIEWS.ARRANGE) },
|
||||||
|
/** Switch to slice mode */
|
||||||
set_slice() { setViewMode(VIEWS.SLICE) },
|
set_slice() { setViewMode(VIEWS.SLICE) },
|
||||||
|
/** Switch to preview mode */
|
||||||
set_preview() { setViewMode(VIEWS.PREVIEW) },
|
set_preview() { setViewMode(VIEWS.PREVIEW) },
|
||||||
|
/** Switch to animate mode */
|
||||||
set_animate() { setViewMode(VIEWS.ANIMATE) },
|
set_animate() { setViewMode(VIEWS.ANIMATE) },
|
||||||
|
/** Check if in arrange mode */
|
||||||
is_arrange() { return viewMode === VIEWS.ARRANGE },
|
is_arrange() { return viewMode === VIEWS.ARRANGE },
|
||||||
|
/** Check if in slice mode */
|
||||||
is_slice() { return viewMode === VIEWS.SLICE },
|
is_slice() { return viewMode === VIEWS.SLICE },
|
||||||
|
/** Check if in preview mode */
|
||||||
is_preview() { return viewMode === VIEWS.PREVIEW },
|
is_preview() { return viewMode === VIEWS.PREVIEW },
|
||||||
|
/** Check if in animate mode */
|
||||||
is_animate() { return viewMode === VIEWS.ANIMATE }
|
is_animate() { return viewMode === VIEWS.ANIMATE }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,35 @@ import STACKS from './stacks.js';
|
||||||
|
|
||||||
const { COLOR, VIEWS } = consts;
|
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) {
|
function bound(v,min,max) {
|
||||||
return Math.max(min,Math.min(max,v));
|
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() {
|
function unitScale() {
|
||||||
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
|
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) {
|
function updateSpeeds(maxSpeed, minSpeed) {
|
||||||
const { ui } = api;
|
const { ui } = api;
|
||||||
const viewMode = api.view.get();
|
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() {
|
function updateSlider() {
|
||||||
const { lo, hi, max } = api.slider.getRange();
|
const { lo, hi, max } = api.slider.getRange();
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
|
|
@ -59,6 +84,13 @@ function updateSlider() {
|
||||||
api.conf.update_fields_from_range();
|
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) {
|
function setVisibleLayer(h, l) {
|
||||||
const { lo, hi, max } = api.slider.getRange();
|
const { lo, hi, max } = api.slider.getRange();
|
||||||
h = h >= 0 ? h : hi;
|
h = h >= 0 ? h : hi;
|
||||||
|
|
@ -70,11 +102,25 @@ function setVisibleLayer(h, l) {
|
||||||
showSlices();
|
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) {
|
function setWireframe(bool, color, opacity) {
|
||||||
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
|
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
|
||||||
SPACE.update();
|
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) {
|
function setEdges(bool) {
|
||||||
if (bool && bool.toggle) {
|
if (bool && bool.toggle) {
|
||||||
api.local.toggle('model.edges');
|
api.local.toggle('model.edges');
|
||||||
|
|
@ -86,6 +132,12 @@ function setEdges(bool) {
|
||||||
SPACE.update();
|
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) {
|
function updateSliderMax(set) {
|
||||||
let max = STACKS.getRange().tallest - 1;
|
let max = STACKS.getRange().tallest - 1;
|
||||||
api.slider.setMax(max);
|
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() {
|
function hideSlices() {
|
||||||
STACKS.clear();
|
STACKS.clear();
|
||||||
api.widgets.setOpacity(COLOR.model_opacity);
|
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) {
|
function setWidgetVisibility(bool) {
|
||||||
api.widgets.each(w => {
|
api.widgets.each(w => {
|
||||||
if (bool) {
|
if (bool) {
|
||||||
|
|
@ -115,9 +175,13 @@ function setWidgetVisibility(bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hide or show slice-layers and their sub-elements
|
* Show slice layer visualization.
|
||||||
*
|
* Only works in SLICE/PREVIEW/ANIMATE views (not ARRANGE).
|
||||||
* @param {number} [layer]
|
* 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) {
|
function showSlices(layer) {
|
||||||
const viewMode = api.view.get();
|
const viewMode = api.view.get();
|
||||||
|
|
@ -146,15 +210,26 @@ function showSlices(layer) {
|
||||||
SPACE.update();
|
SPACE.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show layer slider UI element.
|
||||||
|
*/
|
||||||
function showSlider() {
|
function showSlider() {
|
||||||
api.slider.show();
|
api.slider.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide layer slider and speed legend UI elements.
|
||||||
|
*/
|
||||||
function hideSlider() {
|
function hideSlider() {
|
||||||
api.slider.hide();
|
api.slider.hide();
|
||||||
api.ui.speeds.style.display = 'none';
|
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() {
|
function updateStackLabelState() {
|
||||||
const settings = api.conf.get();
|
const settings = api.conf.get();
|
||||||
// match label checkboxes to preference
|
// match label checkboxes to preference
|
||||||
|
|
@ -165,8 +240,8 @@ function updateStackLabelState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const visuals = {
|
export const visuals = {
|
||||||
wireframe: setWireframe,
|
set_wireframe: setWireframe,
|
||||||
edges: setEdges,
|
set_edges: setEdges,
|
||||||
unit_scale: unitScale,
|
unit_scale: unitScale,
|
||||||
update_speeds: updateSpeeds,
|
update_speeds: updateSpeeds,
|
||||||
update_slider: updateSlider,
|
update_slider: updateSlider,
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,25 @@ const LOC = self.location;
|
||||||
const SETUP = parseOpt(LOC.search.substring(1));
|
const SETUP = parseOpt(LOC.search.substring(1));
|
||||||
const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri'));
|
const FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri'));
|
||||||
|
|
||||||
|
/** Auto-save timer handle */
|
||||||
let autoSaveTimer = null;
|
let autoSaveTimer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the application page.
|
||||||
|
* Emits 'reload' event and defers reload until busy count reaches zero.
|
||||||
|
*/
|
||||||
function reload() {
|
function reload() {
|
||||||
api.event.emit('reload');
|
api.event.emit('reload');
|
||||||
do_reload(100);
|
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) {
|
function do_reload(time) {
|
||||||
// allow time for async saves to complete and busy to to to zero
|
// allow time for async saves to complete and busy to to to zero
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -36,6 +48,11 @@ function do_reload(time) {
|
||||||
}, time || 100);
|
}, time || 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger delayed auto-save if enabled.
|
||||||
|
* Debounces saves with 1-second delay.
|
||||||
|
* Only saves if controller.autoSave is enabled.
|
||||||
|
*/
|
||||||
function auto_save() {
|
function auto_save() {
|
||||||
if (!settings.ctrl().autoSave) {
|
if (!settings.ctrl().autoSave) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -46,6 +63,14 @@ function auto_save() {
|
||||||
}, 1000);
|
}, 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) {
|
function setFocus(sel, point) {
|
||||||
if (point) {
|
if (point) {
|
||||||
SPACE.platform.setCenter(point.x, point.z, point.y);
|
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));
|
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) {
|
function saveWorkspace(quiet) {
|
||||||
api.conf.save();
|
api.conf.save();
|
||||||
const newWidgets = [];
|
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) {
|
function restoreWorkspace(ondone, skip_widget_load) {
|
||||||
let newset = api.conf.restore(false),
|
let newset = api.conf.restore(false),
|
||||||
camera = newset.controller.view,
|
camera = newset.controller.view,
|
||||||
|
|
@ -154,6 +201,10 @@ function restoreWorkspace(ondone, skip_widget_load) {
|
||||||
return toload.length > 0;
|
return toload.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all widgets from workspace.
|
||||||
|
* Clears worker cache/memory, selects all widgets, and deletes them.
|
||||||
|
*/
|
||||||
function clearWorkspace() {
|
function clearWorkspace() {
|
||||||
// free up worker cache/mem
|
// free up worker cache/mem
|
||||||
api.client.clear();
|
api.client.clear();
|
||||||
|
|
@ -161,6 +212,10 @@ function clearWorkspace() {
|
||||||
platform.delete(selection.meshes());
|
platform.delete(selection.meshes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if dark mode is enabled.
|
||||||
|
* @returns {boolean} True if dark mode active
|
||||||
|
*/
|
||||||
function is_dark() {
|
function is_dark() {
|
||||||
return settings.ctrl().dark;
|
return settings.ctrl().dark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
||||||
|
|
||||||
// get tab widgets
|
// get tab widgets
|
||||||
let tabW = widget.group.filter(w => w != widget);
|
let tabW = widget.group.filter(w => w != widget);
|
||||||
|
|
||||||
// merge overlapping tabs
|
// merge overlapping tabs
|
||||||
for (let i=0; i<tabW.length; i++) {
|
for (let i=0; i<tabW.length; i++) {
|
||||||
for (let j=i+1; j<tabW.length; j++) {
|
for (let j=i+1; j<tabW.length; j++) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue