diff --git a/.gitmodules b/.gitmodules index 71384a1a8..45fde450e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -23,3 +23,6 @@ [submodule "extlib/mimalloc"] path = extlib/mimalloc url = https://github.com/microsoft/mimalloc +[submodule "extlib/libspnavdev"] + path = extlib/libspnavdev + url = https://github.com/rpavlik/libspnavdev.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 12948781f..f977ccdc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,10 @@ if(ENABLE_GUI) "${CMAKE_SOURCE_DIR}/extlib/si") set(SPACEWARE_LIBRARIES "${CMAKE_SOURCE_DIR}/extlib/si/siapp.lib") + else() + message(STATUS "Using libspnavdev") + add_vendored_subdirectory(extlib/libspnavdev) + set(HAVE_SPNAVDEV TRUE) endif() elseif(APPLE) find_package(OpenGL REQUIRED) diff --git a/extlib/libspnavdev b/extlib/libspnavdev new file mode 160000 index 000000000..69f892568 --- /dev/null +++ b/extlib/libspnavdev @@ -0,0 +1 @@ +Subproject commit 69f892568b7b69120d52a0a4c49faa02d4ddd830 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45dab944f..fb1d421e8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,7 +106,8 @@ endif() set(platform_SOURCES ${gl_SOURCES} - platform/entrygui.cpp) + platform/entrygui.cpp + platform/spnavdevice.cpp) if(WIN32) list(APPEND platform_SOURCES @@ -134,6 +135,9 @@ else() list(APPEND platform_LIBRARIES ${${pkg_config_lib}_LIBRARIES}) endforeach() endif() +if(HAVE_SPNAVDEV) + list(APPEND platform_LIBRARIES spnavdev) +endif() set(every_platform_SOURCES platform/guiwin.cpp @@ -319,7 +323,7 @@ endif() # solvespace graphical executable if(ENABLE_GUI) - add_executable(solvespace WIN32 MACOSX_BUNDLE + add_executable(solvespace MACOSX_BUNDLE ${solvespace_core_gl_SOURCES} ${platform_SOURCES} $) @@ -340,6 +344,7 @@ if(ENABLE_GUI) set_target_properties(solvespace PROPERTIES OUTPUT_NAME SolveSpace) endif() + endif() # solvespace headless library diff --git a/src/config.h.in b/src/config.h.in index 144c46d04..8ad1dc5c2 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -9,6 +9,9 @@ /* Do we have the si library on win32, or libspnav on *nix? */ #cmakedefine HAVE_SPACEWARE +/* Do we have libspnavdev? */ +#cmakedefine HAVE_SPNAVDEV + /* What OpenGL version do we use? */ #define HAVE_OPENGL @OPENGL@ diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp index b93b87b4a..68afc22eb 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -51,6 +51,8 @@ # undef uint32_t #endif +#include "spnavdevice.h" + #if defined(__GNUC__) // Disable bogus warning emitted by GCC on GetProcAddress, since there seems to be no way // of restructuring the code to easily disable it just at the call site. @@ -541,6 +543,8 @@ class WindowImplWin32 final : public Window { SiHdl hSpaceWare = SI_NO_HANDLE; #endif + std::unique_ptr navDev; + std::shared_ptr menuBar; std::string tooltipText; bool scrollbarVisible = false; @@ -695,6 +699,11 @@ class WindowImplWin32 final : public Window { // Make sure any of our child windows get destroyed before we call DestroyWindow, or their // own destructors may fail. menuBar.reset(); + + if (navDev != NULL) { + KillTimer(hWindow, (UINT_PTR)this); + } + navDev.reset(); sscheck(DestroyWindow(hWindow)); #if defined(HAVE_SPACEWARE) @@ -725,7 +734,7 @@ class WindowImplWin32 final : public Window { if(SiGetEvent(window->hSpaceWare, 0, &sged, &sse) == SI_IS_EVENT) { SixDofEvent event = {}; event.shiftDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); - event.controlDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); + event.controlDown = ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0); if(sse.type == SI_MOTION_EVENT) { // The Z axis translation and rotation are both // backwards in the default mapping. @@ -736,6 +745,8 @@ class WindowImplWin32 final : public Window { event.rotationX = sse.u.spwData.mData[SI_RX]*0.001, event.rotationY = sse.u.spwData.mData[SI_RY]*0.001, event.rotationZ = -sse.u.spwData.mData[SI_RZ]*0.001; + dbp("spacemouse %f, %f, %f, %f, %f, %f", event.translationX, event.translationY, + event.translationZ, event.rotationX, event.rotationY, event.rotationZ); } else if(sse.type == SI_BUTTON_EVENT) { if(SiButtonPressed(&sse) == SI_APP_FIT_BUTTON) { event.type = SixDofEvent::Type::PRESS; @@ -1064,6 +1075,28 @@ class WindowImplWin32 final : public Window { break; } + case WM_TIMER: { + //! @todo where to put this? We don't actually need to handle window messages, + //! just poll it periodically. + if(window->navDev != nullptr && wParam == (WPARAM)window) { + + SixDofEvent event = {}; + event.shiftDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); + event.controlDown = ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0); + if(window->navDev->process(event)) { + if(event.type == SixDofEvent::Type::MOTION) { + dbp("spnavdev %f, %f, %f, %f, %f, %f", event.translationX, + event.translationY, event.translationZ, event.rotationX, + event.rotationY, event.rotationZ); + } + if(window->onSixDofEvent) { + window->onSixDofEvent(event); + } + } + } + break; + } + default: return DefWindowProcW(h, msg, wParam, lParam); } @@ -1437,6 +1470,26 @@ void Request3DConnexionEventsForWindow(WindowRef window) { SiSetUiMode(windowImpl->hSpaceWare, SI_UI_NO_CONTROLS); } } +#elif defined(HAVE_SPNAVDEV) +static std::unique_ptr navWrapper; +void Open3DConnexion() { + navWrapper = std::make_unique(); + if(!navWrapper->active()) { + navWrapper.reset(); + } +} +void Close3DConnexion() { + navWrapper.reset(); +} +void Request3DConnexionEventsForWindow(WindowRef window) { + std::shared_ptr windowImpl = std::static_pointer_cast(window); + if(navWrapper) { + // Have the window adopt our object + windowImpl->navDev = std::move(navWrapper); + navWrapper.reset(); + SetTimer(windowImpl->hWindow, (UINT_PTR)windowImpl.get(), USER_TIMER_MINIMUM, (TIMERPROC) NULL); + } +} #else void Open3DConnexion() {} void Close3DConnexion() {} diff --git a/src/platform/spnavdevice.cpp b/src/platform/spnavdevice.cpp new file mode 100644 index 000000000..911d2a291 --- /dev/null +++ b/src/platform/spnavdevice.cpp @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------- +// Cross-platform handling of spnavdev 6-dof input. +// +// Copyright 2021 Collabora, Ltd. +//----------------------------------------------------------------------------- + +#include "spnavdevice.h" +#include "config.h" + +#ifdef HAVE_SPNAVDEV +# include "spnavdev.h" +# include +# include + +NavDeviceWrapper::NavDeviceWrapper(const char* device) { + dev = spndev_open(device); + if(dev == nullptr) { + return; + } + + int fd = spndev_fd(dev); + populateAxes(); + populateButtons(); +} +NavDeviceWrapper::~NavDeviceWrapper() { + if(dev != nullptr) { + spndev_close(dev); + dev = nullptr; + } +} + +static double transformIndex(union spndev_event const& ev, + NavDeviceWrapper::AxisData const& axisData) { + ssassert(ev.type == SPNDEV_MOTION, "shouldn't be in here if not a motion event"); + if(axisData.spnavdevIndex < 0) { + return 0; + } + return double(ev.mot.v[axisData.spnavdevIndex]); +} +bool NavDeviceWrapper::process(SolveSpace::Platform::SixDofEvent& event) { + using SolveSpace::Platform::SixDofEvent; + union spndev_event ev; + if(0 == spndev_process(dev, &ev)) { + return false; + } + if(ctrlPressed) { + event.controlDown = true; + } + if(shiftPressed) { + event.shiftDown = true; + } + switch(ev.type) { + case SPNDEV_MOTION: + event.type = SixDofEvent::Type::MOTION; + event.translationX = transformIndex(ev, axes[0]); + event.translationY = transformIndex(ev, axes[1]); + event.translationZ = transformIndex(ev, axes[2]); + event.rotationX = transformIndex(ev, axes[3]) * 0.001; + event.rotationY = transformIndex(ev, axes[4]) * 0.001; + event.rotationZ = transformIndex(ev, axes[5]) * 0.001; + return true; + case SPNDEV_BUTTON: { + if(ev.bn.num >= buttons.size()) { + return false; + } + const NavButton meaning = buttons[ev.bn.num]; + auto type = ev.bn.press ? SixDofEvent::Type::PRESS : SixDofEvent::Type::RELEASE; + switch(meaning) { + case NavButton::UNUSED: + // we don't handle this button. + return false; + + case NavButton::SHIFT: + // handled internally to this class + shiftPressed = type == SixDofEvent::Type::PRESS; + return false; + + case NavButton::CTRL: + // handled internally to this class + ctrlPressed = type == SixDofEvent::Type::PRESS; + return false; + + case NavButton::FIT: + event.button = SixDofEvent::Button::FIT; + event.type = type; + return true; + } + break; + } + default: return false; + } + return false; +} + +void NavDeviceWrapper::populateAxes() { + using std::begin; + using std::end; + const std::string axis_names[] = {"Tx", "Ty", "Tz", "Rx", "Ry", "Rz"}; + const auto b = begin(axis_names); + const auto e = end(axis_names); + const auto num_axes = spndev_num_axes(dev); + for(int axis_idx = 0; axis_idx < num_axes; ++axis_idx) { + auto axis_name = spndev_axis_name(dev, axis_idx); + auto it = std::find_if(b, e, [&](std::string const& name) { return name == axis_name; }); + if(it != e) { + ptrdiff_t remapped_index = std::distance(b, it); + axes[remapped_index] = AxisData{axis_idx}; + } + } +} + +void NavDeviceWrapper::populateButtons() { + using std::begin; + using std::end; + using ButtonData = std::pair; + const ButtonData button_name_pairs[] = { + {"CTRL", NavButton::CTRL}, + {"FIT", NavButton::FIT}, + {"SHIFT", NavButton::SHIFT}, + }; + + const auto b = begin(button_name_pairs); + const auto e = end(button_name_pairs); + const auto num_buttons = spndev_num_buttons(dev); + buttons.resize(num_buttons); + for(int button_idx = 0; button_idx < num_buttons; ++button_idx) { + auto button_name = spndev_button_name(dev, button_idx); + auto it = + std::find_if(b, e, [&](ButtonData const& data) { return data.first == button_name; }); + if(it != e) { + buttons[button_idx] = it->second; + } + } +} + +#else + +NavDeviceWrapper::NavDeviceWrapper(const char* device) { +} +NavDeviceWrapper::~NavDeviceWrapper() { +} + +bool NavDeviceWrapper::process(SolveSpace::Platform::SixDofEvent&) { + return false; +} +#endif \ No newline at end of file diff --git a/src/platform/spnavdevice.h b/src/platform/spnavdevice.h new file mode 100644 index 000000000..b358a6555 --- /dev/null +++ b/src/platform/spnavdevice.h @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------------- +// Cross-platform handling of spnavdev 6-dof input. +// +// Copyright 2021 Collabora, Ltd. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_SPNAVDEVICE_H +#define SOLVESPACE_SPNAVDEVICE_H +#include +#include + +namespace SolveSpace { +namespace Platform { + struct SixDofEvent; +} +} // namespace SolveSpace +struct spndev; + +class NavDeviceWrapper { +public: + explicit NavDeviceWrapper(const char* device = nullptr); + ~NavDeviceWrapper(); + + // no copy, no move + NavDeviceWrapper(NavDeviceWrapper const&) = delete; + NavDeviceWrapper& operator=(NavDeviceWrapper const&) = delete; + NavDeviceWrapper(NavDeviceWrapper&&) = delete; + NavDeviceWrapper& operator=(NavDeviceWrapper&&) = delete; + + bool active() const noexcept { + return dev != nullptr; + } + + //! true when the event has data in it to deal with. + bool process(SolveSpace::Platform::SixDofEvent& event); + + enum class NavButton { + UNUSED, + FIT, + CTRL, + SHIFT, + }; + struct AxisData { + AxisData() = default; + AxisData(int spnavdevIndex_) : spnavdevIndex(spnavdevIndex_) { + } + + int spnavdevIndex = -1; + }; + +private: + void populateAxes(); + void populateButtons(); + struct spndev* dev = nullptr; + //! indexed by the spnavdev index + std::vector buttons; + //! indexed by our internal axis index. + std::array axes; + bool ctrlPressed = false; + bool shiftPressed = false; +}; +#endif // !SOLVESPACE_SPNAVDEVICE_H \ No newline at end of file