From 5cf52a5d852cf26d863fbc667cb387ef5b3546f3 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 7 Feb 2025 14:50:23 +0100 Subject: [PATCH 1/4] Define dummy 3DConnexion interface in case module is not available CURA-12404 --- cura/NavlibClient.py | 458 ++++++++++++++++++++++--------------------- 1 file changed, 237 insertions(+), 221 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index f991a007d8..4a81a5d9c2 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -1,4 +1,3 @@ -import pynavlib.pynavlib_interface as pynav from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector from UM.Math.AxisAlignedBox import AxisAlignedBox @@ -6,259 +5,276 @@ from cura.PickingPass import PickingPass from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from cura.Scene.OverlayNode import OverlayNode, SceneNode from UM.Resources import Resources +from UM.Logger import Logger -class NavlibClient(pynav.NavlibNavigationModel): +try: + import pynavlib.pynavlib_interface as pynav - def __init__(self, scene, renderer) -> None: - super().__init__(False, pynav.NavlibOptions.RowMajorOrder) - self._scene = scene - self._renderer = renderer - self._pointer_pick = None - self._was_pick = False - self._hit_selection_only = False - self._picking_pass = None - self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "3dx_pivot.png"), size=3.) + class NavlibClient(pynav.NavlibNavigationModel): - def pick(self, x, y, check_selection = False, radius = 0.): + def __init__(self, scene, renderer) -> None: + super().__init__(False, pynav.NavlibOptions.RowMajorOrder) + self._scene = scene + self._renderer = renderer + self._pointer_pick = None + self._was_pick = False + self._hit_selection_only = False + self._picking_pass = None + self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "3dx_pivot.png"), size=3.) - if self._picking_pass is None or radius < 0.: - return None + def pick(self, x, y, check_selection = False, radius = 0.): - step = 0. - if radius == 0.: - grid_resolution = 0 - else: - grid_resolution = 5 - step = (2. * radius) / float(grid_resolution) + if self._picking_pass is None or radius < 0.: + return None - min_depth = 99999. - result_position = None + step = 0. + if radius == 0.: + grid_resolution = 0 + else: + grid_resolution = 5 + step = (2. * radius) / float(grid_resolution) - for i in range(grid_resolution + 1): - for j in range(grid_resolution + 1): + min_depth = 99999. + result_position = None - coord_x = (x - radius) + i * step - coord_y = (y - radius) + j * step - - picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y) - max_depth = 16777.215 + for i in range(grid_resolution + 1): + for j in range(grid_resolution + 1): - if 0. < picked_depth < max_depth: + coord_x = (x - radius) + i * step + coord_y = (y - radius) + j * step - valid_hit = True - if check_selection: - selection_pass = self._renderer.getRenderPass("selection") - picked_object_id = selection_pass.getIdAtPosition(coord_x, coord_y) - picked_object = self._scene.findObject(picked_object_id) + picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y) + max_depth = 16777.215 - from UM.Scene.Selection import Selection - valid_hit = Selection.isSelected(picked_object) + if 0. < picked_depth < max_depth: - if not valid_hit and grid_resolution > 0.: - continue - elif not valid_hit and grid_resolution == 0.: - return None + valid_hit = True + if check_selection: + selection_pass = self._renderer.getRenderPass("selection") + picked_object_id = selection_pass.getIdAtPosition(coord_x, coord_y) + picked_object = self._scene.findObject(picked_object_id) - if picked_depth < min_depth: - min_depth = picked_depth - result_position = self._picking_pass.getPickedPosition(coord_x, coord_y) - - return result_position - - def get_pointer_position(self)->pynav.NavlibVector: + from UM.Scene.Selection import Selection + valid_hit = Selection.isSelected(picked_object) - from UM.Qt.QtApplication import QtApplication - main_window = QtApplication.getInstance().getMainWindow() + if not valid_hit and grid_resolution > 0.: + continue + elif not valid_hit and grid_resolution == 0.: + return None - x_n = 2. * main_window._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. - y_n = 2. * main_window._mouse_y / self._scene.getActiveCamera().getViewportHeight() - 1. + if picked_depth < min_depth: + min_depth = picked_depth + result_position = self._picking_pass.getPickedPosition(coord_x, coord_y) + + return result_position + + def get_pointer_position(self)->pynav.NavlibVector: + + from UM.Qt.QtApplication import QtApplication + main_window = QtApplication.getInstance().getMainWindow() + + x_n = 2. * main_window._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. + y_n = 2. * main_window._mouse_y / self._scene.getActiveCamera().getViewportHeight() - 1. + + if self.get_is_view_perspective(): + self._was_pick = True + from cura.Utils.Threading import call_on_qt_thread + wrapped_pick = call_on_qt_thread(self.pick) + + self._pointer_pick = wrapped_pick(x_n, y_n) + + return pynav.NavlibVector(0., 0., 0.) + else: + ray = self._scene.getActiveCamera().getRay(x_n, y_n) + pointer_position = ray.origin + ray.direction + + return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) + + def get_view_extents(self)->pynav.NavlibBox: + + view_width = self._scene.getActiveCamera().getViewportWidth() + view_height = self._scene.getActiveCamera().getViewportHeight() + horizontal_zoom = view_width * self._scene.getActiveCamera().getZoomFactor() + vertical_zoom = view_height * self._scene.getActiveCamera().getZoomFactor() + + pt_min = pynav.NavlibVector(-view_width / 2 - horizontal_zoom, -view_height / 2 - vertical_zoom, -9001) + pt_max = pynav.NavlibVector(view_width / 2 + horizontal_zoom, view_height / 2 + vertical_zoom, 9001) + + return pynav.NavlibBox(pt_min, pt_max) + + def get_view_frustum(self)->pynav.NavlibFrustum: + + projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() + half_height = 2. / projection_matrix.getData()[1,1] + half_width = half_height * (projection_matrix.getData()[1,1] / projection_matrix.getData()[0,0]) + + return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.) + + def get_is_view_perspective(self)->bool: + return self._scene.getActiveCamera().isPerspective() + + def get_selection_extents(self)->pynav.NavlibBox: + + from UM.Scene.Selection import Selection + bounding_box = Selection.getBoundingBox() + + if(bounding_box is not None) : + pt_min = pynav.NavlibVector(bounding_box.minimum.x, bounding_box.minimum.y, bounding_box.minimum.z) + 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: + return pynav.NavlibMatrix() + + def get_is_selection_empty(self)->bool: + from UM.Scene.Selection import Selection + return not Selection.hasSelection() + + def get_pivot_visible(self)->bool: + return False + + def get_camera_matrix(self)->pynav.NavlibMatrix: + + transformation = self._scene.getActiveCamera().getLocalTransformation() + + return pynav.NavlibMatrix([[transformation.at(0, 0), transformation.at(0, 1), transformation.at(0, 2), transformation.at(0, 3)], + [transformation.at(1, 0), transformation.at(1, 1), transformation.at(1, 2), transformation.at(1, 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)]]) + + def get_coordinate_system(self)->pynav.NavlibMatrix: + return pynav.NavlibMatrix() + + def get_front_view(self)->pynav.NavlibMatrix: + return pynav.NavlibMatrix() + + def get_model_extents(self)->pynav.NavlibBox: + + result_bbox = AxisAlignedBox() + build_volume_bbox = None + + for node in DepthFirstIterator(self._scene.getRoot()): + node.setCalculateBoundingBox(True) + if node.__class__.__qualname__ == "CuraSceneNode" : + result_bbox = result_bbox + node.getBoundingBox() + elif node.__class__.__qualname__ == "BuildVolume": + build_volume_bbox = node.getBoundingBox() + + if not result_bbox.isValid(): + result_bbox = build_volume_bbox + + if result_bbox is not None: + pt_min = pynav.NavlibVector(result_bbox.minimum.x, result_bbox.minimum.y, result_bbox.minimum.z) + pt_max = pynav.NavlibVector(result_bbox.maximum.x, result_bbox.maximum.y, result_bbox.maximum.z) + self._scene_center = result_bbox.center + self._scene_radius = (result_bbox.maximum - self._scene_center).length() + return pynav.NavlibBox(pt_min, pt_max) + + def get_pivot_position(self)->pynav.NavlibVector: + return pynav.NavlibVector() + + def get_hit_look_at(self)->pynav.NavlibVector: + + 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) + elif self._was_pick and self._pointer_pick is None: + return None - if self.get_is_view_perspective(): - self._was_pick = True from cura.Utils.Threading import call_on_qt_thread wrapped_pick = call_on_qt_thread(self.pick) - - self._pointer_pick = wrapped_pick(x_n, y_n) + picked_position = wrapped_pick(0, 0, self._hit_selection_only, 0.5) - return pynav.NavlibVector(0., 0., 0.) - else: - ray = self._scene.getActiveCamera().getRay(x_n, y_n) - pointer_position = ray.origin + ray.direction + if picked_position is not None: + return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) - return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) + def get_units_to_meters(self)->float: + return 0.05 - def get_view_extents(self)->pynav.NavlibBox: + def is_user_pivot(self)->bool: + return False - view_width = self._scene.getActiveCamera().getViewportWidth() - view_height = self._scene.getActiveCamera().getViewportHeight() - horizontal_zoom = view_width * self._scene.getActiveCamera().getZoomFactor() - vertical_zoom = view_height * self._scene.getActiveCamera().getZoomFactor() + def set_camera_matrix(self, matrix : pynav.NavlibMatrix): - pt_min = pynav.NavlibVector(-view_width / 2 - horizontal_zoom, -view_height / 2 - vertical_zoom, -9001) - pt_max = pynav.NavlibVector(view_width / 2 + horizontal_zoom, view_height / 2 + vertical_zoom, 9001) + # !!!!!! + # Hit testing in Orthographic view is not reliable + # Picking starts in camera position, not on near plane + # which results in wrong depth values (visible geometry + # cannot be picked properly) - Workaround needed (camera position offset) + # !!!!!! + if not self.get_is_view_perspective(): + affine = matrix._matrix + direction = Vector(-affine[0][2], -affine[1][2], -affine[2][2]) + distance = self._scene_center - Vector(affine[0][3], affine[1][3], affine[2][3]) - return pynav.NavlibBox(pt_min, pt_max) + cos_value = direction.dot(distance.normalized()) - def get_view_frustum(self)->pynav.NavlibFrustum: + offset = 0. - projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() - half_height = 2. / projection_matrix.getData()[1,1] - half_width = half_height * (projection_matrix.getData()[1,1] / projection_matrix.getData()[0,0]) + if (distance.length() < self._scene_radius) and (cos_value > 0.): + offset = self._scene_radius + elif (distance.length() < self._scene_radius) and (cos_value < 0.): + offset = 2. * self._scene_radius + elif (distance.length() > self._scene_radius) and (cos_value < 0.): + offset = 2. * distance.length() - return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.) - - def get_is_view_perspective(self)->bool: - return self._scene.getActiveCamera().isPerspective() + matrix._matrix[0][3] = matrix._matrix[0][3] - offset * direction.x + matrix._matrix[1][3] = matrix._matrix[1][3] - offset * direction.y + matrix._matrix[2][3] = matrix._matrix[2][3] - offset * direction.z - def get_selection_extents(self)->pynav.NavlibBox: + transformation = Matrix(data = matrix._matrix) + self._scene.getActiveCamera().setTransformation(transformation) - from UM.Scene.Selection import Selection - bounding_box = Selection.getBoundingBox() + active_camera = self._scene.getActiveCamera() + if active_camera.isPerspective(): + camera_position = active_camera.getWorldPosition() + dist = (camera_position - self._pivot_node.getWorldPosition()).length() + scale = dist / 400. + if scale < 1.: + scale = scale * scale + else: + view_width = active_camera.getViewportWidth() + current_size = view_width + (2. * active_camera.getZoomFactor() * view_width) + scale = current_size / view_width * 5. - if(bounding_box is not None) : - pt_min = pynav.NavlibVector(bounding_box.minimum.x, bounding_box.minimum.y, bounding_box.minimum.z) - pt_max = pynav.NavlibVector(bounding_box.maximum.x, bounding_box.maximum.y, bounding_box.maximum.z) - return pynav.NavlibBox(pt_min, pt_max) + self._pivot_node.scale(scale) - def get_selection_transform(self)->pynav.NavlibMatrix: - return pynav.NavlibMatrix() - - def get_is_selection_empty(self)->bool: - from UM.Scene.Selection import Selection - return not Selection.hasSelection() - - def get_pivot_visible(self)->bool: - return False - - def get_camera_matrix(self)->pynav.NavlibMatrix: + def set_view_extents(self, extents: pynav.NavlibBox): + view_width = self._scene.getActiveCamera().getViewportWidth() + new_zoom = (extents._min._x + view_width / 2.) / - view_width + self._scene.getActiveCamera().setZoomFactor(new_zoom) - transformation = self._scene.getActiveCamera().getLocalTransformation() + def set_hit_selection_only(self, onlySelection : bool): + self._hit_selection_only = onlySelection + + def set_motion_flag(self, motion : bool): + if motion: + width = self._scene.getActiveCamera().getViewportWidth() + height = self._scene.getActiveCamera().getViewportHeight() + self._picking_pass = PickingPass(width, height) + self._renderer.addRenderPass(self._picking_pass) + else: + self._was_pick = False + self._renderer.removeRenderPass(self._picking_pass) + + def set_pivot_position(self, position): + 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): + if visible: + self._scene.getRoot().addChild(self._pivot_node) + else: + self._scene.getRoot().removeChild(self._pivot_node) - return pynav.NavlibMatrix([[transformation.at(0, 0), transformation.at(0, 1), transformation.at(0, 2), transformation.at(0, 3)], - [transformation.at(1, 0), transformation.at(1, 1), transformation.at(1, 2), transformation.at(1, 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)]]) +except BaseException as exception: + Logger.warning(f"Unable to load 3DConnexion library: {exception}") - def get_coordinate_system(self)->pynav.NavlibMatrix: - return pynav.NavlibMatrix() - - def get_front_view(self)->pynav.NavlibMatrix: - return pynav.NavlibMatrix() - - def get_model_extents(self)->pynav.NavlibBox: - - result_bbox = AxisAlignedBox() - build_volume_bbox = None + # Define dummy class instead so that integration is transparent from outside + class NavlibClient: + def __init__(self, scene, renderer) -> None: + pass - for node in DepthFirstIterator(self._scene.getRoot()): - node.setCalculateBoundingBox(True) - if node.__class__.__qualname__ == "CuraSceneNode" : - result_bbox = result_bbox + node.getBoundingBox() - elif node.__class__.__qualname__ == "BuildVolume": - build_volume_bbox = node.getBoundingBox() + def put_profile_hint(self, hint) -> None: + pass - if not result_bbox.isValid(): - result_bbox = build_volume_bbox - - if result_bbox is not None: - pt_min = pynav.NavlibVector(result_bbox.minimum.x, result_bbox.minimum.y, result_bbox.minimum.z) - pt_max = pynav.NavlibVector(result_bbox.maximum.x, result_bbox.maximum.y, result_bbox.maximum.z) - self._scene_center = result_bbox.center - self._scene_radius = (result_bbox.maximum - self._scene_center).length() - return pynav.NavlibBox(pt_min, pt_max) - - def get_pivot_position(self)->pynav.NavlibVector: - return pynav.NavlibVector() - - def get_hit_look_at(self)->pynav.NavlibVector: - - 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) - elif self._was_pick and self._pointer_pick is None: - return None - - from cura.Utils.Threading import call_on_qt_thread - wrapped_pick = call_on_qt_thread(self.pick) - picked_position = wrapped_pick(0, 0, self._hit_selection_only, 0.5) - - if picked_position is not None: - return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) - - def get_units_to_meters(self)->float: - return 0.05 - - def is_user_pivot(self)->bool: - return False - - def set_camera_matrix(self, matrix : pynav.NavlibMatrix): - - # !!!!!! - # Hit testing in Orthographic view is not reliable - # Picking starts in camera position, not on near plane - # which results in wrong depth values (visible geometry - # cannot be picked properly) - Workaround needed (camera position offset) - # !!!!!! - if not self.get_is_view_perspective(): - affine = matrix._matrix - direction = Vector(-affine[0][2], -affine[1][2], -affine[2][2]) - distance = self._scene_center - Vector(affine[0][3], affine[1][3], affine[2][3]) - - cos_value = direction.dot(distance.normalized()) - - offset = 0. - - if (distance.length() < self._scene_radius) and (cos_value > 0.): - offset = self._scene_radius - elif (distance.length() < self._scene_radius) and (cos_value < 0.): - offset = 2. * self._scene_radius - elif (distance.length() > self._scene_radius) and (cos_value < 0.): - offset = 2. * distance.length() - - matrix._matrix[0][3] = matrix._matrix[0][3] - offset * direction.x - matrix._matrix[1][3] = matrix._matrix[1][3] - offset * direction.y - matrix._matrix[2][3] = matrix._matrix[2][3] - offset * direction.z - - transformation = Matrix(data = matrix._matrix) - self._scene.getActiveCamera().setTransformation(transformation) - - active_camera = self._scene.getActiveCamera() - if active_camera.isPerspective(): - camera_position = active_camera.getWorldPosition() - dist = (camera_position - self._pivot_node.getWorldPosition()).length() - scale = dist / 400. - if scale < 1.: - scale = scale * scale - else: - view_width = active_camera.getViewportWidth() - current_size = view_width + (2. * active_camera.getZoomFactor() * view_width) - scale = current_size / view_width * 5. - - self._pivot_node.scale(scale) - - def set_view_extents(self, extents: pynav.NavlibBox): - 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): - self._hit_selection_only = onlySelection - - def set_motion_flag(self, motion : bool): - if motion: - width = self._scene.getActiveCamera().getViewportWidth() - height = self._scene.getActiveCamera().getViewportHeight() - self._picking_pass = PickingPass(width, height) - self._renderer.addRenderPass(self._picking_pass) - else: - self._was_pick = False - self._renderer.removeRenderPass(self._picking_pass) - - def set_pivot_position(self, position): - 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): - if visible: - self._scene.getRoot().addChild(self._pivot_node) - else: - self._scene.getRoot().removeChild(self._pivot_node) - + def enable_navigation(self, enabled) -> None: + pass From b2bb7c6c3e7807f5082dd8a4642b361343f53310 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 7 Feb 2025 15:08:25 +0100 Subject: [PATCH 2/4] Allow giving a specific OS when looking for pip package CURA-12404 --- scripts/get_pypi_hashes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/get_pypi_hashes.py b/scripts/get_pypi_hashes.py index aabe9ece0a..cd11312932 100755 --- a/scripts/get_pypi_hashes.py +++ b/scripts/get_pypi_hashes.py @@ -3,7 +3,7 @@ import argparse from pathlib import Path -def get_package_wheel_hashes(package, version): +def get_package_wheel_hashes(package, version, os): url = f"https://pypi.org/pypi/{package}/{version}/json" data = requests.get(url).json() @@ -12,7 +12,8 @@ def get_package_wheel_hashes(package, version): print(f" hashes:") for url in data["urls"]: - print(f" - sha256:{url['digests']['sha256']}") + if os is None or os in url["filename"]: + print(f" - sha256:{url['digests']['sha256']}") if __name__ == "__main__": @@ -20,5 +21,6 @@ if __name__ == "__main__": description="Display the hashes of the wheel files to be inserted in pip_requirements") parser.add_argument("package", type=Path, help="Name of the target package") parser.add_argument("version", type=Path, help="Version of the target package") + parser.add_argument('--os', type=str, help='Specific package OS', choices=['win', 'macosx', 'manylinux', 'musllinux']) args = parser.parse_args() - get_package_wheel_hashes(args.package, args.version) + get_package_wheel_hashes(args.package, args.version, args.os) From b338a370c05d5993415ad7e49d3c7f7de780ffd1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 7 Feb 2025 15:09:00 +0100 Subject: [PATCH 3/4] Install pynavlib only on Windows and Mac CURA-12404 Because it is not available on Linux --- conandata.yml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/conandata.yml b/conandata.yml index dfe9eb2ea2..20a961ef6e 100644 --- a/conandata.yml +++ b/conandata.yml @@ -567,17 +567,6 @@ pip_requirements_core: hashes: - sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 - sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 - pynavlib: - version: "0.9.4" - hashes: - - sha256:567efd0af97f9014326898b209eea94d9f5cc58e9f589ccf8354584568fcb87d - - sha256:fdd5ab5b6e0a2c9bbcebb154ac7303daf845865a1649be04e1bd8e8e5889401f - - sha256:f0d7ce426e816788aa96b419fd7da263eafb99aca46ce3b6e5dbaf2bbf6b614a - - sha256:493c4b3cacc939b021a694d99723106dbd7ee5515ad4dfc1c7fc8219ef20cf3a - - sha256:33962a322033a78db05a8c2cc3d59e057fbea5b04879c3c54e2fe3041d691a12 - - sha256:332831553a70be05fe58c43a08109b42970cfedc6086ffb4306859142a0e9210 - - sha256:d06d94b1dee4ba024b4a121869e572f571673a3b8c15b4055f52236d43c19a02 - - sha256:9173f61ad83172c306b92bbe38f949889c158cd6dfdc924db01f257a437bf2a6 Windows: twisted-iocpsupport: @@ -600,6 +589,22 @@ pip_requirements_core: hashes: - sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 - sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 + pynavlib: + version: "0.9.4" + hashes: + - sha256:fdd5ab5b6e0a2c9bbcebb154ac7303daf845865a1649be04e1bd8e8e5889401f + - sha256:493c4b3cacc939b021a694d99723106dbd7ee5515ad4dfc1c7fc8219ef20cf3a + - sha256:332831553a70be05fe58c43a08109b42970cfedc6086ffb4306859142a0e9210 + - sha256:9173f61ad83172c306b92bbe38f949889c158cd6dfdc924db01f257a437bf2a6 + + Macos: + pynavlib: + version: "0.9.4" + hashes: + - sha256:567efd0af97f9014326898b209eea94d9f5cc58e9f589ccf8354584568fcb87d + - sha256:f0d7ce426e816788aa96b419fd7da263eafb99aca46ce3b6e5dbaf2bbf6b614a + - sha256:33962a322033a78db05a8c2cc3d59e057fbea5b04879c3c54e2fe3041d691a12 + - sha256:d06d94b1dee4ba024b4a121869e572f571673a3b8c15b4055f52236d43c19a02 pip_requirements_dev: any_os: From 9a92451f442bc9ce36a5768dc5f429192b1c3f34 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 7 Feb 2025 16:15:25 +0100 Subject: [PATCH 4/4] Change the dynamic class definition to be more elegant CURA-12404 --- cura/NavlibClient.py | 530 ++++++++++++++++++++++--------------------- 1 file changed, 266 insertions(+), 264 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index 4a81a5d9c2..68a07a5539 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -9,272 +9,274 @@ from UM.Logger import Logger try: import pynavlib.pynavlib_interface as pynav - - class NavlibClient(pynav.NavlibNavigationModel): - - def __init__(self, scene, renderer) -> None: - super().__init__(False, pynav.NavlibOptions.RowMajorOrder) - self._scene = scene - self._renderer = renderer - self._pointer_pick = None - self._was_pick = False - self._hit_selection_only = False - self._picking_pass = None - self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "3dx_pivot.png"), size=3.) - - def pick(self, x, y, check_selection = False, radius = 0.): - - if self._picking_pass is None or radius < 0.: - return None - - step = 0. - if radius == 0.: - grid_resolution = 0 - else: - grid_resolution = 5 - step = (2. * radius) / float(grid_resolution) - - min_depth = 99999. - result_position = None - - for i in range(grid_resolution + 1): - for j in range(grid_resolution + 1): - - coord_x = (x - radius) + i * step - coord_y = (y - radius) + j * step - - picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y) - max_depth = 16777.215 - - if 0. < picked_depth < max_depth: - - valid_hit = True - if check_selection: - selection_pass = self._renderer.getRenderPass("selection") - picked_object_id = selection_pass.getIdAtPosition(coord_x, coord_y) - picked_object = self._scene.findObject(picked_object_id) - - from UM.Scene.Selection import Selection - valid_hit = Selection.isSelected(picked_object) - - if not valid_hit and grid_resolution > 0.: - continue - elif not valid_hit and grid_resolution == 0.: - return None - - if picked_depth < min_depth: - min_depth = picked_depth - result_position = self._picking_pass.getPickedPosition(coord_x, coord_y) - - return result_position - - def get_pointer_position(self)->pynav.NavlibVector: - - from UM.Qt.QtApplication import QtApplication - main_window = QtApplication.getInstance().getMainWindow() - - x_n = 2. * main_window._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. - y_n = 2. * main_window._mouse_y / self._scene.getActiveCamera().getViewportHeight() - 1. - - if self.get_is_view_perspective(): - self._was_pick = True - from cura.Utils.Threading import call_on_qt_thread - wrapped_pick = call_on_qt_thread(self.pick) - - self._pointer_pick = wrapped_pick(x_n, y_n) - - return pynav.NavlibVector(0., 0., 0.) - else: - ray = self._scene.getActiveCamera().getRay(x_n, y_n) - pointer_position = ray.origin + ray.direction - - return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) - - def get_view_extents(self)->pynav.NavlibBox: - - view_width = self._scene.getActiveCamera().getViewportWidth() - view_height = self._scene.getActiveCamera().getViewportHeight() - horizontal_zoom = view_width * self._scene.getActiveCamera().getZoomFactor() - vertical_zoom = view_height * self._scene.getActiveCamera().getZoomFactor() - - pt_min = pynav.NavlibVector(-view_width / 2 - horizontal_zoom, -view_height / 2 - vertical_zoom, -9001) - pt_max = pynav.NavlibVector(view_width / 2 + horizontal_zoom, view_height / 2 + vertical_zoom, 9001) - - return pynav.NavlibBox(pt_min, pt_max) - - def get_view_frustum(self)->pynav.NavlibFrustum: - - projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() - half_height = 2. / projection_matrix.getData()[1,1] - half_width = half_height * (projection_matrix.getData()[1,1] / projection_matrix.getData()[0,0]) - - return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.) - - def get_is_view_perspective(self)->bool: - return self._scene.getActiveCamera().isPerspective() - - def get_selection_extents(self)->pynav.NavlibBox: - - from UM.Scene.Selection import Selection - bounding_box = Selection.getBoundingBox() - - if(bounding_box is not None) : - pt_min = pynav.NavlibVector(bounding_box.minimum.x, bounding_box.minimum.y, bounding_box.minimum.z) - 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: - return pynav.NavlibMatrix() - - def get_is_selection_empty(self)->bool: - from UM.Scene.Selection import Selection - return not Selection.hasSelection() - - def get_pivot_visible(self)->bool: - return False - - def get_camera_matrix(self)->pynav.NavlibMatrix: - - transformation = self._scene.getActiveCamera().getLocalTransformation() - - return pynav.NavlibMatrix([[transformation.at(0, 0), transformation.at(0, 1), transformation.at(0, 2), transformation.at(0, 3)], - [transformation.at(1, 0), transformation.at(1, 1), transformation.at(1, 2), transformation.at(1, 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)]]) - - def get_coordinate_system(self)->pynav.NavlibMatrix: - return pynav.NavlibMatrix() - - def get_front_view(self)->pynav.NavlibMatrix: - return pynav.NavlibMatrix() - - def get_model_extents(self)->pynav.NavlibBox: - - result_bbox = AxisAlignedBox() - build_volume_bbox = None - - for node in DepthFirstIterator(self._scene.getRoot()): - node.setCalculateBoundingBox(True) - if node.__class__.__qualname__ == "CuraSceneNode" : - result_bbox = result_bbox + node.getBoundingBox() - elif node.__class__.__qualname__ == "BuildVolume": - build_volume_bbox = node.getBoundingBox() - - if not result_bbox.isValid(): - result_bbox = build_volume_bbox - - if result_bbox is not None: - pt_min = pynav.NavlibVector(result_bbox.minimum.x, result_bbox.minimum.y, result_bbox.minimum.z) - pt_max = pynav.NavlibVector(result_bbox.maximum.x, result_bbox.maximum.y, result_bbox.maximum.z) - self._scene_center = result_bbox.center - self._scene_radius = (result_bbox.maximum - self._scene_center).length() - return pynav.NavlibBox(pt_min, pt_max) - - def get_pivot_position(self)->pynav.NavlibVector: - return pynav.NavlibVector() - - def get_hit_look_at(self)->pynav.NavlibVector: - - 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) - elif self._was_pick and self._pointer_pick is None: - return None - - from cura.Utils.Threading import call_on_qt_thread - wrapped_pick = call_on_qt_thread(self.pick) - picked_position = wrapped_pick(0, 0, self._hit_selection_only, 0.5) - - if picked_position is not None: - return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) - - def get_units_to_meters(self)->float: - return 0.05 - - def is_user_pivot(self)->bool: - return False - - def set_camera_matrix(self, matrix : pynav.NavlibMatrix): - - # !!!!!! - # Hit testing in Orthographic view is not reliable - # Picking starts in camera position, not on near plane - # which results in wrong depth values (visible geometry - # cannot be picked properly) - Workaround needed (camera position offset) - # !!!!!! - if not self.get_is_view_perspective(): - affine = matrix._matrix - direction = Vector(-affine[0][2], -affine[1][2], -affine[2][2]) - distance = self._scene_center - Vector(affine[0][3], affine[1][3], affine[2][3]) - - cos_value = direction.dot(distance.normalized()) - - offset = 0. - - if (distance.length() < self._scene_radius) and (cos_value > 0.): - offset = self._scene_radius - elif (distance.length() < self._scene_radius) and (cos_value < 0.): - offset = 2. * self._scene_radius - elif (distance.length() > self._scene_radius) and (cos_value < 0.): - offset = 2. * distance.length() - - matrix._matrix[0][3] = matrix._matrix[0][3] - offset * direction.x - matrix._matrix[1][3] = matrix._matrix[1][3] - offset * direction.y - matrix._matrix[2][3] = matrix._matrix[2][3] - offset * direction.z - - transformation = Matrix(data = matrix._matrix) - self._scene.getActiveCamera().setTransformation(transformation) - - active_camera = self._scene.getActiveCamera() - if active_camera.isPerspective(): - camera_position = active_camera.getWorldPosition() - dist = (camera_position - self._pivot_node.getWorldPosition()).length() - scale = dist / 400. - if scale < 1.: - scale = scale * scale - else: - view_width = active_camera.getViewportWidth() - current_size = view_width + (2. * active_camera.getZoomFactor() * view_width) - scale = current_size / view_width * 5. - - self._pivot_node.scale(scale) - - def set_view_extents(self, extents: pynav.NavlibBox): - 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): - self._hit_selection_only = onlySelection - - def set_motion_flag(self, motion : bool): - if motion: - width = self._scene.getActiveCamera().getViewportWidth() - height = self._scene.getActiveCamera().getViewportHeight() - self._picking_pass = PickingPass(width, height) - self._renderer.addRenderPass(self._picking_pass) - else: - self._was_pick = False - self._renderer.removeRenderPass(self._picking_pass) - - def set_pivot_position(self, position): - 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): - if visible: - self._scene.getRoot().addChild(self._pivot_node) - else: - self._scene.getRoot().removeChild(self._pivot_node) - + parent_class = pynav.NavlibNavigationModel except BaseException as exception: Logger.warning(f"Unable to load 3DConnexion library: {exception}") + pynav = None + parent_class = object - # Define dummy class instead so that integration is transparent from outside - class NavlibClient: - def __init__(self, scene, renderer) -> None: - pass +class NavlibClient(parent_class): - def put_profile_hint(self, hint) -> None: - pass + def __init__(self, scene, renderer) -> None: + if not pynav: + return - def enable_navigation(self, enabled) -> None: - pass + super().__init__(False, pynav.NavlibOptions.RowMajorOrder) + self._scene = scene + self._renderer = renderer + self._pointer_pick = None + self._was_pick = False + self._hit_selection_only = False + self._picking_pass = None + self._pivot_node = OverlayNode(node=SceneNode(), image_path=Resources.getPath(Resources.Images, "3dx_pivot.png"), size=3.) + + def put_profile_hint(self, hint) -> None: + if pynav: + super().put_profile_hint(hint) + + def enable_navigation(self, enabled) -> None: + if pynav: + super().enable_navigation(enabled) + + def pick(self, x, y, check_selection = False, radius = 0.): + + if self._picking_pass is None or radius < 0.: + return None + + step = 0. + if radius == 0.: + grid_resolution = 0 + else: + grid_resolution = 5 + step = (2. * radius) / float(grid_resolution) + + min_depth = 99999. + result_position = None + + for i in range(grid_resolution + 1): + for j in range(grid_resolution + 1): + + coord_x = (x - radius) + i * step + coord_y = (y - radius) + j * step + + picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y) + max_depth = 16777.215 + + if 0. < picked_depth < max_depth: + + valid_hit = True + if check_selection: + selection_pass = self._renderer.getRenderPass("selection") + picked_object_id = selection_pass.getIdAtPosition(coord_x, coord_y) + picked_object = self._scene.findObject(picked_object_id) + + from UM.Scene.Selection import Selection + valid_hit = Selection.isSelected(picked_object) + + if not valid_hit and grid_resolution > 0.: + continue + elif not valid_hit and grid_resolution == 0.: + return None + + if picked_depth < min_depth: + min_depth = picked_depth + result_position = self._picking_pass.getPickedPosition(coord_x, coord_y) + + return result_position + + def get_pointer_position(self) -> "pynav.NavlibVector": + + from UM.Qt.QtApplication import QtApplication + main_window = QtApplication.getInstance().getMainWindow() + + x_n = 2. * main_window._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. + y_n = 2. * main_window._mouse_y / self._scene.getActiveCamera().getViewportHeight() - 1. + + if self.get_is_view_perspective(): + self._was_pick = True + from cura.Utils.Threading import call_on_qt_thread + wrapped_pick = call_on_qt_thread(self.pick) + + self._pointer_pick = wrapped_pick(x_n, y_n) + + return pynav.NavlibVector(0., 0., 0.) + else: + ray = self._scene.getActiveCamera().getRay(x_n, y_n) + pointer_position = ray.origin + ray.direction + + return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) + + def get_view_extents(self) -> "pynav.NavlibBox": + + view_width = self._scene.getActiveCamera().getViewportWidth() + view_height = self._scene.getActiveCamera().getViewportHeight() + horizontal_zoom = view_width * self._scene.getActiveCamera().getZoomFactor() + vertical_zoom = view_height * self._scene.getActiveCamera().getZoomFactor() + + pt_min = pynav.NavlibVector(-view_width / 2 - horizontal_zoom, -view_height / 2 - vertical_zoom, -9001) + pt_max = pynav.NavlibVector(view_width / 2 + horizontal_zoom, view_height / 2 + vertical_zoom, 9001) + + return pynav.NavlibBox(pt_min, pt_max) + + def get_view_frustum(self) -> "pynav.NavlibFrustum": + + projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() + half_height = 2. / projection_matrix.getData()[1,1] + half_width = half_height * (projection_matrix.getData()[1,1] / projection_matrix.getData()[0,0]) + + return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, 1., 5000.) + + def get_is_view_perspective(self) -> bool: + return self._scene.getActiveCamera().isPerspective() + + def get_selection_extents(self) -> "pynav.NavlibBox": + + from UM.Scene.Selection import Selection + bounding_box = Selection.getBoundingBox() + + if(bounding_box is not None) : + pt_min = pynav.NavlibVector(bounding_box.minimum.x, bounding_box.minimum.y, bounding_box.minimum.z) + 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": + return pynav.NavlibMatrix() + + def get_is_selection_empty(self) -> bool: + from UM.Scene.Selection import Selection + return not Selection.hasSelection() + + def get_pivot_visible(self) -> bool: + return False + + def get_camera_matrix(self) -> "pynav.NavlibMatrix": + + transformation = self._scene.getActiveCamera().getLocalTransformation() + + return pynav.NavlibMatrix([[transformation.at(0, 0), transformation.at(0, 1), transformation.at(0, 2), transformation.at(0, 3)], + [transformation.at(1, 0), transformation.at(1, 1), transformation.at(1, 2), transformation.at(1, 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)]]) + + def get_coordinate_system(self) -> "pynav.NavlibMatrix": + return pynav.NavlibMatrix() + + def get_front_view(self) -> "pynav.NavlibMatrix": + return pynav.NavlibMatrix() + + def get_model_extents(self) -> "pynav.NavlibBox": + + result_bbox = AxisAlignedBox() + build_volume_bbox = None + + for node in DepthFirstIterator(self._scene.getRoot()): + node.setCalculateBoundingBox(True) + if node.__class__.__qualname__ == "CuraSceneNode" : + result_bbox = result_bbox + node.getBoundingBox() + elif node.__class__.__qualname__ == "BuildVolume": + build_volume_bbox = node.getBoundingBox() + + if not result_bbox.isValid(): + result_bbox = build_volume_bbox + + if result_bbox is not None: + pt_min = pynav.NavlibVector(result_bbox.minimum.x, result_bbox.minimum.y, result_bbox.minimum.z) + pt_max = pynav.NavlibVector(result_bbox.maximum.x, result_bbox.maximum.y, result_bbox.maximum.z) + self._scene_center = result_bbox.center + self._scene_radius = (result_bbox.maximum - self._scene_center).length() + return pynav.NavlibBox(pt_min, pt_max) + + def get_pivot_position(self) -> "pynav.NavlibVector": + return pynav.NavlibVector() + + def get_hit_look_at(self) -> "pynav.NavlibVector": + + 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) + elif self._was_pick and self._pointer_pick is None: + return None + + from cura.Utils.Threading import call_on_qt_thread + wrapped_pick = call_on_qt_thread(self.pick) + picked_position = wrapped_pick(0, 0, self._hit_selection_only, 0.5) + + if picked_position is not None: + return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) + + def get_units_to_meters(self) -> float: + return 0.05 + + def is_user_pivot(self) -> bool: + return False + + def set_camera_matrix(self, matrix : "pynav.NavlibMatrix"): + + # !!!!!! + # Hit testing in Orthographic view is not reliable + # Picking starts in camera position, not on near plane + # which results in wrong depth values (visible geometry + # cannot be picked properly) - Workaround needed (camera position offset) + # !!!!!! + if not self.get_is_view_perspective(): + affine = matrix._matrix + direction = Vector(-affine[0][2], -affine[1][2], -affine[2][2]) + distance = self._scene_center - Vector(affine[0][3], affine[1][3], affine[2][3]) + + cos_value = direction.dot(distance.normalized()) + + offset = 0. + + if (distance.length() < self._scene_radius) and (cos_value > 0.): + offset = self._scene_radius + elif (distance.length() < self._scene_radius) and (cos_value < 0.): + offset = 2. * self._scene_radius + elif (distance.length() > self._scene_radius) and (cos_value < 0.): + offset = 2. * distance.length() + + matrix._matrix[0][3] = matrix._matrix[0][3] - offset * direction.x + matrix._matrix[1][3] = matrix._matrix[1][3] - offset * direction.y + matrix._matrix[2][3] = matrix._matrix[2][3] - offset * direction.z + + transformation = Matrix(data = matrix._matrix) + self._scene.getActiveCamera().setTransformation(transformation) + + active_camera = self._scene.getActiveCamera() + if active_camera.isPerspective(): + camera_position = active_camera.getWorldPosition() + dist = (camera_position - self._pivot_node.getWorldPosition()).length() + scale = dist / 400. + if scale < 1.: + scale = scale * scale + else: + view_width = active_camera.getViewportWidth() + current_size = view_width + (2. * active_camera.getZoomFactor() * view_width) + scale = current_size / view_width * 5. + + self._pivot_node.scale(scale) + + def set_view_extents(self, extents: "pynav.NavlibBox"): + 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): + self._hit_selection_only = onlySelection + + def set_motion_flag(self, motion : bool): + if motion: + width = self._scene.getActiveCamera().getViewportWidth() + height = self._scene.getActiveCamera().getViewportHeight() + self._picking_pass = PickingPass(width, height) + self._renderer.addRenderPass(self._picking_pass) + else: + self._was_pick = False + self._renderer.removeRenderPass(self._picking_pass) + + def set_pivot_position(self, position): + 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): + if visible: + self._scene.getRoot().addChild(self._pivot_node) + else: + self._scene.getRoot().removeChild(self._pivot_node)