push to get friendly jsdoc for future devs

This commit is contained in:
Stewart Allen 2025-12-25 14:03:37 -05:00
commit dbde596903
20 changed files with 1054 additions and 20 deletions

View file

@ -2,8 +2,18 @@
import { api } from './api.js';
/**
* Active alert records. Each record is an array: [message, timestamp, duration, active]
* @type {Array<[string, number, number, boolean]>}
*/
let alerts = [];
/**
* Display an alert message to the user
* @param {string} message - The message to display
* @param {number} time - Duration in seconds to show the alert
* @returns {Array|undefined} Alert record [message, timestamp, duration, active] or result of update()
*/
function show(message, time) {
if (message === undefined || message === null) {
return update(true);
@ -19,6 +29,11 @@ function show(message, time) {
return rec;
}
/**
* Hide one or more alerts
* @param {Array|Array<Array>} rec - Single alert record or array of records to hide
* @param {Array<Array>} [recs] - Optional array of alert records (deprecated parameter style)
*/
function hide(rec, recs) {
if (Array.isArray(recs)) {
for (let r of recs) {
@ -36,6 +51,11 @@ function hide(rec, recs) {
}
}
/**
* Update the alert display by filtering expired/inactive alerts and rendering active ones.
* Filters alerts by age (based on duration) and active flag, limits display to 5 alerts.
* @param {boolean} [clear] - If true, clears all alerts before updating
*/
function update(clear) {
if (clear) {
alerts = [];

View file

@ -53,14 +53,19 @@ let LOC = self.location,
self.kiri_catalog = FILES;
FILES.show = () => modal.show('files');
// Broker compatibility patch
/**
* Broker compatibility patch - adds 'on' method as alias for subscribe
*/
EVENT.on = (topic, listener) => {
EVENT.subscribe(topic, listener);
return EVENT;
};
/** Busy state counter for tracking active operations */
let busyVal = 0,
/** Hover feature flag */
isHover = false,
/** Explicit undefined for object defaults */
undef = undefined;
// the big kahuna
@ -68,6 +73,10 @@ export const api = {
ajax,
beta,
alerts,
/**
* Busy state management for tracking active operations.
* Emits 'busy' events when state changes.
*/
busy: {
val() { return busyVal },
inc() { api.event.emit("busy", ++busyVal) },
@ -84,10 +93,19 @@ export const api = {
color,
conf: settings.conf,
const: { LANG, LOCAL, SETUP, SECURE, SPACE, STACKS, ...consts },
/**
* Development/debug utilities
*/
devel: {
get enabled() {
return settings.ctrl().devel;
},
/**
* Enable X-ray view of specific layers for debugging slicing.
* Converts layer indices to Z-heights and triggers re-slice.
* @param {number|number[]} layers - Layer index or array of indices to view
* @param {boolean} [raw] - If true, use raw values without height calculation
*/
xray(layers, raw) {
let proc = api.conf.get().process,
size = proc.sliceHeight || proc.slaSlice || 1,
@ -118,6 +136,10 @@ export const api = {
settings: settingsUI.trigger_event
},
electron: navigator.userAgent.includes('Electron'),
/**
* Feature flags and hook functions for customizing application behavior.
* Many of these are hook points for external integrations or plugins.
*/
feature: {
seed: true, // seed profiles on first use
meta: true, // show selected widget metadata
@ -136,7 +158,9 @@ export const api = {
on_mouse_down: undef, // function intercepts mouse down
work_alerts: true, // allow disabling work progress alerts
pmode: consts.PMODES.SPEED, // preview modes
// hover: false, // when true fires mouse hover events
/**
* Hover feature flag. Setting this publishes a "feature.hover" event.
*/
get hover() {
return isHover;
},
@ -202,7 +226,17 @@ export const api = {
download: utilModule.download,
ui2rec() { api.conf.update_from(...arguments) },
rec2ui() { api.conf.update_fields(...arguments) },
/**
* Encode object to base64 string via JSON serialization
* @param {*} obj - Object to encode
* @returns {string} Base64 encoded string
*/
b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
/**
* Decode base64 string to object via JSON parsing
* @param {string} obj - Base64 encoded string
* @returns {*} Decoded object
*/
b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
},
// var: {

View file

@ -92,7 +92,11 @@ function selectDevice(devicename) {
}
}
// only for local filters
/**
* Clone the current device to create a customizable local copy.
* Naming logic: if device already has "My" prefix, appends " copy", otherwise adds "My" prefix.
* Only works for local devices (not stock devices).
*/
function cloneDevice() {
let name = `${getSelectedDevice().replace(/\./g,' ')}`;
let code = api.clone(setconf.get().device);
@ -113,6 +117,18 @@ function updateLaserState() {
$('laser-off').style.display = dev.useLaser ? 'flex' : 'none';
}
/**
* Set device configuration from code and initialize device state.
* Complex initialization that:
* - Parses device code (string or object)
* - Fills missing device fields with defaults
* - Handles first-time device setup with profiles
* - Updates UI elements and platform configuration
* - Manages device/process associations
* - Emits device.select and device.selected events
* @param {string|object} code - Device configuration code or object
* @param {string} devicename - Name of the device being set
*/
function setDeviceCode(code, devicename) {
api.event.emit('device.select', devicename);
try {
@ -277,6 +293,12 @@ function setDeviceCode(code, devicename) {
api.event.settings();
}
/**
* Render device selection UI with both stock and custom devices.
* Builds device list dropdown, sets up event handlers for save/add/delete/rename/export,
* and separates local "My Devices" from "Stock Devices" in the UI.
* @param {string[]} devices - Array of stock device names for current mode
*/
function renderDevices(devices) {
let selected = api.device.get() || devices[0],
features = api.feature,

View file

@ -5,9 +5,19 @@ import { space } from '../../moto/space.js';
const { event } = api;
/**
* Undo/Redo action stack. Each entry contains {undo, redo} action records.
* @type {Array<{undo: object, redo: object}>}
*/
let stack = [];
/** Current position in the undo/redo stack */
let stpos = 0;
/** Accumulator for drag movements - tracks cumulative x,y offset during selection drag */
let moved = { x: 0, y: 0 };
/** Message ID for the current undo/redo alert message */
let msgid;
event.on("init-done", () => {
@ -31,6 +41,9 @@ let redo = api.doit.redo = function() {
}
};
/**
* Clear the entire undo/redo stack and reset moved accumulator
*/
let clear = api.doit.clear = function() {
stack = [];
stpos = 0;
@ -52,6 +65,13 @@ function message(txt) {
msgid = api.show.alert(txt);
}
/**
* Execute an action record (undo or redo).
* Dispatches based on action type: move, rotate, or scale.
* @param {object} rec - Action record with type and action-specific properties
* @param {string} rec.type - Action type: 'move', 'rotate', or 'scale'
* @param {Array} rec.widgets - Widgets to operate on
*/
function action(rec) {
switch (rec.type) {
case 'move':
@ -75,6 +95,11 @@ function action(rec) {
updateButtons();
}
/**
* Push an undo/redo action pair onto the stack.
* Truncates stack at current position (clearing any "future" when adding new action).
* @param {object} ur - Action pair with {undo, redo} records
*/
function pushActions(ur) {
stack.length = stpos++;
stack.push(ur);

View file

@ -10,6 +10,10 @@ import { LASER as laser_driver } from '../mode/laser/driver.js';
import { SLA as sla_client } from '../mode/sla/init-ui.js';
import { hash } from '../../ext/md5.js';
/**
* Sequential print counter for generating unique export filenames.
* Increments with each export and persists in local storage.
*/
let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1;
function localGet(key) {
@ -20,6 +24,12 @@ function localSet(key, val) {
return api.local.set(key, val);
}
/**
* Main export entry point. Dispatches to mode-specific export handlers.
* Extracts widget filenames to use for export filename suggestions.
* @param {object} options - Export options (mode-specific)
* @returns {*} Result from mode-specific export handler
*/
export function exportFile(options) {
let mode = api.mode.get();
let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
@ -39,6 +49,13 @@ export function exportFile(options) {
}
}
/**
* Export gcode for FDM or CAM modes.
* Calls worker to generate gcode, then presents dialog or invokes callback.
* @param {function} callback - Optional callback(gcode_string, output_info)
* @param {string} mode - Current mode ('FDM' or 'CAM')
* @param {string[]} names - Widget filenames for export naming
*/
function callExport(callback, mode, names) {
let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null;
let gcode = [];
@ -65,6 +82,12 @@ function callExport(callback, mode, names) {
});
}
/**
* Export laser/waterjet/drag knife toolpaths.
* Worker generates output, then presents export dialog with SVG/DXF/STL/GCode options.
* @param {object} options - Export options
* @param {string[]} names - Widget filenames for export naming
*/
function callExportLaser(options, names) {
client.export(api.conf.get(), (line) => {
// engine export uses lines
@ -79,6 +102,12 @@ function callExportLaser(options, names) {
});
}
/**
* Export SLA print data.
* Worker generates layer images, then delegates to SLA client for download.
* @param {object} options - Export options
* @param {string[]} names - Widget filenames for export naming
*/
function callExportSLA(options, names) {
client.export(api.conf.get(), (line) => {
api.show.progress(line.progress, "exporting");
@ -92,6 +121,12 @@ function callExportSLA(options, names) {
});
}
/**
* Present laser export dialog with format options (SVG, DXF, STL, GCode).
* Sets up UI handlers for downloading in each supported format.
* @param {Array} data - Layer data from export worker
* @param {string[]} names - Widget filenames for naming suggestion
*/
function exportLaserDialog(data, names) {
localSet('kiri-print-seq', printSeq++);
@ -147,6 +182,11 @@ function exportLaserDialog(data, names) {
$('print-lg').onclick = download_gcode;
}
/**
* Bind an input field to persist its value to local storage on blur.
* @param {string} field - Element ID of the input field
* @param {string} varname - Local storage key to save value under
*/
function bindField(field, varname) {
$(field).onblur = function() {
console.log('save', field, 'to', varname);
@ -154,6 +194,19 @@ function bindField(field, varname) {
};
}
/**
* Present gcode export dialog with download/send options.
* Complex UI setup that handles:
* - Local download of gcode, zip (CAM operations), or 3MF (FDM)
* - OctoPrint integration for remote printing
* - Bambu printer integration (via api.bambu)
* - Print statistics (time, filament, weight)
* - Gcode preview
* @param {string[]} gcode - Array of gcode lines
* @param {object} [sections] - CAM operation sections for zip export
* @param {object} info - Export metadata (time, distance, bytes, etc.)
* @param {string[]} names - Widget filenames for naming suggestion
*/
function exportGCodeDialog(gcode, sections, info, names) {
localSet('kiri-print-seq', printSeq++);
@ -336,6 +389,13 @@ function exportGCodeDialog(gcode, sections, info, names) {
// let bnds = settings.bounds;
// console.log({ wids, bnds });
/**
* Generate 3MF file for Bambu/BambuStudio printers.
* Creates ZIP archive with gcode, thumbnails, and metadata.
* @param {function} then - Callback to receive generated 3MF blob
* @param {string} [ptype='unknown'] - Printer type identifier
* @param {number[]} [ams=[0]] - AMS (filament) slot assignments
*/
function gen3mf(then, ptype = 'unknown', ams = [0]) {
let now = new Date();
let ymd = [

View file

@ -5,8 +5,16 @@ import { base } from '../../geo/base.js';
import { space } from '../../moto/space.js';
const DOC = self.document;
/** Cached platform color before drag-over highlight */
let platformColor;
/**
* Handle file drag-over event.
* Changes platform color to green to indicate drop target.
* Prevents drop when modal dialogs are open.
* @param {DragEvent} evt - Browser drag event
*/
function dragOverHandler(evt) {
evt.stopPropagation();
evt.preventDefault();
@ -21,10 +29,22 @@ function dragOverHandler(evt) {
if (oldcolor !== 0x00ff00) platformColor = oldcolor;
}
/**
* Handle drag-leave event by restoring original platform color.
*/
function dragLeave() {
space.platform.setColor(platformColor);
}
/**
* Handle file drop event.
* Restores platform color and loads dropped files.
* Handles multi-file drops with optional grouping:
* - Single file: loads immediately
* - Multiple files: prompts user whether to group
* - api.feature.drop_group can override behavior (true=always group, false=never group)
* @param {DragEvent} evt - Browser drop event
*/
function dropHandler(evt) {
evt.stopPropagation();
evt.preventDefault();
@ -54,6 +74,10 @@ function dropHandler(evt) {
}
}
/**
* Load a file from the catalog when clicked.
* @param {Event} e - Click event from catalog item
*/
function loadCatalogFile(e) {
api.widgets.load(e.target.getAttribute('load'), function(widget) {
api.platform.add(widget);
@ -61,6 +85,12 @@ function loadCatalogFile(e) {
});
}
/**
* Update the catalog UI with current file list.
* Builds interactive list with rename, load, and delete buttons for each file.
* Sorts files alphabetically (case-insensitive).
* @param {object} files - Dictionary of filename -> {vertices, updated} file metadata
*/
function updateCatalog(files) {
let table = api.ui.catalog.list,
list = [];

View file

@ -1,6 +1,15 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Manages file storage in IndexedDB for 3D model vertices.
* Supports file operations (put, get, delete, rename), listener notifications,
* and deferred loading for externally-stored files.
*/
class Files {
/**
* Create a new Files store
* @param {object} indexdb - IndexedDB wrapper instance
*/
constructor(indexdb) {
let store = this;
this.db = indexdb;
@ -47,10 +56,21 @@ class Files {
this.listeners.remove(listener);
};
/**
* Set handler for deferred file loading.
* Deferred files are not stored locally but fetched on-demand.
* @param {function} handler - Function(mark, name, callback) to load deferred file
*/
setDeferredHandler(handler) {
this.deferredHandler = handler;
};
/**
* Register a deferred file placeholder.
* File data is not stored locally; the mark is used by the deferred handler to fetch it later.
* @param {string} name - Filename
* @param {*} mark - Identifier for deferred handler to fetch file (e.g., URL, storage key)
*/
putDeferred(name, mark) {
// triggers refresh callback
this.files[name] = {
@ -91,6 +111,13 @@ class Files {
.catch(ondone);
};
/**
* Rename a file in the catalog.
* Copies vertex data to new key, updates file list, and removes old key.
* @param {string} name - Current filename
* @param {string} newname - New filename
* @param {function} callback - Function({error}) called when complete
*/
rename(name, newname, callback) {
if (!this.files[name]) return callback({error: 'no such file'});
if (!newname || newname == name) return callback({error: 'invalid new name'});
@ -146,6 +173,12 @@ class Files {
if (callback) callback(false);
};
/**
* Delete files matching a filter function.
* @param {function} fn - Filter function(key) returning true to delete
* @param {*} [from] - Optional start key for range query
* @param {*} [to] - Optional end key for range query
*/
deleteFilter(fn, from, to) {
this.db.keys(keys => {
for (let key of keys) {
@ -157,17 +190,30 @@ class Files {
}
}
/**
* Save file list to IndexedDB and notify listeners.
* @param {Files} store - Files instance
*/
function saveFileList(store) {
store.db.put('files', store.files);
notifyFileListeners(store);
}
/**
* Notify all registered listeners of file list changes.
* @param {Files} store - Files instance
*/
function notifyFileListeners(store) {
for (let i=0; i<store.listeners.length; i++) {
store.listeners[i](store.files);
}
}
/**
* Factory function to create a new Files instance.
* @param {object} indexdb - IndexedDB wrapper instance
* @returns {Files} New Files store
*/
export const openFiles = function(indexdb) {
return new Files(indexdb);
};

View file

@ -1,12 +1,38 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Frame Message API for iframe integration.
*
* Enables parent windows to control embedded Kiri:Moto instances via postMessage.
* Controlled by api.feature.frame flag.
*
* Supported message types:
* - mode: Set operating mode (FDM, CAM, SLA, LASER, etc.)
* - view: Set view mode (ARRANGE, SLICE, PREVIEW, etc.)
* - function: Call api.function methods (slice, print, export, etc.)
* - event: Subscribe to events
* - emit: Emit events
* - get: Query state (mode, device, process, widgets)
* - set: Update state
* - features: Update feature flags
* - device/process/controller: Update settings
* - parse: Parse and load file data (STL, OBJ, 3MF, SVG)
* - load: Load URL
* - clear: Clear platform
* - alert: Show alert
* - progress: Update progress bar
*/
import { api } from './api.js';
import { load } from '../../load/file.js';
import { newWidget } from '../core/widget.js';
import { VIEWS } from '../core/consts.js';
import { widgets } from '../core/widgets.js';
// add frame message api listener
/**
* Frame message API listener.
* Handles postMessage communication between parent window and embedded Kiri:Moto.
*/
window.addEventListener('message', msg => {
const { conf, event, feature, platform, settings, show } = api;

View file

@ -7,8 +7,26 @@ import { space } from '../../moto/space.js';
import { COLOR, PMODES } from '../core/consts.js';
import { exportFile } from './export.js';
/**
* Tracks completion state of operations to prevent redundant work.
* Properties: slice, preview, export
*/
let complete = {};
/**
* 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)
*/
function prepareSlices(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
@ -289,6 +307,17 @@ function prepareSlices(callback, scale = 1, offset = 0) {
sliceNext();
}
/**
* 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)
*/
function preparePreview(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
const widgets = api.widgets.all();
@ -427,6 +456,10 @@ function preparePreview(callback, scale = 1, offset = 0) {
});
}
/**
* Prepare animation (requires SharedArrayBuffer support).
* Checks for browser support and emits function.animate event.
*/
function prepareAnimation() {
if (!window.SharedArrayBuffer) {
api.alerts.show("The security context of this");
@ -437,6 +470,12 @@ function prepareAnimation() {
api.event.emit("function.animate", {mode: api.conf.get().mode});
}
/**
* 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()
*/
function prepareExport() {
const settings = api.conf.get();
const argsave = arguments;
@ -449,12 +488,23 @@ function prepareExport() {
exportFile(...argsave);
}
/**
* Cancel running worker operation by restarting the worker.
*/
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.)
*/
function parseCode(code, type) {
const { conf, event, show, stacks, widgets, view } = api;
const settings = conf.get();
@ -479,6 +529,10 @@ function parseCode(code, type) {
});
}
/**
* Clear operation completion tracking.
* Resets complete state so operations can run again.
*/
function clear_progress() {
complete = {};
}

View file

@ -3,10 +3,17 @@
import { selection } from './selected.js';
import { Widget } from '../core/widget.js';
/**
* Merge selected widgets into a group.
* Grouped widgets move together as a unit.
*/
function groupMerge() {
Widget.Groups.merge(selection.widgets(true));
}
/**
* Split grouped widgets back into individual widgets.
*/
function groupSplit() {
Widget.Groups.split(selection.widgets(false));
}

View file

@ -7,10 +7,18 @@ import { version } from '../../moto/license.js';
const WIN = self.window;
/**
* Show local help dialog.
*/
function showHelp() {
showHelpFile(`local`,() => {});
}
/**
* Show help dialog or open external docs.
* @param {string} local - If truthy, shows local help modal; otherwise opens docs.grid.space
* @param {function} then - Callback after help shown
*/
function showHelpFile(local,then) {
if (!local) {
WIN.open("//docs.grid.space/", "_help");

View file

@ -6,6 +6,14 @@ import { newWidget } from '../core/widget.js';
import { platform } from './platform.js';
import { settings } from './conf/manager.js';
/**
* Show image import dialog with conversion options.
* Prompts for blur, inversion, base size, and border settings.
* Large images (>2.5MB) show a warning before proceeding.
* @param {ArrayBuffer} image - PNG image data
* @param {string} name - Filename
* @param {boolean} [force] - Skip size warning if true
*/
function loadImageDialog(image, name, force) {
if (!force && image.byteLength > 2500000) {
return api.uc.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
@ -46,6 +54,12 @@ function loadImageDialog(image, name, force) {
});
}
/**
* Convert PNG image to 3D mesh using worker.
* Creates height map from pixel brightness and generates vertices.
* @param {ArrayBuffer} image - PNG image data
* @param {object} [opt={}] - Options: file, blur, base, border, inv_image, inv_alpha
*/
function loadImage(image, opt = {}) {
const info = Object.assign({settings: settings.get(), png:image}, opt);
api.client.image2mesh(info, progress => {
@ -58,7 +72,12 @@ function loadImage(image, opt = {}) {
});
}
// convert any image type to png
/**
* Convert any image format to PNG before loading.
* Uses canvas to convert image blob to PNG data URL.
* @param {ArrayBuffer} res - Image data in any format
* @param {string} name - Filename
*/
function loadImageConvert(res, name) {
let url = URL.createObjectURL(new Blob([res]));

View file

@ -1,31 +1,62 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* UI component factory system.
* Creates form elements, dialogs, and manages UI state/visibility.
* Supports mode-specific visibility, unit conversion, and hierarchical grouping.
*/
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
let DOC = self.document,
/** Callback for input changes */
inputAction = null,
/** Previous addTo value for nesting */
lastAddTo = null,
/** Current group array */
lastGroup = null,
/** Last created div container */
lastDiv = null,
/** Current container to add elements to */
addTo = null,
/** Alternative binding target for input actions */
bindTo = null,
/** Map of group name to array of elements */
groups = {},
/** Sticky state prevents auto-hide on blur */
groupSticky = false,
/** Current group name */
groupName = undefined,
heads = {}, // hideable group heads (clickable label)
hidden = {}, // hidden groups (by name)
/** Collapsible group headers (clickable label) */
heads = {},
/** Hidden groups by name */
hidden = {},
/** Elements with mode visibility rules */
hasModes = [],
/** Elements with unit conversion setters */
setters = [],
/** Last mode set for visibility filtering */
lastMode = null,
/** Last expert mode state */
lastExpert = true,
/** Prefix for element IDs */
prefix = "tab",
/** Unit scale multiplier for conversions */
units = 1,
/** Last changed input element */
lastChange = null,
/** Last clicked button */
lastBtn = null,
/** Last clicked text element */
lastTxt = null,
/** Last shown popup */
lastPop = null;
/**
* UI component factory and utilities.
* Provides chainable builder pattern and component creation functions.
*/
export const UI = {
prefix: function(pre) { prefix = pre; return UI },
inputAction: function(fn) { inputAction = fn; return UI },
@ -84,6 +115,10 @@ export const UI = {
}
};
/**
* Set hidden group map and refresh visibility.
* @param {object} map - Map of group name to hidden boolean
*/
function setHidden(map) {
hidden = map;
refresh();
@ -92,6 +127,11 @@ function setHidden(map) {
}
}
/**
* Attach blur event listener to element(s).
* @param {HTMLElement|Array<HTMLElement>} obj - Element or array of elements
* @param {function} fn - Blur handler function
*/
function onBlur(obj, fn) {
if (Array.isArray(obj)) {
for (let o of obj) onBlur(o, fn);
@ -100,14 +140,34 @@ function onBlur(obj, fn) {
obj.addEventListener('blur', fn);
}
/**
* Show alert dialog with OK button.
* @param {string} message - Alert message
* @returns {Promise<boolean>} Resolves when OK clicked
*/
function alert(message) {
return confirm(message, {ok:true});
}
/**
* Show prompt dialog with text input.
* @param {string} message - Prompt message
* @param {string} value - Default input value
* @returns {Promise<string>} Resolves with entered text or undefined if cancelled
*/
function prompt(message, value) {
return confirm(message, {ok:true, cancel:undefined}, value);
}
/**
* Show modal dialog with custom buttons and optional input.
* Blocks keyboard events while open.
* @param {string} message - Dialog message
* @param {object} buttons - Button labels mapped to return values (e.g., {yes: true, no: false})
* @param {string|Array<string>} [input] - Optional input field. Array creates textarea with lines.
* @param {object} [opt={}] - Options: {pre, post} for additional HTML content
* @returns {Promise} Resolves with button value or input value if provided
*/
function confirm(message, buttons, input, opt = {}) {
return new Promise((resolve, reject) => {
let { feature } = api;
@ -307,6 +367,12 @@ function addCollapsableElement(parent, options = {}) {
return row;
}
/**
* Create a value bounding function.
* @param {number} low - Minimum value
* @param {number} high - Maximum value
* @returns {function} Function that clamps value to [low, high] range
*/
function bound(low,high) {
return function(v) {
if (isNaN(v)) return low;
@ -314,6 +380,11 @@ function bound(low,high) {
};
}
/**
* Convert input value to integer.
* Bound to input element as `this`. Applies bounds and unit conversion if configured.
* @returns {number} Integer value
*/
function toInt() {
let nv = this.value !== '' ? parseInt(this.value) : null;
if (isNaN(nv)) nv = 0;
@ -325,6 +396,11 @@ function toInt() {
return nv;
}
/**
* Convert input value to float.
* Bound to input element as `this`. Applies bounds and unit conversion if configured.
* @returns {number} Float value
*/
function toFloat() {
let nv = this.value !== '' ? parseFloat(this.value) : null;
if (nv !== null && this.bound) nv = this.bound(nv);
@ -335,6 +411,11 @@ function toFloat() {
return nv;
}
/**
* Convert comma-separated input to float array.
* Bound to input element as `this`. Applies bounds to each value.
* @returns {Array<number>} Float array
*/
function toFloatArray() {
let nv = this.value !== '' ? this.value.split(',').map(v => parseFloat(v)) : null;
console.log({ toFloatArray: nv });
@ -343,6 +424,11 @@ function toFloatArray() {
return nv;
}
/**
* Convert input value to degrees float (0-360).
* Bound to input element as `this`. Normalizes to 0-359.99 range.
* @returns {number} Degrees float
*/
function toDegsFloat(){
let nv = this.value !== '' ? parseFloat(this.value) : null;
if (nv !== null && this.bound) nv = this.bound(nv);
@ -375,6 +461,12 @@ function newLabel(text, opt = {}) {
return label;
}
/**
* Create a read-only value display element.
* @param {number} [size=6] - Input size attribute
* @param {object} [opt={}] - Options: {class}
* @returns {HTMLInputElement} Read-only input element
*/
function newValue(size = 6, opt = {}) {
let value = DOC.createElement('input');
value.setAttribute("size", size);
@ -452,6 +544,13 @@ function newDiv(opt = {}) {
return div;
}
/**
* Create a collapsible details/summary element.
* Changes addTo context to the details element until endExpand() called.
* @param {string} label - Summary label text
* @param {object} [opt={}] - Options: {class, open, modes, show, etc.}
* @returns {HTMLElement} Details element with collapse() method
*/
function newExpand(label, opt = {}) {
let div = DOC.createElement('details');
div.setAttribute('class', opt.class || 'f-col');
@ -474,6 +573,10 @@ function newExpand(label, opt = {}) {
return div;
}
/**
* End expand context, restoring previous addTo.
* @returns {HTMLElement} Restored addTo element
*/
function endExpand() {
addTo = lastAddTo;
return addTo;
@ -612,6 +715,15 @@ function newText(label, options) {
return txt;
}
/**
* Create a labeled input field with automatic validation and unit conversion.
* Supports text input or textarea (if height > 1).
* Filters non-numeric keys unless opt.text=true.
* Triggers action on blur.
* @param {string} label - Input label text
* @param {object} [opt={}] - Options: {size, height, action, convert, bound, units, round, text, comma, disabled, title, id, trigger, hide}
* @returns {HTMLInputElement|HTMLTextAreaElement} Input element with setVisible() method
*/
function newInput(label, opt = {}) {
let row = newDiv(opt),
hide = opt.hide,
@ -706,6 +818,12 @@ function addUnits(input, round) {
return input;
}
/**
* Create a labeled range slider.
* @param {string} label - Slider label text
* @param {object} options - Options: {min, max, hide, title, action, modes, etc.}
* @returns {HTMLInputElement} Range input with setVisible() method
*/
function newRange(label, options) {
let row = newDiv(options),
ip = DOC.createElement('input'),
@ -732,6 +850,14 @@ function newRange(label, options) {
return ip;
}
/**
* Create a labeled select dropdown.
* Triggers action on change.
* @param {string} label - Select label text
* @param {object} [options={}] - Options: {hide, id, convert, disabled, title, action, trigger, post, modes, etc.}
* @param {Array|string} source - Option source array or source attribute value
* @returns {HTMLSelectElement} Select element with setVisible() method
*/
function newSelect(label, options = {}, source) {
let row = newDiv(options),
ip = DOC.createElement('select'),
@ -774,6 +900,14 @@ function newSelect(label, options = {}, source) {
return ip;
}
/**
* Create a labeled checkbox.
* Triggers action on click.
* @param {string} label - Checkbox label text
* @param {function} [action=bindTo] - Click handler function
* @param {object} [opt={}] - Options: {hide, disabled, title, trigger, modes, etc.}
* @returns {HTMLInputElement} Checkbox element with setVisible() method
*/
function newBoolean(label, action = bindTo, opt = {}) {
let row = newDiv(opt),
ip = DOC.createElement('input'),
@ -828,7 +962,15 @@ function newBlank(options) {
return row;
}
// unlike other elements, does not auto-add to a row
/**
* Create a button element.
* Unlike other elements, does not auto-add to a row.
* Action can be a function or event name string.
* @param {string} label - Button label text
* @param {function|string} action - Click handler or event name to emit
* @param {object} [opt={}] - Options: {class, icon, title, id, modes, etc.}
* @returns {HTMLButtonElement} Button element with mode visibility
*/
function newButton(label, action, opt = {}) {
let b = DOC.createElement('button');
@ -866,6 +1008,12 @@ function newButton(label, action, opt = {}) {
return b;
}
/**
* Create a row container with child elements.
* @param {Array<HTMLElement>} children - Child elements to append
* @param {object} options - Options: {class, noadd, modes, etc.}
* @returns {HTMLElement} Row element
*/
function newRow(children, options) {
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
if (children) children.forEach(function (c) { row.appendChild(c) });

View file

@ -84,6 +84,10 @@ class KeyboardControl {
return c.charCodeAt(0);
}
/**
* Bind keyboard event handlers to space.event
* @private
*/
#bindEvents() {
this.#space.event.addHandlers(self, [
'keyup', this.#handleKeyUp.bind(this),
@ -92,6 +96,13 @@ class KeyboardControl {
]);
}
/**
* Handle keyup events.
* Primarily handles Escape key for dismissing modals/dialogs/selections.
* @private
* @param {KeyboardEvent} evt - Keyboard event
* @returns {boolean} False to prevent default
*/
#handleKeyUp(evt) {
// Allow feature hooks to intercept
if (this.#api.feature.on_key) {
@ -121,6 +132,14 @@ class KeyboardControl {
return false;
}
/**
* Handle keydown events.
* Handles arrow keys for movement/rotation, delete key, and Cmd/Ctrl shortcuts.
* Arrow keys: rotate (default) or move (with Alt). Shift = smaller rotation.
* Meta + Up/Down: navigate layers.
* @private
* @param {KeyboardEvent} evt - Keyboard event
*/
#handleKeyDown(evt) {
if (this.#api.modal.visible()) {
return false;
@ -205,6 +224,21 @@ class KeyboardControl {
}
}
/**
* Handle keypress events.
* Main keyboard shortcuts for application commands:
* - Numbers 0-9: show slice layers (0=all, 1-9=10-90%)
* - s/S: slice, p/P: prepare, x/X: export
* - a: arrange/layout, v: toggle single slice view
* - d: duplicate, m: mirror
* - i: import, r: recent files
* - e: devices, o: tools, l: load settings
* - Z: clear all settings, C: refresh catalog
* - Ctrl+g: group, Ctrl+u: ungroup
* @private
* @param {KeyboardEvent} evt - Keyboard event
* @returns {boolean} False to prevent default
*/
#handleKeyPress(evt) {
let handled = true;
if (this.#api.modal.visible() || this.inputHasFocus()) {

View file

@ -1,10 +1,23 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Global language/translation system.
* Stores language maps and tracks the current language selection.
*/
const LANG = self.lang = { current: {} };
/** Default language code */
const KDFL = 'en-us';
/** Currently selected language code from browser */
let lset = navigator.language.toLocaleLowerCase();
/**
* Map language codes to supported language identifiers.
* Normalizes short codes (en, fr, de) to full language codes (en-us, fr-fr, de-de).
* @param {string} key - Language code (e.g., 'en', 'fr', 'de-de')
* @returns {string} Normalized language code or default 'en-us'
*/
LANG.map = function(key) {
if (!key) {
return KDFL;
@ -23,10 +36,21 @@ LANG.map = function(key) {
return KDFL;
};
/**
* Get current browser language setting.
* @returns {string} Browser language code
*/
LANG.get = function() {
return lset;
};
/**
* Set language by trying a list of language codes in order.
* Falls back to browser language, then English if no arguments provided.
* Populates LANG.current with translations, filling missing keys from English.
* @param {...string} keys - Language codes to try in order
* @returns {string|undefined} Selected language code or undefined if none found
*/
LANG.set = function() {
let map, key, keys = [...arguments];
// provide default if none given

View file

@ -3,11 +3,19 @@
import { newPolygon, Polygon } from '../../geo/polygon.js';
import { polygons as POLY } from '../../geo/polygons.js';
/**
* Layer management system for 3D visualization.
* Organizes geometry (lines, polygons, faces, paths) by layer with color/opacity.
* Supports multiple rendering styles: basic lines, webgl lines, filled areas, 3D extrusion paths.
*/
export class Layers {
constructor() {
this.init();
}
/**
* Initialize or reset layers, profiles, and statistics.
*/
init() {
this.layers = {};
this.profiles = {};
@ -20,21 +28,47 @@ export class Layers {
};
}
/**
* Get layer data by layer index.
* @param {number} layer - Layer index
* @returns {object} Layer data or undefined
*/
getLayer(layer) {
return this.layers[layer];
}
// in radians
/**
* Set rotation for subsequent layers.
* @param {number} [x=0] - X rotation in radians
* @param {number} [y=0] - Y rotation in radians
* @param {number} [z=0] - Z rotation in radians
* @returns {Layers} This instance for chaining
*/
setRotation(x = 0, y = 0, z = 0) {
this.rotation = { x, y, z };
return this;
}
/**
* Set position offset for subsequent layers.
* @param {number} [x=0] - X position
* @param {number} [y=0] - Y position
* @param {number} [z=0] - Z position
* @returns {Layers} This instance for chaining
*/
setPosition(x = 0, y = 0, z = 0) {
this.position = { x, y, z };
return this;
}
/**
* Set current layer for subsequent geometry additions.
* Creates layer if it doesn't exist.
* @param {number} layer - Layer index
* @param {number|object} colors - Single color number or {line, face, opacity}
* @param {boolean} [off] - If true, marks layer as off/hidden
* @returns {Layers} This instance for chaining
*/
setLayer(layer, colors, off) {
let layers = this.layers;
if (typeof(colors) === 'number') {
@ -65,13 +99,24 @@ export class Layers {
return this;
}
// add a line segment (two points)
/**
* Add a single line segment to current layer.
* @param {object} p1 - First point {x, y, z}
* @param {object} p2 - Second point {x, y, z}
* @returns {Layers} This instance for chaining
*/
addLine(p1, p2) {
this.current.lines.push(p1, p2);
return this;
}
// add an array of line segments
/**
* Add multiple line segments to current layer.
* If options provided, converts to open polygons via addPolys.
* @param {Array} lines - Array of points (alternating pairs for line segments)
* @param {object} [options] - If provided, creates open polygons instead
* @returns {Layers} This instance for chaining
*/
addLines(lines, options) {
if (options) {
// the open option encodes lines as open polygons
@ -92,12 +137,23 @@ export class Layers {
return this;
}
// an open or closed polygon
/**
* Add a single polygon to current layer.
* @param {Polygon} poly - Polygon to add
* @param {object} [options] - Rendering options
* @returns {Layers} This instance for chaining
*/
addPoly(poly, options) {
return this.addPolys([ poly ], options);
}
// a polygon rendered as a webgl line
/**
* Add polygons rendered as WebGL lines.
* Dispatches to addFlats or addPaths based on options.
* @param {Array<Polygon>} polys - Array of polygons
* @param {object} [options] - Options: {clean, flat, thin, z, color}
* @returns {Layers} This instance for chaining
*/
addPolys(polys, options) {
if (!polys) {
return this;
@ -134,8 +190,13 @@ export class Layers {
return this;
}
// add an enclosed 3D polygon earcut into faces
// used for FDM solids, bridges, flats debug, CAM hole generation & SLA slice visualization
/**
* Add enclosed 3D polygon earcut into triangular faces.
* Used for FDM solids, bridges, flats debug, CAM hole generation, SLA slice visualization.
* @param {Polygon|Array<Polygon>} polys - Polygon(s) to tessellate
* @param {object} [options] - Options: {outline} to also draw polygon outline
* @returns {Layers} This instance for chaining
*/
addAreas(polys, options) {
const faces = this.current.faces;
polys = Array.isArray(polys) ? polys : [ polys ];
@ -151,7 +212,13 @@ export class Layers {
}
}
// add a 2D polyline path (usually FDM extrusion paths)
/**
* Add 2D polyline paths with width (usually FDM extrusion paths).
* Creates flat ribbons with vertex normals for lighting.
* @param {Array<Polygon>} polys - Polygons to render as flat ribbons
* @param {object} [options] - Options: {offset, outline, color}
* @returns {Layers} This instance for chaining
*/
addFlats(polys, options) {
const opts = options || {};
const offset = opts.offset || 1;
@ -200,7 +267,14 @@ export class Layers {
return this;
}
// add 3D polyline path (FDM extrusion paths)
/**
* Add 3D polyline paths with extrusion (FDM extrusion paths).
* Creates 3D tubes with vertex normals for lighting.
* Supports incremental merging and per-segment color changes.
* @param {Array<Polygon>} polys - Polygons to render as 3D tubes
* @param {object} [options] - Options: {height, offset, z, color}
* @returns {Layers} This instance for chaining
*/
addPaths(polys, options) {
const opts = options || {};
const height = opts.height || 1;
@ -259,6 +333,12 @@ export class Layers {
}
}
/**
* Flatten polygon array or single polygon for rendering.
* Clones and flattens nested polygon structures.
* @param {Polygon|Array<Polygon>} polys - Polygon(s) to flatten
* @returns {Array<Polygon>} Flattened polygon array
*/
function flat(polys) {
if (Array.isArray(polys)) {
return POLY.flatten(polys.clone(true), [], true);

View file

@ -11,20 +11,49 @@ import { settings } from './conf/manager.js';
const { MODES, VIEWS } = consts;
const clone = Object.clone;
/** Current operating mode ID */
let MODE = MODES.FDM;
/**
* Get current mode name as string.
* @returns {string} Mode name (FDM, CAM, SLA, LASER, etc.)
*/
function getMode() {
return settings.mode();
}
/**
* Get current mode name in lowercase.
* @returns {string} Lowercase mode name (fdm, cam, sla, etc.)
*/
function getModeLower() {
return getMode().toLowerCase();
}
/**
* Switch to a different operating mode.
* Convenience wrapper around setMode() that updates platform size after switch.
* @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM)
*/
function switchMode(mode) {
setMode(mode, null, platform.update_size);
}
/**
* Set operating mode with full initialization.
* Complex function that:
* - Validates and sets mode constant
* - Updates UI to show/hide mode-specific controls
* - Restores cached device for the mode
* - Resets view to ARRANGE
* - Saves settings
* - Updates platform and selection
* - Emits mode.set event
*
* @param {string} mode - Target mode name (FDM, CAM, SLA, LASER, DRAG, WJET, WEDM)
* @param {*} lock - Currently unused parameter
* @param {function} [then] - Optional callback after mode set complete
*/
function setMode(mode, lock, then) {
if (!MODES[mode]) {
console.log("invalid mode: "+mode);
@ -75,10 +104,18 @@ function setMode(mode, lock, then) {
}
}
/**
* Get the name of the current process profile for this mode.
* @returns {string} Process profile name
*/
function currentProcessName() {
return settings.get().cproc[getMode()];
}
/**
* Get the process configuration object for the current mode and profile.
* @returns {object} Process configuration
*/
function currentProcessCode() {
return settings.get().sproc[getMode()][currentProcessName()];
}

View file

@ -61,6 +61,11 @@ class InteractionControl {
this.#initialized = true;
}
/**
* Bind hover event handlers for mouse and platform interactions.
* Listens to "feature.hover" event to enable/disable hover functionality.
* @private
*/
#bindHoverHandlers() {
// Set up hover handlers
this.#api.event.on("feature.hover", enable => {
@ -69,17 +74,40 @@ class InteractionControl {
});
}
/**
* Handle mouse hover over widgets.
* Returns widget meshes if no intersection, otherwise triggers hover callback.
* @private
* @param {object} int - Intersection data
* @param {Event} event - Mouse event
* @param {Array} ints - All intersections
*/
#mouseOnHover(int, event, ints) {
if (!this.#api.feature.hover) return;
if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes();
this.#onHover?.({int, ints, event, point: int.point, type: 'widget'});
}
/**
* Handle mouse hover over platform.
* Triggers hover callback with platform intersection point.
* @private
* @param {object} int - Intersection point
* @param {Event} event - Mouse event
*/
#platformOnHover(int, event) {
if (!this.#api.feature.hover) return;
if (int) this.#onHover?.({point: int, event, type: 'platform'});
}
/**
* Bind mouse click and drag handlers.
* Handles:
* - Mouse down: lay-flat (Ctrl/Cmd+click), custom hooks, hover mode
* - Mouse up: widget selection/deselection, custom hooks
* - Drag: move selected widgets with boundary checking
* @private
*/
#bindMouseHandlers() {
// Mouse down handler
this.#space.mouse.downSelect((int, event) => {

View file

@ -13,18 +13,42 @@ import { Widget, newWidget } from '../core/widget.js';
const V0 = new THREE.Vector3(0,0,0);
/** Bounds update callback */
let setbounds = undefined;
/** Flag indicating if grouping operation is in progress */
let grouping = false;
/** Maximum Z height of all widgets on platform */
let topZ = 0;
/**
* Get current settings object.
* @returns {object} Current configuration
*/
function current() {
return api.conf.get();
}
/**
* Get current mode ID constant.
* @returns {number} Mode ID
*/
function get_mode() {
return api.mode.get_id();
}
/**
* Update platform origin position and rulers.
* Calculates origin based on mode, device, and process settings:
* - FDM: Corner or center based on device.originCenter/bedRound
* - CAM: Relative to stock, with optional top origin
* - 2D modes (LASER, DRAG, WJET, WEDM): Corner, center, or bounds-based
* - Belt: Special Y offset handling
* Applies mode-specific origin offsets and updates visual rulers.
*
* @param {boolean} [update_bounds=true] - Whether to recalculate bounds first
*/
function update_origin(update_bounds = true) {
if (update_bounds) {
platform.update_bounds();
@ -105,6 +129,13 @@ function update_origin(update_bounds = true) {
}
}
/**
* Update platform size and visual appearance.
* Sets platform dimensions from device settings, updates grid/rulers,
* and applies dark mode styling if enabled.
*
* @param {boolean} [updateDark=true] - Whether to update dark mode styling
*/
function update_size(updateDark = true) {
const { process, device, controller } = current();
const { showRulers, units } = controller;
@ -145,6 +176,10 @@ function update_size(updateDark = true) {
platform.update_origin();
}
/**
* Update topZ to track maximum Z height of all widgets.
* Used for belt mode and other Z-dependent calculations.
*/
function platformUpdateMidZ() {
topZ = 0;
api.widgets.each(widget => {
@ -153,6 +188,11 @@ function platformUpdateMidZ() {
space.platform.setMaxZ(topZ);
}
/**
* Update widget Z positioning based on CAM anchoring settings.
* In CAM mode, anchors widgets to top/middle/bottom of stock with offset.
* In other modes, resets topZ to 0.
*/
function update_top_z() {
const { process, stock } = current();
const MODE = get_mode();
@ -182,6 +222,13 @@ function update_top_z() {
});
}
/**
* Calculate and update stock dimensions for CAM mode.
* Stock dimensions can be absolute or relative (offset from bounds).
* Falls back to offset mode if any stock dimension is 0.
* Calculates stock center point for origin calculations.
* In non-CAM modes, clears stock object.
*/
function platformUpdateStock() {
const settings = current();
const { bounds, process, mode } = settings;
@ -208,11 +255,22 @@ function platformUpdateStock() {
}
}
/**
* Set explicit platform bounds and trigger update.
* @param {THREE.Box3} bounds - Bounding box to set
* @returns {THREE.Box3} Updated bounds
*/
function set_bounds(bounds) {
setbounds = bounds;
return update_bounds();
}
/**
* Calculate platform bounds from all widgets or use explicit bounds.
* Unions all widget bounding boxes translated by their positions.
* Updates stock, top Z, midZ, and origin after calculation.
* @returns {THREE.Box3} Calculated or explicit bounds
*/
function update_bounds() {
const bounds = setbounds || new THREE.Box3();
if (!setbounds)
@ -236,10 +294,19 @@ function update_bounds() {
return bounds;
}
/**
* Get count of selected widgets in arrange view.
* @returns {number} Selection count (0 if not in arrange view)
*/
function selected_count() {
return api.view.is_arrange() ? api.selection.count() : 0;
}
/**
* Update visual appearance of selected widgets.
* Sets selected color and highlights extruder buttons for FDM multi-extruder devices.
* Saves widget state after selection.
*/
function update_selected() {
const settings = current();
@ -266,6 +333,15 @@ function update_selected() {
}
}
/**
* Select a widget on the platform.
* Only works in arrange view. Handles group selection recursively.
* With shift key, toggles selection. Without shift, replaces selection.
* Emits 'widget.select' event and updates UI.
* @param {Widget} widget - Widget to select
* @param {boolean} shift - Whether shift key is pressed (multi-select)
* @param {boolean} [recurse=true] - Whether to recursively select group members
*/
function select(widget, shift, recurse = true) {
const { event, selection, view } = api;
@ -307,6 +383,14 @@ function select(widget, shift, recurse = true) {
space.update();
}
/**
* Deselect widget(s) on the platform.
* Only works in arrange view. Handles group deselection recursively.
* If no widget provided, deselects all widgets.
* Emits 'widget.deselect' event and updates UI.
* @param {Widget} [widget] - Widget to deselect, or undefined to deselect all
* @param {boolean} [recurse=true] - Whether to recursively deselect group members
*/
function deselect(widget, recurse = true) {
const { selection, view } = api;
@ -342,6 +426,12 @@ function deselect(widget, recurse = true) {
space.update();
}
/**
* Load mesh from URL (.stl or raw vertex data).
* Detects file type and delegates to load_stl or ajax loader.
* @param {string} url - URL to load from
* @param {Function} [onload] - Callback(vertices, widget) on load complete
*/
function load(url, onload) {
if (url.toLowerCase().indexOf(".stl") > 0) {
platform.load_stl(url, onload);
@ -356,6 +446,14 @@ function load(url, onload) {
}
}
/**
* Load STL file from URL and add to platform.
* @param {string} url - URL to STL file
* @param {Function} [onload] - Callback(vertices, widget) on load complete
* @param {FormData} [formdata] - Optional form data for POST request
* @param {boolean} [credentials] - Include credentials in request
* @param {object} [headers] - Additional HTTP headers
*/
function load_stl(url, onload, formdata, credentials, headers) {
new file_load.STL().load(url, (vertices, filename) => {
if (vertices) {
@ -369,6 +467,13 @@ function load_stl(url, onload, formdata, credentials, headers) {
}, formdata, 1 / api.view.unit_scale(), credentials, headers);
}
/**
* Load mesh(es) from URL via file loader.
* Supports multiple file formats. Groups loading to prevent intermediate layouts.
* Emits 'load.url' event with loaded widgets.
* @param {string} url - URL to load from
* @param {object} [options={}] - Load options including optional group array
*/
function load_url(url, options = {}) {
platform.group();
file_load.URL.load(url, options).then(objects => {
@ -390,11 +495,20 @@ function load_url(url, options = {}) {
});
}
/**
* Begin group loading mode.
* Defers layout and group position updates until group_done() is called.
*/
function group() {
grouping = true;
}
// called after all new widgets are loaded to update group positions
/**
* Complete group loading and finalize widget positions.
* Called after all widgets in a group are loaded.
* Triggers layout if drop_layout feature is enabled.
* @param {boolean} [skipLayout] - Whether to skip auto-layout
*/
function group_done(skipLayout) {
grouping = false;
Widget.Groups.loadDone();
@ -403,9 +517,23 @@ function group_done(skipLayout) {
}
}
/** Deferred widget additions pending batch processing */
let deferred = [];
/** Timeout handle for deferred widget batch processing */
let deferTimeout;
/**
* Add widget to platform and 3D scene.
* Can defer addition for batch processing to improve performance.
* Initializes widget annotation (extruder assignment).
* Updates bounds, triggers save, and positions widget if autoLayout is disabled.
* Emits 'widget.add' event.
* @param {Widget} widget - Widget to add
* @param {boolean} shift - Whether to multi-select (shift key pressed)
* @param {boolean} nolayout - Skip layout/positioning
* @param {boolean} defer - Batch multiple adds for better performance
*/
function add(widget, shift, nolayout, defer) {
api.widgets.add(widget);
space.world.add(widget.mesh);
@ -433,6 +561,12 @@ function add(widget, shift, nolayout, defer) {
}
}
/**
* Process batch of deferred widget additions.
* Called after 150ms timeout to batch multiple rapid additions.
* Positions widgets if autoLayout is disabled, then completes grouping.
* @private
*/
function platformAddDeferred() {
let skiplayout = false;
for (let rec of deferred) {
@ -453,6 +587,14 @@ function platformAddDeferred() {
deferred = [];
}
/**
* Find non-colliding position for newly added widget.
* Uses spiral search pattern radiating from center (0,0).
* Tests 360 positions at each radius (10, 20, 30...200mm).
* Skips if this is the first widget on platform.
* @param {Widget} widget - Widget to position
* @private
*/
function positionNewWidget(widget) {
if (api.widgets.count() <= 1) {
return;
@ -501,6 +643,15 @@ function positionNewWidget(widget) {
}
}
/**
* Delete widget(s) from platform.
* Handles single widget, array of widgets, or widget records.
* Removes from API widget collection, selection, groups, and 3D scene.
* Can defer post-processing for batch deletions.
* Emits 'widget.delete' event.
* @param {Widget|Array<Widget>} widget - Widget(s) to delete
* @param {boolean} [defer] - Skip post-processing (for batch operations)
*/
function platformDelete(widget, defer) {
if (!widget) {
return;
@ -525,6 +676,11 @@ function platformDelete(widget, defer) {
}
}
/**
* Post-deletion cleanup and updates.
* Updates slider, bounds, layout, selection, triggers save.
* @private
*/
function delete_post() {
api.view.update_slider_max();
platform.update_bounds();
@ -540,7 +696,11 @@ function delete_post() {
changed();
}
// render list of current widgets
/**
* Render widget list UI with action buttons.
* Creates interactive widget cards with save/rename/replace/disable/delete buttons.
* Highlights widgets on hover. Updates when widgets are added/removed/modified.
*/
function changed() {
h.bind($('ws-widgets'), api.widgets.all().map(w => {
let color;
@ -627,12 +787,36 @@ function changed() {
}));
}
/**
* Select all widgets on platform.
* Adds each widget to selection without recursing into groups.
*/
function select_all() {
api.widgets.each(widget => {
platform.select(widget, true, false)
});
}
/**
* Arrange widgets on platform using automatic layout.
* Behavior varies by mode:
* - FDM: 2D bin packing with support spacing. Belt mode uses linear Y layout with optional X randomization.
* - SLA: 2D bin packing with support spacing
* - CAM/LASER: 2D bin packing with configurable tile spacing
*
* Belt mode special handling:
* - Positions widgets linearly along Y axis
* - Adds belt lead spacing
* - Optional X randomization for better adhesion
* - Auto-expands bed depth if needed
*
* Standard mode:
* - Uses 2D bin packing algorithm (Packer class)
* - Grows packing area by 10% if widgets don't fit
* - Centers packed layout on platform
*
* Emits 'platform.layout' event when complete.
*/
function layout() {
const MODE = get_mode();
const settings = current();
@ -736,6 +920,14 @@ function layout() {
api.event.emit('platform.layout');
}
/**
* Create widget from vertex data and add to platform.
* Optionally saves to catalog and adds to group.
* @param {Array} [group] - Optional group array for grouping multiple widgets
* @param {Float32Array|Array} vertices - Vertex data (converted to Float32Array if needed)
* @param {string} [filename] - Optional filename for metadata and catalog
* @returns {Widget} Created widget
*/
function load_verts(group, vertices, filename) {
const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true);
widget.meta.file = filename;
@ -744,6 +936,15 @@ function load_verts(group, vertices, filename) {
return widget;
}
/**
* Load multiple files from File objects.
* Supports: STL, OBJ, 3MF, SVG, PNG, JPG, KMZ, Gerber (.gbr), gcode, raw vertex data.
* Also handles settings import (.b64, .km, .ini files).
* Groups loading to prevent intermediate layouts.
* Prompts user for grouping when multiple objects detected.
* @param {Array<File>} files - Array of File objects from file input or drag/drop
* @param {Array} [group] - Optional group array for grouping loaded widgets
*/
function load_files(files, group) {
platform.group();
let loading = files.length;
@ -868,6 +1069,12 @@ function load_files(files, group) {
}
}
/**
* Show dialog to configure SVG import settings.
* Prompts for extrusion depth, arc resolution, DPI, and nesting.
* @param {Function} doit - Callback with options: {soup, resolution, segmin, depth, dpi}
* @private
*/
function loadSVGDialog(doit) {
const opt = {pre: [
"<div class='f-col a-center'>",
@ -895,7 +1102,12 @@ function loadSVGDialog(doit) {
});
}
// resize platform (expand) to fit widgets (belt mode)
/**
* Expand platform bed depth to fit widgets (belt mode only).
* Finds maximum Y dimension of all widgets and expands bed if needed.
* Adds 10mm padding. Saves original bed depth.
* @returns {boolean} True if bed was expanded
*/
function fit() {
let maxy = 0;
api.widgets.each(widget => {

View file

@ -7,12 +7,21 @@ import { tool as MeshTool } from '../../mesh/tool.js';
import { encode as objEncode } from '../../load/obj.js';
import { encode as stlEncode } from '../../load/stl.js';
/**
* Array of currently selected widget meshes.
* @type {Array}
*/
const selectedMeshes = [];
function updateTool(ev) {
api.tool.update(ev);
}
/**
* Execute function for each unique group represented in selection.
* Passes the first widget from each group to the function.
* @param {function} fn - Function(widget) to execute for each group
*/
function for_groups(fn) {
let groups = widgets(true).map(w => w.group).uniq();
for (let group of groups) {
@ -20,6 +29,12 @@ function for_groups(fn) {
}
}
/**
* Execute function for each selected widget.
* If nothing selected, defaults to all widgets unless noauto=true.
* @param {function} fn - Function(widget) to execute
* @param {boolean} [noauto] - If true, don't auto-select all widgets when none selected
*/
function for_widgets(fn, noauto) {
let m = selectedMeshes;
let w = api.widgets.all();
@ -29,16 +44,32 @@ function for_widgets(fn, noauto) {
m.slice().forEach(mesh => { fn(mesh.widget) });
}
/**
* Execute function for all widgets with selection status.
* @param {function} fn - Function(widget, is_selected) to execute
*/
function for_status(fn) {
api.widgets.all().forEach(w => {
fn(w, selectedMeshes.contains(w.mesh));
});
}
/**
* Execute function for each selected mesh.
* @param {function} fn - Function(mesh) to execute
*/
function for_meshes(fn) {
selectedMeshes.slice().forEach(mesh => { fn(mesh) });
}
/**
* Move selected widget groups.
* Only works in ARRANGE view.
* @param {number} x - X offset
* @param {number} y - Y offset
* @param {number} z - Z offset
* @param {boolean} [abs] - If true, use absolute positioning instead of relative
*/
function move(x, y, z, abs) {
if (!api.view.is_arrange()) {
return;
@ -51,6 +82,10 @@ function move(x, y, z, abs) {
api.space.auto_save();
}
/**
* Compute bounding box of all selected meshes.
* @returns {THREE.Box3} Bounding box
*/
function get_bounds() {
// Helper to compute bounds of selected meshes
const THREE = self.THREE;
@ -65,6 +100,10 @@ function get_bounds() {
return bounds;
}
/**
* Update UI size/scale info fields based on selected mesh bounds.
* Calculates combined bounding box and displays dimensions/scale in UI.
*/
function update_info() {
const ui = api.ui;
let bounds = new THREE.Box3(), track;
@ -100,6 +139,12 @@ function update_info() {
update_bounds();
}
/**
* Update platform bounds and constrain widgets to bed limits.
* For belt mode, fits widgets and updates origin.
* Automatically moves widgets back into bounds if they exceed bed size.
* @param {Array<Widget>} [widgets] - Widgets to bound. Defaults to selected widgets.
*/
function update_bounds(widgets) {
const settings = api.conf.get();
// update bounds on selection for drag limiting
@ -149,6 +194,11 @@ function update_bounds(widgets) {
settings.bounds_sel = bounds_sel;
}
/**
* Duplicate selected widgets with offset.
* Only works in ARRANGE view.
* New widgets offset by bounding box width + 1 in X direction.
*/
function duplicate() {
if (!api.view.is_arrange()) {
return;
@ -167,6 +217,10 @@ function duplicate() {
});
}
/**
* Mirror selected widgets along X axis.
* Only works in ARRANGE view.
*/
function mirror() {
if (!api.view.is_arrange()) {
return;
@ -178,6 +232,15 @@ function mirror() {
api.space.auto_save();
}
/**
* Scale selected widget groups.
* Only works in ARRANGE view.
* Updates bounds, saves workspace, refreshes info and display.
* @param {number} x - X scale factor
* @param {number} y - Y scale factor
* @param {number} z - Z scale factor
* @param {boolean} [last] - If strictly false, skips auto-save
*/
function scale() {
if (!api.view.is_arrange()) {
return;
@ -196,6 +259,14 @@ function scale() {
space.update();
}
/**
* Rotate selected widget groups.
* Only works in ARRANGE view.
* Updates bounds, saves workspace, refreshes info and display.
* @param {number} x - X rotation in radians
* @param {number} y - Y rotation in radians
* @param {number} z - Z rotation in radians
*/
function rotate(x, y, z) {
if (!api.view.is_arrange()) {
return;
@ -209,6 +280,12 @@ function rotate(x, y, z) {
space.update();
}
/**
* Merge selected widgets into a single widget.
* Combines all vertex data from selected widgets into one mesh.
* @param {object} options - Options
* @param {boolean} [options.deleteMerged=true] - If true, deletes original widgets after merge
*/
function merge({ deleteMerged = true }) {
let sel = widgets();
if (sel.length === 0) {
@ -264,6 +341,12 @@ function isolateBodies(){
}
}
/**
* Export selected widgets to STL or OBJ format.
* If multiple widgets, applies position offsets in STL format.
* @param {string} [format="stl"] - Export format: "stl" or "obj"
* @returns {ArrayBuffer|string} Encoded file data
*/
function exportWidgets(format = "stl") {
let sel = widgets();
if (sel.length === 0) {
@ -307,6 +390,11 @@ function exportWidgets(format = "stl") {
return stlEncode(recs);
}
/**
* Get selected widgets.
* @param {boolean} [orall] - If true and nothing selected, returns all widgets
* @returns {Array<Widget>} Selected widgets or empty array
*/
function widgets(orall) {
let sel = selectedMeshes.slice().map(m => m.widget);
return sel.length ? sel : orall ? api.widgets.all() : []
@ -321,6 +409,11 @@ function settings() {
return api.conf.get();
}
/**
* Prompt user for rotation angles and rotate selection.
* Shows alert if nothing selected.
* Accepts comma-separated X,Y,Z degrees.
*/
function input_rotate() {
if (selection.meshes().length === 0) {
api.show.alert("select object to rotate");
@ -336,6 +429,12 @@ function input_rotate() {
});
}
/**
* Prompt user for position coordinates and move selection.
* Shows alert if nothing selected.
* Accepts comma-separated X,Y coordinates.
* Adjusts for center vs corner origin modes.
*/
function input_position() {
if (selection.meshes().length === 0) {
api.show.alert("select object to position");
@ -361,6 +460,14 @@ function input_position() {
});
}
/**
* Handle size input change event.
* Calculates scale ratio from size change and applies to selection.
* Respects lock checkboxes for proportional scaling.
* Prevents scales below 0.1.
* @param {Event} e - Input change event
* @param {object} ui - UI elements object with size/lock fields
*/
function input_resize(e, ui) {
let dv = parseFloat(e.target.value || 1),
pv = parseFloat(e.target.was || 1),
@ -390,6 +497,14 @@ function input_resize(e, ui) {
ui.size.Z.was = ui.size.Z.value = zv * zr;
}
/**
* Handle scale input change event.
* Calculates scale ratio from scale change and applies to selection.
* Respects lock checkboxes for proportional scaling.
* Prevents scales below 0.1.
* @param {Event} e - Input change event
* @param {object} ui - UI elements object with scale/lock fields
*/
function input_scale(e, ui) {
let dv = parseFloat(e.target.value || 1),
pv = parseFloat(e.target.was || 1),
@ -423,6 +538,11 @@ function parse_as_float(e) {
e.target.value = parseFloat(e.target.value) || 0;
}
/**
* Bind input handlers to UI elements.
* Sets up Enter key handlers for scale, size, tool, and rotation inputs.
* @param {object} ui - UI elements object
*/
function input_binding(ui) {
// on enter but not on blur
space.event.onEnterKey([