ifcviewer: overhaul model/object ID tracking

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

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

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

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

Tests: 125/125 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 14:10:05 +10:00
parent b441fada90
commit 75c9da5098
49 changed files with 914 additions and 885 deletions
+25 -26
View File
@@ -69,61 +69,61 @@ public:
bool shouldReadSidecar() const { return should_read_sidecar_; }
bool shouldWriteSidecar() const { return should_write_sidecar_; }
// Returns the model_ids assigned to the enqueued paths, in order.
// Returns the session_model_ids assigned to the enqueued paths, in order.
// Callers can use these to set up per-model UI state (tree roots, etc.)
// before any load signal fires.
std::vector<uint32_t> addFiles(const QStringList& paths);
std::vector<uint32_t> queueModels(const QStringList& paths);
void cancelCurrentLoad();
bool isLoading() const { return loading_model_id_ != 0 || !load_queue_.empty(); }
bool isLoadingModel(uint32_t mid) const { return loading_model_id_ == mid; }
bool isLoading() const { return loading_session_model_id_ != 0 || !load_queue_.empty(); }
bool isLoadingModel(uint32_t session_model_id) const { return loading_session_model_id_ == session_model_id; }
size_t modelCount() const { return models_.size(); }
// Drop the loader's tracking for `mid` — its streamer, file path, georef
// Drop the loader's tracking for `session_model_id` — its streamer, file path, georef
// cache, and queue slot if still pending. Caller is responsible for the
// viewport / UI cleanup; this only releases the loader's own state.
// Refuses while the model is the active load (use cancelCurrentLoad first).
void removeModel(uint32_t mid);
void removeModel(uint32_t session_model_id);
QString filePath(uint32_t mid) const;
QString displayName(uint32_t mid) const;
ifcopenshell::file* ifcFile(uint32_t mid) const;
QString filePath(uint32_t session_model_id) const;
QString displayName(uint32_t session_model_id) const;
ifcopenshell::file* ifcFile(uint32_t session_model_id) const;
// Lazily computes the model's georef matrix + unit scales the first
// time it's asked for, caches the result, and returns a pointer into the
// cache. Returns nullptr when the IFC file isn't available yet (e.g.
// sidecar-hit path before the data-source thread populates the streamer).
const ModelGeoref* modelGeoref(uint32_t mid);
const ModelGeoref* modelGeoref(uint32_t session_model_id);
signals:
void progressChanged(int percent);
void loadStarted(uint32_t mid, QString display_name);
void loadStarted(uint32_t session_model_id, QString display_name);
// Fired once per sidecar hit, before loadedFromSidecar, with the full
// packed element set. Consumer is responsible for decoding + tree/
// property-map population. Moved arguments — avoid unnecessary copies.
void sidecarElementsReady(uint32_t mid,
void sidecarElementsReady(uint32_t session_model_id,
std::vector<ElementTableRecord> elements,
std::string string_table);
void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
void loadedFromSidecar(uint32_t session_model_id, qint64 elapsed_ms);
// Fired after a sidecar-hit model has its .rdb/.ifc opened as a
// property data source in the background. Consumers can refresh
// any UI that queries ifcFile(mid) for attributes/properties.
void dataSourceReady(uint32_t mid);
// any UI that queries ifcFile(session_model_id) for attributes/properties.
void dataSourceReady(uint32_t session_model_id);
// Fired repeatedly while streaming, as the worker thread produces
// elements. Each batch contains whatever accumulated since the last
// poll tick.
void streamedElementsReady(uint32_t mid, std::vector<ElementInfo> elements);
void streamedElementsReady(uint32_t session_model_id, std::vector<ElementInfo> elements);
// Fired once after the streamer finishes and the viewport has been
// finalized. Consumer may synchronously perform work that needs all
// elements to be known (e.g. sidecar write) — SceneLoader will only
// start the next queued load after all slots return.
void loadedFromStream(uint32_t mid, qint64 elapsed_ms);
void loadCancelled(uint32_t mid);
void loadedFromStream(uint32_t session_model_id, qint64 elapsed_ms);
void loadCancelled(uint32_t session_model_id);
void loadError(uint32_t mid, QString message);
void loadError(uint32_t session_model_id, QString message);
void allLoadsFinished();
private slots:
@@ -143,7 +143,7 @@ private:
GeometryStreamer* streamer = nullptr;
QElapsedTimer load_timer;
// Cached on first SceneLoader::modelGeoref(mid) call once the
// Cached on first SceneLoader::modelGeoref(session_model_id) call once the
// streamer has its IFC file loaded.
ModelGeoref georef;
bool has_georef = false;
@@ -158,21 +158,20 @@ private:
};
void startNextLoad();
void startStreamLoadFor(uint32_t mid);
void loadFromGeometryStreamer(uint32_t session_model_id);
void connectStreamer(GeometryStreamer* streamer);
void joinSidecarThread();
void joinDataSourceThreads();
void applySidecarData(uint32_t mid, StreamingSidecar metadata);
void startDataSourceLoad(uint32_t mid);
void applySidecarData(uint32_t session_model_id, StreamingSidecar metadata);
void startDataSourceLoad(uint32_t session_model_id);
ViewportWindow* viewport_ = nullptr;
bool should_read_sidecar_ = false;
bool should_write_sidecar_ = false;
std::map<uint32_t, Model> models_;
std::deque<uint32_t> load_queue_;
uint32_t next_model_id_ = 1;
uint32_t next_object_id_ = 1;
uint32_t loading_model_id_ = 0;
uint32_t next_session_model_id_ = 1;
uint32_t loading_session_model_id_ = 0;
std::thread sidecar_read_thread_;
// One thread per sidecar-hit model while its .rdb/.ifc opens in the
// background. Joined only at destruction so a slow SPF parse on model