/* SPDX-License-Identifier: GPL-2.0-or-later */ #include "GHOST_NDOFManager.h" #include "GHOST_Debug.h" #include "GHOST_EventKey.h" #include "GHOST_EventNDOF.h" #include "GHOST_WindowManager.h" #include "GHOST_utildefines.h" /* Logging, use `ghost.ndof.*` prefix. */ #include "CLG_log.h" #include #include #include /* For memory functions. */ /* -------------------------------------------------------------------- */ /** \name NDOF Enum Strings * \{ */ /* Printable values for #GHOST_TProgress enum (keep aligned). */ static const char *ndof_progress_string[] = { "not started", "starting", "in progress", "finishing", "finished", }; /* Printable values for #NDOF_ButtonT enum (keep aligned) */ static const char *ndof_button_names[] = { /* Exclude `NDOF_BUTTON_NONE` (-1). */ "NDOF_BUTTON_MENU", "NDOF_BUTTON_FIT", "NDOF_BUTTON_TOP", "NDOF_BUTTON_BOTTOM", "NDOF_BUTTON_LEFT", "NDOF_BUTTON_RIGHT", "NDOF_BUTTON_FRONT", "NDOF_BUTTON_BACK", "NDOF_BUTTON_ISO1", "NDOF_BUTTON_ISO2", "NDOF_BUTTON_ROLL_CW", "NDOF_BUTTON_ROLL_CCW", "NDOF_BUTTON_SPIN_CW", "NDOF_BUTTON_SPIN_CCW", "NDOF_BUTTON_TILT_CW", "NDOF_BUTTON_TILT_CCW", "NDOF_BUTTON_ROTATE", "NDOF_BUTTON_PANZOOM", "NDOF_BUTTON_DOMINANT", "NDOF_BUTTON_PLUS", "NDOF_BUTTON_MINUS", "NDOF_BUTTON_1", "NDOF_BUTTON_2", "NDOF_BUTTON_3", "NDOF_BUTTON_4", "NDOF_BUTTON_5", "NDOF_BUTTON_6", "NDOF_BUTTON_7", "NDOF_BUTTON_8", "NDOF_BUTTON_9", "NDOF_BUTTON_10", "NDOF_BUTTON_A", "NDOF_BUTTON_B", "NDOF_BUTTON_C", "NDOF_BUTTON_V1", "NDOF_BUTTON_V2", "NDOF_BUTTON_V3", /* Keyboard emulation. */ "NDOF_BUTTON_ESC", "NDOF_BUTTON_ENTER", "NDOF_BUTTON_DELETE", "NDOF_BUTTON_TAB", "NDOF_BUTTON_SPACE", "NDOF_BUTTON_ALT", "NDOF_BUTTON_SHIFT", "NDOF_BUTTON_CTRL", }; static const char *ndof_device_names[] = { "UnknownDevice", "SpaceNavigator", "SpaceExplorer", "SpacePilotPro", "SpaceMousePro", "SpaceMouseWireless", "SpaceMouseProWireless", "SpaceMouseEnterprise", "SpacePilot", "Spaceball5000", "SpaceTraveler", }; /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Button Maps * \{ */ /* Shared by the latest 3Dconnexion hardware * SpacePilotPro uses all of these * smaller devices use only some, based on button mask. */ static const NDOF_ButtonT ndof_HID_map_Modern3Dx[] = { NDOF_BUTTON_MENU, NDOF_BUTTON_FIT, NDOF_BUTTON_TOP, NDOF_BUTTON_LEFT, NDOF_BUTTON_RIGHT, NDOF_BUTTON_FRONT, NDOF_BUTTON_BOTTOM, NDOF_BUTTON_BACK, NDOF_BUTTON_ROLL_CW, NDOF_BUTTON_ROLL_CCW, NDOF_BUTTON_ISO1, NDOF_BUTTON_ISO2, NDOF_BUTTON_1, NDOF_BUTTON_2, NDOF_BUTTON_3, NDOF_BUTTON_4, NDOF_BUTTON_5, NDOF_BUTTON_6, NDOF_BUTTON_7, NDOF_BUTTON_8, NDOF_BUTTON_9, NDOF_BUTTON_10, NDOF_BUTTON_ESC, NDOF_BUTTON_ALT, NDOF_BUTTON_SHIFT, NDOF_BUTTON_CTRL, NDOF_BUTTON_ROTATE, NDOF_BUTTON_PANZOOM, NDOF_BUTTON_DOMINANT, NDOF_BUTTON_PLUS, NDOF_BUTTON_MINUS}; static const NDOF_ButtonT ndof_HID_map_SpaceExplorer[] = { NDOF_BUTTON_1, NDOF_BUTTON_2, NDOF_BUTTON_TOP, NDOF_BUTTON_LEFT, NDOF_BUTTON_RIGHT, NDOF_BUTTON_FRONT, NDOF_BUTTON_ESC, NDOF_BUTTON_ALT, NDOF_BUTTON_SHIFT, NDOF_BUTTON_CTRL, NDOF_BUTTON_FIT, NDOF_BUTTON_MENU, NDOF_BUTTON_PLUS, NDOF_BUTTON_MINUS, NDOF_BUTTON_ROTATE, }; /* This is the older SpacePilot (sans Pro). */ static const NDOF_ButtonT ndof_HID_map_SpacePilot[] = { NDOF_BUTTON_1, NDOF_BUTTON_2, NDOF_BUTTON_3, NDOF_BUTTON_4, NDOF_BUTTON_5, NDOF_BUTTON_6, NDOF_BUTTON_TOP, NDOF_BUTTON_LEFT, NDOF_BUTTON_RIGHT, NDOF_BUTTON_FRONT, NDOF_BUTTON_ESC, NDOF_BUTTON_ALT, NDOF_BUTTON_SHIFT, NDOF_BUTTON_CTRL, NDOF_BUTTON_FIT, NDOF_BUTTON_MENU, NDOF_BUTTON_PLUS, NDOF_BUTTON_MINUS, NDOF_BUTTON_DOMINANT, NDOF_BUTTON_ROTATE, NDOF_BUTTON_NONE /* the CONFIG button -- what does it do? */ }; static const NDOF_ButtonT ndof_HID_map_Generic[] = { NDOF_BUTTON_1, NDOF_BUTTON_2, NDOF_BUTTON_3, NDOF_BUTTON_4, NDOF_BUTTON_5, NDOF_BUTTON_6, NDOF_BUTTON_7, NDOF_BUTTON_8, NDOF_BUTTON_9, NDOF_BUTTON_A, NDOF_BUTTON_B, NDOF_BUTTON_C, }; /* Values taken from: https://github.com/FreeSpacenav/spacenavd/wiki/Device-button-names */ static const NDOF_ButtonT ndof_HID_map_SpaceMouseEnterprise[] = { NDOF_BUTTON_1, /* (0) */ NDOF_BUTTON_2, /* (1) */ NDOF_BUTTON_3, /* (2) */ NDOF_BUTTON_4, /* (3) */ NDOF_BUTTON_5, /* (4) */ NDOF_BUTTON_6, /* (5) */ NDOF_BUTTON_7, /* (6) */ NDOF_BUTTON_8, /* (7) */ NDOF_BUTTON_9, /* (8) */ NDOF_BUTTON_A, /* Labeled "10" (9). */ NDOF_BUTTON_B, /* Labeled "11" (10). */ NDOF_BUTTON_C, /* Labeled "12" (11). */ NDOF_BUTTON_MENU, /* (12). */ NDOF_BUTTON_FIT, /* (13). */ NDOF_BUTTON_TOP, /* (14). */ NDOF_BUTTON_RIGHT, /* (15). */ NDOF_BUTTON_FRONT, /* (16). */ NDOF_BUTTON_ROLL_CW, /* (17). */ NDOF_BUTTON_ESC, /* (18). */ NDOF_BUTTON_ALT, /* (19). */ NDOF_BUTTON_SHIFT, /* (20). */ NDOF_BUTTON_CTRL, /* (21). */ NDOF_BUTTON_ROTATE, /* Labeled "Lock Rotate" (22). */ NDOF_BUTTON_ENTER, /* Labeled "Enter" (23). */ NDOF_BUTTON_DELETE, /* (24). */ NDOF_BUTTON_TAB, /* (25). */ NDOF_BUTTON_SPACE, /* (26). */ NDOF_BUTTON_V1, /* Labeled "V1" (27). */ NDOF_BUTTON_V2, /* Labeled "V2" (28). */ NDOF_BUTTON_V3, /* Labeled "V3" (29). */ NDOF_BUTTON_ISO1, /* Labeled "ISO1" (30). */ }; /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Manager Class * \{ */ static const int genericButtonCount = ARRAY_SIZE(ndof_HID_map_Generic); GHOST_NDOFManager::GHOST_NDOFManager(GHOST_System &sys) : system_(sys), device_type_(NDOF_UnknownDevice), /* Each platform has its own device detection code. */ hid_map_button_num_(genericButtonCount), hid_map_button_mask_(0), hid_map_(ndof_HID_map_Generic), button_depressed_(0), motion_time_(0), motion_time_prev_(0), motion_state_(GHOST_kNotStarted), motion_event_pending_(false), motion_dead_zone_(0.0f) { /* To avoid the rare situation where one triple is updated and * the other is not, initialize them both here: */ memset(translation_, 0, sizeof(translation_)); memset(rotation_, 0, sizeof(rotation_)); } /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Device Setup * \{ */ static CLG_LogRef LOG_NDOF_DEVICE = {"ghost.ndof.device"}; #define LOG (&LOG_NDOF_DEVICE) bool GHOST_NDOFManager::setDevice(ushort vendor_id, ushort product_id) { /* Call this function until it returns true * it's a good idea to stop calling it after that, as it will "forget" * whichever device it already found. */ /* Default to safe generic behavior for "unknown" devices * unidentified devices will emit motion events like normal * rogue buttons do nothing by default, but can be customized by the user. */ device_type_ = NDOF_UnknownDevice; hid_map_ = ndof_HID_map_Generic; hid_map_button_num_ = genericButtonCount; hid_map_button_mask_ = 0; /* "mystery device" owners can help build a HID_map for their hardware * A few users have already contributed information about several older devices * that I don't have access to. Thanks! */ switch (vendor_id) { case 0x046D: /* Logitech (3Dconnexion was a subsidiary). */ switch (product_id) { /* -- current devices -- */ case 0xC626: /* Full-size SpaceNavigator. */ case 0xC628: /* The "for Notebooks" one. */ { device_type_ = NDOF_SpaceNavigator; hid_map_button_num_ = 2; hid_map_ = ndof_HID_map_Modern3Dx; break; } case 0xC627: { device_type_ = NDOF_SpaceExplorer; hid_map_button_num_ = 15; hid_map_ = ndof_HID_map_SpaceExplorer; break; } case 0xC629: { device_type_ = NDOF_SpacePilotPro; hid_map_button_num_ = 31; hid_map_ = ndof_HID_map_Modern3Dx; break; } case 0xC62B: { device_type_ = NDOF_SpaceMousePro; hid_map_button_num_ = 27; /* 15 physical buttons, but HID codes range from 0 to 26. */ hid_map_button_mask_ = 0x07C0F137; hid_map_ = ndof_HID_map_Modern3Dx; break; } /* -- older devices -- */ case 0xC625: { device_type_ = NDOF_SpacePilot; hid_map_button_num_ = 21; hid_map_ = ndof_HID_map_SpacePilot; break; } case 0xC621: { device_type_ = NDOF_Spaceball5000; hid_map_button_num_ = 12; break; } case 0xC623: { device_type_ = NDOF_SpaceTraveler; hid_map_button_num_ = 8; break; } default: { CLOG_INFO(LOG, 2, "unknown Logitech product %04hx", product_id); } } break; case 0x256F: /* 3Dconnexion. */ switch (product_id) { case 0xC62E: /* Plugged in. */ case 0xC62F: /* Wireless. */ case 0xC658: /* Wireless (3DConnexion Universal Wireless Receiver in WIN32), see T82412. */ { device_type_ = NDOF_SpaceMouseWireless; hid_map_button_num_ = 2; hid_map_ = ndof_HID_map_Modern3Dx; break; } case 0xC631: /* Plugged in. */ case 0xC632: /* Wireless. */ { device_type_ = NDOF_SpaceMouseProWireless; hid_map_button_num_ = 27; /* 15 physical buttons, but HID codes range from 0 to 26. */ hid_map_button_mask_ = 0x07C0F137; hid_map_ = ndof_HID_map_Modern3Dx; break; } case 0xC633: { device_type_ = NDOF_SpaceMouseEnterprise; hid_map_button_num_ = 31; hid_map_ = ndof_HID_map_SpaceMouseEnterprise; break; } default: { CLOG_INFO(LOG, 2, "unknown 3Dconnexion product %04hx", product_id); } } break; default: CLOG_INFO(LOG, 2, "unknown device %04hx:%04hx", vendor_id, product_id); } if (device_type_ != NDOF_UnknownDevice) { CLOG_INFO(LOG, 2, "using %s", ndof_device_names[device_type_]); } if (hid_map_button_mask_ == 0) { hid_map_button_mask_ = int(~(UINT_MAX << hid_map_button_num_)); } CLOG_INFO(LOG, 2, "%d buttons -> hex:%X", hid_map_button_num_, (uint)hid_map_button_mask_); return device_type_ != NDOF_UnknownDevice; } #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Update State * \{ */ void GHOST_NDOFManager::updateTranslation(const int t[3], uint64_t time) { memcpy(translation_, t, sizeof(translation_)); motion_time_ = time; motion_event_pending_ = true; } void GHOST_NDOFManager::updateRotation(const int r[3], uint64_t time) { memcpy(rotation_, r, sizeof(rotation_)); motion_time_ = time; motion_event_pending_ = true; } /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Buttons * \{ */ static CLG_LogRef LOG_NDOF_BUTTONS = {"ghost.ndof.buttons"}; #define LOG (&LOG_NDOF_BUTTONS) static GHOST_TKey ghost_map_keyboard_from_ndof_buttom(const NDOF_ButtonT button) { switch (button) { case NDOF_BUTTON_ESC: { return GHOST_kKeyEsc; } case NDOF_BUTTON_ENTER: { return GHOST_kKeyEnter; } case NDOF_BUTTON_DELETE: { return GHOST_kKeyDelete; } case NDOF_BUTTON_TAB: { return GHOST_kKeyTab; } case NDOF_BUTTON_SPACE: { return GHOST_kKeySpace; } case NDOF_BUTTON_ALT: { return GHOST_kKeyLeftAlt; } case NDOF_BUTTON_SHIFT: { return GHOST_kKeyLeftShift; } case NDOF_BUTTON_CTRL: { return GHOST_kKeyLeftControl; } default: { return GHOST_kKeyUnknown; } } } void GHOST_NDOFManager::sendButtonEvent(NDOF_ButtonT button, bool press, uint64_t time, GHOST_IWindow *window) { GHOST_ASSERT(button > NDOF_BUTTON_NONE && button < NDOF_BUTTON_NUM, "rogue button trying to escape NDOF manager"); GHOST_EventNDOFButton *event = new GHOST_EventNDOFButton(time, window); GHOST_TEventNDOFButtonData *data = (GHOST_TEventNDOFButtonData *)event->getData(); data->action = press ? GHOST_kPress : GHOST_kRelease; data->button = button; system_.pushEvent(event); } void GHOST_NDOFManager::sendKeyEvent(GHOST_TKey key, bool press, uint64_t time, GHOST_IWindow *window) { GHOST_TEventType type = press ? GHOST_kEventKeyDown : GHOST_kEventKeyUp; GHOST_EventKey *event = new GHOST_EventKey(time, type, window, key, false); system_.pushEvent(event); } void GHOST_NDOFManager::updateButton(int button_number, bool press, uint64_t time) { if (button_number >= hid_map_button_num_) { CLOG_INFO(LOG, 2, "button=%d, press=%d (out of range %d, ignoring!)", button_number, (int)press, hid_map_button_num_); return; } const NDOF_ButtonT button = hid_map_[button_number]; if (button == NDOF_BUTTON_NONE) { CLOG_INFO( LOG, 2, "button=%d, press=%d (mapped to none, ignoring!)", button_number, (int)press); return; } CLOG_INFO(LOG, 2, "button=%d, press=%d, name=%s", button_number, (int)press, ndof_button_names[button]); GHOST_IWindow *window = system_.getWindowManager()->getActiveWindow(); const GHOST_TKey key = ghost_map_keyboard_from_ndof_buttom(button); if (key != GHOST_kKeyUnknown) { sendKeyEvent(key, press, time, window); } else { sendButtonEvent(button, press, time, window); } int mask = 1 << button_number; if (press) { button_depressed_ |= mask; /* Set this button's bit. */ } else { button_depressed_ &= ~mask; /* Clear this button's bit. */ } } void GHOST_NDOFManager::updateButtons(int button_bits, uint64_t time) { button_bits &= hid_map_button_mask_; /* Discard any "garbage" bits. */ int diff = button_depressed_ ^ button_bits; for (int button_number = 0; button_number < hid_map_button_num_; ++button_number) { int mask = 1 << button_number; if (diff & mask) { bool press = button_bits & mask; updateButton(button_number, press, time); } } } #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name NDOF Motion * \{ */ static CLG_LogRef LOG_NDOF_MOTION = {"ghost.ndof.motion"}; #define LOG (&LOG_NDOF_MOTION) void GHOST_NDOFManager::setDeadZone(float dz) { if (dz < 0.0f) { /* Negative values don't make sense, so clamp at zero. */ dz = 0.0f; } motion_dead_zone_ = dz; /* Warn the rogue user/developer about high dead-zone, but allow it. */ CLOG_INFO(LOG, 2, "dead zone set to %.2f%s", dz, (dz > 0.5f) ? " (unexpectedly high)" : ""); } static bool atHomePosition(GHOST_TEventNDOFMotionData *ndof) { #define HOME(foo) (ndof->foo == 0.0f) return HOME(tx) && HOME(ty) && HOME(tz) && HOME(rx) && HOME(ry) && HOME(rz); #undef HOME } static bool nearHomePosition(GHOST_TEventNDOFMotionData *ndof, float threshold) { if (threshold == 0.0f) { return atHomePosition(ndof); } #define HOME(foo) (fabsf(ndof->foo) < threshold) return HOME(tx) && HOME(ty) && HOME(tz) && HOME(rx) && HOME(ry) && HOME(rz); #undef HOME } bool GHOST_NDOFManager::sendMotionEvent() { if (!motion_event_pending_) { return false; } motion_event_pending_ = false; /* Any pending motion is handled right now. */ GHOST_IWindow *window = system_.getWindowManager()->getActiveWindow(); if (window == nullptr) { motion_state_ = GHOST_kNotStarted; /* Avoid large `dt` times when changing windows. */ return false; /* Delivery will fail, so don't bother sending. */ } GHOST_EventNDOFMotion *event = new GHOST_EventNDOFMotion(motion_time_, window); GHOST_TEventNDOFMotionData *data = (GHOST_TEventNDOFMotionData *)event->getData(); /* Scale axis values here to normalize them to around +/- 1 * they are scaled again for overall sensitivity in the WM based on user preferences. */ const float scale = 1.0f / 350.0f; /* 3Dconnexion devices send +/- 350 usually */ data->tx = scale * translation_[0]; data->ty = scale * translation_[1]; data->tz = scale * translation_[2]; data->rx = scale * rotation_[0]; data->ry = scale * rotation_[1]; data->rz = scale * rotation_[2]; data->dt = 0.001f * (motion_time_ - motion_time_prev_); /* In seconds. */ motion_time_prev_ = motion_time_; bool weHaveMotion = !nearHomePosition(data, motion_dead_zone_); /* Determine what kind of motion event to send `(Starting, InProgress, Finishing)` * and where that leaves this NDOF manager `(NotStarted, InProgress, Finished)`. */ switch (motion_state_) { case GHOST_kNotStarted: case GHOST_kFinished: { if (weHaveMotion) { data->progress = GHOST_kStarting; motion_state_ = GHOST_kInProgress; /* Previous motion time will be ancient, so just make up a reasonable time delta. */ data->dt = 0.0125f; } else { /* Send no event and keep current state. */ CLOG_INFO(LOG, 2, "motion ignored"); delete event; return false; } break; } case GHOST_kInProgress: { if (weHaveMotion) { data->progress = GHOST_kInProgress; /* Remain 'InProgress'. */ } else { data->progress = GHOST_kFinishing; motion_state_ = GHOST_kFinished; } break; } default: { /* Will always be one of the above. */ break; } } #if 1 CLOG_INFO(LOG, 2, "motion sent, T=(%.2f,%.2f,%.2f), R=(%.2f,%.2f,%.2f) dt=%.3f, status=%s", data->tx, data->ty, data->tz, data->rx, data->ry, data->rz, data->dt, ndof_progress_string[data->progress]); #else /* Raw values, may be useful for debugging. */ CLOG_INFO(LOG, 2, "motion sent, T=(%d,%d,%d) R=(%d,%d,%d) status=%s", translation_[0], translation_[1], translation_[2], rotation_[0], rotation_[1], rotation_[2], ndof_progress_string[data->progress]); #endif system_.pushEvent(event); return true; } /** \} */