diff --git a/cura/PrinterOutput/Models/MaterialOutputModel.py b/cura/PrinterOutput/Models/MaterialOutputModel.py index dedcc2136b..cb5da7f8be 100644 --- a/cura/PrinterOutput/Models/MaterialOutputModel.py +++ b/cura/PrinterOutput/Models/MaterialOutputModel.py @@ -28,6 +28,7 @@ class MaterialOutputModel(QObject): "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"}, 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 diff --git a/plugins/3MFWriter/UCPDialog.qml b/plugins/3MFWriter/UCPDialog.qml index 5d094f9187..267306f5e2 100644 --- a/plugins/3MFWriter/UCPDialog.qml +++ b/plugins/3MFWriter/UCPDialog.qml @@ -46,7 +46,7 @@ UM.Dialog UM.Label { id: descriptionLabel - text: catalog.i18nc("@action:description", "When exporting a Universal Cura Project, all the models present on the build plate will be included with their current position, orientation and scale. You can also select which per-extruder or per-model settings should be included to ensure a proper printing of the batch, even on different printers.") + text: catalog.i18nc("@action:description", "Universal Cura Project files can be printed on different 3D printers while retaining positional data and selected settings. When exported, all models present on the build plate will be included along with their current position, orientation, and scale. You can also select which per-extruder or per-model settings should be included to ensure proper printing.") font: UM.Theme.getFont("default") wrapMode: Text.Wrap Layout.maximumWidth: headerColumn.width diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 784b223500..4e10dfbee3 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -6815,7 +6815,7 @@ "prime_tower_mode": { "label": "Prime Tower Type", - "description": "How to generate the prime tower: