diff --git a/cura/PrinterOutput/Models/MaterialOutputModel.py b/cura/PrinterOutput/Models/MaterialOutputModel.py index cb5da7f8be..f279358477 100644 --- a/cura/PrinterOutput/Models/MaterialOutputModel.py +++ b/cura/PrinterOutput/Models/MaterialOutputModel.py @@ -10,8 +10,8 @@ class MaterialOutputModel(QObject): def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None: super().__init__(parent) - name, guid = MaterialOutputModel.getMaterialFromDefinition(guid,type, brand, name) - self._guid =guid + name, guid = MaterialOutputModel.getMaterialFromDefinition(guid, type, brand, name) + self._guid = guid self._type = type self._color = color self._brand = brand @@ -24,23 +24,22 @@ class MaterialOutputModel(QObject): @staticmethod def getMaterialFromDefinition(guid, type, brand, name): - _MATERIAL_MAP = { "abs" :{"name" :"abs_175" ,"guid": "2780b345-577b-4a24-a2c5-12e6aad3e690"}, - "abs-wss1" :{"name" :"absr_175" ,"guid": "88c8919c-6a09-471a-b7b6-e801263d862d"}, - "asa" :{"name" :"asa_175" ,"guid": "416eead4-0d8e-4f0b-8bfc-a91a519befa5"}, - "nylon-cf" :{"name" :"cffpa_175" ,"guid": "85bbae0e-938d-46fb-989f-c9b3689dc4f0"}, - "nylon12-cf": {"name": "nylon12-cf_175", "guid": "3c6f2877-71cc-4760-84e6-4b89ab243e3b"}, - "nylon" :{"name" :"nylon_175" ,"guid": "283d439a-3490-4481-920c-c51d8cdecf9c"}, - "pc" :{"name" :"pc_175" ,"guid": "62414577-94d1-490d-b1e4-7ef3ec40db02"}, - "petg" :{"name" :"petg_175" ,"guid": "69386c85-5b6c-421a-bec5-aeb1fb33f060"}, - "pla" :{"name" :"pla_175" ,"guid": "0ff92885-617b-4144-a03c-9989872454bc"}, - "pva" :{"name" :"pva_175" ,"guid": "a4255da2-cb2a-4042-be49-4a83957a2f9a"}, - "wss1" :{"name" :"rapidrinse_175","guid": "a140ef8f-4f26-4e73-abe0-cfc29d6d1024"}, - "sr30" :{"name" :"sr30_175" ,"guid": "77873465-83a9-4283-bc44-4e542b8eb3eb"}, - "im-pla" :{"name" :"tough_pla_175" ,"guid": "96fca5d9-0371-4516-9e96-8e8182677f3c"}, - "bvoh" :{"name" :"bvoh_175" ,"guid": "923e604c-8432-4b09-96aa-9bbbd42207f4"}, - "cpe" :{"name" :"cpe_175" ,"guid": "da1872c1-b991-4795-80ad-bdac0f131726"}, - "hips" :{"name" :"hips_175" ,"guid": "a468d86a-220c-47eb-99a5-bbb47e514eb0"}, - "tpu" :{"name" :"tpu_175" ,"guid": "19baa6a9-94ff-478b-b4a1-8157b74358d2"} + _MATERIAL_MAP = { "abs" :{"name" :"ABS" ,"guid": "2780b345-577b-4a24-a2c5-12e6aad3e690"}, + "abs-cf10": {"name": "ABS-CF", "guid": "495a0ce5-9daf-4a16-b7b2-06856d82394d"}, + "abs-wss1" :{"name" :"ABS-R" ,"guid": "88c8919c-6a09-471a-b7b6-e801263d862d"}, + "asa" :{"name" :"ASA" ,"guid": "416eead4-0d8e-4f0b-8bfc-a91a519befa5"}, + "nylon12-cf":{"name": "Nylon 12 CF" ,"guid": "3c6f2877-71cc-4760-84e6-4b89ab243e3b"}, + "nylon" :{"name" :"Nylon" ,"guid": "283d439a-3490-4481-920c-c51d8cdecf9c"}, + "pc" :{"name" :"PC" ,"guid": "62414577-94d1-490d-b1e4-7ef3ec40db02"}, + "petg" :{"name" :"PETG" ,"guid": "69386c85-5b6c-421a-bec5-aeb1fb33f060"}, + "pla" :{"name" :"PLA" ,"guid": "0ff92885-617b-4144-a03c-9989872454bc"}, + "pva" :{"name" :"PVA" ,"guid": "a4255da2-cb2a-4042-be49-4a83957a2f9a"}, + "wss1" :{"name" :"RapidRinse" ,"guid": "a140ef8f-4f26-4e73-abe0-cfc29d6d1024"}, + "sr30" :{"name" :"SR-30" ,"guid": "77873465-83a9-4283-bc44-4e542b8eb3eb"}, + "bvoh" :{"name" :"BVOH" ,"guid": "923e604c-8432-4b09-96aa-9bbbd42207f4"}, + "cpe" :{"name" :"CPE" ,"guid": "da1872c1-b991-4795-80ad-bdac0f131726"}, + "hips" :{"name" :"HIPS" ,"guid": "a468d86a-220c-47eb-99a5-bbb47e514eb0"}, + "tpu" :{"name" :"TPU 95A" ,"guid": "19baa6a9-94ff-478b-b4a1-8157b74358d2"} } diff --git a/cura/Snapshot.py b/cura/Snapshot.py index f94b3ff42e..65a51539cc 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -21,23 +21,31 @@ from UM.Scene.SceneNode import SceneNode from UM.Qt.QtRenderer import QtRenderer class Snapshot: + + DEFAULT_WIDTH_HEIGHT = 300 + MAX_RENDER_DISTANCE = 10000 + BOUND_BOX_FACTOR = 1.75 + CAMERA_FOVY = 30 + ATTEMPTS_FOR_SNAPSHOT = 10 + @staticmethod - def getImageBoundaries(image: QImage): - # Look at the resulting image to get a good crop. - # Get the pixels as byte array + def getNonZeroPixels(image: QImage): pixel_array = image.bits().asarray(image.sizeInBytes()) width, height = image.width(), image.height() - # Convert to numpy array, assume it's 32 bit (it should always be) pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4]) # Find indices of non zero pixels - nonzero_pixels = numpy.nonzero(pixels) + return numpy.nonzero(pixels) + + @staticmethod + def getImageBoundaries(image: QImage): + nonzero_pixels = Snapshot.getNonZeroPixels(image) min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1) # type: ignore max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1) # type: ignore return min_x, max_x, min_y, max_y @staticmethod - def isometricSnapshot(width: int = 300, height: int = 300, *, node: Optional[SceneNode] = None) -> Optional[QImage]: + def isometricSnapshot(width: int = DEFAULT_WIDTH_HEIGHT, height: int = DEFAULT_WIDTH_HEIGHT, *, node: Optional[SceneNode] = None) -> Optional[QImage]: """ Create an isometric snapshot of the scene. @@ -92,8 +100,8 @@ class Snapshot: camera_width / 2, -camera_height / 2, camera_height / 2, - -10000, - 10000 + -Snapshot.MAX_RENDER_DISTANCE, + Snapshot.MAX_RENDER_DISTANCE ) camera.setPerspective(False) camera.setProjectionMatrix(ortho_matrix) @@ -112,22 +120,25 @@ class Snapshot: return render_pass.getOutput() + @staticmethod + def isNodeRenderable(node): + return not getattr(node, "_outside_buildarea", False) and node.callDecoration( + "isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration( + "isNonThumbnailVisibleMesh") + @staticmethod def nodeBounds(root_node: SceneNode) -> Optional[AxisAlignedBox]: axis_aligned_box = None for node in DepthFirstIterator(root_node): - if not getattr(node, "_outside_buildarea", False): - if node.callDecoration( - "isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration( - "isNonThumbnailVisibleMesh"): - if axis_aligned_box is None: - axis_aligned_box = node.getBoundingBox() - else: - axis_aligned_box = axis_aligned_box + node.getBoundingBox() + if Snapshot.isNodeRenderable(node): + if axis_aligned_box is None: + axis_aligned_box = node.getBoundingBox() + else: + axis_aligned_box = axis_aligned_box + node.getBoundingBox() return axis_aligned_box @staticmethod - def snapshot(width = 300, height = 300): + def snapshot(width = DEFAULT_WIDTH_HEIGHT, height = DEFAULT_WIDTH_HEIGHT, number_of_attempts = ATTEMPTS_FOR_SNAPSHOT): """Return a QImage of the scene Uses PreviewPass that leaves out some elements Aspect ratio assumes a square @@ -163,13 +174,13 @@ class Snapshot: looking_from_offset = Vector(-1, 1, 2) if size > 0: # determine the watch distance depending on the size - looking_from_offset = looking_from_offset * size * 1.75 + looking_from_offset = looking_from_offset * size * Snapshot.BOUND_BOX_FACTOR camera.setPosition(look_at + looking_from_offset) camera.lookAt(look_at) satisfied = False size = None - fovy = 30 + fovy = Snapshot.CAMERA_FOVY while not satisfied: if size is not None: @@ -184,9 +195,14 @@ class Snapshot: pixel_output = preview_pass.getOutput() try: min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output) - except (ValueError, AttributeError): - Logger.logException("w", "Failed to crop the snapshot!") - return None + except (ValueError, AttributeError) as e: + if number_of_attempts == 0: + Logger.warning( f"Failed to crop the snapshot even after {Snapshot.ATTEMPTS_FOR_SNAPSHOT} attempts!") + return None + else: + number_of_attempts = number_of_attempts - 1 + Logger.info("Trying to get the snapshot again.") + return Snapshot.snapshot(width, height, number_of_attempts) size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height) if size > 0.5 or satisfied: diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 1c14c37cfd..d33f0e374a 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import json import re +import threading from typing import Optional, cast, List, Dict, Pattern, Set @@ -65,6 +66,7 @@ class ThreeMFWriter(MeshWriter): self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix()) self._archive: Optional[zipfile.ZipFile] = None self._store_archive = False + self._lock = threading.Lock() @staticmethod def _convertMatrixToString(matrix): @@ -423,6 +425,7 @@ class ThreeMFWriter(MeshWriter): @call_on_qt_thread # must be called from the main thread because of OpenGL def _createSnapshot(self): Logger.log("d", "Creating thumbnail image...") + self._lock.acquire() if not CuraApplication.getInstance().isVisible: Logger.log("w", "Can't create snapshot when renderer not initialized.") return None @@ -431,6 +434,7 @@ class ThreeMFWriter(MeshWriter): except: Logger.logException("w", "Failed to create snapshot image") return None + finally: self._lock.release() return snapshot