mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-30 08:33:10 +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:
@@ -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