Add geometry database (.rdbview) export to IfcViewerFull

Wire a new "Export Geometry Database" tool button in AddModelDialog,
adjacent to "Convert IFC File to Database", to produce a zipped
read-only artifact combining a lossy RDB (with IfcRepresentationItem
stripped) and a .ifcview geometry sidecar. Intended for cloud
coordination workflows where parametric geometry editing is not needed.

Pipeline changes to support this:

- document_serializer_context gains a `skip_supertypes` field; the
  rdb plugin forwards it to RocksDbSerializer so the same registry
  path produces full or lossy RDBs.

- Vertex quantization helpers (octEncodeNormal + quantizeVertex) move
  out of ViewportWindow.cpp into a shared header so the sidecar's
  byte layout stays identical regardless of whether it came from a
  GPU readback or a CPU pipeline.

- New HeadlessSidecarBuilder runs a GeometryStreamer on the calling
  thread, captures MeshChunk/InstanceChunk into a SidecarData on the
  CPU, then computes georef + packed elements + LODs and writes the
  .ifcview — no ViewportWindow or GL context required.

The Controller's export flow runs RDB conversion + sidecar build +
QZipWriter packaging on a background QThread, writing through
`<dest>.tmp` then renaming for atomic appearance in cloud-sync
folders. ifcviewer-full now links Qt6::CorePrivate for QZipWriter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-15 11:54:14 +10:00
parent 56273de443
commit fed739847e
11 changed files with 575 additions and 48 deletions
+2 -1
View File
@@ -21,7 +21,7 @@
message("Running CMakeLists.txt in /src/ifcviewer-full") message("Running CMakeLists.txt in /src/ifcviewer-full")
set(QT_VERSION 6 CACHE STRING "Qt version") set(QT_VERSION 6 CACHE STRING "Qt version")
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Widgets Svg REQUIRED PATHS ${QT_DIR}) find_package(Qt${QT_VERSION} COMPONENTS Core CorePrivate Gui Widgets Svg REQUIRED PATHS ${QT_DIR})
set(IFCVIEWER_FULL_FILES set(IFCVIEWER_FULL_FILES
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
@@ -99,6 +99,7 @@ set_target_properties(IfcViewerFull PROPERTIES
target_link_libraries(IfcViewerFull PRIVATE target_link_libraries(IfcViewerFull PRIVATE
IfcViewer IfcViewer
Qt${QT_VERSION}::Core Qt${QT_VERSION}::Core
Qt${QT_VERSION}::CorePrivate
Qt${QT_VERSION}::Gui Qt${QT_VERSION}::Gui
Qt${QT_VERSION}::Svg Qt${QT_VERSION}::Svg
Qt${QT_VERSION}::Widgets Qt${QT_VERSION}::Widgets
@@ -129,8 +129,18 @@ void AddModelDialog::setupUi() {
"Convert IFC files to databases for smaller filesizes, reduced memory, and faster access. No data is lost.", "Convert IFC files to databases for smaller filesizes, reduced memory, and faster access. No data is lost.",
default_description)); default_description));
auto* export_geometry_database = components::buttons::makeButton("Export Geometry\nDatabase", ":/icons/database-restore.svg", choices);
connect(export_geometry_database, &QToolButton::clicked, this, [this]() {
selected_mode_ = SourceMode::ExportGeometryDatabase;
accept();
});
export_geometry_database->installEventFilter(new HoverDescriptionFilter(
description,
"Convert IFC files to a read-only geometry database for smaller filesizes, reduced memory, and faster access. Ideal for cloud read-only coordination workflows. Only parametric geometry editing capabilities are lost.",
default_description));
row->addWidget(components::buttons::makeButtonGroup("ADD", {add_ifc, add_database, add_geometry}, choices, true, 8)); row->addWidget(components::buttons::makeButtonGroup("ADD", {add_ifc, add_database, add_geometry}, choices, true, 8));
row->addWidget(components::buttons::makeButtonGroup("TOOLS", {convert_database}, choices, false, 8)); row->addWidget(components::buttons::makeButtonGroup("TOOLS", {convert_database, export_geometry_database}, choices, false, 8));
choices_section->addBodyWidget(choices); choices_section->addBodyWidget(choices);
addBodyWidget(description_section); addBodyWidget(description_section);
@@ -31,6 +31,7 @@ enum class SourceMode {
IfcDatabase, IfcDatabase,
GeometryOnly, GeometryOnly,
ConvertToDatabase, ConvertToDatabase,
ExportGeometryDatabase,
}; };
class AddModelDialog : public components::Dialog { class AddModelDialog : public components::Dialog {
@@ -27,6 +27,7 @@
#include "../../SessionState.h" #include "../../SessionState.h"
#include "AddModelDialog.h" #include "AddModelDialog.h"
#include "../../../ifcviewer/Federation.h" #include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/HeadlessSidecarBuilder.h"
#include "../../../ifcviewer/LodBuilder.h" #include "../../../ifcviewer/LodBuilder.h"
#include "../../../ifcviewer/SceneLoader.h" #include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/SidecarCache.h" #include "../../../ifcviewer/SidecarCache.h"
@@ -35,14 +36,21 @@
#include "../../../serializers/document_serializer_plugin.h" #include "../../../serializers/document_serializer_plugin.h"
#include <QDebug> #include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include <QListView> #include <QListView>
#include <QMessageBox> #include <QMessageBox>
#include <QProgressDialog> #include <QProgressDialog>
#include <QStandardPaths>
#include <QThread> #include <QThread>
#include <QTreeView> #include <QTreeView>
#include <QUuid>
#include <QtCore/private/qzipwriter_p.h>
#include <Eigen/Dense> #include <Eigen/Dense>
@@ -205,6 +213,9 @@ void ModelsPanelController::addFiles() {
case modules::models::SourceMode::ConvertToDatabase: case modules::models::SourceMode::ConvertToDatabase:
convertIfcToDatabase(); convertIfcToDatabase();
return; return;
case modules::models::SourceMode::ExportGeometryDatabase:
exportGeometryDatabase();
return;
case modules::models::SourceMode::None: case modules::models::SourceMode::None:
return; return;
} }
@@ -329,6 +340,192 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path
thread->start(); thread->start();
} }
void ModelsPanelController::exportGeometryDatabase() {
QFileDialog input_dialog(host_, "Select IFC File to Export");
input_dialog.setFileMode(QFileDialog::ExistingFile);
input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
input_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (input_dialog.exec() != QDialog::Accepted) return;
const QStringList inputs = input_dialog.selectedFiles();
if (inputs.isEmpty()) return;
const QString input_path = inputs.first();
const QFileInfo input_info(input_path);
const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdbview");
QFileDialog output_dialog(host_, "Save Geometry Database As");
output_dialog.setAcceptMode(QFileDialog::AcceptSave);
output_dialog.setFileMode(QFileDialog::AnyFile);
output_dialog.setNameFilter("Geometry Database (*.rdbview);;All Files (*)");
output_dialog.setDefaultSuffix("rdbview");
output_dialog.selectFile(default_output);
output_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (output_dialog.exec() != QDialog::Accepted) return;
const QStringList outputs = output_dialog.selectedFiles();
if (outputs.isEmpty()) return;
QString output_path = outputs.first();
if (!output_path.endsWith(".rdbview", Qt::CaseInsensitive)) {
output_path += ".rdbview";
}
runGeometryDatabaseExport(input_path, output_path);
}
void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, const QString& output_path) {
auto* progress = new QProgressDialog(host_);
progress->setWindowTitle("Export Geometry Database");
progress->setLabelText(QString("Exporting %1 to %2…")
.arg(QFileInfo(input_path).fileName(),
QFileInfo(output_path).fileName()));
progress->setRange(0, 0);
progress->setCancelButton(nullptr);
progress->setMinimumDuration(0);
progress->setWindowModality(Qt::ApplicationModal);
progress->setAutoClose(false);
progress->setAutoReset(false);
progress->show();
session_state_->setStatusMessage("Exporting",
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
auto timer = std::make_shared<QElapsedTimer>();
timer->start();
auto error_message = std::make_shared<QString>();
QThread* thread = QThread::create([input_path, output_path, error_message]() {
// Scratch dir holds the intermediate .ifcview and .rdb directory
// until they're zipped into the .rdbview. RAII-like cleanup at the
// bottom of this lambda; on early exception we leak it (cheap
// tradeoff to keep the failure log around for the user).
const QString tmp_root = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
.filePath(QString("ifcviewer-export-%1")
.arg(QUuid::createUuid().toString(QUuid::Id128)));
QDir().mkpath(tmp_root);
const QString tmp_anchor = QDir(tmp_root).filePath("model.ifc");
const QString tmp_sidecar = QDir(tmp_root).filePath("model.ifcview");
const QString tmp_rdb_dir = QDir(tmp_root).filePath("model.rdb");
try {
// Step 1: lossy RDB with IfcRepresentationItem stripped.
ifcopenshell::serializers::document_serializer_context context;
context.file = nullptr;
context.input_filename = input_path.toStdString();
context.output_filename = tmp_rdb_dir.toStdString();
context.stream = true;
context.skip_supertypes = { "IfcRepresentationItem" };
auto& registry = ifcopenshell::serializers::document_serializer_registry_instance();
const auto* info = registry.find("rdb");
if (!info) {
throw ifcopenshell::exception(
"No 'rdb' document serializer is registered. The RocksDB serializer plugin may not be installed.");
}
if (!info->supports_input_filename) {
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
}
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
serializer->finalize();
serializer.reset();
// Step 2: .ifcview sidecar via the headless builder.
HeadlessSidecarBuilder builder;
if (!builder.build(input_path, tmp_anchor)) {
throw ifcopenshell::exception(
("Sidecar build failed: " + builder.lastError()).toStdString());
}
if (!QFileInfo::exists(tmp_sidecar)) {
throw ifcopenshell::exception(
("Sidecar build reported success but " + tmp_sidecar + " is missing").toStdString());
}
// Step 3: zip the sidecar + RDB directory into the .rdbview.
// Write to a sibling `.tmp` then rename so a partial file never
// appears at the destination (matters for cloud-sync folders).
const QString tmp_zip = output_path + ".tmp";
QFile::remove(tmp_zip);
{
QZipWriter writer(tmp_zip);
if (writer.status() != QZipWriter::NoError) {
throw ifcopenshell::exception(
("Failed to open " + tmp_zip + " for writing").toStdString());
}
writer.setCompressionPolicy(QZipWriter::AutoCompress);
{
QFile sf(tmp_sidecar);
if (!sf.open(QIODevice::ReadOnly)) {
throw ifcopenshell::exception(
("Failed to read sidecar " + tmp_sidecar).toStdString());
}
writer.addFile("model.ifcview", sf.readAll());
}
QDirIterator it(tmp_rdb_dir, QDir::Files | QDir::NoDotAndDotDot,
QDirIterator::Subdirectories);
const QDir rdb_root(tmp_rdb_dir);
while (it.hasNext()) {
const QString file_path = it.next();
const QString rel = rdb_root.relativeFilePath(file_path);
QFile f(file_path);
if (!f.open(QIODevice::ReadOnly)) {
throw ifcopenshell::exception(
("Failed to read " + file_path + " for zip").toStdString());
}
writer.addFile(QString("model.rdb/%1").arg(rel), f.readAll());
}
writer.close();
if (writer.status() != QZipWriter::NoError) {
throw ifcopenshell::exception(
("Failed to finalize " + tmp_zip).toStdString());
}
}
QFile::remove(output_path);
if (!QFile::rename(tmp_zip, output_path)) {
QFile::remove(tmp_zip);
throw ifcopenshell::exception(
("Failed to move " + tmp_zip + " to " + output_path).toStdString());
}
} catch (const std::exception& e) {
*error_message = QString::fromUtf8(e.what());
} catch (...) {
*error_message = "Unknown error during geometry database export";
}
QDir(tmp_root).removeRecursively();
});
connect(thread, &QThread::finished, this,
[this, thread, progress, timer, error_message, input_path, output_path]() {
const qint64 elapsed = timer->elapsed();
progress->close();
progress->deleteLater();
thread->deleteLater();
if (!error_message->isEmpty()) {
session_state_->setStatusMessage("Error", *error_message);
QMessageBox::warning(host_, "Export Geometry Database",
QString("Export failed:\n%1").arg(*error_message));
return;
}
session_state_->setStatusMessage(
"Exported",
QString("%1 → %2 in %3")
.arg(QFileInfo(input_path).fileName(),
QFileInfo(output_path).fileName(),
formatElapsed(elapsed)));
QMessageBox::information(host_, "Export Geometry Database",
QString("Geometry database written to:\n%1").arg(output_path));
});
thread->start();
}
void ModelsPanelController::addFiles(const QStringList& paths) { void ModelsPanelController::addFiles(const QStringList& paths) {
QStringList accepted_paths; QStringList accepted_paths;
QStringList accepted_fed_ids; QStringList accepted_fed_ids;
@@ -52,11 +52,13 @@ public:
void removeLoadedModel(const QString& fed_id); void removeLoadedModel(const QString& fed_id);
void openSettings(); void openSettings();
void convertIfcToDatabase(); void convertIfcToDatabase();
void exportGeometryDatabase();
private: private:
QString formatElapsed(qint64 ms) const; QString formatElapsed(qint64 ms) const;
void writeSidecarForModel(SceneLoader* loader, uint32_t mid) const; void writeSidecarForModel(SceneLoader* loader, uint32_t mid) const;
void runIfcToDatabaseConversion(const QString& input_path, const QString& output_path); void runIfcToDatabaseConversion(const QString& input_path, const QString& output_path);
void runGeometryDatabaseExport(const QString& input_path, const QString& output_path);
QWidget* host_ = nullptr; QWidget* host_ = nullptr;
ModelsPanel* widget_ = nullptr; ModelsPanel* widget_ = nullptr;
+214
View File
@@ -0,0 +1,214 @@
/********************************************************************************
* *
* 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 "HeadlessSidecarBuilder.h"
#include "Federation.h"
#include "GeometryStreamer.h"
#include "LodBuilder.h"
#include "SidecarCache.h"
#include "VertexQuantization.h"
#include <QEventLoop>
#include <Eigen/Dense>
#include <cstring>
#include <limits>
HeadlessSidecarBuilder::HeadlessSidecarBuilder(QObject* parent)
: QObject(parent)
{
}
void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) {
if (chunk.vertices.empty() || chunk.indices.empty()) return;
// Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float).
const size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
// Recompute a tight local AABB from the actual vertex positions, same
// way ViewportWindow::uploadMeshChunk does so the .ifcview byte layout
// matches the GPU-readback path.
float bmin[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity() };
float bmax[3] = { -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
for (size_t i = 0; i < n_verts; ++i) {
const float* v = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
for (int a = 0; a < 3; ++a) {
if (v[a] < bmin[a]) bmin[a] = v[a];
if (v[a] > bmax[a]) bmax[a] = v[a];
}
}
float extent_recip[3];
for (int a = 0; a < 3; ++a) {
float ext = bmax[a] - bmin[a];
extent_recip[a] = ext > 0.0f ? 1.0f / ext : 0.0f;
}
const size_t vb_offset = sidecar_data_.vertices.size();
sidecar_data_.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
for (size_t i = 0; i < n_verts; ++i) {
quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
bmin, extent_recip,
sidecar_data_.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
}
const size_t ib_offset = sidecar_data_.indices.size();
sidecar_data_.indices.insert(sidecar_data_.indices.end(),
chunk.indices.begin(), chunk.indices.end());
MeshInfo info;
info.vbo_byte_offset = static_cast<uint32_t>(vb_offset);
info.vertex_count = static_cast<uint32_t>(n_verts);
info.ebo_byte_offset = static_cast<uint32_t>(ib_offset * sizeof(uint32_t));
info.index_count = static_cast<uint32_t>(chunk.indices.size());
for (int a = 0; a < 3; ++a) {
info.local_aabb_min[a] = bmin[a];
info.local_aabb_max[a] = bmax[a];
}
info.first_instance = 0;
info.instance_count = 0;
info.lod1_ebo_byte_offset = 0;
info.lod1_index_count = 0;
if (sidecar_data_.meshes.size() <= chunk.local_mesh_id) {
sidecar_data_.meshes.resize(chunk.local_mesh_id + 1);
}
sidecar_data_.meshes[chunk.local_mesh_id] = info;
}
void HeadlessSidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
InstanceCpu inst;
inst.mesh_id = chunk.local_mesh_id;
inst.object_id = chunk.object_id;
inst.color_override_rgba8 = chunk.color_override_rgba8;
inst.model_id = chunk.model_id;
// The streamer's chunk.transform is the placement_transformation. With
// identity stage matrices (no FederatedFalseOrigin / ModelTransformation
// / CoordinateOperation applied yet), transform == placement_transformation
// and chunk.world_aabb_* is already the world AABB. ViewportWindow's
// applyCachedModel will recompose against the consumer's stage matrices
// at load time, so the cached transform/world_aabb is just a sensible
// identity-stage baseline.
std::memcpy(inst.placement_transformation, chunk.transform,
sizeof(inst.placement_transformation));
std::memcpy(inst.transform, chunk.transform, sizeof(inst.transform));
std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min));
std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max));
sidecar_data_.instances.push_back(inst);
}
bool HeadlessSidecarBuilder::build(const QString& ifc_path,
const QString& anchor_path,
int num_threads) {
sidecar_data_ = SidecarData{};
last_error_.clear();
// Streamer lives on the calling thread; its worker_thread_ is its own
// internal QThread. AutoConnection routes meshReady/instanceReady
// through our local event loop.
GeometryStreamer streamer;
QEventLoop loop;
bool failed = false;
connect(&streamer, &GeometryStreamer::meshReady,
this, &HeadlessSidecarBuilder::onMeshReady);
connect(&streamer, &GeometryStreamer::instanceReady,
this, &HeadlessSidecarBuilder::onInstanceReady);
connect(&streamer, &GeometryStreamer::finished,
&loop, &QEventLoop::quit);
connect(&streamer, &GeometryStreamer::cancelled,
&loop, &QEventLoop::quit);
connect(&streamer, &GeometryStreamer::errorOccurred, this,
[&](const QString& msg) {
last_error_ = msg;
failed = true;
loop.quit();
});
streamer.loadFile(ifc_path.toStdString(),
/*start_object_id*/ 1,
/*model_id*/ 1,
num_threads);
loop.exec();
if (failed) return false;
// Per-mesh instance_count, matching ViewportWindow::finalizeModel.
for (auto& mesh : sidecar_data_.meshes) {
mesh.first_instance = 0;
mesh.instance_count = 0;
}
for (const auto& inst : sidecar_data_.instances) {
if (inst.mesh_id < sidecar_data_.meshes.size()) {
++sidecar_data_.meshes[inst.mesh_id].instance_count;
}
}
// CoordinateOperation cache from the IFC the streamer just parsed.
if (auto* file = streamer.ifcFile()) {
ModelGeoref georef = computeModelGeoref(file);
sidecar_data_.has_coordinate_operation = georef.has_coordinate_operation ? 1 : 0;
Eigen::Map<Eigen::Matrix<double, 4, 4, Eigen::ColMajor>>(
sidecar_data_.coordinate_operation_meters) = georef.coordinate_operation_meters;
sidecar_data_.project_length_to_meters = georef.units.project_length_to_meters;
sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters;
}
// Element metadata accumulated by the streamer's worker thread.
for (const auto& info : streamer.drainElements()) {
PackedElementInfo packed;
packed.object_id = info.object_id;
packed.model_id = info.model_id;
packed.ifc_id = info.ifc_id;
packed.parent_id = info.parent_id;
packed.guid_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
packed.guid_length = static_cast<uint32_t>(info.guid.size());
sidecar_data_.string_table += info.guid;
packed.name_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
packed.name_length = static_cast<uint32_t>(info.name.size());
sidecar_data_.string_table += info.name;
packed.type_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
packed.type_length = static_cast<uint32_t>(info.type.size());
sidecar_data_.string_table += info.type;
sidecar_data_.elements.push_back(packed);
}
buildLods(sidecar_data_);
if (!writeSidecar(anchor_path.toStdString(), sidecar_data_)) {
last_error_ = "writeSidecar failed";
return false;
}
return true;
}
+60
View File
@@ -0,0 +1,60 @@
/********************************************************************************
* *
* 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 HEADLESSSIDECARBUILDER_H
#define HEADLESSSIDECARBUILDER_H
#include "InstancedGeometry.h"
#include "SidecarCache.h"
#include <QObject>
#include <QString>
// Produces a .ifcview sidecar from an IFC file without touching the
// ViewportWindow or any GL state. Mirrors the data path that
// SceneLoader + ViewportWindow + ModelsPanelController.writeSidecarForModel
// take for live loads, but does the vertex quantization and SidecarData
// assembly entirely on the CPU.
//
// Threading: call ::build() from a non-GUI worker thread that has a Qt
// event dispatcher (e.g. the thread spawned by QThread::create). build()
// spins a local QEventLoop until the streamer's worker thread completes.
class HeadlessSidecarBuilder : public QObject {
Q_OBJECT
public:
explicit HeadlessSidecarBuilder(QObject* parent = nullptr);
// `anchor_path` is fed to writeSidecar(), which normalises it to
// `<stem(anchor_path)>.ifcview`. Pass either the IFC path itself
// (sidecar lands beside it) or a temp path whose stem you control.
bool build(const QString& ifc_path,
const QString& anchor_path,
int num_threads = 0);
const QString& lastError() const { return last_error_; }
private:
void onMeshReady(const MeshChunk& chunk);
void onInstanceReady(const InstanceChunk& chunk);
SidecarData sidecar_data_;
QString last_error_;
};
#endif // HEADLESSSIDECARBUILDER_H
+81
View File
@@ -0,0 +1,81 @@
/********************************************************************************
* *
* 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/>. *
* *
********************************************************************************/
// Inline helpers that turn streamer-format vertices (7 floats per vertex:
// pos3 + normal3 + color-as-float) into the 12 B quantized VBO layout used
// by both the viewport's GPU buffers and the .ifcview sidecar. Shared
// between ViewportWindow::uploadMeshChunk and HeadlessSidecarBuilder so
// the on-disk format stays identical to what the viewer would have
// produced via the GPU readback path.
#ifndef VERTEXQUANTIZATION_H
#define VERTEXQUANTIZATION_H
#include "InstancedGeometry.h"
#include <cmath>
#include <cstdint>
#include <cstring>
// Meyer et al. octahedral normal encode. Input unit vector -> [-1,1]^2.
inline void octEncodeNormal(const float n[3], float out[2]) {
float ax = std::fabs(n[0]), ay = std::fabs(n[1]), az = std::fabs(n[2]);
float denom = ax + ay + az;
if (denom < 1e-12f) { out[0] = 0.0f; out[1] = 0.0f; return; }
float px = n[0] / denom;
float py = n[1] / denom;
if (n[2] < 0.0f) {
float sx = px >= 0.0f ? 1.0f : -1.0f;
float sy = py >= 0.0f ? 1.0f : -1.0f;
float nx = (1.0f - std::fabs(py)) * sx;
float ny = (1.0f - std::fabs(px)) * sy;
px = nx; py = ny;
}
out[0] = px;
out[1] = py;
}
// Quantize a streamer-format vertex (pos3 + normal3 + color-as-float) into
// the 12 B VBO record, given the mesh's tight local AABB. `extent_recip`
// is 1/(max-min) per axis, or 0 for degenerate axes.
inline void quantizeVertex(const float src[INSTANCED_VERTEX_STRIDE_FLOATS],
const float aabb_min[3],
const float extent_recip[3],
uint8_t dst[INSTANCED_VERTEX_STRIDE_BYTES]) {
// Position -> u16 normalized.
uint16_t* p = reinterpret_cast<uint16_t*>(dst + INSTANCED_VERTEX_POS_OFFSET);
for (int a = 0; a < 3; ++a) {
float t = (src[a] - aabb_min[a]) * extent_recip[a];
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
p[a] = static_cast<uint16_t>(t * 65535.0f + 0.5f);
}
// Normal -> oct i8x2. int8 gives ~1.4° worst-case error — fine for BIM.
float oct[2];
octEncodeNormal(src + 3, oct);
int8_t* n = reinterpret_cast<int8_t*>(dst + INSTANCED_VERTEX_NORMAL_OFFSET);
for (int a = 0; a < 2; ++a) {
float v = oct[a];
if (v < -1.0f) v = -1.0f; else if (v > 1.0f) v = 1.0f;
n[a] = static_cast<int8_t>(std::lrintf(v * 127.0f));
}
// Color passes through — streamer packs 4 bytes into the 7th float slot.
std::memcpy(dst + INSTANCED_VERTEX_COLOR_OFFSET, src + 6, 4);
}
#endif // VERTEXQUANTIZATION_H
+1 -45
View File
@@ -20,6 +20,7 @@
#include "ViewportWindow.h" #include "ViewportWindow.h"
#include "AppSettings.h" #include "AppSettings.h"
#include "VertexQuantization.h"
#include <QMouseEvent> #include <QMouseEvent>
#include <QKeyEvent> #include <QKeyEvent>
@@ -492,51 +493,6 @@ static GLuint linkProgram(QOpenGLFunctions_4_5_Core* gl, GLuint vert, GLuint fra
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Meyer et al. octahedral normal encode. Input unit vector -> [-1,1]^2.
static void octEncode(const float n[3], float out[2]) {
float ax = std::fabs(n[0]), ay = std::fabs(n[1]), az = std::fabs(n[2]);
float denom = ax + ay + az;
if (denom < 1e-12f) { out[0] = 0.0f; out[1] = 0.0f; return; }
float px = n[0] / denom;
float py = n[1] / denom;
if (n[2] < 0.0f) {
float sx = px >= 0.0f ? 1.0f : -1.0f;
float sy = py >= 0.0f ? 1.0f : -1.0f;
float nx = (1.0f - std::fabs(py)) * sx;
float ny = (1.0f - std::fabs(px)) * sy;
px = nx; py = ny;
}
out[0] = px;
out[1] = py;
}
// Quantize a streamer-format vertex (pos3 + normal3 + color-as-float) into
// the 12 B VBO record, given the mesh's tight local AABB. `extent_recip`
// is 1/(max-min) per axis, or 0 for degenerate axes (quantum becomes 0).
static void quantizeVertex(const float src[7],
const float aabb_min[3],
const float extent_recip[3],
uint8_t dst[INSTANCED_VERTEX_STRIDE_BYTES]) {
// Position -> u16 normalized.
uint16_t* p = reinterpret_cast<uint16_t*>(dst + INSTANCED_VERTEX_POS_OFFSET);
for (int a = 0; a < 3; ++a) {
float t = (src[a] - aabb_min[a]) * extent_recip[a];
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
p[a] = static_cast<uint16_t>(t * 65535.0f + 0.5f);
}
// Normal -> oct i8x2. int8 gives ~1.4° worst-case error — fine for BIM.
float oct[2];
octEncode(src + 3, oct);
int8_t* n = reinterpret_cast<int8_t*>(dst + INSTANCED_VERTEX_NORMAL_OFFSET);
for (int a = 0; a < 2; ++a) {
float v = oct[a];
if (v < -1.0f) v = -1.0f; else if (v > 1.0f) v = 1.0f;
n[a] = static_cast<int8_t>(std::lrintf(v * 127.0f));
}
// Color passes through — streamer packs 4 bytes into the 7th float slot.
std::memcpy(dst + INSTANCED_VERTEX_COLOR_OFFSET, src + 6, 4);
}
// Determinant of the upper-left 3x3 of a column-major mat4 stored as 16 floats. // Determinant of the upper-left 3x3 of a column-major mat4 stored as 16 floats.
// Sign tells us whether the transform contains a reflection, which is what // Sign tells us whether the transform contains a reflection, which is what
// decides which glFrontFace winding to draw the instance with. // decides which glFrontFace winding to draw the instance with.
+1 -1
View File
@@ -31,7 +31,7 @@ boost::shared_ptr<Serializer> create_serializer(const ifcopenshell::serializers:
if (context.input_filename.empty()) { if (context.input_filename.empty()) {
throw ifcopenshell::exception("RocksDB document serializer requires an input filename"); throw ifcopenshell::exception("RocksDB document serializer requires an input filename");
} }
return boost::make_shared<RocksDbSerializer>(context.input_filename, context.output_filename); return boost::make_shared<RocksDbSerializer>(context.input_filename, context.output_filename, context.skip_supertypes);
} }
} }
@@ -53,6 +53,11 @@ struct SERIALIZERS_API document_serializer_context {
std::string schema_name; std::string schema_name;
bool stream = false; bool stream = false;
int dialect = 0; int dialect = 0;
// Supertype names to skip when streaming entities into the serializer.
// Currently only honoured by the RocksDB serializer; consumers building
// lossy/read-only databases pass e.g. {"IfcRepresentationItem"} to drop
// geometry definitions.
std::vector<std::string> skip_supertypes;
}; };
class SERIALIZERS_API document_serializer_registry { class SERIALIZERS_API document_serializer_registry {