ifcviewer: overhaul model/object ID tracking

Rename the two overloaded model identifiers and make object_id
assignment single-authority, fixing a pick -> properties mismatch.

Identifiers:
- Per-model UUID fed_id -> model_id; the uint32 runtime handle
  model_id -> session_model_id (SessionState accessors + mirror hashes
  renamed to match). "fed_id" was a misnomer -- the federation is the
  whole collection, not one model.

object_id assignment (fixes wrong class on click):
- Producers (GeometryStreamer, .ifcview sidecar) now stamp model-LOCAL
  object_ids; ViewportCore::applyCachedModel is the sole authority that
  assigns the session-global id (base + local). Removed
  SceneLoader::next_object_id_, GeometryStreamer::lastObjectId(), and the
  streamer's start_object_id parameter.
- The element table is stamped by the same base on both load paths
  (applySidecarData and onStreamerFinished), so registry ids match the
  ids pick returns. Previously the sidecar path double-rebased instances
  vs the registry (click IfcSite -> showed IfcDoor); the live-stream path
  had the same latent mismatch. Both closed.

Naming / cleanup:
- SceneLoader::addFiles -> queueModels; startStreamLoadFor ->
  loadFromGeometryStreamer; readSidecarMetadataOnly -> readSidecarMetadata.
- Federation::addModel takes an explicit display_name (no QFileInfo
  fallback); callers pass QFileInfo(path).fileName().
- Disambiguate cryptic short locals (d->sidecar, m->model, c->chunk, ...)
  in SceneLoader, Federation, ViewportWindow, AreaMeasurement,
  SectionGizmoRenderer, and the SidecarData/SidecarReadPlan spots in
  ViewportCore.

