From c563c0a2caedf90c3fff40ca669fb0a7862aed52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Fri, 27 Sep 2024 16:21:10 +0200 Subject: [PATCH 1/7] Initial version of the integration --- cura/CuraApplication.py | 6 ++ cura/NavlibClient.py | 205 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 cura/NavlibClient.py diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 3bdf7aa126..05a0fdd91e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -130,6 +130,8 @@ from .Machines.Models.IntentSelectionModel import IntentSelectionModel from .PrintOrderManager import PrintOrderManager from .SingleInstance import SingleInstance +from .NavlibClient import NavlibClient + if TYPE_CHECKING: from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer @@ -1037,6 +1039,10 @@ class CuraApplication(QtApplication): controller.setCameraTool("CameraTool") controller.setSelectionTool("SelectionTool") + self._navlib_client = NavlibClient(controller.getScene(), self.getRenderer()) + self._navlib_client.put_profile_hint("UltiMaker Cura") + self._navlib_client.enable_navigation(True) + # Hide the splash screen self.closeSplash() diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py new file mode 100644 index 0000000000..c67a926d47 --- /dev/null +++ b/cura/NavlibClient.py @@ -0,0 +1,205 @@ +import pynavlib.pynavlib_interface as pynav +from UM.Math.Matrix import Matrix +from cura.PickingPass import PickingPass + +class NavlibClient(pynav.NavlibNavigationModel): + + def __init__(self, scene, renderer) -> None: + super().__init__(True, 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 + + def pick(self, x, y): + if self._picking_pass is None: + return None, None + + picked_depth = self._picking_pass.getPickedDepth(x, y) + + max_depth = 16777.215 + + if 0. < picked_depth < max_depth: + selection_pass = self._renderer.getRenderPass("selection") + picked_object_id = selection_pass.getIdAtPosition(x, y) + return self._picking_pass.getPickedPosition(x, y), picked_object_id + + return None, None + + def get_pointer_position(self)->pynav.NavlibVector: + + from UM.Qt.QtApplication import QtApplication + mw = QtApplication.getInstance().getMainWindow() + + x_n = 2. * mw._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. + y_n = 2. * mw._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, id = 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: + projectionMatrix = self._scene.getActiveCamera().getProjectionMatrix() + + pt_min = pynav.NavlibVector(projectionMatrix._left, projectionMatrix._bottom, projectionMatrix._near) + pt_max = pynav.NavlibVector(projectionMatrix._right, projectionMatrix._top, projectionMatrix._far) + + return pynav.NavlibBox(pt_min, pt_max) + + def get_view_frustum(self)->pynav.NavlibFrustum: + + projectionMatrix = self._scene.getActiveCamera().getProjectionMatrix() + halfHeight = 2. / projectionMatrix.getData()[1,1] + halfWidth = halfHeight * (projectionMatrix.getData()[1,1] / projectionMatrix.getData()[0,0]) + + return pynav.NavlibFrustum(-halfWidth, halfWidth, -halfHeight, halfHeight, projectionMatrix._near, 100. * projectionMatrix._far) + + 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) + pass + + 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: + + # Why does running getCalculateBoundingBox on scene + # root always takes into accont all of the objects, no matter + # of their __calculate_aabb settings? + from UM.Scene.SceneNode import SceneNode + + objectsTreeRoot = SceneNode() + objectsTreeRoot.setCalculateBoundingBox(True) + + for node in self._scene.getRoot().getChildren() : + if node.__class__.__qualname__ == "CuraSceneNode" : + objectsTreeRoot.addChild(node) + + for node in objectsTreeRoot.getAllChildren(): + node.setCalculateBoundingBox(True) + + if objectsTreeRoot.getAllChildren().__len__() > 0 : + boundingBox = objectsTreeRoot.getBoundingBox() + else : + self._scene.getRoot().setCalculateBoundingBox(True) + boundingBox = self._scene.getRoot().getBoundingBox() + + if boundingBox is not None: + pt_min = pynav.NavlibVector(boundingBox.minimum.x, boundingBox.minimum.y, boundingBox.minimum.z) + pt_max = pynav.NavlibVector(boundingBox.maximum.x, boundingBox.maximum.y, boundingBox.maximum.z) + 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: + + # !!!!!! + # 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) + # !!!!!! + + 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 + + # Iterate over the grid of given aperture to find + # the depth- wise closest position + + wrapped_pick = call_on_qt_thread(self.pick) + picked_position, picked_object_id = wrapped_pick(0, 0) + + if self._hit_selection_only: + picked_object = self._scene.findObject(picked_object_id) + from UM.Scene.Selection import Selection + if not Selection.isSelected(picked_object): + return None + + # Return the closest point + + if picked_position is not None: + return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) + + pass + + 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): + transformation = Matrix(data = matrix._matrix) + self._scene.getActiveCamera().setTransformation(transformation) + + def set_view_extents(self, extents: pynav.NavlibBox): + self._scene.getActiveCamera().getProjectionMatrix().setOrtho(extents._min._x, extents._max._x, + extents._min._y, extents._max._y, + extents._min._z, extents._max._z) + + def set_hit_selection_only(self, onlySelection : bool): + self._hit_selection_only = onlySelection + pass + + def set_active_command(self, commandId : str): + pass + + 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) + \ No newline at end of file From aa755b0b3262a3325883ed3623fb7be3a402b778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Mon, 7 Oct 2024 16:50:01 +0200 Subject: [PATCH 2/7] Fix for hit testing in ortho mode --- cura/NavlibClient.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index c67a926d47..747bfa467a 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -1,5 +1,5 @@ import pynavlib.pynavlib_interface as pynav -from UM.Math.Matrix import Matrix +from UM.Math.Matrix import Matrix, Vector from cura.PickingPass import PickingPass class NavlibClient(pynav.NavlibNavigationModel): @@ -41,7 +41,7 @@ class NavlibClient(pynav.NavlibNavigationModel): from cura.Utils.Threading import call_on_qt_thread wrapped_pick = call_on_qt_thread(self.pick) - self._pointer_pick, id = wrapped_pick(x_n, y_n) + self._pointer_pick, _ = wrapped_pick(x_n, y_n) return pynav.NavlibVector(0., 0., 0.) else: @@ -131,6 +131,8 @@ class NavlibClient(pynav.NavlibNavigationModel): if boundingBox is not None: pt_min = pynav.NavlibVector(boundingBox.minimum.x, boundingBox.minimum.y, boundingBox.minimum.z) pt_max = pynav.NavlibVector(boundingBox.maximum.x, boundingBox.maximum.y, boundingBox.maximum.z) + self._scene_center = boundingBox.center + self._scene_radius = (boundingBox.maximum - self._scene_center).length() return pynav.NavlibBox(pt_min, pt_max) def get_pivot_position(self)->pynav.NavlibVector: @@ -178,6 +180,27 @@ class NavlibClient(pynav.NavlibNavigationModel): return False def set_camera_matrix(self, matrix : pynav.NavlibMatrix): + + 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) From 49988b53e903ddc0b61f885e53b3828f69af732b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Wed, 9 Oct 2024 14:52:00 +0200 Subject: [PATCH 3/7] Grid-picking implementation --- cura/NavlibClient.py | 126 ++++++++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index 747bfa467a..34096ea0c2 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -13,35 +13,66 @@ class NavlibClient(pynav.NavlibNavigationModel): self._hit_selection_only = False self._picking_pass = None - def pick(self, x, y): - if self._picking_pass is None: - return None, None + def pick(self, x, y, check_selection = False, radius = 0.) : - picked_depth = self._picking_pass.getPickedDepth(x, y) + if self._picking_pass is None or radius < 0. : + return None - max_depth = 16777.215 + step = 0. + if radius == 0. : + grid_resolution = 0 + else: + grid_resolution = 5 + step = (2. * radius) / float(grid_resolution) - if 0. < picked_depth < max_depth: - selection_pass = self._renderer.getRenderPass("selection") - picked_object_id = selection_pass.getIdAtPosition(x, y) - return self._picking_pass.getPickedPosition(x, y), picked_object_id - - return None, None + 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 - mw = QtApplication.getInstance().getMainWindow() + main_window = QtApplication.getInstance().getMainWindow() - x_n = 2. * mw._mouse_x / self._scene.getActiveCamera().getViewportWidth() - 1. - y_n = 2. * mw._mouse_y / self._scene.getActiveCamera().getViewportHeight() - 1. + 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) + self._pointer_pick = wrapped_pick(x_n, y_n) return pynav.NavlibVector(0., 0., 0.) else: @@ -51,20 +82,20 @@ class NavlibClient(pynav.NavlibNavigationModel): return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) def get_view_extents(self)->pynav.NavlibBox: - projectionMatrix = self._scene.getActiveCamera().getProjectionMatrix() + projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() - pt_min = pynav.NavlibVector(projectionMatrix._left, projectionMatrix._bottom, projectionMatrix._near) - pt_max = pynav.NavlibVector(projectionMatrix._right, projectionMatrix._top, projectionMatrix._far) + pt_min = pynav.NavlibVector(projection_matrix._left, projection_matrix._bottom, projection_matrix._near) + pt_max = pynav.NavlibVector(projection_matrix._right, projection_matrix._top, projection_matrix._far) return pynav.NavlibBox(pt_min, pt_max) def get_view_frustum(self)->pynav.NavlibFrustum: - projectionMatrix = self._scene.getActiveCamera().getProjectionMatrix() - halfHeight = 2. / projectionMatrix.getData()[1,1] - halfWidth = halfHeight * (projectionMatrix.getData()[1,1] / projectionMatrix.getData()[0,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]) - return pynav.NavlibFrustum(-halfWidth, halfWidth, -halfHeight, halfHeight, projectionMatrix._near, 100. * projectionMatrix._far) + return pynav.NavlibFrustum(-half_width, half_width, -half_height, half_height, projection_matrix._near, 100. * projection_matrix._far) def get_is_view_perspective(self)->bool: return self._scene.getActiveCamera().isPerspective() @@ -112,27 +143,27 @@ class NavlibClient(pynav.NavlibNavigationModel): # of their __calculate_aabb settings? from UM.Scene.SceneNode import SceneNode - objectsTreeRoot = SceneNode() - objectsTreeRoot.setCalculateBoundingBox(True) + objects_tree_root = SceneNode() + objects_tree_root.setCalculateBoundingBox(True) for node in self._scene.getRoot().getChildren() : if node.__class__.__qualname__ == "CuraSceneNode" : - objectsTreeRoot.addChild(node) + objects_tree_root.addChild(node) - for node in objectsTreeRoot.getAllChildren(): + for node in objects_tree_root.getAllChildren(): node.setCalculateBoundingBox(True) - if objectsTreeRoot.getAllChildren().__len__() > 0 : - boundingBox = objectsTreeRoot.getBoundingBox() + if objects_tree_root.getAllChildren().__len__() > 0 : + bounding_box = objects_tree_root.getBoundingBox() else : self._scene.getRoot().setCalculateBoundingBox(True) - boundingBox = self._scene.getRoot().getBoundingBox() + bounding_box = self._scene.getRoot().getBoundingBox() - if boundingBox is not None: - pt_min = pynav.NavlibVector(boundingBox.minimum.x, boundingBox.minimum.y, boundingBox.minimum.z) - pt_max = pynav.NavlibVector(boundingBox.maximum.x, boundingBox.maximum.y, boundingBox.maximum.z) - self._scene_center = boundingBox.center - self._scene_radius = (boundingBox.maximum - self._scene_center).length() + 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) + self._scene_center = bounding_box.center + self._scene_radius = (bounding_box.maximum - self._scene_center).length() return pynav.NavlibBox(pt_min, pt_max) def get_pivot_position(self)->pynav.NavlibVector: @@ -140,33 +171,14 @@ class NavlibClient(pynav.NavlibNavigationModel): def get_hit_look_at(self)->pynav.NavlibVector: - # !!!!!! - # 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) - # !!!!!! - 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 - - # Iterate over the grid of given aperture to find - # the depth- wise closest position - wrapped_pick = call_on_qt_thread(self.pick) - picked_position, picked_object_id = wrapped_pick(0, 0) - - if self._hit_selection_only: - picked_object = self._scene.findObject(picked_object_id) - from UM.Scene.Selection import Selection - if not Selection.isSelected(picked_object): - return None - - # Return the closest point + 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) @@ -181,6 +193,12 @@ class NavlibClient(pynav.NavlibNavigationModel): 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]) From b9935d4dbb12e2d529ef5e41da38f4903c6da32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Fri, 11 Oct 2024 16:54:13 +0200 Subject: [PATCH 4/7] Reworked retrieval of the model extents --- cura/NavlibClient.py | 72 ++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index 34096ea0c2..e25979a05a 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -1,6 +1,9 @@ import pynavlib.pynavlib_interface as pynav -from UM.Math.Matrix import Matrix, Vector +from UM.Math.Matrix import Matrix +from UM.Math.Vector import Vector +from UM.Math.AxisAlignedBox import AxisAlignedBox from cura.PickingPass import PickingPass +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator class NavlibClient(pynav.NavlibNavigationModel): @@ -13,13 +16,13 @@ class NavlibClient(pynav.NavlibNavigationModel): self._hit_selection_only = False self._picking_pass = None - def pick(self, x, y, check_selection = False, radius = 0.) : + def pick(self, x, y, check_selection = False, radius = 0.): - if self._picking_pass is None or radius < 0. : + if self._picking_pass is None or radius < 0.: return None step = 0. - if radius == 0. : + if radius == 0.: grid_resolution = 0 else: grid_resolution = 5 @@ -28,8 +31,8 @@ class NavlibClient(pynav.NavlibNavigationModel): min_depth = 99999. result_position = None - for i in range(grid_resolution + 1) : - for j in range(grid_resolution + 1) : + 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 @@ -37,7 +40,7 @@ class NavlibClient(pynav.NavlibNavigationModel): picked_depth = self._picking_pass.getPickedDepth(coord_x, coord_y) max_depth = 16777.215 - if 0. < picked_depth < max_depth : + if 0. < picked_depth < max_depth: valid_hit = True if check_selection: @@ -48,12 +51,12 @@ class NavlibClient(pynav.NavlibNavigationModel): from UM.Scene.Selection import Selection valid_hit = Selection.isSelected(picked_object) - if not valid_hit and grid_resolution > 0. : + if not valid_hit and grid_resolution > 0.: continue - elif not valid_hit and grid_resolution == 0. : + elif not valid_hit and grid_resolution == 0.: return None - if picked_depth < min_depth : + if picked_depth < min_depth: min_depth = picked_depth result_position = self._picking_pass.getPickedPosition(coord_x, coord_y) @@ -99,7 +102,7 @@ class NavlibClient(pynav.NavlibNavigationModel): 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 @@ -109,8 +112,7 @@ class NavlibClient(pynav.NavlibNavigationModel): 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) - pass - + def get_selection_transform(self)->pynav.NavlibMatrix: return pynav.NavlibMatrix() @@ -137,33 +139,25 @@ class NavlibClient(pynav.NavlibNavigationModel): return pynav.NavlibMatrix() def get_model_extents(self)->pynav.NavlibBox: + + result_bbox = AxisAlignedBox() + build_volume_bbox = None - # Why does running getCalculateBoundingBox on scene - # root always takes into accont all of the objects, no matter - # of their __calculate_aabb settings? - from UM.Scene.SceneNode import SceneNode - - objects_tree_root = SceneNode() - objects_tree_root.setCalculateBoundingBox(True) - - for node in self._scene.getRoot().getChildren() : - if node.__class__.__qualname__ == "CuraSceneNode" : - objects_tree_root.addChild(node) - - for node in objects_tree_root.getAllChildren(): + 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 objects_tree_root.getAllChildren().__len__() > 0 : - bounding_box = objects_tree_root.getBoundingBox() - else : - self._scene.getRoot().setCalculateBoundingBox(True) - bounding_box = self._scene.getRoot().getBoundingBox() + if not result_bbox.isValid(): + result_bbox = build_volume_bbox - 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) - self._scene_center = bounding_box.center - self._scene_radius = (bounding_box.maximum - self._scene_center).length() + 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: @@ -182,8 +176,6 @@ class NavlibClient(pynav.NavlibNavigationModel): if picked_position is not None: return pynav.NavlibVector(picked_position.x, picked_position.y, picked_position.z) - - pass def get_units_to_meters(self)->float: return 0.05 @@ -229,10 +221,6 @@ class NavlibClient(pynav.NavlibNavigationModel): def set_hit_selection_only(self, onlySelection : bool): self._hit_selection_only = onlySelection - pass - - def set_active_command(self, commandId : str): - pass def set_motion_flag(self, motion : bool): if motion: From 0639ac7b1e44b83f17c7836ce6bf54515baeaf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Wed, 23 Oct 2024 16:29:04 +0200 Subject: [PATCH 5/7] Reworked projection parameters accessing --- cura/NavlibClient.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index e25979a05a..7f3994d631 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -85,10 +85,14 @@ class NavlibClient(pynav.NavlibNavigationModel): return pynav.NavlibVector(pointer_position.x, pointer_position.y, pointer_position.z) def get_view_extents(self)->pynav.NavlibBox: - projection_matrix = self._scene.getActiveCamera().getProjectionMatrix() - - pt_min = pynav.NavlibVector(projection_matrix._left, projection_matrix._bottom, projection_matrix._near) - pt_max = pynav.NavlibVector(projection_matrix._right, projection_matrix._top, projection_matrix._far) + + 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) @@ -98,7 +102,7 @@ class NavlibClient(pynav.NavlibNavigationModel): 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, projection_matrix._near, 100. * projection_matrix._far) + 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() @@ -215,9 +219,9 @@ class NavlibClient(pynav.NavlibNavigationModel): self._scene.getActiveCamera().setTransformation(transformation) def set_view_extents(self, extents: pynav.NavlibBox): - self._scene.getActiveCamera().getProjectionMatrix().setOrtho(extents._min._x, extents._max._x, - extents._min._y, extents._max._y, - extents._min._z, extents._max._z) + 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 From 6be979c670635735379f1e893ff77c84c045a88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Fri, 25 Oct 2024 17:23:10 +0200 Subject: [PATCH 6/7] Implemented CoR visualization --- cura/NavlibClient.py | 27 ++++++++++++- cura/Scene/OverlayNode.py | 68 +++++++++++++++++++++++++++++++++ resources/images/3dx_pivot.png | Bin 0 -> 1365 bytes 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 cura/Scene/OverlayNode.py create mode 100644 resources/images/3dx_pivot.png diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index 7f3994d631..7490377586 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -4,6 +4,7 @@ from UM.Math.Vector import Vector from UM.Math.AxisAlignedBox import AxisAlignedBox from cura.PickingPass import PickingPass from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from cura.Scene.OverlayNode import OverlayNode, SceneNode class NavlibClient(pynav.NavlibNavigationModel): @@ -15,6 +16,7 @@ class NavlibClient(pynav.NavlibNavigationModel): self._was_pick = False self._hit_selection_only = False self._picking_pass = None + self._pivot_node = OverlayNode(node=SceneNode(), image_path="resources/images/3dx_pivot.png", size=3.) def pick(self, x, y, check_selection = False, radius = 0.): @@ -218,6 +220,20 @@ class NavlibClient(pynav.NavlibNavigationModel): 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.0: + 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 @@ -235,4 +251,13 @@ class NavlibClient(pynav.NavlibNavigationModel): else: self._was_pick = False self._renderer.removeRenderPass(self._picking_pass) - \ No newline at end of file + + def set_pivot_position(self, position): + self._pivot_node._target_node.setPosition(position=Vector(position._x, position._y, position._z), transform_space = 3) + + def set_pivot_visible(self, visible): + if visible: + self._scene.getRoot().addChild(self._pivot_node) + else: + self._scene.getRoot().removeChild(self._pivot_node) + diff --git a/cura/Scene/OverlayNode.py b/cura/Scene/OverlayNode.py new file mode 100644 index 0000000000..c3be2d78af --- /dev/null +++ b/cura/Scene/OverlayNode.py @@ -0,0 +1,68 @@ +# Copyright (c) 2016 Ultimaker B.V. +# Cura is released under the terms of the AGPLv3 or higher. + +from UM.Scene.SceneNode import SceneNode +from UM.View.GL.OpenGL import OpenGL +from UM.Mesh.MeshBuilder import MeshBuilder # To create the overlay quad +from UM.Resources import Resources # To find shader locations +from UM.Math.Matrix import Matrix +from UM.Application import Application + +try: + from PyQt6.QtGui import QImage +except: + from PyQt5.QtGui import QImage + +class OverlayNode(SceneNode): + def __init__(self, node, image_path, parent=None, size=1.): + super().__init__(parent) + self._target_node = node + self.setCalculateBoundingBox(False) + + self._overlay_mesh = self._createOverlayQuad(size) + self._drawed_mesh = self._overlay_mesh + self._shader = None + self._scene = Application.getInstance().getController().getScene() + self._scale = 1. + self._image_path = image_path + + def scale(self, factor): + scale_matrix = Matrix() + scale_matrix.setByScaleFactor(factor) + self._drawed_mesh = self._overlay_mesh.getTransformed(scale_matrix) + + def _createOverlayQuad(self, size): + mb = MeshBuilder() + mb.addFaceByPoints(-size / 2, -size / 2, 0, -size / 2, size / 2, 0, size / 2, -size / 2, 0) + mb.addFaceByPoints(size / 2, size / 2, 0, -size / 2, size / 2, 0, size / 2, -size / 2, 0) + + # Set UV coordinates so a texture can be created + mb.setVertexUVCoordinates(0, 0, 1) + mb.setVertexUVCoordinates(1, 0, 0) + mb.setVertexUVCoordinates(4, 0, 0) + mb.setVertexUVCoordinates(2, 1, 1) + mb.setVertexUVCoordinates(5, 1, 1) + mb.setVertexUVCoordinates(3, 1, 0) + + return mb.build() + + def render(self, renderer): + + if not self._shader: + # We now misuse the platform shader, as it actually supports textures + self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "platform.shader")) + # Set the opacity to 0, so that the template is in full control. + self._shader.setUniformValue("u_opacity", 0) + self._texture = OpenGL.getInstance().createTexture() + texture_image = QImage(self._image_path) + self._texture.setImage(texture_image) + self._shader.setTexture(0, self._texture) + + node_position = self._target_node.getWorldPosition() + position_matrix = Matrix() + position_matrix.setByTranslation(node_position) + camera_orientation = self._scene.getActiveCamera().getOrientation().toMatrix() + + renderer.queueNode(self._scene.getRoot(), shader=self._shader, mesh=self._drawed_mesh.getTransformed(position_matrix.multiply(camera_orientation)), type=3) + + return True # This node does it's own rendering. diff --git a/resources/images/3dx_pivot.png b/resources/images/3dx_pivot.png new file mode 100644 index 0000000000000000000000000000000000000000..98525adc860781c9f85ec9b426c95367135aa3f7 GIT binary patch literal 1365 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFe_z-M3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWfo`&anSp|tp`M|! ziMhGCj)IYap@F`Ek-njkuA#Y=v5}R5fdUjL0c|TvNwW%aaf8|g1^l#~=$>Fbx5 zm+O@q>*W`v>l<2HT7t|lGSUUA&@HaaD@m--%_~-hnc$LIoLrPyP?DLSrvNfBF)6>a z#8wIDQivCF3*g4)6+?pw7-0Gpi3R$GdIlgb!4&%X;#ZoR3s+rS5|oN?FIIz#Ln;eW z^@CE2^Gl18ff1Lc46>@g%DE^tu_V7JBtJg~7K#BG`6cH#s~$ri|gC;ZerU}`S{ChKf|!F&b= z#*>~djv*DdN+vmaOfHl-R{#He?)|9w-r0@0a%`@Oho?ABUs1}@w9B)d>9l7%-^Sp$ zBhhoGEbuw3U=Wc~+8MiRnju^11P&d=h)E}(1?^|G+V8)A?Z4Xl;XJWl4^&sqdtO^{ z|NDP;FVAGdKZ^hK?`b}i`sZ+G24}G0{J^cdI9s{i+%?x(zjgV|wz<6iYjrjoTQW8J z|M=^k@hNuU&ZpLOy2|=L+y3QM; ztnuk=e7R=Fvu8|gc1d3T^ziAU o_r-snpI_K~?!IislR8cVhFh!NN1pO!;{X+Op00i_>zopr01c@A@c;k- literal 0 HcmV?d00001 From 036a69c9cdeff6fc1b2ce2b21b58b60530f84e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Skowro=C5=84ski?= Date: Tue, 29 Oct 2024 11:35:34 +0100 Subject: [PATCH 7/7] Clean-up & requirements update --- cura/NavlibClient.py | 13 +++++++------ cura/Scene/OverlayNode.py | 4 ++-- requirements.txt | 2 ++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cura/NavlibClient.py b/cura/NavlibClient.py index 7490377586..e5634879cd 100644 --- a/cura/NavlibClient.py +++ b/cura/NavlibClient.py @@ -5,6 +5,7 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox from cura.PickingPass import PickingPass from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from cura.Scene.OverlayNode import OverlayNode, SceneNode +from UM.Resources import Resources class NavlibClient(pynav.NavlibNavigationModel): @@ -16,7 +17,7 @@ class NavlibClient(pynav.NavlibNavigationModel): self._was_pick = False self._hit_selection_only = False self._picking_pass = None - self._pivot_node = OverlayNode(node=SceneNode(), image_path="resources/images/3dx_pivot.png", size=3.) + 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.): @@ -224,13 +225,13 @@ class NavlibClient(pynav.NavlibNavigationModel): if active_camera.isPerspective(): camera_position = active_camera.getWorldPosition() dist = (camera_position - self._pivot_node.getWorldPosition()).length() - scale = dist/400 - if scale < 1.0: + 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 + current_size = view_width + (2. * active_camera.getZoomFactor() * view_width) + scale = current_size / view_width * 5. self._pivot_node.scale(scale) @@ -253,7 +254,7 @@ class NavlibClient(pynav.NavlibNavigationModel): 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 = 3) + 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: diff --git a/cura/Scene/OverlayNode.py b/cura/Scene/OverlayNode.py index c3be2d78af..c3fb25f149 100644 --- a/cura/Scene/OverlayNode.py +++ b/cura/Scene/OverlayNode.py @@ -14,7 +14,7 @@ except: from PyQt5.QtGui import QImage class OverlayNode(SceneNode): - def __init__(self, node, image_path, parent=None, size=1.): + def __init__(self, node, image_path, size, parent=None): super().__init__(parent) self._target_node = node self.setCalculateBoundingBox(False) @@ -63,6 +63,6 @@ class OverlayNode(SceneNode): position_matrix.setByTranslation(node_position) camera_orientation = self._scene.getActiveCamera().getOrientation().toMatrix() - renderer.queueNode(self._scene.getRoot(), shader=self._shader, mesh=self._drawed_mesh.getTransformed(position_matrix.multiply(camera_orientation)), type=3) + renderer.queueNode(self._scene.getRoot(), shader=self._shader, mesh=self._drawed_mesh.getTransformed(position_matrix.multiply(camera_orientation)), overlay=True) return True # This node does it's own rendering. diff --git a/requirements.txt b/requirements.txt index 344a0156d8..0728d17776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -406,3 +406,5 @@ pywin32-ctypes==0.2.0; \ charset-normalizer==2.1.0; \ --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 + +pynavlib==0.9.1; \ No newline at end of file