Welcome to mirror list, hosted at ThFree Co, Russian Federation.

PrintInformation.py « UI « cura - github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 22710165b38f6883f119cd4f59775123cb014f91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

import json
import math
import os
import unicodedata
from typing import Dict, List, Optional, TYPE_CHECKING

from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot, QTimer

from UM.Logger import Logger
from UM.Qt.Duration import Duration
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError

if TYPE_CHECKING:
    from cura.CuraApplication import CuraApplication

catalog = i18nCatalog("cura")


class PrintInformation(QObject):
    """A class for processing and the print times per build plate as well as managing the job name

    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.
    """


    UNTITLED_JOB_NAME = "Untitled"

    def __init__(self, application: "CuraApplication", parent = None) -> None:
        super().__init__(parent)
        self._application = application

        self.initializeCuraMessagePrintTimeProperties()

        # Indexed by build plate number
        self._material_lengths = {}  # type: Dict[int, List[float]]
        self._material_weights = {}  # type: Dict[int, List[float]]
        self._material_costs = {}   # type: Dict[int, List[float]]
        self._material_names = {}  # type: Dict[int, List[str]]

        self._pre_sliced = False

        self._backend = self._application.getBackend()
        if self._backend:
            self._backend.printDurationMessage.connect(self._onPrintDurationMessage)

        self._application.getController().getScene().sceneChanged.connect(self._onSceneChangedDelayed)

        self._change_timer = QTimer()
        self._change_timer.setInterval(100)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self._onSceneChanged)

        self._is_user_specified_job_name = False
        self._base_name = ""
        self._abbr_machine = ""
        self._job_name = ""
        self._active_build_plate = 0
        self._initVariablesByBuildPlate(self._active_build_plate)

        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._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
        self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)

        self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
        self._material_amounts = []  # type: List[float]
        self._onActiveMaterialsChanged()

    def initializeCuraMessagePrintTimeProperties(self) -> None:
        self._current_print_time = {}  # type: Dict[int, Duration]

        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"),
            "prime_tower": catalog.i18nc("@tooltip", "Prime Tower"),
            "travel": catalog.i18nc("@tooltip", "Travel"),
            "retract": catalog.i18nc("@tooltip", "Retractions"),
            "none": catalog.i18nc("@tooltip", "Other")
        }

        self._print_times_per_feature = {}  # type: Dict[int, Dict[str, Duration]]

    def _initPrintTimesPerFeature(self, build_plate_number: int) -> None:
        # Full fill message values using keys from _print_time_message_translations
        self._print_times_per_feature[build_plate_number] = {}
        for key in self._print_time_message_translations.keys():
            self._print_times_per_feature[build_plate_number][key] = Duration(None, self)

    def _initVariablesByBuildPlate(self, build_plate_number: int) -> None:
        if build_plate_number not in self._print_times_per_feature:
            self._initPrintTimesPerFeature(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(parent = self)

    currentPrintTimeChanged = pyqtSignal()

    preSlicedChanged = pyqtSignal()

    @pyqtProperty(bool, notify=preSlicedChanged)
    def preSliced(self) -> bool:
        return self._pre_sliced

    def setPreSliced(self, pre_sliced: bool) -> None:
        if self._pre_sliced != pre_sliced:
            self._pre_sliced = pre_sliced
            self._updateJobName()
            self.preSlicedChanged.emit()

    @pyqtProperty(Duration, notify = currentPrintTimeChanged)
    def currentPrintTime(self) -> Duration:
        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]

    #   Get all print times (by feature) of the active buildplate.
    def printTimes(self) -> Dict[str, Duration]:
        return self._print_times_per_feature[self._active_build_plate]

    def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None:
        self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature)
        self.currentPrintTimeChanged.emit()

        self._material_amounts = material_amounts
        self._calculateInformation(build_plate_number)

    def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None:
        total_estimated_time = 0

        if build_plate_number not in self._print_times_per_feature:
            self._initPrintTimesPerFeature(build_plate_number)

        for feature, time in print_times_per_feature.items():
            if feature not in self._print_times_per_feature[build_plate_number]:
                self._print_times_per_feature[build_plate_number][feature] = Duration(parent=self)
            duration = self._print_times_per_feature[build_plate_number][feature]

            if time != time:  # Check for NaN. Engine can sometimes give us weird values.
                duration.setDuration(0)
                Logger.log("w", "Received NaN for print duration message")
                continue

            total_estimated_time += time
            duration.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: int) -> None:
        global_stack = self._application.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] = []

        try:
            material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
        except json.JSONDecodeError:
            Logger.warning("Material preference values are corrupt. Will revert to defaults!")
            material_preference_values = {}

        for index, extruder_stack in enumerate(global_stack.extruderList):
            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.material
            radius = extruder_stack.getProperty("material_diameter", "value") / 2

            weight = float(amount) * float(density) / 1000
            cost = 0.

            material_guid = material.getMetaDataEntry("GUID")
            material_name = material.getName()

            if material_guid in material_preference_values:
                material_values = material_preference_values[material_guid]

                if material_values and "spool_weight" in material_values:
                    weight_per_spool = float(material_values["spool_weight"])
                else:
                    weight_per_spool = float(extruder_stack.getMetaDataEntry("properties", {}).get("weight", 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: str) -> None:
        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) -> None:
        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._updateJobName()

            self._initVariablesByBuildPlate(self._active_build_plate)

            self.materialLengthsChanged.emit()
            self.materialWeightsChanged.emit()
            self.materialCostsChanged.emit()
            self.materialNamesChanged.emit()
            self.currentPrintTimeChanged.emit()

    def _onActiveMaterialsChanged(self, *args, **kwargs) -> None:
        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: str, is_user_specified_job_name = False) -> None:
        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) -> None:
        if self._base_name == "":
            self._job_name = self.UNTITLED_JOB_NAME
            self._is_user_specified_job_name = False
            self.jobNameChanged.emit()
            return

        base_name = self._stripAccents(self._base_name)
        self._defineAbbreviatedMachineName()

        # Only update the job name when it's not user-specified.
        if not self._is_user_specified_job_name:
            if self._application.getInstance().getPreferences().getValue("cura/jobname_prefix") and not self._pre_sliced:
                # 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

        # In case there are several buildplates, a suffix is attached
        if self._multi_build_plate_model.maxBuildPlate > 0:
            connector = "_#"
            suffix = connector + str(self._active_build_plate + 1)
            if connector in self._job_name:
                self._job_name = self._job_name.split(connector)[0] # get the real name
            if self._active_build_plate != 0:
                self._job_name += suffix

        self.jobNameChanged.emit()

    @pyqtSlot(str)
    def setProjectName(self, name: str) -> None:
        self.setBaseName(name, is_project_file = True)

    baseNameChanged = pyqtSignal()

    def setBaseName(self, base_name: str, is_project_file: bool = False) -> None:
        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 MimeTypeNotFoundError:
                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 = ""

            # Strip the old "curaproject" extension from the name
            OLD_CURA_PROJECT_EXT = ".curaproject"
            if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
                self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]

            # CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
            OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
            while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
                self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]

            self._updateJobName()

    @pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
    def baseName(self):
        return self._base_name

    def _defineAbbreviatedMachineName(self) -> None:
        """Created an acronym-like abbreviated machine name from the currently active machine name.

        Called each time the global stack is switched.
        """

        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            self._abbr_machine = ""
            return
        active_machine_type_name = global_container_stack.definition.getName()

        self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)

    def _stripAccents(self, to_strip: str) -> str:
        """Utility method that strips accents from characters (eg: â -> a)"""

        return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')

    @pyqtSlot(result = "QVariantMap")
    def getFeaturePrintTimes(self) -> Dict[str, Duration]:
        result = {}
        if self._active_build_plate not in self._print_times_per_feature:
            self._initPrintTimesPerFeature(self._active_build_plate)
        for feature, time in self._print_times_per_feature[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: Optional[int] = None) -> 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_times_per_feature:
            self._print_times_per_feature[build_plate] = {}
        for key in self._print_times_per_feature[build_plate].keys():
            temp_message[key] = 0
        temp_material_amounts = [0.]

        self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)

    def _onSceneChangedDelayed(self, scene_node: SceneNode) -> None:
        # 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._change_timer.start()

    def _onSceneChanged(self) -> None:
        """Listen to scene changes to check if we need to reset the print information"""

        self.setToZeroPrintInformation(self._active_build_plate)