From 06eca938d74e50c1bb91c50be51155085e8d1e2f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 11 Apr 2026 16:30:10 +1000 Subject: [PATCH] Dump of hello world ifc viewer code --- cmake/CMakeLists.txt | 4 + src/ifcviewer/AppSettings.cpp | 57 +++ src/ifcviewer/AppSettings.h | 48 ++ src/ifcviewer/CMakeLists.txt | 61 +++ src/ifcviewer/GeometryStreamer.cpp | 285 ++++++++++++ src/ifcviewer/GeometryStreamer.h | 89 ++++ src/ifcviewer/MainWindow.cpp | 270 ++++++++++++ src/ifcviewer/MainWindow.h | 80 ++++ src/ifcviewer/README.md | 129 ++++++ src/ifcviewer/SettingsWindow.cpp | 68 +++ src/ifcviewer/SettingsWindow.h | 46 ++ src/ifcviewer/ViewportWindow.cpp | 674 +++++++++++++++++++++++++++++ src/ifcviewer/ViewportWindow.h | 146 +++++++ src/ifcviewer/main.cpp | 55 +++ 14 files changed, 2012 insertions(+) create mode 100644 src/ifcviewer/AppSettings.cpp create mode 100644 src/ifcviewer/AppSettings.h create mode 100644 src/ifcviewer/CMakeLists.txt create mode 100644 src/ifcviewer/GeometryStreamer.cpp create mode 100644 src/ifcviewer/GeometryStreamer.h create mode 100644 src/ifcviewer/MainWindow.cpp create mode 100644 src/ifcviewer/MainWindow.h create mode 100644 src/ifcviewer/README.md create mode 100644 src/ifcviewer/SettingsWindow.cpp create mode 100644 src/ifcviewer/SettingsWindow.h create mode 100644 src/ifcviewer/ViewportWindow.cpp create mode 100644 src/ifcviewer/ViewportWindow.h create mode 100644 src/ifcviewer/main.cpp diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 502d23b4cc..69667eff07 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -71,6 +71,7 @@ option(BUILD_EXAMPLES "Build example applications." ON) option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON) option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF) option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6 +option(BUILD_IFCVIEWER "Build IfcViewer, a high-performance IFC viewer" OFF) # Requires Qt6 + OpenGL 4.5 option(BUILD_PACKAGE "" OFF) option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON) @@ -671,6 +672,9 @@ if(BUILD_IFCGEOM) install(TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom) endif(BUILD_IFCGEOM) +if(BUILD_IFCVIEWER) + add_subdirectory(../src/ifcviewer ifcviewer) +endif() # Cmake uninstall target if(NOT TARGET uninstall) diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp new file mode 100644 index 0000000000..07c5f8c3bc --- /dev/null +++ b/src/ifcviewer/AppSettings.cpp @@ -0,0 +1,57 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "AppSettings.h" + +#include + +namespace { +constexpr const char* kGeometryLibraryKey = "geometry/library"; +constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade"; +} + +AppSettings& AppSettings::instance() { + static AppSettings inst; + return inst; +} + +AppSettings::AppSettings() { + load(); +} + +QString AppSettings::geometryLibrary() const { + return geometry_library_; +} + +void AppSettings::setGeometryLibrary(const QString& value) { + if (geometry_library_ == value) return; + geometry_library_ = value; + persist(); + emit geometryLibraryChanged(value); +} + +void AppSettings::load() { + QSettings settings; + geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString(); +} + +void AppSettings::persist() { + QSettings settings; + settings.setValue(kGeometryLibraryKey, geometry_library_); +} diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h new file mode 100644 index 0000000000..9658c10b95 --- /dev/null +++ b/src/ifcviewer/AppSettings.h @@ -0,0 +1,48 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef APPSETTINGS_H +#define APPSETTINGS_H + +#include +#include + +// Application-wide preferences. Cached in memory, persisted via QSettings to +// the OS-native config location (registry on Windows, plist on macOS, INI on +// Linux). Access via AppSettings::instance(). +class AppSettings : public QObject { + Q_OBJECT +public: + static AppSettings& instance(); + + QString geometryLibrary() const; + void setGeometryLibrary(const QString& value); + +signals: + void geometryLibraryChanged(const QString& value); + +private: + AppSettings(); + void load(); + void persist(); + + QString geometry_library_; +}; + +#endif // APPSETTINGS_H diff --git a/src/ifcviewer/CMakeLists.txt b/src/ifcviewer/CMakeLists.txt new file mode 100644 index 0000000000..9f1c4dac50 --- /dev/null +++ b/src/ifcviewer/CMakeLists.txt @@ -0,0 +1,61 @@ +################################################################################ +# # +# This file is part of IfcOpenShell. # +# # +# IfcOpenShell is free software: you can redistribute it and/or modify # +# it under the terms of the Lesser GNU General Public License as published by # +# the Free Software Foundation, either version 3.0 of the License, or # +# (at your option) any later version. # +# # +# IfcOpenShell is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# Lesser GNU General Public License for more details. # +# # +# You should have received a copy of the Lesser GNU General Public License # +# along with this program. If not, see . # +# # +################################################################################ + +message("Running CMakeLists.txt in /src/ifcviewer") + +set(QT_VERSION 6 CACHE STRING "Qt version") +# IfcViewer always needs OpenGL in addition to Core/Gui/Widgets. We don't use +# the CACHE'd QT_COMPONENTS here because it may have been set by another target +# (e.g. qtviewer) without the OpenGL component. +find_package(Qt${QT_VERSION} COMPONENTS Core Gui Widgets OpenGL REQUIRED PATHS ${QT_DIR}) + +find_package(OpenGL REQUIRED) + +file(GLOB IFCVIEWER_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) +file(GLOB IFCVIEWER_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h) +set(IFCVIEWER_FILES ${IFCVIEWER_CPP_FILES} ${IFCVIEWER_H_FILES}) + +add_executable(IfcViewer ${IFCVIEWER_FILES}) + +set_target_properties(IfcViewer PROPERTIES + AUTOMOC ON + WIN32_EXECUTABLE ON + MACOSX_BUNDLE ON +) + +target_link_libraries(IfcViewer PRIVATE + IfcGeom + IfcParse + ${kernel_libraries} + ${OpenCASCADE_LIBRARIES} + ${Boost_LIBRARIES} + ${CGAL_LIBRARIES} + Qt${QT_VERSION}::Core + Qt${QT_VERSION}::Gui + Qt${QT_VERSION}::Widgets + Qt${QT_VERSION}::OpenGL + OpenGL::GL +) + +if(UNIX AND NOT APPLE) + find_package(Threads REQUIRED) + target_link_libraries(IfcViewer PRIVATE Threads::Threads) +endif() + +install(TARGETS IfcViewer EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}) diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp new file mode 100644 index 0000000000..39698c84e6 --- /dev/null +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -0,0 +1,285 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "GeometryStreamer.h" +#include "AppSettings.h" +#include "../ifcgeom/hybrid_kernel.h" + +#include +#include +#include +#include +#include + +GeometryStreamer::GeometryStreamer(QObject* parent) + : QObject(parent) +{ +} + +GeometryStreamer::~GeometryStreamer() { + cancel(); + if (worker_thread_ && worker_thread_->isRunning()) { + worker_thread_->quit(); + worker_thread_->wait(); + } +} + +void GeometryStreamer::loadFile(const std::string& path, int num_threads) { + if (running_.load()) { + cancel(); + if (worker_thread_ && worker_thread_->isRunning()) { + worker_thread_->quit(); + worker_thread_->wait(); + } + } + + cancel_requested_ = false; + running_ = true; + progress_ = 0; + next_object_id_ = 1; + + { + std::lock_guard lock(elements_mutex_); + pending_elements_.clear(); + } + + if (num_threads <= 0) { + num_threads = std::max(1u, std::thread::hardware_concurrency()); + } + + worker_thread_ = std::make_unique(); + QObject* context = new QObject(); + context->moveToThread(worker_thread_.get()); + + connect(worker_thread_.get(), &QThread::started, context, [this, path, num_threads, context]() { + run(path, num_threads); + context->deleteLater(); + worker_thread_->quit(); + }); + + connect(worker_thread_.get(), &QThread::finished, this, [this]() { + running_ = false; + emit finished(); + }); + + worker_thread_->start(); +} + +void GeometryStreamer::cancel() { + cancel_requested_ = true; +} + +std::vector GeometryStreamer::drainElements() { + std::lock_guard lock(elements_mutex_); + std::vector result; + result.swap(pending_elements_); + return result; +} + +void GeometryStreamer::run(const std::string& path, int num_threads) { + try { + ifc_file_ = std::make_unique(path); + } catch (const std::exception& e) { + emit errorOccurred(QString("Failed to parse IFC file: %1").arg(e.what())); + return; + } + + ifcopenshell::geometry::Settings settings; + settings.set("use-world-coords", true); + settings.set("weld-vertices", false); + settings.set("apply-default-materials", true); + + std::unique_ptr iterator; + try { + const std::string geometry_library = + AppSettings::instance().geometryLibrary().toStdString(); + auto kernel = ifcopenshell::geometry::kernels::construct( + ifc_file_.get(), geometry_library, settings); + iterator = std::make_unique( + std::move(kernel), settings, ifc_file_.get(), std::vector(), num_threads); + } catch (const std::exception& e) { + emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what())); + return; + } + + if (!iterator->initialize()) { + emit errorOccurred("No geometry found in IFC file"); + return; + } + + int last_progress = 0; + + do { + if (cancel_requested_.load()) break; + + const IfcGeom::Element* elem = iterator->get(); + if (!elem) continue; + + const auto* tri_elem = dynamic_cast(elem); + if (!tri_elem) continue; + + uint32_t object_id = next_object_id_++; + + // Record element metadata + ElementInfo info; + info.object_id = object_id; + info.ifc_id = tri_elem->id(); + info.guid = tri_elem->guid(); + info.name = tri_elem->name(); + info.type = tri_elem->type(); + info.parent_id = tri_elem->parent_id(); + + { + std::lock_guard lock(elements_mutex_); + pending_elements_.push_back(std::move(info)); + } + + // Convert geometry to upload chunk + UploadChunk chunk = convertElement(tri_elem, object_id); + if (!chunk.indices.empty()) { + emit elementReady(std::move(chunk)); + } + + int p = iterator->progress(); + if (p != last_progress) { + last_progress = p; + progress_ = p; + emit progressChanged(p); + } + } while (iterator->next()); + + progress_ = 100; + emit progressChanged(100); +} + +static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) { + MaterialInfo m; + if (!style) return m; + + const auto& color = style->get_color(); + if (color) { + m.r = static_cast(color.r()); + m.g = static_cast(color.g()); + m.b = static_cast(color.b()); + } + if (!std::isnan(style->transparency)) { + m.a = 1.0f - static_cast(style->transparency); + } + return m; +} + +static inline uint32_t packRGBA8(const MaterialInfo& m) { + auto to_byte = [](float v) -> uint32_t { + float c = std::clamp(v, 0.0f, 1.0f); + return static_cast(c * 255.0f + 0.5f); + }; + uint32_t r = to_byte(m.r); + uint32_t g = to_byte(m.g); + uint32_t b = to_byte(m.b); + uint32_t a = to_byte(m.a); + // Layout in memory (little-endian) reads as bytes [r, g, b, a] which is + // what the GL_UNSIGNED_BYTE * 4 normalized vertex attribute expects. + return r | (g << 8) | (b << 16) | (a << 24); +} + +UploadChunk GeometryStreamer::convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id) { + UploadChunk chunk; + chunk.object_id = object_id; + + const auto& geom = elem->geometry(); + const auto& verts = geom.verts(); + const auto& faces = geom.faces(); + const auto& normals = geom.normals(); + const auto& materials = geom.materials(); + const auto& material_ids = geom.material_ids(); + + if (verts.empty() || faces.empty()) return chunk; + + // Encode object_id as float bits for the vertex attribute + float id_as_float; + static_assert(sizeof(float) == sizeof(uint32_t)); + std::memcpy(&id_as_float, &object_id, sizeof(float)); + + const size_t num_verts = verts.size() / 3; + const size_t num_tris = faces.size() / 3; + const bool have_per_tri_material = (material_ids.size() == num_tris); + + // Per-vertex color requires that any vertex shared between triangles with + // *different* materials be split. We dedupe (orig_vert_idx, mat_id) pairs + // so vertices that are only ever used by one material stay shared. + auto make_key = [](uint32_t orig_idx, int mat_id) -> uint64_t { + return (static_cast(orig_idx) << 32) | + static_cast(mat_id); + }; + + std::unordered_map remap; + remap.reserve(num_verts); + + chunk.vertices.reserve(num_verts * 8); + chunk.indices.reserve(faces.size()); + + auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t { + const uint64_t key = make_key(orig_idx, mat_id); + auto it = remap.find(key); + if (it != remap.end()) return it->second; + + const uint32_t new_idx = static_cast(chunk.vertices.size() / 8); + + // pos + chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 0])); + chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 1])); + chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 2])); + + // normal + if (orig_idx * 3 + 2 < normals.size()) { + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 0])); + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 1])); + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 2])); + } else { + chunk.vertices.push_back(0.0f); + chunk.vertices.push_back(1.0f); + chunk.vertices.push_back(0.0f); + } + + // object_id (float bits) + chunk.vertices.push_back(id_as_float); + + // color (packed RGBA8 reinterpreted as float) + MaterialInfo m; + if (mat_id >= 0 && mat_id < static_cast(materials.size())) { + m = materialFromStyle(materials[mat_id]); + } + uint32_t packed = packRGBA8(m); + float packed_as_float; + std::memcpy(&packed_as_float, &packed, sizeof(float)); + chunk.vertices.push_back(packed_as_float); + + remap.emplace(key, new_idx); + return new_idx; + }; + + for (size_t t = 0; t < num_tris; ++t) { + const int mat_id = have_per_tri_material ? material_ids[t] : -1; + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 0]), mat_id)); + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 1]), mat_id)); + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 2]), mat_id)); + } + + return chunk; +} diff --git a/src/ifcviewer/GeometryStreamer.h b/src/ifcviewer/GeometryStreamer.h new file mode 100644 index 0000000000..06b6364a24 --- /dev/null +++ b/src/ifcviewer/GeometryStreamer.h @@ -0,0 +1,89 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef GEOMETRYSTREAMER_H +#define GEOMETRYSTREAMER_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../ifcparse/IfcFile.h" +#include "../ifcgeom/Iterator.h" + +#include "ViewportWindow.h" + +struct ElementInfo { + uint32_t object_id; + int ifc_id; + std::string guid; + std::string name; + std::string type; + int parent_id; +}; + +class GeometryStreamer : public QObject { + Q_OBJECT +public: + explicit GeometryStreamer(QObject* parent = nullptr); + ~GeometryStreamer(); + + void loadFile(const std::string& path, int num_threads = 0); + void cancel(); + + bool isRunning() const { return running_.load(); } + int progress() const { return progress_.load(); } + + IfcParse::IfcFile* ifcFile() const { return ifc_file_.get(); } + + // Thread-safe access to discovered elements + std::vector drainElements(); + +signals: + void progressChanged(int percent); + void elementReady(UploadChunk chunk); + void finished(); + void errorOccurred(const QString& message); + +private: + void run(const std::string& path, int num_threads); + + UploadChunk convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id); + + std::unique_ptr ifc_file_; + std::unique_ptr worker_thread_; + std::atomic running_{false}; + std::atomic cancel_requested_{false}; + std::atomic progress_{0}; + + std::mutex elements_mutex_; + std::vector pending_elements_; + + // Map from IFC product id to our compact object_id + uint32_t next_object_id_ = 1; // 0 = no object +}; + +#endif // GEOMETRYSTREAMER_H diff --git a/src/ifcviewer/MainWindow.cpp b/src/ifcviewer/MainWindow.cpp new file mode 100644 index 0000000000..1f32ce0877 --- /dev/null +++ b/src/ifcviewer/MainWindow.cpp @@ -0,0 +1,270 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "MainWindow.h" +#include "SettingsWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) +{ + setupUi(); + setupMenus(); + + streamer_ = new GeometryStreamer(this); + connect(streamer_, &GeometryStreamer::progressChanged, this, &MainWindow::onProgressChanged, Qt::QueuedConnection); + connect(streamer_, &GeometryStreamer::elementReady, this, &MainWindow::onElementReady, Qt::QueuedConnection); + connect(streamer_, &GeometryStreamer::finished, this, &MainWindow::onStreamingFinished, Qt::QueuedConnection); + connect(streamer_, &GeometryStreamer::errorOccurred, this, [this](const QString& msg) { + QMessageBox::warning(this, "Error", msg); + }, Qt::QueuedConnection); + + connect(&element_poll_timer_, &QTimer::timeout, this, &MainWindow::pollNewElements); + element_poll_timer_.setInterval(100); + + setWindowTitle("IfcViewer"); + resize(1400, 900); +} + +MainWindow::~MainWindow() {} + +void MainWindow::setupUi() { + // 3D Viewport as central widget + viewport_ = new ViewportWindow(); + viewport_container_ = QWidget::createWindowContainer(viewport_, this); + viewport_container_->setMinimumSize(400, 300); + viewport_container_->setFocusPolicy(Qt::StrongFocus); + setCentralWidget(viewport_container_); + + connect(viewport_, &ViewportWindow::objectPicked, this, &MainWindow::onObjectPicked); + + // Element tree dock + auto* tree_dock = new QDockWidget("Elements", this); + tree_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + element_tree_ = new QTreeWidget(); + element_tree_->setHeaderLabels({"Name", "Type", "GUID"}); + element_tree_->setColumnWidth(0, 200); + element_tree_->setColumnWidth(1, 120); + element_tree_->setSelectionMode(QAbstractItemView::SingleSelection); + connect(element_tree_, &QTreeWidget::itemSelectionChanged, this, &MainWindow::onTreeSelectionChanged); + tree_dock->setWidget(element_tree_); + addDockWidget(Qt::LeftDockWidgetArea, tree_dock); + + // Properties dock + auto* prop_dock = new QDockWidget("Properties", this); + prop_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + property_table_ = new QTableWidget(); + property_table_->setColumnCount(2); + property_table_->setHorizontalHeaderLabels({"Property", "Value"}); + property_table_->horizontalHeader()->setStretchLastSection(true); + property_table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + property_table_->setSelectionBehavior(QAbstractItemView::SelectRows); + prop_dock->setWidget(property_table_); + addDockWidget(Qt::RightDockWidgetArea, prop_dock); + + // Status bar with progress + progress_bar_ = new QProgressBar(); + progress_bar_->setMaximumWidth(200); + progress_bar_->setVisible(false); + status_label_ = new QLabel("Ready"); + statusBar()->addWidget(status_label_, 1); + statusBar()->addPermanentWidget(progress_bar_); +} + +void MainWindow::setupMenus() { + auto* file_menu = menuBar()->addMenu("&File"); + auto* open_action = file_menu->addAction("&Open...", this, &MainWindow::onFileOpen); + open_action->setShortcut(QKeySequence::Open); + file_menu->addAction("&Settings...", this, &MainWindow::onFileSettings); + file_menu->addSeparator(); + file_menu->addAction("&Quit", QKeySequence::Quit, qApp, &QApplication::quit); +} + +void MainWindow::onFileOpen() { + QString path = QFileDialog::getOpenFileName(this, "Open IFC File", QString(), "IFC Files (*.ifc *.ifcxml *.ifczip);;All Files (*)"); + if (!path.isEmpty()) { + openFile(path); + } +} + +void MainWindow::onFileSettings() { + if (settings_ == nullptr) { + settings_ = new SettingsWindow(this); + } + settings_->open(); + settings_->activateWindow(); + settings_->raise(); +} + +void MainWindow::openFile(const QString& path) { + viewport_->resetScene(); + element_tree_->clear(); + property_table_->setRowCount(0); + element_map_.clear(); + tree_items_.clear(); + ifc_id_to_object_id_.clear(); + + progress_bar_->setValue(0); + progress_bar_->setVisible(true); + status_label_->setText("Loading: " + path); + + load_timer_.restart(); + element_poll_timer_.start(); + streamer_->loadFile(path.toStdString()); +} + +void MainWindow::onProgressChanged(int percent) { + progress_bar_->setValue(percent); +} + +void MainWindow::onElementReady(UploadChunk chunk) { + viewport_->uploadChunk(chunk); +} + +void MainWindow::onStreamingFinished() { + element_poll_timer_.stop(); + pollNewElements(); // drain remaining + + progress_bar_->setVisible(false); + + qint64 ms = load_timer_.elapsed(); + QString elapsed = (ms >= 1000) + ? QString::number(ms / 1000.0, 'f', 2) + " s" + : QString::number(ms) + " ms"; + status_label_->setText(QString("Loaded %1 elements in %2") + .arg(element_map_.size()) + .arg(elapsed)); +} + +void MainWindow::onObjectPicked(uint32_t object_id) { + viewport_->setSelectedObjectId(object_id); + + // Select in tree + auto it = tree_items_.find(object_id); + if (it != tree_items_.end()) { + element_tree_->blockSignals(true); + element_tree_->setCurrentItem(it->second); + element_tree_->blockSignals(false); + } + + populateProperties(object_id); +} + +void MainWindow::onTreeSelectionChanged() { + auto items = element_tree_->selectedItems(); + if (items.isEmpty()) return; + + uint32_t object_id = items.first()->data(0, Qt::UserRole).toUInt(); + viewport_->setSelectedObjectId(object_id); + populateProperties(object_id); +} + +void MainWindow::pollNewElements() { + auto elements = streamer_->drainElements(); + for (auto& info : elements) { + element_map_[info.object_id] = info; + ifc_id_to_object_id_[info.ifc_id] = info.object_id; + + // Find parent tree item + QTreeWidgetItem* parent_item = nullptr; + auto parent_obj_it = ifc_id_to_object_id_.find(info.parent_id); + if (parent_obj_it != ifc_id_to_object_id_.end()) { + auto tree_it = tree_items_.find(parent_obj_it->second); + if (tree_it != tree_items_.end()) { + parent_item = tree_it->second; + } + } + + QString display_name = QString::fromStdString(info.name); + if (display_name.isEmpty()) { + display_name = QString::fromStdString(info.type) + " #" + QString::number(info.ifc_id); + } + + QTreeWidgetItem* item; + if (parent_item) { + item = new QTreeWidgetItem(parent_item); + } else { + item = new QTreeWidgetItem(element_tree_); + } + item->setText(0, display_name); + item->setText(1, QString::fromStdString(info.type)); + item->setText(2, QString::fromStdString(info.guid)); + item->setData(0, Qt::UserRole, info.object_id); + + tree_items_[info.object_id] = item; + } +} + +void MainWindow::populateProperties(uint32_t object_id) { + property_table_->setRowCount(0); + if (object_id == 0) return; + + auto it = element_map_.find(object_id); + if (it == element_map_.end()) return; + + const auto& info = it->second; + + auto addRow = [this](const QString& key, const QString& value) { + int row = property_table_->rowCount(); + property_table_->insertRow(row); + property_table_->setItem(row, 0, new QTableWidgetItem(key)); + property_table_->setItem(row, 1, new QTableWidgetItem(value)); + }; + + addRow("IFC ID", QString::number(info.ifc_id)); + addRow("GUID", QString::fromStdString(info.guid)); + addRow("Name", QString::fromStdString(info.name)); + addRow("Type", QString::fromStdString(info.type)); + + // If the file is loaded, try to get property sets + auto* file = streamer_->ifcFile(); + if (!file) return; + + auto* product = file->instance_by_id(info.ifc_id); + if (!product) return; + + // Show all direct attributes + auto& decl = product->declaration(); + if (auto* entity = decl.as_entity()) { + for (size_t i = 0; i < entity->attribute_count(); ++i) { + auto* attr = entity->attribute_by_index(i); + try { + auto val = product->get_attribute_value(i); + if (!val.isNull()) { + std::string str_val; + try { + str_val = static_cast(val); + } catch (...) { + // Not a string-convertible attribute (entity ref, aggregate, etc.) + str_val = "<" + std::string(IfcUtil::ArgumentTypeToString(val.type())) + ">"; + } + addRow(QString::fromStdString(attr->name()), QString::fromStdString(str_val)); + } + } catch (...) {} + } + } +} diff --git a/src/ifcviewer/MainWindow.h b/src/ifcviewer/MainWindow.h new file mode 100644 index 0000000000..d5f4c18a39 --- /dev/null +++ b/src/ifcviewer/MainWindow.h @@ -0,0 +1,80 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ViewportWindow.h" +#include "GeometryStreamer.h" + +class SettingsWindow; + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + explicit MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + + void openFile(const QString& path); + +private slots: + void onFileOpen(); + void onFileSettings(); + void onProgressChanged(int percent); + void onElementReady(UploadChunk chunk); + void onStreamingFinished(); + void onObjectPicked(uint32_t object_id); + void onTreeSelectionChanged(); + void pollNewElements(); + +private: + void setupUi(); + void setupMenus(); + void populateProperties(uint32_t object_id); + + ViewportWindow* viewport_ = nullptr; + SettingsWindow* settings_ = nullptr; + QWidget* viewport_container_ = nullptr; + QTreeWidget* element_tree_ = nullptr; + QTableWidget* property_table_ = nullptr; + QProgressBar* progress_bar_ = nullptr; + QLabel* status_label_ = nullptr; + QTimer element_poll_timer_; + QElapsedTimer load_timer_; + + GeometryStreamer* streamer_ = nullptr; + + // Map object_id -> tree item and element info + std::unordered_map element_map_; + std::unordered_map tree_items_; + std::unordered_map ifc_id_to_object_id_; +}; + +#endif // MAINWINDOW_H diff --git a/src/ifcviewer/README.md b/src/ifcviewer/README.md new file mode 100644 index 0000000000..b9194cefd1 --- /dev/null +++ b/src/ifcviewer/README.md @@ -0,0 +1,129 @@ +# IfcViewer + +A high-performance native IFC viewer built on IfcOpenShell's C++ geometry engine with a Qt6 interface and OpenGL 4.5 rendering. + +## Architecture + +``` ++-------------------------------------------+ +| Qt6 Application (MainWindow) | +| +----------+ +--------------------------+| +| | Element | | 3D Viewport || +| | Tree | | (QWindow + OpenGL 4.5) || +| | | | || +| +----------+ | Single VBO/EBO || +| | Property | | DrawElementsBaseVertex || +| | Table | | GPU pick pass || +| +----------+ +--------------------------+| +| | Status / Progress | ++-------------------------------------------+ + ^ ^ + | | + element metadata UploadChunks + | | ++-------------------------------------------+ +| GeometryStreamer (background QThread) | +| IfcGeom::Iterator with N threads | +| (one per CPU core by default) | ++-------------------------------------------+ +``` + +### Key design decisions + +- **QWindow viewport** embedded via `QWidget::createWindowContainer()`. This gives us a raw native surface for OpenGL, bypassing `QOpenGLWidget`'s compositor overhead. +- **One big vertex buffer + index buffer** (64 MB + 32 MB initial). Geometry is appended as it streams in. No per-object VBOs, no rebinding. +- **Interleaved vertex format**: position (3 floats) + normal (3 floats) + object ID (1 float, bitcast uint32) = 28 bytes per vertex. +- **GPU object picking**: a second render pass writes object IDs to an R32UI framebuffer. Click reads back one pixel. No CPU-side raycasting. +- **Multi-threaded tessellation**: `IfcGeom::Iterator` runs on a background thread and internally parallelizes geometry conversion across all CPU cores. +- **Non-blocking streaming**: the iterator emits `UploadChunk` signals via Qt's queued connection. The main thread uploads to the GPU without blocking iteration. +- **World coordinates**: geometry is emitted in world space (`use-world-coords=true`) so no per-object transform matrices are needed on the GPU. + +### Files + +| File | Purpose | +|------|---------| +| `main.cpp` | Application entry point, GL 4.5 surface format, CLI argument parsing | +| `MainWindow.h/cpp` | Qt main window: dockable element tree, property table, status bar, menus | +| `ViewportWindow.h/cpp` | OpenGL 4.5 Core renderer: shaders, buffer management, camera, picking | +| `GeometryStreamer.h/cpp` | Background geometry processing: loads IFC, runs iterator, emits chunks | +| `CMakeLists.txt` | Build configuration | + +## Dependencies + +- **Qt6** (Core, Gui, Widgets) +- **OpenGL 4.5** (GL_ARB_direct_state_access) - available on Windows and Linux; macOS will need a Vulkan/MoltenVK backend (not yet implemented) +- **IfcOpenShell C++ libraries** (IfcParse, IfcGeom, and their dependencies: Open CASCADE, Boost, Eigen3, optionally CGAL) + +## Building + +IfcViewer is built as part of the IfcOpenShell CMake project. You do not need to build everything - disable the targets you don't need. + +### Minimal build (IfcViewer only) + +From the repository root: + +```sh +mkdir build && cd build + +cmake ../cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_IFCVIEWER=ON \ + -DBUILD_CONVERT=OFF \ + -DBUILD_IFCPYTHON=OFF \ + -DBUILD_GEOMSERVER=OFF \ + -DBUILD_DOCUMENTATION=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DCOLLADA_SUPPORT=OFF \ + -DGLTF_SUPPORT=OFF \ + -DHDF5_SUPPORT=OFF + +make -j$(nproc) IfcViewer +``` + +This builds only IfcParse, IfcGeom (with geometry kernels), and IfcViewer itself. All other targets (IfcConvert, Python bindings, serializers, etc.) are skipped. + +If Qt6 is not in a standard location, pass `-DQT_DIR=/path/to/qt6`. + +### Full build with IfcViewer enabled + +```sh +cmake ../cmake -DBUILD_IFCVIEWER=ON +make -j$(nproc) +``` + +## Usage + +```sh +# Open a file directly +./IfcViewer model.ifc + +# Or use File -> Open from the menu +./IfcViewer +``` + +### Controls + +| Input | Action | +|-------|--------| +| Middle mouse drag | Orbit camera | +| Shift + middle mouse drag | Pan camera | +| Scroll wheel | Zoom | +| Left click | Select object (highlights in viewport and tree) | + +### Keyboard shortcuts + +| Key | Action | +|-----|--------| +| Ctrl+O | Open file | +| Ctrl+Q | Quit | + +## Roadmap + +- [ ] Material color support (currently renders default grey per batch) +- [ ] Buffer growth (reallocate when 64 MB VBO fills up) +- [ ] `glMultiDrawElementsIndirect` for fewer draw calls +- [ ] Vulkan/MoltenVK backend for macOS +- [ ] Spatial tree (BVH) for frustum culling +- [ ] LOD: coarse tessellation during streaming, refine in background +- [ ] Embedded Python scripting console +- [ ] CJK text input support (Qt6 handles this natively) diff --git a/src/ifcviewer/SettingsWindow.cpp b/src/ifcviewer/SettingsWindow.cpp new file mode 100644 index 0000000000..a24f9bc976 --- /dev/null +++ b/src/ifcviewer/SettingsWindow.cpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "SettingsWindow.h" +#include "AppSettings.h" + +#include +#include +#include +#include +#include + +SettingsWindow::SettingsWindow(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle("Settings"); + setupUi(); +} + +void SettingsWindow::setupUi() { + auto* form = new QFormLayout(); + + geometry_library_edit_ = new QLineEdit(this); + geometry_library_edit_->setMinimumWidth(280); + form->addRow("Geometry Library", geometry_library_edit_); + + auto* button_box = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + auto* root = new QVBoxLayout(this); + root->addLayout(form); + root->addWidget(button_box); + + connect(button_box, &QDialogButtonBox::accepted, this, &SettingsWindow::onAccepted); + connect(button_box, &QDialogButtonBox::rejected, this, &SettingsWindow::reject); +} + +void SettingsWindow::showEvent(QShowEvent* event) { + // Re-sync widgets from the persisted settings every time the dialog is + // shown, so a previous Cancel doesn't leave stale text in the field. + syncFromSettings(); + QDialog::showEvent(event); +} + +void SettingsWindow::syncFromSettings() { + geometry_library_edit_->setText(AppSettings::instance().geometryLibrary()); +} + +void SettingsWindow::onAccepted() { + AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text()); + accept(); +} diff --git a/src/ifcviewer/SettingsWindow.h b/src/ifcviewer/SettingsWindow.h new file mode 100644 index 0000000000..77affe7757 --- /dev/null +++ b/src/ifcviewer/SettingsWindow.h @@ -0,0 +1,46 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef SETTINGSWINDOW_H +#define SETTINGSWINDOW_H + +#include + +class QLineEdit; +class QShowEvent; + +class SettingsWindow : public QDialog { + Q_OBJECT +public: + explicit SettingsWindow(QWidget *parent = nullptr); + +protected: + void showEvent(QShowEvent* event) override; + +private slots: + void onAccepted(); + +private: + void setupUi(); + void syncFromSettings(); + + QLineEdit* geometry_library_edit_ = nullptr; +}; + +#endif diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp new file mode 100644 index 0000000000..99624cb9f5 --- /dev/null +++ b/src/ifcviewer/ViewportWindow.cpp @@ -0,0 +1,674 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "ViewportWindow.h" + +#include +#include +#include +#include +#include + +#include +#include + +static const size_t INITIAL_VBO_SIZE = 64 * 1024 * 1024; // 64 MB +static const size_t INITIAL_EBO_SIZE = 32 * 1024 * 1024; // 32 MB +// Cap buffer growth so a runaway upload can't try to allocate the world. +static const size_t MAX_BUFFER_SIZE = 4ull * 1024 * 1024 * 1024; // 4 GB +static const int VERTEX_STRIDE = 8; // pos(3) + normal(3) + object_id(1) + color(1 packed) + +static const char* MAIN_VERTEX_SHADER = R"( +#version 450 core +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec3 a_normal; +layout(location = 2) in float a_object_id; +layout(location = 3) in vec4 a_color; + +uniform mat4 u_view_projection; +uniform uint u_selected_id; + +out vec3 v_normal; +out vec3 v_position; +out vec4 v_color; +flat out uint v_object_id; +flat out uint v_selected; + +void main() { + gl_Position = u_view_projection * vec4(a_position, 1.0); + v_normal = a_normal; + v_position = a_position; + v_color = a_color; + v_object_id = floatBitsToUint(a_object_id); + v_selected = (v_object_id == u_selected_id) ? 1u : 0u; +} +)"; + +static const char* MAIN_FRAGMENT_SHADER = R"( +#version 450 core +in vec3 v_normal; +in vec3 v_position; +in vec4 v_color; +flat in uint v_object_id; +flat in uint v_selected; + +uniform vec3 u_light_dir; + +out vec4 frag_color; + +void main() { + vec3 n = normalize(v_normal); + float ndotl = max(dot(n, u_light_dir), 0.0); + float ambient = 0.25; + float diffuse = 0.75 * ndotl; + vec3 color = v_color.rgb * (ambient + diffuse); + + if (v_selected == 1u) { + color = mix(color, vec3(0.2, 0.6, 1.0), 0.5); + } + + frag_color = vec4(color, v_color.a); +} +)"; + +static const char* PICK_VERTEX_SHADER = R"( +#version 450 core +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec3 a_normal; +layout(location = 2) in float a_object_id; + +uniform mat4 u_view_projection; + +flat out uint v_object_id; + +void main() { + gl_Position = u_view_projection * vec4(a_position, 1.0); + v_object_id = floatBitsToUint(a_object_id); +} +)"; + +static const char* PICK_FRAGMENT_SHADER = R"( +#version 450 core +flat in uint v_object_id; + +out uint frag_id; + +void main() { + frag_id = v_object_id; +} +)"; + +static const char* AXIS_VERTEX_SHADER = R"( +#version 450 core +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec3 a_color; + +uniform mat4 u_mvp; + +out vec3 v_color; + +void main() { + gl_Position = u_mvp * vec4(a_position, 1.0); + v_color = a_color; +} +)"; + +static const char* AXIS_FRAGMENT_SHADER = R"( +#version 450 core +in vec3 v_color; +out vec4 frag_color; + +void main() { + frag_color = vec4(v_color, 1.0); +} +)"; + +static GLuint compileShader(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* source) { + GLuint shader = gl->glCreateShader(type); + gl->glShaderSource(shader, 1, &source, nullptr); + gl->glCompileShader(shader); + GLint ok = 0; + gl->glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[1024]; + gl->glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + qWarning("Shader compile error: %s", log); + } + return shader; +} + +static GLuint linkProgram(QOpenGLFunctions_4_5_Core* gl, GLuint vert, GLuint frag) { + GLuint prog = gl->glCreateProgram(); + gl->glAttachShader(prog, vert); + gl->glAttachShader(prog, frag); + gl->glLinkProgram(prog); + GLint ok = 0; + gl->glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + char log[1024]; + gl->glGetProgramInfoLog(prog, sizeof(log), nullptr, log); + qWarning("Program link error: %s", log); + } + gl->glDeleteShader(vert); + gl->glDeleteShader(frag); + return prog; +} + +ViewportWindow::ViewportWindow(QWindow* parent) + : QWindow(parent) +{ + setSurfaceType(QWindow::OpenGLSurface); + + QSurfaceFormat fmt; + fmt.setVersion(4, 5); + fmt.setProfile(QSurfaceFormat::CoreProfile); + fmt.setDepthBufferSize(24); + fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + fmt.setSamples(4); + setFormat(fmt); + + connect(&render_timer_, &QTimer::timeout, this, [this]() { + if (isExposed()) render(); + }); + render_timer_.setInterval(16); // ~60 fps +} + +ViewportWindow::~ViewportWindow() { + if (context_) { + context_->makeCurrent(this); + if (gl_) { + if (vao_) gl_->glDeleteVertexArrays(1, &vao_); + if (vbo_) gl_->glDeleteBuffers(1, &vbo_); + if (ebo_) gl_->glDeleteBuffers(1, &ebo_); + if (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_); + if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_); + if (main_program_) gl_->glDeleteProgram(main_program_); + if (pick_program_) gl_->glDeleteProgram(pick_program_); + if (axis_program_) gl_->glDeleteProgram(axis_program_); + if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_); + if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_); + if (pick_depth_rbo_) gl_->glDeleteRenderbuffers(1, &pick_depth_rbo_); + } + context_->doneCurrent(); + } +} + +void ViewportWindow::initGL() { + if (gl_initialized_) return; + + context_ = new QOpenGLContext(this); + context_->setFormat(requestedFormat()); + if (!context_->create()) { + qFatal("Failed to create OpenGL context"); + return; + } + context_->makeCurrent(this); + + gl_ = QOpenGLVersionFunctionsFactory::get(context_); + if (!gl_) { + qWarning("OpenGL 4.5 not available, falling back"); + return; + } + + buildShaders(); + buildAxisGizmo(); + + // Create VAO + gl_->glCreateVertexArrays(1, &vao_); + + // Create VBO with initial capacity + vbo_capacity_ = INITIAL_VBO_SIZE; + gl_->glCreateBuffers(1, &vbo_); + gl_->glNamedBufferStorage(vbo_, vbo_capacity_, nullptr, + GL_DYNAMIC_STORAGE_BIT); + + // Create EBO with initial capacity + ebo_capacity_ = INITIAL_EBO_SIZE; + gl_->glCreateBuffers(1, &ebo_); + gl_->glNamedBufferStorage(ebo_, ebo_capacity_, nullptr, + GL_DYNAMIC_STORAGE_BIT); + + // Vertex layout: pos(3f) + normal(3f) + object_id(1f) + color(4 unorm bytes) + // = 8 floats = 32 bytes per vertex. + gl_->glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, VERTEX_STRIDE * sizeof(float)); + gl_->glVertexArrayElementBuffer(vao_, ebo_); + + // position + gl_->glEnableVertexArrayAttrib(vao_, 0); + gl_->glVertexArrayAttribFormat(vao_, 0, 3, GL_FLOAT, GL_FALSE, 0); + gl_->glVertexArrayAttribBinding(vao_, 0, 0); + + // normal + gl_->glEnableVertexArrayAttrib(vao_, 1); + gl_->glVertexArrayAttribFormat(vao_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float)); + gl_->glVertexArrayAttribBinding(vao_, 1, 0); + + // object_id (passed as float, decoded in shader via floatBitsToUint) + gl_->glEnableVertexArrayAttrib(vao_, 2); + gl_->glVertexArrayAttribFormat(vao_, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float)); + gl_->glVertexArrayAttribBinding(vao_, 2, 0); + + // color (RGBA8 packed into the 4 bytes at offset 28; normalized to vec4) + gl_->glEnableVertexArrayAttrib(vao_, 3); + gl_->glVertexArrayAttribFormat(vao_, 3, 4, GL_UNSIGNED_BYTE, GL_TRUE, 7 * sizeof(float)); + gl_->glVertexArrayAttribBinding(vao_, 3, 0); + + gl_->glEnable(GL_DEPTH_TEST); + gl_->glEnable(GL_MULTISAMPLE); + gl_->glClearColor(0.18f, 0.20f, 0.22f, 1.0f); + + gl_initialized_ = true; + frame_clock_.start(); + render_timer_.start(); + + emit initialized(); +} + +void ViewportWindow::buildShaders() { + { + GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, MAIN_VERTEX_SHADER); + GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, MAIN_FRAGMENT_SHADER); + main_program_ = linkProgram(gl_, vs, fs); + } + { + GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, PICK_VERTEX_SHADER); + GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, PICK_FRAGMENT_SHADER); + pick_program_ = linkProgram(gl_, vs, fs); + } + { + GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, AXIS_VERTEX_SHADER); + GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, AXIS_FRAGMENT_SHADER); + axis_program_ = linkProgram(gl_, vs, fs); + } +} + +void ViewportWindow::buildAxisGizmo() { + // 3 line segments (X red, Y green, Z blue), 6 vertices, pos(3) + color(3). + static const float axis_data[] = { + // X axis - red + 0.0f, 0.0f, 0.0f, 1.0f, 0.25f, 0.25f, + 1.0f, 0.0f, 0.0f, 1.0f, 0.25f, 0.25f, + // Y axis - green + 0.0f, 0.0f, 0.0f, 0.30f, 0.95f, 0.30f, + 0.0f, 1.0f, 0.0f, 0.30f, 0.95f, 0.30f, + // Z axis - blue + 0.0f, 0.0f, 0.0f, 0.30f, 0.55f, 1.0f, + 0.0f, 0.0f, 1.0f, 0.30f, 0.55f, 1.0f, + }; + + gl_->glCreateVertexArrays(1, &axis_vao_); + gl_->glCreateBuffers(1, &axis_vbo_); + gl_->glNamedBufferStorage(axis_vbo_, sizeof(axis_data), axis_data, 0); + + gl_->glVertexArrayVertexBuffer(axis_vao_, 0, axis_vbo_, 0, 6 * sizeof(float)); + + gl_->glEnableVertexArrayAttrib(axis_vao_, 0); + gl_->glVertexArrayAttribFormat(axis_vao_, 0, 3, GL_FLOAT, GL_FALSE, 0); + gl_->glVertexArrayAttribBinding(axis_vao_, 0, 0); + + gl_->glEnableVertexArrayAttrib(axis_vao_, 1); + gl_->glVertexArrayAttribFormat(axis_vao_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float)); + gl_->glVertexArrayAttribBinding(axis_vao_, 1, 0); +} + +bool ViewportWindow::growVbo(size_t needed_total) { + // Double until it fits, but don't blow past the cap. + size_t new_capacity = vbo_capacity_; + while (new_capacity < needed_total) { + new_capacity *= 2; + } + if (new_capacity > MAX_BUFFER_SIZE) { + qWarning("VBO grow request (%zu MB) exceeds cap (%zu MB)", + new_capacity / (1024 * 1024), MAX_BUFFER_SIZE / (1024 * 1024)); + return false; + } + + GLuint new_vbo = 0; + gl_->glCreateBuffers(1, &new_vbo); + gl_->glNamedBufferStorage(new_vbo, new_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); + + if (vbo_used_ > 0) { + gl_->glCopyNamedBufferSubData(vbo_, new_vbo, 0, 0, vbo_used_); + } + + gl_->glDeleteBuffers(1, &vbo_); + vbo_ = new_vbo; + vbo_capacity_ = new_capacity; + + // Rebind on the VAO so subsequent draws see the new buffer. + gl_->glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, VERTEX_STRIDE * sizeof(float)); + + qInfo("VBO grew to %zu MB", vbo_capacity_ / (1024 * 1024)); + return true; +} + +bool ViewportWindow::growEbo(size_t needed_total) { + size_t new_capacity = ebo_capacity_; + while (new_capacity < needed_total) { + new_capacity *= 2; + } + if (new_capacity > MAX_BUFFER_SIZE) { + qWarning("EBO grow request (%zu MB) exceeds cap (%zu MB)", + new_capacity / (1024 * 1024), MAX_BUFFER_SIZE / (1024 * 1024)); + return false; + } + + GLuint new_ebo = 0; + gl_->glCreateBuffers(1, &new_ebo); + gl_->glNamedBufferStorage(new_ebo, new_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); + + if (ebo_used_ > 0) { + gl_->glCopyNamedBufferSubData(ebo_, new_ebo, 0, 0, ebo_used_); + } + + gl_->glDeleteBuffers(1, &ebo_); + ebo_ = new_ebo; + ebo_capacity_ = new_capacity; + + gl_->glVertexArrayElementBuffer(vao_, ebo_); + + qInfo("EBO grew to %zu MB", ebo_capacity_ / (1024 * 1024)); + return true; +} + +void ViewportWindow::uploadChunk(const UploadChunk& chunk) { + if (!gl_initialized_) return; + if (chunk.vertices.empty() || chunk.indices.empty()) return; + + context_->makeCurrent(this); + + size_t vb_size = chunk.vertices.size() * sizeof(float); + size_t ib_size = chunk.indices.size() * sizeof(uint32_t); + + if (vbo_used_ + vb_size > vbo_capacity_) { + if (!growVbo(vbo_used_ + vb_size)) { + qWarning("VBO at cap, skipping chunk"); + return; + } + } + if (ebo_used_ + ib_size > ebo_capacity_) { + if (!growEbo(ebo_used_ + ib_size)) { + qWarning("EBO at cap, skipping chunk"); + return; + } + } + + uint32_t base_vertex = vertex_count_; + + gl_->glNamedBufferSubData(vbo_, vbo_used_, vb_size, chunk.vertices.data()); + + // Remap chunk-local indices into global indices so the whole EBO can be + // drawn with a single glDrawElements call. + std::vector global_indices(chunk.indices.size()); + for (size_t i = 0; i < chunk.indices.size(); ++i) { + global_indices[i] = chunk.indices[i] + base_vertex; + } + gl_->glNamedBufferSubData(ebo_, ebo_used_, ib_size, global_indices.data()); + + { + std::lock_guard lock(upload_mutex_); + total_index_count_ += static_cast(chunk.indices.size()); + } + + vbo_used_ += vb_size; + ebo_used_ += ib_size; + vertex_count_ += static_cast(chunk.vertices.size() / VERTEX_STRIDE); + total_triangles_ += static_cast(chunk.indices.size() / 3); +} + +void ViewportWindow::resetScene() { + if (!gl_initialized_) return; + + std::lock_guard lock(upload_mutex_); + total_index_count_ = 0; + vbo_used_ = 0; + ebo_used_ = 0; + vertex_count_ = 0; + total_triangles_ = 0; + selected_object_id_ = 0; +} + +void ViewportWindow::setSelectedObjectId(uint32_t id) { + selected_object_id_ = id; +} + +uint32_t ViewportWindow::pickObjectAt(int x, int y) { + if (!gl_initialized_) return 0; + + context_->makeCurrent(this); + + int w = width() * devicePixelRatio(); + int h = height() * devicePixelRatio(); + + // Create/resize pick FBO if needed + if (pick_width_ != w || pick_height_ != h) { + if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_); + if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_); + if (pick_depth_rbo_) gl_->glDeleteRenderbuffers(1, &pick_depth_rbo_); + + gl_->glCreateFramebuffers(1, &pick_fbo_); + + gl_->glCreateTextures(GL_TEXTURE_2D, 1, &pick_color_tex_); + gl_->glTextureStorage2D(pick_color_tex_, 1, GL_R32UI, w, h); + gl_->glNamedFramebufferTexture(pick_fbo_, GL_COLOR_ATTACHMENT0, pick_color_tex_, 0); + + gl_->glCreateRenderbuffers(1, &pick_depth_rbo_); + gl_->glNamedRenderbufferStorage(pick_depth_rbo_, GL_DEPTH_COMPONENT24, w, h); + gl_->glNamedFramebufferRenderbuffer(pick_fbo_, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, pick_depth_rbo_); + + pick_width_ = w; + pick_height_ = h; + } + + renderPickPass(); + + int px = x * devicePixelRatio(); + int py = (height() - y) * devicePixelRatio(); + uint32_t pixel = 0; + gl_->glGetTextureSubImage(pick_color_tex_, 0, px, py, 0, 1, 1, 1, GL_RED_INTEGER, GL_UNSIGNED_INT, sizeof(pixel), &pixel); + + return pixel; +} + +void ViewportWindow::updateCamera() { + float yaw_rad = qDegreesToRadians(camera_yaw_); + float pitch_rad = qDegreesToRadians(camera_pitch_); + + // IFC / Blender convention: X right, Y forward, Z up. + QVector3D eye; + eye.setX(camera_target_.x() + camera_distance_ * cosf(pitch_rad) * cosf(yaw_rad)); + eye.setY(camera_target_.y() + camera_distance_ * cosf(pitch_rad) * sinf(yaw_rad)); + eye.setZ(camera_target_.z() + camera_distance_ * sinf(pitch_rad)); + + view_matrix_.setToIdentity(); + view_matrix_.lookAt(eye, camera_target_, QVector3D(0, 0, 1)); + + proj_matrix_.setToIdentity(); + float aspect = width() > 0 ? float(width()) / float(height()) : 1.0f; + proj_matrix_.perspective(45.0f, aspect, 0.1f, camera_distance_ * 10.0f); +} + +void ViewportWindow::render() { + if (!gl_initialized_ || !isExposed()) return; + + context_->makeCurrent(this); + updateCamera(); + + int w = width() * devicePixelRatio(); + int h = height() * devicePixelRatio(); + gl_->glViewport(0, 0, w, h); + gl_->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + QMatrix4x4 vp = proj_matrix_ * view_matrix_; + + gl_->glUseProgram(main_program_); + gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(main_program_, "u_view_projection"), 1, GL_FALSE, vp.constData()); + gl_->glUniform3f(gl_->glGetUniformLocation(main_program_, "u_light_dir"), 0.3f, 0.5f, 0.8f); + gl_->glUniform1ui(gl_->glGetUniformLocation(main_program_, "u_selected_id"), selected_object_id_); + + gl_->glBindVertexArray(vao_); + + { + std::lock_guard lock(upload_mutex_); + if (total_index_count_ > 0) { + gl_->glDrawElements(GL_TRIANGLES, total_index_count_, GL_UNSIGNED_INT, nullptr); + } + } + + renderAxisGizmo(); + + context_->swapBuffers(this); +} + +void ViewportWindow::renderAxisGizmo() { + if (!axis_program_ || !axis_vao_) return; + + const int dpr = devicePixelRatio(); + const int gizmo_size = 110 * dpr; + const int margin = 10 * dpr; + + gl_->glViewport(margin, margin, gizmo_size, gizmo_size); + gl_->glDisable(GL_DEPTH_TEST); + + // Build a view matrix from the same camera orientation but with a fixed + // close-up distance, so the gizmo rotates with the scene camera. Z-up. + float yaw_rad = qDegreesToRadians(camera_yaw_); + float pitch_rad = qDegreesToRadians(camera_pitch_); + + QVector3D eye_dir; + eye_dir.setX(cosf(pitch_rad) * cosf(yaw_rad)); + eye_dir.setY(cosf(pitch_rad) * sinf(yaw_rad)); + eye_dir.setZ(sinf(pitch_rad)); + + QMatrix4x4 gizmo_view; + gizmo_view.lookAt(eye_dir * 3.0f, QVector3D(0, 0, 0), QVector3D(0, 0, 1)); + + QMatrix4x4 gizmo_proj; + gizmo_proj.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); + + QMatrix4x4 mvp = gizmo_proj * gizmo_view; + + gl_->glUseProgram(axis_program_); + gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(axis_program_, "u_mvp"), 1, GL_FALSE, mvp.constData()); + + gl_->glLineWidth(2.5f); // ignored on some core-profile drivers, that's OK + gl_->glBindVertexArray(axis_vao_); + gl_->glDrawArrays(GL_LINES, 0, 6); + + gl_->glEnable(GL_DEPTH_TEST); +} + +void ViewportWindow::renderPickPass() { + gl_->glBindFramebuffer(GL_FRAMEBUFFER, pick_fbo_); + gl_->glViewport(0, 0, pick_width_, pick_height_); + + GLuint clear_val = 0; + gl_->glClearBufferuiv(GL_COLOR, 0, &clear_val); + gl_->glClear(GL_DEPTH_BUFFER_BIT); + + QMatrix4x4 vp = proj_matrix_ * view_matrix_; + gl_->glUseProgram(pick_program_); + gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(pick_program_, "u_view_projection"), 1, GL_FALSE, vp.constData()); + + gl_->glBindVertexArray(vao_); + + { + std::lock_guard lock(upload_mutex_); + if (total_index_count_ > 0) { + gl_->glDrawElements(GL_TRIANGLES, total_index_count_, GL_UNSIGNED_INT, nullptr); + } + } + + gl_->glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void ViewportWindow::exposeEvent(QExposeEvent*) { + if (isExposed() && !gl_initialized_) { + initGL(); + } +} + +void ViewportWindow::resizeEvent(QResizeEvent*) { + if (gl_initialized_) render(); +} + +bool ViewportWindow::event(QEvent* e) { + switch (e->type()) { + case QEvent::MouseButtonPress: + handleMousePress(static_cast(e)); + return true; + case QEvent::MouseButtonRelease: + handleMouseRelease(static_cast(e)); + return true; + case QEvent::MouseMove: + handleMouseMove(static_cast(e)); + return true; + case QEvent::Wheel: + handleWheel(static_cast(e)); + return true; + default: + return QWindow::event(e); + } +} + +void ViewportWindow::handleMousePress(QMouseEvent* e) { + active_button_ = e->button(); + last_mouse_pos_ = e->pos(); +} + +void ViewportWindow::handleMouseRelease(QMouseEvent* e) { + if (active_button_ == Qt::LeftButton && (e->pos() - last_mouse_pos_).manhattanLength() < 5) { + uint32_t id = pickObjectAt(e->pos().x(), e->pos().y()); + selected_object_id_ = id; + emit objectPicked(id); + } + active_button_ = Qt::NoButton; +} + +void ViewportWindow::handleMouseMove(QMouseEvent* e) { + QPoint delta = e->pos() - last_mouse_pos_; + last_mouse_pos_ = e->pos(); + + if (active_button_ == Qt::MiddleButton) { + if (e->modifiers() & Qt::ShiftModifier) { + // Pan in screen space, derived from the Z-up camera basis. + float pan_speed = camera_distance_ * 0.002f; + float yaw_rad = qDegreesToRadians(camera_yaw_); + float pitch_rad = qDegreesToRadians(camera_pitch_); + QVector3D right(-sinf(yaw_rad), cosf(yaw_rad), 0.0f); + QVector3D up( + -sinf(pitch_rad) * cosf(yaw_rad), + -sinf(pitch_rad) * sinf(yaw_rad), + cosf(pitch_rad)); + camera_target_ -= right * delta.x() * pan_speed; + camera_target_ += up * delta.y() * pan_speed; + } else { + // Orbit + camera_yaw_ -= delta.x() * 0.3f; + camera_pitch_ += delta.y() * 0.3f; + camera_pitch_ = qBound(-89.0f, camera_pitch_, 89.0f); + } + } +} + +void ViewportWindow::handleWheel(QWheelEvent* e) { + float factor = e->angleDelta().y() > 0 ? 0.9f : 1.1f; + camera_distance_ *= factor; + camera_distance_ = qMax(0.1f, camera_distance_); +} diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h new file mode 100644 index 0000000000..cb718050c8 --- /dev/null +++ b/src/ifcviewer/ViewportWindow.h @@ -0,0 +1,146 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef VIEWPORTWINDOW_H +#define VIEWPORTWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +struct MaterialInfo { + float r = 0.75f, g = 0.75f, b = 0.78f, a = 1.0f; +}; + +struct UploadChunk { + // Interleaved per-vertex layout (8 floats / 32 bytes per vertex): + // pos(3 float) + normal(3 float) + object_id(1 float bitcast from uint) + // + color(1 float holding RGBA8 packed bytes, read on the GPU as + // GL_UNSIGNED_BYTE * 4 normalized). + std::vector vertices; + std::vector indices; // local to this chunk's vertices + uint32_t object_id = 0; +}; + +class ViewportWindow : public QWindow { + Q_OBJECT +public: + explicit ViewportWindow(QWindow* parent = nullptr); + ~ViewportWindow(); + + void uploadChunk(const UploadChunk& chunk); + void resetScene(); + + void setSelectedObjectId(uint32_t id); + uint32_t pickObjectAt(int x, int y); + +signals: + void objectPicked(uint32_t object_id); + void initialized(); + +protected: + void exposeEvent(QExposeEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + bool event(QEvent* event) override; + +private: + void initGL(); + void render(); + void renderPickPass(); + void renderAxisGizmo(); + void updateCamera(); + void buildShaders(); + void buildAxisGizmo(); + bool growVbo(size_t needed_total); + bool growEbo(size_t needed_total); + + // Mouse interaction + void handleMousePress(QMouseEvent* event); + void handleMouseRelease(QMouseEvent* event); + void handleMouseMove(QMouseEvent* event); + void handleWheel(QWheelEvent* event); + + QOpenGLContext* context_ = nullptr; + QOpenGLFunctions_4_5_Core* gl_ = nullptr; + QTimer render_timer_; + QElapsedTimer frame_clock_; + bool gl_initialized_ = false; + + // Shaders + GLuint main_program_ = 0; + GLuint pick_program_ = 0; + GLuint axis_program_ = 0; + + // Axis gizmo (separate VAO/VBO since vertex layout differs from scene) + GLuint axis_vao_ = 0; + GLuint axis_vbo_ = 0; + + // Geometry buffers - one big buffer pair + GLuint vao_ = 0; + GLuint vbo_ = 0; + GLuint ebo_ = 0; + size_t vbo_capacity_ = 0; + size_t ebo_capacity_ = 0; + size_t vbo_used_ = 0; // in bytes + size_t ebo_used_ = 0; // in bytes + uint32_t vertex_count_ = 0; + + // Pick framebuffer + GLuint pick_fbo_ = 0; + GLuint pick_color_tex_ = 0; + GLuint pick_depth_rbo_ = 0; + int pick_width_ = 0; + int pick_height_ = 0; + + // The entire scene is a single mega-batch: per-vertex color removes the + // need to switch materials between draw calls. Indices are written into + // the EBO already offset by base_vertex so one glDrawElements covers all. + uint32_t total_index_count_ = 0; + std::mutex upload_mutex_; + + // Camera + QVector3D camera_target_{0, 0, 0}; + float camera_distance_ = 50.0f; + float camera_yaw_ = 45.0f; + float camera_pitch_ = 30.0f; + QMatrix4x4 view_matrix_; + QMatrix4x4 proj_matrix_; + + // Mouse state + Qt::MouseButton active_button_ = Qt::NoButton; + QPoint last_mouse_pos_; + + // Selection + uint32_t selected_object_id_ = 0; + bool pick_requested_ = false; + int pick_x_ = 0, pick_y_ = 0; + + // Stats + uint32_t total_triangles_ = 0; +}; + +#endif // VIEWPORTWINDOW_H diff --git a/src/ifcviewer/main.cpp b/src/ifcviewer/main.cpp new file mode 100644 index 0000000000..3bca693a37 --- /dev/null +++ b/src/ifcviewer/main.cpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include +#include +#include + +#include "MainWindow.h" + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + app.setApplicationName("IfcViewer"); + app.setOrganizationName("IfcOpenShell"); + + // Request OpenGL 4.5 Core globally + QSurfaceFormat fmt; + fmt.setVersion(4, 5); + fmt.setProfile(QSurfaceFormat::CoreProfile); + fmt.setDepthBufferSize(24); + fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + fmt.setSamples(4); + QSurfaceFormat::setDefaultFormat(fmt); + + QCommandLineParser parser; + parser.setApplicationDescription("IfcOpenShell IFC Viewer"); + parser.addHelpOption(); + parser.addPositionalArgument("file", "IFC file to open"); + parser.process(app); + + MainWindow window; + window.show(); + + auto args = parser.positionalArguments(); + if (!args.isEmpty()) { + window.openFile(args.first()); + } + + return app.exec(); +}