Add SpaceMouse support for Linux via libspnav

This change introduces support for 3Dconnexion SpaceMouse devices on Linux
within the 3DConnexion plugin.

Key changes:
-   Added `LinuxSpacenavClient.py`, which uses `ctypes` to interface with the
    system-provided `libspnav.so.0` library. This client handles opening a
    connection to the `spacenavd` daemon and polling for motion and button
    events.
-   Modified `NavlibClient.py` to include platform detection. On Linux, it
    now uses `LinuxSpacenavClient` for device input. On Windows and macOS,
    it continues to use the existing `pynavlib`.
-   Updated the plugin initialization in `plugins/3DConnexion/__init__.py`
    to gracefully handle cases where `libspnav.so.0` might be missing or
    `spacenavd` is not accessible on Linux, disabling the plugin in such
    scenarios.
-   The core camera manipulation logic in `NavlibClient.py` has been adapted
    to accept transformation matrices from either `pynavlib` or the new
    Linux client, aiming for consistent behavior.
-   Placeholder adaptations for some `pynavlib`-specific methods have been
    added for the Linux path, returning `None` or basic Python types where
    `pynav.*` types were previously used.

This implementation relies on you having `spacenavd` (version 0.6 or newer recommended)
installed and running, along with `libspnav0` (or equivalent).

Testing for this feature is currently manual, involving checking device
response for camera manipulation (pan, zoom, rotate) within Cura on a
Linux environment with a configured SpaceMouse.

Output:
This commit is contained in:
google-labs-jules[bot] 2025-05-23 14:25:32 +00:00
parent c726d61086
commit 4698089f2a
3 changed files with 442 additions and 32 deletions

View file

