ifcviewer-web: navigation parity — view all, XYZ views, ortho, zoom-to-selected

The camera math already lived in the shared ViewportCore; the desktop just
bound keys to it. The web build had no keyboard handler and no camera UI, so
none of it was reachable. Wire it up (Tier 1 + 2 of nav parity; fly mode is a
separate follow-up).

Shared core:
- setStandardView(StandardView) — named Front/Back/Left/Right/Top/Bottom wrapper
  over setStandardView(yaw,pitch), so the axis→angle mapping lives in one place.
- frameSelection() — lifts the desktop's "union selected AABBs → frameAabb(1.30)"
  focus logic out of ViewportWindow into the core. Desktop's focusOnSelectedObject
  and the X/Y/Z hotkeys now call these (DRY, behaviour unchanged).

Web:
- main_web gains a keydown handler matching the desktop bindings — Home=view all,
  F=zoom to selected, P=ortho toggle, X/Y/Z (+Shift=negative)=standard views —
  plus exported entry points (view_all_c / frame_selection_c / toggle_projection_c
  / projection_is_ortho_c / standard_view_c) for the toolbar.
- shell.html adds a bottom nav toolbar (Fit / Focus / Persp-Ortho / the six views)
  with the hotkeys in tooltips; the ortho button reflects state.

Verified: Z key and Front button both move the camera, ortho toggles render +
label, zero GPU errors; 111/111 unit + 6/6 web smoke pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-01 14:42:45 +10:00
parent 10ab982032
commit 001476c5a9
6 changed files with 142 additions and 28 deletions
+34
View File
@@ -445,6 +445,40 @@ void ViewportCore::setStandardView(float yaw_deg, float pitch_deg) {
host_->requestFrame();
}
void ViewportCore::setStandardView(StandardView view) {
switch (view) {
case StandardView::Front: setStandardView(0.0f, 0.0f); break;
case StandardView::Back: setStandardView(180.0f, 0.0f); break;
case StandardView::Right: setStandardView(90.0f, 0.0f); break;
case StandardView::Left: setStandardView(270.0f, 0.0f); break;
case StandardView::Top: setStandardView(camera_yaw_deg_, 90.0f); break;
case StandardView::Bottom: setStandardView(camera_yaw_deg_, -90.0f); break;
}
}
bool ViewportCore::frameSelection() {
if (selection_.count() == 0) return false;
float lo[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity() };
float hi[3] = { -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
bool any = false;
for (uint32_t id : selection_.selectionIds()) {
float mn[3], mx[3];
if (!computeObjectAabb(id, mn, mx)) continue;
for (int i = 0; i < 3; ++i) {
lo[i] = std::min(lo[i], mn[i]);
hi[i] = std::max(hi[i], mx[i]);
}
any = true;
}
if (!any) return false;
frameAabb(lo, hi, 1.30f);
return true;
}
void ViewportCore::orbitBy(float dx_px, float dy_px) {
// 0.4 deg/px matches the GL viewport. pitch is clamped just shy of
// the pole so orbitEye() stays well-conditioned.
+13
View File
@@ -185,6 +185,19 @@ public:
float dist, float yaw_deg, float pitch_deg);
void setStandardView(float yaw_deg, float pitch_deg);
// Named axis-aligned views. Front/Back/Left/Right pin yaw at 0/180/270/90
// (pitch 0); Top/Bottom pin pitch at ±90° and keep the current yaw. Wraps
// setStandardView(yaw,pitch) so the mapping lives in one place (shared by
// the desktop hotkeys and the web toolbar/keys).
enum class StandardView { Front, Back, Left, Right, Top, Bottom };
void setStandardView(StandardView view);
// Frame the current selection: union the selected objects' world AABBs and
// fit the camera to them (same 1.30 padding as the desktop "F" hotkey).
// No-op with an empty selection or no resolvable AABBs; returns whether it
// framed anything. Uses the shared selection + computeObjectAabb/frameAabb.
bool frameSelection();
// ---- Incremental orbit navigation ---------------------------------------
//
// Pixel-delta camera moves, shared by every host (Qt desktop + web).
+7 -27
View File
@@ -1519,31 +1519,10 @@ bool ViewportWindow::computeObjectAabb(uint32_t id,
void ViewportWindow::focusOnSelectedObject() {
if (fps_mode_) return;
if (selection_.count() == 0) {
Log::info() << "[wgpu] focus: no object selected";
return;
// Shared math lives in ViewportCore::frameSelection (also the web path).
if (!core_.frameSelection()) {
Log::info() << "[wgpu] focus: no object selected / no AABB available";
}
float lo[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity() };
float hi[3] = { -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
bool any = false;
for (uint32_t id : selection_.selectionIds()) {
float mn[3], mx[3];
if (!computeObjectAabb(id, mn, mx)) continue;
for (int i = 0; i < 3; ++i) {
lo[i] = std::min(lo[i], mn[i]);
hi[i] = std::max(hi[i], mx[i]);
}
any = true;
}
if (!any) {
Log::info() << "[wgpu] focus: no AABB available";
return;
}
frameAabb(lo, hi, 1.30f);
}
// setStandardView / toggleProjection / cameraString moved to ViewportCore (#84-i).
@@ -2226,10 +2205,11 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) {
&& (mods == Qt::NoModifier || mods == Qt::ShiftModifier)
&& !event->isAutoRepeat()) {
const bool neg = (mods & Qt::ShiftModifier);
using SV = ViewportCore::StandardView;
switch (key) {
case Qt::Key_X: setStandardView(neg ? 180.0f : 0.0f, 0.0f); break;
case Qt::Key_Y: setStandardView(neg ? 270.0f : 90.0f, 0.0f); break;
case Qt::Key_Z: setStandardView(camera_yaw_deg_, neg ? -90.0f : 90.0f); break;
case Qt::Key_X: core_.setStandardView(neg ? SV::Back : SV::Front); break;
case Qt::Key_Y: core_.setStandardView(neg ? SV::Left : SV::Right); break;
case Qt::Key_Z: core_.setStandardView(neg ? SV::Bottom : SV::Top); break;
}
return;
}