From ca4ae077b2aec9f1fe4a7ea14c4de42258f7fa3f Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sun, 4 Jan 2026 14:53:14 -0500 Subject: [PATCH] add visual selection of z top and bottom --- src/kiri/app/api.js | 2 +- src/kiri/app/inputs.js | 8 +++ src/kiri/mode/cam/cl-ops.js | 119 ++++++++++++++++++++------------ src/kiri/mode/cam/cl-stock.js | 121 ++++++++++++++++++--------------- src/kiri/mode/cam/cl-zplane.js | 33 +++++++++ src/kiri/mode/cam/init-ui.js | 82 +++++++++++++++------- web/kiri/index.css | 5 ++ 7 files changed, 245 insertions(+), 125 deletions(-) create mode 100644 src/kiri/mode/cam/cl-zplane.js diff --git a/src/kiri/app/api.js b/src/kiri/app/api.js index f131c0ed..2d9c8ccd 100644 --- a/src/kiri/app/api.js +++ b/src/kiri/app/api.js @@ -166,7 +166,7 @@ export const api = { emit(t,m,o) { return EVENT.publish(t,m,o) }, import() { api.ui.load.click() }, 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, topics() { return EVENT.topics() } }, diff --git a/src/kiri/app/inputs.js b/src/kiri/app/inputs.js index d87dd2f0..883ffdde 100644 --- a/src/kiri/app/inputs.js +++ b/src/kiri/app/inputs.js @@ -735,6 +735,14 @@ function newInput(label, opt = {}) { ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'); 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.setAttribute("class", opt.class || "var-row"); if (height > 1) { diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index d3a35c67..c98541a3 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -203,6 +203,70 @@ export function createPopOp(type, map) { } 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', { tool: 'camLevelTool', spindle: 'camLevelSpindle', @@ -260,8 +324,8 @@ export function createPopOps() { omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), 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 }), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), }; @@ -386,19 +450,6 @@ export function createPopOps() { 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', { mode: 'camTraceType', offset: 'camTraceOffset', @@ -437,8 +488,8 @@ export function createPopOps() { revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }), exp: UC.newExpand("overrides"), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), 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 }), exp: UC.newExpand("overrides"), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), sep: UC.newBlank({ class: "pop-sep" }), 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 }) }; - 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', { spindle: 'camAreaSpindle', tool: 'camAreaTool', @@ -756,8 +785,8 @@ export function createPopOps() { exp_end: UC.endExpand(), exp: UC.newExpand("bounds", { }), sep: UC.newBlank({ class: "pop-sep" }), - ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), - ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), + ov_topz: UC.newInput(LANG.ou_ztop_s, ov_topz), + ov_botz: UC.newInput(LANG.ou_zbot_s, ov_botz), exp_end: UC.endExpand(), }; diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js index acadfd79..a62045b0 100644 --- a/src/kiri/mode/cam/cl-stock.js +++ b/src/kiri/mode/cam/cl-stock.js @@ -8,6 +8,70 @@ const { ui: UI } = api; 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() { if (env.isAnimate) { if (env.isIndexed) { @@ -22,16 +86,12 @@ export function updateStock() { SPACE.world.remove(meshZBottom); SPACE.world.remove(env.camStock); env.camStock = null; - meshZTop = null; - meshZBottom = null; return; } api.platform.update_bounds(); const settings = api.conf.get(); - const widgets = api.widgets.all(); - const { stock, process } = settings; const { x, y, z, center } = stock; @@ -86,57 +146,8 @@ export function updateStock() { lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); } - SPACE.world.remove(meshZTop); - if (process.camZTop && 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 - }); - 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; - } + showZTop(process.camZTop); + showZBottom(process.camZBottom); SPACE.update(); } diff --git a/src/kiri/mode/cam/cl-zplane.js b/src/kiri/mode/cam/cl-zplane.js new file mode 100644 index 00000000..03155141 --- /dev/null +++ b/src/kiri/mode/cam/cl-zplane.js @@ -0,0 +1,33 @@ +/** Copyright Stewart Allen -- 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(); +} diff --git a/src/kiri/mode/cam/init-ui.js b/src/kiri/mode/cam/init-ui.js index aa667ec5..ea57bb41 100644 --- a/src/kiri/mode/cam/init-ui.js +++ b/src/kiri/mode/cam/init-ui.js @@ -2,23 +2,24 @@ import { $ } from '../../../moto/webui.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 { 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 { 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 { 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 { space } from '../../../moto/space.js'; -import { recreateTabs } from './cl-tab.js'; +import { zPlaneStart, zPlaneDone } from './cl-zplane.js'; const { BufferGeometryUtils } = THREE; @@ -62,13 +63,28 @@ export function isDark() { export function clearPops() { if (env.func.unpop) env.func.unpop(); + helicalDone(); originSelectDone(); selectHolesDone(); surfaceDone(); tabDone(); traceDone(); - selectHolesDone(); - helicalDone(); + zPlaneDone(); + // 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() { @@ -288,7 +304,6 @@ export function opRender() { let mark = Date.now(); let html = []; let bind = {}; - let scale = api.view.unit_scale(); let notime = false; let inloop = 0; let numloop; @@ -556,10 +571,26 @@ export function opRender() { 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() { 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.absolute = true; 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.tabs.add", tabAdd); - - api.event.on("cam.tabs.done", tabDone); - - api.event.on("cam.tabs.clear", tabClear); + api.event + .on("cam.zplane.show", zPlaneShow) + .on("cam.zplane.hide", zPlaneHide) + .on("cam.zplane.select", zPlaneSelect); // COMMON TAB/TRACE EVENT HANDLERS api.event.on("slice.begin", () => { diff --git a/web/kiri/index.css b/web/kiri/index.css index d3520e7a..c3b6f387 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1431,6 +1431,11 @@ details[open] summary::after { word-break: break-word; white-space: normal; } +.z-select { + aspect-ratio: 1; + border-radius: 50%; + border-color: rgba(128,128,128,0.5) !important; +} #oplist { gap: 3px; margin-bottom: 3px;