push to get friendly jsdoc for future devs
This commit is contained in:
parent
40f992f12b
commit
dbde596903
20 changed files with 1054 additions and 20 deletions
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
||||
|
|
|
|||
|
|
@ -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) });
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
Loading…
Reference in a new issue