Files
OrcaSlicer/tests/slic3rutils/test_scene_raycaster.cpp
T
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00

189 lines
8.6 KiB
C++

// Orca: This suite links libslic3r_gui; navigation raycasts need no wx application or GL context.
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
// Match the GUI precompiled header: wx/msw/wrapcctl.h needs HDITEM from CommCtrl.h.
#include <CommCtrl.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include "slic3r/GUI/MeshUtils.hpp"
#include "libslic3r/Point.hpp"
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Geometry.hpp"
#include <catch2/generators/catch_generators_range.hpp>
#include <catch2/catch_all.hpp>
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/CameraUtils.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/SceneRaycaster.hpp"
using namespace Slic3r;
using namespace Slic3r::GUI;
namespace {
Camera horizontal_camera(Camera::EType type = Camera::EType::Perspective)
{
Camera camera;
camera.set_type(type);
camera.look_at({0.0, 0.0, 10.0}, {0.0, 100.0, 10.0}, Vec3d::UnitZ());
camera.set_viewport(0, 0, 600, 600);
camera.apply_projection(-1.0, 1.0, -1.0, 1.0, 1.0, 1000.0);
return camera;
}
SceneRaycaster::HitResult scene_hit(const SceneRaycaster& scene, const Camera& camera, const Vec3d& point,
SceneRaycaster::EHitMode mode = SceneRaycaster::EHitMode::SceneOnly)
{
return scene.hit(CameraUtils::project(camera, point).cast<double>(), camera, nullptr, mode);
}
} // namespace
TEST_CASE("Navigation hits the visible bed below a horizontal perspective view", "[SceneRaycaster][Regression]")
{
const MeshRaycaster bed(TriangleMesh(
{{-100.f, 20.f, 0.f}, {100.f, 20.f, 0.f}, {100.f, 200.f, 0.f}, {-100.f, 200.f, 0.f}},
{{0, 1, 2}, {0, 2, 3}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, 0, bed, Transform3d::Identity());
const Camera camera = horizontal_camera();
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 0.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Bed);
CHECK_THAT(hit.position.z(), Catch::Matchers::WithinAbs(0.0, 1e-4));
}
TEST_CASE("Navigation hits a visible side face away from the perspective view center", "[SceneRaycaster][Regression]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster side(TriangleMesh(
{{10.f, 20.f, -100.f}, {10.f, 20.f, 100.f}, {10.f, 200.f, 100.f}, {10.f, 200.f, -100.f}},
{{0, 1, 2}, {0, 2, 3}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, side, Transform3d::Identity());
const Camera camera = horizontal_camera();
const auto hit = scene_hit(scene, camera, {10.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
CHECK_THAT(hit.position.x(), Catch::Matchers::WithinAbs(10.0, 1e-4));
}
TEST_CASE("Navigation ignores gizmos and inactive volumes and chooses the nearest scene surface", "[SceneRaycaster]")
{
const bool gizmos_on_top = GENERATE(false, true);
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const auto type = GENERATE(Camera::EType::Perspective, Camera::EType::Ortho);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.set_gizmos_on_top(gizmos_on_top);
scene.add_raycaster(SceneRaycaster::EType::Gizmo, 0, cube, Geometry::translation_transform({-10.0, 20.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::FallbackGizmo, 0, cube, Geometry::translation_transform({-10.0, 30.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 40.0, 0.0}))->set_active(false);
scene.add_raycaster(SceneRaycaster::EType::Volume, 1, cube, Geometry::translation_transform({-10.0, 150.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 2, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
const Camera camera = horizontal_camera(type);
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
CHECK(hit.raycaster_id == 2);
CHECK_THAT(hit.position.y(), Catch::Matchers::WithinAbs(100.0, 1e-4));
}
TEST_CASE("Navigation skips bed raycasters when the bed is hidden", "[SceneRaycaster][Regression]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const auto type = GENERATE(Camera::EType::Perspective, Camera::EType::Ortho);
const bool looking_downward = GENERATE(false, true);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, 0, cube, Geometry::translation_transform({-10.0, 40.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
Camera camera = horizontal_camera(type);
if (looking_downward)
camera.look_at({0.0, 0.0, 10.0}, {0.0, 100.0, 0.0}, Vec3d::UnitZ());
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == (mode == SceneRaycaster::EHitMode::SceneOnly ?
SceneRaycaster::EType::Bed : SceneRaycaster::EType::Volume));
scene.remove_raycasters(SceneRaycaster::EType::Volume);
CHECK(scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode).is_valid() ==
(mode == SceneRaycaster::EHitMode::SceneOnly));
}
TEST_CASE("Navigation ignores plate controls while retaining plate surfaces and volumes", "[SceneRaycaster][Regression]")
{
const int plate_index = GENERATE(0, 2);
const int component = GENERATE(range(1, int(PartPlate::GRABBER_COUNT)));
const int bed_id = plate_index * PartPlate::GRABBER_COUNT;
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, bed_id + component, cube,
Geometry::translation_transform({-10.0, 40.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Bed, bed_id, cube,
Geometry::translation_transform({-10.0, 100.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube,
Geometry::translation_transform({-10.0, 150.0, 0.0}));
const Camera camera = horizontal_camera();
auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Bed);
CHECK(hit.raycaster_id == bed_id);
scene.remove_raycasters(SceneRaycaster::EType::Bed, bed_id);
hit = scene_hit(scene, camera, {0.0, 100.0, 10.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
// A control alone must leave navigation free to choose its fallback anchor.
scene.remove_raycasters(SceneRaycaster::EType::Volume);
CHECK_FALSE(scene_hit(scene, camera, {0.0, 100.0, 10.0}).is_valid());
}
TEST_CASE("Navigation respects the back-face policy away from the perspective view center", "[SceneRaycaster]")
{
const bool use_back_faces = GENERATE(false, true);
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster side(TriangleMesh(
{{10.f, 20.f, -100.f}, {10.f, 20.f, 100.f}, {10.f, 200.f, 100.f}, {10.f, 200.f, -100.f}},
{{0, 2, 1}, {0, 3, 2}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, side, Transform3d::Identity(), use_back_faces);
const auto hit = scene_hit(scene, horizontal_camera(), {10.0, 100.0, 10.0}, mode);
CHECK(hit.is_valid() == use_back_faces);
}
TEST_CASE("Navigation ignores volume surfaces removed by the clipping plane", "[SceneRaycaster]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 1, cube, Geometry::translation_transform({-10.0, 150.0, 0.0}));
const Camera camera = horizontal_camera();
const ClippingPlane clipping_plane(-Vec3d::UnitY(), -130.0);
const auto hit = scene.hit({300.0, 300.0}, camera, &clipping_plane, mode);
REQUIRE(hit.is_valid());
CHECK(hit.raycaster_id == 1);
CHECK_THAT(hit.position.y(), Catch::Matchers::WithinAbs(150.0, 1e-4));
}