Tests: 125/125 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 14:10:05 +10:00
parent b441fada90
commit 75c9da5098
49 changed files with 914 additions and 885 deletions
+9 -9
View File
@@ -43,9 +43,9 @@ void ElementRegistry::clear() {
elements_.clear();
}
void ElementRegistry::removeModel(uint32_t model_id) {
void ElementRegistry::removeModel(uint32_t session_model_id) {
for (auto it = elements_.begin(); it != elements_.end();) {
if (it->second.model_id == model_id) {
if (it->second.session_model_id == session_model_id) {
it = elements_.erase(it);
} else {
++it;
@@ -53,12 +53,12 @@ void ElementRegistry::removeModel(uint32_t model_id) {
}
}
std::vector<BasicElementInfo> ElementRegistry::basicElementInfoForModel(uint32_t model_id) const {
std::vector<BasicElementInfo> ElementRegistry::basicElementInfoForModel(uint32_t session_model_id) const {
std::vector<BasicElementInfo> result;
result.reserve(elements_.size());
for (const auto& [object_id, info] : elements_) {
(void)object_id;
if (info.model_id != model_id) continue;
if (info.session_model_id != session_model_id) continue;
result.push_back(info);
}
return result;
@@ -76,7 +76,7 @@ std::optional<express::Base> ElementRegistry::findEntity(uint32_t object_id) con
auto info = findBasicElementInfo(object_id);
if (!info) return std::nullopt;
auto* file = loader_->ifcFile(info->model_id);
auto* file = loader_->ifcFile(info->session_model_id);
if (!file) return std::nullopt;
try {
@@ -88,7 +88,7 @@ std::optional<express::Base> ElementRegistry::findEntity(uint32_t object_id) con
}
}
void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
void ElementRegistry::onSidecarElementsReady(uint32_t /*session_model_id*/,
std::vector<ElementTableRecord> elements,
std::string string_table) {
auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString {
@@ -99,7 +99,7 @@ void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
for (const auto& packed_element : elements) {
BasicElementInfo info;
info.object_id = packed_element.object_id;
info.model_id = packed_element.model_id;
info.session_model_id = packed_element.session_model_id;
info.ifc_id = packed_element.ifc_id;
info.guid = string_from_table(packed_element.guid_offset, packed_element.guid_length);
info.name = string_from_table(packed_element.name_offset, packed_element.name_length);
@@ -108,11 +108,11 @@ void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
}
}
void ElementRegistry::onStreamedElementsReady(uint32_t /*model_id*/, std::vector<ElementInfo> elements) {
void ElementRegistry::onStreamedElementsReady(uint32_t /*session_model_id*/, std::vector<ElementInfo> elements) {
for (const auto& element : elements) {
BasicElementInfo info;
info.object_id = element.object_id;
info.model_id = element.model_id;
info.session_model_id = element.session_model_id;
info.ifc_id = element.ifc_id;
info.guid = QString::fromStdString(element.guid);
info.name = QString::fromStdString(element.name);
+5 -5
View File
@@ -37,7 +37,7 @@ namespace bonsaiviewer {
struct BasicElementInfo {
uint32_t object_id = 0;
uint32_t model_id = 0;
uint32_t session_model_id = 0;
int ifc_id = 0;
QString guid;
QString name;
@@ -51,16 +51,16 @@ public:
void bindLoader(SceneLoader* loader);
void clear();
void removeModel(uint32_t model_id);
std::vector<BasicElementInfo> basicElementInfoForModel(uint32_t model_id) const;
void removeModel(uint32_t session_model_id);
std::vector<BasicElementInfo> basicElementInfoForModel(uint32_t session_model_id) const;
std::optional<BasicElementInfo> findBasicElementInfo(uint32_t object_id) const;
std::optional<express::Base> findEntity(uint32_t object_id) const;
private:
void onSidecarElementsReady(uint32_t model_id,
void onSidecarElementsReady(uint32_t session_model_id,
std::vector<ElementTableRecord> elements,
std::string string_table);
void onStreamedElementsReady(uint32_t model_id, std::vector<ElementInfo> elements);
void onStreamedElementsReady(uint32_t session_model_id, std::vector<ElementInfo> elements);
SceneLoader* loader_ = nullptr;
std::unordered_map<uint32_t, BasicElementInfo> elements_;
+15 -15
View File
@@ -71,7 +71,7 @@ double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
if (object_ids.empty()) return 0.0;
// Group selected instances by (model_id, mesh_id) so each unique mesh
// Group selected instances by (session_model_id, mesh_id) so each unique mesh
// is read back at most once per call. Each entry stores the |det| of
// every instance of that mesh in the request.
std::unordered_map<uint64_t, std::vector<double>> by_mesh;
@@ -79,16 +79,16 @@ double volumeOfObjects(ViewportWindow& vp,
for (uint32_t oid : object_ids) {
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
const uint64_t key = (uint64_t(lk.session_model_id) << 32) | lk.mesh_id;
by_mesh[key].push_back(std::abs(det3(lk.placement_transformation)));
}
double total = 0.0;
ViewportWindow::MeshTriangles tris;
for (const auto& [key, dets] : by_mesh) {
const uint32_t model_id = uint32_t(key >> 32);
const uint32_t session_model_id = uint32_t(key >> 32);
const uint32_t mesh_id = uint32_t(key & 0xffffffffu);
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) continue;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) continue;
const double v = meshLocalVolume(tris);
for (double d : dets) total += v * d;
}
@@ -102,7 +102,7 @@ volumesPerObject(ViewportWindow& vp,
if (object_ids.empty()) return out;
out.reserve(object_ids.size());
// Cache the local-frame volume per unique (model_id, mesh_id) so each
// Cache the local-frame volume per unique (session_model_id, mesh_id) so each
// mesh is read back at most once even when many instances share it
// (common for repeated families like windows / columns).
std::unordered_map<uint64_t, double> mesh_vol_local;
@@ -113,11 +113,11 @@ volumesPerObject(ViewportWindow& vp,
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
const uint64_t key = (uint64_t(lk.session_model_id) << 32) | lk.mesh_id;
auto it = mesh_vol_local.find(key);
double v_local = 0.0;
if (it == mesh_vol_local.end()) {
if (vp.readbackMeshTriangles(lk.model_id, lk.mesh_id, tris)) {
if (vp.readbackMeshTriangles(lk.session_model_id, lk.mesh_id, tris)) {
v_local = meshLocalVolume(tris);
}
mesh_vol_local.emplace(key, v_local);
@@ -252,7 +252,7 @@ void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
std::vector<float> world_xyz;
world_xyz.reserve(selected_.size() * 9);
for (const auto& [key, sel] : selected_) {
const uint64_t cache_key = (uint64_t(sel.model_id) << 32)
const uint64_t cache_key = (uint64_t(sel.session_model_id) << 32)
| uint64_t(sel.mesh_id);
auto cit = mesh_cache_.find(cache_key);
if (cit == mesh_cache_.end()) continue;
@@ -292,7 +292,7 @@ void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
if (sels.empty()) continue;
// All tris belonging to one object share its mesh + transform.
const SelectedTri& any = *sels[0];
const uint64_t cache_key = (uint64_t(any.model_id) << 32)
const uint64_t cache_key = (uint64_t(any.session_model_id) << 32)
| uint64_t(any.mesh_id);
auto cit = mesh_cache_.find(cache_key);
if (cit == mesh_cache_.end()) continue;
@@ -362,14 +362,14 @@ void AreaMeasurement::rebuildHighlight(ViewportWindow& vp) {
}
AreaMeasurement::MeshCache* AreaMeasurement::meshCache(ViewportWindow& vp,
uint32_t model_id,
uint32_t session_model_id,
uint32_t mesh_id) {
const uint64_t key = (uint64_t(model_id) << 32) | uint64_t(mesh_id);
const uint64_t key = (uint64_t(session_model_id) << 32) | uint64_t(mesh_id);
auto it = mesh_cache_.find(key);
if (it != mesh_cache_.end()) return &it->second;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) return nullptr;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) return nullptr;
MeshCache c;
c.positions = std::move(tris.positions);
@@ -401,7 +401,7 @@ void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) {
ViewportWindow::MeshLocalPick pick;
if (!vp.pickMeshLocalAt(x, y, pick)) return;
MeshCache* cache = meshCache(vp, pick.model_id, pick.mesh_id);
MeshCache* cache = meshCache(vp, pick.session_model_id, pick.mesh_id);
if (!cache) return;
const size_t n_tris = cache->indices.size() / 3;
if (n_tris == 0) return;
@@ -471,7 +471,7 @@ void AreaMeasurement::onPick(ViewportWindow& vp, int x, int y, bool alt) {
}
} else {
SelectedTri sel;
sel.model_id = pick.model_id;
sel.session_model_id = pick.session_model_id;
sel.mesh_id = pick.mesh_id;
sel.tri = t;
std::memcpy(sel.composed_transform, pick.composed_transform,
@@ -876,7 +876,7 @@ void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
ViewportWindow::MeshTriangles tris;
bool have_extent = false;
double min_t1 = 0.0, max_t1 = 0.0, min_t2 = 0.0, max_t2 = 0.0;
if (vp.readbackMeshTriangles(first_pick_.model_id, first_pick_.mesh_id, tris)) {
if (vp.readbackMeshTriangles(first_pick_.session_model_id, first_pick_.mesh_id, tris)) {
const size_t n_verts = tris.positions.size() / 3;
const size_t n_tris = tris.indices.size() / 3;
if (n_tris > 0) {
+3 -3
View File
@@ -79,7 +79,7 @@ public:
private:
// Cached per-mesh data: triangles + edge→triangles adjacency. Keyed
// by (model_id << 32) | mesh_id. Filled lazily on first pick of that
// by (session_model_id << 32) | mesh_id. Filled lazily on first pick of that
// mesh, dropped on clear().
struct MeshCache {
std::vector<float> positions; // 3 * N_verts
@@ -89,14 +89,14 @@ private:
// edge_key (min<<32 | max) → list of triangle indices touching it.
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
};
MeshCache* meshCache(ViewportWindow& vp, uint32_t model_id, uint32_t mesh_id);
MeshCache* meshCache(ViewportWindow& vp, uint32_t session_model_id, uint32_t mesh_id);
// Per-selected-triangle record. The composed transform is captured at
// pick time so the overlay rebuild doesn't have to re-query the
// viewport for it (and so the overlay keeps working if the picked
// instance later goes hidden).
struct SelectedTri {
uint32_t model_id;
uint32_t session_model_id;
uint32_t mesh_id;
uint32_t tri;
float composed_transform[16];
+32 -32
View File
@@ -64,25 +64,25 @@ void SessionState::createLoader(ViewportWindow* viewport) {
});
connect(loader_, &SceneLoader::progressChanged, this, &SessionState::setProgress);
connect(loader_, &SceneLoader::loadedFromSidecar, this,
[this, format_elapsed](uint32_t model_id, qint64 elapsed_ms) {
[this, format_elapsed](uint32_t session_model_id, qint64 elapsed_ms) {
setStatusMessage("Loaded",
QString("%1 from cache in %2")
.arg(loader_->displayName(model_id))
.arg(loader_->displayName(session_model_id))
.arg(format_elapsed(elapsed_ms)));
endProgress();
emit modelGeometryReady(model_id);
emit modelGeometryReady(session_model_id);
});
connect(loader_, &SceneLoader::loadedFromStream, this,
[this, format_elapsed](uint32_t model_id, qint64 elapsed_ms) {
[this, format_elapsed](uint32_t session_model_id, qint64 elapsed_ms) {
setStatusMessage("Loaded",
QString("%1 streamed in %2")
.arg(loader_->displayName(model_id))
.arg(loader_->displayName(session_model_id))
.arg(format_elapsed(elapsed_ms)));
endProgress();
emit modelGeometryReady(model_id);
emit modelGeometryReady(session_model_id);
});
connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t model_id) {
setStatusMessage("Cancelled", loader_->displayName(model_id));
connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t session_model_id) {
setStatusMessage("Cancelled", loader_->displayName(session_model_id));
endProgress();
});
connect(loader_, &SceneLoader::loadError, this,
@@ -118,44 +118,44 @@ void SessionState::endProgress() {
emit progressEnded();
}
void SessionState::setModelMapping(const QString& fed_id, uint32_t model_id) {
fed_id_to_model_id_[fed_id] = model_id;
model_id_to_fed_id_[model_id] = fed_id;
void SessionState::setModelMapping(const QString& model_id, uint32_t session_model_id) {
model_id_to_session_model_id_[model_id] = session_model_id;
session_model_id_to_model_id_[session_model_id] = model_id;
}
void SessionState::removeModelMappingByFedId(const QString& fed_id) {
cloud_metadata_.remove(fed_id);
auto it = fed_id_to_model_id_.find(fed_id);
if (it == fed_id_to_model_id_.end()) return;
model_id_to_fed_id_.remove(it.value());
fed_id_to_model_id_.erase(it);
void SessionState::removeModelMappingByModelId(const QString& model_id) {
cloud_metadata_.remove(model_id);
auto it = model_id_to_session_model_id_.find(model_id);
if (it == model_id_to_session_model_id_.end()) return;
session_model_id_to_model_id_.remove(it.value());
model_id_to_session_model_id_.erase(it);
}
void SessionState::clearModelMappings() {
fed_id_to_model_id_.clear();
model_id_to_fed_id_.clear();
model_id_to_session_model_id_.clear();
session_model_id_to_model_id_.clear();
cloud_metadata_.clear();
}
void SessionState::setCloudMetadata(const QString& fed_id, const QVariantMap& metadata) {
if (metadata.isEmpty()) cloud_metadata_.remove(fed_id);
else cloud_metadata_.insert(fed_id, metadata);
void SessionState::setCloudMetadata(const QString& model_id, const QVariantMap& metadata) {
if (metadata.isEmpty()) cloud_metadata_.remove(model_id);
else cloud_metadata_.insert(model_id, metadata);
}
QVariantMap SessionState::cloudMetadata(const QString& fed_id) const {
return cloud_metadata_.value(fed_id);
QVariantMap SessionState::cloudMetadata(const QString& model_id) const {
return cloud_metadata_.value(model_id);
}
uint32_t SessionState::modelIdForFedId(const QString& fed_id) const {
return fed_id_to_model_id_.value(fed_id, 0);
uint32_t SessionState::sessionModelIdForModelId(const QString& model_id) const {
return model_id_to_session_model_id_.value(model_id, 0);
}
QString SessionState::fedIdForModelId(uint32_t model_id) const {
return model_id_to_fed_id_.value(model_id);
QString SessionState::modelIdForSessionModelId(uint32_t session_model_id) const {
return session_model_id_to_model_id_.value(session_model_id);
}
QList<uint32_t> SessionState::modelIds() const {
return model_id_to_fed_id_.keys();
QList<uint32_t> SessionState::sessionModelIds() const {
return session_model_id_to_model_id_.keys();
}
void SessionState::notifySelectionChanged() {
@@ -174,8 +174,8 @@ void SessionState::notifyVisibilityChanged() {
emit visibilityChanged();
}
void SessionState::notifyModelGeometryReady(uint32_t model_id) {
emit modelGeometryReady(model_id);
void SessionState::notifyModelGeometryReady(uint32_t session_model_id) {
emit modelGeometryReady(session_model_id);
}
void SessionState::notifyProjectOpened(const QString& path) {
+12 -12
View File
@@ -64,25 +64,25 @@ public:
void setProgress(int percent);
void endProgress();
void setModelMapping(const QString& fed_id, uint32_t model_id);
void removeModelMappingByFedId(const QString& fed_id);
void setModelMapping(const QString& model_id, uint32_t session_model_id);
void removeModelMappingByModelId(const QString& model_id);
void clearModelMappings();
// Per-session cloud metadata returned by connectors (revision/date/
// author/...). Not persisted to the .ifcfed; display only. Lifetime
// is tied to the fed_id — removeModelMappingByFedId and
// is tied to the model_id — removeModelMappingByModelId and
// clearModelMappings drop the matching entries.
void setCloudMetadata(const QString& fed_id, const QVariantMap& metadata);
QVariantMap cloudMetadata(const QString& fed_id) const;
uint32_t modelIdForFedId(const QString& fed_id) const;
QString fedIdForModelId(uint32_t model_id) const;
QList<uint32_t> modelIds() const;
void setCloudMetadata(const QString& model_id, const QVariantMap& metadata);
QVariantMap cloudMetadata(const QString& model_id) const;
uint32_t sessionModelIdForModelId(const QString& model_id) const;
QString modelIdForSessionModelId(uint32_t session_model_id) const;
QList<uint32_t> sessionModelIds() const;
void notifySelectionChanged();
void notifyModelsChanged();
void notifyFederationChanged();
void notifyVisibilityChanged();
void notifyModelGeometryReady(uint32_t model_id);
void notifyModelGeometryReady(uint32_t session_model_id);
void notifyProjectOpened(const QString& path);
void notifyProjectSaved(const QString& path);
void notifyProjectReset();
@@ -100,7 +100,7 @@ signals:
// Fires when a model's geometry has been pushed to the viewport. Fires
// 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);
void modelGeometryReady(uint32_t session_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.
@@ -119,8 +119,8 @@ private:
uint32_t selected_object_id_ = 0;
QString status_mode_;
QString status_detail_;
QHash<QString, uint32_t> fed_id_to_model_id_;
QHash<uint32_t, QString> model_id_to_fed_id_;
QHash<QString, uint32_t> model_id_to_session_model_id_;
QHash<uint32_t, QString> session_model_id_to_model_id_;
QHash<QString, QVariantMap> cloud_metadata_;
};
@@ -84,7 +84,7 @@ The fast path starts when ``SceneLoader`` finds a readable ``.ifcview`` cache.
The reader validates the sidecar header, skips the compressed geometry section,
and reads the metadata blocks.
For desktop loading, ``readSidecarMetadataOnly()`` returns a
For desktop loading, ``readSidecarMetadata()`` returns a
``StreamingSidecar`` containing:
- the sidecar file path
+50 -49
View File
@@ -166,31 +166,31 @@ void removeGroup(SessionState& session, QWidget& host, const QString& group_id)
session.setStatusMessage("Models", "Group removed");
}
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& fed_id) {
const Federation::Model* model = session.federation()->findById(fed_id);
const QString label = model ? model->display_name : fed_id;
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id) {
const Federation::Model* model = session.federation()->findById(model_id);
const QString label = model ? model->display_name : model_id;
const auto choice = QMessageBox::question(
&host, "Remove Model",
QString("Remove model '%1' from the federation?").arg(label),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (choice != QMessageBox::Yes) return;
const uint32_t model_id = session.modelIdForFedId(fed_id);
if (model_id == 0) {
session.federation()->removeModel(fed_id);
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0) {
session.federation()->removeModel(model_id);
session.notifyFederationChanged();
session.setStatusMessage("Models", "Model removed");
return;
}
if (session.loader()->isLoadingModel(model_id)) return;
if (session.loader()->isLoadingModel(session_model_id)) return;
viewport.setSelectedObjectId(0);
session.setSelectedObjectId(0);
session.federation()->removeModel(fed_id);
viewport.removeModel(model_id);
session.loader()->removeModel(model_id);
session.elementRegistry()->removeModel(model_id);
session.removeModelMappingByFedId(fed_id);
session.federation()->removeModel(model_id);
viewport.removeModel(session_model_id);
session.loader()->removeModel(session_model_id);
session.elementRegistry()->removeModel(session_model_id);
session.removeModelMappingByModelId(model_id);
session.notifySelectionChanged();
session.notifyModelsChanged();
session.setStatusMessage("Models", "Model removed");
@@ -198,12 +198,12 @@ void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host,
namespace detail {
void loadModels(SessionState& session, const QStringList& paths, const QStringList& fed_ids) {
void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids) {
if (paths.isEmpty()) return;
const auto model_ids = session.loader()->addFiles(paths);
for (int i = 0; i < paths.size() && i < static_cast<int>(model_ids.size()) && i < fed_ids.size(); ++i) {
session.setModelMapping(fed_ids[i], model_ids[i]);
const auto session_model_ids = session.loader()->queueModels(paths);
for (int i = 0; i < paths.size() && i < static_cast<int>(session_model_ids.size()) && i < model_ids.size(); ++i) {
session.setModelMapping(model_ids[i], session_model_ids[i]);
}
}
@@ -269,20 +269,21 @@ void addModel(SessionState& session, QWidget& host) {
// models yet — the first model that finishes loading will set the
// origin via ViewportView. Checked here (before federation->addModel)
// because federation->addModel doesn't yet populate SessionState's
// model mapping; modelIds() reflects pre-add state at this point.
if (session.modelIds().isEmpty()) {
// model mapping; sessionModelIds() reflects pre-add state at this point.
if (session.sessionModelIds().isEmpty()) {
armFederatedFalseOriginGuess();
}
QStringList accepted_paths;
QStringList accepted_fed_ids;
QStringList accepted_model_ids;
for (const auto& path : paths) {
const QString fed_id = session.federation()->addModel(path);
if (fed_id.isEmpty()) continue;
const QString model_id =
session.federation()->addModel(path, QFileInfo(path).fileName());
if (model_id.isEmpty()) continue;
accepted_paths << path;
accepted_fed_ids << fed_id;
accepted_model_ids << model_id;
}
detail::loadModels(session, accepted_paths, accepted_fed_ids);
detail::loadModels(session, accepted_paths, accepted_model_ids);
session.notifyModelsChanged();
}
@@ -317,15 +318,15 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
proc->call("pull_models_interactive", QJsonValue(),
[sguard, connector_id](const QJsonValue& result) {
if (!sguard) return;
// Arm before the first addCloudModel — modelIds() reflects the
// Arm before the first addCloudModel — sessionModelIds() reflects the
// session state at the moment the connector returns, which is
// when the user's "add into empty session" intent applies.
if (sguard->modelIds().isEmpty()) {
if (sguard->sessionModelIds().isEmpty()) {
armFederatedFalseOriginGuess();
}
const QJsonArray arr = result.toArray();
QStringList paths;
QStringList fed_ids;
QStringList model_ids;
int added = 0;
for (const QJsonValue& value : arr) {
if (value.isNull() || !value.isObject()) continue;
@@ -337,19 +338,19 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
QString src_connector = source.value("connector").toString();
if (src_connector.isEmpty()) src_connector = connector_id;
const QString fed_id = sguard->federation()->addCloudModel(
const QString model_id = sguard->federation()->addCloudModel(
display_name, src_connector, source);
if (fed_id.isEmpty()) continue;
if (model_id.isEmpty()) continue;
const QJsonObject meta = entry.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
paths << path;
fed_ids << fed_id;
model_ids << model_id;
++added;
}
if (!paths.isEmpty()) {
detail::loadModels(*sguard, paths, fed_ids);
detail::loadModels(*sguard, paths, model_ids);
sguard->notifyModelsChanged();
}
sguard->setStatusMessage("Cloud",
@@ -368,28 +369,28 @@ void addModelFromCloud(SessionState& session, QWidget& host) {
namespace {
// Shared "local path on disk" lookup for the right-click cloud commands:
// the loader keeps the path keyed by model_id (set when a file or pull_models
// the loader keeps the path keyed by session_model_id (set when a file or pull_models
// path was queued). Both local-sourced and resolved cloud-sourced models
// have one; only un-resolved cloud models (where pull_models hasn't
// returned yet) won't.
QString localPathForModel(SessionState& session, const QString& fed_id) {
const uint32_t model_id = session.modelIdForFedId(fed_id);
if (model_id == 0 || !session.loader()) return {};
return session.loader()->filePath(model_id);
QString localPathForModel(SessionState& session, const QString& model_id) {
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0 || !session.loader()) return {};
return session.loader()->filePath(session_model_id);
}
} // namespace
void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_id) {
void saveModelToCloud(SessionState& session, QWidget& host, const QString& model_id) {
auto* federation = session.federation();
const Federation::Model* model = federation->findById(fed_id);
const Federation::Model* model = federation->findById(model_id);
if (!model) return;
if (model->source_connector == "local") {
QMessageBox::information(&host, "Save Model To Cloud",
"This model has no cloud target. Use \"Save As To Cloud\" first.");
return;
}
const QString local_path = localPathForModel(session, fed_id);
const QString local_path = localPathForModel(session, model_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model To Cloud",
"Cannot find a local copy of this model to push.");
@@ -416,15 +417,15 @@ void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_i
QPointer<SessionState> sguard(&session);
proc->call("push_model", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
[sguard, model_id, connector_id](const QJsonValue& result) {
if (!sguard) return;
const QJsonObject obj = result.toObject();
const QJsonObject new_source = obj.value("source").toObject();
QString new_connector = new_source.value("connector").toString();
if (new_connector.isEmpty()) new_connector = connector_id;
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
sguard->federation()->setModelSource(model_id, new_connector, new_source);
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Saved to %1").arg(new_connector));
},
@@ -438,10 +439,10 @@ void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_i
});
}
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed_id) {
const Federation::Model* model = session.federation()->findById(fed_id);
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& model_id) {
const Federation::Model* model = session.federation()->findById(model_id);
if (!model) return;
const QString local_path = localPathForModel(session, fed_id);
const QString local_path = localPathForModel(session, model_id);
if (local_path.isEmpty()) {
QMessageBox::warning(&host, "Save Model As To Cloud",
"Cannot find a local copy of this model to push.");
@@ -480,20 +481,20 @@ void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed
QPointer<SessionState> sguard(&session);
proc->call("push_model_interactive", params,
[sguard, fed_id, connector_id](const QJsonValue& result) {
[sguard, model_id, connector_id](const QJsonValue& result) {
if (!sguard) return;
const QJsonObject obj = result.toObject();
const QJsonObject new_source = obj.value("source").toObject();
QString new_connector = new_source.value("connector").toString();
if (new_connector.isEmpty()) new_connector = connector_id;
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
sguard->federation()->setModelSource(model_id, new_connector, new_source);
const QString new_name = obj.value("display_name").toString();
if (!new_name.isEmpty()) {
sguard->federation()->setModelDisplayName(fed_id, new_name);
sguard->federation()->setModelDisplayName(model_id, new_name);
}
const QJsonObject meta = obj.value("metadata").toObject();
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
sguard->setStatusMessage("Cloud",
QString("Pushed to %1").arg(new_connector));
},
+4 -4
View File
@@ -58,7 +58,7 @@ void renameGroup(SessionState& session, QWidget& host, const QString& group_id);
void moveGroup(SessionState& session, const QString& id, const QString& parent_group_id);
void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id);
void removeGroup(SessionState& session, QWidget& host, const QString& group_id);
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& fed_id);
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id);
void addModel(SessionState& session, QWidget& host);
// Connector picker → pull_models_interactive → addCloudModel + load.
// Reachable from AddModelDialog's CloudModel button; the underlying call
@@ -66,10 +66,10 @@ void addModel(SessionState& session, QWidget& host);
void addModelFromCloud(SessionState& session, QWidget& host);
// push_model: push a cloud-sourced model back to its existing target.
// Only valid when model.source_connector != "local". Async.
void saveModelToCloud(SessionState& session, QWidget& host, const QString& fed_id);
void saveModelToCloud(SessionState& session, QWidget& host, const QString& model_id);
// push_model_interactive: pick a connector and push to a fresh cloud
// target. Valid for any model (local or already cloud-sourced). Async.
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& fed_id);
void saveModelAsToCloud(SessionState& session, QWidget& host, const QString& model_id);
void convertIfcToDatabase(SessionState& session, QWidget& host);
void exportGeometryDatabase(SessionState& session, QWidget& host);
void openSettings(SessionState& session, QWidget& host);
@@ -80,7 +80,7 @@ void openSettings(SessionState& session, QWidget& host);
namespace detail {
// Queues already-federated models on the loader and maps their federation-ids to mids.
void loadModels(SessionState& session, const QStringList& paths, const QStringList& fed_ids);
void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids);
} // namespace detail
@@ -87,9 +87,9 @@ QStandardItem* FederationItemModel::makeGroupNameItem(const QString& group_id, c
return item;
}
QStandardItem* FederationItemModel::makeModelNameItem(const QString& fed_id, const QString& display_name) const {
QStandardItem* FederationItemModel::makeModelNameItem(const QString& model_id, const QString& display_name) const {
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/cube.svg"), display_name);
item->setData(fed_id, IdRole);
item->setData(model_id, IdRole);
item->setData(int(ItemKind::Model), KindRole);
item->setEditable(false);
return item;
@@ -129,14 +129,14 @@ QStandardItem* FederationItemModel::parentItemForGroup(const QString& parent_gro
return found ? found : invisibleRootItem();
}
void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QString& fed_id) {
const Federation::Model* model = federation_->findById(fed_id);
void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QString& model_id) {
const Federation::Model* model = federation_->findById(model_id);
if (!model) return;
auto* name_item = makeModelNameItem(fed_id, model->display_name);
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(fed_id));
auto* name_item = makeModelNameItem(model_id, model->display_name);
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(model_id));
parent_item->appendRow({name_item, vis_item});
id_to_name_item_.insert(fed_id, name_item);
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(fed_id));
id_to_name_item_.insert(model_id, name_item);
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(model_id));
}
void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id) {
@@ -228,38 +228,38 @@ void FederationItemModel::onGroupVisibilityChanged(const QString& group_id, bool
refreshSubtreeVisibility(item);
}
void FederationItemModel::onModelAdded(const QString& fed_id) {
const Federation::Model* model = federation_->findById(fed_id);
void FederationItemModel::onModelAdded(const QString& model_id) {
const Federation::Model* model = federation_->findById(model_id);
if (!model) return;
QStandardItem* parent_item = parentItemForGroup(model->group_id);
appendModelTo(parent_item, fed_id);
appendModelTo(parent_item, model_id);
}
void FederationItemModel::onModelRemoved(const QString& fed_id) {
QStandardItem* item = findItem(fed_id);
void FederationItemModel::onModelRemoved(const QString& model_id) {
QStandardItem* item = findItem(model_id);
if (!item) return;
id_to_name_item_.remove(fed_id);
id_to_name_item_.remove(model_id);
QStandardItem* parent_item = item->parent();
if (!parent_item) parent_item = invisibleRootItem();
parent_item->removeRow(item->row());
}
void FederationItemModel::onModelVisibilityChanged(const QString& fed_id, bool /*visible*/) {
QStandardItem* item = findItem(fed_id);
void FederationItemModel::onModelVisibilityChanged(const QString& model_id, bool /*visible*/) {
QStandardItem* item = findItem(model_id);
if (!item) return;
refreshSubtreeVisibility(item);
}
void FederationItemModel::onModelChanged(const QString& fed_id) {
QStandardItem* item = findItem(fed_id);
void FederationItemModel::onModelChanged(const QString& model_id) {
QStandardItem* item = findItem(model_id);
if (!item) return;
const Federation::Model* model = federation_->findById(fed_id);
const Federation::Model* model = federation_->findById(model_id);
if (!model) return;
item->setText(model->display_name);
}
void FederationItemModel::onModelGroupChanged(const QString& fed_id, const QString& new_group_id) {
QStandardItem* item = findItem(fed_id);
void FederationItemModel::onModelGroupChanged(const QString& model_id, const QString& new_group_id) {
QStandardItem* item = findItem(model_id);
if (!item) return;
QStandardItem* current_parent = item->parent();
if (!current_parent) current_parent = invisibleRootItem();
@@ -58,27 +58,27 @@ private slots:
void onGroupRemoved(const QString& group_id);
void onGroupChanged(const QString& group_id);
void onGroupVisibilityChanged(const QString& group_id, bool visible);
void onModelAdded(const QString& fed_id);
void onModelRemoved(const QString& fed_id);
void onModelVisibilityChanged(const QString& fed_id, bool visible);
void onModelGroupChanged(const QString& fed_id, const QString& new_group_id);
void onModelChanged(const QString& fed_id);
void onModelAdded(const QString& model_id);
void onModelRemoved(const QString& model_id);
void onModelVisibilityChanged(const QString& model_id, bool visible);
void onModelGroupChanged(const QString& model_id, const QString& new_group_id);
void onModelChanged(const QString& model_id);
private:
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
QStandardItem* makeModelNameItem(const QString& fed_id, const QString& display_name) const;
QStandardItem* makeModelNameItem(const QString& model_id, const QString& display_name) const;
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
QStandardItem* findItem(const QString& id) const;
QStandardItem* parentItemForGroup(const QString& parent_group_id) const;
void appendModelTo(QStandardItem* parent_item, const QString& fed_id);
void appendModelTo(QStandardItem* parent_item, const QString& model_id);
void appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id);
void refreshSubtreeVisibility(QStandardItem* root);
Federation* federation_ = nullptr;
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and fed_ids
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and model_ids
};
} // namespace bonsaiviewer::modules::models
@@ -378,7 +378,7 @@ void SettingsDialog::populateModelTable() {
model_table_->setItem(row, 0, model_item);
ModelRowWidgets widgets;
widgets.fed_id = model.id;
widgets.model_id = model.id;
widgets.frame = new QComboBox(model_table_);
widgets.frame->addItem("Local", static_cast<int>(AFrame::ModelLocal));
@@ -448,7 +448,7 @@ void SettingsDialog::updateSelectedModelGeoref() {
return;
}
settings_view_->refresh(model_rows_[row].fed_id);
settings_view_->refresh(model_rows_[row].model_id);
}
void SettingsDialog::onAccepted() {
@@ -473,7 +473,7 @@ void SettingsDialog::onAccepted() {
transformation.b = parseVector3(row.to_point->text());
transformation.rxyz_deg = parseVector3(row.rotate->text());
transformation.pivot = parseVector3(row.pivot->text());
federation_->setModelTransformation(row.fed_id, transformation);
federation_->setModelTransformation(row.model_id, transformation);
}
if (session_state_) {
session_state_->notifyFederationChanged();
@@ -55,7 +55,7 @@ protected:
private:
struct ModelRowWidgets {
QString fed_id;
QString model_id;
QComboBox* frame = nullptr;
QTableWidgetItem* from_point = nullptr;
QTableWidgetItem* to_point = nullptr;
@@ -212,7 +212,7 @@ SettingsView::SettingsView(SettingsDialog* widget,
{
}
void SettingsView::refresh(const QString& fed_id) const {
void SettingsView::refresh(const QString& model_id) const {
if (!widget_) {
return;
}
@@ -228,18 +228,18 @@ void SettingsView::refresh(const QString& fed_id) const {
return;
}
const uint32_t model_id = session_state_->modelIdForFedId(fed_id);
if (model_id == 0) {
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(model_id);
if (session_model_id == 0) {
widget_->renderSelectedModelGeoref(unknownState("Not loaded", "No live model"));
return;
}
if (auto* ifc_file = loader->ifcFile(model_id)) {
if (auto* ifc_file = loader->ifcFile(session_model_id)) {
widget_->renderSelectedModelGeoref(stateFromLiveFile(ifc_file));
return;
}
const ModelGeoref* georef = loader->modelGeoref(model_id);
const ModelGeoref* georef = loader->modelGeoref(session_model_id);
if (!georef) {
widget_->renderSelectedModelGeoref(unknownState("Not available yet", "No data source"));
return;
@@ -36,7 +36,7 @@ public:
explicit SettingsView(SettingsDialog* widget,
bonsaiviewer::SessionState* session_state);
void refresh(const QString& fed_id) const;
void refresh(const QString& model_id) const;
private:
SettingsDialog* widget_ = nullptr;
+27 -27
View File
@@ -56,9 +56,9 @@ namespace {
void clearScene(SessionState& session, ViewportWindow& viewport) {
viewport.setSelectedObjectId(0);
session.setSelectedObjectId(0);
for (uint32_t model_id : session.modelIds()) {
viewport.removeModel(model_id);
session.loader()->removeModel(model_id);
for (uint32_t session_model_id : session.sessionModelIds()) {
viewport.removeModel(session_model_id);
session.loader()->removeModel(session_model_id);
}
session.clearModelMappings();
session.elementRegistry()->clear();
@@ -81,7 +81,7 @@ bool confirmDiscardIfDirty(SessionState& session, QWidget& host) {
// Fire-and-forget async resolution of any non-local models in the
// federation. Groups by source_connector and issues one pull_models per
// group. For each returned entry:
// - if the fed_id already has a scene entry pointed at the same path,
// - if the model_id already has a scene entry pointed at the same path,
// just refresh cloud metadata (no reload, preserves view state);
// - if the path differs, tear down the stale scene entry and queue a
// fresh load (federation entry is preserved either way);
@@ -90,21 +90,21 @@ bool confirmDiscardIfDirty(SessionState& session, QWidget& host) {
// has already shown its own UI.
void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
auto* federation = session.federation();
QHash<QString, QStringList> connector_to_fed_ids;
QHash<QString, QStringList> connector_to_model_ids;
for (const auto& model : federation->models()) {
if (model.source_connector == "local") continue;
connector_to_fed_ids[model.source_connector].push_back(model.id);
connector_to_model_ids[model.source_connector].push_back(model.id);
}
if (connector_to_fed_ids.isEmpty()) return;
if (connector_to_model_ids.isEmpty()) return;
auto* registry = session.connectorRegistry();
QPointer<SessionState> sguard(&session);
QPointer<ViewportWindow> vguard(&viewport);
for (auto it = connector_to_fed_ids.constBegin();
it != connector_to_fed_ids.constEnd(); ++it) {
for (auto it = connector_to_model_ids.constBegin();
it != connector_to_model_ids.constEnd(); ++it) {
const QString connector_id = it.key();
const QStringList fed_ids = it.value();
const QStringList model_ids = it.value();
auto* proc = registry->get(connector_id);
if (!proc) {
@@ -114,8 +114,8 @@ void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
}
QJsonArray params;
for (const QString& fed_id : fed_ids) {
const Federation::Model* model = federation->findById(fed_id);
for (const QString& model_id : model_ids) {
const Federation::Model* model = federation->findById(model_id);
if (!model) continue;
QJsonObject source = model->source_data;
source["connector"] = model->source_connector;
@@ -127,25 +127,25 @@ void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
}
proc->call("pull_models", params,
[sguard, vguard, fed_ids](const QJsonValue& result) {
[sguard, vguard, model_ids](const QJsonValue& result) {
if (!sguard) return;
const QJsonArray arr = result.toArray();
QStringList paths_to_load;
QStringList fed_ids_to_load;
QStringList model_ids_to_load;
bool any_detached = false;
for (int i = 0; i < arr.size() && i < fed_ids.size(); ++i) {
for (int i = 0; i < arr.size() && i < model_ids.size(); ++i) {
if (arr[i].isNull()) continue;
const QJsonObject obj = arr[i].toObject();
const QString new_path = obj.value("path").toString();
if (new_path.isEmpty()) continue;
const QString fed_id = fed_ids[i];
const QString model_id = model_ids[i];
const QJsonObject meta = obj.value("metadata").toObject();
const uint32_t existing_mid = sguard->modelIdForFedId(fed_id);
const uint32_t existing_mid = sguard->sessionModelIdForModelId(model_id);
if (existing_mid != 0 && sguard->loader()) {
const QString existing_path = sguard->loader()->filePath(existing_mid);
if (QDir::cleanPath(existing_path) == QDir::cleanPath(new_path)) {
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
continue;
}
// Path changed (new revision lives in a fresh cache dir).
@@ -153,13 +153,13 @@ void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
if (vguard) vguard->removeModel(existing_mid);
sguard->loader()->removeModel(existing_mid);
sguard->elementRegistry()->removeModel(existing_mid);
sguard->removeModelMappingByFedId(fed_id);
sguard->removeModelMappingByModelId(model_id);
any_detached = true;
}
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
sguard->setCloudMetadata(model_id, meta.toVariantMap());
paths_to_load << new_path;
fed_ids_to_load << fed_id;
model_ids_to_load << model_id;
}
if (any_detached) {
if (vguard) vguard->setSelectedObjectId(0);
@@ -168,7 +168,7 @@ void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
}
if (!paths_to_load.isEmpty()) {
modules::models::commands::detail::loadModels(
*sguard, paths_to_load, fed_ids_to_load);
*sguard, paths_to_load, model_ids_to_load);
sguard->notifyModelsChanged();
}
},
@@ -183,8 +183,8 @@ void resolveCloudModels(SessionState& session, ViewportWindow& viewport) {
}
int total = 0;
for (auto it = connector_to_fed_ids.constBegin();
it != connector_to_fed_ids.constEnd(); ++it) {
for (auto it = connector_to_model_ids.constBegin();
it != connector_to_model_ids.constEnd(); ++it) {
total += it.value().size();
}
session.setStatusMessage("Cloud", QString("Resolving %1 model(s)...").arg(total));
@@ -224,7 +224,7 @@ bool openProjectAt(SessionState& session, QWidget& host, ViewportWindow& viewpor
clearScene(session, viewport);
QStringList paths;
QStringList fed_ids;
QStringList model_ids;
for (const auto& model : session.federation()->models()) {
if (model.source_connector != "local") continue;
if (!QFileInfo::exists(model.source_path)) {
@@ -232,9 +232,9 @@ bool openProjectAt(SessionState& session, QWidget& host, ViewportWindow& viewpor
continue;
}
paths << model.source_path;
fed_ids << model.id;
model_ids << model.id;
}
modules::models::commands::detail::loadModels(session, paths, fed_ids);
modules::models::commands::detail::loadModels(session, paths, model_ids);
if (!warnings.isEmpty()) {
QMessageBox::warning(&host, "Open Project",
+25 -25
View File
@@ -67,9 +67,9 @@ ViewportView::ViewportView(bonsaiviewer::SessionState* session_state,
// first geometry-ready consumes the arm). refresh() stays terminal —
// any federation mutation from the guess propagates through
// SessionState's federatedFalseOriginChanged relay.
connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t model_id) {
connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t session_model_id) {
if (modules::models::consumeFederatedFalseOriginGuess()) {
guessFederatedFalseOriginFromFirstModel(model_id);
guessFederatedFalseOriginFromFirstModel(session_model_id);
}
refresh();
});
@@ -136,34 +136,34 @@ void ViewportView::refresh() {
viewport_->setFederatedFalseOrigin(
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
for (uint32_t model_id : session_state_->modelIds()) {
applyCoordinateOperation(model_id);
applyModelVisibility(model_id);
for (uint32_t session_model_id : session_state_->sessionModelIds()) {
applyCoordinateOperation(session_model_id);
applyModelVisibility(session_model_id);
}
}
void ViewportView::applyCoordinateOperation(uint32_t model_id) {
void ViewportView::applyCoordinateOperation(uint32_t session_model_id) {
SceneLoader* loader = session_state_->loader();
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
if (const ModelGeoref* georef = loader->modelGeoref(model_id)) {
if (const ModelGeoref* georef = loader->modelGeoref(session_model_id)) {
if (georef->has_coordinate_operation) {
matrix = georef->coordinate_operation_meters;
}
}
viewport_->setModelCoordinateOperation(model_id, matrix);
applyModelTransformation(model_id);
viewport_->setModelCoordinateOperation(session_model_id, matrix);
applyModelTransformation(session_model_id);
}
void ViewportView::applyModelTransformation(uint32_t model_id) {
void ViewportView::applyModelTransformation(uint32_t session_model_id) {
Federation* federation = session_state_->federation();
SceneLoader* loader = session_state_->loader();
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
const QString fed_id = session_state_->fedIdForModelId(model_id);
if (!fed_id.isEmpty()) {
if (const Federation::Model* model = federation->findById(fed_id)) {
const QString model_id = session_state_->modelIdForSessionModelId(session_model_id);
if (!model_id.isEmpty()) {
if (const Federation::Model* model = federation->findById(model_id)) {
ModelUnits units;
Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity();
if (const ModelGeoref* georef = loader->modelGeoref(model_id)) {
if (const ModelGeoref* georef = loader->modelGeoref(session_model_id)) {
units = georef->units;
if (georef->has_coordinate_operation) {
coordinate_operation = georef->coordinate_operation_meters;
@@ -173,18 +173,18 @@ void ViewportView::applyModelTransformation(uint32_t model_id) {
model->model_transformation, federation->config(), units, coordinate_operation);
}
}
viewport_->setModelTransformation(model_id, matrix);
viewport_->setModelTransformation(session_model_id, matrix);
}
void ViewportView::applyModelVisibility(uint32_t model_id) {
void ViewportView::applyModelVisibility(uint32_t session_model_id) {
Federation* federation = session_state_->federation();
const QString fed_id = session_state_->fedIdForModelId(model_id);
if (fed_id.isEmpty()) return;
const QString model_id = session_state_->modelIdForSessionModelId(session_model_id);
if (model_id.isEmpty()) return;
if (federation->isModelEffectivelyVisible(fed_id)) {
viewport_->showModel(model_id);
if (federation->isModelEffectivelyVisible(model_id)) {
viewport_->showModel(session_model_id);
} else {
viewport_->hideModel(model_id);
viewport_->hideModel(session_model_id);
}
}
@@ -209,7 +209,7 @@ void ViewportView::applyModelVisibility(uint32_t model_id) {
// mutation here propagates through SessionState's federation relay
// (federatedFalseOriginChanged → notifyFederationChanged) without
// re-entering this function.
void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t model_id) {
void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t session_model_id) {
Federation* federation = session_state_->federation();
if (!federation->filePath().isEmpty()) return;
@@ -218,10 +218,10 @@ void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t model_id) {
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
Eigen::Vector3d first_geometry_point_m;
if (!viewport_->firstGeometryPointWorldM(model_id, first_geometry_point_m)) return;
if (!viewport_->firstGeometryPointWorldM(session_model_id, first_geometry_point_m)) return;
SceneLoader* loader = session_state_->loader();
const ModelGeoref* georef = loader->modelGeoref(model_id);
const ModelGeoref* georef = loader->modelGeoref(session_model_id);
if (georef == nullptr) return;
federation->setFederatedFalseOrigin(::guessFederatedFalseOrigin(
@@ -236,7 +236,7 @@ void ViewportView::guessFederatedFalseOriginFromFirstModel(uint32_t model_id) {
// (0,0,0) — the federated false origin in render space — capped at
// 100 m so a model with crazy-coord geometry can't pull the camera
// back into nothing.
viewport_->frameOnFederatedOrigin(model_id, 100.0f);
viewport_->frameOnFederatedOrigin(session_model_id, 100.0f);
}
void ViewportView::updateVolumeReadout() {
+4 -4
View File
@@ -50,10 +50,10 @@ public:
private:
void refresh();
void applyCoordinateOperation(uint32_t model_id);
void applyModelTransformation(uint32_t model_id);
void applyModelVisibility(uint32_t model_id);
void guessFederatedFalseOriginFromFirstModel(uint32_t model_id);
void applyCoordinateOperation(uint32_t session_model_id);
void applyModelTransformation(uint32_t session_model_id);
void applyModelVisibility(uint32_t session_model_id);
void guessFederatedFalseOriginFromFirstModel(uint32_t session_model_id);
void updateVolumeReadout();
bonsaiviewer::SessionState* session_state_ = nullptr;
+29 -29
View File
@@ -150,8 +150,8 @@ void AreaMeasurement::clear(ViewportWindow& vp) {
AreaMeasurement::MeshAdj*
AreaMeasurement::meshAdj(ViewportWindow& vp,
uint32_t model_id, uint32_t mesh_id) {
const uint64_t key = (uint64_t(model_id) << 32) | uint64_t(mesh_id);
uint32_t session_model_id, uint32_t mesh_id) {
const uint64_t key = (uint64_t(session_model_id) << 32) | uint64_t(mesh_id);
auto it = mesh_cache_.find(key);
if (it != mesh_cache_.end()) return &it->second;
@@ -160,7 +160,7 @@ AreaMeasurement::meshAdj(ViewportWindow& vp,
// and live in the viewport already), so just look them up freshly
// each time the user picks a brand-new mesh.
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) return nullptr;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) return nullptr;
if (tris.indices.size() < 3) return nullptr;
MeshAdj a;
@@ -196,11 +196,11 @@ void AreaMeasurement::onPick(ViewportWindow& vp,
if (!vp.pickMeshLocalAt(x_phys, y_phys, pick)) return;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(pick.model_id, pick.mesh_id, tris)) return;
if (!vp.readbackMeshTriangles(pick.session_model_id, pick.mesh_id, tris)) return;
const size_t n_tris = tris.indices.size() / 3;
if (n_tris == 0) return;
MeshAdj* adj = meshAdj(vp, pick.model_id, pick.mesh_id);
MeshAdj* adj = meshAdj(vp, pick.session_model_id, pick.mesh_id);
if (!adj) return;
// Seed: the triangle whose interior (or boundary) is closest to the
@@ -266,7 +266,7 @@ void AreaMeasurement::onPick(ViewportWindow& vp,
}
} else {
SelectedTri sel;
sel.model_id = pick.model_id;
sel.session_model_id = pick.session_model_id;
sel.mesh_id = pick.mesh_id;
sel.tri = t;
std::memcpy(sel.composed_transform, pick.composed_transform,
@@ -298,25 +298,25 @@ void AreaMeasurement::rebuildHighlightAndLabels(ViewportWindow& vp) {
// to avoid repeated viewport lookups when many tris share a mesh.
std::unordered_map<uint64_t, ViewportWindow::MeshTriangles> tris_cache;
auto get_tris = [&](uint32_t model_id, uint32_t mesh_id)
auto get_tris = [&](uint32_t session_model_id, uint32_t mesh_id)
-> ViewportWindow::MeshTriangles* {
const uint64_t k = (uint64_t(model_id) << 32) | uint64_t(mesh_id);
const uint64_t k = (uint64_t(session_model_id) << 32) | uint64_t(mesh_id);
auto it = tris_cache.find(k);
if (it != tris_cache.end()) return &it->second;
ViewportWindow::MeshTriangles t;
if (!vp.readbackMeshTriangles(model_id, mesh_id, t)) return nullptr;
return &tris_cache.emplace(k, std::move(t)).first->second;
ViewportWindow::MeshTriangles tris;
if (!vp.readbackMeshTriangles(session_model_id, mesh_id, tris)) return nullptr;
return &tris_cache.emplace(k, std::move(tris)).first->second;
};
for (const auto& [key, sel] : selected_) {
ViewportWindow::MeshTriangles* t = get_tris(sel.model_id, sel.mesh_id);
if (!t) continue;
if (size_t(sel.tri) * 3 + 2 >= t->indices.size()) continue;
ViewportWindow::MeshTriangles* tris = get_tris(sel.session_model_id, sel.mesh_id);
if (!tris) continue;
if (size_t(sel.tri) * 3 + 2 >= tris->indices.size()) continue;
const float* M = sel.composed_transform; // column-major
for (int e = 0; e < 3; ++e) {
const uint32_t vi = t->indices[3 * sel.tri + e];
if (3 * vi + 2 >= t->positions.size()) continue;
const float* p = &t->positions[3 * vi];
const uint32_t vi = tris->indices[3 * sel.tri + e];
if (3 * vi + 2 >= tris->positions.size()) continue;
const float* p = &tris->positions[3 * vi];
// World = M * (p, 1). Column-major: M[col*4 + row].
const float wx = M[0]*p[0] + M[4]*p[1] + M[8]*p[2] + M[12];
const float wy = M[1]*p[0] + M[5]*p[1] + M[9]*p[2] + M[13];
@@ -342,9 +342,9 @@ void AreaMeasurement::rebuildHighlightAndLabels(ViewportWindow& vp) {
for (const auto& [obj_id, sels] : by_object) {
if (sels.empty()) continue;
const SelectedTri& any = *sels[0];
ViewportWindow::MeshTriangles* t = get_tris(any.model_id, any.mesh_id);
if (!t) continue;
MeshAdj* adj = meshAdj(vp, any.model_id, any.mesh_id);
ViewportWindow::MeshTriangles* tris = get_tris(any.session_model_id, any.mesh_id);
if (!tris) continue;
MeshAdj* adj = meshAdj(vp, any.session_model_id, any.mesh_id);
if (!adj) continue;
std::unordered_set<uint32_t> remaining;
@@ -360,10 +360,10 @@ void AreaMeasurement::rebuildHighlightAndLabels(ViewportWindow& vp) {
while (!frontier.empty()) {
const uint32_t tri = frontier.front(); frontier.pop();
component.push_back(tri);
if (size_t(tri) * 3 + 2 >= t->indices.size()) continue;
if (size_t(tri) * 3 + 2 >= tris->indices.size()) continue;
for (int e = 0; e < 3; ++e) {
const uint32_t ia = t->indices[3 * tri + e];
const uint32_t ib = t->indices[3 * tri + (e + 1) % 3];
const uint32_t ia = tris->indices[3 * tri + e];
const uint32_t ib = tris->indices[3 * tri + (e + 1) % 3];
auto eit = adj->edges.find(edgeKey(ia, ib));
if (eit == adj->edges.end()) continue;
for (uint32_t nt : eit->second) {
@@ -380,12 +380,12 @@ void AreaMeasurement::rebuildHighlightAndLabels(ViewportWindow& vp) {
if (size_t(tri) >= adj->tri_areas.size()) continue;
const double a = adj->tri_areas[tri];
area += a;
const uint32_t ia = t->indices[3 * tri + 0];
const uint32_t ib = t->indices[3 * tri + 1];
const uint32_t ic = t->indices[3 * tri + 2];
const float* va = &t->positions[3 * ia];
const float* vb = &t->positions[3 * ib];
const float* vc = &t->positions[3 * ic];
const uint32_t ia = tris->indices[3 * tri + 0];
const uint32_t ib = tris->indices[3 * tri + 1];
const uint32_t ic = tris->indices[3 * tri + 2];
const float* va = &tris->positions[3 * ia];
const float* vb = &tris->positions[3 * ib];
const float* vc = &tris->positions[3 * ic];
cx += a * (double(va[0]) + vb[0] + vc[0]) / 3.0;
cy += a * (double(va[1]) + vb[1] + vc[1]) / 3.0;
cz += a * (double(va[2]) + vb[2] + vc[2]) / 3.0;
+3 -3
View File
@@ -64,16 +64,16 @@ private:
// edge_key (min<<32 | max) → list of triangle indices touching it.
std::unordered_map<uint64_t, std::vector<uint32_t>> edges;
};
// Keyed by (model_id << 32) | mesh_id.
// Keyed by (session_model_id << 32) | mesh_id.
MeshAdj* meshAdj(ViewportWindow& vp,
uint32_t model_id, uint32_t mesh_id);
uint32_t session_model_id, uint32_t mesh_id);
// Per-selected-triangle record. The composed transform is captured
// at pick time so highlight rebuilds don't have to re-query the
// viewport for it (and so the overlay keeps working if the picked
// instance later goes hidden).
struct SelectedTri {
uint32_t model_id;
uint32_t session_model_id;
uint32_t mesh_id;
uint32_t tri;
float composed_transform[16];
+137 -139
View File
@@ -237,76 +237,76 @@ void Federation::setFederatedFalseOrigin(const FederatedFalseOrigin& o) {
emit federatedFalseOriginChanged();
}
void Federation::setModelTransformation(const QString& fed_id,
void Federation::setModelTransformation(const QString& model_id,
const ModelTransformation& xf) {
for (auto& m : models_) {
if (m.id != fed_id) continue;
m.model_transformation = xf;
for (auto& model : models_) {
if (model.id != model_id) continue;
model.model_transformation = xf;
setDirty(true);
emit modelTransformationChanged(fed_id);
emit modelTransformationChanged(model_id);
return;
}
}
void Federation::setModelVisible(const QString& fed_id, bool visible) {
for (auto& m : models_) {
if (m.id != fed_id) continue;
if (m.visible == visible) return;
m.visible = visible;
void Federation::setModelVisible(const QString& model_id, bool visible) {
for (auto& model : models_) {
if (model.id != model_id) continue;
if (model.visible == visible) return;
model.visible = visible;
setDirty(true);
emit modelVisibilityChanged(fed_id, visible);
emit modelVisibilityChanged(model_id, visible);
return;
}
}
void Federation::setModelGroup(const QString& fed_id, const QString& group_id) {
void Federation::setModelGroup(const QString& model_id, const QString& group_id) {
if (!group_id.isEmpty() && findGroupById(group_id) == nullptr) return;
for (auto& m : models_) {
if (m.id != fed_id) continue;
if (m.group_id == group_id) return;
m.group_id = group_id;
for (auto& model : models_) {
if (model.id != model_id) continue;
if (model.group_id == group_id) return;
model.group_id = group_id;
setDirty(true);
emit modelGroupChanged(fed_id, group_id);
emit modelGroupChanged(model_id, group_id);
return;
}
}
void Federation::setModelDisplayName(const QString& fed_id, const QString& display_name) {
void Federation::setModelDisplayName(const QString& model_id, const QString& display_name) {
if (display_name.isEmpty()) return;
for (auto& m : models_) {
if (m.id != fed_id) continue;
if (m.display_name == display_name) return;
m.display_name = display_name;
for (auto& model : models_) {
if (model.id != model_id) continue;
if (model.display_name == display_name) return;
model.display_name = display_name;
setDirty(true);
emit modelChanged(fed_id);
emit modelChanged(model_id);
return;
}
}
void Federation::setModelSource(const QString& fed_id,
void Federation::setModelSource(const QString& model_id,
const QString& connector_id,
const QJsonObject& source_data) {
if (connector_id.isEmpty()) return;
for (auto& m : models_) {
if (m.id != fed_id) continue;
m.source_connector = connector_id;
m.source_data = source_data;
m.source_data.remove("connector");
for (auto& model : models_) {
if (model.id != model_id) continue;
model.source_connector = connector_id;
model.source_data = source_data;
model.source_data.remove("connector");
if (connector_id == "local") {
// Round-trip the path through source_data when caller chooses
// to encode it there; otherwise leave m.source_path untouched.
// to encode it there; otherwise leave model.source_path untouched.
const QString path_field = source_data.value("path").toString();
if (!path_field.isEmpty()) {
m.source_path = QDir::cleanPath(QFileInfo(path_field).absoluteFilePath());
m.source_data.remove("path");
model.source_path = QDir::cleanPath(QFileInfo(path_field).absoluteFilePath());
model.source_data.remove("path");
}
} else {
// Cloud sources don't track a source_path — local file lives in
// the connector's cache, looked up via SceneLoader.
m.source_path.clear();
model.source_path.clear();
}
setDirty(true);
emit modelChanged(fed_id);
emit modelChanged(model_id);
return;
}
}
@@ -319,14 +319,14 @@ QString Federation::addGroup(const QString& display_name,
if (!parent) return {};
}
auto g = std::make_unique<Group>();
g->id = generateId();
g->display_name = display_name.isEmpty() ? QString("Group") : display_name;
g->parent = parent;
const QString new_id = g->id;
auto group = std::make_unique<Group>();
group->id = generateId();
group->display_name = display_name.isEmpty() ? QString("Group") : display_name;
group->parent = parent;
const QString new_id = group->id;
if (parent) parent->children.push_back(std::move(g));
else root_groups_.push_back(std::move(g));
if (parent) parent->children.push_back(std::move(group));
else root_groups_.push_back(std::move(group));
setDirty(true);
emit groupAdded(new_id);
@@ -356,10 +356,10 @@ void Federation::removeGroup(const QString& group_id) {
// Reparent direct child models up one level.
std::vector<QString> moved_model_ids;
for (auto& m : models_) {
if (m.group_id == group_id) {
m.group_id = new_parent_id;
moved_model_ids.push_back(m.id);
for (auto& model : models_) {
if (model.group_id == group_id) {
model.group_id = new_parent_id;
moved_model_ids.push_back(model.id);
}
}
@@ -369,16 +369,16 @@ void Federation::removeGroup(const QString& group_id) {
setDirty(true);
for (const auto& cid : moved_child_ids) emit groupChanged(cid);
for (const auto& mid : moved_model_ids) emit modelGroupChanged(mid, new_parent_id);
for (const auto& model_id : moved_model_ids) emit modelGroupChanged(model_id, new_parent_id);
emit groupRemoved(group_id);
}
void Federation::setGroupName(const QString& group_id,
const QString& display_name) {
Group* g = findGroupByIdMutable(group_id);
if (!g) return;
if (g->display_name == display_name) return;
g->display_name = display_name;
Group* group = findGroupByIdMutable(group_id);
if (!group) return;
if (group->display_name == display_name) return;
group->display_name = display_name;
setDirty(true);
emit groupChanged(group_id);
}
@@ -411,10 +411,10 @@ void Federation::setGroupParent(const QString& group_id,
}
void Federation::setGroupVisible(const QString& group_id, bool visible) {
Group* g = findGroupByIdMutable(group_id);
if (!g) return;
if (g->visible == visible) return;
g->visible = visible;
Group* group = findGroupByIdMutable(group_id);
if (!group) return;
if (group->visible == visible) return;
group->visible = visible;
setDirty(true);
emit groupVisibilityChanged(group_id, visible);
}
@@ -426,26 +426,26 @@ const Federation::Group* Federation::findGroupById(const QString& group_id) cons
Federation::Group* Federation::findGroupByIdMutable(const QString& group_id) {
if (group_id.isEmpty()) return nullptr;
std::vector<Group*> stack;
for (auto& g : root_groups_) stack.push_back(g.get());
for (auto& group : root_groups_) stack.push_back(group.get());
while (!stack.empty()) {
Group* g = stack.back();
Group* group = stack.back();
stack.pop_back();
if (g->id == group_id) return g;
for (auto& c : g->children) stack.push_back(c.get());
if (group->id == group_id) return group;
for (auto& c : group->children) stack.push_back(c.get());
}
return nullptr;
}
std::vector<const Federation::Group*> Federation::allGroups() const {
std::vector<const Group*> out;
for (const auto& g : root_groups_) appendDfs(g.get(), out);
for (const auto& group : root_groups_) appendDfs(group.get(), out);
return out;
}
void Federation::appendDfs(const Group* g, std::vector<const Group*>& out) {
if (!g) return;
out.push_back(g);
for (const auto& c : g->children) appendDfs(c.get(), out);
void Federation::appendDfs(const Group* group, std::vector<const Group*>& out) {
if (!group) return;
out.push_back(group);
for (const auto& c : group->children) appendDfs(c.get(), out);
}
std::unique_ptr<Federation::Group> Federation::detachGroup(Group* group) {
@@ -471,19 +471,19 @@ bool Federation::isDescendantOrSelf(const Group* group,
bool Federation::isGroupChainVisible(const QString& group_id) const {
if (group_id.isEmpty()) return true;
const Group* g = findGroupById(group_id);
while (g != nullptr) {
if (!g->visible) return false;
g = g->parent;
const Group* group = findGroupById(group_id);
while (group != nullptr) {
if (!group->visible) return false;
group = group->parent;
}
return true;
}
bool Federation::isModelEffectivelyVisible(const QString& fed_id) const {
const Model* m = findById(fed_id);
if (!m) return false;
if (!m->visible) return false;
return isGroupChainVisible(m->group_id);
bool Federation::isModelEffectivelyVisible(const QString& model_id) const {
const Model* model = findById(model_id);
if (!model) return false;
if (!model->visible) return false;
return isGroupChainVisible(model->group_id);
}
void Federation::markClean() {
@@ -496,9 +496,9 @@ void Federation::setDirty(bool d) {
emit dirtyChanged(d);
}
const Federation::Model* Federation::findById(const QString& fed_id) const {
for (const auto& m : models_) {
if (m.id == fed_id) return &m;
const Federation::Model* Federation::findById(const QString& model_id) const {
for (const auto& model : models_) {
if (model.id == model_id) return &model;
}
return nullptr;
}
@@ -508,14 +508,12 @@ QString Federation::addModel(const QString& source_path,
if (source_path.isEmpty()) return {};
if (isFederationPath(source_path)) return {}; // no nested federations
Model m;
m.id = generateId();
m.display_name = display_name.isEmpty()
? QFileInfo(source_path).fileName()
: display_name;
m.source_connector = "local";
m.source_path = QDir::cleanPath(QFileInfo(source_path).absoluteFilePath());
models_.push_back(std::move(m));
Model model;
model.id = generateId();
model.display_name = display_name;
model.source_connector = "local";
model.source_path = QDir::cleanPath(QFileInfo(source_path).absoluteFilePath());
models_.push_back(std::move(model));
const QString new_id = models_.back().id;
setDirty(true);
emit modelAdded(new_id);
@@ -527,25 +525,25 @@ QString Federation::addCloudModel(const QString& display_name,
const QJsonObject& source_data) {
if (connector_id.isEmpty() || connector_id == "local") return {};
Model m;
m.id = generateId();
m.display_name = display_name.isEmpty() ? m.id : display_name;
m.source_connector = connector_id;
m.source_data = source_data;
m.source_data.remove("connector"); // canonicalize: never duplicated
models_.push_back(std::move(m));
Model model;
model.id = generateId();
model.display_name = display_name.isEmpty() ? model.id : display_name;
model.source_connector = connector_id;
model.source_data = source_data;
model.source_data.remove("connector"); // canonicalize: never duplicated
models_.push_back(std::move(model));
const QString new_id = models_.back().id;
setDirty(true);
emit modelAdded(new_id);
return new_id;
}
void Federation::removeModel(const QString& fed_id) {
void Federation::removeModel(const QString& model_id) {
for (auto it = models_.begin(); it != models_.end(); ++it) {
if (it->id == fed_id) {
if (it->id == model_id) {
models_.erase(it);
setDirty(true);
emit modelRemoved(fed_id);
emit modelRemoved(model_id);
return;
}
}
@@ -632,18 +630,18 @@ bool Federation::load(const QString& path,
continue;
}
QJsonObject go = arr[i].toObject();
auto g = std::make_unique<Group>();
g->id = go.value("id").toString();
if (g->id.isEmpty()) g->id = generateId();
g->display_name = go.value("display_name").toString();
auto group = std::make_unique<Group>();
group->id = go.value("id").toString();
if (group->id.isEmpty()) group->id = generateId();
group->display_name = go.value("display_name").toString();
if (QJsonValue vv = go.value("visible"); vv.isBool())
g->visible = vv.toBool();
g->parent = parent;
group->visible = vv.toBool();
group->parent = parent;
if (QJsonValue cv = go.value("groups"); cv.isArray()) {
load_groups(cv.toArray(), g->children, g.get());
load_groups(cv.toArray(), group->children, group.get());
}
sink.push_back(std::move(g));
sink.push_back(std::move(group));
}
};
load_groups(root.value("groups").toArray(), root_groups_, nullptr);
@@ -657,60 +655,60 @@ bool Federation::load(const QString& path,
}
QJsonObject mo = arr[i].toObject();
Model m;
m.id = mo.value("id").toString();
if (m.id.isEmpty()) m.id = generateId();
m.display_name = mo.value("display_name").toString();
Model model;
model.id = mo.value("id").toString();
if (model.id.isEmpty()) model.id = generateId();
model.display_name = mo.value("display_name").toString();
QJsonObject so = mo.value("source").toObject();
m.source_connector = so.value("connector").toString("local");
if (m.source_connector == "local") {
model.source_connector = so.value("connector").toString("local");
if (model.source_connector == "local") {
QString stored = so.value("path").toString();
if (stored.isEmpty()) {
if (warnings) *warnings << QString("models[%1]: missing source.path; skipping.").arg(i);
continue;
}
m.source_path = resolvePath(fed_dir, stored);
if (m.display_name.isEmpty())
m.display_name = QFileInfo(m.source_path).fileName();
model.source_path = resolvePath(fed_dir, stored);
if (model.display_name.isEmpty())
model.display_name = QFileInfo(model.source_path).fileName();
} else {
// Cloud source: keep every key except "connector" itself; the
// connector resolves these to a local path on demand.
QJsonObject data = so;
data.remove("connector");
m.source_data = data;
if (m.display_name.isEmpty())
m.display_name = m.id;
model.source_data = data;
if (model.display_name.isEmpty())
model.display_name = model.id;
}
if (QJsonValue tv = mo.value("model_transformation"); tv.isObject()) {
QJsonObject to = tv.toObject();
const QString af = to.value("a_frame").toString("ModelGlobal");
m.model_transformation.a_frame =
model.model_transformation.a_frame =
(af == "ModelLocal") ? AFrame::ModelLocal : AFrame::ModelGlobal;
auto readVec3 = [](QJsonArray ja) {
if (ja.size() != 3) return Eigen::Vector3d::Zero().eval();
return Eigen::Vector3d(
ja[0].toDouble(), ja[1].toDouble(), ja[2].toDouble());
};
m.model_transformation.a = readVec3(to.value("a").toArray());
m.model_transformation.b = readVec3(to.value("b").toArray());
m.model_transformation.rxyz_deg = readVec3(to.value("rxyz_deg").toArray());
m.model_transformation.pivot = readVec3(to.value("pivot").toArray());
model.model_transformation.a = readVec3(to.value("a").toArray());
model.model_transformation.b = readVec3(to.value("b").toArray());
model.model_transformation.rxyz_deg = readVec3(to.value("rxyz_deg").toArray());
model.model_transformation.pivot = readVec3(to.value("pivot").toArray());
}
QJsonValue vv = mo.value("visible");
if (vv.isBool()) m.visible = vv.toBool();
if (vv.isBool()) model.visible = vv.toBool();
m.group_id = mo.value("group_id").toString();
if (!m.group_id.isEmpty() && findGroupById(m.group_id) == nullptr) {
model.group_id = mo.value("group_id").toString();
if (!model.group_id.isEmpty() && findGroupById(model.group_id) == nullptr) {
if (warnings)
*warnings << QString("models[%1]: unknown group_id '%2'; moved to root.")
.arg(i).arg(m.group_id);
m.group_id.clear();
.arg(i).arg(model.group_id);
model.group_id.clear();
}
models_.push_back(std::move(m));
models_.push_back(std::move(model));
}
QJsonValue hv = root.value("home_view");
@@ -842,12 +840,12 @@ bool Federation::writeJsonAt(const QString& abs_path,
std::function<QJsonArray(const std::vector<std::unique_ptr<Group>>&)> dump;
dump = [&](const std::vector<std::unique_ptr<Group>>& src) {
QJsonArray out;
for (const auto& g : src) {
for (const auto& group : src) {
QJsonObject go;
go["id"] = g->id;
go["display_name"] = g->display_name;
if (!g->visible) go["visible"] = false;
if (!g->children.empty()) go["groups"] = dump(g->children);
go["id"] = group->id;
go["display_name"] = group->display_name;
if (!group->visible) go["visible"] = false;
if (!group->children.empty()) go["groups"] = dump(group->children);
out.append(go);
}
return out;
@@ -856,18 +854,18 @@ bool Federation::writeJsonAt(const QString& abs_path,
}
QJsonArray arr;
for (const auto& m : models_) {
for (const auto& model : models_) {
QJsonObject mo;
mo["id"] = m.id;
mo["display_name"] = m.display_name;
mo["id"] = model.id;
mo["display_name"] = model.display_name;
QJsonObject so;
so["connector"] = m.source_connector;
if (m.source_connector == "local") {
so["path"] = relativizePath(fed_dir, m.source_path);
so["connector"] = model.source_connector;
if (model.source_connector == "local") {
so["path"] = relativizePath(fed_dir, model.source_path);
} else {
// Round-trip connector-specific keys verbatim.
for (auto it = m.source_data.begin(); it != m.source_data.end(); ++it) {
for (auto it = model.source_data.begin(); it != model.source_data.end(); ++it) {
so[it.key()] = it.value();
}
}
@@ -875,7 +873,7 @@ bool Federation::writeJsonAt(const QString& abs_path,
// Skip model_transformation when it's at defaults (identity placement).
const ModelTransformation def;
const ModelTransformation& xf = m.model_transformation;
const ModelTransformation& xf = model.model_transformation;
const bool xf_is_default =
xf.a_frame == def.a_frame && xf.a == def.a && xf.b == def.b &&
xf.rxyz_deg == def.rxyz_deg && xf.pivot == def.pivot;
@@ -895,8 +893,8 @@ bool Federation::writeJsonAt(const QString& abs_path,
mo["model_transformation"] = to;
}
if (!m.visible) mo["visible"] = false;
if (!m.group_id.isEmpty()) mo["group_id"] = m.group_id;
if (!model.visible) mo["visible"] = false;
if (!model.group_id.isEmpty()) mo["group_id"] = model.group_id;
arr.append(mo);
}
+19 -17
View File
@@ -244,8 +244,10 @@ public:
// Mutations
void clear();
// display_name is stored verbatim — callers decide the label (typically
// QFileInfo(source_path).fileName() for local files). No implicit fallback.
QString addModel(const QString& source_path,
const QString& display_name = QString());
const QString& display_name);
// Add a model whose source is a cloud connector (anything other than
// "local"). `source_data` holds the connector-specific keys; the
// top-level "connector" field, if present, is overwritten with
@@ -253,28 +255,28 @@ public:
QString addCloudModel(const QString& display_name,
const QString& connector_id,
const QJsonObject& source_data);
void removeModel(const QString& fed_id);
void removeModel(const QString& model_id);
void setHomeView(const HomeView& hv);
void clearHomeView();
void setConfig(const FederationConfig&);
void setFederatedFalseOrigin(const FederatedFalseOrigin&);
void setModelTransformation(const QString& fed_id, const ModelTransformation&);
void setModelVisible(const QString& fed_id, bool visible);
// Rename a model. No-op when fed_id is unknown, name is empty, or
void setModelTransformation(const QString& model_id, const ModelTransformation&);
void setModelVisible(const QString& model_id, bool visible);
// Rename a model. No-op when model_id is unknown, name is empty, or
// name is unchanged.
void setModelDisplayName(const QString& fed_id, const QString& display_name);
void setModelDisplayName(const QString& model_id, const QString& display_name);
// Replace a model's source. Used after push_model[_interactive] when
// the connector reports a fresh source (e.g. a new version_id) or when
// a previously-local model gets uploaded for the first time. The
// top-level "connector" key in `source_data`, if any, is dropped —
// it's expressed via `connector_id`.
void setModelSource(const QString& fed_id,
void setModelSource(const QString& model_id,
const QString& connector_id,
const QJsonObject& source_data);
// Reassign a model to a group (or to root, when group_id is empty).
// No-op when fed_id is unknown or group_id is unknown-and-non-empty.
void setModelGroup(const QString& fed_id, const QString& group_id);
// No-op when model_id is unknown or group_id is unknown-and-non-empty.
void setModelGroup(const QString& model_id, const QString& group_id);
// Group mutations. All return / accept stable group ids.
QString addGroup(const QString& display_name = QString(),
@@ -292,7 +294,7 @@ public:
// Accessors
const std::vector<Model>& models() const { return models_; }
const Model* findById(const QString& fed_id) const;
const Model* findById(const QString& model_id) const;
// Top-level groups in insertion order; descend via Group::children.
const std::vector<std::unique_ptr<Group>>& rootGroups() const { return root_groups_; }
const Group* findGroupById(const QString& group_id) const;
@@ -304,7 +306,7 @@ public:
bool isGroupChainVisible(const QString& group_id) const;
// True iff the model exists, its own `visible` is true, and every
// ancestor group is visible.
bool isModelEffectivelyVisible(const QString& fed_id) const;
bool isModelEffectivelyVisible(const QString& model_id) const;
bool isDirty() const { return dirty_; }
void markClean();
QString filePath() const { return file_path_; }
@@ -332,14 +334,14 @@ signals:
// to dirtyChanged from the corresponding setters.
void configChanged();
void federatedFalseOriginChanged();
void modelAdded(const QString& fed_id);
void modelRemoved(const QString& fed_id);
void modelTransformationChanged(const QString& fed_id);
void modelVisibilityChanged(const QString& fed_id, bool visible);
void modelGroupChanged(const QString& fed_id, const QString& group_id);
void modelAdded(const QString& model_id);
void modelRemoved(const QString& model_id);
void modelTransformationChanged(const QString& model_id);
void modelVisibilityChanged(const QString& model_id, bool visible);
void modelGroupChanged(const QString& model_id, const QString& group_id);
// Emitted on rename / source change — anything that affects how the
// model is displayed but is not covered by the other granular signals.
void modelChanged(const QString& fed_id);
void modelChanged(const QString& model_id);
void groupAdded(const QString& group_id);
void groupRemoved(const QString& group_id);
+8 -8
View File
@@ -86,7 +86,7 @@ void GeometryStreamer::setIfcFile(std::unique_ptr<ifcopenshell::file> file) {
ifc_file_ = std::move(file);
}
void GeometryStreamer::loadFile(const std::string& path, uint32_t start_object_id, uint32_t model_id, int num_threads) {
void GeometryStreamer::loadFile(const std::string& path, uint32_t session_model_id, int num_threads) {
if (running_.load()) {
cancel();
if (worker_thread_ && worker_thread_->isRunning()) {
@@ -99,8 +99,8 @@ void GeometryStreamer::loadFile(const std::string& path, uint32_t start_object_i
succeeded_ = false;
running_ = true;
progress_ = 0;
next_object_id_ = start_object_id;
model_id_ = model_id;
next_object_id_ = 1; // model-local; globalized at applyCachedModel install time
session_model_id_ = session_model_id;
{
std::lock_guard<std::mutex> lock(elements_mutex_);
@@ -153,12 +153,12 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
// compensates by post-multiplying each instance's PlacementTransformation
// by T(+offset), which is mathematically the identity overall but moves
// the magnitude off the float-precision-sensitive vertex column.
static StreamedMesh buildStreamedMesh(uint32_t model_id,
static StreamedMesh buildStreamedMesh(uint32_t session_model_id,
uint32_t local_mesh_id,
const IfcGeom::TriangulationElement* elem,
const Eigen::Vector3d& offset) {
StreamedMesh mesh;
mesh.model_id = model_id;
mesh.session_model_id = session_model_id;
mesh.local_mesh_id = local_mesh_id;
const auto& geom = elem->geometry();
@@ -557,7 +557,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
ElementInfo info;
info.object_id = object_id;
info.model_id = model_id_;
info.session_model_id = session_model_id_;
info.ifc_id = tri_elem->id();
info.guid = tri_elem->guid();
info.name = tri_elem->name();
@@ -604,7 +604,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
}
StreamedMesh streamed_mesh =
buildStreamedMesh(model_id_, local_mesh_id, tri_elem, offset);
buildStreamedMesh(session_model_id_, local_mesh_id, tri_elem, offset);
MeshAabb mesh_aabb;
for (int a = 0; a < 3; ++a) {
mesh_aabb.lmin[a] = streamed_mesh.local_aabb_min[a];
@@ -635,7 +635,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
}
StreamedInstance inst;
inst.model_id = model_id_;
inst.session_model_id = session_model_id_;
inst.local_mesh_id = local_mesh_id;
inst.object_id = object_id;
inst.color_override_rgba8 = 0;
+6 -5
View File
@@ -36,7 +36,7 @@
struct ElementInfo {
uint32_t object_id;
uint32_t model_id;
uint32_t session_model_id;
int ifc_id;
std::string guid;
std::string name;
@@ -49,7 +49,9 @@ public:
explicit GeometryStreamer(QObject* parent = nullptr);
~GeometryStreamer();
void loadFile(const std::string& path, uint32_t start_object_id, uint32_t model_id, int num_threads = 0);
// Streamer stamps model-LOCAL object_ids (1..N, reset each load).
// ViewportCore::applyCachedModel assigns the session-global ids at install.
void loadFile(const std::string& path, uint32_t session_model_id, int num_threads = 0);
void cancel();
// Adopt an externally-opened ifcopenshell::file as the data source
@@ -59,8 +61,7 @@ public:
bool isRunning() const { return running_.load(); }
int progress() const { return progress_.load(); }
uint32_t lastObjectId() const { return next_object_id_; }
uint32_t modelId() const { return model_id_; }
uint32_t sessionModelId() const { return session_model_id_; }
ifcopenshell::file* ifcFile() const { return ifc_file_.get(); }
@@ -89,7 +90,7 @@ private:
std::vector<ElementInfo> pending_elements_;
uint32_t next_object_id_ = 1;
uint32_t model_id_ = 0;
uint32_t session_model_id_ = 0;
};
#endif // GEOMETRYSTREAMER_H
+2 -2
View File
@@ -79,13 +79,13 @@ bool findInstanceInModels(
const std::unordered_map<uint32_t, ModelGpuData>& models,
InstanceLookup& out) {
if (object_id == 0) return false;
for (const auto& [model_id, model_data] : models) {
for (const auto& [session_model_id, model_data] : models) {
auto it = model_data.object_id_to_instance.find(object_id);
if (it == model_data.object_id_to_instance.end()) continue;
const uint32_t instance_index = it->second;
if (instance_index >= model_data.instances.size()) continue;
const InstanceInfo& instance = model_data.instances[instance_index];
out.model_id = model_id;
out.session_model_id = session_model_id;
out.mesh_id = instance.mesh_id;
std::memcpy(out.placement_transformation,
instance.placement_transformation,
+2 -2
View File
@@ -69,13 +69,13 @@ void composeInstance(
// / ModelTransformation) — the same convention as InstanceInfo so the
// measurement / picking tools can re-compose at need.
struct InstanceLookup {
uint32_t model_id = 0;
uint32_t session_model_id = 0;
uint32_t mesh_id = 0;
double placement_transformation[16]{};
};
// Walk a map of models looking for the one that owns `object_id`,
// fill `out` with that instance's (model_id, mesh_id, placement) and
// fill `out` with that instance's (session_model_id, mesh_id, placement) and
// return true. Returns false for object_id == 0 (the sentinel for
// "no object") or when no model owns the id. Defensive: skips
// instances whose stored index is out-of-range for the model's
+3 -3
View File
@@ -113,7 +113,7 @@ struct InstanceInfo {
uint32_t mesh_id = 0; // index into meshes array
uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0;
uint32_t model_id = 0;
uint32_t session_model_id = 0;
double placement_transformation[16]{};
float transform[16]{};
float world_aabb_min[3]{};
@@ -126,7 +126,7 @@ struct InstanceInfo {
// geometry in local coords. `local_mesh_id` is the streamer-assigned id
// within this model.
struct StreamedMesh {
uint32_t model_id = 0;
uint32_t session_model_id = 0;
uint32_t local_mesh_id = 0;
std::vector<float> vertices; // 7 floats * N_verts (pos3+norm3+color1_packed)
std::vector<uint32_t> indices;
@@ -138,7 +138,7 @@ struct StreamedMesh {
// iterator). For the first instance of a mesh, the StreamedMesh is emitted
// just before this.
struct StreamedInstance {
uint32_t model_id = 0;
uint32_t session_model_id = 0;
uint32_t local_mesh_id = 0;
uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0;
+1 -1
View File
@@ -535,7 +535,7 @@ void LengthMeasurement::rebuildLaserOverlay(ViewportWindow& vp) {
ViewportWindow::MeshTriangles tris;
bool have_extent = false;
double min_t1 = 0.0, max_t1 = 0.0, min_t2 = 0.0, max_t2 = 0.0;
if (vp.readbackMeshTriangles(first_pick_.model_id, first_pick_.mesh_id, tris)) {
if (vp.readbackMeshTriangles(first_pick_.session_model_id, first_pick_.mesh_id, tris)) {
const size_t n_verts = tris.positions.size() / 3;
const size_t n_tris = tris.indices.size() / 3;
if (n_tris > 0) {
+1 -1
View File
@@ -263,7 +263,7 @@ struct ModelGpuData {
// for chunks that were never evicted or were LRU-evicted (the
// latter doesn't have an obvious "evictor" — just a slot
// pressure event).
uint32_t last_evicted_by_model_id = 0;
uint32_t last_evicted_by_session_model_id = 0;
uint32_t last_evicted_by_chunk_idx = UINT32_MAX;
float last_evicted_by_priority = 0.0f;
// Frame at which this chunk was most recently evicted, so the
+147 -140
View File
@@ -63,38 +63,38 @@ void SceneLoader::joinDataSourceThreads() {
data_source_threads_.clear();
}
QString SceneLoader::filePath(uint32_t mid) const {
auto it = models_.find(mid);
QString SceneLoader::filePath(uint32_t session_model_id) const {
auto it = models_.find(session_model_id);
return it == models_.end() ? QString() : it->second.file_path;
}
QString SceneLoader::displayName(uint32_t mid) const {
auto it = models_.find(mid);
QString SceneLoader::displayName(uint32_t session_model_id) const {
auto it = models_.find(session_model_id);
return it == models_.end() ? QString() : it->second.display_name;
}
ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const {
auto it = models_.find(mid);
ifcopenshell::file* SceneLoader::ifcFile(uint32_t session_model_id) const {
auto it = models_.find(session_model_id);
return it == models_.end() ? nullptr : it->second.streamer->ifcFile();
}
const ModelGeoref* SceneLoader::modelGeoref(uint32_t mid) {
auto it = models_.find(mid);
const ModelGeoref* SceneLoader::modelGeoref(uint32_t session_model_id) {
auto it = models_.find(session_model_id);
if (it == models_.end()) return nullptr;
auto& m = it->second;
if (m.has_georef) return &m.georef;
auto* file = m.streamer ? m.streamer->ifcFile() : nullptr;
auto& model = it->second;
if (model.has_georef) return &model.georef;
auto* file = model.streamer ? model.streamer->ifcFile() : nullptr;
if (!file) return nullptr;
m.georef = computeModelGeoref(file);
m.has_georef = true;
return &m.georef;
model.georef = computeModelGeoref(file);
model.has_georef = true;
return &model.georef;
}
std::vector<uint32_t> SceneLoader::addFiles(const QStringList& paths) {
std::vector<uint32_t> SceneLoader::queueModels(const QStringList& paths) {
std::vector<uint32_t> assigned;
assigned.reserve(paths.size());
for (const auto& path : paths) {
uint32_t id = next_model_id_++;
uint32_t id = next_session_model_id_++;
Model model;
model.id = id;
model.file_path = path;
@@ -105,7 +105,7 @@ std::vector<uint32_t> SceneLoader::addFiles(const QStringList& paths) {
assigned.push_back(id);
}
if (loading_model_id_ == 0) {
if (loading_session_model_id_ == 0) {
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
}
return assigned;
@@ -126,18 +126,18 @@ void SceneLoader::connectStreamer(GeometryStreamer* streamer) {
this, &SceneLoader::onStreamerError, Qt::QueuedConnection);
}
void SceneLoader::removeModel(uint32_t mid) {
void SceneLoader::removeModel(uint32_t session_model_id) {
// Refuse while the model is the active load: the streamer thread is still
// running and would race with the deleteLater(). UI gates Remove on
// isLoading(), but guard here too.
if (loading_model_id_ == mid) return;
if (loading_session_model_id_ == session_model_id) return;
for (auto it = load_queue_.begin(); it != load_queue_.end();) {
if (*it == mid) it = load_queue_.erase(it);
if (*it == session_model_id) it = load_queue_.erase(it);
else ++it;
}
auto it = models_.find(mid);
auto it = models_.find(session_model_id);
if (it == models_.end()) return;
if (it->second.streamer) {
it->second.streamer->deleteLater();
@@ -146,29 +146,29 @@ void SceneLoader::removeModel(uint32_t mid) {
}
void SceneLoader::cancelCurrentLoad() {
if (loading_model_id_ == 0) return;
auto it = models_.find(loading_model_id_);
if (loading_session_model_id_ == 0) return;
auto it = models_.find(loading_session_model_id_);
if (it == models_.end() || it->second.streamer == nullptr) return;
it->second.streamer->cancel();
}
void SceneLoader::startNextLoad() {
if (load_queue_.empty()) {
loading_model_id_ = 0;
loading_session_model_id_ = 0;
emit allLoadsFinished();
return;
}
loading_model_id_ = load_queue_.front();
loading_session_model_id_ = load_queue_.front();
load_queue_.pop_front();
auto& model = models_[loading_model_id_];
auto& model = models_[loading_session_model_id_];
model.load_timer.restart();
emit loadStarted(model.id, model.display_name);
std::string ifc_path = model.file_path.toStdString();
uint32_t mid = loading_model_id_;
uint32_t session_model_id = loading_session_model_id_;
const bool is_sidecar_source =
QFileInfo(model.file_path).suffix().compare("ifcview", Qt::CaseInsensitive) == 0;
@@ -176,24 +176,24 @@ void SceneLoader::startNextLoad() {
// .ifcview file directly). Skip the background thread and go straight
// to a stream load.
if (!is_sidecar_source && !should_read_sidecar_) {
startStreamLoadFor(mid);
loadFromGeometryStreamer(session_model_id);
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]() {
QElapsedTimer rt; rt.start();
auto cached = readSidecarMetadataOnly(ifc_path);
sidecar_read_thread_ = std::thread([this, ifc_path, session_model_id, is_sidecar_source]() {
QElapsedTimer read_timer; read_timer.start();
auto cached = readSidecarMetadata(ifc_path);
std::fprintf(stderr, "[info] Sidecar metadata read: %lld ms (%s)\n",
(long long)rt.elapsed(), ifc_path.c_str());
(long long)read_timer.elapsed(), ifc_path.c_str());
auto result = std::make_shared<std::optional<StreamingSidecar>>(std::move(cached));
QMetaObject::invokeMethod(this, [this, mid, result, is_sidecar_source]() {
auto it = models_.find(mid);
QMetaObject::invokeMethod(this, [this, session_model_id, result, is_sidecar_source]() {
auto it = models_.find(session_model_id);
if (*result && !(*result)->meta.instances.empty()) {
applySidecarData(mid, std::move(**result));
applySidecarData(session_model_id, std::move(**result));
if (!is_sidecar_source) {
startDataSourceLoad(mid);
startDataSourceLoad(session_model_id);
}
return;
}
@@ -201,105 +201,102 @@ void SceneLoader::startNextLoad() {
if (it == models_.end()) return;
if (is_sidecar_source) {
loading_model_id_ = 0;
emit loadError(mid, QString("Failed to read IFC Viewer cache:\n%1").arg(it->second.file_path));
loading_session_model_id_ = 0;
emit loadError(session_model_id, QString("Failed to read IFC Viewer cache:\n%1").arg(it->second.file_path));
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
return;
}
startStreamLoadFor(mid);
loadFromGeometryStreamer(session_model_id);
}, Qt::QueuedConnection);
});
}
void SceneLoader::startStreamLoadFor(uint32_t mid) {
auto it = models_.find(mid);
void SceneLoader::loadFromGeometryStreamer(uint32_t session_model_id) {
auto it = models_.find(session_model_id);
if (it == models_.end()) return;
auto& m = it->second;
auto& model = 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();
model.sidecar_builder = std::make_unique<SidecarBuilder>();
}
connectStreamer(m.streamer);
// Elements are buffered here and emitted to the registry once at finalize,
// after applyCachedModel assigns this model's global object_id base.
model.streamed_elements.clear();
connectStreamer(model.streamer);
element_poll_timer_.start();
m.streamer->loadFile(
m.file_path.toStdString(), next_object_id_, loading_model_id_);
model.streamer->loadFile(model.file_path.toStdString(), loading_session_model_id_);
}
void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) {
auto it = models_.find(mid);
void SceneLoader::applySidecarData(uint32_t session_model_id, StreamingSidecar metadata) {
auto it = models_.find(session_model_id);
if (it == models_.end()) return;
auto& model = it->second;
SidecarData& d = metadata.meta;
SidecarData& sidecar = metadata.meta;
std::fprintf(stderr,
"[info] Sidecar hit: %s (%zu chunks, %zu meshes, %zu instances, %zu elements)\n",
model.file_path.toStdString().c_str(),
d.chunks.size(),
d.meshes.size(),
d.instances.size(),
d.elements.size());
// Rebase object/model IDs onto the current session's ID space. Two
// cached models both starting at object_id=1 would collide otherwise.
uint32_t min_oid = UINT32_MAX;
for (const auto& pe : d.elements) {
if (pe.object_id < min_oid) min_oid = pe.object_id;
}
uint32_t oid_offset = 0;
if (!d.elements.empty() && min_oid < UINT32_MAX) {
oid_offset = next_object_id_ - min_oid;
}
for (auto& pe : d.elements) {
pe.object_id += oid_offset;
pe.model_id = mid;
if (pe.object_id >= next_object_id_)
next_object_id_ = pe.object_id + 1;
}
for (auto& inst : d.instances) {
inst.object_id += oid_offset;
inst.model_id = mid;
}
sidecar.chunks.size(),
sidecar.meshes.size(),
sidecar.instances.size(),
sidecar.elements.size());
// Restore the cached CoordinateOperation into the model so
// modelGeoref(mid) returns it without needing the IFC source. Prevents
// modelGeoref(session_model_id) returns it without needing the IFC source. Prevents
// sidecar-loaded models from silently losing their georef when the
// .ifc/.rdb sibling is absent.
{
ModelGeoref& gr = model.georef;
gr.has_coordinate_operation = d.has_coordinate_operation != 0;
Eigen::Map<const Eigen::Matrix<double, 4, 4, Eigen::ColMajor>> M(
d.coordinate_operation_meters);
gr.coordinate_operation_meters = M;
gr.units.project_length_to_meters = d.project_length_to_meters;
gr.units.map_unit_to_meters = d.map_unit_to_meters;
model.has_georef = true;
ModelGeoref& georef = model.georef;
georef.has_coordinate_operation = sidecar.has_coordinate_operation != 0;
Eigen::Map<const Eigen::Matrix<double, 4, 4, Eigen::ColMajor>> coord_op(
sidecar.coordinate_operation_meters);
georef.coordinate_operation_meters = coord_op;
georef.units.project_length_to_meters = sidecar.project_length_to_meters;
georef.units.map_unit_to_meters = sidecar.map_unit_to_meters;
model.has_georef = true;
}
std::vector<ElementTableRecord> elements = std::move(d.elements);
std::string stbl = std::move(d.string_table);
// Pull the element table out before applyCachedModel consumes the metadata.
// The geometry upload doesn't touch elements; it only reads/moves meshes and
// instances.
std::vector<ElementTableRecord> elements = std::move(sidecar.elements);
std::string string_table = std::move(sidecar.string_table);
viewport_->applyCachedModel(mid, std::move(metadata));
// applyCachedModel is the sole authority for the global object_id space: the
// sidecar stores model-LOCAL ids, and it assigns each instance's global id as
// base + local, storing the base on the model. The element table gets the
// same base below — one authority, two halves, no separate id assignment.
viewport_->applyCachedModel(session_model_id, std::move(metadata));
emit sidecarElementsReady(mid, std::move(elements), std::move(stbl));
// Stamp the element records with the same base applyCachedModel gave the
// instances, so registry ids match the ids pick/selection return. Mirrors
// ViewportCore::loadElementMetadataWeb and the live-stream path
// (onStreamerFinished).
const uint32_t base = viewport_->modelObjectIdBase(session_model_id);
for (auto& element : elements) {
element.object_id += base;
element.session_model_id = session_model_id;
}
qint64 ms = model.load_timer.elapsed();
emit loadedFromSidecar(mid, ms);
emit sidecarElementsReady(session_model_id, std::move(elements), std::move(string_table));
loading_model_id_ = 0;
qint64 elapsed_ms = model.load_timer.elapsed();
emit loadedFromSidecar(session_model_id, elapsed_ms);
loading_session_model_id_ = 0;
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
}
void SceneLoader::startDataSourceLoad(uint32_t mid) {
auto it = models_.find(mid);
void SceneLoader::startDataSourceLoad(uint32_t session_model_id) {
auto it = models_.find(session_model_id);
if (it == models_.end()) return;
std::string data_path_std = it->second.file_path.toStdString();
data_source_threads_.emplace_back([this, mid, data_path_std]() {
QElapsedTimer t; t.start();
data_source_threads_.emplace_back([this, session_model_id, data_path_std]() {
QElapsedTimer timer; timer.start();
std::unique_ptr<ifcopenshell::file> file;
try {
file = std::make_unique<ifcopenshell::file>(
@@ -310,11 +307,11 @@ void SceneLoader::startDataSourceLoad(uint32_t mid) {
return;
}
std::fprintf(stderr, "[info] Data source load: %lld ms (%s)\n",
(long long)t.elapsed(), data_path_std.c_str());
(long long)timer.elapsed(), data_path_std.c_str());
auto shared = std::make_shared<std::unique_ptr<ifcopenshell::file>>(std::move(file));
QMetaObject::invokeMethod(this, [this, mid, shared]() {
auto it = models_.find(mid);
QMetaObject::invokeMethod(this, [this, session_model_id, shared]() {
auto it = models_.find(session_model_id);
if (it == models_.end()) return;
auto* streamer = it->second.streamer;
if (streamer == nullptr) return;
@@ -322,7 +319,7 @@ void SceneLoader::startDataSourceLoad(uint32_t mid) {
// path somehow populated it), don't clobber it.
if (streamer->ifcFile() != nullptr) return;
streamer->setIfcFile(std::move(*shared));
emit dataSourceReady(mid);
emit dataSourceReady(session_model_id);
}, Qt::QueuedConnection);
});
}
@@ -333,8 +330,8 @@ void SceneLoader::onStreamerProgressChanged(int percent) {
void SceneLoader::onStreamerMeshReady(StreamedMesh mesh) {
viewport_->uploadStreamedMesh(mesh);
if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_);
if (loading_session_model_id_ != 0) {
auto it = models_.find(loading_session_model_id_);
if (it != models_.end() && it->second.sidecar_builder) {
it->second.sidecar_builder->onMeshReady(mesh);
}
@@ -342,8 +339,8 @@ void SceneLoader::onStreamerMeshReady(StreamedMesh mesh) {
}
void SceneLoader::onStreamerInstanceReady(StreamedInstance instance_record) {
if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_);
if (loading_session_model_id_ != 0) {
auto it = models_.find(loading_session_model_id_);
if (it != models_.end() && it->second.sidecar_builder) {
it->second.sidecar_builder->onInstanceReady(instance_record);
}
@@ -352,76 +349,86 @@ void SceneLoader::onStreamerInstanceReady(StreamedInstance instance_record) {
}
void SceneLoader::onElementPollTick() {
if (loading_model_id_ == 0) return;
auto it = models_.find(loading_model_id_);
if (loading_session_model_id_ == 0) return;
auto it = models_.find(loading_session_model_id_);
if (it == models_.end()) return;
auto batch = it->second.streamer->drainElements();
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));
// Buffer the whole set. The streamer stamps model-LOCAL object_ids, so we
// can't hand these to the registry yet — they're globalized and emitted
// once at finalize (onStreamerFinished), after applyCachedModel assigns
// this model's object_id base. The sidecar builder also reads this buffer.
auto& buf = it->second.streamed_elements;
buf.insert(buf.end(), batch.begin(), batch.end());
}
void SceneLoader::onStreamerFinished() {
element_poll_timer_.stop();
onElementPollTick(); // drain any remaining elements
uint32_t mid = loading_model_id_;
if (mid != 0) {
auto it = models_.find(mid);
uint32_t session_model_id = loading_session_model_id_;
if (session_model_id != 0) {
auto it = models_.find(session_model_id);
if (it != models_.end()) {
auto& m = it->second;
next_object_id_ = m.streamer->lastObjectId();
viewport_->finalizeModel(mid);
auto& model = it->second;
viewport_->finalizeModel(session_model_id);
// Sidecar finalize + disk write. Wgpu has no live LOD1 apply —
// LOD1 indices land in the on-disk sidecar and are picked up
// on the *next* open of this file; first-session view is
// LOD0-only. Acceptable trade-off vs reallocating chunk index
// slices live to splice LOD1 in.
if (m.sidecar_builder) {
//
// The sidecar is written from the LOCAL element/instance ids (the
// globalization below happens after), so a re-opened .ifcview
// stores model-local ids exactly like a freshly-streamed one.
if (model.sidecar_builder) {
ModelGeoref georef;
if (auto* file = m.streamer->ifcFile()) {
if (auto* file = model.streamer->ifcFile()) {
georef = computeModelGeoref(file);
}
QElapsedTimer wt; wt.start();
SidecarData data = m.sidecar_builder->finalize(georef, m.streamed_elements);
QElapsedTimer write_timer; write_timer.start();
SidecarData data = model.sidecar_builder->finalize(georef, model.streamed_elements);
// Lay geometry out in streaming-chunk order + bake the chunk TOC
// (v14) so it streams as one contiguous range per chunk.
reorderSidecarByMorton(data);
const bool ok = writeSidecar(m.file_path.toStdString(), data);
const bool ok = writeSidecar(model.file_path.toStdString(), data);
std::fprintf(stderr,
"[info] Sidecar finalize + write: %lld ms (%s)\n",
(long long)wt.elapsed(), ok ? "ok" : "FAILED");
m.sidecar_builder.reset();
m.streamed_elements.clear();
m.streamed_elements.shrink_to_fit();
(long long)write_timer.elapsed(), ok ? "ok" : "FAILED");
model.sidecar_builder.reset();
}
qint64 ms = m.load_timer.elapsed();
emit loadedFromStream(mid, ms);
// Globalize the buffered element ids by the base applyCachedModel
// assigned to this model's instances, then hand them to the
// registry — one emit, ids matching the GPU/pick space. Mirrors the
// sidecar-hit path (applySidecarData).
const uint32_t base = viewport_->modelObjectIdBase(session_model_id);
for (auto& element : model.streamed_elements) element.object_id += base;
emit streamedElementsReady(session_model_id, std::move(model.streamed_elements));
model.streamed_elements.clear();
model.streamed_elements.shrink_to_fit();
qint64 elapsed_ms = model.load_timer.elapsed();
emit loadedFromStream(session_model_id, elapsed_ms);
}
}
loading_model_id_ = 0;
loading_session_model_id_ = 0;
startNextLoad();
}
void SceneLoader::onStreamerCancelled() {
element_poll_timer_.stop();
const uint32_t mid = loading_model_id_;
loading_model_id_ = 0;
const uint32_t session_model_id = loading_session_model_id_;
loading_session_model_id_ = 0;
if (mid != 0) {
viewport_->removeModel(mid);
emit loadCancelled(mid);
if (session_model_id != 0) {
viewport_->removeModel(session_model_id);
emit loadCancelled(session_model_id);
}
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
}
@@ -429,12 +436,12 @@ void SceneLoader::onStreamerCancelled() {
void SceneLoader::onStreamerError(const QString& msg) {
element_poll_timer_.stop();
const uint32_t mid = loading_model_id_;
loading_model_id_ = 0;
const uint32_t session_model_id = loading_session_model_id_;
loading_session_model_id_ = 0;
if (mid != 0) {
viewport_->removeModel(mid);
if (session_model_id != 0) {
viewport_->removeModel(session_model_id);
}
emit loadError(mid, msg);
emit loadError(session_model_id, msg);
QTimer::singleShot(0, this, &SceneLoader::startNextLoad);
}
+25 -26
View File
@@ -69,61 +69,61 @@ public:
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.
// Returns the session_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.
std::vector<uint32_t> addFiles(const QStringList& paths);
std::vector<uint32_t> queueModels(const QStringList& paths);
void cancelCurrentLoad();
bool isLoading() const { return loading_model_id_ != 0 || !load_queue_.empty(); }
bool isLoadingModel(uint32_t mid) const { return loading_model_id_ == mid; }
bool isLoading() const { return loading_session_model_id_ != 0 || !load_queue_.empty(); }
bool isLoadingModel(uint32_t session_model_id) const { return loading_session_model_id_ == session_model_id; }
size_t modelCount() const { return models_.size(); }
// Drop the loader's tracking for `mid` — its streamer, file path, georef
// Drop the loader's tracking for `session_model_id` — its streamer, file path, georef
// cache, and queue slot if still pending. Caller is responsible for the
// viewport / UI cleanup; this only releases the loader's own state.
// Refuses while the model is the active load (use cancelCurrentLoad first).
void removeModel(uint32_t mid);
void removeModel(uint32_t session_model_id);
QString filePath(uint32_t mid) const;
QString displayName(uint32_t mid) const;
ifcopenshell::file* ifcFile(uint32_t mid) const;
QString filePath(uint32_t session_model_id) const;
QString displayName(uint32_t session_model_id) const;
ifcopenshell::file* ifcFile(uint32_t session_model_id) const;
// Lazily computes the model's georef matrix + unit scales the first
// time it's asked for, caches the result, and returns a pointer into the
// cache. Returns nullptr when the IFC file isn't available yet (e.g.
// sidecar-hit path before the data-source thread populates the streamer).
const ModelGeoref* modelGeoref(uint32_t mid);
const ModelGeoref* modelGeoref(uint32_t session_model_id);
signals:
void progressChanged(int percent);
void loadStarted(uint32_t mid, QString display_name);
void loadStarted(uint32_t session_model_id, QString display_name);
// Fired once per sidecar hit, before loadedFromSidecar, with the full
// packed element set. Consumer is responsible for decoding + tree/
// property-map population. Moved arguments — avoid unnecessary copies.
void sidecarElementsReady(uint32_t mid,
void sidecarElementsReady(uint32_t session_model_id,
std::vector<ElementTableRecord> elements,
std::string string_table);
void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
void loadedFromSidecar(uint32_t session_model_id, qint64 elapsed_ms);
// Fired after a sidecar-hit model has its .rdb/.ifc opened as a
// property data source in the background. Consumers can refresh
// any UI that queries ifcFile(mid) for attributes/properties.
void dataSourceReady(uint32_t mid);
// any UI that queries ifcFile(session_model_id) for attributes/properties.
void dataSourceReady(uint32_t session_model_id);
// Fired repeatedly while streaming, as the worker thread produces
// elements. Each batch contains whatever accumulated since the last
// poll tick.
void streamedElementsReady(uint32_t mid, std::vector<ElementInfo> elements);
void streamedElementsReady(uint32_t session_model_id, std::vector<ElementInfo> elements);
// Fired once after the streamer finishes and the viewport has been
// finalized. Consumer may synchronously perform work that needs all
// elements to be known (e.g. sidecar write) — SceneLoader will only
// start the next queued load after all slots return.
void loadedFromStream(uint32_t mid, qint64 elapsed_ms);
void loadCancelled(uint32_t mid);
void loadedFromStream(uint32_t session_model_id, qint64 elapsed_ms);
void loadCancelled(uint32_t session_model_id);
void loadError(uint32_t mid, QString message);
void loadError(uint32_t session_model_id, QString message);
void allLoadsFinished();
private slots:
@@ -143,7 +143,7 @@ private:
GeometryStreamer* streamer = nullptr;
QElapsedTimer load_timer;
// Cached on first SceneLoader::modelGeoref(mid) call once the
// Cached on first SceneLoader::modelGeoref(session_model_id) call once the
// streamer has its IFC file loaded.
ModelGeoref georef;
bool has_georef = false;
@@ -158,21 +158,20 @@ private:
};
void startNextLoad();
void startStreamLoadFor(uint32_t mid);
void loadFromGeometryStreamer(uint32_t session_model_id);
void connectStreamer(GeometryStreamer* streamer);
void joinSidecarThread();
void joinDataSourceThreads();
void applySidecarData(uint32_t mid, StreamingSidecar metadata);
void startDataSourceLoad(uint32_t mid);
void applySidecarData(uint32_t session_model_id, StreamingSidecar metadata);
void startDataSourceLoad(uint32_t session_model_id);
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;
uint32_t next_object_id_ = 1;
uint32_t loading_model_id_ = 0;
uint32_t next_session_model_id_ = 1;
uint32_t loading_session_model_id_ = 0;
std::thread sidecar_read_thread_;
// One thread per sidecar-hit model while its .rdb/.ifc opens in the
// background. Joined only at destruction so a slow SPF parse on model
+6 -6
View File
@@ -313,16 +313,16 @@ void SectionGizmoRenderer::encode(WGPURenderPassEncoder pass, const Eigen::Matri
const float vh = float(viewport_h_px);
const int n = std::min<int>(int(planes.size()), kMaxPlanes);
for (int i = 0; i < n; ++i) {
const SectionPlane& p = planes[i];
const SectionPlane& plane = planes[i];
Eigen::Vector3f nn, tangent, bitangent;
planeBasis(p.n, nn, tangent, bitangent);
planeBasis(plane.n, nn, tangent, bitangent);
// Fixed 1 m gizmo (matches the desktop OverlayRenderer / GL constant).
// NOT visual_radius: the normal is flipped toward the camera, so a large
// arrow would shoot past the eye (clip.w<0) and vanish.
const float half = 1.0f;
uint8_t slot[256];
packSectionUniform(slot, view_proj, p.origin, half, tangent, line_w,
packSectionUniform(slot, view_proj, plane.origin, half, tangent, line_w,
bitangent, nn, 1.0f, 1.0f, 1.0f, 1.0f, vw, vh);
const uint32_t slot_offset = uint32_t(i) * kSectionUniformSlot;
wgpuQueueWriteBuffer(queue_, uniform_buffer_, slot_offset, slot, sizeof(slot));
@@ -340,12 +340,12 @@ int SectionGizmoRenderer::hitTest(int x, int y, const std::vector<SectionPlane>&
float best_d = tolerance_px;
const int n = std::min<int>(int(planes.size()), kMaxPlanes);
for (int i = 0; i < n; ++i) {
const SectionPlane& p = planes[i];
const SectionPlane& plane = planes[i];
// The arrow runs origin → origin + n * 1 m (visual radius scales the
// gizmo, but hit-test the unit arrow to mirror the desktop).
Eigen::Vector2f s_origin, s_tip;
if (!projectWorldToLogicalScreen(vp, p.origin, viewport_w_px, viewport_h_px, s_origin)) continue;
if (!projectWorldToLogicalScreen(vp, p.origin + p.n * 1.0f, viewport_w_px, viewport_h_px, s_tip)) continue;
if (!projectWorldToLogicalScreen(vp, plane.origin, viewport_w_px, viewport_h_px, s_origin)) continue;
if (!projectWorldToLogicalScreen(vp, plane.origin + plane.n * 1.0f, viewport_w_px, viewport_h_px, s_tip)) continue;
const Eigen::Vector2f ab = s_tip - s_origin;
const float ab_len2 = ab.squaredNorm();
if (ab_len2 < 1e-3f) continue;
+3 -4
View File
@@ -103,7 +103,7 @@ void SidecarBuilder::onInstanceReady(const StreamedInstance& instance_record) {
instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = instance_record.color_override_rgba8;
instance.model_id = instance_record.model_id;
instance.session_model_id = instance_record.session_model_id;
// The streamer's instance transform is the double-precision
// placement_transformation. The cached float transform/world_aabb is only
@@ -142,7 +142,7 @@ SidecarData SidecarBuilder::finalize(const ModelGeoref& georef,
for (const auto& info : elements) {
ElementTableRecord packed;
packed.object_id = info.object_id;
packed.model_id = info.model_id;
packed.session_model_id = info.session_model_id;
packed.ifc_id = info.ifc_id;
packed.guid_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
@@ -191,8 +191,7 @@ bool SidecarBuilder::build(const QString& ifc_path,
});
streamer.loadFile(ifc_path.toStdString(),
/*start_object_id*/ 1,
/*model_id*/ 1,
/*session_model_id*/ 1,
num_threads);
loop.exec();
+1 -1
View File
@@ -254,7 +254,7 @@ struct BufReader {
} // namespace
// Full read: reconstruct the whole SidecarData (test/tooling path — the runtime
// streams via readSidecarMetadataOnly + per-chunk loads and never calls this).
// streams via readSidecarMetadata + per-chunk loads and never calls this).
// Decompresses the metadata blocks, then scatters each chunk's decompressed
// geometry back into the whole-model vertex/index arrays using the mesh offsets.
std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
+1 -1
View File
@@ -115,7 +115,7 @@ struct SidecarChunk {
// into a separate string table.
struct ElementTableRecord {
uint32_t object_id;
uint32_t model_id;
uint32_t session_model_id;
int32_t ifc_id;
uint32_t guid_offset;
uint32_t guid_length;
+1 -1
View File
@@ -125,7 +125,7 @@ bool parseSidecarElementMetadata(const uint8_t* data, size_t n, SidecarData& out
return true;
}
std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_path) {
std::optional<StreamingSidecar> readSidecarMetadata(const std::string& ifc_path) {
const std::string path = sidecarPath(ifc_path);
FILE* f = std::fopen(path.c_str(), "rb");
if (!f) return std::nullopt;
+1 -1
View File
@@ -69,7 +69,7 @@ struct StreamingSidecar {
// Read just the metadata + section offsets. Returns nullopt on any I/O or
// version error (same failure modes as readSidecar). The file is closed
// before return — callers re-open for per-chunk reads.
std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_path);
std::optional<StreamingSidecar> readSidecarMetadata(const std::string& ifc_path);
// Read + decompress one chunk's geometry (v16) from disk: the vertex zstd frame
// at [geometry_section_offset + v_comp_off, +v_comp_size) → out_vbytes (v_raw
+1 -1
View File
@@ -95,7 +95,7 @@ void StreamingThread::workerLoop() {
// thread — they cross back to the main thread when the result
// is drained and applied (pool.alloc + queueWriteBuffer).
Result result;
result.model_id = req.model_id;
result.session_model_id = req.session_model_id;
result.chunk_idx = req.chunk_idx;
result.success = readChunkGeometryCompressed(
req.file_path, req.geometry_section_offset,
+2 -2
View File
@@ -44,7 +44,7 @@
class StreamingThread {
public:
struct Request {
uint32_t model_id;
uint32_t session_model_id;
std::size_t chunk_idx;
std::string file_path;
// v16: the chunk's two zstd frames in the geometry section. The reader
@@ -56,7 +56,7 @@ public:
};
struct Result {
uint32_t model_id;
uint32_t session_model_id;
std::size_t chunk_idx;
bool success;
std::vector<uint8_t> vbytes;
+129 -124
View File
@@ -102,8 +102,8 @@ void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool) {
// ---- Scene mutators -------------------------------------------------------
void ViewportCore::removeModel(uint32_t model_id) {
auto it = models_gpu_.find(model_id);
void ViewportCore::removeModel(uint32_t session_model_id) {
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
releaseWgpuModelGpuData(it->second, pool_);
models_gpu_.erase(it);
@@ -111,7 +111,7 @@ void ViewportCore::removeModel(uint32_t model_id) {
}
void ViewportCore::resetScene() {
for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
for (auto& [session_model_id, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
models_gpu_.clear();
// A fresh scene should auto-frame its first model. Without this the flag
// stays set from the previous scene (on web, the embedded sample sets it at
@@ -121,15 +121,15 @@ void ViewportCore::resetScene() {
host_->requestFrame();
}
void ViewportCore::hideModel(uint32_t model_id) {
auto it = models_gpu_.find(model_id);
void ViewportCore::hideModel(uint32_t session_model_id) {
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end() || it->second.hidden) return;
it->second.hidden = true;
host_->requestFrame();
}
void ViewportCore::showModel(uint32_t model_id) {
auto it = models_gpu_.find(model_id);
void ViewportCore::showModel(uint32_t session_model_id) {
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end() || !it->second.hidden) return;
it->second.hidden = false;
host_->requestFrame();
@@ -141,22 +141,22 @@ void ViewportCore::setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters)
for (auto& kv : models_gpu_) recomposeAndUploadModel(kv.first);
}
void ViewportCore::setModelCoordinateOperation(uint32_t model_id,
void ViewportCore::setModelCoordinateOperation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters) {
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
if (it->second.coordinate_operation_meters == matrix_meters) return;
it->second.coordinate_operation_meters = matrix_meters;
recomposeAndUploadModel(model_id);
recomposeAndUploadModel(session_model_id);
}
void ViewportCore::setModelTransformation(uint32_t model_id,
void ViewportCore::setModelTransformation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters) {
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
if (it->second.model_transformation_meters == matrix_meters) return;
it->second.model_transformation_meters = matrix_meters;
recomposeAndUploadModel(model_id);
recomposeAndUploadModel(session_model_id);
}
// ---- Camera math ----------------------------------------------------------
@@ -199,7 +199,7 @@ bool ViewportCore::computeSceneAabb(float mn[3], float mx[3]) const {
mn[i] = std::numeric_limits<float>::infinity();
mx[i] = -std::numeric_limits<float>::infinity();
}
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& inst : m.instances) {
for (int i = 0; i < 3; ++i) {
@@ -265,9 +265,9 @@ float ViewportCore::chunkScreenAreaPx(const ModelGpuData::Chunk& c,
return (xmax - xmin) * (ymax - ymin);
}
void ViewportCore::recomposeAndUploadModel(uint32_t model_id) {
void ViewportCore::recomposeAndUploadModel(uint32_t session_model_id) {
if (!wgpu_initialized_) return;
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
ModelGpuData& m = it->second;
if (m.instances.empty() || m.instance_storage == nullptr) return;
@@ -316,9 +316,14 @@ bool ViewportCore::findInstance(uint32_t object_id,
return InstanceCompose::findInstanceInModels(object_id, models_gpu_, out);
}
bool ViewportCore::firstGeometryPointWorldM(uint32_t model_id,
uint32_t ViewportCore::modelObjectIdBase(uint32_t session_model_id) const {
auto it = models_gpu_.find(session_model_id);
return it == models_gpu_.end() ? 0u : it->second.object_id_base;
}
bool ViewportCore::firstGeometryPointWorldM(uint32_t session_model_id,
Eigen::Vector3d& out) const {
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return false;
const ModelGpuData& m = it->second;
if (m.instances.empty()) return false;
@@ -636,7 +641,7 @@ bool ViewportCore::computeObjectAabb(uint32_t object_id,
mn[i] = std::numeric_limits<float>::infinity();
mx[i] = -std::numeric_limits<float>::infinity();
}
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
for (const auto& inst : m.instances) {
if (inst.object_id != object_id) continue;
for (int i = 0; i < 3; ++i) {
@@ -678,7 +683,7 @@ double ViewportCore::volumeOfObjects(
if (object_ids.empty()) return 0.0;
double total = 0.0;
for (uint32_t oid : object_ids) {
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
auto it = m.object_id_to_instance.find(oid);
if (it == m.object_id_to_instance.end()) continue;
const InstanceInfo& inst = m.instances[it->second];
@@ -699,7 +704,7 @@ ViewportCore::volumesPerObject(
if (object_ids.empty()) return out;
out.reserve(object_ids.size());
for (uint32_t oid : object_ids) {
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
auto it = m.object_id_to_instance.find(oid);
if (it == m.object_id_to_instance.end()) continue;
const InstanceInfo& inst = m.instances[it->second];
@@ -842,11 +847,11 @@ fn find_draw(vid: u32) -> u32 {
var lo: u32 = 0u;
var hi: u32 = u_model.draw_count;
while (lo + 1u < hi) {
let mid = (lo + hi) >> 1u;
if (prefix_sums[mid] <= vid) {
lo = mid;
let session_model_id = (lo + hi) >> 1u;
if (prefix_sums[session_model_id] <= vid) {
lo = session_model_id;
} else {
hi = mid;
hi = session_model_id;
}
}
return lo;
@@ -1784,7 +1789,7 @@ void ViewportCore::shutdown() {
// we've torn down model state. Worker drains its queue then joins.
streaming_thread_.stop();
for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
for (auto& [session_model_id, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
models_gpu_.clear();
if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; }
@@ -2035,10 +2040,10 @@ bool ViewportCore::applyStreamedChunk(
StreamingThread::Request ViewportCore::makeChunkRequest(
const ModelGpuData& m, std::size_t chunk_idx,
std::uint32_t model_id) {
std::uint32_t session_model_id) {
const auto& c = m.chunks[chunk_idx];
StreamingThread::Request req;
req.model_id = model_id;
req.session_model_id = session_model_id;
req.chunk_idx = chunk_idx;
req.file_path = m.streaming_file_path;
// v16: one compressed vertex frame + one compressed index frame per chunk.
@@ -2130,7 +2135,7 @@ void ViewportCore::driveStreamingLoads() {
// the chunk has *actually* contributed pixels (post-HiZ) over the
// last ~30 frames.
constexpr float HISTORY_ALPHA = 1.0f / 30.0f;
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (auto& c : m.chunks) {
if (c.is_resident && c.frustum_visible_count > 0) {
@@ -2199,7 +2204,7 @@ void ViewportCore::driveStreamingLoads() {
ModelGpuData* victim_m = nullptr;
std::size_t victim_ci = 0;
std::uint64_t victim_lru = std::numeric_limits<std::uint64_t>::max();
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) {
auto& c = m.chunks[ci];
if (!c.is_resident) continue;
@@ -2233,7 +2238,7 @@ void ViewportCore::driveStreamingLoads() {
ModelGpuData* victim_m = nullptr;
std::size_t victim_ci = 0;
float victim_priority = threshold;
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) {
auto& c = m.chunks[ci];
if (!c.is_resident) continue;
@@ -2256,7 +2261,7 @@ void ViewportCore::driveStreamingLoads() {
// 2-cycle detection: this victim was previously evicted by
// THIS exact candidate — the smoking gun for a swap loop.
const bool is_2_cycle =
victim.last_evicted_by_model_id == cand_mid
victim.last_evicted_by_session_model_id == cand_mid
&& victim.last_evicted_by_chunk_idx == cand_ci
&& victim.load_count > 1;
Log::info()
@@ -2271,7 +2276,7 @@ void ViewportCore::driveStreamingLoads() {
<< ", threshold=" << int(threshold) << ")";
}
victim.last_evicted_by_model_id = cand_mid;
victim.last_evicted_by_session_model_id = cand_mid;
victim.last_evicted_by_chunk_idx = cand_ci;
victim.last_evicted_by_priority = cand_priority;
victim.last_evicted_frame_idx = streaming_frame_idx_;
@@ -2287,7 +2292,7 @@ void ViewportCore::driveStreamingLoads() {
{
auto results = streaming_thread_.drainResults();
for (auto& res : results) {
auto it = models_gpu_.find(res.model_id);
auto it = models_gpu_.find(res.session_model_id);
if (it == models_gpu_.end()) continue; // model unloaded
auto& m = it->second;
if (res.chunk_idx >= m.chunks.size()) continue;
@@ -2295,7 +2300,7 @@ void ViewportCore::driveStreamingLoads() {
c.is_loading = false;
if (!res.success) {
Log::warn() << "[wgpu stream] worker read failed for model "
<< res.model_id << " chunk " << res.chunk_idx;
<< res.session_model_id << " chunk " << res.chunk_idx;
continue;
}
if (!applyStreamedChunk(m, res.chunk_idx, res.vbytes, res.idx)) {
@@ -2330,12 +2335,12 @@ void ViewportCore::driveStreamingLoads() {
struct Candidate {
ModelGpuData* m;
std::size_t ci;
std::uint32_t mid;
std::uint32_t session_model_id;
float priority;
};
std::vector<Candidate> candidates;
candidates.reserve(64);
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
if (m.streaming_file_path.empty() || m.hidden) continue;
for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) {
auto& c = m.chunks[ci];
@@ -2348,7 +2353,7 @@ void ViewportCore::driveStreamingLoads() {
// what's resolvable now; the rest stream in as you approach.
if (c.contribution_visible_count == 0) continue;
if (c.blocked_cooldown_until_frame_idx > streaming_frame_idx_) continue;
candidates.push_back({&m, ci, mid, candidate_priority(c)});
candidates.push_back({&m, ci, session_model_id, candidate_priority(c)});
}
}
streaming_candidates_this_frame_ = int(candidates.size());
@@ -2394,7 +2399,7 @@ void ViewportCore::driveStreamingLoads() {
// once it exceeds the memory budget (highest-contribution chunks win).
while (pool_.total_free_bytes() < streaming_web_inflight_bytes_ + need) {
if (evict_one_lru()) continue;
if (evict_lowest_priority_than(cand.mid, std::uint32_t(cand.ci),
if (evict_lowest_priority_than(cand.session_model_id, std::uint32_t(cand.ci),
cand.priority)) continue;
break;
}
@@ -2412,7 +2417,7 @@ void ViewportCore::driveStreamingLoads() {
&& !pool_can_fit(c.index_count * sizeof(std::uint32_t)))
|| pool_.total_free_bytes() < need) {
if (evict_one_lru()) continue;
if (evict_lowest_priority_than(cand.mid, std::uint32_t(cand.ci),
if (evict_lowest_priority_than(cand.session_model_id, std::uint32_t(cand.ci),
cand.priority)) continue;
break;
}
@@ -2474,7 +2479,7 @@ void ViewportCore::driveStreamingLoads() {
c.is_loading = true;
c.last_visible_frame_idx = streaming_frame_idx_;
++streaming_web_inflight_count_;
beginWebChunkLoad(cand.mid, cand.ci);
beginWebChunkLoad(cand.session_model_id, cand.ci);
++enqueued;
continue;
}
@@ -2490,7 +2495,7 @@ void ViewportCore::driveStreamingLoads() {
continue;
}
if (streaming_thread_.enqueue(makeChunkRequest(*cand.m, cand.ci, cand.mid))) {
if (streaming_thread_.enqueue(makeChunkRequest(*cand.m, cand.ci, cand.session_model_id))) {
c.is_loading = true;
++enqueued;
}
@@ -2507,7 +2512,7 @@ void ViewportCore::driveStreamingLoads() {
// next few frames so an on-demand render loop doesn't stall before the
// geometry actually appears. Bounded, so the loop still quiesces at idle.
bool visible_pending = false;
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.streaming_file_path.empty() || m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.is_resident && (c.frustum_visible_count > 0 || c.is_loading)) {
@@ -2578,7 +2583,7 @@ void ViewportCore::driveStreamingLoads() {
<< " ev_pri=" << streaming_evictions_pri_this_frame_
<< " blocked=" << streaming_blocked_oom_this_frame_;
struct Stat { std::uint32_t mid; std::size_t ci; float area; };
struct Stat { std::uint32_t session_model_id; std::size_t ci; float area; };
std::vector<Stat> all;
all.reserve(64);
for (const auto& [mid2, m2] : models_gpu_) {
@@ -2594,7 +2599,7 @@ void ViewportCore::driveStreamingLoads() {
const std::size_t n = std::min<std::size_t>(5, all.size());
for (std::size_t i = 0; i < n; ++i) {
Log::info()
<< " top cand #" << i << ": model " << all[i].mid
<< " top cand #" << i << ": model " << all[i].session_model_id
<< " chunk " << all[i].ci
<< " area=" << int(all[i].area) << "px2";
}
@@ -2607,7 +2612,7 @@ void ViewportCore::driveStreamingLoads() {
std::size_t resident = 0;
std::uint32_t max_load_count = 0;
std::size_t cycled = 0;
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
for (const auto& c : m.chunks) {
if (c.is_resident) ++resident;
if (c.load_count > max_load_count) max_load_count = c.load_count;
@@ -2897,11 +2902,11 @@ WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue,
// Holds a unique_ptr so address stability is preserved as the map grows.
SidecarData& getOrCreateDirectStaging(
std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>>& staging,
std::uint32_t model_id) {
auto it = staging.find(model_id);
std::uint32_t session_model_id) {
auto it = staging.find(session_model_id);
if (it == staging.end()) {
auto [it_new, _] = staging.emplace(
model_id, std::make_unique<SidecarData>());
session_model_id, std::make_unique<SidecarData>());
return *it_new->second;
}
return *it->second;
@@ -2909,7 +2914,7 @@ SidecarData& getOrCreateDirectStaging(
} // namespace
void ViewportCore::applyCachedModel(std::uint32_t model_id,
void ViewportCore::applyCachedModel(std::uint32_t session_model_id,
StreamingSidecar metadata) {
if (!device_ || !queue_) {
Log::warn() << "applyCachedModel without an initialised device";
@@ -2917,7 +2922,7 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
}
// Replace any existing state for this id.
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it != models_gpu_.end()) {
releaseWgpuModelGpuData(it->second, pool_);
models_gpu_.erase(it);
@@ -3203,11 +3208,11 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
}
}
auto [inserted, _] = models_gpu_.emplace(model_id, std::move(model_gpu_data));
auto [inserted, _] = models_gpu_.emplace(session_model_id, std::move(model_gpu_data));
ModelGpuData& inserted_model = inserted->second;
Log::info()
<< "[wgpu stream] applyCachedModel mid=" << model_id
<< "[wgpu stream] applyCachedModel session_model_id=" << session_model_id
<< " verts=" << inserted_model.vertex_bytes << "B (deferred)"
<< " idx=" << inserted_model.index_count
<< " meshes=" << inserted_model.mesh_count
@@ -3224,7 +3229,7 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
if (mesh.vertices.empty() || mesh.indices.empty()) return;
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, mesh.model_id);
SidecarData& staging = getOrCreateDirectStaging(pending_direct_loads_, mesh.session_model_id);
// Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float).
// Same quantisation as SidecarBuilder::onMeshReady so direct-load and
@@ -3250,17 +3255,17 @@ void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
extent_recip[a] = ext > 0.0f ? 1.0f / ext : 0.0f;
}
const std::size_t vb_offset = s.vertices.size();
s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
const std::size_t vb_offset = staging.vertices.size();
staging.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
for (std::size_t i = 0; i < n_verts; ++i) {
quantizeVertex(mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
bmin, extent_recip,
s.vertices.data() + vb_offset
staging.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
}
const std::size_t ib_offset = s.indices.size();
s.indices.insert(s.indices.end(),
const std::size_t ib_offset = staging.indices.size();
staging.indices.insert(staging.indices.end(),
mesh.indices.begin(), mesh.indices.end());
MeshInfo info{};
@@ -3277,20 +3282,20 @@ void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
info.lod1_ebo_byte_offset = 0;
info.lod1_index_count = 0;
if (s.meshes.size() <= mesh.local_mesh_id) {
s.meshes.resize(mesh.local_mesh_id + 1);
if (staging.meshes.size() <= mesh.local_mesh_id) {
staging.meshes.resize(mesh.local_mesh_id + 1);
}
s.meshes[mesh.local_mesh_id] = info;
staging.meshes[mesh.local_mesh_id] = info;
}
void ViewportCore::uploadStreamedInstance(const StreamedInstance& instance_record) {
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, instance_record.model_id);
SidecarData& staging = getOrCreateDirectStaging(pending_direct_loads_, instance_record.session_model_id);
InstanceInfo instance{};
instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = instance_record.color_override_rgba8;
instance.model_id = instance_record.model_id;
instance.session_model_id = instance_record.session_model_id;
std::memcpy(instance.placement_transformation, instance_record.transform,
sizeof(instance.placement_transformation));
for (int i = 0; i < 16; ++i) {
@@ -3299,7 +3304,7 @@ void ViewportCore::uploadStreamedInstance(const StreamedInstance& instance_recor
std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
s.instances.push_back(instance);
staging.instances.push_back(instance);
}
std::uint32_t ViewportCore::loadSidecarFromPath(const std::string& path) {
@@ -3307,14 +3312,14 @@ std::uint32_t ViewportCore::loadSidecarFromPath(const std::string& path) {
Log::warn() << "loadSidecarFromPath: wgpu not initialised";
return 0;
}
auto meta_opt = readSidecarMetadataOnly(path);
auto meta_opt = readSidecarMetadata(path);
if (!meta_opt) {
Log::warn() << "loadSidecarFromPath: could not read sidecar metadata from " << path;
return 0;
}
const std::uint32_t mid = next_model_id_++;
applyCachedModel(mid, std::move(*meta_opt));
return mid;
const std::uint32_t session_model_id = next_session_model_id_++;
applyCachedModel(session_model_id, std::move(*meta_opt));
return session_model_id;
}
#if defined(__EMSCRIPTEN__)
@@ -3407,9 +3412,9 @@ void webIssueCurrentPlan(int id) {
if (done) done(true, std::move(out));
return;
}
const SidecarReadPlan& p = r.plans[r.plan_idx];
r.scratch.assign(std::size_t(p.read_size), 0);
ifcvReadRangeInto(r.source_id, id, double(p.file_offset), double(p.read_size),
const SidecarReadPlan& plan = r.plans[r.plan_idx];
r.scratch.assign(std::size_t(plan.read_size), 0);
ifcvReadRangeInto(r.source_id, id, double(plan.file_offset), double(plan.read_size),
r.scratch.data());
}
@@ -3456,8 +3461,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_on_range_done(int reqId, int ok) {
if (done) done(false, {});
return;
}
const SidecarReadPlan& p = r.plans[r.plan_idx];
for (const auto& s : p.slices) {
const SidecarReadPlan& plan = r.plans[r.plan_idx];
for (const auto& s : plan.slices) {
std::memcpy(r.out.data() + s.dst_offset,
r.scratch.data() + s.src_offset, std::size_t(s.bytes));
}
@@ -3465,8 +3470,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_on_range_done(int reqId, int ok) {
webIssueCurrentPlan(reqId);
}
void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_idx) {
auto it = models_gpu_.find(model_id);
void ViewportCore::beginWebChunkLoad(std::uint32_t session_model_id, std::size_t chunk_idx) {
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
ModelGpuData& m = it->second;
if (chunk_idx >= m.chunks.size()) return;
@@ -3493,13 +3498,13 @@ void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_i
};
auto join = std::make_shared<ChunkJoin>();
std::function<void()> finish =
[this, model_id, chunk_idx, need, v_raw, i_raw, join]() {
[this, session_model_id, chunk_idx, need, v_raw, i_raw, join]() {
if (!join->v_done || !join->i_done) return; // wait for the other frame
streaming_web_inflight_bytes_ -= std::min(streaming_web_inflight_bytes_, need);
if (streaming_web_inflight_count_ > 0) --streaming_web_inflight_count_;
host_->requestFrame();
auto mit = models_gpu_.find(model_id);
auto mit = models_gpu_.find(session_model_id);
if (mit == models_gpu_.end()) return;
ModelGpuData& mm = mit->second;
if (chunk_idx >= mm.chunks.size()) return;
@@ -3608,15 +3613,15 @@ void ViewportCore::loadSidecarMetadataWeb(int source_id, std::string source_labe
}
const std::size_t n_meshes = sc.meta.meshes.size();
const std::size_t n_instances = sc.meta.instances.size();
const std::uint32_t mid = next_model_id_++;
applyCachedModel(mid, std::move(sc));
const std::uint32_t session_model_id = next_session_model_id_++;
applyCachedModel(session_model_id, std::move(sc));
// Mark web-streamed + set the source IMMEDIATELY — the
// model now has non-resident chunks and the RAF loop's
// driveStreamingLoads will run before the element metadata header
// read below returns. If streaming_from_web weren't set
// yet it would take the sync fopen path and fail
// ("failed to read/decompress chunk 0").
if (auto m0 = models_gpu_.find(mid); m0 != models_gpu_.end()) {
if (auto m0 = models_gpu_.find(session_model_id); m0 != models_gpu_.end()) {
m0->second.streaming_from_web = true;
m0->second.web_source_id = source_id;
}
@@ -3625,10 +3630,10 @@ void ViewportCore::loadSidecarMetadataWeb(int source_id, std::string source_labe
const std::uint64_t element_metadata_hdr_off =
geometry_metadata_off + geometry_metadata_comp;
webReadRangesAsync(source_id, 0, {{element_metadata_hdr_off, 16}},
[this, mid, element_metadata_hdr_off, source_id, source_label,
[this, session_model_id, element_metadata_hdr_off, source_id, source_label,
n_meshes, n_instances]
(bool ok4, std::vector<std::uint8_t>&& dh) {
auto mit = models_gpu_.find(mid);
auto mit = models_gpu_.find(session_model_id);
if (mit != models_gpu_.end()) {
if (ok4 && dh.size() >= 16) {
std::uint64_t dc = 0, dr = 0;
@@ -3646,7 +3651,7 @@ void ViewportCore::loadSidecarMetadataWeb(int source_id, std::string source_labe
// as each federated model streams in.
host_->requestFrame();
Log::info() << "ifcviewer-web: loaded sidecar (" << source_label
<< ", id " << mid << ", " << n_meshes << " meshes, "
<< ", id " << session_model_id << ", " << n_meshes << " meshes, "
<< n_instances << " instances)";
});
});
@@ -3654,14 +3659,14 @@ void ViewportCore::loadSidecarMetadataWeb(int source_id, std::string source_labe
});
}
void ViewportCore::loadElementMetadataWeb(std::uint32_t model_id,
void ViewportCore::loadElementMetadataWeb(std::uint32_t session_model_id,
std::function<void(bool)> done) {
// On-demand fetch of the v15 element metadata block (elements + string table)
// for a web-streamed model — the property data a UI needs (selected-
// object name, search) but rendering doesn't. Fetches at most once. Reads
// from the model's own registered byte-source, so it works per-model even
// with several federated files loaded.
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) { if (done) done(false); return; }
ModelGpuData& m = it->second;
if (m.element_metadata_loaded || m.element_metadata_comp_size == 0) {
@@ -3672,8 +3677,8 @@ void ViewportCore::loadElementMetadataWeb(std::uint32_t model_id,
const std::uint64_t raw_size = m.element_metadata_raw_size;
webReadRangesAsync(m.web_source_id, 0,
{{m.element_metadata_comp_offset, m.element_metadata_comp_size}},
[this, model_id, raw_size, done](bool ok, std::vector<std::uint8_t>&& cz) {
auto mit = models_gpu_.find(model_id);
[this, session_model_id, raw_size, done](bool ok, std::vector<std::uint8_t>&& cz) {
auto mit = models_gpu_.find(session_model_id);
if (mit == models_gpu_.end()) { if (done) done(false); return; }
std::vector<std::uint8_t> buf(static_cast<std::size_t>(raw_size));
SidecarData tmp;
@@ -3700,13 +3705,13 @@ void ViewportCore::loadElementMetadataWeb(std::uint32_t model_id,
void ViewportCore::logSelectedObjectGuidWeb(std::uint32_t object_id) {
InstanceCompose::InstanceLookup lk;
if (!findInstance(object_id, lk)) return; // empty pick / unknown id
const std::uint32_t model_id = lk.model_id;
loadElementMetadataWeb(model_id, [this, object_id, model_id](bool ok) {
const std::uint32_t session_model_id = lk.session_model_id;
loadElementMetadataWeb(session_model_id, [this, object_id, session_model_id](bool ok) {
if (!ok) {
Log::warn() << "pick: element metadata fetch failed for object " << object_id;
return;
}
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
const ModelGpuData& m = it->second;
for (const auto& e : m.elements) {
@@ -3727,7 +3732,7 @@ void ViewportCore::logSelectedObjectGuidWeb(std::uint32_t object_id) {
void ViewportCore::streamingProgress(int& resident_chunks, int& total_chunks) const {
resident_chunks = 0;
total_chunks = 0;
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
for (const auto& c : m.chunks) {
++total_chunks;
if (c.is_resident) ++resident_chunks;
@@ -3744,11 +3749,11 @@ void ViewportCore::streamingModelProgress(int idx, int& resident_chunks,
resident_chunks = 0;
total_chunks = 0;
if (idx < 0 || idx >= int(models_gpu_.size())) return;
// Order by model_id (= load order) so a model keeps the same UI slot as it
// Order by session_model_id (= load order) so a model keeps the same UI slot as it
// streams, instead of hopping with unordered_map iteration order.
std::vector<std::uint32_t> ids;
ids.reserve(models_gpu_.size());
for (const auto& [mid, m] : models_gpu_) ids.push_back(mid);
for (const auto& [session_model_id, m] : models_gpu_) ids.push_back(session_model_id);
std::sort(ids.begin(), ids.end());
auto it = models_gpu_.find(ids[std::size_t(idx)]);
if (it == models_gpu_.end()) return;
@@ -3767,7 +3772,7 @@ void ViewportCore::streamingByteProgress(std::uint64_t& total_bytes,
// So loaded/needed = how done this view is; needed/total = how much of the
// whole model this view even requires.
total_bytes = needed_bytes = loaded_bytes = 0;
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& c : m.chunks) {
// Report COMPRESSED bytes — what actually crosses the network. Fall
@@ -3784,11 +3789,11 @@ void ViewportCore::streamingByteProgress(std::uint64_t& total_bytes,
}
}
void ViewportCore::finalizeModel(std::uint32_t model_id) {
auto it = pending_direct_loads_.find(model_id);
void ViewportCore::finalizeModel(std::uint32_t session_model_id) {
auto it = pending_direct_loads_.find(session_model_id);
if (it == pending_direct_loads_.end()) {
Log::warn()
<< "[wgpu direct] finalizeModel(" << model_id
<< "[wgpu direct] finalizeModel(" << session_model_id
<< ") with no staged data; skipping";
return;
}
@@ -3801,7 +3806,7 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) {
return;
}
if (sidecar_data.meshes.empty() || sidecar_data.instances.empty()) {
Log::info() << "[wgpu direct] finalizeModel(" << model_id
Log::info() << "[wgpu direct] finalizeModel(" << session_model_id
<< "): empty staging (meshes=" << sidecar_data.meshes.size()
<< " instances=" << sidecar_data.instances.size() << ")";
return;
@@ -3822,12 +3827,12 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) {
std::vector<std::uint8_t> raw_vertices = std::move(metadata.meta.vertices);
std::vector<std::uint32_t> raw_indices = std::move(metadata.meta.indices);
applyCachedModel(model_id, std::move(metadata));
applyCachedModel(session_model_id, std::move(metadata));
auto model_it = models_gpu_.find(model_id);
auto model_it = models_gpu_.find(session_model_id);
if (model_it == models_gpu_.end()) {
Log::warn()
<< "[wgpu direct] finalizeModel(" << model_id
<< "[wgpu direct] finalizeModel(" << session_model_id
<< "): applyCachedModel produced no model entry";
return;
}
@@ -3863,7 +3868,7 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) {
if (!applyStreamedChunk(model_gpu_data, chunk_index, vbytes, indices)) {
Log::warn()
<< "[wgpu direct] finalizeModel(" << model_id
<< "[wgpu direct] finalizeModel(" << session_model_id
<< "): applyStreamedChunk failed on chunk " << chunk_index
<< " (pool OOM?)";
continue;
@@ -3872,7 +3877,7 @@ void ViewportCore::finalizeModel(std::uint32_t model_id) {
}
Log::info()
<< "[wgpu direct] finalizeModel mid=" << model_id
<< "[wgpu direct] finalizeModel session_model_id=" << session_model_id
<< " meshes=" << model_gpu_data.meshes.size()
<< " instances=" << model_gpu_data.instances.size()
<< " chunks=" << chunks_uploaded << "/" << model_gpu_data.chunks.size()
@@ -4923,7 +4928,7 @@ void ViewportCore::encodePickReadbackToStaging(int x_pixels, int y_pixels,
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
wgpuRenderPassEncoderSetPipeline(pass, pick_pipeline_);
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.bind_group || c.total_visible_vertices == 0) continue;
@@ -5122,7 +5127,7 @@ void ViewportCore::isolateSelected() {
// objects stay model-hidden (element-level hiding on top is redundant), and
// object_id 0 (unpickable) is skipped.
const auto& sel_ids = selection_.selectionIds();
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const InstanceInfo& inst : m.instances) {
if (inst.object_id == 0) continue;
@@ -5273,7 +5278,7 @@ bool ViewportCore::encodeBoxPickToStaging(int& x, int& y, int& w, int& h,
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
wgpuRenderPassEncoderSetPipeline(pass, pick_pipeline_);
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.bind_group || c.total_visible_vertices == 0) continue;
@@ -5428,7 +5433,7 @@ bool ViewportCore::raycastSurfaceForObject(std::uint32_t object_id, int x_pixels
Eigen::Vector3f best_normal;
float best_radius = 0.0f;
bool found = false;
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& inst : m.instances) {
if (inst.object_id != object_id) continue;
@@ -5604,9 +5609,9 @@ bool ViewportCore::pickMeshLocalAt(int x, int y, MeshLocalPick& out) {
Eigen::Vector3f world_pos, world_normal;
if (!pickSurfaceAt(x, y, obj_id, world_pos, world_normal)) return false;
// Use the OUTER mid (the live map key) rather than inst.model_id —
// InstanceInfo::model_id is stale across sessions.
for (const auto& [mid, m] : models_gpu_) {
// Use the OUTER session_model_id (the live map key) rather than inst.session_model_id —
// InstanceInfo::session_model_id is stale across sessions.
for (const auto& [session_model_id, m] : models_gpu_) {
auto it = m.object_id_to_instance.find(obj_id);
if (it == m.object_id_to_instance.end()) continue;
const InstanceInfo& inst = m.instances[it->second];
@@ -5714,7 +5719,7 @@ bool ViewportCore::pickMeshLocalAt(int x, int y, MeshLocalPick& out) {
refined_world_pos.z(), 1.0f);
out.object_id = obj_id;
out.model_id = mid;
out.session_model_id = session_model_id;
out.mesh_id = inst.mesh_id;
out.mesh_local[0] = mp.x();
out.mesh_local[1] = mp.y();
@@ -5744,7 +5749,7 @@ bool ViewportCore::raycast(const float origin[3], const float dir[3],
std::uint32_t best_oid = 0;
float best_normal[3] = {0, 0, 0};
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (std::uint32_t inst_idx = 0; inst_idx < std::uint32_t(m.instances.size()); ++inst_idx) {
const InstanceInfo& inst = m.instances[inst_idx];
@@ -6275,10 +6280,10 @@ void ViewportCore::render() {
#endif
std::vector<std::pair<std::uint32_t, std::future<std::uint32_t>>> futures;
futures.reserve(models_gpu_.size());
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
auto& m_ref = m;
futures.emplace_back(mid, std::async(std::launch::async,
futures.emplace_back(session_model_id, std::async(std::launch::async,
[this, &m_ref, &planes, &eye_a, &fwd_a, &right_a, &up_a,
focal_px, effective_min_px, &hiz_occluded]() {
return cullModelCpuCompute(
@@ -6288,11 +6293,11 @@ void ViewportCore::render() {
hiz_occluded);
}));
}
for (auto& [mid, fut] : futures) {
for (auto& [session_model_id, fut] : futures) {
hiz_reject_count_ += fut.get();
}
} else {
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
hiz_reject_count_ += cullModelCpuCompute(
m, planes, eye_a, fwd_a, right_a, up_a, focal_px,
@@ -6304,7 +6309,7 @@ void ViewportCore::render() {
const double cull_compute_ms = double(cull_timer.nsecsElapsed()) / 1e6;
Stopwatch upload_timer;
upload_timer.start();
for (auto& [mid, m] : models_gpu_) {
for (auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
cullModelCpuUpload(m);
for (const auto& c : m.chunks) {
@@ -6377,7 +6382,7 @@ void ViewportCore::render() {
wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_);
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.bind_group || c.opaque_visible_vertices == 0) continue;
@@ -6388,7 +6393,7 @@ void ViewportCore::render() {
}
wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_transparent_);
for (const auto& [mid, m] : models_gpu_) {
for (const auto& [session_model_id, m] : models_gpu_) {
if (m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.bind_group) continue;
@@ -6477,7 +6482,7 @@ void ViewportCore::render() {
: 0.0;
std::uint32_t total_obj = 0, total_tri = 0, total_meshes = 0;
for (const auto& [mid, mm] : models_gpu_) {
for (const auto& [session_model_id, mm] : models_gpu_) {
total_obj += std::uint32_t(mm.instances.size());
total_tri += mm.index_count / 3;
total_meshes += std::uint32_t(mm.meshes.size());
@@ -6492,7 +6497,7 @@ void ViewportCore::render() {
stats.visible_triangles = last_visible_triangles_;
stats.unique_meshes = total_meshes;
std::uint32_t draw_calls = 0;
for (const auto& [mid, mm] : models_gpu_) {
for (const auto& [session_model_id, mm] : models_gpu_) {
if (mm.hidden) continue;
for (const auto& c : mm.chunks) {
if (c.is_resident && c.total_visible_draws > 0) ++draw_calls;
@@ -6534,7 +6539,7 @@ void ViewportCore::render() {
std::uint32_t total_instances = 0;
std::size_t chunks_total = 0, chunks_resident = 0;
std::size_t chunks_frustum_vis = 0, chunks_missing = 0;
for (const auto& [mid, mo] : models_gpu_) {
for (const auto& [session_model_id, mo] : models_gpu_) {
total_vbo += mo.vram_bytes_vbo;
total_ebo += mo.vram_bytes_ebo;
total_ssbo += mo.vram_bytes_ssbo;
@@ -6610,7 +6615,7 @@ void ViewportCore::render() {
if ((bench_count_ % 50) == 0) {
std::uint64_t total_vbo = 0, total_ebo = 0, total_ssbo = 0;
std::uint32_t total_instances = 0;
for (const auto& [mid, mo] : models_gpu_) {
for (const auto& [session_model_id, mo] : models_gpu_) {
total_vbo += mo.vram_bytes_vbo;
total_ebo += mo.vram_bytes_ebo;
total_ssbo += mo.vram_bytes_ssbo;
+27 -21
View File
@@ -123,9 +123,15 @@ public:
// A point that actually lies on the model's first instance — used
// by the federation false-origin guess on first geometry. Pure
// read of models_gpu_; no GPU touch.
bool firstGeometryPointWorldM(uint32_t model_id,
bool firstGeometryPointWorldM(uint32_t session_model_id,
Eigen::Vector3d& out) const;
// The global-id base applyCachedModel added to this model's instance
// object_ids. Callers that hold the element table separately (the desktop
// sidecar path) rebase their element records by the same base so registry
// ids match the ids pick/selection return. 0 if the model is unknown.
uint32_t modelObjectIdBase(uint32_t session_model_id) const;
// ---- Scene mutators -----------------------------------------------------
//
// All of these flip scene state (or post a recompose) and ask the
@@ -133,26 +139,26 @@ public:
// is responsible for coalescing those requests (Qt's requestUpdate
// does it natively; the web host wraps requestAnimationFrame).
void removeModel(uint32_t model_id);
void removeModel(uint32_t session_model_id);
void resetScene();
void hideModel(uint32_t model_id);
void showModel(uint32_t model_id);
void hideModel(uint32_t session_model_id);
void showModel(uint32_t session_model_id);
// Federation matrix setters. Each writes to model state and posts
// a recompose so per-instance world matrices stay consistent with
// the configured georef + transformation pipeline.
void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters);
void setModelCoordinateOperation(uint32_t model_id,
void setModelCoordinateOperation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters);
void setModelTransformation(uint32_t model_id,
void setModelTransformation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters);
// Walk every instance of `model_id`, recompose its transform from
// Walk every instance of `session_model_id`, recompose its transform from
// the current federation matrices, refresh per-chunk world AABBs,
// and re-upload InstanceGpu[] into m.instance_storage. No-op if
// the model is unknown, has no instances, or wgpu init hasn't
// completed.
void recomposeAndUploadModel(uint32_t model_id);
void recomposeAndUploadModel(uint32_t session_model_id);
// ---- Camera math --------------------------------------------------------
//
@@ -384,7 +390,7 @@ public:
// sidecar offsets. Pure function of model + chunk metadata; safe to
// call from the main thread.
static StreamingThread::Request makeChunkRequest(
const ModelGpuData& m, std::size_t chunk_idx, std::uint32_t model_id);
const ModelGpuData& m, std::size_t chunk_idx, std::uint32_t session_model_id);
// Per-frame streaming driver. Called from render() after cull. Walks
// every model's chunks once for residency bookkeeping, drains the
@@ -397,20 +403,20 @@ public:
// ---- Sidecar / direct load (#84-q) -----------------------------------
//
// Apply a parsed sidecar's metadata + planned chunk layout to
// models_gpu_[model_id]. Builds the per-chunk small buffers
// models_gpu_[session_model_id]. Builds the per-chunk small buffers
// (visible_draws / prefix_sums / per_chunk_uniform), the per-model
// mesh + instance storage SSBOs, and the spatial chunk plan; chunk
// vertex/index slices stay non-resident until the streaming loader
// brings them in. Triggers an auto-viewAll on the first model (so a
// freshly-loaded scene frames itself).
void applyCachedModel(std::uint32_t model_id, StreamingSidecar metadata);
void applyCachedModel(std::uint32_t session_model_id, StreamingSidecar metadata);
// Qt-free sidecar load: readSidecarMetadataOnly + applyCachedModel.
// Qt-free sidecar load: readSidecarMetadata + applyCachedModel.
// Used by the web build (and any other non-Qt embedder) so the
// public ViewportWindow::loadSidecar's QString + QFile triage
// tilde-expansion doesn't have to be replicated. Returns 0 on
// any failure (device not ready, file missing, magic / version
// mismatch) and the freshly-assigned model_id on success.
// mismatch) and the freshly-assigned session_model_id on success.
std::uint32_t loadSidecarFromPath(const std::string& path);
#if defined(__EMSCRIPTEN__)
@@ -431,7 +437,7 @@ public:
// / search) needs, fetched only when asked so first paint never waits on
// it. Populates ModelGpuData.elements/string_table; fires done(ok). At most
// one fetch per model.
void loadElementMetadataWeb(std::uint32_t model_id,
void loadElementMetadataWeb(std::uint32_t session_model_id,
std::function<void(bool)> done = {});
// Demo consumer of the element metadata fetch: on pick, ensure the owning model's
@@ -444,7 +450,7 @@ public:
// the active web source). applyStreamedChunk runs in the JS completion
// callback; c.is_loading is held until then. No-op if the model/chunk
// vanished mid-flight (e.g. a resetScene landed between issue and done).
void beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_idx);
void beginWebChunkLoad(std::uint32_t session_model_id, std::size_t chunk_idx);
#endif
// Streaming progress for a loading UI: resident vs total streaming chunks
@@ -454,7 +460,7 @@ public:
// Per-model progress for a federation loading UI. count() is how many
// models have metadata (are in the scene); progress(idx,…) gives the
// idx-th model's resident/total chunks, ordered by model_id (= load order)
// idx-th model's resident/total chunks, ordered by session_model_id (= load order)
// so each model keeps a stable UI slot as it streams.
int streamingModelCount() const;
void streamingModelProgress(int idx, int& resident_chunks,
@@ -474,7 +480,7 @@ public:
// ViewportCore so both halves can share it.
void uploadStreamedMesh(const StreamedMesh& mesh);
void uploadStreamedInstance(const StreamedInstance& instance_record);
void finalizeModel(std::uint32_t model_id);
void finalizeModel(std::uint32_t session_model_id);
// ---- Cross-chunk + screenshot capture (#84-v) -------------------------
//
@@ -774,7 +780,7 @@ public:
// round-trip from mesh-local back to world without re-deriving it.
struct MeshLocalPick {
std::uint32_t object_id = 0;
std::uint32_t model_id = 0;
std::uint32_t session_model_id = 0;
std::uint32_t mesh_id = 0;
float mesh_local [3] = {0, 0, 0};
float world_pos [3] = {0, 0, 0};
@@ -1082,9 +1088,9 @@ private:
// on subsequent frames.
StreamingThread streaming_thread_;
// Per-model GPU + CPU state, keyed by viewport-assigned model_id.
// Per-model GPU + CPU state, keyed by viewport-assigned session_model_id.
std::unordered_map<uint32_t, ModelGpuData> models_gpu_;
uint32_t next_model_id_ = 1;
uint32_t next_session_model_id_ = 1;
// Globally-unique object_id allocator. Each applyCachedModel rebases
// the sidecar's local object_ids by base_object_id_so_far so picks
// are unambiguous across models.
@@ -1163,7 +1169,7 @@ private:
std::string pending_screenshot_path_;
// Bonsai direct-load staging map. uploadStreamedMesh +
// uploadStreamedInstance append into entries keyed by model_id; the
// uploadStreamedInstance append into entries keyed by session_model_id; the
// finalizeModel call moves the entry out, hands it to
// applyCachedModel, and uploads the chunk slices synchronously.
std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>>
+62 -58
View File
@@ -215,7 +215,7 @@ ViewportWindow::ViewportWindow(QWindow* parent)
streaming_thread_(core_.streaming_thread_),
streaming_frame_idx_(core_.streaming_frame_idx_),
models_gpu_ (core_.models_gpu_),
next_model_id_ (core_.next_model_id_),
next_session_model_id_ (core_.next_session_model_id_),
next_object_id_ (core_.next_object_id_),
federated_false_origin_meters_(core_.federated_false_origin_meters_),
wgpu_initialized_(core_.wgpu_initialized_),
@@ -511,7 +511,7 @@ uint32_t ViewportWindow::loadSidecar(const std::string& path_std) {
// Metadata-only read: mesh dict + instance dict + georef. Per-chunk
// vertex/index bytes are deferred to the per-frame loader as chunks
// become frustum-visible.
auto meta_opt = readSidecarMetadataOnly(resolved.toStdString());
auto meta_opt = readSidecarMetadata(resolved.toStdString());
if (!meta_opt) {
// Triage: distinguish missing file from magic/version mismatch by
// peeking the header ourselves, so users know which to fix.
@@ -549,13 +549,13 @@ uint32_t ViewportWindow::loadSidecar(const std::string& path_std) {
return 0;
}
const uint32_t mid = next_model_id_++;
applyCachedModel(mid, std::move(*meta_opt));
return mid;
const uint32_t session_model_id = next_session_model_id_++;
applyCachedModel(session_model_id, std::move(*meta_opt));
return session_model_id;
}
void ViewportWindow::applyCachedModel(uint32_t model_id, StreamingSidecar metadata) {
core_.applyCachedModel(model_id, std::move(metadata));
void ViewportWindow::applyCachedModel(uint32_t session_model_id, StreamingSidecar metadata) {
core_.applyCachedModel(session_model_id, std::move(metadata));
}
// -----------------------------------------------------------------------------
@@ -573,7 +573,7 @@ void ViewportWindow::uploadStreamedInstance(const StreamedInstance& instance_rec
core_.uploadStreamedInstance(instance_record);
}
void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); }
void ViewportWindow::finalizeModel(uint32_t session_model_id) { core_.finalizeModel(session_model_id); }
// removeModel / resetScene / hideModel / showModel /
// setFederatedFalseOrigin / setModelCoordinateOperation /
@@ -581,41 +581,45 @@ void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(mode
// ViewportCore (#84-f). The public-API entry points below forward
// so existing bonsai-side callers don't have to change.
void ViewportWindow::removeModel(uint32_t model_id) { core_.removeModel(model_id); }
void ViewportWindow::removeModel(uint32_t session_model_id) { core_.removeModel(session_model_id); }
void ViewportWindow::resetScene() { core_.resetScene(); }
void ViewportWindow::hideModel(uint32_t model_id) { core_.hideModel(model_id); }
void ViewportWindow::showModel(uint32_t model_id) { core_.showModel(model_id); }
void ViewportWindow::hideModel(uint32_t session_model_id) { core_.hideModel(session_model_id); }
void ViewportWindow::showModel(uint32_t session_model_id) { core_.showModel(session_model_id); }
void ViewportWindow::setFederatedFalseOrigin(const Eigen::Matrix4d& m) {
core_.setFederatedFalseOrigin(m);
}
void ViewportWindow::setModelCoordinateOperation(uint32_t mid,
void ViewportWindow::setModelCoordinateOperation(uint32_t session_model_id,
const Eigen::Matrix4d& m) {
core_.setModelCoordinateOperation(mid, m);
core_.setModelCoordinateOperation(session_model_id, m);
}
void ViewportWindow::setModelTransformation(uint32_t mid,
void ViewportWindow::setModelTransformation(uint32_t session_model_id,
const Eigen::Matrix4d& m) {
core_.setModelTransformation(mid, m);
core_.setModelTransformation(session_model_id, m);
}
void ViewportWindow::recomposeAndUploadModel(uint32_t mid) {
core_.recomposeAndUploadModel(mid);
void ViewportWindow::recomposeAndUploadModel(uint32_t session_model_id) {
core_.recomposeAndUploadModel(session_model_id);
}
bool ViewportWindow::findInstance(uint32_t object_id, InstanceLookup& out) const {
return core_.findInstance(object_id, out);
}
bool ViewportWindow::firstGeometryPointWorldM(uint32_t model_id,
bool ViewportWindow::firstGeometryPointWorldM(uint32_t session_model_id,
Eigen::Vector3d& out) const {
return core_.firstGeometryPointWorldM(model_id, out);
return core_.firstGeometryPointWorldM(session_model_id, out);
}
void ViewportWindow::frameOnFederatedOrigin(uint32_t model_id,
uint32_t ViewportWindow::modelObjectIdBase(uint32_t session_model_id) const {
return core_.modelObjectIdBase(session_model_id);
}
void ViewportWindow::frameOnFederatedOrigin(uint32_t session_model_id,
float max_distance_m) {
auto it = models_gpu_.find(model_id);
auto it = models_gpu_.find(session_model_id);
if (it == models_gpu_.end()) return;
const ModelGpuData& m = it->second;
if (m.instances.empty()) return;
const ModelGpuData& model = it->second;
if (model.instances.empty()) return;
float mn[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
@@ -623,7 +627,7 @@ void ViewportWindow::frameOnFederatedOrigin(uint32_t model_id,
float mx[3] = { -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
for (const auto& inst : m.instances) {
for (const auto& inst : model.instances) {
for (int a = 0; a < 3; ++a) {
mn[a] = std::min(mn[a], inst.world_aabb_min[a]);
mx[a] = std::max(mx[a], inst.world_aabb_max[a]);
@@ -659,7 +663,7 @@ void ViewportWindow::frameOnFederatedOrigin(uint32_t model_id,
}
Log::info().noquote().nospace()
<< "[wgpu] frameOnFederatedOrigin model=" << model_id
<< "[wgpu] frameOnFederatedOrigin model=" << session_model_id
<< " distance=" << camera_distance_
<< " (cap=" << max_distance_m << "m, model radius=" << radius << ")";
@@ -1023,13 +1027,13 @@ void ViewportWindow::setHighlightTriangles(const std::vector<float>& world_xyz,
if (isExposed()) requestUpdate();
}
bool ViewportWindow::readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
bool ViewportWindow::readbackMeshTriangles(uint32_t session_model_id, uint32_t mesh_id,
MeshTriangles& out) const {
auto mit = models_gpu_.find(model_id);
auto mit = models_gpu_.find(session_model_id);
if (mit == models_gpu_.end()) return false;
const ModelGpuData& m = mit->second;
if (mesh_id >= m.mesh_triangles_cache.size()) return false;
const auto& src = m.mesh_triangles_cache[mesh_id];
const ModelGpuData& model = mit->second;
if (mesh_id >= model.mesh_triangles_cache.size()) return false;
const auto& src = model.mesh_triangles_cache[mesh_id];
if (src.indices.empty() || src.positions.empty()) return false;
// Copy out — callers iterate freely without worrying about lifetime
// (streaming may evict a chunk and rebuild the shadow on next load).
@@ -1049,12 +1053,12 @@ bool ViewportWindow::meshLocalToGlobal(uint32_t object_id,
const float mesh_local[3],
double global_out[3]) const {
// Find the instance via the per-model object_id_to_instance map.
// Use the live map key (`mid`) — see pickMeshLocalAt comment about
// stale InstanceInfo::model_id from sidecar writes.
for (const auto& [mid, m] : models_gpu_) {
auto it = m.object_id_to_instance.find(object_id);
if (it == m.object_id_to_instance.end()) continue;
const InstanceInfo& inst = m.instances[it->second];
// Use the live map key (`session_model_id`) — see pickMeshLocalAt comment about
// stale InstanceInfo::session_model_id from sidecar writes.
for (const auto& [session_model_id, model] : models_gpu_) {
auto it = model.object_id_to_instance.find(object_id);
if (it == model.object_id_to_instance.end()) continue;
const InstanceInfo& inst = model.instances[it->second];
// CoordinateOperation · placement · local — gives the IFC's own
// georeferenced world frame (ENH). Excludes FederatedFalseOrigin
// and ModelTransformation, matching the GL meshLocalToGlobal
@@ -1072,7 +1076,7 @@ bool ViewportWindow::meshLocalToGlobal(uint32_t object_id,
static_cast<double>(mesh_local[2]),
1.0);
const Eigen::Vector3d global =
(m.coordinate_operation_meters * P * local).head<3>();
(model.coordinate_operation_meters * P * local).head<3>();
global_out[0] = global.x();
global_out[1] = global.y();
global_out[2] = global.z();
@@ -1148,9 +1152,9 @@ void ViewportWindow::invertElementVisibility() {
// don't mutate the set we're iterating over.
std::vector<uint32_t> to_hide;
to_hide.reserve(1024);
for (const auto& [mid, m] : models_gpu_) {
if (m.hidden) continue;
for (const InstanceInfo& inst : m.instances) {
for (const auto& [session_model_id, model] : models_gpu_) {
if (model.hidden) continue;
for (const InstanceInfo& inst : model.instances) {
if (inst.object_id == 0) continue;
if (!visibility_.isHidden(inst.object_id)) {
to_hide.push_back(inst.object_id);
@@ -1251,10 +1255,10 @@ void ViewportWindow::updateVolumeReadout() {
// first matching instance. For label placement at the AABB
// centre this is identical-looking; only the rare multi-
// representation object_id sees a slightly smaller union.
for (const auto& [mid, m] : models_gpu_) {
auto it = m.object_id_to_instance.find(oid);
if (it == m.object_id_to_instance.end()) continue;
const InstanceInfo& inst = m.instances[it->second];
for (const auto& [session_model_id, model] : models_gpu_) {
auto it = model.object_id_to_instance.find(oid);
if (it == model.object_id_to_instance.end()) continue;
const InstanceInfo& inst = model.instances[it->second];
OverlayRenderer::Label lbl;
lbl.world_pos[0] = (inst.world_aabb_min[0] + inst.world_aabb_max[0]) * 0.5f;
lbl.world_pos[1] = (inst.world_aabb_min[1] + inst.world_aabb_max[1]) * 0.5f;
@@ -1335,7 +1339,7 @@ void ViewportWindow::ensureSelectionFlagsBuffer() { core_.ensureSelectionFlagsBu
// uploadSelectionFlagsIfDirty moved to ViewportCore (#84-k).
void ViewportWindow::uploadSelectionFlagsIfDirty() { core_.uploadSelectionFlagsIfDirty(); }
void ViewportWindow::buildModelBindGroup(ModelGpuData& m) { core_.buildModelBindGroup(m); }
void ViewportWindow::buildModelBindGroup(ModelGpuData& model) { core_.buildModelBindGroup(model); }
// buildChunkBindGroup moved to ViewportCore (#84-n).
@@ -1738,15 +1742,15 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
std::set<std::pair<uint32_t, size_t>> seen;
Log::info().noquote().nospace()
<< "[track] object " << id << " — enumerating chunks:";
for (auto& [mid, m] : models_gpu_) {
for (const auto& inst : m.instances) {
for (auto& [session_model_id, model] : models_gpu_) {
for (const auto& inst : model.instances) {
if (inst.object_id != id) continue;
if (inst.mesh_id >= m.mesh_chunk_idx.size()) continue;
const size_t ci = m.mesh_chunk_idx[inst.mesh_id];
if (!seen.insert({mid, ci}).second) continue;
const auto& c = m.chunks[ci];
if (inst.mesh_id >= model.mesh_chunk_idx.size()) continue;
const size_t ci = model.mesh_chunk_idx[inst.mesh_id];
if (!seen.insert({session_model_id, ci}).second) continue;
const auto& chunk = model.chunks[ci];
Log::info().noquote().nospace()
<< " model " << mid << " chunk " << ci
<< " model " << session_model_id << " chunk " << ci
<< " inst_aabb "
<< QString::number(inst.world_aabb_max[0] - inst.world_aabb_min[0], 'f', 1)
<< "×"
@@ -1754,17 +1758,17 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
<< "×"
<< QString::number(inst.world_aabb_max[2] - inst.world_aabb_min[2], 'f', 1) << "m"
<< " chunk_aabb "
<< QString::number(c.aabb_max[0] - c.aabb_min[0], 'f', 1) << "×"
<< QString::number(c.aabb_max[1] - c.aabb_min[1], 'f', 1) << "×"
<< QString::number(c.aabb_max[2] - c.aabb_min[2], 'f', 1) << "m"
<< " resident=" << (c.is_resident ? "Y" : "N");
<< QString::number(chunk.aabb_max[0] - chunk.aabb_min[0], 'f', 1) << "×"
<< QString::number(chunk.aabb_max[1] - chunk.aabb_min[1], 'f', 1) << "×"
<< QString::number(chunk.aabb_max[2] - chunk.aabb_min[2], 'f', 1) << "m"
<< " resident=" << (chunk.is_resident ? "Y" : "N");
// First hit becomes the "primary" slot the
// eviction watcher uses. Good enough until we wire
// a multi-chunk watcher.
if (tracked_chunk_idx_ == SIZE_MAX) {
tracked_chunk_mid_ = mid;
tracked_chunk_mid_ = session_model_id;
tracked_chunk_idx_ = ci;
tracked_was_resident_ = c.is_resident;
tracked_was_resident_ = chunk.is_resident;
}
}
}
+17 -16
View File
@@ -108,7 +108,7 @@ public:
// Synchronous metadata load + GPU upload. Requires wgpu init to have
// completed (i.e. the window has been exposed at least once). Returns
// the assigned model_id, or 0 on failure. Reads metadata only (mesh
// the assigned session_model_id, or 0 on failure. Reads metadata only (mesh
// dict + instance dict + georef); per-chunk vertex / index bytes are
// read on demand by the per-frame loader as chunks become visible.
uint32_t loadSidecar(const std::string& path);
@@ -118,7 +118,7 @@ public:
// unclaimed and is_resident=false. The per-frame loader
// (driveStreamingLoads) sub-allocates the chunk's vertex + index
// ranges from pool_ on demand as cull flags them visible.
void applyCachedModel(uint32_t model_id,
void applyCachedModel(uint32_t session_model_id,
struct StreamingSidecar metadata);
// Direct-IFC ingestion (mirrors GL ViewportWindow). The host (typically
@@ -129,20 +129,20 @@ public:
// staged data, allocates pool slices, and uploads — same render path
// as a sidecar load. Bytes are gathered from memory (no disk I/O), so
// every chunk lands `is_resident=true` immediately. The streamer's
// model_id is passed through unchanged; the viewport's globally-unique
// session_model_id is passed through unchanged; the viewport's globally-unique
// object_id rebasing happens at finalize time.
void uploadStreamedMesh(const struct StreamedMesh& mesh);
void uploadStreamedInstance(const struct StreamedInstance& instance_record);
void finalizeModel(uint32_t model_id);
void finalizeModel(uint32_t session_model_id);
void removeModel(uint32_t model_id);
void removeModel(uint32_t session_model_id);
void resetScene();
// Model-level visibility. Mirrors the GL ViewportWindow API — flips
// ModelGpuData::hidden, which every render/pick/cull pass already
// consults. requestUpdate() so the change is visible immediately.
void hideModel(uint32_t model_id);
void showModel(uint32_t model_id);
void hideModel(uint32_t session_model_id);
void showModel(uint32_t session_model_id);
// Federation pipeline: composed instance transform =
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation
@@ -153,9 +153,9 @@ public:
// integration compiles against these signatures; visual georef parity
// arrives with the recompose+SSBO-rewrite work tracked separately.
void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters);
void setModelCoordinateOperation(uint32_t model_id,
void setModelCoordinateOperation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters);
void setModelTransformation(uint32_t model_id,
void setModelTransformation(uint32_t session_model_id,
const Eigen::Matrix4d& matrix_meters);
size_t modelCount() const { return models_gpu_.size(); }
@@ -378,11 +378,11 @@ public:
// CPU mesh shadow: positions (3 floats/vert, mesh-local) + indices
// (LOD0). Populated at applyCachedModel / applyStreamedChunk —
// returns false if the mesh isn't loaded yet (streaming) or the
// (model_id, mesh_id) pair doesn't resolve. Matches the GL
// (session_model_id, mesh_id) pair doesn't resolve. Matches the GL
// ViewportWindow::MeshTriangles + readbackMeshTriangles shape so
// the measure tools port verbatim.
using MeshTriangles = ModelGpuData::MeshTriangles;
bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
bool readbackMeshTriangles(uint32_t session_model_id, uint32_t mesh_id,
MeshTriangles& out) const;
// Pure CPU lookup: object_id → owning model + mesh + raw placement
@@ -401,7 +401,8 @@ public:
// (ViewportView::guessFederatedFalseOriginFromFirstModel) consumes
// this lazily on modelGeometryReady. Returns false when the model
// is unknown or has no instances.
bool firstGeometryPointWorldM(uint32_t model_id,
uint32_t modelObjectIdBase(uint32_t session_model_id) const;
bool firstGeometryPointWorldM(uint32_t session_model_id,
Eigen::Vector3d& out) const;
// Re-frame the camera onto the federated false origin in post-shift
@@ -421,7 +422,7 @@ public:
// Unlike viewAll() this *never* iterates all loaded models — it
// frames around the specific model the guess fired for, ignoring
// models with bad coordinates elsewhere in the session.
void frameOnFederatedOrigin(uint32_t model_id, float max_distance_m);
void frameOnFederatedOrigin(uint32_t session_model_id, float max_distance_m);
// Selection accessor. Exposed for callers (bonsai's volume readout)
// that need to read selectionIds() / activeObjectId(). Mutation goes
@@ -578,11 +579,11 @@ private:
// stayed during the move and forwards to core_ — once every internal
// caller routes through ViewportCore directly the forwarder goes away.
// Walk every instance of `model_id`, recompose its transform from the
// Walk every instance of `session_model_id`, recompose its transform from the
// current federation matrices, refresh per-chunk world AABBs, and
// re-upload InstanceGpu[] into m.instance_storage. No-op if the model
// is unknown, has no instances, or wgpu init hasn't completed.
void recomposeAndUploadModel(uint32_t model_id);
void recomposeAndUploadModel(uint32_t session_model_id);
bool& wgpu_initialized_;
int& configured_w_;
@@ -900,7 +901,7 @@ private:
// Per-model state aliases (storage in core_).
std::unordered_map<uint32_t, ModelGpuData>& models_gpu_;
uint32_t& next_model_id_;
uint32_t& next_session_model_id_;
uint32_t& next_object_id_;
// Sidecar paths queued before init completes.
+39 -33
View File
@@ -60,6 +60,12 @@ QString writeStubFile(const QString& path) {
return QDir::cleanPath(fi.absoluteFilePath());
}
// addModel with the filename as its label — mirrors how the app
// (models/Commands.cpp) calls it now that addModel takes the label explicitly.
QString addLocalModel(Federation& fed, const QString& path) {
return fed.addModel(path, QFileInfo(path).fileName());
}
QJsonObject readJsonFile(const QString& path) {
QFile f(path);
REQUIRE(f.open(QIODevice::ReadOnly));
@@ -88,7 +94,7 @@ TEST_CASE("addModel emits dirty=true; markClean clears it; remove re-dirties", "
QSignalSpy spy(&fed, &Federation::dirtyChanged);
QString abs = writeStubFile(tmp.filePath("a.ifc"));
QString id = fed.addModel(abs);
QString id = addLocalModel(fed, abs);
REQUIRE_FALSE(id.isEmpty());
REQUIRE(fed.isDirty());
REQUIRE(spy.count() == 1);
@@ -108,9 +114,9 @@ TEST_CASE("addModel emits dirty=true; markClean clears it; remove re-dirties", "
TEST_CASE("addModel rejects empty paths and nested .ifcfed sources", "[federation]") {
ensureQApp();
Federation fed;
REQUIRE(fed.addModel("").isEmpty());
REQUIRE(fed.addModel("nested.ifcfed").isEmpty());
REQUIRE(fed.addModel("nested.IfcFed").isEmpty()); // case-insensitive
REQUIRE(addLocalModel(fed, "").isEmpty());
REQUIRE(addLocalModel(fed, "nested.ifcfed").isEmpty());
REQUIRE(addLocalModel(fed, "nested.IfcFed").isEmpty()); // case-insensitive
REQUIRE(fed.models().empty());
REQUIRE_FALSE(fed.isDirty());
}
@@ -152,7 +158,7 @@ TEST_CASE("setModelVisible toggles flag, dirty, and signal; idempotent", "[feder
REQUIRE(tmp.isValid());
Federation fed;
QString id = fed.addModel(writeStubFile(tmp.filePath("a.ifc")));
QString id = addLocalModel(fed, writeStubFile(tmp.filePath("a.ifc")));
REQUIRE_FALSE(id.isEmpty());
REQUIRE(fed.findById(id)->visible); // visible by default
fed.markClean();
@@ -176,7 +182,7 @@ TEST_CASE("setModelVisible toggles flag, dirty, and signal; idempotent", "[feder
REQUIRE(dirty_spy.count() == 0);
REQUIRE(vis_spy.count() == 0);
// Unknown fed_id is a no-op (no crash, no signal).
// Unknown model_id is a no-op (no crash, no signal).
fed.setModelVisible("not-a-real-id", false);
REQUIRE_FALSE(fed.isDirty());
REQUIRE(vis_spy.count() == 0);
@@ -199,7 +205,7 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view",
Federation src;
QString id1 = src.addModel(src1, "Wall");
QString id2 = src.addModel(src2); // default display_name from filename
QString id2 = addLocalModel(src, src2); // filename as label
REQUIRE_FALSE(id1.isEmpty());
REQUIRE_FALSE(id2.isEmpty());
@@ -261,8 +267,8 @@ TEST_CASE("save stores paths relative when under fed_dir, absolute otherwise", "
QString outside = writeStubFile(root.filePath("elsewhere/outside.ifc"));
Federation fed;
fed.addModel(inside);
fed.addModel(outside);
addLocalModel(fed, inside);
addLocalModel(fed, outside);
QString err;
REQUIRE(fed.save(fed_path, &err));
@@ -304,7 +310,7 @@ TEST_CASE("Save-As to a different directory recomputes path relativity", "[feder
QString fed_b = fed_dir_b + "/proj.ifcfed";
Federation fed;
fed.addModel(src);
addLocalModel(fed, src);
QString err;
REQUIRE(fed.save(fed_a, &err));
@@ -514,31 +520,31 @@ TEST_CASE("setModelGroup assigns and reassigns; rejects unknown group",
ensureQApp();
QTemporaryDir tmp;
Federation fed;
QString mid = fed.addModel(writeStubFile(tmp.filePath("a.ifc")));
QString model_id = addLocalModel(fed, writeStubFile(tmp.filePath("a.ifc")));
QString gid = fed.addGroup("G");
fed.markClean();
QSignalSpy spy(&fed, &Federation::modelGroupChanged);
fed.setModelGroup(mid, gid);
REQUIRE(fed.findById(mid)->group_id == gid);
fed.setModelGroup(model_id, gid);
REQUIRE(fed.findById(model_id)->group_id == gid);
REQUIRE(fed.isDirty());
REQUIRE(spy.count() == 1);
// Idempotent.
fed.markClean();
spy.clear();
fed.setModelGroup(mid, gid);
fed.setModelGroup(model_id, gid);
REQUIRE_FALSE(fed.isDirty());
REQUIRE(spy.count() == 0);
// Unknown group is rejected.
fed.setModelGroup(mid, "no-such-group");
REQUIRE(fed.findById(mid)->group_id == gid);
fed.setModelGroup(model_id, "no-such-group");
REQUIRE(fed.findById(model_id)->group_id == gid);
REQUIRE_FALSE(fed.isDirty());
// Reassign back to root.
fed.setModelGroup(mid, QString());
REQUIRE(fed.findById(mid)->group_id.isEmpty());
fed.setModelGroup(model_id, QString());
REQUIRE(fed.findById(model_id)->group_id.isEmpty());
REQUIRE(spy.count() == 1);
}
@@ -547,12 +553,12 @@ TEST_CASE("setGroupVisible affects effective visibility cascade",
ensureQApp();
QTemporaryDir tmp;
Federation fed;
QString mid = fed.addModel(writeStubFile(tmp.filePath("a.ifc")));
QString model_id = addLocalModel(fed, writeStubFile(tmp.filePath("a.ifc")));
QString outer = fed.addGroup("Outer");
QString inner = fed.addGroup("Inner", outer);
fed.setModelGroup(mid, inner);
fed.setModelGroup(model_id, inner);
REQUIRE(fed.isModelEffectivelyVisible(mid));
REQUIRE(fed.isModelEffectivelyVisible(model_id));
REQUIRE(fed.isGroupChainVisible(inner));
// Hide the outer group: inner chain visibility flips, model effective
@@ -560,21 +566,21 @@ TEST_CASE("setGroupVisible affects effective visibility cascade",
fed.setGroupVisible(outer, false);
REQUIRE_FALSE(fed.isGroupChainVisible(outer));
REQUIRE_FALSE(fed.isGroupChainVisible(inner));
REQUIRE_FALSE(fed.isModelEffectivelyVisible(mid));
REQUIRE(fed.findById(mid)->visible);
REQUIRE_FALSE(fed.isModelEffectivelyVisible(model_id));
REQUIRE(fed.findById(model_id)->visible);
// Hiding a model directly while its group is also hidden — still
// effectively hidden.
fed.setModelVisible(mid, false);
REQUIRE_FALSE(fed.isModelEffectivelyVisible(mid));
fed.setModelVisible(model_id, false);
REQUIRE_FALSE(fed.isModelEffectivelyVisible(model_id));
// Re-show the outer group; model is still hidden by its own flag.
fed.setGroupVisible(outer, true);
REQUIRE(fed.isGroupChainVisible(inner));
REQUIRE_FALSE(fed.isModelEffectivelyVisible(mid));
REQUIRE_FALSE(fed.isModelEffectivelyVisible(model_id));
fed.setModelVisible(mid, true);
REQUIRE(fed.isModelEffectivelyVisible(mid));
fed.setModelVisible(model_id, true);
REQUIRE(fed.isModelEffectivelyVisible(model_id));
}
TEST_CASE("setGroupParent rejects cycles and self-parenting",
@@ -610,9 +616,9 @@ TEST_CASE("removeGroup reparents direct children + models up one level",
QString mid_outer = fed.addGroup("MidOuter", outer);
QString inner = fed.addGroup("Inner", mid_outer);
QString m_outer = fed.addModel(writeStubFile(tmp.filePath("a.ifc")));
QString m_mid = fed.addModel(writeStubFile(tmp.filePath("b.ifc")));
QString m_inner = fed.addModel(writeStubFile(tmp.filePath("c.ifc")));
QString m_outer = addLocalModel(fed, writeStubFile(tmp.filePath("a.ifc")));
QString m_mid = addLocalModel(fed, writeStubFile(tmp.filePath("b.ifc")));
QString m_inner = addLocalModel(fed, writeStubFile(tmp.filePath("c.ifc")));
fed.setModelGroup(m_outer, outer);
fed.setModelGroup(m_mid, mid_outer);
fed.setModelGroup(m_inner, inner);
@@ -653,8 +659,8 @@ TEST_CASE("groups + model.group_id round-trip through nested JSON save/load",
Federation src;
site_id = src.addGroup("Site");
bldg_id = src.addGroup("Building 1", site_id);
m_root = src.addModel(writeStubFile(tmp.filePath("root.ifc")));
m_bldg = src.addModel(writeStubFile(tmp.filePath("bldg.ifc")));
m_root = addLocalModel(src, writeStubFile(tmp.filePath("root.ifc")));
m_bldg = addLocalModel(src, writeStubFile(tmp.filePath("bldg.ifc")));
src.setModelGroup(m_bldg, bldg_id);
src.setGroupVisible(bldg_id, false);
@@ -323,7 +323,7 @@ TEST_CASE("findInstanceInModels fills the correct lookup for an owned id", "[ins
InstanceCompose::InstanceLookup out;
REQUIRE(InstanceCompose::findInstanceInModels(8u, models, out));
REQUIRE(out.model_id == 2u);
REQUIRE(out.session_model_id == 2u);
REQUIRE(out.mesh_id == 4u);
REQUIRE(out.placement_transformation[12] == 22.0);
REQUIRE(out.placement_transformation[0] == 1.0);
@@ -247,13 +247,13 @@ TEST_CASE("quantizeVertex passes the packed color through unchanged", "[instgeom
TEST_CASE("StreamedMesh and StreamedInstance default-init to zeroed metadata", "[instgeom]") {
StreamedMesh mc;
REQUIRE(mc.model_id == 0);
REQUIRE(mc.session_model_id == 0);
REQUIRE(mc.local_mesh_id == 0);
REQUIRE(mc.vertices.empty());
REQUIRE(mc.indices.empty());
StreamedInstance ic;
REQUIRE(ic.model_id == 0);
REQUIRE(ic.session_model_id == 0);
REQUIRE(ic.local_mesh_id == 0);
REQUIRE(ic.object_id == 0);
REQUIRE(ic.color_override_rgba8 == 0);
+2 -2
View File
@@ -87,7 +87,7 @@ SidecarData buildFixture() {
inst.mesh_id = (i < 3) ? 0u : 1u;
inst.object_id = uint32_t(100 + i);
inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u));
inst.model_id = 1;
inst.session_model_id = 1;
for (int k = 0; k < 16; ++k) {
inst.placement_transformation[k] = double(i) * 0.25 + double(k);
inst.transform[k] = float(i) * 0.5f + float(k);
@@ -111,7 +111,7 @@ SidecarData buildFixture() {
for (size_t i = 0; i < sd.elements.size(); ++i) {
ElementTableRecord& e = sd.elements[i];
e.object_id = uint32_t(100 + i);
e.model_id = 1;
e.session_model_id = 1;
e.ifc_id = int32_t(1000 + i);
e.guid_offset = 0; e.guid_length = 0;
e.name_offset = 1; e.name_length = 4; // "Wall"
+1 -1
View File
@@ -84,7 +84,7 @@ SidecarData buildFixture() {
InstanceInfo ic;
ic.mesh_id = uint32_t(i); // authoritative
ic.object_id = obj++;
ic.model_id = 1;
ic.session_model_id = 1;
const float x = float((i * 13 + k * 5) % 11);
const float y = float((i * 7 + k * 3) % 9);
const float z = float((i * 5 + k * 2) % 7);
@@ -70,7 +70,7 @@ SidecarData buildFixture() {
for (size_t i = 0; i < sd.instances.size(); ++i) {
sd.instances[i].mesh_id = (i < 2) ? 0u : 1u;
sd.instances[i].object_id = uint32_t(100 + i);
sd.instances[i].model_id = 1;
sd.instances[i].session_model_id = 1;
}
sd.has_coordinate_operation = 1;
@@ -82,7 +82,7 @@ SidecarData buildFixture() {
sd.elements.resize(2);
for (size_t i = 0; i < sd.elements.size(); ++i) {
sd.elements[i].object_id = uint32_t(100 + i);
sd.elements[i].model_id = 1;
sd.elements[i].session_model_id = 1;
sd.elements[i].ifc_id = int32_t(1000 + i);
}
// v16 stores geometry per-chunk (compressed); a fixture with geometry needs
@@ -93,14 +93,14 @@ SidecarData buildFixture() {
} // namespace
TEST_CASE("readSidecarMetadataOnly returns metadata, skips bulk geometry",
TEST_CASE("readSidecarMetadata returns metadata, skips bulk geometry",
"[streaming]") {
fs::path dir = makeScratchDir("metaonly");
fs::path ifc = dir / "model.ifc";
SidecarData sd = buildFixture();
REQUIRE(writeSidecar(ifc.string(), sd));
auto meta = readSidecarMetadataOnly(ifc.string());
auto meta = readSidecarMetadata(ifc.string());
REQUIRE(meta.has_value());
// Bulk geometry is skipped, not loaded.
@@ -127,9 +127,9 @@ TEST_CASE("readSidecarMetadataOnly returns metadata, skips bulk geometry",
REQUIRE(std::memcmp(&meta->meta.meshes[1], &sd.meshes[1], sizeof(MeshInfo)) == 0);
}
TEST_CASE("readSidecarMetadataOnly rejects missing / corrupt files", "[streaming]") {
TEST_CASE("readSidecarMetadata rejects missing / corrupt files", "[streaming]") {
fs::path dir = makeScratchDir("reject");
REQUIRE_FALSE(readSidecarMetadataOnly((dir / "absent.ifc").string()).has_value());
REQUIRE_FALSE(readSidecarMetadata((dir / "absent.ifc").string()).has_value());
// Truncated head (under 16 bytes).
fs::path bad = dir / "bad.ifc";
@@ -140,7 +140,7 @@ TEST_CASE("readSidecarMetadataOnly rejects missing / corrupt files", "[streaming
std::fwrite(junk, 1, sizeof(junk), f);
std::fclose(f);
}
REQUIRE_FALSE(readSidecarMetadataOnly(bad.string()).has_value());
REQUIRE_FALSE(readSidecarMetadata(bad.string()).has_value());
}
TEST_CASE("readChunkGeometryCompressed decompresses a chunk's blobs", "[streaming]") {
@@ -148,7 +148,7 @@ TEST_CASE("readChunkGeometryCompressed decompresses a chunk's blobs", "[streamin
fs::path ifc = dir / "model.ifc";
SidecarData sd = buildFixture();
REQUIRE(writeSidecar(ifc.string(), sd));
auto meta = readSidecarMetadataOnly(ifc.string());
auto meta = readSidecarMetadata(ifc.string());
REQUIRE(meta.has_value());
REQUIRE(meta->meta.chunks.size() == 2);
@@ -202,7 +202,7 @@ TEST_CASE("v16 element metadata block: fetch via locator, decompress, parse", "[
SidecarData sd = buildFixture();
REQUIRE(writeSidecar(ifc.string(), sd));
auto meta = readSidecarMetadataOnly(ifc.string());
auto meta = readSidecarMetadata(ifc.string());
REQUIRE(meta.has_value());
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // geometry metadata
REQUIRE(meta->meta.chunks.size() == sd.chunks.size());