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 # Copyright (c) 2025 3Dconnexion, UltiMaker
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import platform # Added
import logging # Added
from typing import Optional from typing import Optional
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -13,23 +16,116 @@ from UM.Resources import Resources
from UM.Tool import Tool from UM.Tool import Tool
from UM.View.Renderer import Renderer from UM.View.Renderer import Renderer
from .OverlayNode import OverlayNode from .OverlayNode import OverlayNode
from .LinuxSpacenavClient import LinuxSpacenavClient, SPNAV_EVENT_MOTION, SPNAV_EVENT_BUTTON # Added
import pynavlib.pynavlib_interface as pynav import pynavlib.pynavlib_interface as pynav
logger = logging.getLogger(__name__) # Added
class NavlibClient(pynav.NavlibNavigationModel, Tool): class NavlibClient(pynav.NavlibNavigationModel, Tool):
def __init__(self, scene: Scene, renderer: Renderer) -> None: def __init__(self, scene: Scene, renderer: Renderer) -> None:
pynav.NavlibNavigationModel.__init__(self, False, pynav.NavlibOptions.RowMajorOrder) self._platform_system = platform.system()
Tool.__init__(self) self._linux_spacenav_client: Optional[LinuxSpacenavClient] = None
self._scene = scene self._scene = scene
self._renderer = renderer self._renderer = renderer
self._pointer_pick = None self._pointer_pick = None # Retain for pick()
self._was_pick = False self._was_pick = False # Retain for pick() / get_hit_look_at()
self._hit_selection_only = False self._hit_selection_only = False # Retain for pick() / get_hit_look_at()
self._picking_pass = None 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._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "cor.png"), size=2.5)
self.put_profile_hint("UltiMaker Cura") self._scene_center = Vector() # Used in set_camera_matrix, ensure it exists
self.enable_navigation(True) 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]: 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 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 from UM.Qt.QtApplication import QtApplication
main_window = QtApplication.getInstance().getMainWindow() 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) 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_width = self._scene.getActiveCamera().getViewportWidth()
view_height = self._scene.getActiveCamera().getViewportHeight() view_height = self._scene.getActiveCamera().getViewportHeight()
@ -111,7 +216,10 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
return pynav.NavlibBox(pt_min, pt_max) 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() projection_matrix = self._scene.getActiveCamera().getProjectionMatrix()
half_height = 2. / projection_matrix.getData()[1,1] 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.) return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.)
def get_is_view_perspective(self) -> bool: def get_is_view_perspective(self) -> bool:
# This method can be common if _scene and getActiveCamera() are available.
return self._scene.getActiveCamera().isPerspective() 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 from UM.Scene.Selection import Selection
bounding_box = Selection.getBoundingBox() 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) pt_max = pynav.NavlibVector(bounding_box.maximum.x, bounding_box.maximum.y, bounding_box.maximum.z)
return pynav.NavlibBox(pt_min, pt_max) 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() return pynav.NavlibMatrix()
def get_is_selection_empty(self) -> bool: def get_is_selection_empty(self) -> bool:
# This method can be common if Selection is accessible.
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
return not Selection.hasSelection() return not Selection.hasSelection()
def get_pivot_visible(self) -> bool: 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() 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(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)]]) [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() 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() 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() result_bbox = AxisAlignedBox()
build_volume_bbox = None build_volume_bbox = None
@ -179,10 +312,17 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
self._scene_radius = (result_bbox.maximum - self._scene_center).length() self._scene_radius = (result_bbox.maximum - self._scene_center).length()
return pynav.NavlibBox(pt_min, pt_max) 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() 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: 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) 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) return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z)
def get_units_to_meters(self) -> float: 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 return 0.05
def is_user_pivot(self) -> bool: 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 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 # Hit testing in Orthographic view is not reliable
# Picking starts in camera position, not on near plane # Picking starts in camera position, not on near plane
@ -246,28 +404,75 @@ class NavlibClient(pynav.NavlibNavigationModel, Tool):
self._pivot_node.scale(scale) self._pivot_node.scale(scale)
def set_view_extents(self, extents: "pynav.NavlibBox") -> None: 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() view_width = self._scene.getActiveCamera().getViewportWidth()
new_zoom = (extents._min._x + view_width / 2.) / - view_width new_zoom = (extents._min._x + view_width / 2.) / - view_width
self._scene.getActiveCamera().setZoomFactor(new_zoom) self._scene.getActiveCamera().setZoomFactor(new_zoom)
def set_hit_selection_only(self, onlySelection : bool) -> None: def set_hit_selection_only(self, onlySelection : bool) -> None:
# This can be common logic.
self._hit_selection_only = onlySelection self._hit_selection_only = onlySelection
def set_motion_flag(self, motion : bool) -> None: 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: if motion:
width = self._scene.getActiveCamera().getViewportWidth() if self._picking_pass is None: # Ensure picking pass is only added once
height = self._scene.getActiveCamera().getViewportHeight() width = self._scene.getActiveCamera().getViewportWidth()
self._picking_pass = PickingPass(width, height) height = self._scene.getActiveCamera().getViewportHeight()
self._renderer.addRenderPass(self._picking_pass) 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: else:
self._was_pick = False 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) 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: def set_pivot_visible(self, visible) -> None:
# This logic can be common if _scene and _pivot_node are available.
if visible: 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: 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]: def register(app: "Application") -> Dict[str, Any]:
try: try:
from .NavlibClient import NavlibClient 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: except BaseException as exception:
Logger.warning(f"Unable to load 3Dconnexion library: {exception}") Logger.warning(f"Unable to load or initialize 3Dconnexion client: {exception}")
return { } return {}