make rotate/scale menus draggable

This commit is contained in:
Stewart Allen 2026-02-24 15:18:03 -05:00
commit 05c69f32cd
3 changed files with 206 additions and 12 deletions

View file

@ -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;

View file

@ -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)
];
}

View file

@ -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;