diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index f51174aec0..7700ee2e71 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -85,7 +85,7 @@ class CrashHandler: dialog = QDialog() dialog.setMinimumWidth(500) dialog.setMinimumHeight(170) - dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't startup")) + dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't start")) dialog.finished.connect(self._closeEarlyCrashDialog) layout = QVBoxLayout(dialog) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 98c682a8a3..56f1528b9b 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -498,8 +498,13 @@ class CuraApplication(QtApplication): def getStaticVersion(cls): return CuraVersion + ## Handle removing the unneeded plugins + # \sa PluginRegistry + def _removePlugins(self): + self._plugin_registry.removePlugins() + ## Handle loading of all plugin types (and the backend explicitly) - # \sa PluginRegistery + # \sa PluginRegistry def _loadPlugins(self): self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_writer", self._addProfileWriter) @@ -1280,8 +1285,11 @@ class CuraApplication(QtApplication): def reloadAll(self): Logger.log("i", "Reloading all loaded mesh data.") nodes = [] + has_merged_nodes = False for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if not isinstance(node, CuraSceneNode) or not node.getMeshData(): + if not isinstance(node, CuraSceneNode) or not node.getMeshData() : + if node.getName() == "MergedMesh": + has_merged_nodes = True continue nodes.append(node) @@ -1295,10 +1303,14 @@ class CuraApplication(QtApplication): job = ReadMeshJob(file_name) job._node = node job.finished.connect(self._reloadMeshFinished) + if has_merged_nodes: + job.finished.connect(self.updateOriginOfMergedMeshes) + job.start() else: Logger.log("w", "Unable to reload data because we don't have a filename.") + ## Get logging data of the backend engine # \returns \type{string} Logging data @pyqtSlot(result = str) @@ -1368,6 +1380,58 @@ class CuraApplication(QtApplication): # Use the previously found center of the group bounding box as the new location of the group group_node.setPosition(group_node.getBoundingBox().center) + group_node.setName("MergedMesh") # add a specific name to distinguish this node + + + ## Updates origin position of all merged meshes + # \param jobNode \type{Job} empty object which passed which is required by JobQueue + def updateOriginOfMergedMeshes(self, jobNode): + group_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": + + #checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator + for decorator in node.getDecorators(): + if isinstance(decorator, GroupDecorator): + group_nodes.append(node) + break + + for group_node in group_nodes: + meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()] + + # Compute the center of the objects + object_centers = [] + # Forget about the translation that the original objects have + zero_translation = Matrix(data=numpy.zeros(3)) + for mesh, node in zip(meshes, group_node.getChildren()): + transformation = node.getLocalTransformation() + transformation.setTranslation(zero_translation) + transformed_mesh = mesh.getTransformed(transformation) + center = transformed_mesh.getCenterPosition() + if center is not None: + object_centers.append(center) + + if object_centers and len(object_centers) > 0: + middle_x = sum([v.x for v in object_centers]) / len(object_centers) + middle_y = sum([v.y for v in object_centers]) / len(object_centers) + middle_z = sum([v.z for v in object_centers]) / len(object_centers) + offset = Vector(middle_x, middle_y, middle_z) + else: + offset = Vector(0, 0, 0) + + # Move each node to the same position. + for mesh, node in zip(meshes, group_node.getChildren()): + transformation = node.getLocalTransformation() + transformation.setTranslation(zero_translation) + transformed_mesh = mesh.getTransformed(transformation) + + # Align the object around its zero position + # and also apply the offset to center it inside the group. + node.setPosition(-transformed_mesh.getZeroPosition() - offset) + + # Use the previously found center of the group bounding box as the new location of the group + group_node.setPosition(group_node.getBoundingBox().center) + @pyqtSlot() def groupSelected(self): diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index fd6c4680e8..66ee43209f 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -69,7 +69,7 @@ class FirmwareUpdateCheckerJob(Job): # If we do this in a cool way, the download url should be available in the JSON file if self._set_download_url_callback: - self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware") + self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update") message.actionTriggered.connect(self._callback) message.show() diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py new file mode 100644 index 0000000000..8a501ceb27 --- /dev/null +++ b/plugins/ModelChecker/ModelChecker.py @@ -0,0 +1,116 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os + +from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty + +from UM.Application import Application +from UM.Extension import Extension +from UM.Logger import Logger +from UM.Message import Message +from UM.i18n import i18nCatalog +from UM.PluginRegistry import PluginRegistry +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator + +catalog = i18nCatalog("cura") + + +class ModelChecker(QObject, Extension): + ## Signal that gets emitted when anything changed that we need to check. + onChanged = pyqtSignal() + + def __init__(self): + super().__init__() + + self._button_view = None + + self._caution_message = Message("", #Message text gets set when the message gets shown, to display the models in question. + lifetime = 0, + title = catalog.i18nc("@info:title", "Model Checker Warning")) + + Application.getInstance().initializationFinished.connect(self._pluginsInitialized) + Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged) + + ## Pass-through to allow UM.Signal to connect with a pyqtSignal. + def _onChanged(self, _): + self.onChanged.emit() + + ## Called when plug-ins are initialized. + # + # This makes sure that we listen to changes of the material and that the + # button is created that indicates warnings with the current set-up. + def _pluginsInitialized(self): + Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged) + self._createView() + + def checkObjectsForShrinkage(self): + shrinkage_threshold = 0.5 #From what shrinkage percentage a warning will be issued about the model size. + warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials. + warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials. + + material_shrinkage = self._getMaterialShrinkage() + + warning_nodes = [] + + # Check node material shrinkage and bounding box size + for node in self.sliceableNodes(): + node_extruder_position = node.callDecoration("getActiveExtruderPosition") + if material_shrinkage[node_extruder_position] > shrinkage_threshold: + bbox = node.getBoundingBox() + if bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z: + warning_nodes.append(node) + + self._caution_message.setText(catalog.i18nc( + "@info:status", + "Some models may not be printed optimal due to object size and chosen material for models: {model_names}.\n" + "Tips that may be useful to improve the print quality:\n" + "1) Use rounded corners\n" + "2) Turn the fan off (only if the are no tiny details on the model)\n" + "3) Use a different material").format(model_names = ", ".join([n.getName() for n in warning_nodes]))) + + return len(warning_nodes) > 0 + + def sliceableNodes(self): + # Add all sliceable scene nodes to check + scene = Application.getInstance().getController().getScene() + for node in DepthFirstIterator(scene.getRoot()): + if node.callDecoration("isSliceable"): + yield node + + ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. + def _createView(self): + Logger.log("d", "Creating model checker view.") + + # Create the plugin dialog component + path = os.path.join(PluginRegistry.getInstance().getPluginPath("ModelChecker"), "ModelChecker.qml") + self._button_view = Application.getInstance().createQmlComponent(path, {"manager": self}) + + # The qml is only the button + Application.getInstance().addAdditionalComponent("jobSpecsButton", self._button_view) + + Logger.log("d", "Model checker view created.") + + @pyqtProperty(bool, notify = onChanged) + def runChecks(self): + danger_shrinkage = self.checkObjectsForShrinkage() + + return any((danger_shrinkage, )) #If any of the checks fail, show the warning button. + + @pyqtSlot() + def showWarnings(self): + self._caution_message.show() + + def _getMaterialShrinkage(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack is None: + return {} + + material_shrinkage = {} + # Get all shrinkage values of materials used + for extruder_position, extruder in global_container_stack.extruders.items(): + shrinkage = extruder.material.getProperty("material_shrinkage_percentage", "value") + if shrinkage is None: + shrinkage = 0 + material_shrinkage[extruder_position] = shrinkage + return material_shrinkage diff --git a/plugins/ModelChecker/ModelChecker.qml b/plugins/ModelChecker/ModelChecker.qml new file mode 100644 index 0000000000..3db54d4387 --- /dev/null +++ b/plugins/ModelChecker/ModelChecker.qml @@ -0,0 +1,43 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.0 as Cura + + +Button +{ + id: modelCheckerButton + + UM.I18nCatalog{id: catalog; name:"cura"} + + visible: manager.runChecks + tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.") + onClicked: manager.showWarnings() + + width: UM.Theme.getSize("save_button_specs_icons").width + height: UM.Theme.getSize("save_button_specs_icons").height + + style: ButtonStyle + { + background: Item + { + UM.RecolorImage + { + width: UM.Theme.getSize("save_button_specs_icons").width; + height: UM.Theme.getSize("save_button_specs_icons").height; + sourceSize.width: width; + sourceSize.height: width; + color: control.hovered ? UM.Theme.getColor("text_scene_hover") : UM.Theme.getColor("text_scene"); + source: "model_checker.svg" + } + } + } +} diff --git a/plugins/ModelChecker/__init__.py b/plugins/ModelChecker/__init__.py new file mode 100644 index 0000000000..5f4d443729 --- /dev/null +++ b/plugins/ModelChecker/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2018 Ultimaker B.V. +# This example is released under the terms of the AGPLv3 or higher. + +from . import ModelChecker + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + + +def getMetaData(): + return {} + +def register(app): + return { "extension": ModelChecker.ModelChecker() } diff --git a/plugins/ModelChecker/model_checker.svg b/plugins/ModelChecker/model_checker.svg new file mode 100644 index 0000000000..ce9594302e --- /dev/null +++ b/plugins/ModelChecker/model_checker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/ModelChecker/plugin.json b/plugins/ModelChecker/plugin.json new file mode 100644 index 0000000000..a9190adcaa --- /dev/null +++ b/plugins/ModelChecker/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Model Checker", + "author": "Ultimaker B.V.", + "version": "0.1", + "api": 4, + "description": "Checks models and print configuration for possible printing issues and give suggestions.", + "i18n-catalog": "cura" +} diff --git a/plugins/PostProcessingPlugin/scripts/FilamentChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py index 2bb7891634..07e887b082 100644 --- a/plugins/PostProcessingPlugin/scripts/FilamentChange.py +++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py @@ -27,18 +27,18 @@ class FilamentChange(Script): "initial_retract": { "label": "Initial Retraction", - "description": "Initial filament retraction distance", + "description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.", "unit": "mm", "type": "float", - "default_value": 300.0 + "default_value": 30.0 }, "later_retract": { "label": "Later Retraction Distance", - "description": "Later filament retraction distance for removal", + "description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.", "unit": "mm", "type": "float", - "default_value": 30.0 + "default_value": 300.0 } } }""" diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 456d64e250..3697e38661 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -74,7 +74,7 @@ class SimulationView(View): self._global_container_stack = None self._proxy = SimulationViewProxy() - self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) + self._controller.getScene().sceneChanged.connect(self._onSceneChanged) self._resetSettings() self._legend_items = None diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py index 620f367e25..e39266884d 100644 --- a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py +++ b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py @@ -127,6 +127,9 @@ class VersionUpgrade32to33(VersionUpgrade): parser["metadata"]["position"] = str(extruder_position) del parser["metadata"]["extruder"] + quality_type = parser["metadata"]["quality_type"] + parser["metadata"]["quality_type"] = quality_type.lower() + #Update version number. parser["general"]["version"] = "3" diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 5ff6838373..341f2bd3bb 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -983,7 +983,8 @@ class XmlMaterialProfile(InstanceContainer): "retraction amount": "retraction_amount", "retraction speed": "retraction_speed", "adhesion tendency": "material_adhesion_tendency", - "surface energy": "material_surface_energy" + "surface energy": "material_surface_energy", + "shrinkage percentage": "material_shrinkage_percentage", } __unmapped_settings = [ "hardware compatible", diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 21ee543333..d7d9698439 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2097,6 +2097,19 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "material_shrinkage_percentage": + { + "label": "Shrinkage Ratio", + "description": "Shrinkage ratio in percentage.", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": "0", + "maximum_value": "100", + "enabled": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, "material_flow": { "label": "Flow", diff --git a/resources/qml/JobSpecs.qml b/resources/qml/JobSpecs.qml index 742e8d6765..3238c66a1e 100644 --- a/resources/qml/JobSpecs.qml +++ b/resources/qml/JobSpecs.qml @@ -115,15 +115,50 @@ Item { } } + Row { + id: additionalComponentsRow + anchors.top: jobNameRow.bottom + anchors.right: parent.right + } + Label { id: boundingSpec anchors.top: jobNameRow.bottom - anchors.right: parent.right + anchors.right: additionalComponentsRow.left + anchors.rightMargin: + { + if (additionalComponentsRow.width > 0) + { + return UM.Theme.getSize("default_margin").width + } + else + { + return 0; + } + } height: UM.Theme.getSize("jobspecs_line").height verticalAlignment: Text.AlignVCenter font: UM.Theme.getFont("small") color: UM.Theme.getColor("text_scene") text: CuraApplication.getSceneBoundingBoxString } + + Component.onCompleted: { + base.addAdditionalComponents("jobSpecsButton") + } + + Connections { + target: CuraApplication + onAdditionalComponentsChanged: base.addAdditionalComponents("jobSpecsButton") + } + + function addAdditionalComponents (areaId) { + if(areaId == "jobSpecsButton") { + for (var component in CuraApplication.additionalComponents["jobSpecsButton"]) { + CuraApplication.additionalComponents["jobSpecsButton"][component].parent = additionalComponentsRow + } + } + } + } diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml index 8fe9dacf9a..6654708482 100644 --- a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -98,4 +98,10 @@ Button target: Cura.MachineManager onCurrentConfigurationChanged: updateOnSync() } + + Connections + { + target: Cura.MachineManager + onOutputDevicesChanged: updateOnSync() + } } \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 4f4b2306a8..0fde7f3bc9 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -411,6 +411,8 @@ "save_button_save_to_button": [0.3, 2.7], "save_button_specs_icons": [1.4, 1.4], + "job_specs_button": [2.7, 2.7], + "monitor_preheat_temperature_control": [4.5, 2.0], "modal_window_minimum": [60.0, 45],