Files
OrcaSlicer/tests/slic3rutils/test_scene_raycaster.cpp
T

179 lines
8.2 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_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));
}