Fix: Correct camera panning for the perspective view (#15212)

This commit is contained in:
Valerii Bokhan
2026-09-23 20:38:26 -03:00
committed by GitHub
parent 02746cd0f5
commit 022885a250
7 changed files with 505 additions and 71 deletions
+210 -51
View File
@@ -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:
+24 -1
View File
@@ -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(); }
+31 -10
View File
@@ -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);
+11 -1
View File
@@ -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);
+50 -8
View File
@@ -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
+1
View File
@@ -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
+178
View File
@@ -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));
}