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