extract slider control from init into slider.js core singleton

This commit is contained in:
Stewart Allen 2025-12-24 16:51:42 -05:00
commit 667a7fad78
7 changed files with 440 additions and 188 deletions

View file

@ -1,6 +1,6 @@
{
"name": "grid-apps",
"version": "4.5.0",
"version": "4.6.0",
"description": "grid.space 3d slicing & modeling tools",
"author": "Stewart Allen <sa@grid.space>",
"license": "MIT",

View file

@ -4,6 +4,7 @@ import { $, $c } from '../../moto/webui.js';
import { api } from './api.js';
import { base } from '../../geo/base.js';
import { local as sdb } from '../../data/local.js';
import { slider } from './slider.js';
import { space } from '../../moto/space.js';
import { LOCAL, SETUP } from './main.js';
import { VIEWS, MODES, SEED } from './consts.js';
@ -20,6 +21,7 @@ import { menu as menuCAM } from '../mode/cam/init-menu.js';
import { menu as menuFDM } from '../mode/fdm/init-menu.js';
import { menu as menuLaser } from '../mode/laser/init-menu.js';
import { menu as menuSLA } from '../mode/sla/init-menu.js';
import STACKS from './stacks.js';
import './tools.js';
let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES,
@ -304,14 +306,20 @@ function keyDownHandler(evt) {
break;
case 38: // up arrow
if (inputHasFocus()) return false;
if (evt.metaKey) return api.show.layer(api.var.layer_at+1);
if (evt.metaKey) {
const { hi } = slider.getRange();
return api.show.layer(hi + 1);
}
if (deg) selection.rotate(deg, 0, 0);
if (move > 0) selection.move(0, move, 0);
evt.preventDefault();
break;
case 40: // down arrow
if (inputHasFocus()) return false;
if (evt.metaKey) return api.show.layer(api.var.layer_at-1);
if (evt.metaKey) {
const { hi } = slider.getRange();
return api.show.layer(hi - 1);
}
if (deg) selection.rotate(-deg, 0, 0);
if (move > 0) selection.move(0, -move, 0);
evt.preventDefault();
@ -364,16 +372,56 @@ function keyHandler(evt) {
}
switch (evt.charCode) {
case cca('`'): api.show.slices(0); break;
case cca('0'): api.show.slices(api.var.layer_max); break;
case cca('1'): api.show.slices(api.var.layer_max/10); break;
case cca('2'): api.show.slices(api.var.layer_max*2/10); break;
case cca('3'): api.show.slices(api.var.layer_max*3/10); break;
case cca('4'): api.show.slices(api.var.layer_max*4/10); break;
case cca('5'): api.show.slices(api.var.layer_max*5/10); break;
case cca('6'): api.show.slices(api.var.layer_max*6/10); break;
case cca('7'): api.show.slices(api.var.layer_max*7/10); break;
case cca('8'): api.show.slices(api.var.layer_max*8/10); break;
case cca('9'): api.show.slices(api.var.layer_max*9/10); break;
case cca('0'): {
const { max } = slider.getRange();
api.show.slices(max);
break;
}
case cca('1'): {
const { max } = slider.getRange();
api.show.slices(max/10);
break;
}
case cca('2'): {
const { max } = slider.getRange();
api.show.slices(max*2/10);
break;
}
case cca('3'): {
const { max } = slider.getRange();
api.show.slices(max*3/10);
break;
}
case cca('4'): {
const { max } = slider.getRange();
api.show.slices(max*4/10);
break;
}
case cca('5'): {
const { max } = slider.getRange();
api.show.slices(max*5/10);
break;
}
case cca('6'): {
const { max } = slider.getRange();
api.show.slices(max*6/10);
break;
}
case cca('7'): {
const { max } = slider.getRange();
api.show.slices(max*7/10);
break;
}
case cca('8'): {
const { max } = slider.getRange();
api.show.slices(max*8/10);
break;
}
case cca('9'): {
const { max } = slider.getRange();
api.show.slices(max*9/10);
break;
}
case cca('?'):
api.help.show();
break;
@ -434,10 +482,11 @@ function keyHandler(evt) {
if (api.view.get() === VIEWS.ARRANGE) {
api.space.set_focus(selection.widgets());
}
if (api.var.layer_hi == api.var.layer_lo) {
api.var.layer_lo = 0;
const { lo, hi } = slider.getRange();
if (hi === lo) {
slider.setRange(0, hi);
} else {
api.var.layer_lo = api.var.layer_hi;
slider.setRange(hi, hi);
}
api.show.slices();
break;
@ -804,25 +853,28 @@ function init_one() {
space.sky.setColor(controller.dark ? 0 : 0xffffff);
space.setAntiAlias(controller.antiAlias);
space.init(container, function (delta) {
let vars = api.var;
if (vars.layer_max === 0 || !delta) return;
const { lo, hi, max } = slider.getRange();
if (max === 0 || !delta) return;
if (controller.reverseZoom) delta = -delta;
let same = vars.layer_hi === vars.layer_lo;
let track = vars.layer_lo > 0;
let same = hi === lo;
let track = lo > 0;
let newHi = hi;
let newLo = lo;
if (delta > 0) {
vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1);
newHi = Math.max(same ? 0 : lo, hi - 1);
if (track) {
vars.layer_lo = Math.max(0, vars.layer_lo - 1);
newLo = Math.max(0, lo - 1);
}
} else if (delta < 0) {
vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1);
newHi = Math.min(max, hi + 1);
if (track) {
vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1);
newLo = Math.min(newHi, lo + 1);
}
}
if (same) {
vars.layer_lo = vars.layer_hi;
newLo = newHi;
}
slider.setRange(newLo, newHi);
view.update_slider();
show.slices();
}, controller.ortho);
@ -1086,148 +1138,45 @@ function init_one() {
};
// slider setup
const mobile = space.info.mob;
const slbar = mobile ? 80 : 30;
const slbar2 = slbar * 2;
const slider = ui.sliderRange;
const drag = { };
slider.init({
ui: {
layers: ui.layers,
slider: ui.slider,
sliderRange: ui.sliderRange,
sliderHold: ui.sliderHold,
sliderMid: ui.sliderMid,
sliderLo: ui.sliderLo,
sliderHi: ui.sliderHi,
sliderMin: ui.sliderMin,
sliderMax: ui.sliderMax,
sliderZero: $('slider-zero'),
},
tracker: tracker,
mobile: space.info.mob,
onLayerChange: (hi, lo) => {
api.show.layer(hi, lo);
},
onStackUpdate: (lo, hi) => {
STACKS.setRange(lo, hi);
},
onSceneUpdate: () => {
space.scene.active();
}
});
if (mobile) {
ui.slider.classList.add('slider-mobile');
ui.sliderLo.classList.add('slider-mobile');
ui.sliderHi.classList.add('slider-mobile');
// add css style for mobile devices
// expose slider to API
api.slider = slider;
// add mobile class to body if needed
if (space.info.mob) {
DOC.body.classList.add('mobile');
}
function pxToInt(txt) {
return txt ? parseInt(txt.substring(0,txt.length-2)) : 0;
}
function sliderUpdate() {
let start = drag.low / drag.maxval;
let end = (drag.low + drag.mid - slbar) / drag.maxval;
api.event.emit('slider.pos', { start, end });
api.var.layer_lo = Math.round(start * api.var.layer_max);
api.var.layer_hi = Math.round(end * api.var.layer_max);
api.show.layer();
space.scene.active();
}
function dragit(el, delta) {
el.ontouchstart = el.onmousedown = (ev) => {
// el.classList.add('sli-drag-el');
tracker.style.display = 'block';
ev.stopPropagation();
let obj = (ev.touches ? ev.touches[0] : ev);
drag.width = slider.clientWidth;
drag.maxval = drag.width - slbar2;
drag.start = obj.screenX;
drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft);
drag.mdat = drag.mid = ui.sliderMid.clientWidth;
drag.hiat = pxToInt(ui.sliderHold.style.marginRight);
drag.mdmax = drag.width - slbar - drag.loat;
drag.himax = drag.width - slbar - drag.mdat;
let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => {
// el.classList.remove('sli-drag-el');
if (ev) {
ev.stopPropagation();
ev.preventDefault();
}
slider.onmousemove = undefined;
tracker.style.display = 'none';
};
el.ontouchend = cancel_drag;
el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.buttons === 0) {
return cancel_drag();
}
if (delta) {
let obj = (ev.touches ? ev.touches[0] : ev);
delta(obj.screenX - drag.start);
}
};
};
}
dragit(ui.sliderLo, (delta) => {
let midval = drag.mdat - delta;
let lowval = drag.loat + delta;
if (midval < slbar || lowval < 0) {
return;
}
ui.sliderHold.style.marginLeft = `${lowval}px`;
ui.sliderMid.style.width = `${midval}px`;
drag.low = lowval;
drag.mid = midval;
sliderUpdate();
});
dragit(ui.sliderMid, (delta) => {
let loval = drag.loat + delta;
let hival = drag.hiat - delta;
if (loval < 0 || hival < 0) return;
ui.sliderHold.style.marginLeft = `${loval}px`;
ui.sliderHold.style.marginRight = `${hival}px`;
drag.low = loval;
sliderUpdate();
});
dragit(ui.sliderHi, (delta) => {
let midval = drag.mdat + delta;
let hival = drag.hiat - delta;
if (midval < slbar || midval > drag.mdmax || hival < 0) return;
ui.sliderMid.style.width = `${midval}px`;
ui.sliderHold.style.marginRight = `${hival}px`;
drag.mid = midval;
sliderUpdate();
});
ui.load.onchange = function(event) {
api.platform.load_files(event.target.files);
ui.load.value = ''; // reset so you can re-import the same filee
};
ui.sliderMin.onclick = () => {
api.show.layer(0,0);
}
ui.sliderMax.onclick = () => {
api.show.layer(api.var.layer_max,0);
}
ui.slider.onmouseover = (ev) => {
api.event.emit('slider.label');
};
ui.slider.onmouseleave = (ev) => {
if (!ev.buttons) api.event.emit('slider.unlabel');
};
api.event.on('slider.unlabel', (values) => {
});
api.event.on('slider.label', (values) => {
let digits = api.var.layer_max.toString().length;
$('slider-zero').style.width = `${digits}em`;
$('slider-max').style.width = `${digits}em`;
$('slider-zero').innerText = api.var.layer_lo;
$('slider-max').innerText = api.var.layer_hi;
});
api.event.on('slider.set', (values) => {
let width = slider.clientWidth;
let maxval = width - slbar2;
let start = Math.max(0, Math.min(1, values.start));
let end = Math.max(start, Math.min(1, values.end));
let lowval = start * maxval;
let midval = ((end - start) * maxval) + slbar;
let hival = maxval - end * maxval;
ui.sliderHold.style.marginLeft = `${lowval}px`;
ui.sliderMid.style.width = `${midval}px`;
ui.sliderHold.style.marginRight = `${hival}px`;
});
// store layer preferences
api.event.on('stack.show', label => {
let mode = api.mode.get();

View file

@ -314,20 +314,21 @@ function updateSpeeds(maxSpeed, minSpeed) {
}
function updateSlider() {
api.event.emit("slider.set", {
start: (api.var.layer_lo / api.var.layer_max),
end: (api.var.layer_hi / api.var.layer_max)
});
const { lo, hi, max } = api.slider.getRange();
if (max > 0) {
api.slider.updatePosition(lo / max, hi / max);
}
api.conf.update_fields_from_range();
}
function setVisibleLayer(h, l) {
h = h >= 0 ? h : api.var.layer_hi;
l = l >= 0 ? l : api.var.layer_lo;
api.var.layer_hi = bound(h, 0, api.var.layer_max);
api.var.layer_lo = bound(l, 0, h);
api.event.emit("slider.label");
updateSlider();
const { lo, hi, max } = api.slider.getRange();
h = h >= 0 ? h : hi;
l = l >= 0 ? l : lo;
const newHi = bound(h, 0, max);
const newLo = bound(l, 0, newHi);
api.slider.setRange(newLo, newHi);
api.slider.showLabels();
showSlices();
}
@ -349,11 +350,11 @@ function setEdges(bool) {
function updateSliderMax(set) {
let max = STACKS.getRange().tallest - 1;
api.var.layer_max = api.ui.sliderMax.innerText = max;
if (set || max < api.var.layer_hi) {
api.var.layer_hi = api.var.layer_max;
api.event.emit("slider.label");
updateSlider();
api.slider.setMax(max);
const { hi } = api.slider.getRange();
if (set || max < hi) {
api.slider.setRange(max, max);
api.slider.showLabels();
}
}
@ -387,31 +388,31 @@ function showSlices(layer) {
showSlider();
const { lo, hi, max } = api.slider.getRange();
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
layer = parseInt(layer);
} else {
layer = api.var.layer_hi;
layer = hi;
}
layer = bound(layer, 0, api.var.layer_max);
if (layer < api.var.layer_lo) api.var.layer_lo = layer;
api.var.layer_hi = layer;
api.event.emit("slider.label");
layer = bound(layer, 0, max);
const newLo = layer < lo ? layer : lo;
api.slider.setRange(newLo, layer);
api.slider.showLabels();
updateSlider();
STACKS.setRange(api.var.layer_lo, api.var.layer_hi);
// STACKS.setRange is called by slider callback
SPACE.update();
}
function showSlider() {
api.ui.layers.style.display = 'flex';
api.ui.slider.style.display = 'flex';
api.slider.show();
}
function hideSlider() {
api.ui.layers.style.display = 'none';
api.ui.slider.style.display = 'none';
api.slider.hide();
api.ui.speeds.style.display = 'none';
}

View file

@ -115,7 +115,7 @@ function updateSettings(opt = {}) {
if (settings.mode === 'FDM' && api.view.is_slice()) {
let changes = {};
let values = process;
let { layer_lo, layer_hi, layer_max } = api.var;
const { lo: layer_lo, hi: layer_hi, max: layer_max } = api.slider.getRange();
let range = { lo: layer_lo, hi: layer_hi };
let add = false;
if (layer_lo > 0 || layer_hi < layer_max) {
@ -450,8 +450,8 @@ function updateFieldsFromRange() {
let match = 0;
let values = {};
let restores = Object.clone(overrides);
let { layer_lo, layer_hi } = api.var;
for (let range of getOverlappingRanges(api.var.layer_lo, api.var.layer_hi)) {
const { lo: layer_lo, hi: layer_hi } = api.slider.getRange();
for (let range of getOverlappingRanges(layer_lo, layer_hi)) {
for (let key of Object.keys(range.fields)) {
values[key] = range.fields[key];
overrides[key] = settings.process[key];

302
src/kiri/core/slider.js Normal file
View file

@ -0,0 +1,302 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* Layer Slider Controller (Singleton)
* Manages layer range selection and visualization for slice/preview views
*/
class SliderControl {
#layerLo = 0;
#layerHi = 0;
#layerMax = 0;
#ui;
#tracker;
#mobile;
#sliderBar;
#sliderBar2;
#drag = {};
// Callbacks to replace event emissions
#onLayerChange;
#onStackUpdate;
#onSceneUpdate;
#initialized = false;
/**
* Initialize the slider with configuration
* @param {Object} config
* @param {Object} config.ui - UI element references
* @param {HTMLElement} config.tracker - Drag tracker element
* @param {boolean} config.mobile - Mobile device flag
* @param {Function} config.onLayerChange - Called when layer range changes
* @param {Function} config.onStackUpdate - Called to update stack visibility
* @param {Function} config.onSceneUpdate - Called to refresh scene
*/
init({ ui, tracker, mobile, onLayerChange, onStackUpdate, onSceneUpdate }) {
if (this.#initialized) {
throw new Error('Slider already initialized');
}
this.#ui = ui;
this.#tracker = tracker;
this.#mobile = mobile;
this.#sliderBar = mobile ? 80 : 30;
this.#sliderBar2 = this.#sliderBar * 2;
this.#onLayerChange = onLayerChange;
this.#onStackUpdate = onStackUpdate;
this.#onSceneUpdate = onSceneUpdate;
this.#setupUI();
this.#bindEvents();
this.#initialized = true;
}
/**
* Get current layer range
* @returns {{lo: number, hi: number, max: number}}
*/
getRange() {
return {
lo: this.#layerLo,
hi: this.#layerHi,
max: this.#layerMax
};
}
/**
* Set layer range (bounds are enforced)
* @param {number} lo - Lower layer
* @param {number} hi - Upper layer
*/
setRange(lo, hi) {
this.#layerLo = Math.max(0, Math.min(hi, lo));
this.#layerHi = Math.max(this.#layerLo, Math.min(this.#layerMax, hi));
this.#updateVisuals();
this.showLabels();
this.#notifyChange();
}
/**
* Set maximum layer count
* @param {number} max - Maximum layer value
*/
setMax(max) {
this.#layerMax = max;
this.#ui.sliderMax.innerText = max;
if (max < this.#layerHi) {
this.#layerHi = max;
this.showLabels();
this.#updateVisuals();
}
}
/**
* Update slider visual position from normalized values
* @param {number} startPercent - Start position (0-1)
* @param {number} endPercent - End position (0-1)
*/
updatePosition(startPercent, endPercent) {
const width = this.#ui.sliderRange.clientWidth;
const maxval = width - this.#sliderBar2;
const start = Math.max(0, Math.min(1, startPercent));
const end = Math.max(start, Math.min(1, endPercent));
const lowval = start * maxval;
const midval = ((end - start) * maxval) + this.#sliderBar;
const hival = maxval - end * maxval;
this.#ui.sliderHold.style.marginLeft = `${lowval}px`;
this.#ui.sliderMid.style.width = `${midval}px`;
this.#ui.sliderHold.style.marginRight = `${hival}px`;
}
/**
* Show layer number labels
*/
showLabels() {
const digits = this.#layerMax.toString().length;
this.#ui.sliderZero.style.width = `${digits}em`;
this.#ui.sliderMax.style.width = `${digits}em`;
this.#ui.sliderZero.innerText = this.#layerLo;
this.#ui.sliderMax.innerText = this.#layerHi;
}
/**
* Show slider UI
*/
show() {
this.#ui.layers.style.display = 'flex';
this.#ui.slider.style.display = 'flex';
}
/**
* Hide slider UI
*/
hide() {
this.#ui.layers.style.display = 'none';
this.#ui.slider.style.display = 'none';
}
#setupUI() {
if (this.#mobile) {
this.#ui.slider.classList.add('slider-mobile');
this.#ui.sliderLo.classList.add('slider-mobile');
this.#ui.sliderHi.classList.add('slider-mobile');
}
}
#bindEvents() {
// Min/Max buttons
this.#ui.sliderMin.onclick = () => {
this.#layerLo = 0;
this.#layerHi = 0;
this.showLabels();
this.#updateVisuals();
this.#notifyChange();
};
this.#ui.sliderMax.onclick = () => {
this.#layerLo = this.#layerMax;
this.#layerHi = this.#layerMax;
this.showLabels();
this.#updateVisuals();
this.#notifyChange();
};
// Mouse hover for labels
this.#ui.slider.onmouseover = () => {
this.showLabels();
};
this.#ui.slider.onmouseleave = (ev) => {
if (!ev.buttons) {
this.hideLabels();
}
};
// Drag handlers for the three slider sections
this.#bindDrag(this.#ui.sliderLo, this.#handleLoDrag.bind(this));
this.#bindDrag(this.#ui.sliderMid, this.#handleMidDrag.bind(this));
this.#bindDrag(this.#ui.sliderHi, this.#handleHiDrag.bind(this));
}
#bindDrag(el, deltaHandler) {
const slider = this.#ui.sliderRange;
el.ontouchstart = el.onmousedown = (ev) => {
this.#tracker.style.display = 'block';
ev.stopPropagation();
let obj = (ev.touches ? ev.touches[0] : ev);
this.#drag.width = slider.clientWidth;
this.#drag.maxval = this.#drag.width - this.#sliderBar2;
this.#drag.start = obj.screenX;
this.#drag.loat = this.#drag.low = this.#pxToInt(this.#ui.sliderHold.style.marginLeft);
this.#drag.mdat = this.#drag.mid = this.#ui.sliderMid.clientWidth;
this.#drag.hiat = this.#pxToInt(this.#ui.sliderHold.style.marginRight);
this.#drag.mdmax = this.#drag.width - this.#sliderBar - this.#drag.loat;
this.#drag.himax = this.#drag.width - this.#sliderBar - this.#drag.mdat;
const cancelDrag = this.#tracker.ontouchend = this.#tracker.onmouseup = (ev) => {
if (ev) {
ev.stopPropagation();
ev.preventDefault();
}
slider.onmousemove = undefined;
this.#tracker.style.display = 'none';
};
el.ontouchend = cancelDrag;
el.ontouchmove = this.#tracker.ontouchmove = this.#tracker.onmousemove = (ev) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.buttons === 0) {
return cancelDrag();
}
if (deltaHandler) {
let obj = (ev.touches ? ev.touches[0] : ev);
deltaHandler(obj.screenX - this.#drag.start);
}
};
};
}
#handleLoDrag(delta) {
let midval = this.#drag.mdat - delta;
let lowval = this.#drag.loat + delta;
if (midval < this.#sliderBar || lowval < 0) {
return;
}
this.#ui.sliderHold.style.marginLeft = `${lowval}px`;
this.#ui.sliderMid.style.width = `${midval}px`;
this.#drag.low = lowval;
this.#drag.mid = midval;
this.#updateFromDrag();
}
#handleMidDrag(delta) {
let loval = this.#drag.loat + delta;
let hival = this.#drag.hiat - delta;
if (loval < 0 || hival < 0) {
return;
}
this.#ui.sliderHold.style.marginLeft = `${loval}px`;
this.#ui.sliderHold.style.marginRight = `${hival}px`;
this.#drag.low = loval;
this.#updateFromDrag();
}
#handleHiDrag(delta) {
let midval = this.#drag.mdat + delta;
let hival = this.#drag.hiat - delta;
if (midval < this.#sliderBar || midval > this.#drag.mdmax || hival < 0) {
return;
}
this.#ui.sliderMid.style.width = `${midval}px`;
this.#ui.sliderHold.style.marginRight = `${hival}px`;
this.#drag.mid = midval;
this.#updateFromDrag();
}
#updateFromDrag() {
const start = this.#drag.low / this.#drag.maxval;
const end = (this.#drag.low + this.#drag.mid - this.#sliderBar) / this.#drag.maxval;
this.#layerLo = Math.round(start * this.#layerMax);
this.#layerHi = Math.round(end * this.#layerMax);
this.#notifyChange();
}
#updateVisuals() {
if (this.#layerMax === 0) return;
const start = this.#layerLo / this.#layerMax;
const end = this.#layerHi / this.#layerMax;
this.updatePosition(start, end);
}
#notifyChange() {
// Notify callbacks of layer change
this.#onLayerChange?.(this.#layerHi, this.#layerLo);
this.#onStackUpdate?.(this.#layerLo, this.#layerHi);
this.#onSceneUpdate?.();
}
#pxToInt(txt) {
return txt ? parseInt(txt.substring(0, txt.length - 2)) : 0;
}
hideLabels() {
// Labels hide themselves on mouse leave, but can be called explicitly
}
}
// Export singleton instance
export const slider = new SliderControl();

View file

@ -3,7 +3,7 @@
const terms = {
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
LICENSE: "See the license.md file included with the source distribution",
VERSION: "4.5.0"
VERSION: "4.6.0"
};
export const beta = 0;

View file

@ -1,5 +1,5 @@
{
"name": "Kiri:Moto 4.5.0",
"name": "Kiri:Moto 4.6.0",
"short_name": "Kiri:Moto",
"description": "Slicer for 3D printers, CNC mills, laser cutters and more",
"start_url": "/boot/index.html",