mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
ifcviewer-full: hide and remove model actions
Right-click a model root in the Elements tree to get Hide/Show and Remove. Hide flips the federation's per-model visible flag (already round-tripped to .ifcfed), pushes ViewportWindow::hideModel/showModel, and italicises + greys the tree root as a visual cue. Remove drops the model from the viewport, the SceneLoader (streamer + caches), the MainWindow UI maps and tree, and the Federation — disabled while the model is the active load. Visibility is reapplied on each model's load completion (sidecar or stream), so a federation saved with hidden models opens with them hidden. clearScene() now also drops SceneLoader state so streamers no longer leak across federation transitions. API additions: - Federation::setModelVisible + modelVisibilityChanged signal - SceneLoader::removeModel + isLoadingModel Tests cover the setter (dirty + signal + idempotence + unknown id); extends the existing round-trip test to actually exercise the visibility load/save it always claimed to. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -254,6 +254,17 @@ void Federation::setModelTransformation(const QString& fed_id,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
setDirty(true);
|
||||
emit modelVisibilityChanged(fed_id, visible);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Federation::markClean() {
|
||||
setDirty(false);
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ public:
|
||||
void setConfig(const FederationConfig&);
|
||||
void setFederatedFalseOrigin(const FederatedFalseOrigin&);
|
||||
void setModelTransformation(const QString& fed_id, const ModelTransformation&);
|
||||
void setModelVisible(const QString& fed_id, bool visible);
|
||||
|
||||
// Accessors
|
||||
const std::vector<Model>& models() const { return models_; }
|
||||
@@ -232,6 +233,7 @@ signals:
|
||||
void configChanged();
|
||||
void federatedFalseOriginChanged();
|
||||
void modelTransformationChanged(const QString& fed_id);
|
||||
void modelVisibilityChanged(const QString& fed_id, bool visible);
|
||||
|
||||
private:
|
||||
void setDirty(bool d);
|
||||
|
||||
@@ -123,6 +123,25 @@ void SceneLoader::connectStreamer(GeometryStreamer* streamer) {
|
||||
this, &SceneLoader::onStreamerError, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void SceneLoader::removeModel(uint32_t mid) {
|
||||
// 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;
|
||||
|
||||
for (auto it = load_queue_.begin(); it != load_queue_.end();) {
|
||||
if (*it == mid) it = load_queue_.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
if (it->second.streamer) {
|
||||
it->second.streamer->deleteLater();
|
||||
}
|
||||
models_.erase(it);
|
||||
}
|
||||
|
||||
void SceneLoader::cancelCurrentLoad() {
|
||||
if (loading_model_id_ == 0) return;
|
||||
auto it = models_.find(loading_model_id_);
|
||||
|
||||
@@ -60,8 +60,15 @@ public:
|
||||
std::vector<uint32_t> addFiles(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; }
|
||||
size_t modelCount() const { return models_.size(); }
|
||||
|
||||
// Drop the loader's tracking for `mid` — 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);
|
||||
|
||||
QString filePath(uint32_t mid) const;
|
||||
QString displayName(uint32_t mid) const;
|
||||
ifcopenshell::file* ifcFile(uint32_t mid) const;
|
||||
|
||||
@@ -145,6 +145,48 @@ TEST_CASE("setHomeView / clearHomeView toggle dirty + has_home_view", "[federati
|
||||
REQUIRE(spy.count() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("setModelVisible toggles flag, dirty, and signal; idempotent", "[federation]") {
|
||||
ensureQApp();
|
||||
QTemporaryDir tmp;
|
||||
REQUIRE(tmp.isValid());
|
||||
|
||||
Federation fed;
|
||||
QString id = fed.addModel(writeStubFile(tmp.filePath("a.ifc")));
|
||||
REQUIRE_FALSE(id.isEmpty());
|
||||
REQUIRE(fed.findById(id)->visible); // visible by default
|
||||
fed.markClean();
|
||||
|
||||
QSignalSpy dirty_spy(&fed, &Federation::dirtyChanged);
|
||||
QSignalSpy vis_spy(&fed, &Federation::modelVisibilityChanged);
|
||||
|
||||
fed.setModelVisible(id, false);
|
||||
REQUIRE_FALSE(fed.findById(id)->visible);
|
||||
REQUIRE(fed.isDirty());
|
||||
REQUIRE(dirty_spy.count() == 1);
|
||||
REQUIRE(vis_spy.count() == 1);
|
||||
REQUIRE(vis_spy.takeFirst().at(0).toString() == id);
|
||||
|
||||
// Idempotent: same value, no signal, dirty unchanged.
|
||||
fed.markClean();
|
||||
dirty_spy.clear();
|
||||
vis_spy.clear();
|
||||
fed.setModelVisible(id, false);
|
||||
REQUIRE_FALSE(fed.isDirty());
|
||||
REQUIRE(dirty_spy.count() == 0);
|
||||
REQUIRE(vis_spy.count() == 0);
|
||||
|
||||
// Unknown fed_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);
|
||||
|
||||
// Toggle back on.
|
||||
fed.setModelVisible(id, true);
|
||||
REQUIRE(fed.findById(id)->visible);
|
||||
REQUIRE(fed.isDirty());
|
||||
REQUIRE(vis_spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("save then load round-trips models, transform, visibility, home view", "[federation]") {
|
||||
ensureQApp();
|
||||
QTemporaryDir tmp;
|
||||
@@ -160,6 +202,9 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view",
|
||||
REQUIRE_FALSE(id1.isEmpty());
|
||||
REQUIRE_FALSE(id2.isEmpty());
|
||||
|
||||
// Hide the second model — exercises the visibility round-trip.
|
||||
src.setModelVisible(id2, false);
|
||||
|
||||
Federation::HomeView hv;
|
||||
hv.target = QVector3D(10, 20, 30);
|
||||
hv.distance = 77.0f;
|
||||
@@ -183,9 +228,11 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view",
|
||||
REQUIRE(dst.models()[0].id == id1);
|
||||
REQUIRE(dst.models()[0].display_name == "Wall");
|
||||
REQUIRE(dst.models()[0].source_path == src1);
|
||||
REQUIRE(dst.models()[0].visible);
|
||||
REQUIRE(dst.models()[1].id == id2);
|
||||
REQUIRE(dst.models()[1].display_name == "slab.ifc");
|
||||
REQUIRE(dst.models()[1].source_path == src2);
|
||||
REQUIRE_FALSE(dst.models()[1].visible);
|
||||
|
||||
REQUIRE(dst.hasHomeView());
|
||||
REQUIRE(dst.homeView().target == QVector3D(10, 20, 30));
|
||||
|
||||
Reference in New Issue
Block a user