mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Unify sidecar production via SidecarBuilder
Renamed HeadlessSidecarBuilder to SidecarBuilder and reused it for live loads. SceneLoader now constructs one per stream load, forwards meshReady /instanceReady chunks alongside the viewport upload, and finalizes + writes the sidecar at onStreamerFinished — no more GPU readback path via ViewportWindow::snapshotModel (removed). Same code path now produces sidecars for both live loads and the .rdbview offline export. Sidecar use is opt-in per direction via SceneLoader::setShouldReadSidecar and setShouldWriteSidecar; both default off so embedders that don't want caching get a pure-streaming loader. ifcviewer-full and ifcviewer-minimal opt in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -470,8 +470,6 @@ void MainWindow::setupLoader() {
|
||||
session_state_->createLoader(viewport_widget_->viewport());
|
||||
viewport_view_ = new modules::viewport::ViewportView(
|
||||
session_state_, viewport_widget_->viewport(), this);
|
||||
modules::models::commands::addHandlers(
|
||||
*session_state_, *viewport_widget_->viewport(), *this);
|
||||
|
||||
// Load errors surface through SessionState as a session-level signal; the
|
||||
// status text + progress are already cleared there, we only show the modal.
|
||||
|
||||
@@ -36,6 +36,8 @@ SessionState::SessionState(QObject* parent)
|
||||
void SessionState::createLoader(ViewportWindow* viewport) {
|
||||
Q_ASSERT(!loader_);
|
||||
loader_ = new SceneLoader(viewport, this);
|
||||
loader_->setShouldReadSidecar(true);
|
||||
loader_->setShouldWriteSidecar(true);
|
||||
element_registry_->bindLoader(loader_);
|
||||
|
||||
auto format_elapsed = [](qint64 ms) {
|
||||
@@ -69,7 +71,6 @@ void SessionState::createLoader(ViewportWindow* viewport) {
|
||||
.arg(format_elapsed(elapsed_ms)));
|
||||
endProgress();
|
||||
emit modelGeometryReady(mid);
|
||||
emit modelGeometryStreamed(mid);
|
||||
});
|
||||
connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t mid) {
|
||||
setStatusMessage("Cancelled", loader_->displayName(mid));
|
||||
|
||||
@@ -90,10 +90,6 @@ signals:
|
||||
// for both sidecar-cache and stream loads; subscribers that just need to
|
||||
// re-derive view state (e.g. ViewportView::refresh) listen to this.
|
||||
void modelGeometryReady(uint32_t model_id);
|
||||
// Fires only after a stream load — i.e. when no sidecar cache existed
|
||||
// yet. The sidecar-write subscriber listens to this so it doesn't
|
||||
// re-persist a cache that was just read from disk.
|
||||
void modelGeometryStreamed(uint32_t model_id);
|
||||
// Fires when SceneLoader reports a load failure. SessionState turns the
|
||||
// raw loader signal into a session-level one so views (e.g. the MessageBox)
|
||||
// can subscribe without touching the loader directly.
|
||||
|
||||
@@ -26,10 +26,8 @@
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/HeadlessSidecarBuilder.h"
|
||||
#include "../../../ifcviewer/LodBuilder.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/SidecarCache.h"
|
||||
#include "../../../ifcviewer/SidecarBuilder.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
#include "../../../ifcgeom/Serializer.h"
|
||||
#include "../../../serializers/document_serializer_plugin.h"
|
||||
@@ -173,11 +171,6 @@ void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QStri
|
||||
s.setStatusMessage("Models", "Model removed");
|
||||
}
|
||||
|
||||
void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context) {
|
||||
QObject::connect(&s, &SessionState::modelGeometryStreamed, &context,
|
||||
[&s, &vp](uint32_t mid) { writeSidecarForLoadedModel(s, vp, mid); });
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) {
|
||||
@@ -432,7 +425,7 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) {
|
||||
serializer->finalize();
|
||||
serializer.reset();
|
||||
|
||||
HeadlessSidecarBuilder builder;
|
||||
SidecarBuilder builder;
|
||||
if (!builder.build(input_path, tmp_anchor)) {
|
||||
throw ifcopenshell::exception(
|
||||
("Sidecar build failed: " + builder.lastError()).toStdString());
|
||||
@@ -531,64 +524,4 @@ void openSettings(SessionState& s, QWidget& host) {
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid) {
|
||||
SceneLoader* loader = s.loader();
|
||||
if (!loader) return;
|
||||
|
||||
SidecarData sidecar_data;
|
||||
if (!vp.snapshotModel(mid, sidecar_data)) return;
|
||||
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (auto* element_registry = s.elementRegistry()) {
|
||||
for (const auto& info : element_registry->basicElementInfoForModel(mid)) {
|
||||
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;
|
||||
|
||||
const std::string guid = info.guid.toStdString();
|
||||
packed.guid_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.guid_length = static_cast<uint32_t>(guid.size());
|
||||
sidecar_data.string_table += guid;
|
||||
|
||||
const std::string name = info.name.toStdString();
|
||||
packed.name_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.name_length = static_cast<uint32_t>(name.size());
|
||||
sidecar_data.string_table += name;
|
||||
|
||||
const std::string type = info.type.toStdString();
|
||||
packed.type_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.type_length = static_cast<uint32_t>(type.size());
|
||||
sidecar_data.string_table += type;
|
||||
|
||||
sidecar_data.elements.push_back(packed);
|
||||
}
|
||||
}
|
||||
|
||||
QElapsedTimer lod_timer;
|
||||
lod_timer.start();
|
||||
buildLods(sidecar_data);
|
||||
const LodStats lod_stats = summariseLods(sidecar_data);
|
||||
qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 "
|
||||
"(%u tris -> %u tris for those meshes)",
|
||||
lod_timer.elapsed(),
|
||||
lod_stats.meshes_with_lod1, lod_stats.meshes_total,
|
||||
lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1);
|
||||
|
||||
vp.applyLodExtension(mid, sidecar_data);
|
||||
|
||||
QElapsedTimer sidecar_timer;
|
||||
sidecar_timer.start();
|
||||
const bool ok = writeSidecar(loader->filePath(mid).toStdString(), sidecar_data);
|
||||
qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED");
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::models::commands
|
||||
|
||||
@@ -47,17 +47,6 @@ void convertIfcToDatabase(SessionState& s, QWidget& host);
|
||||
void exportGeometryDatabase(SessionState& s, QWidget& host);
|
||||
void openSettings(SessionState& s, QWidget& host);
|
||||
|
||||
// Snapshots the in-memory geometry + element registry for a freshly streamed
|
||||
// model and persists it as a sidecar next to the source IFC. Called after
|
||||
// SceneLoader::loadedFromStream so subsequent loads can skip the stream phase.
|
||||
void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid);
|
||||
|
||||
// Persistent post-load handler. Call once at app startup. Whenever the
|
||||
// session reports a model was streamed (not loaded from cache), persists a
|
||||
// sidecar so the next load skips the stream phase. Connection lifetime is
|
||||
// tied to `context`.
|
||||
void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context);
|
||||
|
||||
// Internal building blocks shared by commands here and by ProjectController.
|
||||
// These NEVER call notify*() — the caller is responsible for emitting once
|
||||
// at the end of its execution.
|
||||
|
||||
@@ -39,6 +39,8 @@ MinimalWindow::MinimalWindow(QWidget* parent)
|
||||
statusBar()->addPermanentWidget(stats_label_);
|
||||
|
||||
loader_ = new SceneLoader(viewport_, this);
|
||||
loader_->setShouldReadSidecar(true);
|
||||
loader_->setShouldWriteSidecar(true);
|
||||
connect(loader_, &SceneLoader::loadStarted,
|
||||
this, &MinimalWindow::onLoadStarted);
|
||||
connect(loader_, &SceneLoader::loadedFromSidecar,
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
void SceneLoader::setShouldReadSidecar(bool enabled) {
|
||||
should_read_sidecar_ = enabled;
|
||||
}
|
||||
|
||||
void SceneLoader::setShouldWriteSidecar(bool enabled) {
|
||||
should_write_sidecar_ = enabled;
|
||||
}
|
||||
|
||||
SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent)
|
||||
: QObject(parent), viewport_(viewport)
|
||||
{
|
||||
@@ -169,6 +177,14 @@ void SceneLoader::startNextLoad() {
|
||||
const bool is_sidecar_source =
|
||||
QFileInfo(model.file_path).suffix().compare("ifcview", Qt::CaseInsensitive) == 0;
|
||||
|
||||
// No sidecar read probe when caching is off (and the user didn't pick a
|
||||
// .ifcview file directly). Skip the background thread and go straight
|
||||
// to a stream load.
|
||||
if (!is_sidecar_source && !should_read_sidecar_) {
|
||||
startStreamLoadFor(mid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sidecar read on a background thread so the UI stays responsive.
|
||||
joinSidecarThread();
|
||||
sidecar_read_thread_ = std::thread([this, ifc_path, mid, is_sidecar_source]() {
|
||||
@@ -195,15 +211,28 @@ void SceneLoader::startNextLoad() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& m = it->second;
|
||||
connectStreamer(m.streamer);
|
||||
element_poll_timer_.start();
|
||||
m.streamer->loadFile(
|
||||
m.file_path.toStdString(), next_object_id_, loading_model_id_);
|
||||
startStreamLoadFor(mid);
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void SceneLoader::startStreamLoadFor(uint32_t mid) {
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
auto& m = it->second;
|
||||
// Accumulate sidecar data alongside the GPU upload so the first load
|
||||
// naturally produces a cache for the next one — no GPU readback at
|
||||
// finish time. Skipped when caching writes are off.
|
||||
if (should_write_sidecar_) {
|
||||
m.sidecar_builder = std::make_unique<SidecarBuilder>();
|
||||
m.streamed_elements.clear();
|
||||
}
|
||||
connectStreamer(m.streamer);
|
||||
element_poll_timer_.start();
|
||||
m.streamer->loadFile(
|
||||
m.file_path.toStdString(), next_object_id_, loading_model_id_);
|
||||
}
|
||||
|
||||
void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
@@ -314,16 +343,27 @@ void SceneLoader::onStreamerProgressChanged(int percent) {
|
||||
|
||||
void SceneLoader::onStreamerMeshReady(MeshChunk chunk) {
|
||||
viewport_->uploadMeshChunk(chunk);
|
||||
if (loading_model_id_ != 0) {
|
||||
auto it = models_.find(loading_model_id_);
|
||||
if (it != models_.end() && it->second.sidecar_builder) {
|
||||
it->second.sidecar_builder->onMeshReady(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) {
|
||||
if (loading_model_id_ != 0) {
|
||||
auto it = models_.find(loading_model_id_);
|
||||
if (it != models_.end() && !it->second.has_first_placement) {
|
||||
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
|
||||
it->second.first_placement =
|
||||
Eigen::Map<const Mat4fCol>(chunk.transform).cast<double>();
|
||||
it->second.has_first_placement = true;
|
||||
if (it != models_.end()) {
|
||||
if (!it->second.has_first_placement) {
|
||||
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
|
||||
it->second.first_placement =
|
||||
Eigen::Map<const Mat4fCol>(chunk.transform).cast<double>();
|
||||
it->second.has_first_placement = true;
|
||||
}
|
||||
if (it->second.sidecar_builder) {
|
||||
it->second.sidecar_builder->onInstanceReady(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
viewport_->uploadInstanceChunk(chunk);
|
||||
@@ -335,9 +375,15 @@ void SceneLoader::onElementPollTick() {
|
||||
if (it == models_.end()) return;
|
||||
|
||||
auto batch = it->second.streamer->drainElements();
|
||||
if (!batch.empty()) {
|
||||
emit streamedElementsReady(loading_model_id_, std::move(batch));
|
||||
if (batch.empty()) return;
|
||||
|
||||
// Mirror into the per-model accumulator so finalize() has the full set
|
||||
// without re-draining (the streamer's queue is consumed by this drain).
|
||||
if (it->second.sidecar_builder) {
|
||||
auto& buf = it->second.streamed_elements;
|
||||
buf.insert(buf.end(), batch.begin(), batch.end());
|
||||
}
|
||||
emit streamedElementsReady(loading_model_id_, std::move(batch));
|
||||
}
|
||||
|
||||
void SceneLoader::onStreamerFinished() {
|
||||
@@ -348,10 +394,29 @@ void SceneLoader::onStreamerFinished() {
|
||||
if (mid != 0) {
|
||||
auto it = models_.find(mid);
|
||||
if (it != models_.end()) {
|
||||
next_object_id_ = it->second.streamer->lastObjectId();
|
||||
auto& m = it->second;
|
||||
next_object_id_ = m.streamer->lastObjectId();
|
||||
viewport_->finalizeModel(mid);
|
||||
|
||||
qint64 ms = it->second.load_timer.elapsed();
|
||||
// Sidecar finalize + LOD application + disk write. Runs on the
|
||||
// GUI thread so applyLodExtension's GL touches are safe.
|
||||
if (m.sidecar_builder) {
|
||||
ModelGeoref georef;
|
||||
if (auto* file = m.streamer->ifcFile()) {
|
||||
georef = computeModelGeoref(file);
|
||||
}
|
||||
QElapsedTimer wt; wt.start();
|
||||
SidecarData data = m.sidecar_builder->finalize(georef, m.streamed_elements);
|
||||
viewport_->applyLodExtension(mid, data);
|
||||
const bool ok = writeSidecar(m.file_path.toStdString(), data);
|
||||
qDebug(" Sidecar finalize + write: %lld ms (%s)",
|
||||
wt.elapsed(), ok ? "ok" : "FAILED");
|
||||
m.sidecar_builder.reset();
|
||||
m.streamed_elements.clear();
|
||||
m.streamed_elements.shrink_to_fit();
|
||||
}
|
||||
|
||||
qint64 ms = m.load_timer.elapsed();
|
||||
emit loadedFromStream(mid, ms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "Federation.h"
|
||||
#include "ViewportWindow.h"
|
||||
#include "GeometryStreamer.h"
|
||||
#include "SidecarBuilder.h"
|
||||
#include "SidecarCache.h"
|
||||
|
||||
// Drives IFC file loading into a ViewportWindow. Owns the per-model
|
||||
@@ -45,15 +47,27 @@
|
||||
//
|
||||
// Consumers (MainWindow, MinimalWindow) observe progress through signals
|
||||
// and never touch the streamer, sidecar thread, or queue directly.
|
||||
// Sidecar *writes* are intentionally left to the consumer: they need the
|
||||
// consumer's element metadata (guid/name/type strings) which SceneLoader
|
||||
// does not retain.
|
||||
// Sidecar *writes* happen automatically as a side effect of stream loads —
|
||||
// the SidecarBuilder accumulates from the streamer chunks alongside the
|
||||
// viewport upload, and SceneLoader finalizes + writes the result when the
|
||||
// stream finishes. No GPU readback involved.
|
||||
class SceneLoader : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SceneLoader(ViewportWindow* viewport, QObject* parent = nullptr);
|
||||
~SceneLoader();
|
||||
|
||||
// Sidecar cache use is opt-in per direction. Embedders that don't care
|
||||
// about .ifcview can leave both off (default) and SceneLoader will never
|
||||
// probe for or produce one. Toggles only affect *subsequent* loads;
|
||||
// a load already in flight finishes with whatever was set when it
|
||||
// started. Opening a `.ifcview` file directly always reads it,
|
||||
// regardless of these flags.
|
||||
void setShouldReadSidecar(bool enabled);
|
||||
void setShouldWriteSidecar(bool enabled);
|
||||
bool shouldReadSidecar() const { return should_read_sidecar_; }
|
||||
bool shouldWriteSidecar() const { return should_write_sidecar_; }
|
||||
|
||||
// Returns the model_ids assigned to the enqueued paths, in order.
|
||||
// Callers can use these to set up per-model UI state (tree roots, etc.)
|
||||
// before any load signal fires.
|
||||
@@ -146,9 +160,18 @@ private:
|
||||
// streamer's first InstanceChunk.
|
||||
Eigen::Matrix4d first_placement = Eigen::Matrix4d::Identity();
|
||||
bool has_first_placement = false;
|
||||
|
||||
// Live-load sidecar accumulator. Constructed at the start of a
|
||||
// stream load when shouldWriteSidecar is on; null otherwise.
|
||||
std::unique_ptr<SidecarBuilder> sidecar_builder;
|
||||
// Element batches accumulated as the streamer yields, mirrored from
|
||||
// what's emitted via streamedElementsReady so finalize() has the
|
||||
// full set without re-draining.
|
||||
std::vector<ElementInfo> streamed_elements;
|
||||
};
|
||||
|
||||
void startNextLoad();
|
||||
void startStreamLoadFor(uint32_t mid);
|
||||
void connectStreamer(GeometryStreamer* streamer);
|
||||
void joinSidecarThread();
|
||||
void joinDataSourceThreads();
|
||||
@@ -156,6 +179,8 @@ private:
|
||||
void startDataSourceLoad(uint32_t mid);
|
||||
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
bool should_read_sidecar_ = false;
|
||||
bool should_write_sidecar_ = false;
|
||||
std::map<uint32_t, Model> models_;
|
||||
std::deque<uint32_t> load_queue_;
|
||||
uint32_t next_model_id_ = 1;
|
||||
|
||||
@@ -17,10 +17,9 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "HeadlessSidecarBuilder.h"
|
||||
#include "SidecarBuilder.h"
|
||||
|
||||
#include "Federation.h"
|
||||
#include "GeometryStreamer.h"
|
||||
#include "LodBuilder.h"
|
||||
#include "SidecarCache.h"
|
||||
#include "VertexQuantization.h"
|
||||
@@ -31,13 +30,14 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
HeadlessSidecarBuilder::HeadlessSidecarBuilder(QObject* parent)
|
||||
SidecarBuilder::SidecarBuilder(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
if (chunk.vertices.empty() || chunk.indices.empty()) return;
|
||||
|
||||
// Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float).
|
||||
@@ -45,7 +45,7 @@ void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
|
||||
// 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.
|
||||
// matches the live-render path.
|
||||
float bmin[3] = { std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity() };
|
||||
@@ -98,7 +98,7 @@ void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
sidecar_data_.meshes[chunk.local_mesh_id] = info;
|
||||
}
|
||||
|
||||
void HeadlessSidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
|
||||
void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
|
||||
InstanceCpu inst;
|
||||
inst.mesh_id = chunk.local_mesh_id;
|
||||
inst.object_id = chunk.object_id;
|
||||
@@ -121,44 +121,8 @@ void HeadlessSidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
|
||||
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;
|
||||
|
||||
SidecarData SidecarBuilder::finalize(const ModelGeoref& georef,
|
||||
const std::vector<ElementInfo>& elements) {
|
||||
// Per-mesh instance_count, matching ViewportWindow::finalizeModel.
|
||||
for (auto& mesh : sidecar_data_.meshes) {
|
||||
mesh.first_instance = 0;
|
||||
@@ -170,18 +134,13 @@ bool HeadlessSidecarBuilder::build(const QString& ifc_path,
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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()) {
|
||||
for (const auto& info : elements) {
|
||||
PackedElementInfo packed;
|
||||
packed.object_id = info.object_id;
|
||||
packed.model_id = info.model_id;
|
||||
@@ -205,7 +164,51 @@ bool HeadlessSidecarBuilder::build(const QString& ifc_path,
|
||||
|
||||
buildLods(sidecar_data_);
|
||||
|
||||
if (!writeSidecar(anchor_path.toStdString(), sidecar_data_)) {
|
||||
return std::exchange(sidecar_data_, SidecarData{});
|
||||
}
|
||||
|
||||
bool SidecarBuilder::build(const QString& ifc_path,
|
||||
const QString& anchor_path,
|
||||
int num_threads) {
|
||||
sidecar_data_ = SidecarData{};
|
||||
last_error_.clear();
|
||||
|
||||
GeometryStreamer streamer;
|
||||
QEventLoop loop;
|
||||
bool failed = false;
|
||||
|
||||
connect(&streamer, &GeometryStreamer::meshReady,
|
||||
this, &SidecarBuilder::onMeshReady);
|
||||
connect(&streamer, &GeometryStreamer::instanceReady,
|
||||
this, &SidecarBuilder::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;
|
||||
|
||||
ModelGeoref georef;
|
||||
if (auto* file = streamer.ifcFile()) {
|
||||
georef = computeModelGeoref(file);
|
||||
}
|
||||
|
||||
SidecarData data = finalize(georef, streamer.drainElements());
|
||||
|
||||
if (!writeSidecar(anchor_path.toStdString(), data)) {
|
||||
last_error_ = "writeSidecar failed";
|
||||
return false;
|
||||
}
|
||||
@@ -17,44 +17,63 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef HEADLESSSIDECARBUILDER_H
|
||||
#define HEADLESSSIDECARBUILDER_H
|
||||
#ifndef SIDECARBUILDER_H
|
||||
#define SIDECARBUILDER_H
|
||||
|
||||
#include "Federation.h"
|
||||
#include "GeometryStreamer.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.
|
||||
#include <vector>
|
||||
|
||||
// Assembles a .ifcview SidecarData from streamer output, then finalizes it
|
||||
// (LOD build, georef, packed elements) ready for writeSidecar() and/or
|
||||
// ViewportWindow::applyLodExtension(). Two use modes:
|
||||
//
|
||||
// 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 {
|
||||
// 1. Live load — host (SceneLoader) drives its own GeometryStreamer and
|
||||
// forwards meshReady/instanceReady chunks via onMeshReady/onInstanceReady
|
||||
// while the viewport also consumes them. When the stream finishes the
|
||||
// host calls finalize(georef, elements) and writeSidecar() with the
|
||||
// returned data. No GPU readback involved.
|
||||
//
|
||||
// 2. Offline — build() owns a streamer, runs it on a non-GUI worker thread,
|
||||
// and writes the sidecar to disk. Used by the .rdbview export path.
|
||||
class SidecarBuilder : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HeadlessSidecarBuilder(QObject* parent = nullptr);
|
||||
explicit SidecarBuilder(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.
|
||||
// Convenience for the offline path: construct an internal streamer,
|
||||
// accumulate, finalize, and write to disk. anchor_path is normalised to
|
||||
// <stem>.ifcview by writeSidecar. Call from a non-GUI worker thread with
|
||||
// a Qt event dispatcher; build() spins a local QEventLoop until the
|
||||
// streamer's worker thread completes.
|
||||
bool build(const QString& ifc_path,
|
||||
const QString& anchor_path,
|
||||
int num_threads = 0);
|
||||
|
||||
const QString& lastError() const { return last_error_; }
|
||||
|
||||
private:
|
||||
// Accumulator interface. Safe to call repeatedly from the same thread the
|
||||
// streamer signals are delivered to.
|
||||
void onMeshReady(const MeshChunk& chunk);
|
||||
void onInstanceReady(const InstanceChunk& chunk);
|
||||
|
||||
// Finishes assembly using the georef + element batch the host collected
|
||||
// during streaming. Returns the assembled SidecarData by move; the
|
||||
// builder's internal state is left empty so the same instance can be
|
||||
// reused for another load.
|
||||
SidecarData finalize(const ModelGeoref& georef,
|
||||
const std::vector<ElementInfo>& elements);
|
||||
|
||||
const QString& lastError() const { return last_error_; }
|
||||
|
||||
private:
|
||||
SidecarData sidecar_data_;
|
||||
QString last_error_;
|
||||
};
|
||||
|
||||
#endif // HEADLESSSIDECARBUILDER_H
|
||||
#endif // SIDECARBUILDER_H
|
||||
|
||||
@@ -20,9 +20,8 @@
|
||||
// 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.
|
||||
// between ViewportWindow::uploadMeshChunk and SidecarBuilder so the
|
||||
// on-disk format stays identical to what the viewport renders.
|
||||
|
||||
#ifndef VERTEXQUANTIZATION_H
|
||||
#define VERTEXQUANTIZATION_H
|
||||
|
||||
@@ -1187,28 +1187,6 @@ void ViewportWindow::finalizeModel(uint32_t model_id) {
|
||||
m.ssbo_capacity / (1024.0*1024.0));
|
||||
}
|
||||
|
||||
bool ViewportWindow::snapshotModel(uint32_t model_id, SidecarData& out) const {
|
||||
auto it = models_gpu_.find(model_id);
|
||||
if (!gl_ || it == models_gpu_.end()) return false;
|
||||
const auto& m = it->second;
|
||||
if (!m.finalized) return false;
|
||||
|
||||
// GPU readback of the packed VBO/EBO ranges actually in use. VBO is
|
||||
// raw bytes at the quantized layout.
|
||||
if (m.vbo_used > 0) {
|
||||
out.vertices.resize(m.vbo_used);
|
||||
gl_->glGetNamedBufferSubData(m.vbo, 0, m.vbo_used, out.vertices.data());
|
||||
}
|
||||
if (m.ebo_used > 0) {
|
||||
out.indices.resize(m.ebo_used / sizeof(uint32_t));
|
||||
gl_->glGetNamedBufferSubData(m.ebo, 0, m.ebo_used, out.indices.data());
|
||||
}
|
||||
|
||||
out.meshes = m.meshes;
|
||||
out.instances = m.instances;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
if (!gl_initialized_) {
|
||||
PendingOperation op;
|
||||
|
||||
@@ -161,12 +161,6 @@ public:
|
||||
|
||||
void resetScene();
|
||||
|
||||
// Snapshot the finalised model into a SidecarData struct for caching.
|
||||
// Vertices + indices are read back from the GPU; meshes/instances come
|
||||
// from the CPU-side vectors. Leaves `elements` and `string_table` empty
|
||||
// for the caller to fill in.
|
||||
bool snapshotModel(uint32_t model_id, SidecarData& out) const;
|
||||
|
||||
// Restore a finalised model from a cached SidecarData struct. Replaces
|
||||
// any existing state for model_id and marks it drawable.
|
||||
void applyCachedModel(uint32_t model_id, SidecarData data);
|
||||
|
||||
Reference in New Issue
Block a user