mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-17 14:02:27 +00:00
Dump of hello world ifc viewer code
This commit is contained in:
@@ -71,6 +71,7 @@ option(BUILD_EXAMPLES "Build example applications." ON)
|
|||||||
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." 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_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_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(BUILD_PACKAGE "" OFF)
|
||||||
|
|
||||||
option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON)
|
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)
|
install(TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom)
|
||||||
endif(BUILD_IFCGEOM)
|
endif(BUILD_IFCGEOM)
|
||||||
|
if(BUILD_IFCVIEWER)
|
||||||
|
add_subdirectory(../src/ifcviewer ifcviewer)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Cmake uninstall target
|
# Cmake uninstall target
|
||||||
if(NOT TARGET uninstall)
|
if(NOT TARGET uninstall)
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include "AppSettings.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
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_);
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef APPSETTINGS_H
|
||||||
|
#define APPSETTINGS_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
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})
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include "GeometryStreamer.h"
|
||||||
|
#include "AppSettings.h"
|
||||||
|
#include "../ifcgeom/hybrid_kernel.h"
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<std::mutex> lock(elements_mutex_);
|
||||||
|
pending_elements_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_threads <= 0) {
|
||||||
|
num_threads = std::max(1u, std::thread::hardware_concurrency());
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_thread_ = std::make_unique<QThread>();
|
||||||
|
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<ElementInfo> GeometryStreamer::drainElements() {
|
||||||
|
std::lock_guard<std::mutex> lock(elements_mutex_);
|
||||||
|
std::vector<ElementInfo> result;
|
||||||
|
result.swap(pending_elements_);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||||
|
try {
|
||||||
|
ifc_file_ = std::make_unique<IfcParse::IfcFile>(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<IfcGeom::Iterator> 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<IfcGeom::Iterator>(
|
||||||
|
std::move(kernel), settings, ifc_file_.get(), std::vector<IfcGeom::filter_t>(), 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<const IfcGeom::TriangulationElement*>(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<std::mutex> 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<float>(color.r());
|
||||||
|
m.g = static_cast<float>(color.g());
|
||||||
|
m.b = static_cast<float>(color.b());
|
||||||
|
}
|
||||||
|
if (!std::isnan(style->transparency)) {
|
||||||
|
m.a = 1.0f - static_cast<float>(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<uint32_t>(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<uint64_t>(orig_idx) << 32) |
|
||||||
|
static_cast<uint32_t>(mat_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<uint64_t, uint32_t> 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<uint32_t>(chunk.vertices.size() / 8);
|
||||||
|
|
||||||
|
// pos
|
||||||
|
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 0]));
|
||||||
|
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 1]));
|
||||||
|
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 2]));
|
||||||
|
|
||||||
|
// normal
|
||||||
|
if (orig_idx * 3 + 2 < normals.size()) {
|
||||||
|
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
|
||||||
|
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
|
||||||
|
chunk.vertices.push_back(static_cast<float>(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<int>(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<uint32_t>(faces[t * 3 + 0]), mat_id));
|
||||||
|
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
|
||||||
|
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef GEOMETRYSTREAMER_H
|
||||||
|
#define GEOMETRYSTREAMER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
#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<ElementInfo> 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<IfcParse::IfcFile> ifc_file_;
|
||||||
|
std::unique_ptr<QThread> worker_thread_;
|
||||||
|
std::atomic<bool> running_{false};
|
||||||
|
std::atomic<bool> cancel_requested_{false};
|
||||||
|
std::atomic<int> progress_{0};
|
||||||
|
|
||||||
|
std::mutex elements_mutex_;
|
||||||
|
std::vector<ElementInfo> pending_elements_;
|
||||||
|
|
||||||
|
// Map from IFC product id to our compact object_id
|
||||||
|
uint32_t next_object_id_ = 1; // 0 = no object
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // GEOMETRYSTREAMER_H
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include "MainWindow.h"
|
||||||
|
#include "SettingsWindow.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QMenuBar>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStatusBar>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QDockWidget>
|
||||||
|
|
||||||
|
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<std::string>(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 (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef MAINWINDOW_H
|
||||||
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#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<uint32_t, ElementInfo> element_map_;
|
||||||
|
std::unordered_map<uint32_t, QTreeWidgetItem*> tree_items_;
|
||||||
|
std::unordered_map<int, uint32_t> ifc_id_to_object_id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // MAINWINDOW_H
|
||||||
@@ -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)
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include "SettingsWindow.h"
|
||||||
|
#include "AppSettings.h"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QShowEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef SETTINGSWINDOW_H
|
||||||
|
#define SETTINGSWINDOW_H
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include "ViewportWindow.h"
|
||||||
|
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QSurfaceFormat>
|
||||||
|
#include <QtMath>
|
||||||
|
#include <QtOpenGL/QOpenGLVersionFunctionsFactory>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<QOpenGLFunctions_4_5_Core>(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<uint32_t> 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<std::mutex> lock(upload_mutex_);
|
||||||
|
total_index_count_ += static_cast<uint32_t>(chunk.indices.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
vbo_used_ += vb_size;
|
||||||
|
ebo_used_ += ib_size;
|
||||||
|
vertex_count_ += static_cast<uint32_t>(chunk.vertices.size() / VERTEX_STRIDE);
|
||||||
|
total_triangles_ += static_cast<uint32_t>(chunk.indices.size() / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewportWindow::resetScene() {
|
||||||
|
if (!gl_initialized_) return;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<QMouseEvent*>(e));
|
||||||
|
return true;
|
||||||
|
case QEvent::MouseButtonRelease:
|
||||||
|
handleMouseRelease(static_cast<QMouseEvent*>(e));
|
||||||
|
return true;
|
||||||
|
case QEvent::MouseMove:
|
||||||
|
handleMouseMove(static_cast<QMouseEvent*>(e));
|
||||||
|
return true;
|
||||||
|
case QEvent::Wheel:
|
||||||
|
handleWheel(static_cast<QWheelEvent*>(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_);
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef VIEWPORTWINDOW_H
|
||||||
|
#define VIEWPORTWINDOW_H
|
||||||
|
|
||||||
|
#include <QWindow>
|
||||||
|
#include <QOpenGLContext>
|
||||||
|
#include <QtOpenGL/QOpenGLFunctions_4_5_Core>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QMatrix4x4>
|
||||||
|
#include <QVector3D>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
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<float> vertices;
|
||||||
|
std::vector<uint32_t> 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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QSurfaceFormat>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user