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