@ -0,0 +1,191 @@
import ctypes
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Constants from spnav.h
SPNAV_EVENT_MOTION = 1
SPNAV_EVENT_BUTTON = 2
SPNAV_EVENT_ANY = SPNAV_EVENT_MOTION | SPNAV_EVENT_BUTTON
# Structures and Union based on spnav.h
class SpnavMotionEvent(ctypes.Structure):
_fields_ = [
("type", ctypes.c_int),
("x", ctypes.c_int),
("y", ctypes.c_int),
("z", ctypes.c_int),
("rx", ctypes.c_int),
("ry", ctypes.c_int),
("rz", ctypes.c_int),
("period", ctypes.c_ushort),
]
class SpnavButtonEvent(ctypes.Structure):
_fields_ = [
("type", ctypes.c_int),
("press", ctypes.c_int),
("bnum", ctypes.c_int),
]
class SpnavEvent(ctypes.Union):
_fields_ = [
("type", ctypes.c_int),
("motion", SpnavMotionEvent),
("button", SpnavButtonEvent),
]
class LinuxSpacenavClient:
def __init__(self):
self.lib = None
self.available = False
try:
self.lib = ctypes.CDLL("libspnav.so.0")
self.available = True
except OSError:
try:
self.lib = ctypes.CDLL("libspnav.so")
self.available = True
except OSError:
logging.warning("libspnav.so.0 or libspnav.so not found. Spacenav functionality will be unavailable.")
return
if self.available:
logging.info("Successfully loaded libspnav.")
# Define function prototypes
try:
self.spnav_open = self.lib.spnav_open
self.spnav_open.restype = ctypes.c_int
self.spnav_open.argtypes = []
self.spnav_close = self.lib.spnav_close
self.spnav_close.restype = ctypes.c_int
self.spnav_close.argtypes = []
self.spnav_fd = self.lib.spnav_fd
self.spnav_fd.restype = ctypes.c_int
self.spnav_fd.argtypes = []
self.spnav_poll_event = self.lib.spnav_poll_event
self.spnav_poll_event.restype = ctypes.c_int
self.spnav_poll_event.argtypes = [ctypes.POINTER(SpnavEvent)]
self.spnav_remove_events = self.lib.spnav_remove_events
self.spnav_remove_events.restype = ctypes.c_int
self.spnav_remove_events.argtypes = [ctypes.c_int]
logging.info("Function prototypes defined successfully.")
except AttributeError as e:
logging.error(f"Error setting up function prototypes: {e}")
self.available = False
def open(self) -> bool:
if not self.available:
logging.warning("spnav_open called but library not available.")
return False
ret = self.spnav_open()
if ret == 0:
logging.info("Successfully opened connection to spacenavd (native protocol).")
return True
else:
# spnav_open returns -1 on error and sets errno.
# However, ctypes doesn't automatically pick up errno from C.
# For now, just log a generic error.
logging.error(f"spnav_open failed with return code {ret}.")
return False
def close(self) -> None:
if not self.available:
logging.warning("spnav_close called but library not available.")
return
ret = self.spnav_close()
if ret == 0:
logging.info("Successfully closed connection to spacenavd.")
else:
logging.error(f"spnav_close failed with return code {ret}.")
def poll_event(self) -> SpnavEvent | None:
if not self.available:
logging.warning("spnav_poll_event called but library not available.")
return None
event = SpnavEvent()
ret = self.spnav_poll_event(ctypes.byref(event))
if ret > 0:
# logging.debug(f"spnav_poll_event returned event type: {event.type}") # Too verbose for INFO
return event
elif ret == 0:
# No event pending
return None
else:
# Error
logging.error(f"spnav_poll_event failed with return code {ret}.")
return None
def get_fd(self) -> int:
if not self.available:
logging.warning("spnav_fd called but library not available.")
return -1
fd = self.spnav_fd()
if fd == -1:
logging.error("spnav_fd failed.")
else:
logging.info(f"spnav_fd returned file descriptor: {fd}")
return fd
def remove_events(self, event_type: int) -> int:
if not self.available:
logging.warning("spnav_remove_events called but library not available.")
return -1 # Or some other error indicator
ret = self.spnav_remove_events(event_type)
if ret < 0:
# spnav_remove_events returns number of events removed, or -1 on error
logging.error(f"spnav_remove_events failed with return code {ret} for event_type {event_type}.")
else:
logging.info(f"spnav_remove_events successfully removed {ret} events of type {event_type}.")
return ret
if __name__ == '__main__':
logging.info("Attempting to initialize LinuxSpacenavClient for testing...")
client = LinuxSpacenavClient()
if client.available:
logging.info("LinuxSpacenavClient available.")
if client.open():
logging.info("Spacenav opened successfully. You can try moving the device.")
# Example of polling for a few events
for _ in range(5): # Try to read 5 events
event = client.poll_event()
if event:
if event.type == SPNAV_EVENT_MOTION:
logging.info(f"Motion Event: x={event.motion.x}, y={event.motion.y}, z={event.motion.z}, rx={event.motion.rx}, ry={event.motion.ry}, rz={event.motion.rz}")
elif event.type == SPNAV_EVENT_BUTTON:
logging.info(f"Button Event: press={event.button.press}, bnum={event.button.bnum}")
else:
logging.info("No event polled.")
# break # if no event, might not be more immediately
# Example of getting file descriptor
fd = client.get_fd()
logging.info(f"File descriptor: {fd}")
# Example of removing pending events
# Note: This might clear events that your application wants. Use carefully.
# Usually, you'd call this if the event queue is full or if you want to ignore old events.
# client.remove_events(SPNAV_EVENT_ANY)
# logging.info("Attempted to remove any pending events.")
client.close()
logging.info("Spacenav closed.")
else:
logging.error("Failed to open spacenav.")
else:
logging.warning("LinuxSpacenavClient not available. Cannot run tests.")
logging.info("LinuxSpacenavClient.py basic test finished.")

View file

