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:
Dion Moult
2026-05-04 12:22:14 +10:00
parent 4597be7fda
commit de5eb9641f
7 changed files with 183 additions and 0 deletions
+47
View File
@@ -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));