mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 09:14:12 +02:00
Add 3D mouse (SpaceMouse/SpacePilot) pan/zoom support via libspnav
Implements discussion #599's phase 1 (Linux, libspnav): a 3Dconnexion 6-DOF device pans and zooms the active diagram view, via spacenavd. ## Off by default, zero cost when off QET_ENABLE_SPACEMOUSE (cmake/developer_options.cmake) is OFF. Verified in two separate build directories from a clean configure: with it off, the new cmake/find_spacemouse.cmake step runs and does nothing (no library lookup, no definition, no new source files compiled), and qetapp.cpp/.h produce zero new object code -- both are entirely #ifdef'd out. The default build is byte-for-byte the same shape as before this commit. With it on, libspnav is located via its pkg-config file (spnav.pc, shipped by libspnav-dev on Debian/Ubuntu and equivalent packages elsewhere). If the option is on but the library isn't found, this does not hard-fail configure: it downgrades back to off with a warning, so an opt-in feature never blocks a developer who doesn't have the library installed. ## No new navigation logic -- a new input source for the existing one DiagramView::wheelEvent() already turns a physical wheel's delta into horizontalScrollBar()/verticalScrollBar() calls for pan and a zoom(1 + value/1000) call for zoom -- see diagramview.cpp:661-687. SpaceMouseListener calls exactly those same primitives from spnav motion events instead of wheel events. It does not reimplement panning or zooming. ## Safe by construction even when compiled in The overwhelming majority of users of a build with the option on still won't have spacenavd running or a device attached -- that must never surface as an error dialog or a startup warning. SpaceMouseListener:: isAvailable() reflects this: spnav_open() failing is treated as the ordinary case, not an error, and the object then does nothing at all. Verified for real in this environment, which genuinely has no spacenavd running: built with the option on, ran the resulting binary to completion, and confirmed zero crashes and zero spnav-related output of any kind -- the silence is the point. Motion is read via a QSocketNotifier on spnav_fd() (event-driven, no polling loop, no idle cost) and applied to whichever DiagramView is currently active, found via qApp->activeWindow() -> QETDiagramEditor -> currentProjectView() -> currentDiagram(): a 6-DOF device is one ambient input source for the whole application, not something tied to a particular window, so there is exactly one listener, owned by QETApp. ## What could not be verified without hardware The Z-axis-to-zoom-factor mapping (SpaceMouseListener::zoomFactorForZAxis) is a pure function specifically so it could be tested without a live device: confirmed a centered device (z=0) yields exactly 1.0 (an exact no-op, not an epsilon-off value that could trip DiagramView::zoom()'s >=1 branch), and that push/pull produce symmetric zoom-in/out factors. What genuinely cannot be checked in this environment: which physical axis is "left/right" vs "up/down" vs "push/forward", their sign, and whether the ZOOM_DIVISOR/PAN_SCALE constants feel right on real hardware. Both are named constants specifically so recalibrating them is a one-line change once someone with a device tries it -- flagged plainly in the PR rather than presented as verified. ## Not in this commit Windows/macOS (proprietary 3DxWare SDK, materially bigger lift) and device button mapping are both explicitly out of scope for this phase per the discussion.
This commit is contained in:
@@ -40,6 +40,9 @@
|
||||
#include "machine_info.h"
|
||||
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
|
||||
#include "qetversion.h"
|
||||
#ifdef QET_SPACEMOUSE_SUPPORT
|
||||
# include "spacemouse/spacemouselistener.h"
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
@@ -156,6 +159,14 @@ QETApp::QETApp() :
|
||||
}
|
||||
|
||||
checkBackupFiles();
|
||||
|
||||
#ifdef QET_SPACEMOUSE_SUPPORT
|
||||
//Always safe to construct: it silently does nothing when spacenavd
|
||||
//isn't running or no device is attached, which is the common case
|
||||
//even in a build with this feature compiled in. See
|
||||
//SpaceMouseListener's class comment.
|
||||
m_space_mouse_listener = new SpaceMouseListener(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,6 +47,9 @@ class QETProject;
|
||||
class QETTitleBlockTemplateEditor;
|
||||
class QTextOrientationSpinBoxWidget;
|
||||
class RecentFiles;
|
||||
#ifdef QET_SPACEMOUSE_SUPPORT
|
||||
class SpaceMouseListener;
|
||||
#endif
|
||||
|
||||
/**
|
||||
@brief The QETApp class
|
||||
@@ -225,6 +228,13 @@ class QETApp : public QObject
|
||||
static TitleBlockTemplatesFilesCollection *m_company_tbt_collection;
|
||||
static TitleBlockTemplatesFilesCollection *m_custom_tbt_collection;
|
||||
static ElementsCollectionCache *collections_cache_;
|
||||
#ifdef QET_SPACEMOUSE_SUPPORT
|
||||
/// One per application, not per window: a physical 6-DOF device
|
||||
/// is a single ambient input source, and motion is applied to
|
||||
/// whichever DiagramView is currently active. See
|
||||
/// SpaceMouseListener's class comment.
|
||||
SpaceMouseListener *m_space_mouse_listener = nullptr;
|
||||
#endif
|
||||
static QMap<uint, QETProject *> registered_projects_;
|
||||
static uint next_project_id;
|
||||
static RecentFiles *m_projects_recent_files;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
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 "spacemouselistener.h"
|
||||
|
||||
#include "../diagramview.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QScrollBar>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
#include <spnav.h>
|
||||
|
||||
namespace {
|
||||
//A spnav motion delta 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
|
||||
//user's spacenavd sensitivity setting, which is configured once
|
||||
//outside QET and out of scope here). DiagramView::wheelEvent()
|
||||
//already turns such a tick into a small per-event zoom step via
|
||||
//zoom(1 + value/1000); reused as a starting point.
|
||||
//
|
||||
//Neither this divisor nor PAN_SCALE below has been calibrated
|
||||
//against real hardware -- there is none in the environment this was
|
||||
//built in. Both are named constants specifically so that is a
|
||||
//one-line fix once someone with a device tries it.
|
||||
constexpr qreal ZOOM_DIVISOR = 1000.0;
|
||||
constexpr qreal PAN_SCALE = 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseListener::SpaceMouseListener
|
||||
Try to connect to spacenavd. Failure -- no daemon running, no device
|
||||
attached -- is left silent: it is the expected state for most users and
|
||||
must never surface as an error dialog or a log warning on every
|
||||
ordinary startup.
|
||||
@param parent
|
||||
*/
|
||||
SpaceMouseListener::SpaceMouseListener(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
if (spnav_open() == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_available = true;
|
||||
m_notifier = new QSocketNotifier(spnav_fd(), QSocketNotifier::Read, this);
|
||||
connect(m_notifier, &QSocketNotifier::activated,
|
||||
this, &SpaceMouseListener::readEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseListener::~SpaceMouseListener
|
||||
*/
|
||||
SpaceMouseListener::~SpaceMouseListener()
|
||||
{
|
||||
if (m_available) {
|
||||
spnav_close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseListener::zoomFactorForZAxis
|
||||
@param z : raw Z-axis (push/pull) delta from a spnav motion event
|
||||
@return the multiplicative factor DiagramView::zoom() expects
|
||||
*/
|
||||
qreal SpaceMouseListener::zoomFactorForZAxis(int z)
|
||||
{
|
||||
return 1.0 + (static_cast<qreal>(z) / ZOOM_DIVISOR);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseListener::readEvents
|
||||
Called when the spacenavd socket has data available. Drains every event
|
||||
currently queued -- spnav_poll_event() returns 0 once the queue is
|
||||
empty -- rather than handling just one per activation, so events cannot
|
||||
silently back up if several arrive between two Qt event loop turns.
|
||||
*/
|
||||
void SpaceMouseListener::readEvents()
|
||||
{
|
||||
spnav_event event;
|
||||
while (spnav_poll_event(&event))
|
||||
{
|
||||
if (event.type == SPNAV_EVENT_MOTION) {
|
||||
dispatchMotion(event.motion);
|
||||
}
|
||||
//Button events (SPNAV_EVENT_BUTTON) are deliberately not
|
||||
//handled: mapping device buttons to QET actions is the "Related,
|
||||
//not proposed here" follow-up in discussion #599, not this
|
||||
//phase.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseListener::dispatchMotion
|
||||
Apply one motion event to whichever DiagramView is currently active.
|
||||
X/Y translation pans it, Z translation zooms it -- the same two
|
||||
primitives (scrollbars, DiagramView::zoom()) DiagramView::wheelEvent()
|
||||
already drives from a physical wheel, so there is no new navigation
|
||||
logic here, only a new input source feeding the existing one.
|
||||
|
||||
A 6-DOF device also reports rotation (rx, ry, rz); QET's view has
|
||||
nothing rotation maps to, so those three axes are read by nothing here.
|
||||
|
||||
Which of the three translation axes is "left/right" vs "forward/back"
|
||||
vs "up/down" on the physical device, and their sign, is a hardware
|
||||
convention this could not be checked against real hardware while
|
||||
writing it -- see the PR description.
|
||||
@param motion
|
||||
*/
|
||||
void SpaceMouseListener::dispatchMotion(const spnav_event_motion &motion)
|
||||
{
|
||||
auto *editor = qobject_cast<QETDiagramEditor *>(qApp->activeWindow());
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectView *project_view = editor->currentProjectView();
|
||||
if (!project_view) {
|
||||
return;
|
||||
}
|
||||
|
||||
DiagramView *view = project_view->currentDiagram();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (motion.x || motion.y)
|
||||
{
|
||||
view->horizontalScrollBar()->setValue(
|
||||
view->horizontalScrollBar()->value() - qRound(motion.x * PAN_SCALE));
|
||||
view->verticalScrollBar()->setValue(
|
||||
view->verticalScrollBar()->value() - qRound(motion.y * PAN_SCALE));
|
||||
}
|
||||
|
||||
if (motion.z) {
|
||||
view->zoom(zoomFactorForZAxis(motion.z));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 SPACEMOUSELISTENER_H
|
||||
#define SPACEMOUSELISTENER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class QSocketNotifier;
|
||||
struct spnav_event_motion;
|
||||
|
||||
/**
|
||||
@brief The SpaceMouseListener class
|
||||
Phase 1 (Linux, libspnav) of
|
||||
https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/599 :
|
||||
bridges a 3Dconnexion SpaceMouse/SpacePilot 6-DOF device, via the
|
||||
spacenavd daemon and libspnav, to DiagramView's existing pan/zoom
|
||||
primitives (the same horizontalScrollBar()/verticalScrollBar()/zoom()
|
||||
calls DiagramView::wheelEvent() already uses for a physical wheel).
|
||||
|
||||
Only compiled in when the QET_ENABLE_SPACEMOUSE CMake option is on. Even
|
||||
then, constructing one is always safe: when no spacenavd is running or
|
||||
no device is attached -- the expected state for the overwhelming
|
||||
majority of users, even of a build with the option on -- it silently
|
||||
does nothing rather than failing or nagging the user. There is exactly
|
||||
one of these, owned by QETApp, because a physical 6-DOF device is a
|
||||
single ambient input source for the whole application, not something
|
||||
tied to one window; motion is applied to whichever DiagramView is
|
||||
currently active (see targetView()).
|
||||
*/
|
||||
class SpaceMouseListener : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpaceMouseListener(QObject *parent = nullptr);
|
||||
~SpaceMouseListener() override;
|
||||
|
||||
/// True once a connection to spacenavd was established.
|
||||
/// False is the common case, not an error -- see the class
|
||||
/// comment -- so callers should not warn the user when this
|
||||
/// is false.
|
||||
bool isAvailable() const {return m_available;}
|
||||
|
||||
/// Pure translation from a device Z-axis delta to the
|
||||
/// multiplicative factor DiagramView::zoom() expects. A free
|
||||
/// function so the mapping can be unit-tested without a live
|
||||
/// spacenavd connection or a real device.
|
||||
static qreal zoomFactorForZAxis(int z);
|
||||
|
||||
private slots:
|
||||
/// Drain and dispatch every event currently queued on the
|
||||
/// spacenavd socket. Connected to a QSocketNotifier on that
|
||||
/// socket's fd rather than polled on a timer, so this is
|
||||
/// idle-cost-free between events.
|
||||
void readEvents();
|
||||
|
||||
private:
|
||||
void dispatchMotion(const spnav_event_motion &motion);
|
||||
|
||||
bool m_available = false;
|
||||
QSocketNotifier *m_notifier = nullptr;
|
||||
};
|
||||
|
||||
#endif // SPACEMOUSELISTENER_H
|
||||
Reference in New Issue
Block a user