Files
qelectrotech-source-mirror/sources/spacemouse/spacemousemotion.cpp
T
ispyisail aaeaff55cc Add a hidapi 3D mouse backend that needs no 3Dconnexion driver
The 3D mouse only worked on Linux, through spacenavd. This adds a second
backend that reads the device directly over USB through hidapi, with no
3Dconnexion driver or SDK: the route to Windows and macOS (discussion
#599), and usable on Linux without spacenavd.

SpaceMouseHid decodes the raw reports from the device's own report
descriptor -- where each axis and button sits, its range, absolute or
relative -- so no per-model table is needed, with the classic report
1/2/3 layout as a fallback when the descriptor cannot be read and the
0x1c button list newer devices send. Absolute axes are rescaled to
+-500 exactly as spacenavd does, so both backends give QET the same
values. HidBackend polls from the main thread (fast while moving, slow
when still), emits one sample per poll, and looks for a device every 3 s
so plugging one in or back in needs no restart.

QET_SPACEMOUSE_BACKEND (auto, spnav, hid) picks the backend; auto keeps
libspnav on Linux when it is found and uses hidapi otherwise. hidapi is
found through pkg-config as hidapi-hidraw (Linux) or hidapi (MSYS2,
Homebrew).

A sample arriving in the same millisecond as the previous one now counts
for no time instead of a full period, so a burst of queued samples no
longer moves the view further than the time it covers.

Tested without a device: tst_spacemousehid (descriptor parsing, broken
and hostile descriptors, every report form, recordings from real devices
once they are added to fixtures/spacemouse), and end to end on Linux
through a virtual USB device created with /dev/uhid: the same moves give
byte-identical screenshots through the hidapi and libspnav backends, an
absolute axis is rescaled as spacenavd does, buttons trigger their
bound action, and unplugging and replugging while QET runs (including
with a dialog open that a device button opened) reconnects cleanly.
Not tested on Windows, macOS or real hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:55:30 +12:00

148 lines
4.9 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "spacemousemotion.h"
#include <QSettings>
#include <QtMath>
namespace {
//A device deflection is an integer on roughly the same order of
//magnitude as a QWheelEvent::angleDelta() tick (about +-120 per
//detent, more under a hard push/twist -- exact range depends on the
//backend and the driver's own sensitivity). DiagramView::wheelEvent()
//turns such a tick into a zoom step of about 1 + value/1000, reused
//here as a starting point.
//
//Neither constant has been calibrated against real hardware; the
//user-facing speed settings scale both.
constexpr qreal ZOOM_DIVISOR = 1000.0;
constexpr qreal PAN_SCALE = 1.0;
const QString GROUP = QStringLiteral("spacemouse/motion/");
}
/**
@brief SpaceMouseSettings::load
@return the settings saved by the configuration page, or the defaults
*/
SpaceMouseSettings SpaceMouseSettings::load()
{
QSettings settings;
SpaceMouseSettings s;
s.pan_speed = settings.value(GROUP + "pan_speed", s.pan_speed).toInt();
s.zoom_speed = settings.value(GROUP + "zoom_speed", s.zoom_speed).toInt();
s.dead_zone = settings.value(GROUP + "dead_zone", s.dead_zone).toInt();
s.invert_pan_x = settings.value(GROUP + "invert_pan_x", s.invert_pan_x).toBool();
s.invert_pan_y = settings.value(GROUP + "invert_pan_y", s.invert_pan_y).toBool();
s.invert_zoom = settings.value(GROUP + "invert_zoom", s.invert_zoom).toBool();
s.zoom_axis = settings.value(GROUP + "zoom_axis").toString() == QLatin1String("twist")
? ZoomAxis::Twist
: ZoomAxis::PushPull;
return s;
}
/**
@brief SpaceMouseSettings::save
*/
void SpaceMouseSettings::save() const
{
QSettings settings;
settings.setValue(GROUP + "pan_speed", pan_speed);
settings.setValue(GROUP + "zoom_speed", zoom_speed);
settings.setValue(GROUP + "dead_zone", dead_zone);
settings.setValue(GROUP + "invert_pan_x", invert_pan_x);
settings.setValue(GROUP + "invert_pan_y", invert_pan_y);
settings.setValue(GROUP + "invert_zoom", invert_zoom);
settings.setValue(GROUP + "zoom_axis",
zoom_axis == ZoomAxis::Twist ? QStringLiteral("twist")
: QStringLiteral("push_pull"));
}
bool SpaceMouseSettings::operator==(const SpaceMouseSettings &other) const
{
return pan_speed == other.pan_speed
&& zoom_speed == other.zoom_speed
&& dead_zone == other.dead_zone
&& invert_pan_x == other.invert_pan_x
&& invert_pan_y == other.invert_pan_y
&& invert_zoom == other.invert_zoom
&& zoom_axis == other.zoom_axis;
}
/**
@brief SpaceMouseMotion::stepFor
@param elapsed_ms
@return see the declaration
*/
qreal SpaceMouseMotion::stepFor(qint64 elapsed_ms)
{
if (elapsed_ms < 0 || elapsed_ms > MAX_PERIOD_MS) {
return 1.0;
}
return elapsed_ms / NOMINAL_PERIOD_MS;
}
/**
@brief SpaceMouseMotion::applyDeadZone
@param value
@param dead_zone
@return see the declaration
*/
int SpaceMouseMotion::applyDeadZone(int value, int dead_zone)
{
if (dead_zone <= 0) {
return value;
}
if (qAbs(value) <= dead_zone) {
return 0;
}
return value > 0 ? value - dead_zone : value + dead_zone;
}
/**
@brief SpaceMouseMotion::map
Pan is a scrollbar delta: pushing the cap right moves the drawing right,
so the scrollbar goes the other way, as in DiagramView::wheelEvent().
Zoom is exponential in the deflection, so the factor is always positive
and an equal push and pull cancel out exactly.
@param sample
@param elapsed_ms
@param settings
@return see the declaration
*/
SpaceMouseViewMotion SpaceMouseMotion::map(const SpaceMouseSample &sample,
qint64 elapsed_ms,
const SpaceMouseSettings &settings)
{
const qreal step = stepFor(elapsed_ms);
const int x = applyDeadZone(sample.x, settings.dead_zone);
const int y = applyDeadZone(sample.y, settings.dead_zone);
const int zoom_value = applyDeadZone(
settings.zoom_axis == SpaceMouseSettings::ZoomAxis::Twist ? sample.rz : sample.z,
settings.dead_zone);
const qreal pan = PAN_SCALE * settings.pan_speed / 100.0 * step;
const qreal zoom = settings.zoom_speed / 100.0 / ZOOM_DIVISOR * step;
SpaceMouseViewMotion motion;
motion.scroll_x = -x * pan * (settings.invert_pan_x ? -1 : 1);
motion.scroll_y = -y * pan * (settings.invert_pan_y ? -1 : 1);
motion.zoom_factor = qExp(zoom_value * zoom * (settings.invert_zoom ? -1 : 1));
return motion;
}