mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user