Merge pull request #1025 from ispyisail/feature/spacemouse-hid

Add a 3D mouse backend that reads the device over USB (hidapi)
This commit is contained in:
Laurent Trinques
2026-09-25 04:12:35 +02:00
committed by GitHub
16 changed files with 1371 additions and 55 deletions
+197
View File
@@ -0,0 +1,197 @@
/*
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 "hidbackend.h"
#include <QTimer>
#include <hidapi.h>
namespace {
constexpr int SCAN_INTERVAL_MS = 3000;
constexpr int FAST_POLL_MS = 8; // devices report at up to ~125 Hz
constexpr int SLOW_POLL_MS = 50;
//Fast polls in a row with nothing to read before slowing down.
constexpr int IDLE_POLLS_BEFORE_SLOW = 125;
constexpr int MAX_REPORT_SIZE = 64;
constexpr int MAX_DESCRIPTOR_SIZE = 4096;
}
/**
@brief HidBackend::HidBackend
@param parent
*/
HidBackend::HidBackend(QObject *parent) :
SpaceMouseBackend(parent)
{
if (hid_init() != 0) {
return;
}
m_hid_initialised = true;
m_read_timer = new QTimer(this);
connect(m_read_timer, &QTimer::timeout, this, &HidBackend::readReports);
m_scan_timer = new QTimer(this);
m_scan_timer->setInterval(SCAN_INTERVAL_MS);
connect(m_scan_timer, &QTimer::timeout, this, &HidBackend::scan);
scan();
}
/**
@brief HidBackend::~HidBackend
*/
HidBackend::~HidBackend()
{
close();
if (m_hid_initialised) {
hid_exit();
}
}
/**
@brief HidBackend::scan
Open the first 3D mouse found, or keep looking every few seconds.
*/
void HidBackend::scan()
{
if (m_device) {
return;
}
hid_device_info *list = hid_enumerate(0, 0);
for (hid_device_info *info = list; info && !m_device; info = info->next)
{
if (!SpaceMouseHid::isSpaceMouse(info->vendor_id, info->product_id,
info->usage_page, info->usage)) {
continue;
}
m_device = hid_open_path(info->path);
}
hid_free_enumeration(list);
if (!m_device) {
m_scan_timer->start();
return;
}
m_scan_timer->stop();
hid_set_nonblocking(m_device, 1);
SpaceMouseHid::Layout layout;
#if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 14, 0)
unsigned char descriptor[MAX_DESCRIPTOR_SIZE];
const int size = hid_get_report_descriptor(m_device, descriptor, sizeof descriptor);
if (size > 0) {
layout = SpaceMouseHid::parseDescriptor(
QByteArray(reinterpret_cast<const char *>(descriptor), size));
}
#endif
if (!layout.hasAxes()) {
layout = SpaceMouseHid::fallbackLayout();
}
m_decoder = SpaceMouseHid::Decoder(layout);
m_idle_polls = 0;
m_read_timer->start(FAST_POLL_MS);
}
/**
@brief HidBackend::readReports
Drain every report waiting, emit what they mean, and adjust the polling
rate. A read error means the device went away (unplugged, or the
wireless receiver lost it): go back to looking for one.
*/
void HidBackend::readReports()
{
//A button can trigger an action that opens a dialog, whose event
//loop fires this timer again before the signal returns. That is
//fine -- the device keeps working in the dialog -- because the
//signals are emitted last, after every use of m_device below; this
//guard only keeps two reads from ever overlapping.
if (m_reading || !m_device) {
return;
}
m_reading = true;
unsigned char buffer[MAX_REPORT_SIZE];
bool got_any = false;
//Everything read in one poll is one moment: a device that sends
//translation and rotation as two reports, or two samples that
//queued up, gives one sample here -- the latest state -- so the
//listener's time scaling sees one sample per real interval.
bool moved = false;
SpaceMouseSample latest;
QList<int> pressed;
for (;;)
{
const int size = hid_read(m_device, buffer, sizeof buffer);
if (size == 0) {
break;
}
if (size < 0) {
close();
m_scan_timer->start();
break;
}
got_any = true;
const SpaceMouseHid::Decoder::Result result = m_decoder.feed(
QByteArray(reinterpret_cast<const char *>(buffer), size));
if (result.motion) {
moved = true;
latest = result.sample;
}
pressed += result.pressed;
}
if (m_device) {
if (got_any) {
m_idle_polls = 0;
if (m_read_timer->interval() != FAST_POLL_MS) {
m_read_timer->setInterval(FAST_POLL_MS);
}
} else if (++m_idle_polls == IDLE_POLLS_BEFORE_SLOW) {
m_read_timer->setInterval(SLOW_POLL_MS);
}
}
//Signals last: whatever they trigger (even the device going away
//during a dialog), nothing after them touches the device.
m_reading = false;
if (moved) {
emit motion(latest);
}
for (int button : pressed) {
emit buttonPressed(button);
}
}
/**
@brief HidBackend::close
*/
void HidBackend::close()
{
if (m_read_timer) {
m_read_timer->stop();
}
if (m_device) {
hid_close(m_device);
m_device = nullptr;
}
}
+69
View File
@@ -0,0 +1,69 @@
/*
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/>.
*/
#ifndef HIDBACKEND_H
#define HIDBACKEND_H
#include "spacemousebackend.h"
#include "spacemousehid.h"
class QTimer;
struct hid_device_;
/**
@brief The HidBackend class
SpaceMouseBackend that reads the device directly over USB through
hidapi, with no 3Dconnexion driver or SDK: the backend for Windows and
macOS, and for Linux without spacenavd. Only compiled in when
QET_SPACEMOUSE_BACKEND_HID is defined (cmake/find_spacemouse.cmake).
The raw reports are decoded by SpaceMouseHid, from the device's own
report descriptor, into the same values spacenavd would give.
hidapi has no event to wait on, so the device is polled from the main
thread: fast while it is moving, slowly once it has been still for a
moment. With no device, it looks for one every few seconds, so plugging
one in works without restarting QElectroTech. As with every backend,
no device is the normal case and is never reported as an error.
*/
class HidBackend : public SpaceMouseBackend
{
Q_OBJECT
public:
explicit HidBackend(QObject *parent = nullptr);
~HidBackend() override;
bool isAvailable() const override { return m_device != nullptr; }
private slots:
void scan();
void readReports();
private:
void close();
bool m_hid_initialised = false;
hid_device_ *m_device = nullptr;
SpaceMouseHid::Decoder m_decoder;
QTimer *m_scan_timer = nullptr;
QTimer *m_read_timer = nullptr;
int m_idle_polls = 0;
bool m_reading = false;
};
#endif // HIDBACKEND_H
+8 -15
View File
@@ -27,23 +27,16 @@
Platform seam for discussion #599's 3D mouse support. A backend owns
one platform's connection to the actual 6-DOF device driver -- opening
it, pumping whatever event source that platform uses, closing it -- and
reports motion through the one signal below. Everything platform-
independent (which DiagramView to apply motion to, the pan/zoom
primitives to call, the Z-to-zoom-factor mapping) lives in
SpaceMouseListener instead, once, so it does not have to be duplicated
reports motion through the signals below. Everything platform-
independent (which view to apply motion to, the pan/zoom primitives to
call, the sample-to-motion mapping) lives in SpaceMouseListener and
SpaceMouseMotion instead, once, so it does not have to be duplicated
or re-verified per backend.
SpnavBackend (Linux, spacenavd/libspnav) is the only implementation so
far -- built, linked, and its "no daemon/device present" path actually
run in the environment this was written in. A Windows/macOS backend
(3Dconnexion's proprietary 3DxWare SDK) would implement this same
interface and be selected in SpaceMouseListener's constructor, without
changing SpnavBackend or anything downstream of the motion signal.
Deliberately not attempted here: this was written on Linux with no
3DxWare SDK and no Windows/macOS toolchain available to compile,
link, or run a single line of it against, and shipping platform code
that has never even built would be a materially different, weaker
thing than everything else in this feature.
Two implementations, chosen at build time (cmake/find_spacemouse.cmake):
SpnavBackend (Linux, through spacenavd/libspnav) and HidBackend (any
platform, directly over USB through hidapi, with no 3Dconnexion driver
or SDK). Both report the same values for the same movement.
*/
class SpaceMouseBackend : public QObject
{
+357
View File
@@ -0,0 +1,357 @@
/*
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 "spacemousehid.h"
#include <QHash>
#include <algorithm>
namespace {
constexpr unsigned short PAGE_GENERIC_DESKTOP = 0x01;
constexpr unsigned short PAGE_BUTTON = 0x09;
constexpr unsigned short USAGE_MULTI_AXIS = 0x08;
constexpr unsigned short USAGE_X = 0x30; // X, Y, Z, Rx, Ry, Rz follow
//Newer devices (SpaceMouse Enterprise, the wireless ones) report the
//buttons held as a list of 16-bit button numbers in this report,
//outside the Button usage page.
constexpr int REPORT_BUTTON_LIST = 0x1c;
//spacenavd rescales absolute axes to this range, so doing the same
//makes both backends hand QET the same numbers.
constexpr int SCALED_MIN = -500;
constexpr int SCALED_MAX = 500;
//Limits on what a descriptor can make the parser walk.
constexpr quint32 MAX_FIELDS = 1024;
constexpr qint64 MAX_BITS = 1 << 20;
//3D mice sold under Logitech's vendor id, for the ones whose
//interface does not report a usage.
const QSet<unsigned short> LOGITECH_3D_MICE = {
0xc603, 0xc605, 0xc606, 0xc621, 0xc623, 0xc625,
0xc626, 0xc627, 0xc628, 0xc629, 0xc62b
};
/// Read \a size bits at \a offset, little-endian as HID packs them.
bool extract(const QByteArray &payload, int offset, int size, bool is_signed, int *value)
{
if (size <= 0 || size > 32 || offset < 0
|| (offset + size + 7) / 8 > payload.size()) {
return false;
}
quint32 raw = 0;
for (int b = 0; b < size; ++b) {
const int bit = offset + b;
if (static_cast<quint8>(payload.at(bit / 8)) & (1u << (bit % 8))) {
raw |= 1u << b;
}
}
if (is_signed && size < 32 && (raw & (1u << (size - 1)))) {
raw |= ~((1u << size) - 1);
}
*value = static_cast<qint32>(raw);
return true;
}
int scaled(const SpaceMouseHid::Field &field, int value)
{
if (field.relative || field.logical_max <= field.logical_min) {
return value;
}
const qint64 range = qint64(field.logical_max) - field.logical_min;
return static_cast<int>((qint64(value) - field.logical_min)
* (SCALED_MAX - SCALED_MIN) / range + SCALED_MIN);
}
}
bool SpaceMouseHid::isSpaceMouse(unsigned short vendor, unsigned short product,
unsigned short usage_page, unsigned short usage)
{
const bool multi_axis = usage_page == PAGE_GENERIC_DESKTOP && usage == USAGE_MULTI_AXIS;
const bool usage_unknown = usage_page == 0 && usage == 0;
if (vendor == VENDOR_3DCONNEXION) {
return multi_axis || usage_unknown;
}
if (vendor == VENDOR_LOGITECH) {
return multi_axis || (usage_unknown && LOGITECH_3D_MICE.contains(product));
}
return false;
}
bool SpaceMouseHid::Layout::hasAxes() const
{
for (const Field &f : axes) {
if (f.isValid()) {
return true;
}
}
return false;
}
/**
@brief SpaceMouseHid::parseDescriptor
Walks the short items of a HID report descriptor (HID 1.11, 6.2.2),
keeping only what the decoder needs: the Input fields carrying the six
Generic Desktop axes and the Button page.
@param descriptor
@return see the declaration
*/
SpaceMouseHid::Layout SpaceMouseHid::parseDescriptor(const QByteArray &descriptor)
{
struct Globals {
quint32 usage_page = 0;
qint32 logical_min = 0;
qint32 logical_max = 0;
quint32 report_size = 0;
quint32 report_count = 0;
quint32 report_id = 0;
};
Layout layout;
Globals globals;
QList<Globals> stack;
QList<quint32> usages; // full 32-bit usages (page << 16 | id)
quint32 usage_min = 0, usage_max = 0;
bool have_range = false;
QHash<quint32, int> next_bit; // per report id
auto clearLocals = [&]() {
usages.clear();
have_range = false;
usage_min = usage_max = 0;
};
auto fullUsage = [&](quint32 value, int size) {
return size == 4 ? value : (globals.usage_page << 16) | value;
};
int i = 0;
const int n = descriptor.size();
while (i < n)
{
const quint8 prefix = static_cast<quint8>(descriptor.at(i));
if (prefix == 0xfe) { // long item: never used by these devices
if (i + 1 >= n) break;
i += 3 + static_cast<quint8>(descriptor.at(i + 1));
continue;
}
const int size = (prefix & 0x03) == 3 ? 4 : (prefix & 0x03);
const int type = (prefix >> 2) & 0x03;
const int tag = prefix >> 4;
if (i + 1 + size > n) break;
quint32 uvalue = 0;
for (int b = 0; b < size; ++b) {
uvalue |= quint32(static_cast<quint8>(descriptor.at(i + 1 + b))) << (8 * b);
}
qint32 svalue = static_cast<qint32>(uvalue);
if (size > 0 && size < 4 && (uvalue & (1u << (8 * size - 1)))) {
svalue = static_cast<qint32>(uvalue | ~((1u << (8 * size)) - 1));
}
i += 1 + size;
if (type == 1) // global
{
switch (tag) {
case 0: globals.usage_page = uvalue; break;
case 1: globals.logical_min = svalue; break;
case 2: globals.logical_max = svalue; break;
case 7: globals.report_size = uvalue; break;
case 8: globals.report_id = uvalue; layout.numbered_reports = true; break;
case 9: globals.report_count = uvalue; break;
case 10: stack.append(globals); break;
case 11: if (!stack.isEmpty()) globals = stack.takeLast(); break;
}
}
else if (type == 2) // local
{
switch (tag) {
case 0: usages.append(fullUsage(uvalue, size)); break;
case 1: usage_min = fullUsage(uvalue, size); have_range = true; break;
case 2: usage_max = fullUsage(uvalue, size); have_range = true; break;
}
}
else if (type == 0) // main
{
if (tag == 8) // Input
{
const bool constant = uvalue & 0x01;
const bool variable = uvalue & 0x02;
const bool relative = uvalue & 0x04;
int &bit = next_bit[globals.report_id];
//A real report is at most a few hundred bytes; a count
//beyond that is a broken (or hostile) descriptor, and
//walking it field by field would stall the application.
const quint32 count = qMin<quint32>(globals.report_count, MAX_FIELDS);
for (quint32 k = 0; k < count; ++k)
{
quint32 usage = 0;
if (have_range && usage_max >= usage_min) {
usage = qMin(usage_min + k, usage_max);
} else if (!usages.isEmpty()) {
usage = usages.at(qMin<int>(k, usages.size() - 1));
}
Field field;
field.report_id = static_cast<int>(globals.report_id);
field.bit_offset = bit + static_cast<int>(k * globals.report_size);
field.bit_size = static_cast<int>(globals.report_size);
field.logical_min = globals.logical_min;
field.logical_max = globals.logical_max;
field.relative = relative;
if (!constant && variable) {
const quint32 page = usage >> 16, id = usage & 0xffff;
if (page == PAGE_GENERIC_DESKTOP && id >= USAGE_X && id < USAGE_X + 6) {
layout.axes[id - USAGE_X] = field;
} else if (page == PAGE_BUTTON && id >= 1 && id <= 64) {
if (layout.buttons.size() < int(id)) {
layout.buttons.resize(id);
}
layout.buttons[id - 1] = field;
}
}
}
bit = static_cast<int>(qMin<qint64>(
bit + qint64(count) * globals.report_size, MAX_BITS));
}
clearLocals(); // every main item (Input, Collection, ...) ends the locals
}
}
return layout;
}
SpaceMouseHid::Layout SpaceMouseHid::fallbackLayout()
{
Layout layout;
layout.numbered_reports = true;
for (int a = 0; a < 6; ++a) {
Field &f = layout.axes[a];
f.report_id = a < 3 ? 1 : 2;
f.bit_offset = (a % 3) * 16;
f.bit_size = 16;
f.logical_min = -32768; // signed, and relative: passed through as sent
f.logical_max = 32767;
f.relative = true;
}
layout.buttons.resize(32);
for (int b = 0; b < 32; ++b) {
Field &f = layout.buttons[b];
f.report_id = 3;
f.bit_offset = b;
f.bit_size = 1;
f.logical_max = 1;
}
return layout;
}
SpaceMouseHid::Decoder::Decoder(const Layout &layout) :
m_layout(layout)
{}
/**
@brief SpaceMouseHid::Decoder::feed
@param report : one report as read from the device, report id first
when the device numbers its reports
@return see Result
*/
SpaceMouseHid::Decoder::Result SpaceMouseHid::Decoder::feed(const QByteArray &report)
{
Result result;
if (report.isEmpty()) {
return result;
}
const int id = m_layout.numbered_reports ? static_cast<quint8>(report.at(0)) : 0;
const QByteArray payload = m_layout.numbered_reports ? report.mid(1) : report;
for (int a = 0; a < 6; ++a)
{
const Field &f = m_layout.axes[a];
int value = 0;
if (f.isValid() && f.report_id == id
&& extract(payload, f.bit_offset, f.bit_size, f.logical_min < 0, &value)) {
m_axes[a] = scaled(f, value);
result.motion = true;
}
}
//Some devices send rotation after translation in one longer report
//1, while their layout (or the fallback, which has no descriptor)
//puts rotation in a report of its own: read it from report 1 too.
if (id == 1 && payload.size() >= 12
&& m_layout.axes[3].isValid() && m_layout.axes[3].report_id != 1) {
for (int a = 3; a < 6; ++a) {
const Field &f = m_layout.axes[a];
int value = 0;
if (extract(payload, 48 + f.bit_offset, f.bit_size, f.logical_min < 0, &value)) {
m_axes[a] = scaled(f, value);
}
}
}
QSet<int> down;
bool buttons_in_report = false;
for (int b = 0; b < m_layout.buttons.size(); ++b)
{
const Field &f = m_layout.buttons.at(b);
int value = 0;
if (f.isValid() && f.report_id == id
&& extract(payload, f.bit_offset, f.bit_size, false, &value)) {
buttons_in_report = true;
if (value) {
down.insert(b);
}
}
}
if (!buttons_in_report && m_layout.numbered_reports && id == REPORT_BUTTON_LIST)
{
buttons_in_report = true;
for (int offset = 0; offset + 1 < payload.size(); offset += 2) {
const int number = static_cast<quint8>(payload.at(offset))
| static_cast<quint8>(payload.at(offset + 1)) << 8;
//0 means no button, so the numbers start at 1: count from
//0 like the Button page and spacenavd do.
if (number) {
down.insert(number - 1);
}
}
}
if (buttons_in_report)
{
for (int b : down) {
if (!m_down.contains(b)) {
result.pressed.append(b);
}
}
std::sort(result.pressed.begin(), result.pressed.end());
m_down = down;
}
if (result.motion) {
result.sample.x = m_axes[0];
result.sample.y = m_axes[1];
result.sample.z = m_axes[2];
result.sample.rx = m_axes[3];
result.sample.ry = m_axes[4];
result.sample.rz = m_axes[5];
}
return result;
}
+107
View File
@@ -0,0 +1,107 @@
/*
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/>.
*/
#ifndef SPACEMOUSEHID_H
#define SPACEMOUSEHID_H
#include "spacemousemotion.h"
#include <QByteArray>
#include <QList>
#include <QSet>
#include <QVector>
/**
Decoding of a 3D mouse's raw USB HID reports, for the backends that read
the device directly (HidBackend) rather than through a driver that has
already decoded them (spacenavd). No I/O here, so it is unit-tested from
recorded bytes (tests/qttest/tst_spacemousehid).
*/
namespace SpaceMouseHid
{
/// USB vendor ids 3Dconnexion devices use: Logitech's for the older
/// ones, 3Dconnexion's own for everything since about 2011.
constexpr unsigned short VENDOR_LOGITECH = 0x046d;
constexpr unsigned short VENDOR_3DCONNEXION = 0x256f;
/// @return whether a HID interface is a 3D mouse: a 3Dconnexion
/// vendor id and the Generic Desktop "multi-axis controller" usage.
/// Logitech devices that do not report a usage are matched by
/// product id instead, from the list of their 3D mice.
bool isSpaceMouse(unsigned short vendor, unsigned short product,
unsigned short usage_page, unsigned short usage);
/// Where one value sits in the reports, per the report descriptor.
struct Field
{
int report_id = 0; ///< 0 when the device does not number its reports
int bit_offset = 0; ///< after the report id byte, if any
int bit_size = 0;
int logical_min = 0;
int logical_max = 0;
bool relative = false;
bool isValid() const { return bit_size > 0; }
};
/// What the report descriptor says about the six axes and the buttons.
struct Layout
{
Field axes[6]; ///< x, y, z, rx, ry, rz
QVector<Field> buttons; ///< button n (0-based) is buttons[n]
bool numbered_reports = false;
bool hasAxes() const;
};
/// @return the layout described by \a descriptor, or one where
/// hasAxes() is false if it describes no axes (or is empty)
Layout parseDescriptor(const QByteArray &descriptor);
/// @return the fixed layout of the classic 3Dconnexion reports
/// (1 = translation, 2 = rotation, 3 = button bitmask), used when
/// the descriptor cannot be read
Layout fallbackLayout();
/**
@brief The Decoder class
Turns a stream of raw reports into samples and button presses.
A device can send translation and rotation in separate reports, so
the decoder keeps the last value of every axis and reports all six
each time any of them changes.
*/
class Decoder
{
public:
struct Result
{
bool motion = false; ///< sample is new
SpaceMouseSample sample;
QList<int> pressed; ///< buttons pressed since the previous report
};
explicit Decoder(const Layout &layout = fallbackLayout());
Result feed(const QByteArray &report);
private:
Layout m_layout;
int m_axes[6] = {0, 0, 0, 0, 0, 0};
QSet<int> m_down; ///< buttons currently held
};
}
#endif // SPACEMOUSEHID_H
+6 -4
View File
@@ -22,6 +22,9 @@
#ifdef QET_SPACEMOUSE_BACKEND_SPNAV
# include "spnavbackend.h"
#endif
#ifdef QET_SPACEMOUSE_BACKEND_HID
# include "hidbackend.h"
#endif
#include "../diagramview.h"
#include "../editor/elementview.h"
@@ -45,12 +48,11 @@ SpaceMouseListener::SpaceMouseListener(QObject *parent) :
QObject(parent),
m_settings(SpaceMouseSettings::load())
{
#ifdef QET_SPACEMOUSE_BACKEND_SPNAV
#if defined(QET_SPACEMOUSE_BACKEND_SPNAV)
m_backend = new SpnavBackend(this);
#elif defined(QET_SPACEMOUSE_BACKEND_HID)
m_backend = new HidBackend(this);
#endif
//A future Windows/macOS backend (3Dconnexion's proprietary 3DxWare
//SDK) slots in here behind its own QET_SPACEMOUSE_BACKEND_3DXWARE
//guard, without changing anything below this constructor.
if (m_backend) {
connect(m_backend, &SpaceMouseBackend::motion,
+1 -1
View File
@@ -91,7 +91,7 @@ bool SpaceMouseSettings::operator==(const SpaceMouseSettings &other) const
*/
qreal SpaceMouseMotion::stepFor(qint64 elapsed_ms)
{
if (elapsed_ms <= 0 || elapsed_ms > MAX_PERIOD_MS) {
if (elapsed_ms < 0 || elapsed_ms > MAX_PERIOD_MS) {
return 1.0;
}
return elapsed_ms / NOMINAL_PERIOD_MS;
+3 -1
View File
@@ -85,7 +85,9 @@ namespace SpaceMouseMotion
constexpr qint64 MAX_PERIOD_MS = 100;
/// @return how many nominal periods a sample covers, given the
/// milliseconds since the previous one (negative = no previous one)
/// milliseconds since the previous one: none for a sample in the
/// same millisecond (it covers no time), one for the first sample
/// or the first after a rest (elapsed negative or over the maximum)
qreal stepFor(qint64 elapsed_ms);
/// @return \a value with \a dead_zone removed: 0 inside it, and