@ -1,6 +1,9 @@
# Copyright (c) 2025 3Dconnexion, UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import platform # Added
import logging # Added
from typing import Optional
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
@ -13,23 +16,116 @@ from UM.Resources import Resources
from UM.Tool import Tool
from UM.View.Renderer import Renderer
from .OverlayNode import OverlayNode
from .LinuxSpacenavClient import LinuxSpacenavClient, SPNAV_EVENT_MOTION, SPNAV_EVENT_BUTTON # Added
import pynavlib.pynavlib_interface as pynav
logger = logging.getLogger(__name__) # Added
class NavlibClient(pynav.NavlibNavigationModel, Tool):
def __init__(self, scene: Scene, renderer: Renderer) -> None:
pynav.NavlibNavigationModel.__init__(self, False, pynav.NavlibOptions.RowMajorOrder)
Tool.__init__(self)
self._platform_system = platform.system()
self._linux_spacenav_client: Optional[LinuxSpacenavClient] = None
self._scene = scene
self._renderer = renderer
self._pointer_pick = None
self._was_pick = False
self._hit_selection_only = False
self._picking_pass = None
self._pointer_pick = None # Retain for pick()
self._was_pick = False # Retain for pick() / get_hit_look_at()
self._hit_selection_only = False # Retain for pick() / get_hit_look_at()
self._picking_pass = None # Retain for pick() / set_motion_flag()
# Pivot node might be useful on Linux too, initialize it.
self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "cor.png"), size=2.5)
self.put_profile_hint("UltiMaker Cura")
self.enable_navigation(True)
self._scene_center = Vector() # Used in set_camera_matrix, ensure it exists
self._scene_radius = 1.0 # Used in set_camera_matrix, ensure it exists
if self._platform_system == "Linux":
# Tool.__init__(self) # Explicitly NOT calling Tool.__init__ for Linux as per current subtask
logger.info("Attempting to initialize LinuxSpacenavClient for Linux platform.")
# Essential scene/renderer setup, even if not a full Tool
# self._scene = scene # Already assigned above
# self._renderer = renderer # Already assigned above
try:
self._linux_spacenav_client = LinuxSpacenavClient()
if self._linux_spacenav_client.available:
if not self._linux_spacenav_client.open():
logger.warning("Failed to open connection via LinuxSpacenavClient.")
self._linux_spacenav_client = None
else:
logger.info("LinuxSpacenavClient initialized and opened successfully.")
# self.enable_navigation(True) # This is a pynavlib call, handle equivalent in set_motion_flag
else:
logger.warning("LinuxSpacenavClient is not available (library not found or functions missing).")
self._linux_spacenav_client = None
except Exception as e:
logger.error(f"Exception during LinuxSpacenavClient initialization: {e}")
self._linux_spacenav_client = None
else:
pynav.NavlibNavigationModel.__init__(self, False, pynav.NavlibOptions.RowMajorOrder)
Tool.__init__(self)
self.put_profile_hint("UltiMaker Cura")
self.enable_navigation(True)
def event(self, event):
if self._platform_system == "Linux" and self._linux_spacenav_client:
self._update_linux_events()
# For Linux, we might not want to call super().event() if it's Tool's base event,
# unless specific Tool event handling is desired independent of spacenav.
# For now, let's assume spacenav events are primary for this tool on Linux.
return True # Indicate event was handled
# Original event handling for non-Linux or if Linux client failed
if hasattr(super(), "event"): # pynav.NavlibNavigationModel does not have event, Tool does.
return super().event(event) # Call Tool.event
return False
def _update_linux_events(self):
if not self._linux_spacenav_client:
return
while True:
event_data = self._linux_spacenav_client.poll_event()
if event_data is None:
break # No more events
if event_data.type == SPNAV_EVENT_MOTION:
logger.info(f"Linux Motion Event: t({event_data.motion.x}, {event_data.motion.y}, {event_data.motion.z}) "
f"r({event_data.motion.rx}, {event_data.motion.ry}, {event_data.motion.rz})")
# Placeholder: Construct a simple transformation matrix
# This needs significant refinement for actual camera control.
# Scaling factors are arbitrary for now.
scale_t = 0.01
scale_r = 0.001
# Create a new matrix for delta transformation
delta_matrix = Matrix()
# Apply translation (Todo: map to camera coordinates correctly)
delta_matrix.translate(Vector(event_data.motion.x * scale_t,
-event_data.motion.y * scale_t, # Inverting Y for typical screen coords
event_data.motion.z * scale_t))
# Apply rotations (Todo: map to camera axes correctly)
# For now, just using some fixed axes for rotation demonstration.
# These rotations should be composed correctly.
# rx -> pitch, ry -> yaw, rz -> roll (example mapping)
if abs(event_data.motion.rx) > 10 : # Some threshold
delta_matrix.rotateByAngle(Vector(1,0,0), event_data.motion.rx * scale_r)
if abs(event_data.motion.ry) > 10:
delta_matrix.rotateByAngle(Vector(0,1,0), event_data.motion.ry * scale_r)
if abs(event_data.motion.rz) > 10:
delta_matrix.rotateByAngle(Vector(0,0,1), event_data.motion.rz * scale_r)
current_cam_matrix = self._scene.getActiveCamera().getLocalTransformation()
new_cam_matrix = current_cam_matrix.multiply(delta_matrix) # Apply delta
self.set_camera_matrix(new_cam_matrix)
elif event_data.type == SPNAV_EVENT_BUTTON:
logger.info(f"Linux Button Event: press={event_data.button.press}, bnum={event_data.button.bnum}")
def pick(self, x: float, y: float, check_selection: bool = False, radius: float = 0.) -> Optional[Vector]:
@ -77,7 +173,12 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return result_position
def get_pointer_position(self) -> "pynav.NavlibVector":
def get_pointer_position(self) -> Optional["pynav.NavlibVector"]: # Adjusted return type for Linux
if self._platform_system == "Linux":
# Not directly applicable or available from libspnav.
# Could potentially use mouse position if needed for some hybrid mode.
logger.debug("get_pointer_position called on Linux, returning None.")
return None
from UM.Qt.QtApplication import QtApplication
main_window = QtApplication.getInstance().getMainWindow()
@ -99,7 +200,11 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z)
def get_view_extents(self) -> "pynav.NavlibBox":
def get_view_extents(self) -> Optional["pynav.NavlibBox"]: # Adjusted return type for Linux
if self._platform_system == "Linux":
logger.debug("get_view_extents called on Linux, returning None (pynav.NavlibBox specific).")
# Could return a dict if some generic extent info is needed
return None
view_width = self._scene.getActiveCamera().getViewportWidth()
view_height = self._scene.getActiveCamera().getViewportHeight()
@ -111,7 +216,10 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return pynav.NavlibBox(pt_min, pt_max)
def get_view_frustum(self) -> "pynav.NavlibFrustum":
def get_view_frustum(self) -> Optional["pynav.NavlibFrustum"]: # Adjusted return type for Linux
if self._platform_system == "Linux":
logger.debug("get_view_frustum called on Linux, returning None (pynav.NavlibFrustum specific).")
return None
projection_matrix = self._scene.getActiveCamera().getProjectionMatrix()
half_height = 2. / projection_matrix.getData()[1,1]
@ -120,9 +228,14 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.)
def get_is_view_perspective(self) -> bool:
# This method can be common if _scene and getActiveCamera() are available.
return self._scene.getActiveCamera().isPerspective()
def get_selection_extents(self) -> "pynav.NavlibBox":
def get_selection_extents(self) -> Optional["pynav.NavlibBox"]: # Adjusted return type for Linux
if self._platform_system == "Linux":
logger.debug("get_selection_extents called on Linux, returning None (pynav.NavlibBox specific).")
# Could try to implement similar logic to below and return a dict if needed.
return None
from UM.Scene.Selection import Selection
bounding_box = Selection.getBoundingBox()
@ -132,17 +245,27 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
pt_max = pynav.NavlibVector(bounding_box.maximum.x, bounding_box.maximum.y, bounding_box.maximum.z)
return pynav.NavlibBox(pt_min, pt_max)
def get_selection_transform(self) -> "pynav.NavlibMatrix":
def get_selection_transform(self) -> Optional["pynav.NavlibMatrix"]: # Adjusted return type for Linux
if self._platform_system == "Linux":
logger.debug("get_selection_transform called on Linux, returning None (pynav.NavlibMatrix specific).")
return None # No direct equivalent from libspnav
return pynav.NavlibMatrix()
def get_is_selection_empty(self) -> bool:
# This method can be common if Selection is accessible.
from UM.Scene.Selection import Selection
return not Selection.hasSelection()
def get_pivot_visible(self) -> bool:
return False
if self._platform_system == "Linux":
# If we want to use the pivot node on Linux, this needs proper implementation
return self._pivot_node.isEnabled() # Assuming OverlayNode has isEnabled or similar
return False # Original behavior for non-Linux for now
def get_camera_matrix(self) -> "pynav.NavlibMatrix":
def get_camera_matrix(self) -> "pynav.NavlibMatrix" or Matrix: # Adjusted return type
if self._platform_system == "Linux":
# Return UM.Math.Matrix directly for Linux
return self._scene.getActiveCamera().getLocalTransformation()
transformation = self._scene.getActiveCamera().getLocalTransformation()
@ -151,13 +274,23 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
[transformation.at(2, 0), transformation.at(2, 1), transformation.at(2, 2), transformation.at(2, 3)],
[transformation.at(3, 0), transformation.at(3, 1), transformation.at(3, 2), transformation.at(3, 3)]])
def get_coordinate_system(self) -> "pynav.NavlibMatrix":
def get_coordinate_system(self) -> Optional["pynav.NavlibMatrix"]: # Adjusted
if self._platform_system == "Linux":
logger.debug("get_coordinate_system called on Linux, returning None.")
return None
return pynav.NavlibMatrix()
def get_front_view(self) -> "pynav.NavlibMatrix":
def get_front_view(self) -> Optional["pynav.NavlibMatrix"]: # Adjusted
if self._platform_system == "Linux":
logger.debug("get_front_view called on Linux, returning None.")
return None
return pynav.NavlibMatrix()
def get_model_extents(self) -> "pynav.NavlibBox":
def get_model_extents(self) -> Optional["pynav.NavlibBox"]: # Adjusted
if self._platform_system == "Linux":
# Could implement the logic below and return a dict {min: [x,y,z], max: [x,y,z]}
logger.debug("get_model_extents called on Linux, returning None (pynav.NavlibBox specific).")
return None
result_bbox = AxisAlignedBox()
build_volume_bbox = None
@ -179,10 +312,17 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
self._scene_radius = (result_bbox.maximum - self._scene_center).length()
return pynav.NavlibBox(pt_min, pt_max)
def get_pivot_position(self) -> "pynav.NavlibVector":
def get_pivot_position(self) -> Optional["pynav.NavlibVector"]: # Adjusted
if self._platform_system == "Linux":
# If using pivot node: return Vector(self._pivot_node.getPosition().x, ...)
logger.debug("get_pivot_position called on Linux, returning None.")
return None
return pynav.NavlibVector()
def get_hit_look_at(self) -> "pynav.NavlibVector":
def get_hit_look_at(self) -> Optional["pynav.NavlibVector"]: # Adjusted
if self._platform_system == "Linux":
logger.debug("get_hit_look_at called on Linux, returning None (relies on picking).")
return None
if self._was_pick and self._pointer_pick is not None:
return pynav.NavlibVector(self._pointer_pick.x, self._pointer_pick.y, self._pointer_pick.z)
@ -197,13 +337,31 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z)
def get_units_to_meters(self) -> float:
if self._platform_system == "Linux":
# This value might need to be configurable or determined differently.
# For now, returning a default.
return 0.05
return 0.05
def is_user_pivot(self) -> bool:
if self._platform_system == "Linux":
# If pivot control is added for Linux, this needs actual implementation
return False
return False
def set_camera_matrix(self, matrix : "pynav.NavlibMatrix") -> None:
def set_camera_matrix(self, matrix : "pynav.NavlibMatrix" or Matrix) -> None:
if self._platform_system == "Linux":
if not isinstance(matrix, Matrix):
logger.error("set_camera_matrix on Linux called with incorrect matrix type.")
return
# Directly set the transformation for the active camera
self._scene.getActiveCamera().setTransformation(matrix)
# TODO: Consider if pivot node scaling is needed here for Linux
# The logic below for pivot scaling might be reusable if self._pivot_node is active.
# For now, keeping it simple.
return
# Original non-Linux logic:
# !!!!!!
# Hit testing in Orthographic view is not reliable
# Picking starts in camera position, not on near plane
@ -246,28 +404,75 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
self._pivot_node.scale(scale)
def set_view_extents(self, extents: "pynav.NavlibBox") -> None:
if self._platform_system == "Linux":
logger.debug("set_view_extents called on Linux. No-op for now.")
# If needed, would have to interpret extents (possibly a dict) and set zoom.
return
view_width = self._scene.getActiveCamera().getViewportWidth()
new_zoom = (extents._min._x + view_width / 2.) / - view_width
self._scene.getActiveCamera().setZoomFactor(new_zoom)
def set_hit_selection_only(self, onlySelection : bool) -> None:
# This can be common logic.
self._hit_selection_only = onlySelection
def set_motion_flag(self, motion : bool) -> None:
if self._platform_system == "Linux":
if self._linux_spacenav_client and self._linux_spacenav_client.available:
if motion:
logger.info("set_motion_flag(True) called on Linux. Ensuring Spacenav is open.")
if self._linux_spacenav_client.open():
logger.info("LinuxSpacenavClient is open.")
else:
logger.warning("Failed to open LinuxSpacenavClient on set_motion_flag(True).")
else:
logger.info("set_motion_flag(False) called on Linux. Closing Spacenav.")
self._linux_spacenav_client.close() # close() in client logs success/failure
elif motion: # motion is True, but client is not available/initialized
logger.warning("set_motion_flag(True) called on Linux, but Spacenav client is not available.")
# Since Tool.__init__ is not called on Linux, picking pass management is not relevant here.
return
# Original non-Linux logic:
if motion:
width = self._scene.getActiveCamera().getViewportWidth()
height = self._scene.getActiveCamera().getViewportHeight()
self._picking_pass = PickingPass(width, height)
self._renderer.addRenderPass(self._picking_pass)
if self._picking_pass is None: # Ensure picking pass is only added once
width = self._scene.getActiveCamera().getViewportWidth()
height = self._scene.getActiveCamera().getViewportHeight()
if width > 0 and height > 0:
self._picking_pass = PickingPass(width, height)
self._renderer.addRenderPass(self._picking_pass)
else:
logger.warning("Cannot create PickingPass, viewport dimensions are invalid.")
else:
self._was_pick = False
self._renderer.removeRenderPass(self._picking_pass)
if self._picking_pass is not None:
self._renderer.removeRenderPass(self._picking_pass)
self._picking_pass = None # Explicitly set to None after removal
def set_pivot_position(self, position) -> None: # `position` is pynav.NavlibVector or UM.Math.Vector
if self._platform_system == "Linux":
if isinstance(position, Vector): # Assuming position might be UM.Math.Vector for Linux
self._pivot_node._target_node.setPosition(position=position, transform_space = SceneNode.TransformSpace.World)
logger.debug(f"Set pivot position on Linux to {position}")
else:
logger.warning("set_pivot_position on Linux called with unexpected type.")
return
def set_pivot_position(self, position) -> None:
self._pivot_node._target_node.setPosition(position=Vector(position._x, position._y, position._z), transform_space = SceneNode.TransformSpace.World)
def set_pivot_visible(self, visible) -> None:
# This logic can be common if _scene and _pivot_node are available.
if visible:
self._scene.getRoot().addChild(self._pivot_node)
if self._pivot_node not in self._scene.getRoot().getChildren():
self._scene.getRoot().addChild(self._pivot_node)
else:
self._scene.getRoot().removeChild(self._pivot_node)
if self._pivot_node in self._scene.getRoot().getChildren():
self._scene.getRoot().removeChild(self._pivot_node)
# Ensure logging is configured if this file is run standalone (e.g. for type checking)
# This is more of a library class, so direct execution isn't typical.
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
logger.info("NavlibClient.py - basic structure for type checking or direct import.")

View file

@ -20,7 +20,21 @@ def getMetaData() -> Dict[str, Any]:
def register(app: "Application") -> Dict[str, Any]:
try:
from .NavlibClient import NavlibClient
return { "tool": NavlibClient(app.getController().getScene(), app.getRenderer()) }
client = NavlibClient(app.getController().getScene(), app.getRenderer())
# Check for Linux-specific initialization failure
if hasattr(client, "_platform_system") and client._platform_system == "Linux":
if not hasattr(client, "_linux_spacenav_client") or \
client._linux_spacenav_client is None or \
not client._linux_spacenav_client.available:
Logger.warning("Failed to initialize LinuxSpacenavClient. 3Dconnexion plugin will be disabled on Linux.")
return {} # Disable plugin on Linux due to internal init failure
# If pynavlib failed on non-Linux, it would likely raise an import error or similar,
# caught by the BaseException below.
# If on Linux and the above check passed, or on other platforms and NavlibClient init was successful.
return {"tool": client}
except BaseException as exception:
Logger.warning(f"Unable to load 3Dconnexion library: {exception}")
return { }
Logger.warning(f"Unable to load or initialize 3Dconnexion client: {exception}")
return {}