diff --git a/conandata.yml b/conandata.yml index 05a8183365..243f9d8857 100644 --- a/conandata.yml +++ b/conandata.yml @@ -125,6 +125,7 @@ pyinstaller: - "PyQt6.sip" - "stl" - "keyrings.alt" + - "pynavlib" collect_all_WINDOWS_ONLY: - "PyQt6.Qt" - "PyQt6.Qt6" @@ -567,6 +568,7 @@ pip_requirements_core: hashes: - sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 - sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 + Windows: twisted-iocpsupport: version: "1.0.2" @@ -588,6 +590,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: diff --git a/conanfile.py b/conanfile.py index e7a8ccf1d8..06005723a1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -411,7 +411,8 @@ class CuraConan(ConanFile): pyinstaller_metadata = self.conan_data["pyinstaller"] datas = [] for data in pyinstaller_metadata["datas"].values(): - if (not self.options.internal and data.get("internal", False)) or (not self.options.enterprise and data.get("enterprise_only", False)): + if (not self.options.internal and data.get("internal", False)) or ( + not self.options.enterprise and data.get("enterprise_only", False)): continue if "oses" in data and self.settings.os not in data["oses"]: @@ -572,8 +573,10 @@ class CuraConan(ConanFile): if self.options.enterprise: rmdir(self, str(Path(self.source_folder, "plugins", "NativeCADplugin"))) native_cad_plugin = self.dependencies["native_cad_plugin"].cpp_info - copy(self, "*", native_cad_plugin.resdirs[0], str(Path(self.source_folder, "plugins", "NativeCADplugin")), keep_path = True) - copy(self, "bundled_*.json", native_cad_plugin.resdirs[1], str(Path(self.source_folder, "resources", "bundled_packages")), keep_path = False) + copy(self, "*", native_cad_plugin.resdirs[0], str(Path(self.source_folder, "plugins", "NativeCADplugin")), + keep_path = True) + copy(self, "bundled_*.json", native_cad_plugin.resdirs[1], + str(Path(self.source_folder, "resources", "bundled_packages")), keep_path = False) # Copy resources of cura_binary_data cura_binary_data = self.dependencies["cura_binary_data"].cpp_info @@ -611,7 +614,8 @@ class CuraConan(ConanFile): def build(self): if self.settings.os == "Windows" and not self.conf.get("tools.microsoft.bash:path", check_type=str): - self.output.warning("Skipping generation of binary translation files because Bash could not be found and is required") + self.output.warning( + "Skipping generation of binary translation files because Bash could not be found and is required") return for po_file in Path(self.source_folder, "resources", "i18n").glob("**/*.po"): @@ -624,7 +628,8 @@ class CuraConan(ConanFile): ''' Note: this deploy step is actually used to prepare for building a Cura distribution with pyinstaller, which is not the original purpose in the Conan philosophy ''' - copy(self, "*", os.path.join(self.package_folder, self.cpp.package.resdirs[2]), os.path.join(self.deploy_folder, "packaging"), keep_path = True) + copy(self, "*", os.path.join(self.package_folder, self.cpp.package.resdirs[2]), + os.path.join(self.deploy_folder, "packaging"), keep_path=True) # Copy resources of Cura (keep folder structure) needed by pyinstaller to determine the module structure copy(self, "*", os.path.join(self.package_folder, self.cpp_info.bindirs[0]), str(self._base_dir), keep_path = False) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 4c16061074..986608cd49 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -398,7 +398,8 @@ class MachineManager(QObject): self.setVariantByName(extruder.getMetaDataEntry("position"), machine_node.preferred_variant_name) variant_node = machine_node.variants.get(machine_node.preferred_variant_name) - material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file")) if variant_node else None + material_node = variant_node.materials.get( + extruder.material.getMetaDataEntry("base_file")) if variant_node else None if material_node is None: Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") if not self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material): diff --git a/cura/UI/WelcomePagesModel.py b/cura/UI/WelcomePagesModel.py index 833f34e269..6ef1959b4a 100644 --- a/cura/UI/WelcomePagesModel.py +++ b/cura/UI/WelcomePagesModel.py @@ -217,7 +217,8 @@ class WelcomePagesModel(ListModel): def _getBuiltinWelcomePagePath(page_filename: str) -> QUrl: """Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages".""" from cura.CuraApplication import CuraApplication - return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "WelcomePages", page_filename)) + return QUrl.fromLocalFile( + Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "WelcomePages", page_filename)) # FIXME: HACKs for optimization that we don't update the model every time the active machine gets changed. def _onActiveMachineChanged(self) -> None: diff --git a/packaging/MacOS/build_macos.py b/packaging/MacOS/build_macos.py index a7063f688d..d426aa349a 100644 --- a/packaging/MacOS/build_macos.py +++ b/packaging/MacOS/build_macos.py @@ -154,7 +154,7 @@ if __name__ == "__main__": parser.add_argument("--app_name", required = True, type = str, help = "Filename of the .app that will be contained within the dmg/pkg") args = parser.parse_args() - cura_version = args.cura_conan_version.replace("+","-") # + is not allowed for bundle identifier + cura_version = args.cura_conan_version.replace("+", "-") # + is not allowed for bundle identifier app_name = f"{args.app_name}.app" diff --git a/packaging/NSIS/create_windows_installer.py b/packaging/NSIS/create_windows_installer.py index 375ad25b01..d15d62b951 100644 --- a/packaging/NSIS/create_windows_installer.py +++ b/packaging/NSIS/create_windows_installer.py @@ -78,7 +78,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description = "Create Windows exe installer of Cura.") parser.add_argument("--source_path", type=str, help="Path to Conan install Cura folder.") parser.add_argument("--dist_path", type=str, help="Path to Pyinstaller dist folder") - parser.add_argument("--filename", type = str, help = "Filename of the exe (e.g. 'UltiMaker-Cura-5.1.0-beta-Windows-X64.exe')") + parser.add_argument("--filename", type=str, help="Filename of the exe (e.g. 'UltiMaker-Cura-5.1.0-beta-Windows-X64.exe')") parser.add_argument("--version", type=str, help="The full cura version, e.g. 5.9.0-beta.1+24132") args = parser.parse_args() generate_nsi(args.source_path, args.dist_path, args.filename, args.version) diff --git a/plugins/3DConnexion/NavlibClient.py b/plugins/3DConnexion/NavlibClient.py new file mode 100644 index 0000000000..f9feb7f875 --- /dev/null +++ b/plugins/3DConnexion/NavlibClient.py @@ -0,0 +1,273 @@ +# Copyright (c) 2025 3Dconnexion, UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional +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 +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Scene import Scene +from UM.Resources import Resources +from UM.Tool import Tool +from UM.View.Renderer import Renderer +from .OverlayNode import OverlayNode +import pynavlib.pynavlib_interface as pynav + + +class NavlibClient(pynav.NavlibNavigationModel, Tool): + + def __init__(self, scene: Scene, renderer: Renderer) -> None: + pynav.NavlibNavigationModel.__init__(self, False, pynav.NavlibOptions.RowMajorOrder) + Tool.__init__(self) + self._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, "cor.png"), size=2.5) + self.put_profile_hint("UltiMaker Cura") + self.enable_navigation(True) + + def pick(self, x: float, y: float, check_selection: bool = False, radius: float = 0.) -> Optional[Vector]: + + 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 / main_window.width() - 1. + y_n = 2. * main_window._mouse_y / main_window.height() - 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") -> None: + + # !!!!!! + # 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. + 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") -> None: + view_width = self._scene.getActiveCamera().getViewportWidth() + new_zoom = (extents._min._x + view_width / 2.) / - view_width + self._scene.getActiveCamera().setZoomFactor(new_zoom) + + def set_hit_selection_only(self, onlySelection : bool) -> None: + self._hit_selection_only = onlySelection + + def set_motion_flag(self, motion : bool) -> None: + 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) -> None: + self._pivot_node._target_node.setPosition(position=Vector(position._x, position._y, position._z), transform_space = SceneNode.TransformSpace.World) + + def set_pivot_visible(self, visible) -> None: + if visible: + self._scene.getRoot().addChild(self._pivot_node) + else: + self._scene.getRoot().removeChild(self._pivot_node) diff --git a/plugins/3DConnexion/OverlayNode.py b/plugins/3DConnexion/OverlayNode.py new file mode 100644 index 0000000000..f96d9e14c7 --- /dev/null +++ b/plugins/3DConnexion/OverlayNode.py @@ -0,0 +1,68 @@ +# Copyright (c) 2025 3Dconnexion, UltiMaker +# Cura is released under the terms of the LGPLv3 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, size, parent=None): + 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)), overlay=True) + + return True # This node does it's own rendering. diff --git a/plugins/3DConnexion/__init__.py b/plugins/3DConnexion/__init__.py new file mode 100644 index 0000000000..0a226b0969 --- /dev/null +++ b/plugins/3DConnexion/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Logger import Logger + +from typing import TYPE_CHECKING, Dict, Any + +if TYPE_CHECKING: + from UM.Application import Application + + +def getMetaData() -> Dict[str, Any]: + return { + "tool": { + "visible": False + } + } + + +def register(app: "Application") -> Dict[str, Any]: + try: + from .NavlibClient import NavlibClient + return { "tool": NavlibClient(app.getController().getScene(), app.getRenderer()) } + except BaseException as exception: + Logger.warning(f"Unable to load 3Dconnexion library: {exception}") + return { } diff --git a/plugins/3DConnexion/plugin.json b/plugins/3DConnexion/plugin.json new file mode 100644 index 0000000000..eadb70fc44 --- /dev/null +++ b/plugins/3DConnexion/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "3DConnexion mouses", + "author": "3DConnexion", + "version": "1.0.0", + "description": "Allows working with 3D mouses inside Cura.", + "api": 8, + "i18n-catalog": "cura" +} diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json index c89db0e151..1b32ee3484 100644 --- a/resources/bundled_packages/cura.json +++ b/resources/bundled_packages/cura.json @@ -1,4 +1,21 @@ { + "3DConnexion": { + "package_info": { + "package_id": "3DConnexion", + "package_type": "plugin", + "display_name": "3DConnexion mouses", + "description": "Allows working with 3D mouses inside Cura.\nOnly available on Windows and MacOS.", + "package_version": "1.0.0", + "sdk_version": "8.6.0", + "website": "https://3dconnexion.com", + "author": { + "author_id": "UltimakerPackages", + "display_name": "3DConnexion", + "email": "plugins@ultimaker.com", + "website": "https://3dconnexion.com" + } + } + }, "3MFReader": { "package_info": { "package_id": "3MFReader", diff --git a/resources/images/cor.png b/resources/images/cor.png new file mode 100644 index 0000000000..9648b720cb Binary files /dev/null and b/resources/images/cor.png differ diff --git a/scripts/get_pypi_hashes.py b/scripts/get_pypi_hashes.py index de2314099e..cd11312932 100755 --- a/scripts/get_pypi_hashes.py +++ b/scripts/get_pypi_hashes.py @@ -2,7 +2,8 @@ import requests 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() @@ -11,12 +12,15 @@ 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__": - parser = argparse.ArgumentParser(description="Display the hashes of the wheel files to be inserted in pip_requirements") + parser = argparse.ArgumentParser( + 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) diff --git a/tests/Settings/TestCuraStackBuilder.py b/tests/Settings/TestCuraStackBuilder.py index b049433923..4776d20422 100644 --- a/tests/Settings/TestCuraStackBuilder.py +++ b/tests/Settings/TestCuraStackBuilder.py @@ -51,7 +51,8 @@ def test_createMachineWithUnknownDefinition(application, container_registry): application.getContainerRegistry = MagicMock(return_value=container_registry) with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)): mocked_config_error = MagicMock() - with patch("UM.ConfigurationErrorMessage.ConfigurationErrorMessage.getInstance", MagicMock(return_value=mocked_config_error)): + with patch("UM.ConfigurationErrorMessage.ConfigurationErrorMessage.getInstance", + MagicMock(return_value=mocked_config_error)): assert CuraStackBuilder.createMachine("Whatever", "NOPE") is None mocked_config_error.addFaultyContainers.assert_called_once_with("NOPE")