ifcviewer: preset-driven nav mouse bindings + a "Web" preset (desktop + web)

Make orbit/pan/select mouse bindings pure data owned by ViewportCore so both
hosts and every preset share one source of truth, and add a "Web" preset. This
rounds out the matrix: the desktop gains a web-style scheme and the web inherits
all presets, with no per-platform hardcoding.

- Core: NavBindings { orbit, pan, select button + modifier } + setNavPreset
  ("blender" default | "rhino" | "revit" | "web") + navBindings(). Select is
  preset-driven too (was hardcoded LMB) so "web" moves it to RMB. web = orbit
  LMB, pan MMB, select RMB (LMB drag orbits with no click/drag ambiguity; RMB
  click-selects / drag-marquees). NavMod uses "Plain" not "None" (X11 #defines
  None to 0L).
- Desktop ViewportWindow: applyNavPreset sources the core table (mapped to Qt);
  marquee-arm / single-pick dispatch keys off select_button_. Default stays
  blender → no behaviour change.
- Desktop config: AppSettings::NavPreset gains Web + navPresetName(); the
  Settings dialog lists it. This also FIXES a pre-existing gap — the preset combo
  was persisted but never applied (only WGPU_NAV_PRESET env worked). MainWindow
  now applies the persisted preset at startup (env override still wins) and live
  on navPresetChanged, so all four presets actually work from the dialog.
- Web main_web: classifyPress routes the pressed button through navBindings()
  (orbit/pan/select), defaulting to the "web" preset; context menu already
  suppressed so RMB is free.

Tests: setNavPreset table (Catch2, 123 total); web smoke select tests use RMB.
BonsaiViewer builds; 9/9 web smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-03 08:37:27 +10:00
parent cf05bbd1bb
commit f1d97ac5aa
11 changed files with 430 additions and 256 deletions
+32 -21
View File
@@ -1533,21 +1533,32 @@ void ViewportWindow::fpsIntegrate() {
// chunkScreenAreaPx moved to ViewportCore (#84-h).
void ViewportWindow::applyNavPreset(const char* name) {
// Matches GL AppSettings::NavPreset semantics exactly.
// blender — Orbit MMB, Pan Shift+MMB (default)
// rhino — Orbit RMB, Pan Shift+RMB
// revit — Orbit Shift+MMB, Pan MMB
if (name && std::strcmp(name, "rhino") == 0) {
orbit_button_ = Qt::RightButton; orbit_mods_ = Qt::NoModifier;
pan_button_ = Qt::RightButton; pan_mods_ = Qt::ShiftModifier;
} else if (name && std::strcmp(name, "revit") == 0) {
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::ShiftModifier;
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::NoModifier;
} else {
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::NoModifier;
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::ShiftModifier;
static Qt::MouseButton toQtBtn(ViewportCore::MouseBtn b) {
switch (b) {
case ViewportCore::MouseBtn::Left: return Qt::LeftButton;
case ViewportCore::MouseBtn::Middle: return Qt::MiddleButton;
case ViewportCore::MouseBtn::Right: return Qt::RightButton;
}
return Qt::LeftButton;
}
static Qt::KeyboardModifiers toQtMod(ViewportCore::NavMod m) {
switch (m) {
case ViewportCore::NavMod::Plain: return Qt::NoModifier;
case ViewportCore::NavMod::Shift: return Qt::ShiftModifier;
case ViewportCore::NavMod::Ctrl: return Qt::ControlModifier;
case ViewportCore::NavMod::Alt: return Qt::AltModifier;
}
return Qt::NoModifier;
}
void ViewportWindow::applyNavPreset(const char* name) {
// The preset table lives in ViewportCore (shared with web). Map its bindings
// to the Qt types the mouse handlers compare against.
core_.setNavPreset(name);
const auto& b = core_.navBindings();
orbit_button_ = toQtBtn(b.orbit); orbit_mods_ = toQtMod(b.orbit_mod);
pan_button_ = toQtBtn(b.pan); pan_mods_ = toQtMod(b.pan_mod);
select_button_ = toQtBtn(b.select); select_mods_ = toQtMod(b.select_mod);
}
// -----------------------------------------------------------------------------
@@ -1633,15 +1644,15 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
&& (mods & Qt::KeyboardModifierMask) == pan_mods_) {
nav_drag_kind_ = NavDrag::Pan;
setPivotIndicatorVisible(true);
} else if (event->button() == Qt::LeftButton
} else if (event->button() == select_button_
&& !section_tool_active_
&& tool_mode_ != ToolMode::Area
&& tool_mode_ != ToolMode::Length
&& nav_drag_kind_ == NavDrag::Inactive) {
// Arm marquee box-select. Plain / Shift / Ctrl LMB without a tool
// intercepting the click; if the cursor never moves past the
// threshold this stays armed-only and the release falls through
// to single-pick.
// Arm marquee box-select. Plain / Shift / Ctrl on the select button
// (Shift/Ctrl = add/remove) without a tool intercepting the click; if
// the cursor never moves past the threshold this stays armed-only and
// the release falls through to single-pick.
box_select_armed_ = true;
box_select_active_ = false;
box_select_start_pos_ = nav_press_pos_;
@@ -1660,7 +1671,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
// Marquee finalisation: only commit when the drag actually became
// active (cursor moved past threshold). Press-time mods decide the
// set op so a mid-drag Shift release doesn't flip the behaviour.
if (box_select_armed_ && event->button() == Qt::LeftButton) {
if (box_select_armed_ && event->button() == select_button_) {
const bool was_active = box_select_active_;
box_select_armed_ = false;
box_select_active_ = false;
@@ -1702,7 +1713,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
// LMB-click without drag → pick the object under the cursor and
// route through the selection state. Shift = add, Ctrl = remove,
// no modifier = replace. Empty-space click clears.
if (event->button() == Qt::LeftButton && !nav_dragged_) {
if (event->button() == select_button_ && !nav_dragged_) {
const Eigen::Vector2i pos = toV2i(event->position().toPoint());
const int px = int(pos.x() * devicePixelRatio());
const int py = int(pos.y() * devicePixelRatio());