mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 01:12:35 +00:00
Fix: Correct camera panning for the perspective view (#15212)
This commit is contained in:
+210
-51
@@ -20,6 +20,7 @@
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "3DScene.hpp"
|
||||
#include "BackgroundSlicingProcess.hpp"
|
||||
#include "CameraUtils.hpp"
|
||||
#include "GLShader.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "Tab.hpp"
|
||||
@@ -117,6 +118,36 @@ void GLCanvas3D::load_render_colors()
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static void pan_camera(Camera& camera, const Vec2d& screen_delta, const Vec3d& anchor)
|
||||
{
|
||||
// Orca: Derive world-units-per-pixel from the projection which produced the visible frame.
|
||||
// Perspective additionally scales with the eye-space depth of the point being dragged.
|
||||
const auto& viewport = camera.get_viewport();
|
||||
const auto& projection = camera.get_projection_matrix().matrix();
|
||||
const double depth_scale = camera.get_type() == Camera::EType::Perspective ?
|
||||
(anchor - camera.get_position()).dot(camera.get_dir_forward()) : 1.0;
|
||||
const double projection_x = projection(0, 0) * viewport[2];
|
||||
const double projection_y = projection(1, 1) * viewport[3];
|
||||
|
||||
// Orca: X/Y projection coefficients already include zoom. Using them directly avoids a
|
||||
// project/unproject round-trip through window depth, whose precision depends on the scene frustum.
|
||||
if (viewport[2] > 0 && viewport[3] > 0 && anchor.allFinite() && depth_scale > EPSILON &&
|
||||
std::abs(projection_x) > EPSILON && std::abs(projection_y) > EPSILON) {
|
||||
const Vec3d displacement = 2.0 * depth_scale *
|
||||
(screen_delta.y() / projection_y * camera.get_dir_up() -
|
||||
screen_delta.x() / projection_x * camera.get_dir_right());
|
||||
if (displacement.allFinite()) {
|
||||
camera.translate(displacement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Preserve the former target-plane behavior if the projection or anchor is invalid.
|
||||
// Screen Y grows downward, and the camera moves opposite to the drag.
|
||||
camera.translate(camera.get_inv_zoom() *
|
||||
(screen_delta.y() * camera.get_dir_up() - screen_delta.x() * camera.get_dir_right()));
|
||||
}
|
||||
|
||||
#ifdef __WXGTK3__
|
||||
// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support.
|
||||
RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {}
|
||||
@@ -2206,15 +2237,14 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
|
||||
_render_background();
|
||||
|
||||
//BBS add partplater rendering logic
|
||||
bool only_current = false, only_body = false, no_partplate = false;
|
||||
bool only_current = false, only_body = false;
|
||||
const bool show_bed = is_bed_visible();
|
||||
bool show_grid = true;
|
||||
GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type();
|
||||
if (!m_main_toolbar.is_enabled()) {
|
||||
//only_body = true;
|
||||
only_current = true;
|
||||
}
|
||||
else if ((gizmo_type == GLGizmosManager::FdmSupports) || (gizmo_type == GLGizmosManager::Seam) || (gizmo_type == GLGizmosManager::MmSegmentation) || (gizmo_type == GLGizmosManager::FuzzySkin))
|
||||
no_partplate = true;
|
||||
else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward())
|
||||
show_grid = false;
|
||||
if (m_axes_at_bed_center)
|
||||
@@ -2228,11 +2258,11 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
|
||||
if (m_canvas_type == ECanvasType::CanvasView3D) {
|
||||
// m_show_bed gates the plate list too: hiding the bed but leaving its grid and outline
|
||||
// floating would read as a rendering fault rather than a deliberate view option.
|
||||
if (!no_partplate && m_show_bed)
|
||||
if (show_bed)
|
||||
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
|
||||
if (!no_partplate && m_show_bed) //BBS: add outline logic
|
||||
if (show_bed) //BBS: add outline logic
|
||||
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
|
||||
if (m_axes_at_bed_center && m_show_bed && !no_partplate)
|
||||
if (m_axes_at_bed_center && show_bed)
|
||||
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
|
||||
_render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix());
|
||||
|
||||
@@ -3281,6 +3311,9 @@ void GLCanvas3D::unbind_event_handlers()
|
||||
m_canvas->Unbind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
|
||||
m_canvas->Unbind(wxEVT_GESTURE_ZOOM, &GLCanvas3D::on_gesture, this);
|
||||
m_canvas->Unbind(wxEVT_GESTURE_ROTATE, &GLCanvas3D::on_gesture, this);
|
||||
#if __WXOSX__
|
||||
initGestures(m_canvas->GetHandle(), nullptr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4029,27 +4062,38 @@ void GLCanvas3D::on_gesture(wxGestureEvent &evt)
|
||||
|
||||
auto & camera = wxGetApp().plater()->get_camera();
|
||||
if (evt.GetEventType() == wxEVT_GESTURE_PAN) {
|
||||
auto p = evt.GetPosition();
|
||||
// Orca: Gesture coordinates must use framebuffer pixels, and one stable world-space
|
||||
// anchor must be retained for the complete gesture to prevent perspective drift.
|
||||
const auto p = evt.GetPosition();
|
||||
auto d = static_cast<wxPanGestureEvent&>(evt).GetDelta();
|
||||
float z = 0;
|
||||
const Vec3d &p2 = _mouse_to_3d({p.x, p.y}, &z);
|
||||
const Vec3d &p1 = _mouse_to_3d({p.x - d.x, p.y - d.y}, &z);
|
||||
camera.set_target(camera.get_target() + p1 - p2);
|
||||
Vec2d screen_position(p.x, p.y);
|
||||
Vec2d screen_delta(d.x, d.y);
|
||||
apply_retina_scale(screen_position);
|
||||
apply_retina_scale(screen_delta);
|
||||
if (evt.IsGestureStart() || !m_gesture_pan_anchor.has_value())
|
||||
m_gesture_pan_anchor = get_camera_pan_anchor(camera, ECameraNavigationType::Gesture,
|
||||
screen_position - screen_delta);
|
||||
pan_camera(camera, screen_delta, *m_gesture_pan_anchor);
|
||||
if (evt.IsGestureEnd())
|
||||
m_gesture_pan_anchor.reset();
|
||||
} else if (evt.GetEventType() == wxEVT_GESTURE_ZOOM) {
|
||||
static float zoom_start = 1;
|
||||
if (evt.IsGestureStart())
|
||||
zoom_start = camera.get_zoom();
|
||||
camera.set_zoom(zoom_start * static_cast<wxZoomGestureEvent&>(evt).GetZoomFactor());
|
||||
} else if (evt.GetEventType() == wxEVT_GESTURE_ROTATE) {
|
||||
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
// Orca: Rotation starts a different navigation operation, so a previous pan anchor
|
||||
// must not be reused; rotation and pan fallbacks share the same navigation pivot.
|
||||
m_gesture_pan_anchor.reset();
|
||||
bool rotate_limit = current_printer_technology() != ptSLA;
|
||||
static double last_rotate = 0;
|
||||
if (evt.IsGestureStart())
|
||||
last_rotate = 0;
|
||||
auto rotate = static_cast<wxRotateGestureEvent&>(evt).GetRotationAngle() - last_rotate;
|
||||
last_rotate += rotate;
|
||||
if (plate)
|
||||
camera.rotate_on_sphere_with_target(-rotate, 0, rotate_limit, plate->get_bounding_box().center());
|
||||
const std::optional<Vec3d> rotate_target = get_camera_orbit_target(ECameraNavigationType::Gesture);
|
||||
if (rotate_target.has_value())
|
||||
camera.rotate_on_sphere_with_target(-rotate, 0, rotate_limit, *rotate_target);
|
||||
else
|
||||
camera.rotate_on_sphere(-rotate, 0, rotate_limit);
|
||||
camera.auto_type(Camera::EType::Perspective);
|
||||
@@ -4339,6 +4383,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_SWITCH_TO_GLOBAL));
|
||||
}
|
||||
else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) {
|
||||
// Orca: Retain the click position even if the first motion event crosses a surface edge.
|
||||
m_mouse.set_start_position_2D_as_invalid();
|
||||
m_mouse.drag.start_position_2D = pos;
|
||||
|
||||
//BBS: add orient deactivate logic
|
||||
if (!m_gizmos.on_mouse(evt)) {
|
||||
if (_deactivate_arrange_menu() || _deactivate_orient_menu())
|
||||
@@ -4532,6 +4580,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
}
|
||||
// do not process the dragging if the left mouse was set down in another canvas
|
||||
else if (is_camera_rotate(evt, button_mappings)) {
|
||||
// Orca: Rotation and panning use different drag coordinates and cached anchors.
|
||||
// Clear the pan state before processing rotation or switching buttons mid-drag.
|
||||
m_mouse.set_start_position_2D_as_invalid();
|
||||
|
||||
if (!has_mouse_capture()) // ORCA keep tracking mouse position while drag active and cursor not in window bounds
|
||||
m_canvas->CaptureMouse();
|
||||
@@ -4549,12 +4600,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
if (this->m_canvas_type == ECanvasType::CanvasAssembleView || m_gizmos.get_current_type() == GLGizmosManager::FdmSupports ||
|
||||
m_gizmos.get_current_type() == GLGizmosManager::Seam || m_gizmos.get_current_type() == GLGizmosManager::MmSegmentation ||
|
||||
m_gizmos.get_current_type() == GLGizmosManager::FuzzySkin) {
|
||||
Vec3d rotate_target = Vec3d::Zero();
|
||||
if (!m_selection.is_empty())
|
||||
rotate_target = m_selection.get_bounding_box().center();
|
||||
// Orca: Reuse the centralized pivot policy for scene-oriented tools.
|
||||
const std::optional<Vec3d> rotate_target = get_camera_orbit_target(ECameraNavigationType::Mouse);
|
||||
if (rotate_target.has_value())
|
||||
camera.rotate_on_sphere_with_target(rot.x(), rot.y(), false, *rotate_target);
|
||||
else
|
||||
rotate_target = volumes_bounding_box().center();
|
||||
camera.rotate_on_sphere_with_target(rot.x(), rot.y(), false, rotate_target);
|
||||
camera.rotate_on_sphere(rot.x(), rot.y(), false);
|
||||
}
|
||||
else {
|
||||
if (wxGetApp().app_config->get_bool("use_free_camera"))
|
||||
@@ -4578,28 +4629,11 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
}
|
||||
camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, m_rotation_center);
|
||||
} else {
|
||||
Vec3d rotate_target = Vec3d::Zero();
|
||||
if (m_canvas_type == ECanvasType::CanvasPreview) {
|
||||
PartPlate *plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
if (plate)
|
||||
rotate_target = plate->get_bounding_box().center();
|
||||
}
|
||||
else {
|
||||
if (!m_selection.is_empty())
|
||||
rotate_target = m_selection.get_bounding_box().center();
|
||||
else {
|
||||
// Rotate around the center of objects on current plate
|
||||
auto bbox = volumes_bounding_box(true);
|
||||
if (!bbox.defined) {
|
||||
// Rotate around current plate center if current plate is empty
|
||||
bbox = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_bounding_box();
|
||||
}
|
||||
rotate_target = bbox.center();
|
||||
}
|
||||
}
|
||||
|
||||
if (!rotate_target.isZero())
|
||||
camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, rotate_target);
|
||||
// Orca: Keep regular mouse orbit and perspective-pan fallback centered
|
||||
// on the same selection, active-plate, or scene reference.
|
||||
const std::optional<Vec3d> rotate_target = get_camera_orbit_target(ECameraNavigationType::Mouse);
|
||||
if (rotate_target.has_value())
|
||||
camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, *rotate_target);
|
||||
else
|
||||
camera.rotate_on_sphere(rot.x(), rot.y(), rotate_limit);
|
||||
}
|
||||
@@ -4615,16 +4649,14 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
m_mouse.drag.start_position_3D = Vec3d((double)pos(0), (double)pos(1), 0.0);
|
||||
}
|
||||
else if (is_camera_pan(evt, button_mappings)) {
|
||||
// Orca: Pan uses screen coordinates and must not inherit the rotation start point.
|
||||
m_mouse.set_start_position_3D_as_invalid();
|
||||
|
||||
if (!has_mouse_capture()) // ORCA keep tracking mouse position while drag active and cursor not in window bounds
|
||||
m_canvas->CaptureMouse();
|
||||
|
||||
// if dragging with right button or if button functions swapped and dragging with left button over blank area then pan
|
||||
if (m_mouse.is_start_position_2D_defined()) {
|
||||
// get point in model space at Z = 0
|
||||
float z = 0.0f;
|
||||
const Vec3d& cur_pos = _mouse_to_3d(pos, &z);
|
||||
Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z);
|
||||
Camera& camera = wxGetApp().plater()->get_camera();
|
||||
if (this->m_canvas_type != ECanvasType::CanvasAssembleView) {
|
||||
// Orca: Use a constrained camera when navigating the 3D scene with a regular mouse, if the free camera is not selected
|
||||
@@ -4636,7 +4668,14 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
camera.recover_from_free_camera();
|
||||
}
|
||||
|
||||
camera.set_target(camera.get_target() + orig - cur_pos);
|
||||
// Orca: Cache the surface under the initial click and apply every incremental
|
||||
// cursor delta at that depth, so perspective zoom and camera angle stay exact.
|
||||
const Vec2d screen_delta =
|
||||
pos.cast<double>() - m_mouse.drag.start_position_2D.cast<double>();
|
||||
if (!m_mouse.drag.camera_pan_anchor.has_value())
|
||||
m_mouse.drag.camera_pan_anchor = get_camera_pan_anchor(camera, ECameraNavigationType::Mouse,
|
||||
m_mouse.drag.start_position_2D.cast<double>());
|
||||
pan_camera(camera, screen_delta, *m_mouse.drag.camera_pan_anchor);
|
||||
m_dirty = true;
|
||||
m_mouse.ignore_right_up = true; // will be reset on button up event even if not right button is pressed
|
||||
}
|
||||
@@ -7329,11 +7368,8 @@ void GLCanvas3D::_picking_pass()
|
||||
m_hover_volume_idxs.clear();
|
||||
m_hover_plate_idxs.clear();
|
||||
|
||||
// Orca: ignore clipping plane if not applying
|
||||
GLGizmoBase *current_gizmo = m_gizmos.get_current();
|
||||
const ClippingPlane clipping_plane = ((!current_gizmo || current_gizmo->apply_clipping_plane()) ? m_gizmos.get_clipping_plane() :
|
||||
ClippingPlane::ClipsNothing())
|
||||
.inverted_normal();
|
||||
// Orca: Picking and camera navigation must interpret the active gizmo clipping plane identically.
|
||||
const ClippingPlane clipping_plane = get_raycaster_clipping_plane();
|
||||
const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(m_mouse.position, wxGetApp().plater()->get_camera(), &clipping_plane);
|
||||
if (hit.is_valid()) {
|
||||
switch (hit.type)
|
||||
@@ -10627,6 +10663,129 @@ Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos)
|
||||
return mouse_ray(mouse_pos).intersect_plane(0.0);
|
||||
}
|
||||
|
||||
ClippingPlane GLCanvas3D::get_raycaster_clipping_plane() const
|
||||
{
|
||||
// Orca: Ignore the gizmo clipping plane when the active tool does not apply it, and
|
||||
// invert the result into the convention expected by SceneRaycaster.
|
||||
GLGizmoBase* current_gizmo = m_gizmos.get_current();
|
||||
return ((!current_gizmo || current_gizmo->apply_clipping_plane()) ? m_gizmos.get_clipping_plane() :
|
||||
ClippingPlane::ClipsNothing())
|
||||
.inverted_normal();
|
||||
}
|
||||
|
||||
std::optional<Vec3d> GLCanvas3D::get_camera_orbit_target(ECameraNavigationType navigation_type) const
|
||||
{
|
||||
// Orca: Centralize the pre-existing pivot rules so orbiting and pan fallback cannot
|
||||
// choose different reference depths for the same canvas and active tool.
|
||||
PartPlate* current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
if (navigation_type == ECameraNavigationType::Gesture)
|
||||
return current_plate == nullptr ? std::nullopt :
|
||||
std::make_optional(current_plate->get_bounding_box().center());
|
||||
|
||||
const GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type();
|
||||
const bool use_scene_target = m_canvas_type == ECanvasType::CanvasAssembleView ||
|
||||
gizmo_type == GLGizmosManager::FdmSupports || gizmo_type == GLGizmosManager::Seam ||
|
||||
gizmo_type == GLGizmosManager::MmSegmentation || gizmo_type == GLGizmosManager::FuzzySkin;
|
||||
if (use_scene_target) {
|
||||
if (!m_selection.is_empty())
|
||||
return m_selection.get_bounding_box().center();
|
||||
|
||||
// Orca: Preserve the world-origin fallback used by orbit in an empty scene.
|
||||
return volumes_bounding_box().center();
|
||||
}
|
||||
|
||||
// Orca: Free-camera rotation uses Camera::m_target rather than a plate or selection pivot.
|
||||
if (wxGetApp().app_config->get_bool("use_free_camera"))
|
||||
return std::nullopt;
|
||||
|
||||
Vec3d target = Vec3d::Zero();
|
||||
if (m_canvas_type == ECanvasType::CanvasPreview) {
|
||||
if (current_plate != nullptr)
|
||||
target = current_plate->get_bounding_box().center();
|
||||
} else if (!m_selection.is_empty()) {
|
||||
target = m_selection.get_bounding_box().center();
|
||||
} else {
|
||||
// Orca: Match regular mouse orbit: objects on the active plate, then the plate itself.
|
||||
BoundingBoxf3 bbox = volumes_bounding_box(true);
|
||||
if (!bbox.defined && current_plate != nullptr)
|
||||
bbox = current_plate->get_bounding_box();
|
||||
if (bbox.defined)
|
||||
target = bbox.center();
|
||||
}
|
||||
|
||||
// Orca: Preserve the existing zero sentinel used by regular mouse orbit.
|
||||
return target.isZero() ? std::nullopt : std::make_optional(target);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::is_bed_visible() const
|
||||
{
|
||||
if (m_canvas_type == ECanvasType::CanvasPreview)
|
||||
return m_render_preview;
|
||||
if (m_canvas_type != ECanvasType::CanvasView3D || !m_show_bed)
|
||||
return false;
|
||||
if (!m_main_toolbar.is_enabled())
|
||||
return true;
|
||||
|
||||
const auto type = m_gizmos.get_current_type();
|
||||
return type != GLGizmosManager::FdmSupports && type != GLGizmosManager::Seam &&
|
||||
type != GLGizmosManager::MmSegmentation && type != GLGizmosManager::FuzzySkin;
|
||||
}
|
||||
|
||||
Vec3d GLCanvas3D::get_camera_pan_anchor(Camera& camera, ECameraNavigationType navigation_type,
|
||||
const Vec2d& screen_position) const
|
||||
{
|
||||
// Orthographic panning has the same scale at every depth, so no raycast is needed.
|
||||
if (camera.get_type() != Camera::EType::Perspective)
|
||||
return camera.get_target();
|
||||
|
||||
// Orca: Reject non-finite anchors and points behind the camera before their depth is
|
||||
// allowed to scale a perspective pan.
|
||||
const Vec3d camera_position = camera.get_position();
|
||||
const Vec3d camera_forward = camera.get_dir_forward();
|
||||
const auto is_valid_anchor = [&camera_position, &camera_forward](const Vec3d& anchor) {
|
||||
return anchor.allFinite() && (anchor - camera_position).dot(camera_forward) > EPSILON;
|
||||
};
|
||||
|
||||
// Orca: Prefer the nearest visible bed or volume surface and exclude gizmos and
|
||||
// selected-volume picking priority from navigation depth selection.
|
||||
const ClippingPlane clipping_plane = get_raycaster_clipping_plane();
|
||||
const bool bed_visible = is_bed_visible();
|
||||
const SceneRaycaster::HitResult hit = m_scene_raycaster.hit(screen_position, camera, &clipping_plane,
|
||||
bed_visible ? SceneRaycaster::EHitMode::SceneOnly : SceneRaycaster::EHitMode::VolumesOnly);
|
||||
if (hit.is_valid()) {
|
||||
const Vec3d hit_position = hit.position.cast<double>();
|
||||
if (is_valid_anchor(hit_position))
|
||||
return hit_position;
|
||||
}
|
||||
|
||||
// Orca: When the cursor is just outside the visible plate, use the point under it on the active
|
||||
// plate plane. Using the plate center here would give it a different perspective depth.
|
||||
PartPlate* current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
// Orca: An almost edge-on perspective makes intersection depth extremely sensitive to
|
||||
// the cursor's vertical position. Use the stable orbit depth around horizontal views.
|
||||
static constexpr double min_plate_plane_forward_z = 0.05;
|
||||
if (bed_visible && std::abs(camera_forward.z()) >= min_plate_plane_forward_z &&
|
||||
current_plate != nullptr && current_plate->get_bounding_box().defined) {
|
||||
Vec3d ray_origin;
|
||||
Vec3d ray_direction;
|
||||
CameraUtils::ray_from_screen_pos(camera, screen_position, ray_origin, ray_direction);
|
||||
const double z_direction = ray_direction.z();
|
||||
if (ray_origin.allFinite() && ray_direction.allFinite() && std::abs(z_direction) > EPSILON) {
|
||||
const double plate_z = current_plate->get_bounding_box().center().z();
|
||||
const double distance = (plate_z - ray_origin.z()) / z_direction;
|
||||
const Vec3d plate_position = ray_origin + distance * ray_direction;
|
||||
const double eye_depth = (plate_position - camera_position).dot(camera_forward);
|
||||
if (distance >= 0.0 && is_valid_anchor(plate_position) &&
|
||||
eye_depth >= camera.get_near_z() && eye_depth <= camera.get_far_z())
|
||||
return plate_position;
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Near-horizontal rays and points outside the scene depth use the orbit reference point.
|
||||
const std::optional<Vec3d> orbit_target = get_camera_orbit_target(navigation_type);
|
||||
return orbit_target.has_value() && is_valid_anchor(*orbit_target) ? *orbit_target : camera.get_target();
|
||||
}
|
||||
|
||||
// While it looks like we can call
|
||||
// this->reload_scene(true, true)
|
||||
// the two functions are quite different:
|
||||
|
||||
@@ -338,6 +338,8 @@ class GLCanvas3D
|
||||
int move_volume_idx{ -1 };
|
||||
bool move_requires_threshold{ false };
|
||||
Point move_start_threshold_position_2D{ Invalid_2D_Point };
|
||||
// Orca: Keep the world-space point selected at the start of a mouse pan.
|
||||
std::optional<Vec3d> camera_pan_anchor;
|
||||
};
|
||||
|
||||
bool dragging{ false };
|
||||
@@ -346,7 +348,12 @@ class GLCanvas3D
|
||||
Drag drag;
|
||||
bool ignore_right_up;
|
||||
|
||||
void set_start_position_2D_as_invalid() { drag.start_position_2D = Drag::Invalid_2D_Point; }
|
||||
// Orca: The screen-space start and world-space anchor describe the same pan session.
|
||||
// Invalidating one must invalidate the other so a new drag cannot reuse stale depth.
|
||||
void set_start_position_2D_as_invalid() {
|
||||
drag.start_position_2D = Drag::Invalid_2D_Point;
|
||||
drag.camera_pan_anchor.reset();
|
||||
}
|
||||
void set_start_position_3D_as_invalid() { drag.start_position_3D = Drag::Invalid_3D_Point; }
|
||||
void set_move_start_threshold_position_2D_as_invalid() { drag.move_start_threshold_position_2D = Drag::Invalid_2D_Point; }
|
||||
|
||||
@@ -549,6 +556,8 @@ private:
|
||||
bool m_fps_overlay_tick{ false };
|
||||
LayersEditing m_layers_editing;
|
||||
Mouse m_mouse;
|
||||
// Orca: Gesture pans have their own lifecycle and stable world-space anchor.
|
||||
std::optional<Vec3d> m_gesture_pan_anchor;
|
||||
GLGizmosManager m_gizmos;
|
||||
//BBS: GUI refactor: GLToolbar
|
||||
mutable GLToolbar m_main_toolbar;
|
||||
@@ -1406,6 +1415,20 @@ private:
|
||||
// Convert the screen space coordinate to world coordinate on the bed.
|
||||
Vec3d _mouse_to_bed_3d(const Point& mouse_pos);
|
||||
|
||||
// Orca: Navigation type selects the legacy pivot policy used when no visible surface is hit.
|
||||
enum class ECameraNavigationType : unsigned char
|
||||
{
|
||||
Mouse,
|
||||
Gesture
|
||||
};
|
||||
|
||||
// Orca: These helpers keep clipping, orbit pivots, and perspective-pan depth selection consistent.
|
||||
ClippingPlane get_raycaster_clipping_plane() const;
|
||||
bool is_bed_visible() const;
|
||||
std::optional<Vec3d> get_camera_orbit_target(ECameraNavigationType navigation_type) const;
|
||||
Vec3d get_camera_pan_anchor(Camera& camera, ECameraNavigationType navigation_type,
|
||||
const Vec2d& screen_position) const;
|
||||
|
||||
void _start_timer() { m_timer.Start(100, wxTIMER_CONTINUOUS); }
|
||||
void _stop_timer() { m_timer.Stop(); }
|
||||
|
||||
|
||||
@@ -99,8 +99,11 @@ void SceneRaycaster::remove_raycaster(std::shared_ptr<SceneRaycasterItem> item)
|
||||
}
|
||||
}
|
||||
|
||||
SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane) const
|
||||
SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Camera& camera,
|
||||
const ClippingPlane* clipping_plane, SceneRaycaster::EHitMode mode) const
|
||||
{
|
||||
// Orca: Picking may favor an already selected volume for interaction, while camera
|
||||
// navigation must always use the geometrically closest visible scene surface.
|
||||
// helper class used to return currently selected volume as hit when overlapping with other volumes
|
||||
// to allow the user to click and drag on a selected volume
|
||||
class VolumeKeeper
|
||||
@@ -110,7 +113,11 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came
|
||||
bool m_selected_volume_already_found{ false };
|
||||
|
||||
public:
|
||||
VolumeKeeper() {
|
||||
explicit VolumeKeeper(bool enabled) {
|
||||
// Orca: Disable selected-volume bias for navigation raycasts.
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
const Selection& selection = wxGetApp().plater()->get_selection();
|
||||
if (selection.is_single_volume() || selection.is_single_modifier()) {
|
||||
const GLVolume* volume = selection.get_first_volume();
|
||||
@@ -135,7 +142,7 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came
|
||||
}
|
||||
};
|
||||
|
||||
VolumeKeeper volume_keeper;
|
||||
VolumeKeeper volume_keeper(mode == EHitMode::Picking);
|
||||
|
||||
double closest_hit_squared_distance = std::numeric_limits<double>::max();
|
||||
auto is_closest = [&closest_hit_squared_distance, &volume_keeper](const Camera& camera, const Vec3f& hit) {
|
||||
@@ -154,7 +161,7 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came
|
||||
|
||||
HitResult ret;
|
||||
|
||||
auto test_raycasters = [this, is_closest, clipping_plane, &volume_keeper](EType type, const Vec2d& mouse_pos, const Camera& camera, HitResult& ret) {
|
||||
auto test_raycasters = [this, is_closest, clipping_plane, mode, &volume_keeper](EType type, const Vec2d& mouse_pos, const Camera& camera, HitResult& ret) {
|
||||
const ClippingPlane* clip_plane = (clipping_plane != nullptr && type == EType::Volume) ? clipping_plane : nullptr;
|
||||
const std::vector<std::shared_ptr<SceneRaycasterItem>>* raycasters = get_raycasters(type);
|
||||
const Vec3f camera_forward = camera.get_dir_forward().cast<float>();
|
||||
@@ -163,12 +170,22 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came
|
||||
if (!item->is_active())
|
||||
continue;
|
||||
|
||||
// Each plate's component 0 is its surface; the remaining Bed IDs are controls.
|
||||
// Keep controls clickable, but do not use them as camera-pan anchors.
|
||||
if (mode != EHitMode::Picking && type == EType::Bed &&
|
||||
decode_id(type, item->get_id()) % PartPlate::GRABBER_COUNT != 0)
|
||||
continue;
|
||||
|
||||
current_hit.raycaster_id = item->get_id();
|
||||
const Transform3d& trafo = item->get_transform();
|
||||
if (item->get_raycaster()->closest_hit(mouse_pos, trafo, camera, current_hit.position, current_hit.normal, clip_plane)) {
|
||||
current_hit.position = (trafo * current_hit.position.cast<double>()).cast<float>();
|
||||
current_hit.normal = (trafo.matrix().block(0, 0, 3, 3).inverse().transpose() * current_hit.normal.cast<double>()).normalized().cast<float>();
|
||||
if (item->use_back_faces() || current_hit.normal.dot(camera_forward) < 0.0f) {
|
||||
// Orca: Perspective rays away from the viewport center are not parallel to camera_forward.
|
||||
// Keep picking's legacy policy, but accept every front-facing navigation surface.
|
||||
const Vec3f view_direction = mode != EHitMode::Picking && camera.get_type() == Camera::EType::Perspective ?
|
||||
Vec3f((current_hit.position.cast<double>() - camera.get_position()).cast<float>()) : camera_forward;
|
||||
if (item->use_back_faces() || current_hit.normal.dot(view_direction) < 0.0f) {
|
||||
if (is_closest(camera, current_hit.position)) {
|
||||
if (volume_keeper.is_active()) {
|
||||
if (volume_keeper.check_hit_result(current_hit))
|
||||
@@ -182,14 +199,18 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came
|
||||
}
|
||||
};
|
||||
|
||||
if (!m_gizmos.empty())
|
||||
test_raycasters(EType::Gizmo, mouse_pos, camera, ret);
|
||||
// Orca: Gizmo geometry is an interaction target, not a valid depth anchor for camera movement.
|
||||
if (mode == EHitMode::Picking) {
|
||||
if (!m_gizmos.empty())
|
||||
test_raycasters(EType::Gizmo, mouse_pos, camera, ret);
|
||||
|
||||
if (!m_fallback_gizmos.empty() && !ret.is_valid())
|
||||
test_raycasters(EType::FallbackGizmo, mouse_pos, camera, ret);
|
||||
if (!m_fallback_gizmos.empty() && !ret.is_valid())
|
||||
test_raycasters(EType::FallbackGizmo, mouse_pos, camera, ret);
|
||||
}
|
||||
|
||||
if (!m_gizmos_on_top || !ret.is_valid()) {
|
||||
if (camera.is_looking_downward() && !m_bed.empty())
|
||||
// Orca: In perspective the bottom of the viewport can see the bed even at a horizontal view.
|
||||
if ((mode == EHitMode::SceneOnly || (mode == EHitMode::Picking && camera.is_looking_downward())) && !m_bed.empty())
|
||||
test_raycasters(EType::Bed, mouse_pos, camera, ret);
|
||||
if (!m_volumes.empty())
|
||||
test_raycasters(EType::Volume, mouse_pos, camera, ret);
|
||||
|
||||
@@ -57,6 +57,15 @@ public:
|
||||
FallbackGizmo = 2000000
|
||||
};
|
||||
|
||||
// Orca: Navigation needs the closest visible scene surface, without picking-specific
|
||||
// gizmo and selected-volume priority.
|
||||
enum class EHitMode : unsigned char
|
||||
{
|
||||
Picking,
|
||||
SceneOnly,
|
||||
VolumesOnly // Navigation when the bed is hidden.
|
||||
};
|
||||
|
||||
struct HitResult
|
||||
{
|
||||
EType type{ EType::None };
|
||||
@@ -97,7 +106,8 @@ public:
|
||||
|
||||
void set_gizmos_on_top(bool value) { m_gizmos_on_top = value; }
|
||||
|
||||
HitResult hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane = nullptr) const;
|
||||
HitResult hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane = nullptr,
|
||||
EHitMode mode = EHitMode::Picking) const;
|
||||
|
||||
#if ENABLE_RAYCAST_PICKING_DEBUG
|
||||
void render_hit(const Camera& camera);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#import "MacDarkMode.hpp"
|
||||
#include "../GUI/Widgets/Label.hpp"
|
||||
|
||||
#include "wx/graphics.h"
|
||||
#include "wx/osx/core/cfstring.h"
|
||||
#include "wx/osx/private.h"
|
||||
|
||||
#import <algorithm>
|
||||
|
||||
@@ -334,11 +336,28 @@ bool addObserver = false;
|
||||
}
|
||||
@end
|
||||
|
||||
// Orca: A Shift-trackpad pan belongs to one GL view; sharing its lifecycle across views
|
||||
// could make a gesture reuse another canvas's world-space anchor.
|
||||
static char scroll_pan_active_key;
|
||||
static char gesture_handler_key;
|
||||
|
||||
static wxEvtHandler* get_gesture_handler(NSView* view)
|
||||
{
|
||||
return static_cast<wxEvtHandler*>([objc_getAssociatedObject(view, &gesture_handler_key) pointerValue]);
|
||||
}
|
||||
|
||||
static bool is_scroll_pan_active(NSView* view)
|
||||
{
|
||||
return [objc_getAssociatedObject(view, &scroll_pan_active_key) boolValue];
|
||||
}
|
||||
|
||||
static void set_scroll_pan_active(NSView* view, bool active)
|
||||
{
|
||||
objc_setAssociatedObject(view, &scroll_pan_active_key, active ? @YES : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
@implementation wxNSCustomOpenGLView (Gesture)
|
||||
|
||||
wxEvtHandler * _gestureHandler = nullptr;
|
||||
|
||||
- (void) onGestureMove: (NSPanGestureRecognizer*) gesture
|
||||
{
|
||||
wxPanGestureEvent evt;
|
||||
@@ -364,22 +383,43 @@ wxEvtHandler * _gestureHandler = nullptr;
|
||||
- (void) postEvent: (wxGestureEvent &) evt withGesture: (NSGestureRecognizer* ) gesture
|
||||
{
|
||||
NSPoint pos = [gesture locationInView: self];
|
||||
evt.SetPosition({(int) pos.x, (int) pos.y});
|
||||
evt.SetPosition(wxFromNSPoint(self, pos));
|
||||
if (gesture.state == NSGestureRecognizerStateBegan)
|
||||
evt.SetGestureStart();
|
||||
else if (gesture.state == NSGestureRecognizerStateEnded)
|
||||
evt.SetGestureEnd();
|
||||
_gestureHandler->ProcessEvent(evt);
|
||||
if (wxEvtHandler* handler = get_gesture_handler(self))
|
||||
handler->ProcessEvent(evt);
|
||||
}
|
||||
|
||||
- (void) scrollWheel2:(NSEvent *)event
|
||||
{
|
||||
bool shiftDown = [event modifierFlags] & NSShiftKeyMask;
|
||||
if (_gestureHandler && shiftDown && event.hasPreciseScrollingDeltas) {
|
||||
wxEvtHandler* handler = get_gesture_handler(self);
|
||||
if (handler && shiftDown && event.hasPreciseScrollingDeltas) {
|
||||
wxPanGestureEvent evt;
|
||||
evt.SetDelta({-(int)[event scrollingDeltaX], - (int)[event scrollingDeltaY]});
|
||||
_gestureHandler->ProcessEvent(evt);
|
||||
// NSOpenGLView uses bottom-left coordinates; wx gestures use top-left coordinates.
|
||||
const wxPoint pos = wxFromNSPoint(self, [self convertPoint:[event locationInWindow] fromView:nil]);
|
||||
const wxPoint delta(-(int)[event scrollingDeltaX], -(int)[event scrollingDeltaY]);
|
||||
// Orca: GLCanvas3D derives the anchor position as position - delta, so synthesize
|
||||
// the post-delta position from the native cursor coordinate.
|
||||
evt.SetPosition(pos + delta);
|
||||
evt.SetDelta(delta);
|
||||
// Orca: Preserve the anchor throughout a trackpad scroll, including its momentum events.
|
||||
// Keep it after phase Ended: momentum may follow. The next Began replaces it.
|
||||
const NSEventPhase phase = event.phase;
|
||||
const NSEventPhase momentum_phase = event.momentumPhase;
|
||||
const bool unphased = phase == NSEventPhaseNone && momentum_phase == NSEventPhaseNone;
|
||||
if (!is_scroll_pan_active(self) || unphased || (phase & (NSEventPhaseMayBegin | NSEventPhaseBegan)))
|
||||
evt.SetGestureStart();
|
||||
if (unphased || (phase & NSEventPhaseCancelled) ||
|
||||
(momentum_phase & (NSEventPhaseEnded | NSEventPhaseCancelled)))
|
||||
evt.SetGestureEnd();
|
||||
set_scroll_pan_active(self, !evt.IsGestureEnd());
|
||||
handler->ProcessEvent(evt);
|
||||
} else {
|
||||
// Orca: Switching away from Shift-pan must not reuse its depth when Shift is pressed again.
|
||||
set_scroll_pan_active(self, false);
|
||||
[self scrollWheel2: event];
|
||||
}
|
||||
}
|
||||
@@ -401,7 +441,9 @@ wxEvtHandler * _gestureHandler = nullptr;
|
||||
// [self addGestureRecognizer:pan];
|
||||
// [self addGestureRecognizer:magnification];
|
||||
// [self addGestureRecognizer:rotation];
|
||||
_gestureHandler = handler;
|
||||
objc_setAssociatedObject(self, &gesture_handler_key, handler ? [NSValue valueWithPointer:handler] : nil,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
set_scroll_pan_active(self, false);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_creality_cfs_match.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_scene_raycaster.cpp
|
||||
test_lazy.cpp
|
||||
test_prebuild_queue.cpp
|
||||
test_staged_build.cpp
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// 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));
|
||||
}
|
||||
Reference in New Issue
Block a user