Files
FreeCAD/src/Gui/Navigation/NavigationStyle.cpp
T
Maik-0000FF 687c75e5bc Gui: Fix Linux SpaceMouse performance: event coalescing, camera batching, per-axis deadzone (#28110)
* Fix Linux SpaceMouse jerkiness: coalesce spnav events and batch camera updates

On Linux, pollSpacenav() posts every spnav motion event individually
(125-250Hz from spacenavd), each triggering a full scene redraw. Combined
with camera->orientation and camera->position each triggering separate
Coin3D notifications, this results in up to 500 redraws/sec.

Fix 1 - Event coalescing (GuiNativeEventLinux.cpp):
The drain loop now keeps only the latest motion state and posts once after
all pending events are consumed. SpaceMouse events are absolute deflection
(current puck position), not deltas, so the latest event always contains
the complete state.

Fix 2 - Batched camera updates (NavigationStyle.cpp):
Wrap camera property changes in enableNotify(false/true) + touch() so
Coin3D fires a single redraw instead of two per event.

Together this reduces redraws from ~500/sec to ~60/sec (display refresh),
eliminating the jerky navigation that Linux users experience with complex
models.

Fixes #25926

* Add per-axis deadzone filtering for Linux SpaceMouse

Each SpaceMouse axis has different sensitivity and noise characteristics.
A single global deadzone is too coarse — rotation axes may need different
thresholds than translation axes.

Read per-axis deadzone values from user.cfg (BaseApp/Spaceball/Motion)
using keys PanLRDeadzone, PanUDDeadzone, ZoomDeadzone, TiltDeadzone,
RollDeadzone, SpinDeadzone. Values default to 0 (no filtering).

Axis values below their individual threshold are zeroed before posting
the motion event, preventing unintended drift from sensor noise while
preserving intentional small movements.

This is the Linux equivalent of deadzone handling that NavLib provides
on Windows/macOS.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Use std::array for axisDeadzoneKeys to avoid magic number

Replace C-style array with std::array and use .size() instead of
hardcoded loop bound, as suggested in code review.

* Cache deadzone values with ParameterGrp Observer instead of polling

Replace per-event GetParameterGroupByPath + GetInt calls with a static
DeadzoneCache that loads values once and updates them via
ParameterGrp::ObserverType when user.cfg changes. Also use
static constexpr for the keys array and std::abs for readability.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Move DeadzoneCache from static local to member variable

Replace the static local DeadzoneCache in pollSpacenav() with a
std::unique_ptr<DeadzoneCache> member of GuiNativeEvent, initialized
in initSpaceball() after the spnav connection is established.
Forward-declare in the header to keep includes minimal.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Maik-0000FF <Maik-0000FF@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-07 22:37:59 +01:00

2163 lines
68 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2008 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <Inventor/SbViewportRegion.h>
#include <Inventor/SoPickedPoint.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/draggers/SoDragger.h>
#include <Inventor/errors/SoDebugError.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoCamera.h>
#include <Inventor/nodes/SoOrthographicCamera.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/projectors/SbSphereSheetProjector.h>
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QByteArray>
#include <QCursor>
#include <QMenu>
#include <cmath>
#include <limits>
#include <Base/Interpreter.h>
#include <App/Application.h>
#include "Navigation/NavigationStyle.h"
#include "Navigation/NavigationStylePy.h"
#include "Application.h"
#include "Command.h"
#include "Action.h"
#include "Inventor/SoMouseWheelEvent.h"
#include "MenuManager.h"
#include "MouseSelection.h"
#include "Navigation/NavigationAnimator.h"
#include "Navigation/NavigationAnimation.h"
#include "View3DInventorViewer.h"
using namespace Gui;
class FCSphereSheetProjector: public SbSphereSheetProjector
{
using inherited = SbSphereSheetProjector;
public:
enum OrbitStyle
{
Turntable,
Trackball,
FreeTurntable,
TrackballClassic,
RoundedArcball
};
static constexpr float defaultSphereRadius = 0.8F;
FCSphereSheetProjector(const SbSphere& sph, const SbBool orienttoeye = true)
: SbSphereSheetProjector(sph, orienttoeye)
{}
void setViewVolume(const SbViewVolume& vol) override
{
inherited::setViewVolume(vol);
}
void setWorkingSpace(const SbMatrix& space) override
{
// inherited::setWorkingSpace(space);
this->worldToScreen = space.inverse();
}
SbVec3f project(const SbVec2f& point) override
{
if (orbit != RoundedArcball) {
return inherited::project(point);
}
// Rounded Arcball implementation
// based on SbSphereSheetProjector in Open Inventor
SbVec3f result;
SbLine workingLine = getWorkingLine(point);
if (needSetup) {
setupPlane();
}
SbVec3f planeIntersection;
SbVec3f sphereIntersection, dontCare;
SbBool hitSphere;
if (intersectFront == TRUE) {
hitSphere = sphere.intersect(workingLine, sphereIntersection, dontCare);
}
else {
hitSphere = sphere.intersect(workingLine, dontCare, sphereIntersection);
}
if (hitSphere) {
// drop the sphere intersection onto the tolerance plane
SbLine projectLine(sphereIntersection, sphereIntersection + planeDir);
if (!tolPlane.intersect(projectLine, planeIntersection)) {
#ifdef FC_DEBUG
SoDebugError::post(
"SbSphereSheetProjector::project",
"Could not intersect working line with plane"
);
#endif
}
}
else if (!tolPlane.intersect(workingLine, planeIntersection)) {
#ifdef FC_DEBUG
SoDebugError::post("SbSphereSheetProjector::project", "Could not intersect with plane");
#endif
}
// Three possibilities:
// (1) Intersection is on the sphere inside where the fillet
// hits it
// (2) Intersection is on the fillet
// (3) Intersection is on the plane
float distance = (planeIntersection - planePoint).length();
// Amount of filleting
// 0 = no fillet (just sphere and plane)
// infinity = only fillet
float border = 0.5;
// Radius where the fillet meets the plane in "squished space"
float r_a = 1.0F + border;
// Radius where the sphere meets the fillet in "squished space"
float r_i = 2.0 / (r_a + 1.0 / r_a);
// Distance squared in "squished space"
float d_2 = (distance * distance) * r_a * r_a;
// Distance in "squished space"
float d = std::sqrt(d_2);
// Compute how far off the plane we are
float offsetDist = 0.0;
if (d > r_a) {
// On the plane
offsetDist = 0.0;
}
else if (d < r_i) {
// On the sphere inside the fillet
offsetDist = std::sqrt(1.0 - d_2);
}
else {
// On the fillet
float d_r = r_a - d;
float a = border * (1.0 + border / 2.0);
offsetDist = a - std::sqrt((a + d_r) * (a - d_r));
}
SbVec3f offset;
if (orientToEye) {
if (viewVol.getProjectionType() == SbViewVolume::PERSPECTIVE) {
offset = workingProjPoint - planeIntersection;
}
else {
worldToWorking.multDirMatrix(viewVol.zVector(), offset);
}
offset.normalize();
}
else {
offset.setValue(0, 0, 1);
}
if (intersectFront == FALSE) {
offset *= -1.0;
}
offset *= offsetDist;
result = planeIntersection + offset;
lastPoint = result;
return result;
}
SbRotation getRotation(const SbVec3f& point1, const SbVec3f& point2) override
{
SbRotation rot = inherited::getRotation(point1, point2);
if (orbit == Turntable || orbit == FreeTurntable) {
return getTurntable(rot, point1, point2);
}
if (orbit == TrackballClassic) {
return getTrackballClassic(point1, point2);
}
return rot;
}
void setOrbitStyle(OrbitStyle style)
{
this->orbit = style;
}
OrbitStyle getOrbitStyle() const
{
return this->orbit;
}
private:
SbRotation getTurntable(SbRotation rot, const SbVec3f& point1, const SbVec3f& point2) const
{
// 0000333: Turntable camera rotation
SbVec3f axis;
float angle {};
rot.getValue(axis, angle);
SbVec3f dif = point1 - point2;
if (fabs(dif[1]) > fabs(dif[0])) {
SbVec3f xaxis(1, 0, 0);
if (dif[1] < 0) {
angle = -angle;
}
rot.setValue(xaxis, angle);
}
else {
SbVec3f zaxis(0, 0, 1);
this->worldToScreen.multDirMatrix(zaxis, zaxis);
if (zaxis[1] < 0) {
if (dif[0] < 0) {
angle = -angle;
}
}
else {
if (dif[0] > 0) {
angle = -angle;
}
}
rot.setValue(zaxis, angle);
}
return rot;
}
SbRotation getTrackballClassic(const SbVec3f& point1, const SbVec3f& point2) const
{
// Classic trackball
SbRotation zrot;
SbRotation yrot;
SbVec3f dif = point1 - point2;
SbVec3f zaxis(1, 0, 0);
zrot.setValue(zaxis, dif[1]);
SbVec3f yaxis(0, 1, 0);
yrot.setValue(yaxis, -dif[0]);
return zrot * yrot;
}
private:
SbMatrix worldToScreen;
OrbitStyle orbit {RoundedArcball};
};
NavigationStyleEvent::NavigationStyleEvent(const Base::Type& s)
: QEvent(QEvent::User)
, t(s)
{}
NavigationStyleEvent::~NavigationStyleEvent() = default;
const Base::Type& NavigationStyleEvent::style() const
{
return t;
}
TYPESYSTEM_SOURCE_ABSTRACT(Gui::NavigationStyle, Base::BaseClass)
NavigationStyle::NavigationStyle()
: viewer(nullptr)
, mouseSelection(nullptr)
, pythonObject(nullptr)
{
this->rotationCenterMode = NavigationStyle::RotationCenterMode::ScenePointAtCursor
| NavigationStyle::RotationCenterMode::FocalPointAtCursor;
initialize();
}
NavigationStyle::~NavigationStyle()
{
finalize();
delete this->animator;
if (!pythonObject.is(nullptr)) {
Base::PyGILStateLocker lock;
Base::PyObjectBase* obj = static_cast<Base::PyObjectBase*>(pythonObject.ptr());
obj->setInvalid();
}
}
NavigationStyle& NavigationStyle::operator=(const NavigationStyle& ns)
{
this->panningplane = ns.panningplane;
this->menuenabled = ns.menuenabled;
this->animationEnabled = ns.animationEnabled;
this->spinningAnimationEnabled = ns.spinningAnimationEnabled;
static_cast<FCSphereSheetProjector*>(this->spinprojector)
->setOrbitStyle(static_cast<FCSphereSheetProjector*>(ns.spinprojector)->getOrbitStyle());
return *this;
}
void NavigationStyle::setViewer(View3DInventorViewer* view)
{
this->viewer = view;
}
void NavigationStyle::initialize()
{
this->animator = new NavigationAnimator();
this->sensitivity = 2.0f;
this->resetcursorpos = false;
this->currentmode = NavigationStyle::IDLE;
this->animationEnabled = true;
this->spinningAnimationEnabled = false;
this->spinsamplecounter = 0;
this->spinincrement = SbRotation::identity();
this->rotationCenterFound = false;
this->rotationCenterIsScenePointAtCursor = false;
// FIXME: use a smaller sphere than the default one to have a larger
// area close to the borders that gives us "z-axis rotation"?
// 19990425 mortene.
this->spinprojector = new FCSphereSheetProjector(
SbSphere(SbVec3f(0, 0, 0), FCSphereSheetProjector::defaultSphereRadius)
);
SbViewVolume volume;
volume.ortho(-1, 1, -1, 1, -1, 1);
this->spinprojector->setViewVolume(volume);
this->log.size = 16;
this->log.position = new SbVec2s[16];
this->log.time = new SbTime[16];
this->log.historysize = 0;
this->menuenabled = true;
this->button1down = false;
this->button2down = false;
this->button3down = false;
this->ctrldown = false;
this->shiftdown = false;
this->altdown = false;
this->invertZoom = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetBool("InvertZoom", true);
this->zoomAtCursor = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetBool("ZoomAtCursor", true);
this->zoomStep = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetFloat("ZoomStep", 0.2f);
long mode = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetInt("RotationMode", 0);
if (mode == 0) {
setRotationCenterMode(NavigationStyle::RotationCenterMode::WindowCenter);
}
else if (mode == 1) {
setRotationCenterMode(
NavigationStyle::RotationCenterMode::ScenePointAtCursor
| NavigationStyle::RotationCenterMode::FocalPointAtCursor
);
}
else if (mode == 2) {
setRotationCenterMode(
NavigationStyle::RotationCenterMode::ScenePointAtCursor
| NavigationStyle::RotationCenterMode::BoundingBoxCenter
);
}
this->hasDragged = false;
this->hasPanned = false;
this->hasZoomed = false;
}
void NavigationStyle::finalize()
{
delete this->spinprojector;
delete[] this->log.position;
delete[] this->log.time;
}
void NavigationStyle::interactiveCountInc()
{
viewer->interactiveCountInc();
}
void NavigationStyle::interactiveCountDec()
{
viewer->interactiveCountDec();
}
int NavigationStyle::getInteractiveCount() const
{
return viewer->getInteractiveCount();
}
void NavigationStyle::setOrbitStyle(NavigationStyle::OrbitStyle style)
{
auto projector = static_cast<FCSphereSheetProjector*>(this->spinprojector);
projector->setOrbitStyle(FCSphereSheetProjector::OrbitStyle(style));
}
NavigationStyle::OrbitStyle NavigationStyle::getOrbitStyle() const
{
auto projector = static_cast<FCSphereSheetProjector*>(this->spinprojector);
return NavigationStyle::OrbitStyle(projector->getOrbitStyle());
}
SbBool NavigationStyle::isViewing() const
{
return viewer->isViewing();
}
void NavigationStyle::setViewing(SbBool enable)
{
viewer->setViewing(enable);
}
SbBool NavigationStyle::isSeekMode() const
{
return viewer->isSeekMode();
}
void NavigationStyle::setSeekMode(SbBool enable)
{
viewer->setSeekMode(enable);
}
SbBool NavigationStyle::seekToPoint(const SbVec2s screenpos)
{
return viewer->seekToPoint(screenpos);
}
void NavigationStyle::seekToPoint(const SbVec3f& scenepos)
{
viewer->seekToPoint(scenepos);
}
void NavigationStyle::lookAtPoint(const SbVec2s screenpos)
{
const SoCamera* camera = viewer->getCamera();
if (!camera) {
return;
}
SoRayPickAction rpaction(viewer->getViewportRegion());
rpaction.setPoint(screenpos);
rpaction.setRadius(viewer->getPickRadius());
rpaction.apply(viewer->getSoRenderManager()->getSceneGraph());
const SoPickedPoint* picked = rpaction.getPickedPoint();
// Point is either the hitpoint or the projected point on the panning plane
SbVec3f point;
if (picked) {
point = picked->getPoint();
}
else {
const SbViewportRegion& vp = viewer->getViewportRegion();
const float aspectratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(aspectratio);
// See note in Coin docs for SoCamera::getViewVolume re:viewport mapping
if (aspectratio < 1.0) {
vv.scale(1.0 / aspectratio);
}
SbLine line;
vv.projectPointToLine(normalizePixelPos(screenpos), line);
panningplane.intersect(line, point);
}
lookAtPoint(point);
}
void NavigationStyle::lookAtPoint(const SbVec3f& position)
{
this->rotationCenterFound = false;
translateCamera(position - getFocalPoint());
}
SoCamera* NavigationStyle::getCamera() const
{
return this->viewer->getCamera();
}
std::shared_ptr<NavigationAnimation> NavigationStyle::setCameraOrientation(
const SbRotation& orientation,
const SbBool moveToCenter
) const
{
SoCamera* camera = getCamera();
if (!camera) {
return {};
}
animator->stop();
const SbVec3f focalPoint = getFocalPoint();
SbVec3f translation(0, 0, 0);
if (moveToCenter) {
SoGetBoundingBoxAction action(viewer->getSoRenderManager()->getViewportRegion());
action.apply(viewer->getSceneGraph());
SbBox3f box = action.getBoundingBox();
if (!box.isEmpty()) {
translation = box.getCenter() - focalPoint;
}
}
// Start an animation and return it
if (isAnimationEnabled()) {
return viewer->startAnimation(orientation, focalPoint, translation);
}
// or set the pose directly
// Distance from rotation center to camera position in camera coordinate system
const SbVec3f rotationCenterDistanceCam = camera->focalDistance.getValue() * SbVec3f(0, 0, 1);
// Set to the given orientation
camera->orientation = orientation;
// Distance from rotation center to new camera position in global coordinate system
SbVec3f newRotationCenterDistance;
camera->orientation.getValue().multVec(rotationCenterDistanceCam, newRotationCenterDistance);
// Reposition camera so the rotation center stays in the same place
// Optionally add translation to move to center
camera->position = focalPoint + newRotationCenterDistance + translation;
return {};
}
std::shared_ptr<NavigationAnimation> NavigationStyle::translateCamera(const SbVec3f& translation) const
{
SoCamera* camera = getCamera();
if (!camera) {
return {};
}
animator->stop();
// Start an animation and return it
if (isAnimationEnabled()) {
return viewer->startAnimation(camera->orientation.getValue(), SbVec3f(0, 0, 0), translation);
}
// or set the pose directly
camera->position = camera->position.getValue() + translation;
return {};
}
void NavigationStyle::boxZoom(const SbBox2s& box)
{
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) { // no camera
return;
}
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
SbViewVolume vv = cam->getViewVolume(vp.getViewportAspectRatio());
short sizeX {}, sizeY {};
box.getSize(sizeX, sizeY);
SbVec2s size = vp.getViewportSizePixels();
// The bbox must not be empty i.e. width and length is zero, but it is possible that
// either width or length is zero
if (sizeX == 0 && sizeY == 0) {
return;
}
// Get the new center in normalized pixel coordinates
short xmin {}, xmax {}, ymin {}, ymax {};
box.getBounds(xmin, ymin, xmax, ymax);
const SbVec2f center(
(float)((xmin + xmax) / 2) / (float)std::max((int)(size[0] - 1), 1),
(float)(size[1] - (ymin + ymax) / 2) / (float)std::max((int)(size[1] - 1), 1)
);
SbPlane plane = vv.getPlane(cam->focalDistance.getValue());
panCamera(cam, vp.getViewportAspectRatio(), plane, SbVec2f(0.5, 0.5), center);
// Set height or height angle of the camera
float scaleX = (float)sizeX / (float)size[0];
float scaleY = (float)sizeY / (float)size[1];
float scaleFactor = std::max<float>(scaleX, scaleY);
doScale(cam, scaleFactor);
}
void NavigationStyle::scale(float factor)
{
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) { // no camera
return;
}
// Find the current center of the screen
SbVec3f direction;
cam->orientation.getValue().multVec(SbVec3f(0, 0, -1), direction);
SbVec3f initCenter = cam->position.getValue() + cam->focalDistance.getValue() * direction;
// Move the camera to the origin for scaling
cam->position = cam->position.getValue() - initCenter;
// Scale the view
doScale(cam, factor);
// Move the camera back to it's initial position scaled
cam->position = cam->position.getValue() + initCenter * factor;
}
void NavigationStyle::viewAll()
{
// Get the bounding box of the scene
SoGetBoundingBoxAction action(viewer->getSoRenderManager()->getViewportRegion());
action.apply(viewer->getSceneGraph());
SbBox3f box = action.getBoundingBox();
if (box.isEmpty()) {
return;
}
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) {
return;
}
SbViewVolume vol = cam->getViewVolume();
if (vol.ulf == vol.llf) {
return; // empty frustum (no view up vector defined)
}
SbVec2f s = vol.projectBox(box);
SbVec2s size = viewer->getSoRenderManager()->getSize();
SbVec3f pt1, pt2, pt3, tmp;
vol.projectPointToLine(SbVec2f(0.0f, 0.0f), pt1, tmp);
vol.projectPointToLine(SbVec2f(s[0], 0.0f), pt2, tmp);
vol.projectPointToLine(SbVec2f(0.0f, s[1]), pt3, tmp);
float cam_width = (pt2 - pt1).length();
float cam_height = (pt3 - pt1).length();
// add a small border
cam_height = 1.08f * std::max<float>((cam_width * (float)size[1]) / (float)size[0], cam_height);
float aspect = cam->aspectRatio.getValue();
if (cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
auto ocam = static_cast<SoOrthographicCamera*>(cam);
if (aspect < 1.0f) {
ocam->height = cam_height / aspect;
}
else {
ocam->height = cam_height;
}
}
}
#if (COIN_MAJOR_VERSION * 100 + COIN_MINOR_VERSION * 10 + COIN_MICRO_VERSION < 403)
void NavigationStyle::findBoundingSphere()
{
// Find a bounding sphere for the scene
SoGetBoundingBoxAction action(viewer->getSoRenderManager()->getViewportRegion());
action.apply(viewer->getSceneGraph());
boundingSphere.circumscribe(action.getBoundingBox());
}
#endif
/** Rotate the camera by the given amount, then reposition it so we're still pointing at the same
* focal point
*/
void NavigationStyle::reorientCamera(SoCamera* camera, const SbRotation& rotation)
{
reorientCamera(camera, rotation, getFocalPoint());
}
/** Rotate the camera by the given amount, then reposition it so the rotation center stays in the
* same place
*/
void NavigationStyle::reorientCamera(
SoCamera* camera,
const SbRotation& rotation,
const SbVec3f& rotationCenter
)
{
if (!camera) {
return;
}
// Distance from rotation center to camera position in camera coordinate system
SbVec3f rotationCenterDistanceCam;
camera->orientation.getValue().inverse().multVec(
camera->position.getValue() - rotationCenter,
rotationCenterDistanceCam
);
// Set new orientation value by accumulating the new rotation
camera->orientation = rotation * camera->orientation.getValue();
// Distance from rotation center to new camera position in global coordinate system
SbVec3f newRotationCenterDistance;
camera->orientation.getValue().multVec(rotationCenterDistanceCam, newRotationCenterDistance);
// Reposition camera so the rotation center stays in the same place
camera->position = rotationCenter + newRotationCenterDistance;
if (camera->getTypeId().isDerivedFrom(SoOrthographicCamera::getClassTypeId())) {
// Adjust the camera position to keep the focal point in the same place while making sure
// the focal distance stays small. This increases rotation stability for small and large
// scenes
SbVec3f direction;
camera->orientation.getValue().multVec(SbVec3f(0, 0, -1), direction);
#if (COIN_MAJOR_VERSION * 100 + COIN_MINOR_VERSION * 10 + COIN_MICRO_VERSION < 405)
// Large focal distance puts the camera far away which causes Coin's auto clipping
// calculations to add more slack (more space between near and far plane) and thus reduces
// chances or hidden geometry.
constexpr float orthographicFocalDistance = 250;
#else
constexpr float orthographicFocalDistance = 1;
#endif
camera->position = getFocalPoint() - orthographicFocalDistance * direction;
camera->focalDistance = orthographicFocalDistance;
}
#if (COIN_MAJOR_VERSION * 100 + COIN_MINOR_VERSION * 10 + COIN_MICRO_VERSION < 403)
// Fix issue with near clipping in orthogonal view
if (camera->getTypeId().isDerivedFrom(SoOrthographicCamera::getClassTypeId())) {
// The center of the bounding sphere in camera coordinate system
SbVec3f center;
camera->orientation.getValue().inverse().multVec(
boundingSphere.getCenter() - camera->position.getValue(),
center
);
SbVec3f dir;
camera->orientation.getValue().multVec(SbVec3f(0, 0, -1), dir);
// Reposition the camera but keep the focal point the same
// nearDistance is 0 and farDistance is the diameter of the bounding sphere
float repositionDistance = -center.getValue()[2] - boundingSphere.getRadius();
camera->position = camera->position.getValue() + repositionDistance * dir;
camera->nearDistance = 0;
camera->farDistance = 2 * boundingSphere.getRadius() + 1;
camera->focalDistance = camera->focalDistance.getValue() - repositionDistance;
}
#endif
}
void NavigationStyle::panCamera(
SoCamera* cam,
float aspectratio,
const SbPlane& panplane,
const SbVec2f& currpos,
const SbVec2f& prevpos
)
{
if (!cam) { // can happen for empty scenegraph
return;
}
if (currpos == prevpos) { // useless invocation
return;
}
// Find projection points for the last and current mouse coordinates.
SbViewVolume vv = cam->getViewVolume(aspectratio);
// See note in Coin docs for SoCamera::getViewVolume re:viewport mapping
if (aspectratio < 1.0) {
vv.scale(1.0 / aspectratio);
}
SbLine line;
vv.projectPointToLine(currpos, line);
SbVec3f current_planept;
panplane.intersect(line, current_planept);
vv.projectPointToLine(prevpos, line);
SbVec3f old_planept;
panplane.intersect(line, old_planept);
// Reposition camera according to the vector difference between the
// projected points.
cam->position = cam->position.getValue() - (current_planept - old_planept);
if (this->currentmode != NavigationStyle::IDLE) {
hasPanned = true;
}
}
void NavigationStyle::setupPanningPlane(const SoCamera* camera)
{
// The plane we're projecting the mouse coordinates to get 3D
// coordinates should stay the same during the whole pan
// operation, so we should calculate this value here.
if (!camera) { // can happen for empty scenegraph
this->panningplane = SbPlane(SbVec3f(0, 0, 1), 0);
}
else {
const SbViewportRegion& vp = viewer->getViewportRegion();
const float aspectratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(aspectratio);
// See note in Coin docs for SoCamera::getViewVolume re:viewport mapping
if (aspectratio < 1.0) {
vv.scale(1.0 / aspectratio);
}
this->panningplane = vv.getPlane(camera->focalDistance.getValue());
}
}
/** Dependent on the camera type this will either shrink or expand the
* height of the viewport (orthogonal camera) or move the camera
* closer or further away from the focal point in the scene.
*/
void NavigationStyle::zoom(SoCamera* cam, float diffvalue)
{
if (!cam) { // can happen for empty scenegraph
return;
}
animator->stop();
SoType t = cam->getTypeId();
SbName tname = t.getName();
// This will be in the range of <0, ->>.
auto multiplicator = float(exp(diffvalue));
if (t.isDerivedFrom(SoOrthographicCamera::getClassTypeId())) {
// Since there's no perspective, "zooming" in the original sense
// of the word won't have any visible effect. So we just increase
// or decrease the field-of-view values of the camera instead, to
// "shrink" the projection size of the model / scene.
auto oc = static_cast<SoOrthographicCamera*>(cam);
oc->height = oc->height.getValue() * multiplicator;
}
else {
// FrustumCamera can be found in the SmallChange CVS module (it's
// a camera that lets you specify (for instance) an off-center
// frustum (similar to glFrustum())
if (!t.isDerivedFrom(SoPerspectiveCamera::getClassTypeId()) && tname != "FrustumCamera") {
#ifdef FC_DEBUG
static SbBool first = true;
if (first) {
SoDebugError::postWarning(
"NavigationStyle::zoom",
"Unknown camera type, "
"will zoom by moving position, "
"but this might not be correct."
);
first = false;
}
#endif
}
const float oldfocaldist = cam->focalDistance.getValue();
const float newfocaldist = oldfocaldist * multiplicator;
SbVec3f direction;
cam->orientation.getValue().multVec(SbVec3f(0, 0, -1), direction);
const SbVec3f oldpos = cam->position.getValue();
const SbVec3f newpos = oldpos + (newfocaldist - oldfocaldist) * -direction;
// This catches a rather common user interface "buglet": if the
// user zooms the camera out to a distance from origo larger than
// what we still can safely do floating point calculations on
// (i.e. without getting NaN or Inf values), the faulty floating
// point values will propagate until we start to get debug error
// messages and eventually an assert failure from core Coin code.
//
// With the below bounds check, this problem is avoided.
//
// (But note that we depend on the input argument ''diffvalue'' to
// be small enough that zooming happens gradually. Ideally, we
// should also check distorigo with isinf() and isnan() (or
// inversely; isinfite()), but those only became standardized with
// C99.)
const float distorigo = newpos.length();
// sqrt(FLT_MAX) == ~ 1e+19, which should be both safe for further
// calculations and ok for the end-user and app-programmer.
float maxDistance = std::sqrt(std::numeric_limits<float>::max());
if (distorigo > maxDistance) {
// do nothing here
}
else {
cam->position = newpos;
cam->focalDistance = newfocaldist;
}
}
if (this->currentmode != NavigationStyle::IDLE) {
hasZoomed = true;
}
}
// Calculate a zoom/dolly factor from the difference of the current
// cursor position and the last.
void NavigationStyle::zoomByCursor(const SbVec2f& thispos, const SbVec2f& prevpos)
{
// There is no "geometrically correct" value, 20 just seems to give
// about the right "feel".
float value = (thispos[1] - prevpos[1]) * 10.0f /*20.0f*/;
if (this->invertZoom) {
value = -value;
}
zoom(viewer->getSoRenderManager()->getCamera(), value);
}
void NavigationStyle::zoomIn()
{
zoom(viewer->getSoRenderManager()->getCamera(), -this->zoomStep);
}
void NavigationStyle::zoomOut()
{
zoom(viewer->getSoRenderManager()->getCamera(), this->zoomStep);
}
/*!
* Returns the steps if the mouse wheel is rotated
*/
int NavigationStyle::getDelta() const
{
return 120;
}
void NavigationStyle::doZoom(SoCamera* camera, int wheeldelta, const SbVec2f& pos)
{
float value = this->zoomStep * wheeldelta / float(getDelta());
if (this->invertZoom) {
value = -value;
}
doZoom(camera, value, pos);
}
/*!
*\brief NavigationStyle::doZoom Zooms in or out by specified factor, keeping the point on screen
* specified by parameter pos fixed or not according to user preference
* (NavigationStyle::zoomAtCursor). Ignores invertZoom user preference.
*/
void NavigationStyle::doZoom(SoCamera* camera, float logfactor, const SbVec2f& pos)
{
// something is asking for big zoom factor. This func is made for interactive zooming,
// where the changes are per mouse move and thus are small.
if (fabs(logfactor) > 4.0) {
return;
}
SbBool zoomAtCur = this->zoomAtCursor;
if (zoomAtCur) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(vp.getViewportAspectRatio());
SbPlane panplane = vv.getPlane(camera->focalDistance.getValue());
panCamera(viewer->getSoRenderManager()->getCamera(), ratio, panplane, SbVec2f(0.5, 0.5), pos);
}
zoom(camera, logfactor);
if (zoomAtCur) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(vp.getViewportAspectRatio());
SbPlane panplane = vv.getPlane(camera->focalDistance.getValue());
panCamera(viewer->getSoRenderManager()->getCamera(), ratio, panplane, pos, SbVec2f(0.5, 0.5));
// Change the position of the rotation center indicator after zooming at cursor
// Rotation mode is WindowCenter
if (!rotationCenterMode) {
viewer->changeRotationCenterPosition(getFocalPoint());
#if (COIN_MAJOR_VERSION * 100 + COIN_MINOR_VERSION * 10 + COIN_MICRO_VERSION < 403)
findBoundingSphere();
#endif
}
}
}
void NavigationStyle::doScale(SoCamera* cam, float factor)
{
if (cam->getTypeId() == SoOrthographicCamera::getClassTypeId()) {
float height = static_cast<SoOrthographicCamera*>(cam)->height.getValue() * factor;
static_cast<SoOrthographicCamera*>(cam)->height = height;
}
else if (cam->getTypeId() == SoPerspectiveCamera::getClassTypeId()) {
float height = static_cast<SoPerspectiveCamera*>(cam)->heightAngle.getValue() / 2.0f;
height = 2.0f * atan(tan(height) * factor);
static_cast<SoPerspectiveCamera*>(cam)->heightAngle = height;
}
}
void NavigationStyle::doRotate(SoCamera* camera, float angle, const SbVec2f& pos)
{
SbBool zoomAtCur = this->zoomAtCursor;
if (zoomAtCur) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(vp.getViewportAspectRatio());
SbPlane panplane = vv.getPlane(camera->focalDistance.getValue());
panCamera(viewer->getSoRenderManager()->getCamera(), ratio, panplane, SbVec2f(0.5, 0.5), pos);
}
SbRotation rotcam = camera->orientation.getValue();
// get view direction
SbVec3f vdir;
rotcam.multVec(SbVec3f(0, 0, -1), vdir);
// rotate
SbRotation drot(vdir, angle);
camera->orientation.setValue(rotcam * drot);
if (zoomAtCur) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SbViewVolume vv = camera->getViewVolume(vp.getViewportAspectRatio());
SbPlane panplane = vv.getPlane(camera->focalDistance.getValue());
panCamera(viewer->getSoRenderManager()->getCamera(), ratio, panplane, pos, SbVec2f(0.5, 0.5));
}
}
SbVec3f NavigationStyle::getRotationCenter(SbBool& found) const
{
found = this->rotationCenterFound;
return this->rotationCenter;
}
std::optional<SbVec2s>& NavigationStyle::getRightClickPosition()
{
return rightClickPosition;
}
void NavigationStyle::setRotationCenter(const SbVec3f& cnt)
{
this->rotationCenter = cnt;
this->rotationCenterFound = true;
const auto camera = getCamera();
if (camera->isOfType(SoPerspectiveCamera::getClassTypeId())) {
SbVec3f direction;
camera->orientation.getValue().multVec(SbVec3f(0, 0, -1), direction);
// Calculate distance from camera to rotation center
const auto rotationCenterDistance = rotationCenter - camera->position.getValue();
const auto rotationCenterDepth = rotationCenterDistance.dot(direction);
// Set focal distance to match rotation center depth so we can zoom at the new rotation
// center with a perspective camera
camera->focalDistance.setValue(rotationCenterDepth);
}
}
SbVec3f NavigationStyle::getFocalPoint() const
{
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) {
return {0, 0, 0};
}
// Find global coordinates of focal point.
SbVec3f direction;
cam->orientation.getValue().multVec(SbVec3f(0, 0, -1), direction);
SbVec3f focal = cam->position.getValue() + cam->focalDistance.getValue() * direction;
return focal;
}
/** Uses the sphere sheet projector to map the mouse position onto
* a 3D point and find a rotation from this and the last calculated point.
*/
void NavigationStyle::spin(const SbVec2f& pointerpos)
{
if (this->log.historysize < 2) {
return;
}
assert(this->spinprojector);
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
SbVec2s glsize(vp.getViewportSizePixels());
SbVec2f lastpos;
lastpos[0] = float(this->log.position[1][0]) / float(std::max((int)(glsize[0] - 1), 1));
lastpos[1] = float(this->log.position[1][1]) / float(std::max((int)(glsize[1] - 1), 1));
if (getOrbitStyle() == FreeTurntable) {
SbVec2f midpos(lastpos[0], pointerpos[1]);
spinInternal(pointerpos, midpos);
spinInternal(midpos, lastpos);
}
else {
spinInternal(pointerpos, lastpos);
}
if (this->currentmode != NavigationStyle::IDLE) {
hasDragged = true;
}
}
void NavigationStyle::spinInternal(const SbVec2f& pointerpos, const SbVec2f& lastpos)
{
float sensitivity = getSensitivity();
// Adjust the spin projector sphere to the screen position of the rotation center when the mouse
// intersects an object
if ((getOrbitStyle() == Trackball || getOrbitStyle() == TrackballClassic
|| getOrbitStyle() == RoundedArcball)
&& rotationCenterMode & RotationCenterMode::ScenePointAtCursor && rotationCenterFound
&& rotationCenterIsScenePointAtCursor) {
const auto pointOnScreen = viewer->getPointOnViewport(rotationCenter);
const auto sphereCenter = 2 * normalizePixelPos(pointOnScreen) - SbVec2f {1, 1};
float x, y;
sphereCenter.getValue(x, y);
const float sphereScale = 1 + sphereCenter.length();
const float radius = FCSphereSheetProjector::defaultSphereRadius * sphereScale;
sensitivity *= sphereScale;
spinprojector->setSphere(SbSphere {SbVec3f {x, y, 0}, radius});
}
else {
spinprojector->setSphere(
SbSphere {SbVec3f {0, 0, 0}, FCSphereSheetProjector::defaultSphereRadius}
);
}
// 0000333: Turntable camera rotation
SbMatrix mat;
viewer->getSoRenderManager()->getCamera()->orientation.getValue().getValue(mat);
this->spinprojector->setWorkingSpace(mat);
this->spinprojector->project(lastpos);
SbRotation r;
this->spinprojector->projectAndGetRotation(pointerpos, r);
if (sensitivity > 1.0f) {
SbVec3f axis;
float radians {};
r.getValue(axis, radians);
radians = sensitivity * radians;
r.setValue(axis, radians);
}
r.invert();
if (this->rotationCenterMode && this->rotationCenterFound) {
this->reorientCamera(viewer->getSoRenderManager()->getCamera(), r, rotationCenter);
}
else {
this->reorientCamera(viewer->getSoRenderManager()->getCamera(), r);
}
// Calculate an average angle magnitude value to make the transition
// to a possible spin animation mode appear smooth.
SbVec3f dummy_axis, newaxis;
float acc_angle {}, newangle {};
this->spinincrement.getValue(dummy_axis, acc_angle);
acc_angle *= this->spinsamplecounter; // weight
r.getValue(newaxis, newangle);
acc_angle += newangle;
this->spinsamplecounter++;
acc_angle /= this->spinsamplecounter;
// FIXME: accumulate and average axis vectors as well? 19990501 mortene.
this->spinincrement.setValue(newaxis, acc_angle);
// Don't carry too much baggage, as that'll give unwanted results
// when the user quickly trigger (as in "click-drag-release") a spin
// animation.
if (this->spinsamplecounter > 3) {
this->spinsamplecounter = 3;
}
}
/*!
* \brief NavigationStyle::spin_simplified is a simplified version of
* NavigationStyle::spin(..), which uses less global variables. Doesn't support
* starting an animated spinning.
*
* \param cam the camera to affect. The rotation amount is determined by delta
* (curpos-prevpos), and rotation axis is also affected by average pos.
* \param curpos current normalized position or mouse pointer
* \param prevpos previous normalized position of mouse pointer
*/
void NavigationStyle::spin_simplified(SbVec2f curpos, SbVec2f prevpos)
{
assert(this->spinprojector);
if (getOrbitStyle() == FreeTurntable) {
SbVec2f midpos(prevpos[0], curpos[1]);
spinSimplifiedInternal(curpos, midpos);
spinSimplifiedInternal(midpos, prevpos);
}
else {
spinSimplifiedInternal(curpos, prevpos);
}
hasDragged = true;
}
void NavigationStyle::spinSimplifiedInternal(SbVec2f curpos, SbVec2f prevpos)
{
// 0000333: Turntable camera rotation
SbMatrix mat;
viewer->getSoRenderManager()->getCamera()->orientation.getValue().getValue(mat);
this->spinprojector->setWorkingSpace(mat);
this->spinprojector->project(prevpos);
SbRotation r;
this->spinprojector->projectAndGetRotation(curpos, r);
float sensitivity = getSensitivity();
if (sensitivity > 1.0f) {
SbVec3f axis;
float radians {};
r.getValue(axis, radians);
radians = sensitivity * radians;
r.setValue(axis, radians);
}
r.invert();
if (this->rotationCenterMode && this->rotationCenterFound) {
this->reorientCamera(viewer->getSoRenderManager()->getCamera(), r, rotationCenter);
}
else {
this->reorientCamera(viewer->getSoRenderManager()->getCamera(), r);
}
}
SbBool NavigationStyle::doSpin()
{
if (this->log.historysize >= 3) {
SbTime stoptime = (SbTime::getTimeOfDay() - this->log.time[0]);
if (isSpinningAnimationEnabled() && stoptime.getValue() < 0.100) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
const SbVec2s glsize(vp.getViewportSizePixels());
SbVec3f from = this->spinprojector->project(SbVec2f(
float(this->log.position[2][0]) / float(std::max(glsize[0] - 1, 1)),
float(this->log.position[2][1]) / float(std::max(glsize[1] - 1, 1))
));
SbVec3f to = this->spinprojector->project(this->lastmouseposition);
SbRotation rot = this->spinprojector->getRotation(from, to);
SbTime delta = (this->log.time[0] - this->log.time[2]);
double deltatime = delta.getValue();
rot.invert();
rot.scaleAngle(float(0.200 / deltatime));
SbVec3f axis;
float radians {};
rot.getValue(axis, radians);
if ((radians > 0.01f) && (deltatime < 0.300)) {
viewer->startSpinningAnimation(axis, radians * 5);
return true;
}
}
}
return false;
}
void NavigationStyle::saveCursorPosition(const SoEvent* const ev)
{
this->globalPos.setValue(QCursor::pos().x(), QCursor::pos().y());
this->localPos = ev->getPosition();
rotationCenterIsScenePointAtCursor = false;
// mode is WindowCenter
if (!this->rotationCenterMode) {
setRotationCenter(getFocalPoint());
}
// Option to get point on model (slow) or always on focal plane (fast)
//
// mode is ScenePointAtCursor to get exact point if possible
if (this->rotationCenterMode & NavigationStyle::RotationCenterMode::ScenePointAtCursor) {
SoRayPickAction rpaction(viewer->getSoRenderManager()->getViewportRegion());
rpaction.setPoint(this->localPos);
rpaction.setRadius(viewer->getPickRadius());
rpaction.apply(viewer->getSoRenderManager()->getSceneGraph());
SoPickedPoint* picked = rpaction.getPickedPoint();
if (picked) {
setRotationCenter(picked->getPoint());
rotationCenterIsScenePointAtCursor = true;
return;
}
}
// mode is FocalPointAtCursor or a ScenePointAtCursor failed
if (this->rotationCenterMode & NavigationStyle::RotationCenterMode::FocalPointAtCursor) {
// get the intersection point of the ray and the focal plane
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) { // no camera
return;
}
SbViewVolume vv = cam->getViewVolume(ratio);
SbLine line;
SbVec2f currpos = ev->getNormalizedPosition(vp);
vv.projectPointToLine(currpos, line);
SbVec3f current_planept;
SbPlane panplane = vv.getPlane(cam->focalDistance.getValue());
panplane.intersect(line, current_planept);
setRotationCenter(current_planept);
}
// mode is BoundingBoxCenter or a ScenePointAtCursor failed
if (this->rotationCenterMode & NavigationStyle::RotationCenterMode::BoundingBoxCenter) {
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
float ratio = vp.getViewportAspectRatio();
SoCamera* cam = viewer->getSoRenderManager()->getCamera();
if (!cam) { // no camera
return;
}
// Get the bounding box center of the physical object group
SoGetBoundingBoxAction action(viewer->getSoRenderManager()->getViewportRegion());
action.apply(viewer->objectGroup);
SbBox3f boundingBox = action.getBoundingBox();
SbVec3f boundingBoxCenter = boundingBox.getCenter();
setRotationCenter(boundingBoxCenter);
// To drag around the center point of the bbox we have to determine
// its projection on the screen because this information is used in
// NavigationStyle::spin() for the panning
SbViewVolume vv = cam->getViewVolume(ratio);
vv.projectToScreen(boundingBoxCenter, boundingBoxCenter);
SbVec2s size = vp.getViewportSizePixels();
auto tox = static_cast<short>(boundingBoxCenter[0] * size[0]);
auto toy = static_cast<short>(boundingBoxCenter[1] * size[1]);
this->localPos.setValue(tox, toy);
}
}
SbVec2f NavigationStyle::normalizePixelPos(SbVec2s pixpos)
{
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
const SbVec2s size(vp.getViewportSizePixels());
return {
(float)pixpos[0] / (float)std::max((int)(size[0] - 1), 1),
(float)pixpos[1] / (float)std::max((int)(size[1] - 1), 1)
};
}
SbVec2f NavigationStyle::normalizePixelPos(SbVec2f pixpos)
{
const SbViewportRegion& vp = viewer->getSoRenderManager()->getViewportRegion();
const SbVec2s size(vp.getViewportSizePixels());
return {
pixpos[0] / (float)std::max((int)(size[0] - 1), 1),
pixpos[1] / (float)std::max((int)(size[1] - 1), 1)
};
}
void NavigationStyle::moveCursorPosition()
{
if (!isResetCursorPosition()) {
return;
}
QPoint cpos = QCursor::pos();
if (abs(cpos.x() - globalPos[0]) > 10 || abs(cpos.y() - globalPos[1]) > 10) {
QCursor::setPos(globalPos[0], globalPos[1] - 1);
this->log.position[0] = localPos;
}
}
SbBool NavigationStyle::handleEventInForeground(const SoEvent* const e)
{
SoHandleEventAction action(viewer->getSoRenderManager()->getViewportRegion());
action.setEvent(e);
action.setPickRadius(viewer->getPickRadius());
action.apply(viewer->foregroundroot);
return action.isHandled();
}
/**
* @brief Decide if it should be possible to start any animation
*
* If the enable flag is false and we're currently animating, the animation will be stopped
*/
void NavigationStyle::setAnimationEnabled(const SbBool enable)
{
animationEnabled = enable;
if (!enable && isAnimating()) {
animator->stop();
}
}
/**
* @brief Decide if it should be possible to start a spin animation of the model in the viewer by
* releasing the mouse button while dragging
*
* If the enable flag is false and we're currently animating, the spin animation will be stopped
*/
void NavigationStyle::setSpinningAnimationEnabled(const SbBool enable)
{
spinningAnimationEnabled = enable;
if (!enable && isSpinning()) {
animator->stop();
}
}
/**
* @return Whether or not it is possible to start any animation
*/
SbBool NavigationStyle::isAnimationEnabled() const
{
return animationEnabled;
}
/**
* @return Whether or not it is possible to start a spinning animation e.g. after dragging
*/
SbBool NavigationStyle::isSpinningAnimationEnabled() const
{
return animationEnabled && spinningAnimationEnabled;
}
/**
* @return Whether or not any animation is currently active
*/
SbBool NavigationStyle::isAnimating() const
{
return animator->isAnimating();
}
/**
* @return Whether or not a spinning animation is currently active e.g. after a user drag
*/
SbBool NavigationStyle::isSpinning() const
{
return currentmode == NavigationStyle::SPINNING;
}
void NavigationStyle::startAnimating(const std::shared_ptr<NavigationAnimation>& animation, bool wait) const
{
if (wait) {
animator->startAndWait(animation);
}
else {
animator->start(animation);
}
}
void NavigationStyle::stopAnimating() const
{
animator->stop();
}
void NavigationStyle::setSensitivity(float val)
{
this->sensitivity = val;
}
float NavigationStyle::getSensitivity() const
{
return this->sensitivity;
}
void NavigationStyle::setResetCursorPosition(SbBool on)
{
this->resetcursorpos = on;
}
SbBool NavigationStyle::isResetCursorPosition() const
{
return this->resetcursorpos;
}
void NavigationStyle::setZoomInverted(SbBool on)
{
this->invertZoom = on;
}
SbBool NavigationStyle::isZoomInverted() const
{
return this->invertZoom;
}
void NavigationStyle::setZoomStep(float val)
{
this->zoomStep = val;
}
void NavigationStyle::setZoomAtCursor(SbBool on)
{
this->zoomAtCursor = on;
}
SbBool NavigationStyle::isZoomAtCursor() const
{
return this->zoomAtCursor;
}
void NavigationStyle::setRotationCenterMode(NavigationStyle::RotationCenterModes mode)
{
this->rotationCenterMode = mode;
}
NavigationStyle::RotationCenterModes NavigationStyle::getRotationCenterMode() const
{
return this->rotationCenterMode;
}
void NavigationStyle::startSelection(AbstractMouseSelection* mouse)
{
if (!mouse) {
return;
}
if (mouseSelection) {
SoDebugError::postWarning(
"NavigationStyle::startSelection",
"Set new mouse selection while an old is still active."
);
}
mouseSelection = mouse;
mouseSelection->grabMouseModel(viewer);
}
void NavigationStyle::startSelection(NavigationStyle::SelectionMode mode)
{
if (mouseSelection) {
return;
}
if (isSelecting()) {
stopSelection();
}
switch (mode) {
case Lasso:
mouseSelection = new PolyPickerSelection();
break;
case Rectangle:
mouseSelection = new RectangleSelection();
break;
case Rubberband:
mouseSelection = new RubberbandSelection();
break;
case BoxZoom:
mouseSelection = new BoxZoomSelection();
break;
case Clip:
mouseSelection = new PolyClipSelection();
break;
default:
break;
}
if (mouseSelection) {
mouseSelection->grabMouseModel(viewer);
}
}
void NavigationStyle::abortSelection()
{
pcPolygon.clear();
if (mouseSelection) {
mouseSelection->releaseMouseModel(true);
delete mouseSelection;
mouseSelection = nullptr;
}
}
void NavigationStyle::stopSelection()
{
pcPolygon.clear();
if (mouseSelection) {
mouseSelection->releaseMouseModel();
delete mouseSelection;
mouseSelection = nullptr;
}
}
SbBool NavigationStyle::isSelecting() const
{
return (mouseSelection ? true : false);
}
const std::vector<SbVec2s>& NavigationStyle::getPolygon(SelectionRole* role) const
{
if (role) {
*role = this->selectedRole;
}
return pcPolygon;
}
bool NavigationStyle::isDraggerUnderCursor(const SbVec2s pos) const
{
SoRayPickAction rp(this->viewer->getSoRenderManager()->getViewportRegion());
rp.setRadius(viewer->getPickRadius());
rp.setPoint(pos);
rp.apply(this->viewer->getSoRenderManager()->getSceneGraph());
SoPickedPoint* pick = rp.getPickedPoint();
if (pick) {
const auto fullpath = static_cast<const SoFullPath*>(pick->getPath());
for (int i = 0; i < fullpath->getLength(); ++i) {
if (fullpath->getNode(i)->isOfType(SoDragger::getClassTypeId())) {
return true;
}
}
return false;
}
return false;
}
// This method adds another point to the mouse location log, used for spin
// animation calculations.
void NavigationStyle::addToLog(const SbVec2s pos, const SbTime time)
{
// In case someone changes the const size setting at the top of this
// file too small.
assert(this->log.size > 2 && "mouse log too small!");
if (this->log.historysize > 0 && pos == this->log.position[0]) {
return;
}
int lastidx = this->log.historysize;
// If we've filled up the log, we should throw away the last item:
if (lastidx == this->log.size) {
lastidx--;
}
assert(lastidx < this->log.size);
for (int i = lastidx; i > 0; i--) {
this->log.position[i] = this->log.position[i - 1];
this->log.time[i] = this->log.time[i - 1];
}
this->log.position[0] = pos;
this->log.time[0] = time;
if (this->log.historysize < this->log.size) {
this->log.historysize += 1;
}
}
// This method "clears" the mouse location log, used for spin
// animation calculations.
void NavigationStyle::clearLog()
{
this->log.historysize = 0;
}
void NavigationStyle::syncModifierKeys(const SoEvent* const ev)
{
// Mismatches in state of the modifier keys happens if the user
// presses or releases them outside the viewer window.
if (this->ctrldown != ev->wasCtrlDown()) {
this->ctrldown = ev->wasCtrlDown();
}
if (this->shiftdown != ev->wasShiftDown()) {
this->shiftdown = ev->wasShiftDown();
}
if (this->altdown != ev->wasAltDown()) {
this->altdown = ev->wasAltDown();
}
}
// The viewer is a state machine, and all changes to the current state
// are made through this call.
void NavigationStyle::setViewingMode(const ViewerMode newmode)
{
const ViewerMode oldmode = this->currentmode;
// Reset flags when changing from IDLE to another mode or if the mode is IDLE and the buttons
// are released
if ((oldmode == IDLE && newmode != IDLE)
|| (newmode == IDLE && !button1down && !button2down && !button3down)) {
hasPanned = false;
hasDragged = false;
hasZoomed = false;
}
if (newmode == oldmode) {
// The rotation center could have been changed even if the mode has not changed
if (newmode == NavigationStyle::DRAGGING && rotationCenterFound) {
viewer->changeRotationCenterPosition(rotationCenter);
}
return;
}
switch (newmode) {
case DRAGGING:
// Set up initial projection point for the projector object when
// first starting a drag operation.
animator->stop();
viewer->showRotationCenter(true);
#if (COIN_MAJOR_VERSION * 100 + COIN_MINOR_VERSION * 10 + COIN_MICRO_VERSION < 403)
findBoundingSphere();
#endif
this->spinprojector->project(this->lastmouseposition);
this->interactiveCountInc();
this->clearLog();
break;
case SPINNING:
this->interactiveCountInc();
viewer->getSoRenderManager()->scheduleRedraw();
break;
case PANNING:
animator->stop();
setupPanningPlane(viewer->getSoRenderManager()->getCamera());
this->interactiveCountInc();
break;
case ZOOMING:
animator->stop();
this->interactiveCountInc();
break;
case BOXZOOM:
animator->stop();
this->interactiveCountInc();
break;
default: // include default to avoid compiler warnings.
break;
}
switch (oldmode) {
case SPINNING:
case DRAGGING:
viewer->showRotationCenter(false);
[[fallthrough]];
case PANNING:
case ZOOMING:
case BOXZOOM:
this->interactiveCountDec();
break;
default:
break;
}
viewer->setCursorRepresentation(newmode);
this->currentmode = newmode;
}
int NavigationStyle::getViewingMode() const
{
return (int)this->currentmode;
}
SbBool NavigationStyle::processEvent(const SoEvent* const ev)
{
// If we're in picking mode then all events must be redirected to the
// appropriate mouse model.
if (mouseSelection) {
int hd = mouseSelection->handleEvent(ev, viewer->getSoRenderManager()->getViewportRegion());
if (hd == AbstractMouseSelection::Continue || hd == AbstractMouseSelection::Restart) {
return true;
}
else if (hd == AbstractMouseSelection::Finish) {
pcPolygon = mouseSelection->getPositions();
selectedRole = mouseSelection->selectedRole();
delete mouseSelection;
mouseSelection = nullptr;
syncWithEvent(ev);
return NavigationStyle::processSoEvent(ev);
}
else if (hd == AbstractMouseSelection::Cancel) {
pcPolygon.clear();
delete mouseSelection;
mouseSelection = nullptr;
syncWithEvent(ev);
return NavigationStyle::processSoEvent(ev);
}
}
const ViewerMode curmode = this->currentmode;
SbBool processed = false;
processed = this->processSoEvent(ev);
// check for left click without selecting something
if ((curmode == NavigationStyle::SELECTION || curmode == NavigationStyle::IDLE) && !processed) {
if (SoMouseButtonEvent::isButtonReleaseEvent(ev, SoMouseButtonEvent::BUTTON1)) {
if (!ev->wasCtrlDown()) {
Gui::Selection().clearSelection();
}
}
}
return processed;
}
SbBool NavigationStyle::processSoEvent(const SoEvent* const ev)
{
bool processed = false;
bool offeredtoViewerEventBase = false;
// handle mouse wheel zoom
if (ev->isOfType(SoMouseWheelEvent::getClassTypeId())) {
auto const event = static_cast<const SoMouseWheelEvent*>(ev);
processed = processWheelEvent(event);
viewer->processSoEventBase(ev);
offeredtoViewerEventBase = true;
}
if (!processed && !offeredtoViewerEventBase) {
processed = viewer->processSoEventBase(ev);
}
return processed;
}
void NavigationStyle::syncWithEvent(const SoEvent* const ev)
{
// Events when in "ready-to-seek" mode are ignored, except those
// which influence the seek mode itself -- these are handled further
// up the inheritance hierarchy.
if (this->isSeekMode()) {
return;
}
const SoType type(ev->getTypeId());
// Mismatches in state of the modifier keys happens if the user
// presses or releases them outside the viewer window.
syncModifierKeys(ev);
// Keyboard handling
if (type.isDerivedFrom(SoKeyboardEvent::getClassTypeId())) {
auto const event = static_cast<const SoKeyboardEvent*>(ev);
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
switch (event->getKey()) {
case SoKeyboardEvent::LEFT_CONTROL:
case SoKeyboardEvent::RIGHT_CONTROL:
this->ctrldown = press;
break;
case SoKeyboardEvent::LEFT_SHIFT:
case SoKeyboardEvent::RIGHT_SHIFT:
this->shiftdown = press;
break;
case SoKeyboardEvent::LEFT_ALT:
case SoKeyboardEvent::RIGHT_ALT:
this->altdown = press;
break;
default:
break;
}
}
// Mouse Button / Spaceball Button handling
if (type.isDerivedFrom(SoMouseButtonEvent::getClassTypeId())) {
auto const event = static_cast<const SoMouseButtonEvent*>(ev);
const int button = event->getButton();
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
#ifdef FC_DEBUG
SoDebugError::postInfo("processSoEvent", "button = %d", button);
#endif
switch (button) {
case SoMouseButtonEvent::BUTTON1:
this->button1down = press;
break;
case SoMouseButtonEvent::BUTTON2:
this->button2down = press;
break;
case SoMouseButtonEvent::BUTTON3:
this->button3down = press;
break;
default:
break;
}
}
}
SbBool NavigationStyle::processMotionEvent(const SoMotion3Event* const ev)
{
SoCamera* const camera = viewer->getSoRenderManager()->getCamera();
if (!camera) {
return false;
}
SbViewVolume volume(camera->getViewVolume());
SbVec3f center(volume.getSightPoint(camera->focalDistance.getValue()));
float scale(volume.getWorldToScreenScale(center, 1.0));
float translationFactor = scale * .0001;
SbVec3f dir = ev->getTranslation();
if (camera->getTypeId().isDerivedFrom(SoOrthographicCamera::getClassTypeId())) {
auto oCam = static_cast<SoOrthographicCamera*>(camera);
oCam->scaleHeight(1.0 + (dir[2] * 0.0001));
dir[2] = 0.0; // don't move the cam for z translation.
}
SbRotation newRotation(ev->getRotation() * camera->orientation.getValue());
SbVec3f newPosition, newDirection;
newRotation.multVec(SbVec3f(0.0, 0.0, -1.0), newDirection);
newPosition = center - (newDirection * camera->focalDistance.getValue());
newRotation.multVec(dir, dir);
SbVec3f finalPosition = newPosition + (dir * translationFactor);
camera->enableNotify(false);
camera->orientation.setValue(newRotation);
camera->position = finalPosition;
camera->enableNotify(true);
camera->touch();
return true;
}
SbBool NavigationStyle::processKeyboardEvent(const SoKeyboardEvent* const event)
{
SbBool processed = false;
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
switch (event->getKey()) {
case SoKeyboardEvent::LEFT_CONTROL:
case SoKeyboardEvent::RIGHT_CONTROL:
this->ctrldown = press;
break;
case SoKeyboardEvent::LEFT_SHIFT:
case SoKeyboardEvent::RIGHT_SHIFT:
this->shiftdown = press;
break;
case SoKeyboardEvent::LEFT_ALT:
case SoKeyboardEvent::RIGHT_ALT:
this->altdown = press;
break;
case SoKeyboardEvent::S:
case SoKeyboardEvent::HOME:
case SoKeyboardEvent::LEFT_ARROW:
case SoKeyboardEvent::UP_ARROW:
case SoKeyboardEvent::RIGHT_ARROW:
case SoKeyboardEvent::DOWN_ARROW:
if (!this->isViewing()) {
this->setViewing(true);
}
break;
case SoKeyboardEvent::PAGE_UP: {
processed = true;
const SbVec2f posn = normalizePixelPos(event->getPosition());
doZoom(viewer->getSoRenderManager()->getCamera(), getDelta(), posn);
break;
}
case SoKeyboardEvent::PAGE_DOWN: {
processed = true;
const SbVec2f posn = normalizePixelPos(event->getPosition());
doZoom(viewer->getSoRenderManager()->getCamera(), -getDelta(), posn);
break;
}
default:
break;
}
return processed;
}
SbBool NavigationStyle::processClickEvent(const SoMouseButtonEvent* const event)
{
// issue #0002433: avoid to swallow the UP event if down the
// scene graph somewhere a dialog gets opened
SbBool processed = false;
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
if (press) {
SbTime tmp = (event->getTime() - mouseDownConsumedEvent.getTime());
float dci = (float)QApplication::doubleClickInterval() / 1000.0f;
// a double-click?
if (tmp.getValue() < dci) {
mouseDownConsumedEvent = *event;
mouseDownConsumedEvent.setTime(event->getTime());
processed = true;
}
else {
mouseDownConsumedEvent.setTime(event->getTime());
// 'ANY' is used to mark that we don't know yet if it will
// be a double-click event.
mouseDownConsumedEvent.setButton(SoMouseButtonEvent::ANY);
}
}
else if (!press) {
if (mouseDownConsumedEvent.getButton() == SoMouseButtonEvent::BUTTON1) {
// now handle the postponed event
NavigationStyle::processSoEvent(&mouseDownConsumedEvent);
mouseDownConsumedEvent.setButton(SoMouseButtonEvent::ANY);
}
}
return processed;
}
SbBool NavigationStyle::processWheelEvent(const SoMouseWheelEvent* const event)
{
const SbVec2s pos(event->getPosition());
const SbVec2f posn = normalizePixelPos(pos);
// handle mouse wheel zoom
doZoom(viewer->getSoRenderManager()->getCamera(), event->getDelta(), posn);
return true;
}
void NavigationStyle::setPopupMenuEnabled(const SbBool on)
{
this->menuenabled = on;
}
SbBool NavigationStyle::isPopupMenuEnabled() const
{
return this->menuenabled;
}
void NavigationStyle::openPopupMenu(const SbVec2s& position)
{
// store the right-click position for potential use by Clarify Selection
rightClickPosition = position;
// ask workbenches and view provider, ...
MenuItem view;
Gui::Application::Instance->setupContextMenu("View", &view);
auto contextMenu = new QMenu(viewer->getGLWidget());
MenuManager::getInstance()->setupContextMenu(&view, *contextMenu);
contextMenu->setAttribute(Qt::WA_DeleteOnClose);
// Add Clarify Selection option if there are objects under cursor
bool separator = false;
auto posAction = !contextMenu->actions().empty() ? contextMenu->actions().front() : nullptr;
// Get picked objects at position
SoRayPickAction rp(viewer->getSoRenderManager()->getViewportRegion());
rp.setPoint(position);
rp.setRadius(viewer->getPickRadius());
rp.setPickAll(true);
rp.apply(viewer->getSoRenderManager()->getSceneGraph());
const SoPickedPointList& pplist = rp.getPickedPointList();
QAction* pickAction = nullptr;
if (pplist.getLength() > 0) {
separator = true;
if (auto cmd
= Application::Instance->commandManager().getCommandByName("Std_ClarifySelection")) {
pickAction = new QAction(cmd->getAction()->text(), contextMenu);
pickAction->setShortcut(cmd->getAction()->shortcut());
}
else {
pickAction = new QAction(QObject::tr("Clarify Selection"), contextMenu);
}
if (posAction) {
contextMenu->insertAction(posAction, pickAction);
contextMenu->insertSeparator(posAction);
}
else {
contextMenu->addAction(pickAction);
}
}
if (separator && posAction) {
contextMenu->insertSeparator(posAction);
}
auto clarifyFunction = [pickAction](QAction* selectedAction) {
if (selectedAction == pickAction) {
// Execute the Clarify Selection command at this position
auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection");
if (cmd && cmd->isActive()) {
cmd->invoke(0); // required placeholder value - we don't use group command
}
}
};
QObject::connect(contextMenu, &QMenu::triggered, clarifyFunction);
contextMenu->popup(QCursor::pos());
rightClickPosition.reset();
}
PyObject* NavigationStyle::getPyObject()
{
if (pythonObject.is(nullptr)) {
// ref counter is set to 1
pythonObject = Py::asObject(new NavigationStylePy(this));
}
return Py::new_reference_to(pythonObject);
}
// ----------------------------------------------------------------------------------
TYPESYSTEM_SOURCE_ABSTRACT(Gui::UserNavigationStyle, Gui::NavigationStyle)
std::string UserNavigationStyle::userFriendlyName() const
{
std::string name = this->getTypeId().getName();
// remove namespaces
std::size_t pos = name.rfind("::");
if (pos != std::string::npos) {
name = name.substr(pos + 2);
}
// remove 'NavigationStyle'
pos = name.find("NavigationStyle");
if (pos != std::string::npos) {
name = name.substr(0, pos);
}
return name;
}
std::map<Base::Type, std::string> UserNavigationStyle::getUserFriendlyNames()
{
std::map<Base::Type, std::string> names;
std::vector<Base::Type> types;
Base::Type::getAllDerivedFrom(UserNavigationStyle::getClassTypeId(), types);
for (auto& type : types) {
if (type != UserNavigationStyle::getClassTypeId()) {
std::unique_ptr<UserNavigationStyle> inst(
static_cast<UserNavigationStyle*>(type.createInstance())
);
if (inst) {
names[type] = inst->userFriendlyName();
}
}
}
return names;
}