NavlibClient.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # Copyright (c) 2025 3Dconnexion, UltiMaker
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Optional
  4. from UM.Math.Matrix import Matrix
  5. from UM.Math.Vector import Vector
  6. from UM.Math.AxisAlignedBox import AxisAlignedBox
  7. from cura.PickingPass import PickingPass
  8. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  9. from UM.Scene.SceneNode import SceneNode
  10. from UM.Scene.Scene import Scene
  11. from UM.Resources import Resources
  12. from UM.Tool import Tool
  13. from UM.View.Renderer import Renderer
  14. from .OverlayNode import OverlayNode
  15. import pynavlib.pynavlib_interface as pynav
  16. class NavlibClient(pynav.NavlibNavigationModel, Tool):
  17. def __init__(self, scene: Scene, renderer: Renderer) -> None:
  18. pynav.NavlibNavigationModel.__init__(self, False, pynav.NavlibOptions.RowMajorOrder)
  19. Tool.__init__(self)
  20. self._scene = scene
  21. self._renderer = renderer
  22. self._pointer_pick = None
  23. self._was_pick = False
  24. self._hit_selection_only = False
  25. self._picking_pass = None
  26. self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "cor.png"), size=2.5)
  27. self.put_profile_hint("UltiMaker Cura")
  28. self.enable_navigation(True)
  29. def pick(self, x: float, y: float, check_selection: bool = False, radius: float = 0.) -> Optional[Vector]:
  30. if self._picking_pass is None or radius < 0.:
  31. return None
  32. step = 0.
  33. if radius == 0.:
  34. grid_resolution = 0
  35. else:
  36. grid_resolution = 5
  37. step = (2. * radius) / float(grid_resolution)
  38. min_depth = 99999.
  39. result_position = None
  40. for i in range(grid_resolution + 1):
  41. for j in range(grid_resolution + 1):
  42. coord_x = (x - radius) + i * step
  43. coord_y = (y - radius) + j * step
  44. picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y)
  45. max_depth = 16777.215
  46. if 0. < picked_depth < max_depth:
  47. valid_hit = True
  48. if check_selection:
  49. selection_pass = self._renderer.getRenderPass("selection")
  50. picked_object_id = selection_pass.getIdAtPosition(coord_x, coord_y)
  51. picked_object = self._scene.findObject(picked_object_id)
  52. from UM.Scene.Selection import Selection
  53. valid_hit = Selection.isSelected(picked_object)
  54. if not valid_hit and grid_resolution > 0.:
  55. continue
  56. elif not valid_hit and grid_resolution == 0.:
  57. return None
  58. if picked_depth < min_depth:
  59. min_depth = picked_depth
  60. result_position = self._picking_pass.getPickedPosition(coord_x, coord_y)
  61. return result_position
  62. def get_pointer_position(self) -> "pynav.NavlibVector":
  63. from UM.Qt.QtApplication import QtApplication
  64. main_window = QtApplication.getInstance().getMainWindow()
  65. x_n = 2. * main_window._mouse_x / main_window.width() - 1.
  66. y_n = 2. * main_window._mouse_y / main_window.height() - 1.
  67. if self.get_is_view_perspective():
  68. self._was_pick = True
  69. from cura.Utils.Threading import call_on_qt_thread
  70. wrapped_pick = call_on_qt_thread(self.pick)
  71. self._pointer_pick = wrapped_pick(x_n, y_n)
  72. return pynav.NavlibVector(0., 0., 0.)
  73. else:
  74. ray = self._scene.getActiveCamera().getRay(x_n, y_n)
  75. pointer_position = ray.origin + ray.direction
  76. return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z)
  77. def get_view_extents(self) -> "pynav.NavlibBox":
  78. view_width = self._scene.getActiveCamera().getViewportWidth()
  79. view_height = self._scene.getActiveCamera().getViewportHeight()
  80. horizontal_zoom = view_width * self._scene.getActiveCamera().getZoomFactor()
  81. vertical_zoom = view_height * self._scene.getActiveCamera().getZoomFactor()
  82. pt_min = pynav.NavlibVector(-view_width / 2 - horizontal_zoom, -view_height / 2 - vertical_zoom, -9001)
  83. pt_max = pynav.NavlibVector(view_width / 2 + horizontal_zoom, view_height / 2 + vertical_zoom, 9001)
  84. return pynav.NavlibBox(pt_min, pt_max)
  85. def get_view_frustum(self) -> "pynav.NavlibFrustum":
  86. projection_matrix = self._scene.getActiveCamera().getProjectionMatrix()
  87. half_height = 2. / projection_matrix.getData()[1,1]
  88. half_width = half_height * (projection_matrix.getData()[1,1] / projection_matrix.getData()[0,0])
  89. return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.)
  90. def get_is_view_perspective(self) -> bool:
  91. return self._scene.getActiveCamera().isPerspective()
  92. def get_selection_extents(self) -> "pynav.NavlibBox":
  93. from UM.Scene.Selection import Selection
  94. bounding_box = Selection.getBoundingBox()
  95. if(bounding_box is not None) :
  96. pt_min = pynav.NavlibVector(bounding_box.minimum.x, bounding_box.minimum.y, bounding_box.minimum.z)
  97. pt_max = pynav.NavlibVector(bounding_box.maximum.x, bounding_box.maximum.y, bounding_box.maximum.z)
  98. return pynav.NavlibBox(pt_min, pt_max)
  99. def get_selection_transform(self) -> "pynav.NavlibMatrix":
  100. return pynav.NavlibMatrix()
  101. def get_is_selection_empty(self) -> bool:
  102. from UM.Scene.Selection import Selection
  103. return not Selection.hasSelection()
  104. def get_pivot_visible(self) -> bool:
  105. return False
  106. def get_camera_matrix(self) -> "pynav.NavlibMatrix":
  107. transformation = self._scene.getActiveCamera().getLocalTransformation()
  108. return pynav.NavlibMatrix([[transformation.at(0, 0), transformation.at(0, 1), transformation.at(0, 2), transformation.at(0, 3)],
  109. [transformation.at(1, 0), transformation.at(1, 1), transformation.at(1, 2), transformation.at(1, 3)],
  110. [transformation.at(2, 0), transformation.at(2, 1), transformation.at(2, 2), transformation.at(2, 3)],
  111. [transformation.at(3, 0), transformation.at(3, 1), transformation.at(3, 2), transformation.at(3, 3)]])
  112. def get_coordinate_system(self) -> "pynav.NavlibMatrix":
  113. return pynav.NavlibMatrix()
  114. def get_front_view(self) -> "pynav.NavlibMatrix":
  115. return pynav.NavlibMatrix()
  116. def get_model_extents(self) -> "pynav.NavlibBox":
  117. result_bbox = AxisAlignedBox()
  118. build_volume_bbox = None
  119. for node in DepthFirstIterator(self._scene.getRoot()):
  120. node.setCalculateBoundingBox(True)
  121. if node.__class__.__qualname__ == "CuraSceneNode" :
  122. result_bbox = result_bbox + node.getBoundingBox()
  123. elif node.__class__.__qualname__ == "BuildVolume":
  124. build_volume_bbox = node.getBoundingBox()
  125. if not result_bbox.isValid():
  126. result_bbox = build_volume_bbox
  127. if result_bbox is not None:
  128. pt_min = pynav.NavlibVector(result_bbox.minimum.x, result_bbox.minimum.y, result_bbox.minimum.z)
  129. pt_max = pynav.NavlibVector(result_bbox.maximum.x, result_bbox.maximum.y, result_bbox.maximum.z)
  130. self._scene_center = result_bbox.center
  131. self._scene_radius = (result_bbox.maximum - self._scene_center).length()
  132. return pynav.NavlibBox(pt_min, pt_max)
  133. def get_pivot_position(self) -> "pynav.NavlibVector":
  134. return pynav.NavlibVector()
  135. def get_hit_look_at(self) -> "pynav.NavlibVector":
  136. if self._was_pick and self._pointer_pick is not None:
  137. return pynav.NavlibVector(self._pointer_pick.x, self._pointer_pick.y, self._pointer_pick.z)
  138. elif self._was_pick and self._pointer_pick is None:
  139. return None
  140. from cura.Utils.Threading import call_on_qt_thread
  141. wrapped_pick = call_on_qt_thread(self.pick)
  142. picked_position = wrapped_pick(0, 0, self._hit_selection_only, 0.5)
  143. if picked_position is not None:
  144. return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z)
  145. def get_units_to_meters(self) -> float:
  146. return 0.05
  147. def is_user_pivot(self) -> bool:
  148. return False
  149. def set_camera_matrix(self, matrix : "pynav.NavlibMatrix") -> None:
  150. # !!!!!!
  151. # Hit testing in Orthographic view is not reliable
  152. # Picking starts in camera position, not on near plane
  153. # which results in wrong depth values (visible geometry
  154. # cannot be picked properly) - Workaround needed (camera position offset)
  155. # !!!!!!
  156. if not self.get_is_view_perspective():
  157. affine = matrix._matrix
  158. direction = Vector(-affine[0][2], -affine[1][2], -affine[2][2])
  159. distance = self._scene_center - Vector(affine[0][3], affine[1][3], affine[2][3])
  160. cos_value = direction.dot(distance.normalized())
  161. offset = 0.
  162. if (distance.length() < self._scene_radius) and (cos_value > 0.):
  163. offset = self._scene_radius
  164. elif (distance.length() < self._scene_radius) and (cos_value < 0.):
  165. offset = 2. * self._scene_radius
  166. elif (distance.length() > self._scene_radius) and (cos_value < 0.):
  167. offset = 2. * distance.length()
  168. matrix._matrix[0][3] = matrix._matrix[0][3] - offset * direction.x
  169. matrix._matrix[1][3] = matrix._matrix[1][3] - offset * direction.y
  170. matrix._matrix[2][3] = matrix._matrix[2][3] - offset * direction.z
  171. transformation = Matrix(data = matrix._matrix)
  172. self._scene.getActiveCamera().setTransformation(transformation)
  173. active_camera = self._scene.getActiveCamera()
  174. if active_camera.isPerspective():
  175. camera_position = active_camera.getWorldPosition()
  176. dist = (camera_position - self._pivot_node.getWorldPosition()).length()
  177. scale = dist / 400.
  178. else:
  179. view_width = active_camera.getViewportWidth()
  180. current_size = view_width + (2. * active_camera.getZoomFactor() * view_width)
  181. scale = current_size / view_width * 5.
  182. self._pivot_node.scale(scale)
  183. def set_view_extents(self, extents: "pynav.NavlibBox") -> None:
  184. view_width = self._scene.getActiveCamera().getViewportWidth()
  185. new_zoom = (extents._min._x + view_width / 2.) / - view_width
  186. self._scene.getActiveCamera().setZoomFactor(new_zoom)
  187. def set_hit_selection_only(self, onlySelection : bool) -> None:
  188. self._hit_selection_only = onlySelection
  189. def set_motion_flag(self, motion : bool) -> None:
  190. if motion:
  191. width = self._scene.getActiveCamera().getViewportWidth()
  192. height = self._scene.getActiveCamera().getViewportHeight()
  193. self._picking_pass = PickingPass(width, height)
  194. self._renderer.addRenderPass(self._picking_pass)
  195. else:
  196. self._was_pick = False
  197. self._renderer.removeRenderPass(self._picking_pass)
  198. def set_pivot_position(self, position) -> None:
  199. self._pivot_node._target_node.setPosition(position=Vector(position._x, position._y, position._z), transform_space = SceneNode.TransformSpace.World)
  200. def set_pivot_visible(self, visible) -> None:
  201. if visible:
  202. self._scene.getRoot().addChild(self._pivot_node)
  203. else:
  204. self._scene.getRoot().removeChild(self._pivot_node)