diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index 57b6b1d0..283318e5 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -218,6 +218,117 @@ function ui_sync() { } function setup_keybd_nav() { + const panelPosKeys = { + 'panel-rotate': 'win.roate', + 'panel-scale': 'win.scale' + }; + + function parsePanelPos(key) { + if (!key) return null; + const raw = api.local.get(key); + if (!raw) return null; + if (typeof raw === 'object' && raw.left !== undefined && raw.top !== undefined) { + return raw; + } + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw); + if (parsed && parsed.left !== undefined && parsed.top !== undefined) { + return parsed; + } + } catch (e) { + // ignore malformed stored values + } + } + return null; + } + + function placePanel(panel, pos) { + if (!panel || !pos) return; + panel.style.left = `${Math.round(pos.left)}px`; + panel.style.top = `${Math.round(pos.top)}px`; + panel.style.right = 'auto'; + panel.style.bottom = 'auto'; + } + + function placePanelDefault(panel) { + if (!panel) return; + const rect = panel.getBoundingClientRect(); + const width = rect.width || 260; + const modeToolsRect = $('mode-tools')?.getBoundingClientRect(); + const baseTop = modeToolsRect ? (modeToolsRect.bottom + 10) : 92; + const minPad = 8; + const maxLeft = Math.max(minPad, window.innerWidth - width - minPad); + const left = Math.min(maxLeft, Math.max(minPad, (window.innerWidth - width) / 2)); + placePanel(panel, { left, top: baseTop }); + } + + function showSelectionPanel(pid) { + const panel = $(pid); + if (!panel) return; + panel.classList.remove('hide'); + const key = panelPosKeys[pid]; + const saved = parsePanelPos(key); + if (saved) { + placePanel(panel, saved); + } else { + placePanelDefault(panel); + } + } + + function hideSelectionPanel(pid) { + const panel = $(pid); + if (!panel) return; + panel.classList.add('hide'); + } + + function toggleSelectionPanel(pid) { + const el = $(pid); + if (!el) return; + if (el.classList.contains('hide')) { + showSelectionPanel(pid); + } else { + hideSelectionPanel(pid); + } + } + + function makePanelDraggable(panelId, handleId, storageKey) { + const panel = $(panelId); + const handle = $(handleId); + if (!panel || !handle) return; + let sx = 0, sy = 0, px = 0, py = 0, dragging = false; + handle.onmousedown = (ev) => { + if (ev.button !== 0) return; + dragging = true; + sx = ev.clientX; + sy = ev.clientY; + const rect = panel.getBoundingClientRect(); + px = rect.left; + py = rect.top; + ev.preventDefault(); + ev.stopPropagation(); + }; + document.addEventListener('mousemove', (ev) => { + if (!dragging) return; + const nx = px + (ev.clientX - sx); + const ny = py + (ev.clientY - sy); + panel.style.left = `${Math.round(nx)}px`; + panel.style.top = `${Math.round(ny)}px`; + panel.style.right = 'auto'; + panel.style.bottom = 'auto'; + }); + document.addEventListener('mouseup', () => { + if (dragging && storageKey) { + const rect = panel.getBoundingClientRect(); + api.local.set(storageKey, JSON.stringify({ + left: Math.round(rect.left), + top: Math.round(rect.top) + })); + } + dragging = false; + }); + } + // bind interface action elements ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; @@ -296,6 +407,12 @@ function setup_keybd_nav() { $('mesh-split').onclick = selection.isolateBodies; $('context-duplicate').onclick = selection.duplicate; $('context-mirror').onclick = selection.mirror; + $('context-rotate-panel').onclick = () => toggleSelectionPanel('panel-rotate'); + $('context-scale-panel').onclick = () => toggleSelectionPanel('panel-scale'); + $('panel-rotate-close').onmousedown = (ev) => { ev.stopPropagation(); }; + $('panel-scale-close').onmousedown = (ev) => { ev.stopPropagation(); }; + $('panel-rotate-close').onclick = (ev) => { ev.stopPropagation(); hideSelectionPanel('panel-rotate'); }; + $('panel-scale-close').onclick = (ev) => { ev.stopPropagation(); hideSelectionPanel('panel-scale'); }; $('context-layflat').onclick = view_tools.startLayFlat; $('context-lefty').onclick = view_tools.startLeftAlign; $('context-setfocus').onclick = () => { @@ -305,6 +422,9 @@ function setup_keybd_nav() { $('view-fit').onclick = api.const.SPACE.view.fit; $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; + makePanelDraggable('panel-rotate', 'panel-rotate-head', 'win.roate'); + makePanelDraggable('panel-scale', 'panel-scale-head', 'win.scale'); + // enable modal hiding $('mod-x').onclick = api.modal.hide; diff --git a/src/kiri/app/menubar.js b/src/kiri/app/menubar.js index dbab0d07..e729b3c4 100644 --- a/src/kiri/app/menubar.js +++ b/src/kiri/app/menubar.js @@ -58,10 +58,18 @@ function toolbarToolNozzle() { ]); } -function toolbarToolRotate(actions) { - return span({ id: 'tool-rotate' }, [ - i({ class: 'fas fa-rotate-right', title: 'rotate selection' }), - div({ id: 'ft-rotate', class: 'grid pop' }, [ +function rotatePanel(actions) { + return div({ id: 'panel-rotate', class: 'selection-panel hide' }, [ + div({ id: 'panel-rotate-head', class: 'selection-panel-head' }, [ + div({ class: 'selection-panel-head-title' }, [ + i({ class: 'fas fa-rotate-right' }), + label({ _: 'Rotate' }) + ]), + button({ id: 'panel-rotate-close', class: 'selection-panel-close', title: 'close' }, [ + i({ class: 'fas fa-times' }) + ]) + ]), + div({ id: 'ft-rotate', class: 'grid selection-panel-body' }, [ div({ id: 'rot_x_lt', ...on(actions, 'rot_x_lt') }, icon('fas fa-chevron-left')), label({ _: 'X' }), div({ id: 'rot_x_gt', ...on(actions, 'rot_x_gt') }, icon('fas fa-chevron-right')), @@ -81,10 +89,18 @@ function toolbarToolRotate(actions) { ]); } -function toolbarToolScale(actions) { - return span({ id: 'tool-scale' }, [ - i({ class: 'fas fa-expand', title: 'scale or resize selection' }), - div({ id: 'ft-scale', class: 'grid pop' }, [ +function scalePanel(actions) { + return div({ id: 'panel-scale', class: 'selection-panel hide' }, [ + div({ id: 'panel-scale-head', class: 'selection-panel-head' }, [ + div({ class: 'selection-panel-head-title' }, [ + i({ class: 'fas fa-expand' }), + label({ _: 'Scale / Size' }) + ]), + button({ id: 'panel-scale-close', class: 'selection-panel-close', title: 'close' }, [ + i({ class: 'fas fa-times' }) + ]) + ]), + div({ id: 'ft-scale', class: 'grid selection-panel-body' }, [ div([label({ _: 'X' }), input({ id: 'lock_x', type: 'checkbox', _checked: true })]), div([label({ _: 'Y' }), input({ id: 'lock_y', type: 'checkbox', _checked: true })]), div([label({ _: 'Z' }), input({ id: 'lock_z', type: 'checkbox', _checked: true })]), @@ -157,15 +173,16 @@ function content(actions) { menuItem(actions, { id: 'context-mirror', lk: 'rc_mirr', text: 'mirror', iconClass: 'fas fa-arrows-left-right-to-line' }), menuItem(actions, { id: 'context-duplicate', lk: 'rc_dupl', text: 'duplicate', iconClass: 'fas fa-copy' }), hr(), + menuItem(actions, { id: 'context-rotate-panel', text: 'rotate', iconClass: 'fas fa-rotate-right' }), + menuItem(actions, { id: 'context-scale-panel', text: 'scale / size', iconClass: 'fas fa-expand' }), + hr(), menuItem(actions, { id: 'mesh-merge', lk: 'rc_merg', text: 'merge meshes' }), menuItem(actions, { id: 'mesh-split', lk: 'rc_splt', text: 'isolate meshes' }), ] }), div({ class: 'menubar-separator' }), div({ class: 'f-row top-menu' }, [ - toolbarToolNozzle(), - toolbarToolRotate(actions), - toolbarToolScale(actions) + toolbarToolNozzle() ]), div({ class: 'grow' }), topMenu(actions, { @@ -209,7 +226,9 @@ function content(actions) { menuItem(actions, { children: label({ id: 'lset-pt', _: 'português' }) }) ] }) - ]) + ]), + rotatePanel(actions), + scalePanel(actions) ]; } diff --git a/web/kiri/index.css b/web/kiri/index.css index 614d1ed4..38c74da8 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1032,6 +1032,61 @@ details[open] summary::after { flex-direction: row; align-items: center; } + +/* draggable selection transform panels */ +.selection-panel { + position: fixed; + display: flex; + flex-direction: column; + z-index: 220; + min-width: 230px; + border: 1px solid var(--border-gray); + border-radius: 6px; + background-color: var(--menubar-back); + color: var(--menubar-fore); + box-shadow: 0 8px 22px rgba(0,0,0,0.25); + pointer-events: all; +} +#panel-rotate { + top: 92px; + right: 26px; +} +#panel-scale { + top: 270px; + right: 26px; +} +.selection-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 10px; + border-bottom: 1px solid var(--border-gray); + cursor: move; + user-select: none; +} +.selection-panel-head-title { + display: flex; + align-items: center; + gap: 8px; +} +.selection-panel-head label { + font-weight: 600; +} +.selection-panel-close { + border: none; + background: transparent; + color: inherit; + border-radius: 4px; + padding: 2px 6px; + cursor: pointer; +} +.selection-panel-close:hover { + background-color: var(--menu-hover-bg); +} +.selection-panel-body { + padding: 8px; +} + /** selection rotation */ #ft-rotate { gap: 2px;