123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- # Copyright (c) 2018 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- from typing import Dict
- import math
- import os.path
- import unicodedata
- import json
- import re # To create abbreviations for printer names.
- from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
- from UM.Application import Application
- from UM.Logger import Logger
- from UM.Qt.Duration import Duration
- from UM.Preferences import Preferences
- from UM.Scene.SceneNode import SceneNode
- from UM.i18n import i18nCatalog
- from UM.MimeTypeDatabase import MimeTypeDatabase
- catalog = i18nCatalog("cura")
- ## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
- #
- # This class contains all the logic relating to calculation and slicing for the
- # time/quality slider concept. It is a rather tricky combination of event handling
- # and state management. The logic behind this is as follows:
- #
- # - A scene change or setting change event happens.
- # We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
- # - This triggers a new slice with the current settings - this is the "current settings pass".
- # - When the slice is done, we update the current print time and material amount.
- # - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
- # - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
- # - When the Extra Fine pass is done, we update the maximum print time.
- #
- # This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
- # This job name is requested by the JobSpecs qml file.
- class PrintInformation(QObject):
- class SlicePass:
- CurrentSettings = 1
- LowQualitySettings = 2
- HighQualitySettings = 3
- class SliceReason:
- SceneChanged = 1
- SettingChanged = 2
- ActiveMachineChanged = 3
- Other = 4
- def __init__(self, parent = None):
- super().__init__(parent)
- self.initializeCuraMessagePrintTimeProperties()
- self._material_lengths = {} # indexed by build plate number
- self._material_weights = {}
- self._material_costs = {}
- self._material_names = {}
- self._pre_sliced = False
- self._backend = Application.getInstance().getBackend()
- if self._backend:
- self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
- Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
- self._is_user_specified_job_name = False
- self._base_name = ""
- self._abbr_machine = ""
- self._job_name = ""
- self._project_name = ""
- self._active_build_plate = 0
- self._initVariablesWithBuildPlate(self._active_build_plate)
- self._application = Application.getInstance()
- self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
- self._application.globalContainerStackChanged.connect(self._updateJobName)
- self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
- self._application.fileLoaded.connect(self.setBaseName)
- self._application.workspaceLoaded.connect(self.setProjectName)
- self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
- Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
- self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
- self._onActiveMaterialsChanged()
- self._material_amounts = []
- # Crate cura message translations and using translation keys initialize empty time Duration object for total time
- # and time for each feature
- def initializeCuraMessagePrintTimeProperties(self):
- self._current_print_time = {} # Duration(None, self)
- self._print_time_message_translations = {
- "inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
- "inset_x": catalog.i18nc("@tooltip", "Inner Walls"),
- "skin": catalog.i18nc("@tooltip", "Skin"),
- "infill": catalog.i18nc("@tooltip", "Infill"),
- "support_infill": catalog.i18nc("@tooltip", "Support Infill"),
- "support_interface": catalog.i18nc("@tooltip", "Support Interface"),
- "support": catalog.i18nc("@tooltip", "Support"),
- "skirt": catalog.i18nc("@tooltip", "Skirt"),
- "travel": catalog.i18nc("@tooltip", "Travel"),
- "retract": catalog.i18nc("@tooltip", "Retractions"),
- "none": catalog.i18nc("@tooltip", "Other")
- }
- self._print_time_message_values = {}
- def _initPrintTimeMessageValues(self, build_plate_number):
- # Full fill message values using keys from _print_time_message_translations
- self._print_time_message_values[build_plate_number] = {}
- for key in self._print_time_message_translations.keys():
- self._print_time_message_values[build_plate_number][key] = Duration(None, self)
- def _initVariablesWithBuildPlate(self, build_plate_number):
- if build_plate_number not in self._print_time_message_values:
- self._initPrintTimeMessageValues(build_plate_number)
- if self._active_build_plate not in self._material_lengths:
- self._material_lengths[self._active_build_plate] = []
- if self._active_build_plate not in self._material_weights:
- self._material_weights[self._active_build_plate] = []
- if self._active_build_plate not in self._material_costs:
- self._material_costs[self._active_build_plate] = []
- if self._active_build_plate not in self._material_names:
- self._material_names[self._active_build_plate] = []
- if self._active_build_plate not in self._current_print_time:
- self._current_print_time[self._active_build_plate] = Duration(None, self)
- currentPrintTimeChanged = pyqtSignal()
- preSlicedChanged = pyqtSignal()
- @pyqtProperty(bool, notify=preSlicedChanged)
- def preSliced(self):
- return self._pre_sliced
- def setPreSliced(self, pre_sliced):
- self._pre_sliced = pre_sliced
- self._updateJobName()
- self.preSlicedChanged.emit()
- @pyqtProperty(Duration, notify = currentPrintTimeChanged)
- def currentPrintTime(self):
- return self._current_print_time[self._active_build_plate]
- materialLengthsChanged = pyqtSignal()
- @pyqtProperty("QVariantList", notify = materialLengthsChanged)
- def materialLengths(self):
- return self._material_lengths[self._active_build_plate]
- materialWeightsChanged = pyqtSignal()
- @pyqtProperty("QVariantList", notify = materialWeightsChanged)
- def materialWeights(self):
- return self._material_weights[self._active_build_plate]
- materialCostsChanged = pyqtSignal()
- @pyqtProperty("QVariantList", notify = materialCostsChanged)
- def materialCosts(self):
- return self._material_costs[self._active_build_plate]
- materialNamesChanged = pyqtSignal()
- @pyqtProperty("QVariantList", notify = materialNamesChanged)
- def materialNames(self):
- return self._material_names[self._active_build_plate]
- def printTimes(self):
- return self._print_time_message_values[self._active_build_plate]
- def _onPrintDurationMessage(self, build_plate_number, print_time: Dict[str, int], material_amounts: list):
- self._updateTotalPrintTimePerFeature(build_plate_number, print_time)
- self.currentPrintTimeChanged.emit()
- self._material_amounts = material_amounts
- self._calculateInformation(build_plate_number)
- def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
- total_estimated_time = 0
- if build_plate_number not in self._print_time_message_values:
- self._initPrintTimeMessageValues(build_plate_number)
- for feature, time in print_time.items():
- if time != time: # Check for NaN. Engine can sometimes give us weird values.
- self._print_time_message_values[build_plate_number].get(feature).setDuration(0)
- Logger.log("w", "Received NaN for print duration message")
- continue
- total_estimated_time += time
- self._print_time_message_values[build_plate_number].get(feature).setDuration(time)
- if build_plate_number not in self._current_print_time:
- self._current_print_time[build_plate_number] = Duration(None, self)
- self._current_print_time[build_plate_number].setDuration(total_estimated_time)
- def _calculateInformation(self, build_plate_number):
- global_stack = Application.getInstance().getGlobalContainerStack()
- if global_stack is None:
- return
- self._material_lengths[build_plate_number] = []
- self._material_weights[build_plate_number] = []
- self._material_costs[build_plate_number] = []
- self._material_names[build_plate_number] = []
- material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
- extruder_stacks = global_stack.extruders
- for position, extruder_stack in extruder_stacks.items():
- index = int(position)
- if index >= len(self._material_amounts):
- continue
- amount = self._material_amounts[index]
- ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
- # list comprehension filtering to solve this for us.
- density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
- material = extruder_stack.findContainer({"type": "material"})
- radius = extruder_stack.getProperty("material_diameter", "value") / 2
- weight = float(amount) * float(density) / 1000
- cost = 0
- material_name = catalog.i18nc("@label unknown material", "Unknown")
- if material:
- material_guid = material.getMetaDataEntry("GUID")
- material_name = material.getName()
- if material_guid in material_preference_values:
- material_values = material_preference_values[material_guid]
- weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
- cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
- if weight_per_spool != 0:
- cost = cost_per_spool * weight / weight_per_spool
- else:
- cost = 0
- # Material amount is sent as an amount of mm^3, so calculate length from that
- if radius != 0:
- length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
- else:
- length = 0
- self._material_weights[build_plate_number].append(weight)
- self._material_lengths[build_plate_number].append(length)
- self._material_costs[build_plate_number].append(cost)
- self._material_names[build_plate_number].append(material_name)
- self.materialLengthsChanged.emit()
- self.materialWeightsChanged.emit()
- self.materialCostsChanged.emit()
- self.materialNamesChanged.emit()
- def _onPreferencesChanged(self, preference):
- if preference != "cura/material_settings":
- return
- for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
- self._calculateInformation(build_plate_number)
- def _onActiveBuildPlateChanged(self):
- new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
- if new_active_build_plate != self._active_build_plate:
- self._active_build_plate = new_active_build_plate
- self._initVariablesWithBuildPlate(self._active_build_plate)
- self.materialLengthsChanged.emit()
- self.materialWeightsChanged.emit()
- self.materialCostsChanged.emit()
- self.materialNamesChanged.emit()
- self.currentPrintTimeChanged.emit()
- def _onActiveMaterialsChanged(self, *args, **kwargs):
- for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
- self._calculateInformation(build_plate_number)
- # Manual override of job name should also set the base name so that when the printer prefix is updated, it the
- # prefix can be added to the manually added name, not the old base name
- @pyqtSlot(str, bool)
- def setJobName(self, name, is_user_specified_job_name = False):
- self._is_user_specified_job_name = is_user_specified_job_name
- self._job_name = name
- self._base_name = name.replace(self._abbr_machine + "_", "")
- if name == "":
- self._is_user_specified_job_name = False
- self.jobNameChanged.emit()
- jobNameChanged = pyqtSignal()
- @pyqtProperty(str, notify = jobNameChanged)
- def jobName(self):
- return self._job_name
- def _updateJobName(self):
- if self._base_name == "":
- self._job_name = "unnamed"
- self._is_user_specified_job_name = False
- self.jobNameChanged.emit()
- return
- base_name = self._stripAccents(self._base_name)
- self._setAbbreviatedMachineName()
- # Only update the job name when it's not user-specified.
- if not self._is_user_specified_job_name:
- if self._pre_sliced:
- self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
- elif Preferences.getInstance().getValue("cura/jobname_prefix"):
- # Don't add abbreviation if it already has the exact same abbreviation.
- if base_name.startswith(self._abbr_machine + "_"):
- self._job_name = base_name
- else:
- self._job_name = self._abbr_machine + "_" + base_name
- else:
- self._job_name = base_name
- self.jobNameChanged.emit()
- @pyqtSlot(str)
- def setProjectName(self, name):
- self.setBaseName(name, is_project_file = True)
- baseNameChanged = pyqtSignal()
- def setBaseName(self, base_name: str, is_project_file: bool = False):
- self._is_user_specified_job_name = False
- # Ensure that we don't use entire path but only filename
- name = os.path.basename(base_name)
- # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
- # extension. This cuts the extension off if necessary.
- check_name = os.path.splitext(name)[0]
- filename_parts = os.path.basename(base_name).split(".")
- # If it's a gcode, also always update the job name
- is_gcode = False
- if len(filename_parts) > 1:
- # Only check the extension(s)
- is_gcode = "gcode" in filename_parts[1:]
- # if this is a profile file, always update the job name
- # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
- is_empty = check_name == ""
- if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != check_name)):
- # Only take the file name part, Note : file name might have 'dot' in name as well
- data = ""
- try:
- mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
- data = mime_type.stripExtension(name)
- except:
- Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
- if data is not None and check_name is not None:
- self._base_name = data
- else:
- self._base_name = ""
- self._updateJobName()
- @pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
- def baseName(self):
- return self._base_name
- ## Created an acronymn-like abbreviated machine name from the currently active machine name
- # Called each time the global stack is switched
- def _setAbbreviatedMachineName(self):
- global_container_stack = Application.getInstance().getGlobalContainerStack()
- if not global_container_stack:
- self._abbr_machine = ""
- return
- active_machine_type_name = global_container_stack.definition.getName()
- abbr_machine = ""
- for word in re.findall(r"[\w']+", active_machine_type_name):
- if word.lower() == "ultimaker":
- abbr_machine += "UM"
- elif word.isdigit():
- abbr_machine += word
- else:
- stripped_word = self._stripAccents(word.upper())
- # - use only the first character if the word is too long (> 3 characters)
- # - use the whole word if it's not too long (<= 3 characters)
- if len(stripped_word) > 3:
- stripped_word = stripped_word[0]
- abbr_machine += stripped_word
- self._abbr_machine = abbr_machine
- ## Utility method that strips accents from characters (eg: â -> a)
- def _stripAccents(self, str):
- return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
- @pyqtSlot(result = "QVariantMap")
- def getFeaturePrintTimes(self):
- result = {}
- if self._active_build_plate not in self._print_time_message_values:
- self._initPrintTimeMessageValues(self._active_build_plate)
- for feature, time in self._print_time_message_values[self._active_build_plate].items():
- if feature in self._print_time_message_translations:
- result[self._print_time_message_translations[feature]] = time
- else:
- result[feature] = time
- return result
- # Simulate message with zero time duration
- def setToZeroPrintInformation(self, build_plate = None):
- if build_plate is None:
- build_plate = self._active_build_plate
- # Construct the 0-time message
- temp_message = {}
- if build_plate not in self._print_time_message_values:
- self._print_time_message_values[build_plate] = {}
- for key in self._print_time_message_values[build_plate].keys():
- temp_message[key] = 0
- temp_material_amounts = [0]
- self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)
- ## Listen to scene changes to check if we need to reset the print information
- def _onSceneChanged(self, scene_node):
- # Ignore any changes that are not related to sliceable objects
- if not isinstance(scene_node, SceneNode)\
- or not scene_node.callDecoration("isSliceable")\
- or not scene_node.callDecoration("getBuildPlateNumber") == self._active_build_plate:
- return
- self.setToZeroPrintInformation(self._active_build_plate)
|