mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 06:32:09 +00:00
Route bonsai through wgpu; delete the GL backend
Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.
SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
the StreamingSidecar off to the new applyCachedModel. Field accesses
inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
finalizeModel). The applyLodExtension call is dropped — wgpu has no
live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
next open.
Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
duration of the migration; the GL-side IfcViewer also publicly links
IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.
GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
removed. The first has no replacement (wgpu doesn't use a per-instance
BVH); the latter two are ported separately. test_lod_builder,
test_sidecar_cache, test_instanced_geometry, test_federation remain
(backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
of the surviving translation units reach for them.
Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -32,10 +32,6 @@ function(add_ifcviewer_unit_test name)
|
||||
catch_discover_tests(${name})
|
||||
endfunction()
|
||||
|
||||
add_ifcviewer_unit_test(test_bvh_accel
|
||||
SOURCES ${IFCVIEWER_SRC}/BvhAccel.cpp
|
||||
)
|
||||
|
||||
if(WITH_MESH_OPTIMIZER)
|
||||
add_ifcviewer_unit_test(test_lod_builder
|
||||
SOURCES
|
||||
@@ -50,11 +46,9 @@ add_ifcviewer_unit_test(test_sidecar_cache
|
||||
|
||||
add_ifcviewer_unit_test(test_instanced_geometry)
|
||||
|
||||
# Federation, Visibility and Selection are Qt-derived (QObject + signals).
|
||||
# Unlike the other Tier-1 tests they have to pull Qt6 in directly and enable
|
||||
# AUTOMOC for the Q_OBJECT moc-generation. OpenGL is needed only by the
|
||||
# Selection test (Selection.cpp references QOpenGLFunctions_4_5_Core).
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Test OpenGL REQUIRED PATHS ${QT_DIR})
|
||||
# Federation is Qt-derived (QObject + signals). It has to pull Qt6 in
|
||||
# directly and enable AUTOMOC for the Q_OBJECT moc-generation.
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui Test REQUIRED PATHS ${QT_DIR})
|
||||
|
||||
find_package(Eigen3 REQUIRED)
|
||||
|
||||
@@ -82,36 +76,3 @@ target_link_libraries(test_federation PRIVATE
|
||||
IfcParse # Unit.cpp uses express::Base / file APIs
|
||||
)
|
||||
catch_discover_tests(test_federation)
|
||||
|
||||
# VisibilityState / SelectionState are QObjects (for their changed() signal)
|
||||
# but their construction + mutation API touch no GL, so the tests exercise
|
||||
# the pure CPU state machine without ever creating a context. Selection.cpp
|
||||
# still references QOpenGLFunctions_4_5_Core, so test_selection has to link
|
||||
# Qt6::OpenGL even though no GL call is reached at runtime.
|
||||
add_executable(test_visibility
|
||||
test_visibility.cpp
|
||||
${IFCVIEWER_SRC}/Visibility.cpp
|
||||
)
|
||||
set_target_properties(test_visibility PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(test_visibility PRIVATE ${IFCVIEWER_SRC})
|
||||
target_link_libraries(test_visibility PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Test # QSignalSpy
|
||||
)
|
||||
catch_discover_tests(test_visibility)
|
||||
|
||||
add_executable(test_selection
|
||||
test_selection.cpp
|
||||
${IFCVIEWER_SRC}/Selection.cpp
|
||||
)
|
||||
set_target_properties(test_selection PROPERTIES AUTOMOC ON)
|
||||
target_include_directories(test_selection PRIVATE ${IFCVIEWER_SRC})
|
||||
target_link_libraries(test_selection PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
Qt${QT_VERSION}::Core
|
||||
Qt${QT_VERSION}::Gui # QtOpenGL depends on QtGui
|
||||
Qt${QT_VERSION}::OpenGL # Selection.cpp: QOpenGLFunctions_4_5_Core
|
||||
Qt${QT_VERSION}::Test # QSignalSpy
|
||||
)
|
||||
catch_discover_tests(test_selection)
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "BvhAccel.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
BvhItem makeItem(float x, float y, float z, float r, uint32_t model_id = 1) {
|
||||
BvhItem it{};
|
||||
it.aabb_min[0] = x - r;
|
||||
it.aabb_min[1] = y - r;
|
||||
it.aabb_min[2] = z - r;
|
||||
it.aabb_max[0] = x + r;
|
||||
it.aabb_max[1] = y + r;
|
||||
it.aabb_max[2] = z + r;
|
||||
it.model_id = model_id;
|
||||
return it;
|
||||
}
|
||||
|
||||
bool aabbContains(const float outer_min[3], const float outer_max[3],
|
||||
const float inner_min[3], const float inner_max[3]) {
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (inner_min[a] < outer_min[a]) return false;
|
||||
if (inner_max[a] > outer_max[a]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void verifyNode(const ModelBvh& mbvh,
|
||||
const std::vector<BvhItem>& items,
|
||||
uint32_t node_idx) {
|
||||
REQUIRE(node_idx < mbvh.nodes.size());
|
||||
const BvhNode& node = mbvh.nodes[node_idx];
|
||||
|
||||
if (node.count > 0) {
|
||||
// Leaf: every item's AABB must be inside the node AABB.
|
||||
REQUIRE(node.count <= BVH_MAX_LEAF_SIZE);
|
||||
for (uint32_t k = 0; k < node.count; ++k) {
|
||||
uint32_t idx = mbvh.item_indices[node.right_or_first + k];
|
||||
REQUIRE(idx < items.size());
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max,
|
||||
items[idx].aabb_min, items[idx].aabb_max));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Interior: left child is at node_idx + 1, right at node.right_or_first.
|
||||
uint32_t left_idx = node_idx + 1;
|
||||
uint32_t right_idx = node.right_or_first;
|
||||
REQUIRE(left_idx < mbvh.nodes.size());
|
||||
REQUIRE(right_idx < mbvh.nodes.size());
|
||||
REQUIRE(left_idx != right_idx);
|
||||
|
||||
const BvhNode& l = mbvh.nodes[left_idx];
|
||||
const BvhNode& r = mbvh.nodes[right_idx];
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max, l.aabb_min, l.aabb_max));
|
||||
REQUIRE(aabbContains(node.aabb_min, node.aabb_max, r.aabb_min, r.aabb_max));
|
||||
REQUIRE(node.axis < 3);
|
||||
|
||||
verifyNode(mbvh, items, left_idx);
|
||||
verifyNode(mbvh, items, right_idx);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("BvhNode is 32 bytes (sidecar/cache layout invariant)", "[bvh]") {
|
||||
REQUIRE(sizeof(BvhNode) == 32);
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne on empty input produces no nodes", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
ModelBvh mbvh = buildModelBvhOne(items, /*model_id=*/42);
|
||||
REQUIRE(mbvh.model_id == 42);
|
||||
REQUIRE(mbvh.nodes.empty());
|
||||
REQUIRE(mbvh.item_indices.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne with <= BVH_MAX_LEAF_SIZE items yields a single leaf", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f));
|
||||
}
|
||||
ModelBvh mbvh = buildModelBvhOne(items, 1);
|
||||
REQUIRE(mbvh.nodes.size() == 1);
|
||||
REQUIRE(mbvh.nodes[0].count == 5);
|
||||
REQUIRE(mbvh.item_indices.size() == 5);
|
||||
verifyNode(mbvh, items, 0);
|
||||
}
|
||||
|
||||
TEST_CASE("buildModelBvhOne with many items splits and respects invariants", "[bvh]") {
|
||||
std::mt19937 rng(0xC0FFEE);
|
||||
std::uniform_real_distribution<float> coord(-100.0f, 100.0f);
|
||||
std::uniform_real_distribution<float> radius(0.1f, 1.0f);
|
||||
|
||||
constexpr int N = 256;
|
||||
std::vector<BvhItem> items;
|
||||
items.reserve(N);
|
||||
for (int i = 0; i < N; ++i) {
|
||||
items.push_back(makeItem(coord(rng), coord(rng), coord(rng), radius(rng)));
|
||||
}
|
||||
|
||||
ModelBvh mbvh = buildModelBvhOne(items, /*model_id=*/7);
|
||||
REQUIRE(mbvh.model_id == 7);
|
||||
REQUIRE(!mbvh.nodes.empty());
|
||||
REQUIRE(mbvh.item_indices.size() == N);
|
||||
|
||||
// Permutation invariant: each item must appear exactly once.
|
||||
std::vector<int> seen(N, 0);
|
||||
for (uint32_t idx : mbvh.item_indices) {
|
||||
REQUIRE(idx < uint32_t(N));
|
||||
seen[idx]++;
|
||||
}
|
||||
for (int s : seen) REQUIRE(s == 1);
|
||||
|
||||
// Recursive structural invariants.
|
||||
verifyNode(mbvh, items, 0);
|
||||
|
||||
// Sum of leaf counts must equal item count.
|
||||
uint32_t leaf_total = 0;
|
||||
for (const auto& n : mbvh.nodes) {
|
||||
if (n.count > 0) leaf_total += n.count;
|
||||
}
|
||||
REQUIRE(leaf_total == N);
|
||||
}
|
||||
|
||||
TEST_CASE("buildBvhSet partitions by model_id and gates on BVH_MIN_OBJECTS", "[bvh]") {
|
||||
// Model 1: well above BVH_MIN_OBJECTS — should get a BVH.
|
||||
// Model 2: a single item — below the gate, must be skipped.
|
||||
std::vector<BvhItem> items;
|
||||
for (uint32_t i = 0; i < BVH_MIN_OBJECTS + 4; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f, /*model_id=*/1));
|
||||
}
|
||||
items.push_back(makeItem(0.0f, 0.0f, 0.0f, 0.5f, /*model_id=*/2));
|
||||
|
||||
auto set = buildBvhSet(items);
|
||||
REQUIRE(set);
|
||||
REQUIRE(set->bvh_model_ids.count(1) == 1);
|
||||
REQUIRE(set->bvh_model_ids.count(2) == 0);
|
||||
REQUIRE(set->models.count(1) == 1);
|
||||
REQUIRE(set->models.count(2) == 0);
|
||||
|
||||
const auto& mbvh = set->models.at(1);
|
||||
REQUIRE(mbvh.model_id == 1);
|
||||
REQUIRE(mbvh.item_indices.size() == BVH_MIN_OBJECTS + 4);
|
||||
// item_indices reference positions in the *full* items array — the model-1
|
||||
// items are at indices [0, BVH_MIN_OBJECTS + 4), so every entry must be
|
||||
// less than that.
|
||||
for (uint32_t idx : mbvh.item_indices) {
|
||||
REQUIRE(idx < BVH_MIN_OBJECTS + 4);
|
||||
}
|
||||
verifyNode(mbvh, items, 0);
|
||||
}
|
||||
|
||||
TEST_CASE("buildBvhSet returns empty set when nothing meets the gate", "[bvh]") {
|
||||
std::vector<BvhItem> items;
|
||||
for (uint32_t i = 0; i < BVH_MIN_OBJECTS - 1; ++i) {
|
||||
items.push_back(makeItem(float(i), 0.0f, 0.0f, 0.5f));
|
||||
}
|
||||
auto set = buildBvhSet(items);
|
||||
REQUIRE(set);
|
||||
REQUIRE(set->bvh_model_ids.empty());
|
||||
REQUIRE(set->models.empty());
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of SelectionState — the viewport's multi-selection state
|
||||
// machine. The class also owns a GL flags SSBO, but every GL path guards on
|
||||
// a context that initializeGl() has wired up; the test never calls
|
||||
// initializeGl(), so the CPU-side selection set / active-id logic runs in
|
||||
// full isolation (gl_ stays null and bindForRender is simply not exercised).
|
||||
|
||||
#include "Selection.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
namespace {
|
||||
|
||||
// Catch2 owns main(), so QCoreApplication can't live in a TU constructor.
|
||||
// Lazily construct it (intentionally leaked) the first time any test asks.
|
||||
void ensureQApp() {
|
||||
if (QCoreApplication::instance()) return;
|
||||
static int argc = 1;
|
||||
static char arg0[] = "test_selection";
|
||||
static char* argv[] = {arg0, nullptr};
|
||||
new QCoreApplication(argc, argv);
|
||||
}
|
||||
|
||||
// First arg of the most recent changed(active_id) signal.
|
||||
uint32_t lastActive(QSignalSpy& spy) {
|
||||
REQUIRE(spy.count() > 0);
|
||||
return spy.takeLast().at(0).toUInt();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("SelectionState starts empty with no active id", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.size() == 0);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE_FALSE(sel.isSelected(1));
|
||||
}
|
||||
|
||||
TEST_CASE("setSelectedObjectId selects a single id and makes it active",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.setSelectedObjectId(5);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 1);
|
||||
REQUIRE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 5);
|
||||
REQUIRE(lastActive(spy) == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelectedObjectId(0) clears the selection", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelectedObjectId(5);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
sel.setSelectedObjectId(0);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE(lastActive(spy) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelection coerces active to 0 when it is not in the set",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
|
||||
sel.setSelection({1, 2, 3}, /*active=*/9); // 9 not in the set
|
||||
REQUIRE(sel.size() == 3);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
|
||||
sel.setSelection({1, 2, 3}, /*active=*/2); // 2 is in the set
|
||||
REQUIRE(sel.activeObjectId() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("setSelection drops object_id 0 and no-ops on identical state",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.setSelection({0, 1, 2}, /*active=*/1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 2); // id 0 stripped
|
||||
REQUIRE_FALSE(sel.isSelected(0));
|
||||
REQUIRE(sel.activeObjectId() == 1);
|
||||
|
||||
// Same set + same active — no churn.
|
||||
sel.setSelection({1, 2}, /*active=*/1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("addToSelection adds ids, keeps active, ignores 0 and no-ops",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelectedObjectId(1);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
sel.addToSelection({2, 3});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 3);
|
||||
REQUIRE(sel.activeObjectId() == 1); // active unchanged by add
|
||||
REQUIRE(lastActive(spy) == 1);
|
||||
|
||||
// Nothing new -> no signal.
|
||||
sel.addToSelection({2});
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
// id 0 is never added.
|
||||
sel.addToSelection({0});
|
||||
REQUIRE(spy.count() == 0);
|
||||
REQUIRE_FALSE(sel.isSelected(0));
|
||||
REQUIRE(sel.size() == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("removeFromSelection clears active when the active id is removed",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
sel.setSelection({1, 2, 3}, /*active=*/2);
|
||||
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
// Removing a non-active id keeps the active id.
|
||||
sel.removeFromSelection({1});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(sel.size() == 2);
|
||||
REQUIRE(sel.activeObjectId() == 2);
|
||||
|
||||
// Removing the active id drops the active.
|
||||
sel.removeFromSelection({2});
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
REQUIRE(sel.isSelected(3));
|
||||
|
||||
// Removing something absent -> no signal.
|
||||
sel.removeFromSelection({99});
|
||||
REQUIRE(spy.count() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("toggleInSelection adds-as-active then removes-and-clears-active",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.toggleInSelection(5); // add
|
||||
REQUIRE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 5); // last-toggled becomes active
|
||||
|
||||
sel.toggleInSelection(6); // add — active follows the click
|
||||
REQUIRE(sel.isSelected(6));
|
||||
REQUIRE(sel.activeObjectId() == 6);
|
||||
|
||||
sel.toggleInSelection(5); // remove non-active — active unchanged
|
||||
REQUIRE_FALSE(sel.isSelected(5));
|
||||
REQUIRE(sel.activeObjectId() == 6);
|
||||
|
||||
sel.toggleInSelection(6); // remove the active — active cleared
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
|
||||
REQUIRE(spy.count() == 4);
|
||||
|
||||
// id 0 is never toggled.
|
||||
spy.clear();
|
||||
sel.toggleInSelection(0);
|
||||
REQUIRE(spy.count() == 0);
|
||||
REQUIRE(sel.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("clearSelection empties the set; no-op when already empty",
|
||||
"[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.clearSelection(); // already empty
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
sel.setSelectedObjectId(1);
|
||||
REQUIRE(spy.count() == 1);
|
||||
sel.clearSelection();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("reset clears state and emits only when state existed", "[selection]") {
|
||||
ensureQApp();
|
||||
SelectionState sel;
|
||||
QSignalSpy spy(&sel, &SelectionState::changed);
|
||||
|
||||
sel.reset(); // nothing to clear
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
sel.setSelectedObjectId(7);
|
||||
REQUIRE(spy.count() == 1);
|
||||
sel.reset();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(sel.empty());
|
||||
REQUIRE(sel.activeObjectId() == 0);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Tier-1 coverage of VisibilityState — the per-element hidden-set tracker.
|
||||
// It is a QObject (for the changed() signal) but touches no GL, so the test
|
||||
// exercises the full state machine directly and asserts the hot-path
|
||||
// isHidden() flag mirror stays consistent with the canonical hidden set.
|
||||
|
||||
#include "Visibility.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
namespace {
|
||||
|
||||
// Catch2 owns main(), so QCoreApplication can't live in a TU constructor.
|
||||
// Lazily construct it (intentionally leaked) the first time any test asks.
|
||||
void ensureQApp() {
|
||||
if (QCoreApplication::instance()) return;
|
||||
static int argc = 1;
|
||||
static char arg0[] = "test_visibility";
|
||||
static char* argv[] = {arg0, nullptr};
|
||||
new QCoreApplication(argc, argv);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("VisibilityState starts empty", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE(vis.size() == 0);
|
||||
REQUIRE_FALSE(vis.isHidden(0)); // 0 is the "no object" sentinel
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(1u << 20));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects unions ids, emits changed, and isHidden reflects it",
|
||||
"[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
|
||||
vis.hideObjects({1, 2, 3});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.size() == 3);
|
||||
REQUIRE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE_FALSE(vis.isHidden(4));
|
||||
|
||||
// Unioning in a fresh id signals once more and grows the set.
|
||||
vis.hideObjects({2, 4}); // 2 already hidden, 4 is new
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(vis.size() == 4);
|
||||
REQUIRE(vis.isHidden(4));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects ignores object_id 0 and is idempotent", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
|
||||
vis.hideObjects({0});
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE(spy.count() == 0); // nothing changed
|
||||
REQUIRE_FALSE(vis.isHidden(0));
|
||||
|
||||
vis.hideObjects({7});
|
||||
REQUIRE(spy.count() == 1);
|
||||
vis.hideObjects({7}); // already hidden — no-op
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("showObjects subtracts and emits only when something changes",
|
||||
"[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2, 3});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.showObjects({2});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(1));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE(vis.size() == 2);
|
||||
|
||||
// Showing an id that was never hidden changes nothing.
|
||||
vis.showObjects({99});
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("setHidden replaces the set wholesale and drops id 0", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.setHidden({3, 4, 0});
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.size() == 2); // id 0 was stripped
|
||||
REQUIRE_FALSE(vis.isHidden(0));
|
||||
REQUIRE_FALSE(vis.isHidden(1)); // old members cleared
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
REQUIRE(vis.isHidden(3));
|
||||
REQUIRE(vis.isHidden(4));
|
||||
|
||||
// Replacing with an identical set is a no-op.
|
||||
vis.setHidden({3, 4});
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("showAll clears the set; no-op when already empty", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
vis.hideObjects({1, 2});
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.showAll();
|
||||
REQUIRE(spy.count() == 1);
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE_FALSE(vis.isHidden(1));
|
||||
REQUIRE_FALSE(vis.isHidden(2));
|
||||
|
||||
vis.showAll(); // already empty
|
||||
REQUIRE(spy.count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("reset clears state and emits only when state existed", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
|
||||
QSignalSpy spy(&vis, &VisibilityState::changed);
|
||||
vis.reset(); // nothing to clear
|
||||
REQUIRE(spy.count() == 0);
|
||||
|
||||
vis.hideObjects({5});
|
||||
REQUIRE(spy.count() == 1);
|
||||
vis.reset();
|
||||
REQUIRE(spy.count() == 2);
|
||||
REQUIRE(vis.empty());
|
||||
REQUIRE_FALSE(vis.isHidden(5));
|
||||
}
|
||||
|
||||
TEST_CASE("hideObjects grows the flag mirror for large object ids", "[visibility]") {
|
||||
ensureQApp();
|
||||
VisibilityState vis;
|
||||
|
||||
// A high id forces the cpu_flags_ vector (used by the hot-path isHidden)
|
||||
// to resize. isHidden must report it correctly without going out of
|
||||
// bounds, and neighbouring ids must stay visible.
|
||||
const uint32_t big = 1'000'000;
|
||||
vis.hideObjects({big});
|
||||
REQUIRE(vis.isHidden(big));
|
||||
REQUIRE_FALSE(vis.isHidden(big - 1));
|
||||
REQUIRE_FALSE(vis.isHidden(big + 1));
|
||||
REQUIRE(vis.hiddenIds().count(big) == 1);
|
||||
|
||||
vis.noteObjectId(big * 2); // pre-grow only — id stays visible
|
||||
REQUIRE_FALSE(vis.isHidden(big * 2));
|
||||
}
|
||||
Reference in New Issue
Block a user