add visual selection of z top and bottom

This commit is contained in:
Stewart Allen 2026-01-04 14:53:14 -05:00
commit ca4ae077b2
7 changed files with 244 additions and 124 deletions

View file

@ -166,7 +166,7 @@ export const api = {
emit(t,m,o) { return EVENT.publish(t,m,o) }, emit(t,m,o) { return EVENT.publish(t,m,o) },
import() { api.ui.load.click() }, import() { api.ui.load.click() },
listeners(topic) { return EVENT.targets(topic) }, listeners(topic) { return EVENT.targets(topic) },
on(t,l) { return EVENT.on(t,l) }, on(t,l) { EVENT.on(t,l); return api.event },
settings: settingsUI.trigger_event, settings: settingsUI.trigger_event,
topics() { return EVENT.topics() } topics() { return EVENT.topics() }
}, },

View file

@ -735,6 +735,14 @@ function newInput(label, opt = {}) {
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'); ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input');
row.appendChild(newLabel(label)); row.appendChild(newLabel(label));
if (opt.selector) {
let sel = DOC.createElement('button');
sel.classList.add('z-select');
sel.addEventListener('click', event => opt.selector('click', { input: ip, event, opt }));
sel.addEventListener('mouseenter', event => opt.selector('enter', { input: ip, event, opt }));
sel.addEventListener('mouseleave', event => opt.selector('leave', { input: ip, event, opt }));
row.appendChild(sel);
}
row.appendChild(ip); row.appendChild(ip);
row.setAttribute("class", opt.class || "var-row"); row.setAttribute("class", opt.class || "var-row");
if (height > 1) { if (height > 1) {

View file

@ -203,6 +203,70 @@ export function createPopOp(type, map) {
} }
export function createPopOps() { export function createPopOps() {
const bottom = true;
const open = true;
const top = true;
function selector(type, { input, event, opt }) {
let which = opt?.top ? 'top' : opt?.bottom ? 'bottom' : 'unknown';
let value = parseFloat(input.value);
switch (type) {
case 'click':
let rec = env.poppedRec;
let { field } = opt;
api.event.emit('cam.zplane.select', { which, onselect(value) {
rec[field] = value;
input.focus();
input.value = value;
input.blur();
} } );
break;
case 'enter':
api.event.emit('cam.zplane.show', { which, value });
break;
case 'leave':
api.event.emit('cam.zplane.hide', { which });
break;
}
}
function isClear() {
return env.poppedRec.mode === 'clear';
}
function isTrace() {
return env.poppedRec.mode === 'trace';
}
function isShadow() {
return env.poppedRec.shadow;
}
function isSurface() {
return env.poppedRec.mode === 'surface';
}
function isSurfaceLinear() {
return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
}
function canDogBones() {
if (!env.poppedRec) return false;
return env.poppedRec.mode === 'follow';
}
function canDogBonesRev() {
return canDogBones() && env.poppedRec.dogbone;
}
function zDogSep() {
return canDogBones() || zBottom();
}
const ov_topz = { title: LANG.ou_ztop_l, convert: toFloat, units, selector, top, field: "ov_topz" };
const ov_botz = { title: LANG.ou_zbot_l, convert: toFloat, units, selector, bottom, field: "ov_botz" };
createPopOp('level', { createPopOp('level', {
tool: 'camLevelTool', tool: 'camLevelTool',
spindle: 'camLevelSpindle', spindle: 'camLevelSpindle',
@ -260,8 +324,8 @@ export function createPopOps() {
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
}; };
@ -304,8 +368,8 @@ export function createPopOps() {
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
}; };
@ -386,19 +450,6 @@ export function createPopOps() {
linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }), linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }),
}; };
function canDogBones() {
if (!env.poppedRec) return false;
return env.poppedRec.mode === 'follow';
}
function canDogBonesRev() {
return canDogBones() && env.poppedRec.dogbone;
}
function zDogSep() {
return canDogBones() || zBottom();
}
createPopOp('trace', { createPopOp('trace', {
mode: 'camTraceType', mode: 'camTraceType',
offset: 'camTraceOffset', offset: 'camTraceOffset',
@ -437,8 +488,8 @@ export function createPopOps() {
revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }), revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }), menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }),
@ -483,8 +534,8 @@ export function createPopOps() {
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }), menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }),
@ -669,28 +720,6 @@ export function createPopOps() {
count: UC.newInput('operations', { title: "num operations", convert: toInt, bound: UC.bound(1, 20), post: opRender }) count: UC.newInput('operations', { title: "num operations", convert: toInt, bound: UC.bound(1, 20), post: opRender })
}; };
function isClear() {
return env.poppedRec.mode === 'clear';
}
function isTrace() {
return env.poppedRec.mode === 'trace';
}
function isShadow() {
return env.poppedRec.shadow;
}
function isSurface() {
return env.poppedRec.mode === 'surface';
}
function isSurfaceLinear() {
return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
}
const open = true;
createPopOp('area', { createPopOp('area', {
spindle: 'camAreaSpindle', spindle: 'camAreaSpindle',
tool: 'camAreaTool', tool: 'camAreaTool',
@ -756,8 +785,8 @@ export function createPopOps() {
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
exp: UC.newExpand("bounds", { }), exp: UC.newExpand("bounds", { }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
}; };

View file

@ -8,6 +8,70 @@ const { ui: UI } = api;
let meshZTop, meshZBottom; let meshZTop, meshZBottom;
{
let geo = new THREE.PlaneGeometry(1, 1);
let mat = new THREE.MeshBasicMaterial({
color: 0x779977,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZTop = new THREE.Mesh(geo, mat);
meshZTop.renderOrder = 1;
}
{
let geo = new THREE.PlaneGeometry(1, 1);
let mat = new THREE.MeshBasicMaterial({
color: 0x997777,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZBottom = new THREE.Mesh(geo, mat);
meshZBottom.renderOrder = 1;
}
function getBounds() {
const settings = api.conf.get();
const { stock } = settings;
const { x, y, z, center } = stock;
const widgets = api.widgets.all();
let max = { x, y, z };
for (let w of widgets) {
max.x = Math.max(max.x, w.track.box.w);
max.y = Math.max(max.y, w.track.box.h);
max.z = Math.max(max.z, w.track.box.d);
}
return widgets.length ? { max, center } : {};
}
export function showZTop(zval) {
SPACE.world.remove(meshZTop);
let { max, center } = getBounds();
if (zval && max && center) {
meshZTop.position.x = center.x;
meshZTop.position.y = center.y;
meshZTop.position.z = zval;
meshZTop.scale.x = max.x;
meshZTop.scale.y = max.y;
SPACE.world.add(meshZTop);
}
}
export function showZBottom(zval) {
SPACE.world.remove(meshZBottom);
let { max, center } = getBounds();
if (zval && max && center) {
meshZBottom.position.x = center.x;
meshZBottom.position.y = center.y;
meshZBottom.position.z = zval;
meshZBottom.scale.x = max.x;
meshZBottom.scale.y = max.y;
SPACE.world.add(meshZBottom);
}
}
export function updateStock() { export function updateStock() {
if (env.isAnimate) { if (env.isAnimate) {
if (env.isIndexed) { if (env.isIndexed) {
@ -22,16 +86,12 @@ export function updateStock() {
SPACE.world.remove(meshZBottom); SPACE.world.remove(meshZBottom);
SPACE.world.remove(env.camStock); SPACE.world.remove(env.camStock);
env.camStock = null; env.camStock = null;
meshZTop = null;
meshZBottom = null;
return; return;
} }
api.platform.update_bounds(); api.platform.update_bounds();
const settings = api.conf.get(); const settings = api.conf.get();
const widgets = api.widgets.all();
const { stock, process } = settings; const { stock, process } = settings;
const { x, y, z, center } = stock; const { x, y, z, center } = stock;
@ -86,57 +146,8 @@ export function updateStock() {
lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
} }
SPACE.world.remove(meshZTop); showZTop(process.camZTop);
if (process.camZTop && widgets.length) { showZBottom(process.camZBottom);
let max = { x, y, z };
for (let w of widgets) {
max.x = Math.max(max.x, w.track.box.w);
max.y = Math.max(max.y, w.track.box.h);
max.z = Math.max(max.z, w.track.box.d);
}
let geo = new THREE.PlaneGeometry(max.x, max.y);
let mat = new THREE.MeshBasicMaterial({
color: 0x777777,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZTop = new THREE.Mesh(geo, mat);
meshZTop._max = max;
meshZTop.renderOrder = 1;
meshZTop.position.x = center.x;
meshZTop.position.y = center.y;
meshZTop.position.z = process.camZTop;
SPACE.world.add(meshZTop);
} else {
meshZTop = undefined;
}
SPACE.world.remove(meshZBottom);
if (process.camZBottom && widgets.length) {
let max = { x, y, z };
for (let w of widgets) {
max.x = Math.max(max.x, w.track.box.w);
max.y = Math.max(max.y, w.track.box.h);
max.z = Math.max(max.z, w.track.box.d);
}
let geo = new THREE.PlaneGeometry(max.x, max.y);
let mat = new THREE.MeshBasicMaterial({
color: 0x777777,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZBottom = new THREE.Mesh(geo, mat);
meshZBottom._max = max;
meshZBottom.renderOrder = 1;
meshZBottom.position.x = center.x;
meshZBottom.position.y = center.y;
meshZBottom.position.z = process.camZBottom;
SPACE.world.add(meshZBottom);
} else {
meshZBottom = undefined;
}
SPACE.update(); SPACE.update();
} }

View file

@ -0,0 +1,33 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../app/api.js';
import { clearPops, hoverStart } from './init-ui.js';
import { showZTop, showZBottom, updateStock } from './cl-stock.js';
let alert;
let plane;
let ondone;
let lastY;
export function zPlaneStart(which, onselect) {
alert = api.show.alert("[esc] cancels z plane selection");
plane = which;
ondone = onselect;
hoverStart(onHover, onHoverUp);
}
export function zPlaneDone() {
api.hide.alert(alert);
updateStock();
}
function onHover(data) {
const { int, type, point } = data;
if (plane === 'top') showZTop(lastY = point.y); else
if (plane === 'bottom') showZBottom(lastY = point.y);
}
function onHoverUp(int) {
ondone(lastY);
clearPops();
}

View file

@ -2,23 +2,24 @@
import { $ } from '../../../moto/webui.js'; import { $ } from '../../../moto/webui.js';
import { api } from '../../app/api.js'; import { api } from '../../app/api.js';
import { load } from '../../../load/file.js';
import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js';
import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js';
import { space as SPACE } from '../../../moto/space.js';
import { Layers } from '../../app/layers.js';
import { Stack } from '../../app/stack.js';
import { updateStock } from './cl-stock.js';
import { createPopOps } from './cl-ops.js'; import { createPopOps } from './cl-ops.js';
import { helicalOn, helicalDone } from './cl-helical.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { load } from '../../../load/file.js';
import { Layers } from '../../app/layers.js';
import { originSelectDone } from './cl-origin.js';
import { recreateTabs } from './cl-tab.js';
import { showZTop, showZBottom, updateStock } from './cl-stock.js';
import { space as SPACE } from '../../../moto/space.js';
import { space } from '../../../moto/space.js';
import { Stack } from '../../app/stack.js';
import { surfaceOn, surfaceDone } from './cl-surface.js';
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js'; import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js';
import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { surfaceOn, surfaceDone } from './cl-surface.js';
import { helicalOn, helicalDone } from './cl-helical.js';
import { originSelectDone } from './cl-origin.js';
import { Widget, newWidget } from '../../app/widget.js'; import { Widget, newWidget } from '../../app/widget.js';
import { space } from '../../../moto/space.js'; import { zPlaneStart, zPlaneDone } from './cl-zplane.js';
import { recreateTabs } from './cl-tab.js';
const { BufferGeometryUtils } = THREE; const { BufferGeometryUtils } = THREE;
@ -62,13 +63,28 @@ export function isDark() {
export function clearPops() { export function clearPops() {
if (env.func.unpop) env.func.unpop(); if (env.func.unpop) env.func.unpop();
helicalDone();
originSelectDone(); originSelectDone();
selectHolesDone(); selectHolesDone();
surfaceDone(); surfaceDone();
tabDone(); tabDone();
traceDone(); traceDone();
selectHolesDone(); zPlaneDone();
helicalDone(); // unbind
hoverDone();
}
export function hoverStart(onhover, onhoverup) {
clearPops();
env.hover = onhover;
env.hoverUp = onhoverup;
api.feature.hover = true;
}
export function hoverDone() {
env.hover = noop;
env.hoverUp = noop;
api.feature.hover = false;
} }
function animFn() { function animFn() {
@ -288,7 +304,6 @@ export function opRender() {
let mark = Date.now(); let mark = Date.now();
let html = []; let html = [];
let bind = {}; let bind = {};
let scale = api.view.unit_scale();
let notime = false; let notime = false;
let inloop = 0; let inloop = 0;
let numloop; let numloop;
@ -556,10 +571,26 @@ export function opRender() {
updateStock(); updateStock();
} }
export function zPlaneShow({ which, value }) {
if (!value) return;
if (which === 'top') showZTop(value); else
if (which === 'bottom') showZBottom(value);
}
export function zPlaneHide({ which }) {
updateStock();
}
export function zPlaneSelect({ which, onselect }) {
clearPops();
zPlaneStart(which, value => {
onselect(parseFloat(value));
});
}
export function init() { export function init() {
api.event.on('tool.mesh.face-normal', normal => { api.event.on('tool.mesh.face-normal', normal => {
// console.log({ env.poppedRec });
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2);
env.poppedRec.absolute = true; env.poppedRec.absolute = true;
opRender(); opRender();
@ -850,17 +881,20 @@ export function init() {
} }
}); });
api.event.on("cam.op.add", opAdd); api.event
.on("cam.op.add", opAdd)
.on("cam.op.del", opDel)
.on("cam.op.render", opRender);
api.event.on("cam.op.del", opDel); api.event
.on("cam.tabs.add", tabAdd)
.on("cam.tabs.done", tabDone)
.on("cam.tabs.clear", tabClear);
api.event.on("cam.op.render", opRender); api.event
.on("cam.zplane.show", zPlaneShow)
api.event.on("cam.tabs.add", tabAdd); .on("cam.zplane.hide", zPlaneHide)
.on("cam.zplane.select", zPlaneSelect);
api.event.on("cam.tabs.done", tabDone);
api.event.on("cam.tabs.clear", tabClear);
// COMMON TAB/TRACE EVENT HANDLERS // COMMON TAB/TRACE EVENT HANDLERS
api.event.on("slice.begin", () => { api.event.on("slice.begin", () => {

View file

@ -1431,6 +1431,11 @@ details[open] summary::after {
word-break: break-word; word-break: break-word;
white-space: normal; white-space: normal;
} }
.z-select {
aspect-ratio: 1;
border-radius: 50%;
border-color: rgba(128,128,128,0.5) !important;
}
#oplist { #oplist {
gap: 3px; gap: 3px;
margin-bottom: 3px; margin-bottom: 3px;