mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-06-26 09:25:24 -06:00
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:
parent
c726d61086
commit
4698089f2a
3 changed files with 442 additions and 32 deletions
191
plugins/3DConnexion/LinuxSpacenavClient.py
Normal file
191
plugins/3DConnexion/LinuxSpacenavClient.py
Normal 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.")
|
|
@ -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.")
|
||||
|
|
|
@ -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 {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue