From a4f7db45cdadab6d30733bef24036c3de8ef3aa7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 11 May 2026 12:18:38 +1000 Subject: [PATCH] Interface mockup 16 --- src/ifcviewer/ViewportWindow.cpp | 25 ++-- src/ifcviewer/ViewportWindow.h | 2 +- src/interface/CMakeLists.txt | 2 + src/interface/MainWindow.cpp | 48 +++++-- src/interface/modules/viewport/Controller.cpp | 125 ++++++++++++++++++ src/interface/modules/viewport/Controller.h | 14 ++ 6 files changed, 186 insertions(+), 30 deletions(-) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 7d7deaec8f..09f8bac763 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1690,16 +1690,6 @@ ViewportWindow::CameraState ViewportWindow::cameraState() const { void ViewportWindow::keyPressEvent(QKeyEvent* event) { const int key = event->key(); - // Shift+F toggles FPS/fly mode. Checked before auto-repeat filtering so - // a held Shift+F doesn't thrash between modes. - if (key == Qt::Key_F - && (event->modifiers() & Qt::ShiftModifier) - && !event->isAutoRepeat()) { - if (camera_mode_ == CameraMode::Fps) exitFpsMode(); - else enterFpsMode(); - return; - } - if (camera_mode_ == CameraMode::Fps) { if (key == Qt::Key_Escape && !event->isAutoRepeat()) { exitFpsMode(); @@ -1731,7 +1721,6 @@ void ViewportWindow::keyPressEvent(QKeyEvent* event) { } // Plain F (no modifiers): focus camera on the currently selected object. - // Shift+F is FPS-mode toggle and was handled above. if (key == Qt::Key_F && event->modifiers() == Qt::NoModifier && !event->isAutoRepeat()) { @@ -3678,14 +3667,16 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) { // gesture. Tool-mode LMB defers pick handling to surfacePickedInTool // on release for tools that consume clicks (Area / Length); Volume is // passive and routes input through normal selection. - const bool tool_consumes_clicks = - (tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length); - if (e->button() == Qt::LeftButton && !tool_consumes_clicks) { - press_pick_id_ = pickObjectAt(e->pos().x(), e->pos().y()); + if (e->button() == Qt::LeftButton) { box_select_start_pos_ = e->pos(); box_select_current_pos_ = e->pos(); - box_select_press_mods_ = e->modifiers(); - box_select_armed_ = true; + const bool tool_consumes_clicks = + (tool_mode_ == ToolMode::Area || tool_mode_ == ToolMode::Length); + if (!tool_consumes_clicks) { + press_pick_id_ = pickObjectAt(e->pos().x(), e->pos().y()); + box_select_press_mods_ = e->modifiers(); + box_select_armed_ = true; + } } } void ViewportWindow::handleMouseRelease(QMouseEvent* e) { diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 43ed4e8dae..cb1d7f577f 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -314,6 +314,7 @@ public: void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); void setBackgroundColor(const QColor& color); + void enterFpsMode(); // Selection. The viewport owns a SelectionState that tracks both // the multi-set and the "active" (last single-clicked) id. External @@ -601,7 +602,6 @@ private: // way one long frame only produces a single catch-up step instead of // also missing a QTimer tick. enum class CameraMode { Orbit, Fps }; - void enterFpsMode(); void exitFpsMode(); void fpsIntegrate(); // called from render() void recenterFpsCursor(); diff --git a/src/interface/CMakeLists.txt b/src/interface/CMakeLists.txt index 2b703edf4e..a573e8fd88 100644 --- a/src/interface/CMakeLists.txt +++ b/src/interface/CMakeLists.txt @@ -80,6 +80,8 @@ set(INTERFACE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Panel.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/viewport/Controller.h + ${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer-full/Measurement.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer-full/Measurement.h ${CMAKE_CURRENT_SOURCE_DIR}/interface_resources.qrc ) diff --git a/src/interface/MainWindow.cpp b/src/interface/MainWindow.cpp index 4884d6481a..bdb88f1d05 100644 --- a/src/interface/MainWindow.cpp +++ b/src/interface/MainWindow.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,33 @@ void MainWindow::setupChrome() { setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | QMainWindow::GroupedDragging); + + auto bind_shortcut = [this](const QKeySequence& sequence, auto fn) { + auto* shortcut = new QShortcut(sequence, this); + shortcut->setContext(Qt::WindowShortcut); + connect(shortcut, &QShortcut::activated, this, fn); + }; + bind_shortcut(QKeySequence("Ctrl+Shift+L"), [this]() { + viewport_controller_->toggleDistanceMode(); + }); + bind_shortcut(QKeySequence("Ctrl+Shift+A"), [this]() { + viewport_controller_->toggleAreaMode(); + }); + bind_shortcut(QKeySequence("Ctrl+Shift+V"), [this]() { + viewport_controller_->toggleVolumeMode(); + }); + bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_F), [this]() { + viewport_controller_->setFlyMode(); + }); + bind_shortcut(QKeySequence(Qt::Key_H), [this]() { + viewport_controller_->hideSelectedElements(); + }); + bind_shortcut(QKeySequence(Qt::SHIFT | Qt::Key_H), [this]() { + viewport_controller_->isolateSelectedElements(); + }); + bind_shortcut(QKeySequence(Qt::ALT | Qt::Key_H), [this]() { + viewport_controller_->showAllElements(); + }); updateWindowTitle(); } @@ -211,18 +239,14 @@ QWidget* MainWindow::buildNavigateRibbonPage() { viewport_widget_->viewport()->projectionOrtho() ? "Ortho" : "Perspective"); }); - auto* orbit_mode = components::buttons::makeButton("Orbit", ":/icons/rotate-camera-right.svg", this); - connect(orbit_mode, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Mode", "Orbit mode active"); - }); auto* fly_mode = components::buttons::makeButton("Fly", ":/icons/drone.svg", this); connect(fly_mode, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Mode", "Fly mode coming soon"); + viewport_controller_->setFlyMode(); }); row->addWidget(components::buttons::makeButtonGroup("CAMERA", {set_home, go_home, view_all, view_selected}, this)); row->addWidget(components::buttons::makeButtonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}, this)); - row->addWidget(components::buttons::makeButtonGroup("MODE", {orbit_mode, fly_mode}, this)); + row->addWidget(components::buttons::makeButtonGroup("MODE", {fly_mode}, this)); row->addStretch(1); return page; } @@ -236,15 +260,15 @@ QWidget* MainWindow::buildInspectRibbonPage() { auto* hide_selected = components::buttons::makeButton("Hide", ":/icons/eye-closed.svg", this); connect(hide_selected, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Selection", "Hide selected coming soon"); + viewport_controller_->hideSelectedElements(); }); auto* isolate_selected = components::buttons::makeButton("Isolate", ":/icons/eye-solid.svg", this); connect(isolate_selected, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Selection", "Isolate selected coming soon"); + viewport_controller_->isolateSelectedElements(); }); auto* show_all = components::buttons::makeButton("Show All", ":/icons/eye.svg", this); connect(show_all, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Selection", "Show all coming soon"); + viewport_controller_->showAllElements(); }); auto* invert_selection = components::buttons::makeButton("Invert", ":/icons/intersect.svg", this); connect(invert_selection, &QToolButton::clicked, this, [this]() { @@ -253,15 +277,15 @@ QWidget* MainWindow::buildInspectRibbonPage() { auto* distance = components::buttons::makeButton("Distance", ":/icons/select-edge3d.svg", this); connect(distance, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Measure", "Distance coming soon"); + viewport_controller_->toggleDistanceMode(); }); auto* area = components::buttons::makeButton("Area", ":/icons/select-face3d.svg", this); connect(area, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Measure", "Area coming soon"); + viewport_controller_->toggleAreaMode(); }); auto* volume = components::buttons::makeButton("Volume", ":/icons/select-point3d.svg", this); connect(volume, &QToolButton::clicked, this, [this]() { - session_state_->setStatusMessage("Measure", "Volume coming soon"); + viewport_controller_->toggleVolumeMode(); }); row->addWidget(components::buttons::makeButtonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}, this)); diff --git a/src/interface/modules/viewport/Controller.cpp b/src/interface/modules/viewport/Controller.cpp index 84153949f9..edfe8fb04f 100644 --- a/src/interface/modules/viewport/Controller.cpp +++ b/src/interface/modules/viewport/Controller.cpp @@ -26,8 +26,13 @@ #include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/SceneLoader.h" #include "../../../ifcviewer/ViewportWindow.h" +#include "../../../ifcviewer/OverlayRenderer.h" +#include "../../../ifcviewer-full/Measurement.h" #include +#include + +#include namespace ifcinterface::modules::viewport { @@ -37,6 +42,8 @@ ViewportController::ViewportController(ifcinterface::SessionState* session_state : QObject(parent) , session_state_(session_state) , viewport_(viewport) + , area_measurement_(std::make_unique()) + , length_measurement_(std::make_unique()) { Federation* federation = session_state_->federation(); SceneLoader* loader = session_state_->loader(); @@ -91,8 +98,60 @@ ViewportController::ViewportController(ifcinterface::SessionState* session_state applyModelVisibility(mid); maybeGuessFederatedFalseOrigin(mid); }); + connect(viewport_, &ViewportWindow::surfacePickedInTool, this, + [this](int x, int y, int modifiers) { + const bool alt = (modifiers & Qt::AltModifier) != 0; + switch (viewport_->toolMode()) { + case ViewportWindow::ToolMode::Area: + area_measurement_->onPick(*viewport_, x, y, alt); + viewport_->setHudText(QString("Area: %1 m² (%2 tris)") + .arg(area_measurement_->totalArea(), 0, 'f', 4) + .arg(area_measurement_->triangleCount())); + break; + case ViewportWindow::ToolMode::Length: + length_measurement_->onPick(*viewport_, x, y, alt); + break; + case ViewportWindow::ToolMode::None: + case ViewportWindow::ToolMode::Volume: + break; + } + }); + connect(viewport_, &ViewportWindow::toolModeChanged, this, + [this](ViewportWindow::ToolMode mode) { + area_measurement_->clear(*viewport_); + length_measurement_->clear(*viewport_); + switch (mode) { + case ViewportWindow::ToolMode::None: + viewport_->setHudText(QString()); + viewport_->setOverlayLabels({}); + session_state_->setStatusMessage("Measure", "Measurement tool off"); + break; + case ViewportWindow::ToolMode::Length: + viewport_->setHudText("Length tool: click first point"); + session_state_->setStatusMessage("Measure", "Length tool: LMB add point, Backspace remove last, Esc exits"); + break; + case ViewportWindow::ToolMode::Area: + viewport_->setHudText("Area: 0.0000 m² (0 tris)"); + session_state_->setStatusMessage("Measure", "Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits"); + break; + case ViewportWindow::ToolMode::Volume: + session_state_->setStatusMessage("Measure", "Volume tool: click / box-select objects, Esc exits"); + updateVolumeReadout(); + break; + } + }); + connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() { + if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) { + length_measurement_->removeLastPoint(*viewport_); + } + }); + connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) { + updateVolumeReadout(); + }); } +ViewportController::~ViewportController() = default; + void ViewportController::applyCoordinateOperation(uint32_t mid) { SceneLoader* loader = session_state_->loader(); Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity(); @@ -170,6 +229,72 @@ void ViewportController::goHomeView() { session_state_->setStatusMessage("Camera", "Home view restored"); } +void ViewportController::setFlyMode() { + viewport_->requestActivate(); + viewport_->enterFpsMode(); + session_state_->setStatusMessage("Mode", "Fly mode active"); +} + +void ViewportController::toggleDistanceMode() { + viewport_->toggleLengthTool(); +} + +void ViewportController::toggleAreaMode() { + viewport_->toggleAreaTool(); +} + +void ViewportController::toggleVolumeMode() { + viewport_->toggleVolumeTool(); +} + +void ViewportController::hideSelectedElements() { + viewport_->hideSelectedElements(); +} + +void ViewportController::isolateSelectedElements() { + viewport_->isolateSelectedElements(); +} + +void ViewportController::showAllElements() { + viewport_->showAllElements(); +} + +void ViewportController::updateVolumeReadout() { + if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return; + + const auto& sel = viewport_->selection().selectionIds(); + if (sel.empty()) { + viewport_->setHudText(QString()); + viewport_->setOverlayLabels({}); + return; + } + + std::vector ids(sel.begin(), sel.end()); + const auto per_obj = volumesPerObject(*viewport_, ids); + + double total = 0.0; + std::vector labels; + labels.reserve(per_obj.size()); + for (const auto& [oid, v] : per_obj) { + total += v; + QVector3D mn, mx; + if (!viewport_->computeObjectAabb(oid, mn, mx)) continue; + OverlayRenderer::Label lbl; + const QVector3D c = (mn + mx) * 0.5f; + lbl.world_pos[0] = c.x(); + lbl.world_pos[1] = c.y(); + lbl.world_pos[2] = c.z(); + lbl.text = QString::number(v, 'f', 4) + " m³"; + labels.push_back(std::move(lbl)); + } + + viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)") + .arg(total, 0, 'f', 4) + .arg(per_obj.size()) + .arg(per_obj.size() == 1 ? "" : "s")); + viewport_->setOverlayLabels(labels); +} + void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) { Federation* federation = session_state_->federation(); SceneLoader* loader = session_state_->loader(); diff --git a/src/interface/modules/viewport/Controller.h b/src/interface/modules/viewport/Controller.h index c473624ef7..56c39d2032 100644 --- a/src/interface/modules/viewport/Controller.h +++ b/src/interface/modules/viewport/Controller.h @@ -22,9 +22,12 @@ #define IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H #include +#include namespace ifcinterface { class SessionState; } class ViewportWindow; +class AreaMeasurement; +class LengthMeasurement; namespace ifcinterface::modules::viewport { @@ -35,19 +38,30 @@ public: explicit ViewportController(ifcinterface::SessionState* session_state, ViewportWindow* viewport, QObject* parent = nullptr); + ~ViewportController() override; void applyFederatedFalseOrigin(); void setHomeView(); void goHomeView(); + void setFlyMode(); + void toggleDistanceMode(); + void toggleAreaMode(); + void toggleVolumeMode(); + void hideSelectedElements(); + void isolateSelectedElements(); + void showAllElements(); private: void applyCoordinateOperation(uint32_t mid); void applyModelTransformation(uint32_t mid); void applyModelVisibility(uint32_t mid); void maybeGuessFederatedFalseOrigin(uint32_t mid); + void updateVolumeReadout(); ifcinterface::SessionState* session_state_ = nullptr; ViewportWindow* viewport_ = nullptr; + std::unique_ptr area_measurement_; + std::unique_ptr length_measurement_; }; } // namespace ifcinterface::modules::viewport