Merge branch 'master' into feature_xmlmaterials_cura_settings

# Conflicts:
#	plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
fieldOfView 2018-03-06 11:40:58 +01:00
commit 0578e74e71
840 changed files with 56973 additions and 26870 deletions

39
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,39 @@
<!--
The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template will be removed.
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
Thank you for using Cura!
-->
**Application Version**
<!-- The version of the application this issue occurs with -->
**Platform**
<!-- Information about the platform the issue occurs on -->
**Qt**
<!-- The version of Qt used (not necessary if you're using the version from Ultimaker's website) -->
**PyQt**
<!-- The version of PyQt used (not necessary if you're using the version from Ultimaker's website) -->
**Display Driver**
<!-- Video driver name and version -->
**Steps to Reproduce**
<!-- Add the steps needed that lead up to the issue (replace this text) -->
**Actual Results**
<!-- What happens after the above steps have been followed (replace this text) -->
**Expected results**
<!-- What should happen after the above steps have been followed (replace this text) -->
**Additional Information**
<!-- Extra information relevant to the issue, like screenshots (replace this text) -->

20
.gitignore vendored
View file

@ -33,20 +33,22 @@ cura.desktop
.settings .settings
#Externally located plug-ins. #Externally located plug-ins.
plugins/CuraSolidWorksPlugin
plugins/Doodle3D-cura-plugin
plugins/GodMode
plugins/PostProcessingPlugin
plugins/X3GWriter
plugins/FlatProfileExporter
plugins/ProfileFlattener
plugins/cura-god-mode-plugin
plugins/cura-big-flame-graph plugins/cura-big-flame-graph
plugins/cura-god-mode-plugin
plugins/cura-siemensnx-plugin plugins/cura-siemensnx-plugin
plugins/CuraVariSlicePlugin plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin
plugins/CuraLiveScriptingPlugin plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator plugins/CuraPrintProfileCreator
plugins/CuraSolidWorksPlugin
plugins/CuraVariSlicePlugin
plugins/Doodle3D-cura-plugin
plugins/FlatProfileExporter
plugins/GodMode
plugins/OctoPrintPlugin plugins/OctoPrintPlugin
plugins/ProfileFlattener
plugins/X3GWriter
#Build stuff #Build stuff
CMakeCache.txt CMakeCache.txt

68
Jenkinsfile vendored
View file

@ -1,45 +1,47 @@
parallel_nodes(['linux && cura', 'windows && cura']) { parallel_nodes(['linux && cura', 'windows && cura']) {
// Prepare building timeout(time: 2, unit: "HOURS") {
stage('Prepare') { // Prepare building
// Ensure we start with a clean build directory. stage('Prepare') {
step([$class: 'WsCleanup']) // Ensure we start with a clean build directory.
step([$class: 'WsCleanup'])
// Checkout whatever sources are linked to this pipeline. // Checkout whatever sources are linked to this pipeline.
checkout scm checkout scm
} }
// If any error occurs during building, we want to catch it and continue with the "finale" stage. // If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError { catchError {
// Building and testing should happen in a subdirectory. // Building and testing should happen in a subdirectory.
dir('build') { dir('build') {
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup. // Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
stage('Build') { stage('Build') {
def branch = env.BRANCH_NAME def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) { if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
branch = "master" branch = "master"
}
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
cmake("..", "-DCMAKE_PREFIX_PATH=\"${env.CURA_ENVIRONMENT_PATH}/${branch}\" -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=\"${uranium_dir}\"")
} }
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it. // Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}") stage('Unit Test') {
cmake("..", "-DCMAKE_PREFIX_PATH=\"${env.CURA_ENVIRONMENT_PATH}/${branch}\" -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=\"${uranium_dir}\"") try {
} make('test')
} catch(e) {
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable". currentBuild.result = "UNSTABLE"
stage('Unit Test') { }
try {
make('test')
} catch(e) {
currentBuild.result = "UNSTABLE"
} }
} }
} }
}
// Perform any post-build actions like notification and publishing of unit tests. // Perform any post-build actions like notification and publishing of unit tests.
stage('Finalize') { stage('Finalize') {
// Publish the test results to Jenkins. // Publish the test results to Jenkins.
junit allowEmptyResults: true, testResults: 'build/junit*.xml' junit allowEmptyResults: true, testResults: 'build/junit*.xml'
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.']) notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
}
} }
} }

View file

@ -1,22 +1,16 @@
Cura Cura
==== ====
This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
This is the new, shiny frontend for Cura. [daid/Cura](https://github.com/daid/Cura.git) is the old legacy Cura that everyone knows and loves/hates.
We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
Logging Issues Logging Issues
------------ ------------
Use [this](https://github.com/Ultimaker/Uranium/wiki/Bug-Reporting-Template) template to report issues. New issues that do not adhere to this template will take us a lot longer to handle and will therefore have a lower pirority.
For crashes and similar issues, please attach the following information: For crashes and similar issues, please attach the following information:
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output) * (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
* The Cura GUI log file, located at * The Cura GUI log file, located at
* %APPDATA%\cura\\`<Cura version>`\cura.log (Windows), or usually C:\Users\\`<your username>`\AppData\Roaming\cura\\`<Cura version>`\cura.log * `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* $User/Library/Application Support/cura/`<Cura version>`/cura.log (OSX) * `$USER/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* $USER/.local/share/cura/`<Cura version>`/cura.log (Ubuntu/Linux) * `$USER/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
@ -24,72 +18,35 @@ For additional support, you could also ask in the #cura channel on FreeNode IRC.
Dependencies Dependencies
------------ ------------
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
* [Uranium](https://github.com/Ultimaker/Uranium) * [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
Cura is built on top of the Uranium framework. * [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) * [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers
This will be needed at runtime to perform the actual slicing.
* [PySerial](https://github.com/pyserial/pyserial)
Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf)
Only required to detect mDNS-enabled printers
Configuring Cura
----------------
Link your CuraEngine backend by inserting the following lines in `$HOME/.config/cura/config.cfg` :
```
[backend]
location = /[path_to_the..]/CuraEngine/build/CuraEngine
```
Build scripts Build scripts
------------- -------------
Please checkout [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
Please checkout [cura-build](https://github.com/Ultimaker/cura-build) Running from Source
Third party plugins
------------- -------------
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code. Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
* [OctoPrint Plugin](https://github.com/fieldofview/OctoPrintPlugin): Send printjobs directly to OctoPrint and monitor their progress in Cura.
* [Electric Print Cost Calculator Plugin](https://github.com/zoff99/ElectricPrintCostCalculator): Calculate the electric costs of a print.
Making profiles for other printers Plugins
---------------------------------- -------------
If your make of printer is not in the list of supported printers, and using the "Custom FDM Printer" does not offer enough flexibility, you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original.def.json) as a template. Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.
* Change the machine ID to something unique Supported printers
* Change the machine_name to your printer's name -------------
* If you have a 3D model of your platform you can put it in resources/meshes and put its name under platform Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support for new machines.
* Set your machine's dimensions with machine_width, machine_depth, and machine_height
* If your printer's origin is in the center of the bed, set machine_center_is_zero to true.
* Set your print head dimensions with the machine_head_shape parameters
* Set the start and end gcode in machine_start_gcode and machine_end_gcode
Once you are done, put the profile you have made into resources/definitions, or in definitions in your cura profile folder. Configuring Cura
----------------
If you want to make a definition for a multi-extrusion printer, have a look at [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original_dual.def.json) as a template, along with the two extruder definitions it references [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_1st.def.json) and [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_2nd.def.json) Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
Translating Cura Translating Cura
---------------- ----------------
If you'd like to contribute a translation of Cura, please first look for [any existing translation](https://github.com/Ultimaker/Cura/tree/master/resources/i18n). If your language is already there in the source code but not in Cura's interface, it may be partially translated. Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages.
There are four files that need to be translated for Cura: License
1. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot ----------------
2. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmextruder.def.json.pot Cura is released under the terms of the LGPLv3 or higher. A copy of this license should be included with the software.
3. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmprinter.def.json.pot (This one is the most work.)
4. https://github.com/Ultimaker/Uranium/blob/master/resources/i18n/uranium.pot
Copy these files and rename them to `*.po` (remove the `t`). Then create the actual translations by filling in the empty `msgstr` entries. These are gettext files, which are plain text so you can open them with any text editor such as Notepad or GEdit, but it is probably easier with a specialised tool such as [POEdit](https://poedit.net/) or [Virtaal](http://virtaal.translatehouse.org/).
Do not hestiate to ask us about a translation or the meaning of some text via Github Issues.
Once the translation is complete, it's probably best to test them in Cura. Use your favourite software to convert the .po file to a .mo file (such as [GetText](https://www.gnu.org/software/gettext/)). Then put the .mo files in the `.../resources/i18n/<language code>/LC_MESSAGES` folder in your Cura installation. Then find your Cura configuration file (next to the log as described above, except on Linux where it is located in `~/.config/cura`) and change the language preference to the name of the folder you just created. Then start Cura. If working correctly, your Cura should now be translated.
To submit your translation, ideally you would make two pull requests where all `*.po` files are located in that same `<language code>` folder in the resources of both the Cura and Uranium repositories. Put `cura.po`, `fdmprinter.def.json.po` and `fdmextruder.def.json.po` in the Cura repository, and put `uranium.po` in the Uranium repository. Then submit the pull requests to Github. For people with less experience with Git, you can also e-mail the translations to the e-mail address listed at the top of the [cura.pot](https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot) file as the `Report-Msgid-Bugs-To` entry and we'll make sure it gets checked and included.
After the translation is submitted, the Cura maintainers will check for its completeness and check whether it is consistent. We will take special care to look for common mistakes, such as translating mark-up `<message>` code and such. We are often not fluent in every language, so we expect the translator and the international users to make corrections where necessary. Of course, there will always be some mistakes in every translation.
When the next Cura release comes around, some of the texts will have changed and some new texts will have been added. Around the time when the beta is released we will invoke a string freeze, meaning that no developer is allowed to make changes to the texts. Then we will update the translation template `.pot` files and ask all our translators to update their translations. If you are unable to update the translation in time for the actual release, we will remove the language from the drop-down menu in the Preferences window. The translation stays in Cura however, so that someone might pick it up again later and update it with the newest texts. Also, users who had previously selected the language can still continue Cura in their language but English text will appear among the original text.

View file

@ -24,16 +24,23 @@ function(cura_add_test)
if(WIN32) if(WIN32)
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH}) string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}\\;$ENV{PYTHONPATH}")
else() else()
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH}) string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}:$ENV{PYTHONPATH}")
endif() endif()
add_test( get_test_property(${_NAME} ENVIRONMENT test_exists) #Find out if the test exists by getting a property from it that always exists (such as ENVIRONMENT because we set that ourselves).
NAME ${_NAME} if (NOT ${test_exists})
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} add_test(
) NAME ${_NAME}
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") )
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
else()
message(WARNING "Duplicate test ${_NAME}!")
endif()
endfunction() endfunction()
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}") cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")

View file

@ -3,7 +3,7 @@
<component type="desktop"> <component type="desktop">
<id>cura.desktop</id> <id>cura.desktop</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>AGPL-3.0 and CC-BY-SA-4.0</project_license> <project_license>LGPL-3.0 and CC-BY-SA-4.0</project_license>
<name>Cura</name> <name>Cura</name>
<summary>The world's most advanced 3d printer software</summary> <summary>The world's most advanced 3d printer software</summary>
<description> <description>
@ -15,7 +15,7 @@
</p> </p>
<ul> <ul>
<li>Novices can start printing right away</li> <li>Novices can start printing right away</li>
<li>Experts are able to customize 200 settings to achieve the best results</li> <li>Experts are able to customize 300 settings to achieve the best results</li>
<li>Optimized profiles for Ultimaker materials</li> <li>Optimized profiles for Ultimaker materials</li>
<li>Supported by a global network of Ultimaker certified service partners</li> <li>Supported by a global network of Ultimaker certified service partners</li>
<li>Print multiple objects at once with different settings for each object</li> <li>Print multiple objects at once with different settings for each object</li>
@ -26,6 +26,6 @@
<screenshots> <screenshots>
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot> <screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
</screenshots> </screenshots>
<url type="homepage">https://ultimaker.com/en/products/cura-software</url> <url type="homepage">https://ultimaker.com/en/products/cura-software?utm_source=cura&utm_medium=software&utm_campaign=resources</url>
<translation type="gettext">Cura</translation> <translation type="gettext">Cura</translation>
</component> </component>

25
cura/Arrange.py → cura/Arranging/Arrange.py Executable file → Normal file
View file

@ -1,8 +1,8 @@
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from collections import namedtuple from collections import namedtuple
@ -30,6 +30,7 @@ class Arrange:
self._offset_x = offset_x self._offset_x = offset_x
self._offset_y = offset_y self._offset_y = offset_y
self._last_priority = 0 self._last_priority = 0
self._is_empty = True
## Helper to create an Arranger instance ## Helper to create an Arranger instance
# #
@ -38,8 +39,8 @@ class Arrange:
# \param scene_root Root for finding all scene nodes # \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed # \param fixed_nodes Scene nodes to be placed
@classmethod @classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
arranger = Arrange(220, 220, 110, 110, scale = scale) arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
if fixed_nodes is None: if fixed_nodes is None:
@ -64,7 +65,7 @@ class Arrange:
for area in disallowed_areas: for area in disallowed_areas:
points = copy.deepcopy(area._points) points = copy.deepcopy(area._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr) arranger.place(0, 0, shape_arr, update_empty = False)
return arranger return arranger
## Find placement for a node (using offset shape) and place it (using hull shape) ## Find placement for a node (using offset shape) and place it (using hull shape)
@ -168,7 +169,8 @@ class Arrange:
# \param x x-coordinate # \param x x-coordinate
# \param y y-coordinate # \param y y-coordinate
# \param shape_arr ShapeArray object # \param shape_arr ShapeArray object
def place(self, x, y, shape_arr): # \param update_empty updates the _is_empty, used when adding disallowed areas
def place(self, x, y, shape_arr, update_empty = True):
x = int(self._scale * x) x = int(self._scale * x)
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x offset_x = x + self._offset_x + shape_arr.offset_x
@ -181,10 +183,17 @@ class Arrange:
max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1) max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
occupied_slice = self._occupied[min_y:max_y, min_x:max_x] occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
# we use a slice of shape because it can be out of bounds # we use a slice of shape because it can be out of bounds
occupied_slice[numpy.where(shape_arr.arr[ new_occupied = numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1 min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)
if update_empty and new_occupied:
self._is_empty = False
occupied_slice[new_occupied] = 1
# Set priority to low (= high number), so it won't get picked at trying out. # Set priority to low (= high number), so it won't get picked at trying out.
prio_slice = self._priority[min_y:max_y, min_x:max_x] prio_slice = self._priority[min_y:max_y, min_x:max_x]
prio_slice[numpy.where(shape_arr.arr[ prio_slice[numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999 min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
@property
def isEmpty(self):
return self._is_empty

View file

@ -0,0 +1,154 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Message import Message
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray
from typing import List
class ArrangeArray:
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
self._x = x
self._y = y
self._fixed_nodes = fixed_nodes
self._count = 0
self._first_empty = None
self._has_empty = False
self._arrange = []
def _update_first_empty(self):
for i, a in enumerate(self._arrange):
if a.isEmpty:
self._first_empty = i
self._has_empty = True
return
self._first_empty = None
self._has_empty = False
def add(self):
new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes)
self._arrange.append(new_arrange)
self._count += 1
self._update_first_empty()
def count(self):
return self._count
def get(self, index):
return self._arrange[index]
def getFirstEmpty(self):
if not self._is_empty:
self.add()
return self._arrange[self._first_empty]
class ArrangeObjectsAllBuildPlatesJob(Job):
def __init__(self, nodes: List[SceneNode], min_offset = 8):
super().__init__()
self._nodes = nodes
self._min_offset = min_offset
def run(self):
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
lifetime = 0,
dismissable=False,
progress = 0,
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()
# Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in self._nodes:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
# Sort the nodes with the biggest area first.
nodes_arr.sort(key=lambda item: item[0])
nodes_arr.reverse()
x, y = 200, 200
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
arrange_array.add()
# Place nodes one at a time
start_priority = 0
grouped_operation = GroupedOperation()
found_solution_for_all = True
left_over_nodes = [] # nodes that do not fit on an empty build plate
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
# For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue).
# We also skip possibilities by slicing through the possibilities (step = 10)
try_placement = True
current_build_plate_number = 0 # always start with the first one
# # Only for first build plate
# if last_size == size and last_build_plate_number == current_build_plate_number:
# # This optimization works if many of the objects have the same size
# # Continue with same build plate number
# start_priority = last_priority
# else:
# start_priority = 0
while try_placement:
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
while current_build_plate_number >= arrange_array.count():
arrange_array.add()
arranger = arrange_array.get(current_build_plate_number)
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox():
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = 0
if x is not None: # We could find a place
arranger.place(x, y, hull_shape_arr) # place the object in the arranger
node.callDecoration("setBuildPlateNumber", current_build_plate_number)
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
try_placement = False
else:
# very naive, because we skip to the next build plate if one model doesn't fit.
if arranger.isEmpty:
# apparently we can never place this object
left_over_nodes.append(node)
try_placement = False
else:
# try next build plate
current_build_plate_number += 1
try_placement = True
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
Job.yieldThread()
for node in left_over_nodes:
node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate
found_solution_for_all = False
grouped_operation.push()
status_message.hide()
if not found_solution_for_all:
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
no_full_solution_message.show()

View file

@ -4,7 +4,6 @@
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Logger import Logger from UM.Logger import Logger
@ -12,9 +11,9 @@ from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from typing import List from typing import List
@ -89,3 +88,5 @@ class ArrangeObjectsJob(Job):
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
no_full_solution_message.show() no_full_solution_message.show()
self.finished.emit(self)

13
cura/ShapeArray.py → cura/Arranging/ShapeArray.py Executable file → Normal file
View file

@ -29,8 +29,12 @@ class ShapeArray:
offset_x = int(numpy.amin(flip_vertices[:, 1])) offset_x = int(numpy.amin(flip_vertices[:, 1]))
flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y) flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x) flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))] shape = numpy.array([int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))])
shape[numpy.where(shape == 0)] = 1
arr = cls.arrayFromPolygon(shape, flip_vertices) arr = cls.arrayFromPolygon(shape, flip_vertices)
if not numpy.ndarray.any(arr):
# set at least 1 pixel
arr[0][0] = 1
return cls(arr, offset_x, offset_y) return cls(arr, offset_x, offset_y)
## Instantiate an offset and hull ShapeArray from a scene node. ## Instantiate an offset and hull ShapeArray from a scene node.
@ -43,13 +47,12 @@ class ShapeArray:
transform_x = transform._data[0][3] transform_x = transform._data[0][3]
transform_y = transform._data[2][3] transform_y = transform._data[2][3]
hull_verts = node.callDecoration("getConvexHull") hull_verts = node.callDecoration("getConvexHull")
# If a model is too small then it will not contain any points
if hull_verts is None or not hull_verts.getPoints().any():
return None, None
# For one_at_a_time printing you need the convex hull head. # For one_at_a_time printing you need the convex hull head.
hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
# If a model is to small then it will not contain any points
if not hull_verts.getPoints().any():
return None, None
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
offset_points = copy.deepcopy(offset_verts._points) # x, y offset_points = copy.deepcopy(offset_verts._points) # x, y
offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x) offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)

View file

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -25,7 +26,7 @@ catalog = i18nCatalog("cura")
import numpy import numpy
import math import math
from typing import List from typing import List, Optional
# Setting for clearance around the prime # Setting for clearance around the prime
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -73,6 +74,11 @@ class BuildVolume(SceneNode):
self._adhesion_type = None self._adhesion_type = None
self._platform = Platform(self) self._platform = Platform(self)
self._build_volume_message = Message(catalog.i18nc("@info:status",
"The build volume height has been reduced due to the value of the"
" \"Print Sequence\" setting to prevent the gantry from colliding"
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
self._global_container_stack = None self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
self._onStackChanged() self._onStackChanged()
@ -96,11 +102,6 @@ class BuildVolume(SceneNode):
self._setting_change_timer.setSingleShot(True) self._setting_change_timer.setSingleShot(True)
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished) self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
self._build_volume_message = Message(catalog.i18nc("@info:status",
"The build volume height has been reduced due to the value of the"
" \"Print Sequence\" setting to prevent the gantry from colliding"
" with printed models."), title = catalog.i18nc("@info:title","Build Volume"))
# Must be after setting _build_volume_message, apparently that is used in getMachineManager. # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
# Therefore this works. # Therefore this works.
@ -241,6 +242,44 @@ class BuildVolume(SceneNode):
for child_node in group_node.getAllChildren(): for child_node in group_node.getAllChildren():
child_node._outside_buildarea = group_node._outside_buildarea child_node._outside_buildarea = group_node._outside_buildarea
## Update the outsideBuildArea of a single node, given bounds or current build volume
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
if not isinstance(node, CuraSceneNode):
return
if bounds is None:
build_volume_bounding_box = self.getBoundingBox()
if build_volume_bounding_box:
# It's over 9000!
build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
else:
# No bounding box. This is triggered when running Cura from command line with a model for the first time
# In that situation there is a model, but no machine (and therefore no build volume.
return
else:
build_volume_bounding_box = bounds
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
bbox = node.getBoundingBox()
# Mark the node as outside the build volume if the bounding box test fails.
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node.setOutsideBuildArea(True)
return
convex_hull = self.callDecoration("getConvexHull")
if convex_hull:
if not convex_hull.isValid():
return
# Check for collisions between disallowed areas and the object
for area in self.getDisallowedAreas():
overlap = convex_hull.intersectsPolygon(area)
if overlap is None:
continue
node.setOutsideBuildArea(True)
return
node.setOutsideBuildArea(False)
## Recalculates the build volume & disallowed areas. ## Recalculates the build volume & disallowed areas.
def rebuild(self): def rebuild(self):
if not self._width or not self._height or not self._depth: if not self._width or not self._height or not self._depth:
@ -698,12 +737,17 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2 prime_tower_y = prime_tower_y + machine_depth / 2
prime_tower_area = Polygon([ if self._global_container_stack.getProperty("prime_tower_circular", "value"):
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size], radius = prime_tower_size / 2
[prime_tower_x, prime_tower_y - prime_tower_size], prime_tower_area = Polygon.approximatedCircle(radius)
[prime_tower_x, prime_tower_y], prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
[prime_tower_x - prime_tower_size, prime_tower_y], else:
]) prime_tower_area = Polygon([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y],
[prime_tower_x - prime_tower_size, prime_tower_y],
])
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0)) prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
for extruder in used_extruders: for extruder in used_extruders:
result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset. result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
@ -782,6 +826,7 @@ class BuildVolume(SceneNode):
offset_y = extruder.getProperty("machine_nozzle_offset_y", "value") offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
if offset_y is None: if offset_y is None:
offset_y = 0 offset_y = 0
offset_y = -offset_y #Y direction of g-code is the inverse of Y direction of Cura's scene space.
result[extruder_id] = [] result[extruder_id] = []
for polygon in machine_disallowed_polygons: for polygon in machine_disallowed_polygons:
@ -892,8 +937,8 @@ class BuildVolume(SceneNode):
# stack. # stack.
# #
# \return A sequence of setting values, one for each extruder. # \return A sequence of setting values, one for each extruder.
def _getSettingFromAllExtruders(self, setting_key, property = "value"): def _getSettingFromAllExtruders(self, setting_key):
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property) all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
for i in range(len(all_values)): for i in range(len(all_values)):
if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"): if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"):
@ -906,7 +951,7 @@ class BuildVolume(SceneNode):
# not part of the collision radius, such as bed adhesion (skirt/brim/raft) # not part of the collision radius, such as bed adhesion (skirt/brim/raft)
# and travel avoid distance. # and travel avoid distance.
def _getEdgeDisallowedSize(self): def _getEdgeDisallowedSize(self):
if not self._global_container_stack: if not self._global_container_stack or not self._global_container_stack.extruders:
return 0 return 0
container_stack = self._global_container_stack container_stack = self._global_container_stack
@ -984,7 +1029,7 @@ class BuildVolume(SceneNode):
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"] _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"] _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.

View file

@ -12,7 +12,7 @@ class CameraImageProvider(QQuickImageProvider):
def requestImage(self, id, size): def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try: try:
return output_device.getCameraImage(), QSize(15, 15) return output_device.activePrinter.camera.getImage(), QSize(15, 15)
except AttributeError: except AttributeError:
pass pass
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys
import platform import platform
import traceback import traceback
import faulthandler import faulthandler
@ -13,15 +12,18 @@ import json
import ssl import ssl
import urllib.request import urllib.request
import urllib.error import urllib.error
import shutil
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from PyQt5.QtGui import QDesktopServices
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Platform import Platform from UM.Platform import Platform
from UM.Resources import Resources
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -37,7 +39,7 @@ else:
# List of exceptions that should be considered "fatal" and abort the program. # List of exceptions that should be considered "fatal" and abort the program.
# These are primarily some exception types that we simply cannot really recover from # These are primarily some exception types that we simply cannot really recover from
# (MemoryError and SystemError) and exceptions that indicate grave errors in the # (MemoryError and SystemError) and exceptions that indicate grave errors in the
# code that cause the Python interpreter to fail (SyntaxError, ImportError). # code that cause the Python interpreter to fail (SyntaxError, ImportError).
fatal_exception_types = [ fatal_exception_types = [
MemoryError, MemoryError,
SyntaxError, SyntaxError,
@ -49,11 +51,12 @@ fatal_exception_types = [
class CrashHandler: class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura" crash_url = "https://stats.ultimaker.com/api/cura"
def __init__(self, exception_type, value, tb): def __init__(self, exception_type, value, tb, has_started = True):
self.exception_type = exception_type self.exception_type = exception_type
self.value = value self.value = value
self.traceback = tb self.traceback = tb
self.dialog = QDialog() self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication
# While we create the GUI, the information will be stored for sending afterwards # While we create the GUI, the information will be stored for sending afterwards
self.data = dict() self.data = dict()
@ -64,20 +67,143 @@ class CrashHandler:
for part in line.rstrip("\n").split("\n"): for part in line.rstrip("\n").split("\n"):
Logger.log("c", part) Logger.log("c", part)
if not CuraDebugMode and exception_type not in fatal_exception_types: # If Cura has fully started, we only show fatal errors.
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
# without any information.
if has_started and exception_type not in fatal_exception_types:
return return
application = QCoreApplication.instance() if not has_started:
if not application: self._send_report_checkbox = None
sys.exit(1) self.early_crash_dialog = self._createEarlyCrashDialog()
self.dialog = QDialog()
self._createDialog() self._createDialog()
def _createEarlyCrashDialog(self):
dialog = QDialog()
dialog.setMinimumWidth(500)
dialog.setMinimumHeight(170)
dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed"))
dialog.finished.connect(self._closeEarlyCrashDialog)
layout = QVBoxLayout(dialog)
label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred.</p></b>
<p>Unfortunately, Cura encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.</p>
<p>Backups can be found in the configuration folder.</p>
<p>Please send us this Crash Report to fix the problem.</p>
"""))
label.setWordWrap(True)
layout.addWidget(label)
# "send report" check box and show details
self._send_report_checkbox = QCheckBox(catalog.i18nc("@action:button", "Send crash report to Ultimaker"), dialog)
self._send_report_checkbox.setChecked(True)
show_details_button = QPushButton(catalog.i18nc("@action:button", "Show detailed crash report"), dialog)
show_details_button.setMaximumWidth(200)
show_details_button.clicked.connect(self._showDetailedReport)
show_configuration_folder_button = QPushButton(catalog.i18nc("@action:button", "Show configuration folder"), dialog)
show_configuration_folder_button.setMaximumWidth(200)
show_configuration_folder_button.clicked.connect(self._showConfigurationFolder)
layout.addWidget(self._send_report_checkbox)
layout.addWidget(show_details_button)
layout.addWidget(show_configuration_folder_button)
# "backup and start clean" and "close" buttons
buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close)
buttons.addButton(catalog.i18nc("@action:button", "Backup and Reset Configuration"), QDialogButtonBox.AcceptRole)
buttons.rejected.connect(self._closeEarlyCrashDialog)
buttons.accepted.connect(self._backupAndStartClean)
layout.addWidget(buttons)
return dialog
def _closeEarlyCrashDialog(self):
if self._send_report_checkbox.isChecked():
self._sendCrashReport()
os._exit(1)
def _backupAndStartClean(self):
# backup the current cura directories and create clean ones
from cura.CuraVersion import CuraVersion
from UM.Resources import Resources
# The early crash may happen before those information is set in Resources, so we need to set them here to
# make sure that Resources can find the correct place.
Resources.ApplicationIdentifier = "cura"
Resources.ApplicationVersion = CuraVersion
config_path = Resources.getConfigStoragePath()
data_path = Resources.getDataStoragePath()
cache_path = Resources.getCacheStoragePath()
folders_to_backup = []
folders_to_remove = [] # only cache folder needs to be removed
folders_to_backup.append(config_path)
if data_path != config_path:
folders_to_backup.append(data_path)
# Only remove the cache folder if it's not the same as data or config
if cache_path not in (config_path, data_path):
folders_to_remove.append(cache_path)
for folder in folders_to_remove:
shutil.rmtree(folder, ignore_errors = True)
for folder in folders_to_backup:
base_name = os.path.basename(folder)
root_dir = os.path.dirname(folder)
import datetime
date_now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
idx = 0
file_name = base_name + "_" + date_now
zip_file_path = os.path.join(root_dir, file_name + ".zip")
while os.path.exists(zip_file_path):
idx += 1
file_name = base_name + "_" + date_now + "_" + idx
zip_file_path = os.path.join(root_dir, file_name + ".zip")
try:
# only create the zip backup when the folder exists
if os.path.exists(folder):
# remove the .zip extension because make_archive() adds it
zip_file_path = zip_file_path[:-4]
shutil.make_archive(zip_file_path, "zip", root_dir = root_dir, base_dir = base_name)
# remove the folder only when the backup is successful
shutil.rmtree(folder, ignore_errors = True)
# create an empty folder so Resources will not try to copy the old ones
os.makedirs(folder, 0o0755, exist_ok=True)
except Exception as e:
Logger.logException("e", "Failed to backup [%s] to file [%s]", folder, zip_file_path)
if not self.has_started:
print("Failed to backup [%s] to file [%s]: %s", folder, zip_file_path, e)
self.early_crash_dialog.close()
def _showConfigurationFolder(self):
path = Resources.getConfigStoragePath();
QDesktopServices.openUrl(QUrl.fromLocalFile( path ))
def _showDetailedReport(self):
self.dialog.exec_()
## Creates a modal dialog. ## Creates a modal dialog.
def _createDialog(self): def _createDialog(self):
self.dialog.setMinimumWidth(640) self.dialog.setMinimumWidth(640)
self.dialog.setMinimumHeight(640) self.dialog.setMinimumHeight(640)
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
# if the application has not fully started, this will be a detailed report dialog which should not
# close the application when it's closed.
if self.has_started:
self.dialog.finished.connect(self._close)
layout = QVBoxLayout(self.dialog) layout = QVBoxLayout(self.dialog)
@ -88,6 +214,9 @@ class CrashHandler:
layout.addWidget(self._userDescriptionWidget()) layout.addWidget(self._userDescriptionWidget())
layout.addWidget(self._buttonsWidget()) layout.addWidget(self._buttonsWidget())
def _close(self):
os._exit(1)
def _messageWidget(self): def _messageWidget(self):
label = QLabel() label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b> label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
@ -147,8 +276,8 @@ class CrashHandler:
layout = QVBoxLayout() layout = QVBoxLayout()
text_area = QTextEdit() text_area = QTextEdit()
trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback) trace_list = traceback.format_exception(self.exception_type, self.value, self.traceback)
trace = "".join(trace_dict) trace = "".join(trace_list)
text_area.setText(trace) text_area.setText(trace)
text_area.setReadOnly(True) text_area.setReadOnly(True)
@ -156,14 +285,28 @@ class CrashHandler:
group.setLayout(layout) group.setLayout(layout)
# Parsing all the information to fill the dictionary # Parsing all the information to fill the dictionary
summary = trace_dict[len(trace_dict)-1].rstrip("\n") summary = ""
module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n") if len(trace_list) >= 1:
summary = trace_list[len(trace_list)-1].rstrip("\n")
module = [""]
if len(trace_list) >= 2:
module = trace_list[len(trace_list)-2].rstrip("\n").split("\n")
module_split = module[0].split(", ") module_split = module[0].split(", ")
filepath = module_split[0].split("\"")[1]
filepath_directory_split = module_split[0].split("\"")
filepath = ""
if len(filepath_directory_split) > 1:
filepath = filepath_directory_split[1]
directory, filename = os.path.split(filepath) directory, filename = os.path.split(filepath)
line = int(module_split[1].lstrip("line ")) line = ""
function = module_split[2].lstrip("in ") if len(module_split) > 1:
code = module[1].lstrip(" ") line = int(module_split[1].lstrip("line "))
function = ""
if len(module_split) > 2:
function = module_split[2].lstrip("in ")
code = ""
if len(module) > 1:
code = module[1].lstrip(" ")
# Using this workaround for a cross-platform path splitting # Using this workaround for a cross-platform path splitting
split_path = [] split_path = []
@ -188,7 +331,7 @@ class CrashHandler:
json_metadata_file = os.path.join(directory, "plugin.json") json_metadata_file = os.path.join(directory, "plugin.json")
try: try:
with open(json_metadata_file, "r") as f: with open(json_metadata_file, "r", encoding = "utf-8") as f:
try: try:
metadata = json.loads(f.read()) metadata = json.loads(f.read())
module_version = metadata["version"] module_version = metadata["version"]
@ -216,9 +359,9 @@ class CrashHandler:
text_area = QTextEdit() text_area = QTextEdit()
tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True) tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
os.close(tmp_file_fd) os.close(tmp_file_fd)
with open(tmp_file_path, "w") as f: with open(tmp_file_path, "w", encoding = "utf-8") as f:
faulthandler.dump_traceback(f, all_threads=True) faulthandler.dump_traceback(f, all_threads=True)
with open(tmp_file_path, "r") as f: with open(tmp_file_path, "r", encoding = "utf-8") as f:
logdata = f.read() logdata = f.read()
text_area.setText(logdata) text_area.setText(logdata)
@ -248,9 +391,13 @@ class CrashHandler:
def _buttonsWidget(self): def _buttonsWidget(self):
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close) buttons.addButton(QDialogButtonBox.Close)
buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole) # Like above, this will be served as a separate detailed report dialog if the application has not yet been
# fully loaded. In this case, "send report" will be a check box in the early crash dialog, so there is no
# need for this extra button.
if self.has_started:
buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
buttons.accepted.connect(self._sendCrashReport)
buttons.rejected.connect(self.dialog.close) buttons.rejected.connect(self.dialog.close)
buttons.accepted.connect(self._sendCrashReport)
return buttons return buttons
@ -268,15 +415,23 @@ class CrashHandler:
kwoptions["context"] = ssl._create_unverified_context() kwoptions["context"] = ssl._create_unverified_context()
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
if not self.has_started:
print("Sending crash report info to [%s]...\n" % self.crash_url)
try: try:
f = urllib.request.urlopen(self.crash_url, **kwoptions) f = urllib.request.urlopen(self.crash_url, **kwoptions)
Logger.log("i", "Sent crash report info.") Logger.log("i", "Sent crash report info.")
if not self.has_started:
print("Sent crash report info.\n")
f.close() f.close()
except urllib.error.HTTPError: except urllib.error.HTTPError as e:
Logger.logException("e", "An HTTP error occurred while trying to send crash report") Logger.logException("e", "An HTTP error occurred while trying to send crash report")
except Exception: # We don't want any exception to cause problems if not self.has_started:
print("An HTTP error occurred while trying to send crash report: %s" % e)
except Exception as e: # We don't want any exception to cause problems
Logger.logException("e", "An exception occurred while trying to send crash report") Logger.logException("e", "An exception occurred while trying to send crash report")
if not self.has_started:
print("An exception occurred while trying to send crash report: %s" % e)
os._exit(1) os._exit(1)
@ -288,4 +443,4 @@ class CrashHandler:
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it # When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
if self.dialog: if self.dialog:
self.dialog.exec_() self.dialog.exec_()
os._exit(1) os._exit(1)

View file

@ -13,12 +13,18 @@ from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation
from cura.SetParentOperation import SetParentOperation from cura.Operations.SetParentOperation import SetParentOperation
from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
from UM.Logger import Logger
class CuraActions(QObject): class CuraActions(QObject):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
@ -54,7 +60,11 @@ class CuraActions(QObject):
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
current_node = current_node.getParent() current_node = current_node.getParent()
center_operation = SetTransformOperation(current_node, Vector()) # This was formerly done with SetTransformOperation but because of
# unpredictable matrix deconstruction it was possible that mirrors
# could manifest as rotations. Centering is therefore done by
# moving the node to negative whatever its position is:
center_operation = TranslateOperation(current_node, -current_node._position)
operation.addOperation(center_operation) operation.addOperation(center_operation)
operation.push() operation.push()
@ -63,7 +73,7 @@ class CuraActions(QObject):
# \param count The number of times to multiply the selection. # \param count The number of times to multiply the selection.
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8) job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
job.start() job.start()
## Delete all selected objects. ## Delete all selected objects.
@ -84,6 +94,10 @@ class CuraActions(QObject):
removed_group_nodes.append(group_node) removed_group_nodes.append(group_node)
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent())) op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node)) op.addOperation(RemoveSceneNodeOperation(group_node))
# Reset the print information
Application.getInstance().getController().getScene().sceneChanged.emit(node)
op.push() op.push()
## Set the extruder that should be used to print the selection. ## Set the extruder that should be used to print the selection.
@ -124,5 +138,31 @@ class CuraActions(QObject):
operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) operation.addOperation(SetObjectExtruderOperation(node, extruder_id))
operation.push() operation.push()
@pyqtSlot(int)
def setBuildPlateForSelection(self, build_plate_nr: int) -> None:
Logger.log("d", "Setting build plate number... %d" % build_plate_nr)
operation = GroupedOperation()
root = Application.getInstance().getController().getScene().getRoot()
nodes_to_change = []
for node in Selection.getAllSelectedObjects():
parent_node = node # Find the parent node to change instead
while parent_node.getParent() != root:
parent_node = parent_node.getParent()
for single_node in BreadthFirstIterator(parent_node):
nodes_to_change.append(single_node)
if not nodes_to_change:
Logger.log("d", "Nothing to change.")
return
for node in nodes_to_change:
operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr))
operation.push()
Selection.clear()
def _openUrl(self, url): def _openUrl(self, url):
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from collections import OrderedDict
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer
##
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
#
# ContainerNode is a multi-purpose class. It has two main purposes:
# 1. It encapsulates an InstanceContainer. It contains that InstanceContainer's
# - metadata (Always)
# - container (lazy-loaded when needed)
# 2. It also serves as a node in a hierarchical InstanceContainer lookup table/tree.
# This is used in Variant, Material, and Quality Managers.
#
class ContainerNode:
__slots__ = ("metadata", "container", "children_map")
def __init__(self, metadata: Optional[dict] = None):
self.metadata = metadata
self.container = None
self.children_map = OrderedDict()
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_key)
def getContainer(self) -> "InstanceContainer":
if self.metadata is None:
raise RuntimeError("Cannot get container for a ContainerNode without metadata")
if self.container is None:
container_id = self.metadata["id"]
Logger.log("i", "Lazy-loading container [%s]", container_id)
from UM.Settings.ContainerRegistry import ContainerRegistry
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not container_list:
raise RuntimeError("Failed to lazy-load container [%s], cannot find it" % container_id)
self.container = container_list[0]
return self.container
def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.metadata.get("id"))

View file

@ -0,0 +1,26 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
#
# A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
#
# Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information:
# - name: "generic_abs", root_material_id
# - root_material_node: MaterialNode of "generic_abs"
# - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs",
# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
#
class MaterialGroup:
__slots__ = ("name", "root_material_node", "derived_material_node_list")
def __init__(self, name: str):
self.name = name
self.root_material_node = None
self.derived_material_node_list = []
def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.name)

View file

@ -0,0 +1,479 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict, OrderedDict
import copy
import uuid
from typing import Optional, TYPE_CHECKING
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
from UM.Application import Application
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction
from UM.Util import parseBool
from .MaterialNode import MaterialNode
from .MaterialGroup import MaterialGroup
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
#
# MaterialManager maintains a number of maps and trees for material lookup.
# The models GUI and QML use are now only dependent on the MaterialManager. That means as long as the data in
# MaterialManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
#
# For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
# again. This means the update is exactly the same as initialization. There are performance concerns about this approach
# but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
# because it's simple.
#
class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
def __init__(self, container_registry, parent = None):
super().__init__(parent)
self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry
self._fallback_materials_map = dict() # material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup
self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175
self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter
self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla)
# This is used in Legacy UM3 send material function and the material management page.
self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups
# The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found.
self._default_machine_definition_id = "fdmprinter"
self._default_approximate_diameter_for_quality_search = "3"
# When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
# want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
# react too many time.
self._update_timer = QTimer(self)
self._update_timer.setInterval(300)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps)
self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
def initialize(self):
# Find all materials and put them in a matrix for quick search.
material_metadata_list = self._container_registry.findContainersMetadata(type = "material")
self._material_group_map = dict()
# Map #1
# root_material_id -> MaterialGroup
for material_metadata in material_metadata_list:
material_id = material_metadata["id"]
# We don't store empty material in the lookup tables
if material_id == "empty_material":
continue
root_material_id = material_metadata.get("base_file")
if root_material_id not in self._material_group_map:
self._material_group_map[root_material_id] = MaterialGroup(root_material_id)
group = self._material_group_map[root_material_id]
# We only add root materials here
if material_id == root_material_id:
group.root_material_node = MaterialNode(material_metadata)
else:
new_node = MaterialNode(material_metadata)
group.derived_material_node_list.append(new_node)
# Order this map alphabetically so it's easier to navigate in a debugger
self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
# Map #1.5
# GUID -> material group list
self._guid_material_groups_map = defaultdict(list)
for root_material_id, material_group in self._material_group_map.items():
guid = material_group.root_material_node.metadata["GUID"]
self._guid_material_groups_map[guid].append(material_group)
# Map #2
# Lookup table for material type -> fallback material metadata, only for read-only materials
grouped_by_type_dict = dict()
for root_material_id, material_node in self._material_group_map.items():
if not self._container_registry.isReadOnly(root_material_id):
continue
material_type = material_node.root_material_node.metadata["material"]
if material_type not in grouped_by_type_dict:
grouped_by_type_dict[material_type] = {"generic": None,
"others": []}
brand = material_node.root_material_node.metadata["brand"]
if brand.lower() == "generic":
to_add = True
if material_type in grouped_by_type_dict:
diameter = material_node.root_material_node.metadata.get("approximate_diameter")
if diameter != self._default_approximate_diameter_for_quality_search:
to_add = False # don't add if it's not the default diameter
if to_add:
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
self._fallback_materials_map = grouped_by_type_dict
# Map #3
# There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
# and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
# be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
# for quality search.
self._material_diameter_map = defaultdict(dict)
self._diameter_material_map = dict()
# Group the material IDs by the same name, material, brand, and color but with different diameters.
material_group_dict = dict()
keys_to_fetch = ("name", "material", "brand", "color")
for root_material_id, machine_node in self._material_group_map.items():
if not self._container_registry.isReadOnly(root_material_id):
continue
root_material_metadata = machine_node.root_material_node.metadata
key_data = []
for key in keys_to_fetch:
key_data.append(root_material_metadata.get(key))
key_data = tuple(key_data)
if key_data not in material_group_dict:
material_group_dict[key_data] = dict()
approximate_diameter = root_material_metadata.get("approximate_diameter")
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
# Map [root_material_id][diameter] -> root_material_id for this diameter
for data_dict in material_group_dict.values():
for root_material_id1 in data_dict.values():
if root_material_id1 in self._material_diameter_map:
continue
diameter_map = data_dict
for root_material_id2 in data_dict.values():
self._material_diameter_map[root_material_id2] = diameter_map
default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
if default_root_material_id is None:
default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one
for root_material_id in data_dict.values():
self._diameter_material_map[root_material_id] = default_root_material_id
# Map #4
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
self._diameter_machine_variant_material_map = dict()
for material_metadata in material_metadata_list:
# We don't store empty material in the lookup tables
if material_metadata["id"] == "empty_material":
continue
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"]
if approximate_diameter not in self._diameter_machine_variant_material_map:
self._diameter_machine_variant_material_map[approximate_diameter] = {}
machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
if definition not in machine_variant_material_map:
machine_variant_material_map[definition] = MaterialNode()
machine_node = machine_variant_material_map[definition]
variant_name = material_metadata.get("variant_name")
if not variant_name:
# if there is no variant, this material is for the machine, so put its metadata in the machine node.
machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# this material is variant-specific, so we save it in a variant-specific node under the
# machine-specific node
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = MaterialNode()
variant_node = machine_node.children_map[variant_name]
if root_material_id not in variant_node.material_map:
variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# Sanity check: make sure we don't have duplicated variant-specific materials for the same machine
raise RuntimeError("Found duplicate variant name [%s] for machine [%s] in material [%s]" %
(variant_name, definition, material_metadata["id"]))
self.materialsUpdated.emit()
def _updateMaps(self):
self.initialize()
def _onContainerMetadataChanged(self, container):
self._onContainerChanged(container)
def _onContainerChanged(self, container):
container_type = container.getMetaDataEntry("type")
if container_type != "material":
return
# update the maps
self._update_timer.start()
def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
return self._material_group_map.get(root_material_id)
def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
return self._material_diameter_map.get(root_material_id).get(approximate_diameter, root_material_id)
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id)
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
return self._guid_material_groups_map.get(guid)
#
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
#
def getAvailableMaterials(self, machine_definition_id: str, extruder_variant_name: Optional[str],
diameter: float) -> dict:
# round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_variant_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
return dict()
# If there are variant materials, get the variant material
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
machine_node = machine_variant_material_map.get(machine_definition_id)
default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
variant_node = None
if extruder_variant_name is not None and machine_node is not None:
variant_node = machine_node.getChildNode(extruder_variant_name)
nodes_to_check = [variant_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials:
# 1. variant-specific material
# 2. machine-specific material
# 3. generic material (for fdmprinter)
material_id_metadata_dict = dict()
for node in nodes_to_check:
if node is not None:
for material_id, node in node.material_map.items():
if material_id not in material_id_metadata_dict:
material_id_metadata_dict[material_id] = node
return material_id_metadata_dict
#
# A convenience function to get available materials for the given machine with the extruder position.
#
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[dict]:
machine_definition_id = machine.definition.getId()
variant_name = None
if extruder_stack.variant.getId() != "empty_variant":
variant_name = extruder_stack.variant.getName()
diameter = extruder_stack.approximateMaterialDiameter
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine_definition_id, variant_name, diameter)
#
# Gets MaterialNode for the given extruder and machine with the given material name.
# Returns None if:
# 1. the given machine doesn't have materials;
# 2. cannot find any material InstanceContainers with the given settings.
#
def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str],
diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
# round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_variant_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
diameter, rounded_diameter, root_material_id)
return None
# If there are variant materials, get the variant material
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
machine_node = machine_variant_material_map.get(machine_definition_id)
variant_node = None
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
if machine_node is None:
machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
if machine_node is not None and extruder_variant_name is not None:
variant_node = machine_node.getChildNode(extruder_variant_name)
# Fallback mechanism of finding materials:
# 1. variant-specific material
# 2. machine-specific material
# 3. generic material (for fdmprinter)
nodes_to_check = [variant_node, machine_node,
machine_variant_material_map.get(self._default_machine_definition_id)]
material_node = None
for node in nodes_to_check:
if node is not None:
material_node = node.material_map.get(root_material_id)
if material_node:
break
return material_node
#
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
# the generic material IDs to search for qualities.
#
# An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
# extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
# A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
# be "generic_pla". This function is intended to get a generic fallback material for the given material type.
#
# This function returns the generic root material ID for the given material type, where material types are "PLA",
# "ABS", etc.
#
def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
# For safety
if material_type not in self._fallback_materials_map:
Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
return None
fallback_material = self._fallback_materials_map[material_type]
if fallback_material:
return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
else:
return None
def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: str) -> Optional["MaterialNode"]:
node = None
machine_definition = global_stack.definition
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
material_diameter = machine_definition.getProperty("material_diameter", "value")
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(global_stack)
approximate_material_diameter = str(round(material_diameter))
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
material_diameter, root_material_id)
return node
#
# Methods for GUI
#
#
# Sets the new name for the given material.
#
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str):
root_material_id = material_node.metadata["base_file"]
if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
return
material_group = self.getMaterialGroup(root_material_id)
material_group.root_material_node.getContainer().setName(name)
#
# Removes the given material.
#
@pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode"):
root_material_id = material_node.metadata["base_file"]
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("d", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
return
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
for node in nodes_to_remove:
self._container_registry.removeContainer(node.metadata["id"])
#
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
# Returns the root material ID of the duplicated material if successful.
#
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]:
root_material_id = material_node.metadata["base_file"]
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
return None
base_container = material_group.root_material_node.getContainer()
# Ensure all settings are saved.
self._application.saveSettings()
# Create a new ID & container to hold the data.
new_containers = []
if new_base_id is None:
new_base_id = self._container_registry.uniqueName(base_container.getId())
new_base_container = copy.deepcopy(base_container)
new_base_container.getMetaData()["id"] = new_base_id
new_base_container.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
for key, value in new_metadata.items():
new_base_container.getMetaData()[key] = value
new_containers.append(new_base_container)
# Clone all of them.
for node in material_group.derived_material_node_list:
container_to_copy = node.getContainer()
# Create unique IDs for every clone.
new_id = new_base_id
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant_name"):
variant_name = container_to_copy.getMetaDataEntry("variant_name")
new_id += "_" + variant_name.replace(" ", "_")
new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id
new_container.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
for key, value in new_metadata.items():
new_container.getMetaData()[key] = value
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
self._container_registry.addContainer(container_to_add)
return new_base_id
#
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
#
@pyqtSlot(result = str)
def createMaterial(self) -> str:
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# Ensure all settings are saved.
self._application.saveSettings()
global_stack = self._application.getGlobalContainerStack()
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
root_material_id = "generic_pla"
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
material_group = self.getMaterialGroup(root_material_id)
# Create a new ID & container to hold the data.
new_id = self._container_registry.uniqueName("custom_material")
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
"brand": catalog.i18nc("@label", "Custom"),
"GUID": str(uuid.uuid4()),
}
self.duplicateMaterial(material_group.root_material_node,
new_base_id = new_id,
new_metadata = new_metadata)
return new_id

View file

@ -0,0 +1,21 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from .ContainerNode import ContainerNode
#
# A MaterialNode is a node in the material lookup tree/map/table. It contains 2 (extra) fields:
# - material_map: a one-to-one map of "material_root_id" to material_node.
# - children_map: the key-value map for child nodes of this node. This is used in a lookup tree.
#
#
class MaterialNode(ContainerNode):
__slots__ = ("material_map", "children_map")
def __init__(self, metadata: Optional[dict] = None):
super().__init__(metadata = metadata)
self.material_map = {} # material_root_id -> material_node
self.children_map = {} # mapping for the child nodes

View file

@ -0,0 +1,46 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
#
# This is the base model class for GenericMaterialsModel and BrandMaterialsModel
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
#
class BaseMaterialsModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
IdRole = Qt.UserRole + 2
NameRole = Qt.UserRole + 3
BrandRole = Qt.UserRole + 4
MaterialRole = Qt.UserRole + 5
ColorRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialRole, "material")
self.addRoleName(self.ColorRole, "color_name")
self.addRoleName(self.ContainerNodeRole, "container_node")
self._extruder_position = 0
def setExtruderPosition(self, position: int):
if self._extruder_position != position:
self._extruder_position = position
self.extruderPositionChanged.emit()
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
def extruderPosition(self) -> int:
return self._extruder_positoin

View file

@ -0,0 +1,131 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
from .BaseMaterialsModel import BaseMaterialsModel
#
# This is an intermediate model to group materials with different colours for a same brand and type.
#
class MaterialsModelGroupedByType(ListModel):
NameRole = Qt.UserRole + 1
ColorsRole = Qt.UserRole + 2
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ColorsRole, "colors")
#
# This model is used to show branded materials in the material drop down menu.
# The structure of the menu looks like this:
# Brand -> Material Type -> list of materials
#
# To illustrate, a branded material menu may look like this:
# Ultimaker -> PLA -> Yellow PLA
# -> Black PLA
# -> ...
# -> ABS -> White ABS
# ...
#
class BrandMaterialsModel(ListModel):
NameRole = Qt.UserRole + 1
MaterialsRole = Qt.UserRole + 2
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.MaterialsRole, "materials")
self._extruder_position = 0
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._extruder_manager.activeExtruderChanged.connect(self._update)
self._material_manager.materialsUpdated.connect(self._update)
self._update()
def setExtruderPosition(self, position: int):
if self._extruder_position != position:
self._extruder_position = position
self.extruderPositionChanged.emit()
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
def extruderPosition(self) -> int:
return self._extruder_position
def _update(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[str(self._extruder_position)]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
extruder_stack)
if available_material_dict is None:
self.setItems([])
return
brand_item_list = []
brand_group_dict = {}
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
brand = metadata["brand"]
# Only add results for generic materials
if brand.lower() == "generic":
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
material_type = metadata["material"]
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
item = {"root_material_id": root_material_id,
"id": metadata["id"],
"name": metadata["name"],
"brand": metadata["brand"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"container_node": container_node
}
brand_group_dict[brand][material_type].append(item)
for brand, material_dict in brand_group_dict.items():
brand_item = {"name": brand,
"materials": MaterialsModelGroupedByType(self)}
material_type_item_list = []
for material_type, material_list in material_dict.items():
material_type_item = {"name": material_type,
"colors": BaseMaterialsModel(self)}
material_type_item["colors"].clear()
material_type_item["colors"].setItems(material_list)
material_type_item_list.append(material_type_item)
brand_item["materials"].setItems(material_type_item_list)
brand_item_list.append(brand_item)
self.setItems(brand_item_list)

View file

@ -0,0 +1,49 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Util import parseBool
from cura.Machines.VariantManager import VariantType
class BuildPlateModel(ListModel):
NameRole = Qt.UserRole + 1
ContainerNodeRole = Qt.UserRole + 2
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ContainerNodeRole, "container_node")
self._application = Application.getInstance()
self._variant_manager = self._application._variant_manager
self._machine_manager = self._application.getMachineManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._update()
def _update(self):
global_stack = self._machine_manager._global_container_stack
if not global_stack:
self.setItems([])
return
has_variants = parseBool(global_stack.getMetaDataEntry("has_variant_buildplates", False))
if not has_variants:
self.setItems([])
return
variant_dict = self._variant_manager.getVariantNodes(global_stack, variant_type = VariantType.BUILD_PLATE)
item_list = []
for name, variant_node in variant_dict.items():
item = {"name": name,
"container_node": variant_node}
item_list.append(item)
self.setItems(item_list)

View file

@ -0,0 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
#
# This model is used for the custom profile items in the profile drop down menu.
#
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
def _update(self):
Logger.log("d", "Updating %s ...", self.__class__.__name__)
active_global_stack = self._machine_manager.activeMachine
if active_global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
return
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack)
item_list = []
for key in sorted(quality_changes_group_dict):
quality_changes_group = quality_changes_group_dict[key]
item = {"name": quality_changes_group.name,
"layer_height": "",
"layer_height_without_unit": "",
"available": quality_changes_group.is_available,
"quality_changes_group": quality_changes_group}
item_list.append(item)
self.setItems(item_list)

View file

@ -0,0 +1,60 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .BaseMaterialsModel import BaseMaterialsModel
class GenericMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None):
super().__init__(parent)
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._extruder_manager.activeExtruderChanged.connect(self._update)
self._material_manager.materialsUpdated.connect(self._update)
self._update()
def _update(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[extruder_position]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
extruder_stack)
if available_material_dict is None:
self.setItems([])
return
item_list = []
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
# Only add results for generic materials
if metadata["brand"].lower() != "generic":
continue
item = {"root_material_id": root_material_id,
"id": metadata["id"],
"name": metadata["name"],
"brand": metadata["brand"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"container_node": container_node
}
item_list.append(item)
# Sort the item list by material name alphabetically
item_list = sorted(item_list, key = lambda d: d["name"])
self.setItems(item_list)

View file

@ -0,0 +1,101 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtProperty
from UM.Qt.ListModel import ListModel
#
# This model is for the Material management page.
#
class MaterialManagementModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
DisplayNameRole = Qt.UserRole + 2
BrandRole = Qt.UserRole + 3
MaterialTypeRole = Qt.UserRole + 4
ColorNameRole = Qt.UserRole + 5
ColorCodeRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
ContainerIdRole = Qt.UserRole + 8
DescriptionRole = Qt.UserRole + 9
AdhesionInfoRole = Qt.UserRole + 10
ApproximateDiameterRole = Qt.UserRole + 11
GuidRole = Qt.UserRole + 12
DensityRole = Qt.UserRole + 13
DiameterRole = Qt.UserRole + 14
IsReadOnlyRole = Qt.UserRole + 15
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
self.addRoleName(self.DisplayNameRole, "name")
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialTypeRole, "material")
self.addRoleName(self.ColorNameRole, "color_name")
self.addRoleName(self.ColorCodeRole, "color_code")
self.addRoleName(self.ContainerNodeRole, "container_node")
self.addRoleName(self.ContainerIdRole, "container_id")
self.addRoleName(self.DescriptionRole, "description")
self.addRoleName(self.AdhesionInfoRole, "adhesion_info")
self.addRoleName(self.ApproximateDiameterRole, "approximate_diameter")
self.addRoleName(self.GuidRole, "guid")
self.addRoleName(self.DensityRole, "density")
self.addRoleName(self.DiameterRole, "diameter")
self.addRoleName(self.IsReadOnlyRole, "is_read_only")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._extruder_manager.activeExtruderChanged.connect(self._update)
self._material_manager.materialsUpdated.connect(self._update)
self._update()
def _update(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
active_extruder_stack = self._machine_manager.activeStack
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
active_extruder_stack)
if available_material_dict is None:
self.setItems([])
return
material_list = []
for root_material_id, container_node in available_material_dict.items():
keys_to_fetch = ("name",
"brand",
"material",
"color_name",
"color_code",
"description",
"adhesion_info",
"approximate_diameter",)
item = {"root_material_id": container_node.metadata["base_file"],
"container_node": container_node,
"guid": container_node.metadata["GUID"],
"container_id": container_node.metadata["id"],
"density": container_node.metadata.get("properties", {}).get("density", ""),
"diameter": container_node.metadata.get("properties", {}).get("diameter", ""),
"is_read_only": self._container_registry.isReadOnly(container_node.metadata["id"]),
}
for key in keys_to_fetch:
item[key] = container_node.metadata.get(key, "")
material_list.append(item)
material_list = sorted(material_list, key = lambda k: (k["brand"].lower(), k["name"]))
self.setItems(material_list)

View file

@ -0,0 +1,57 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty
from UM.Application import Application
from UM.Scene.Selection import Selection
from UM.Qt.ListModel import ListModel
#
# This is the model for multi build plate feature.
# This has nothing to do with the build plate types you can choose on the sidebar for a machine.
#
class MultiBuildPlateModel(ListModel):
maxBuildPlateChanged = pyqtSignal()
activeBuildPlateChanged = pyqtSignal()
selectionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self._application = Application.getInstance()
self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
self._max_build_plate = 1 # default
self._active_build_plate = -1
def setMaxBuildPlate(self, max_build_plate):
self._max_build_plate = max_build_plate
self.maxBuildPlateChanged.emit()
## Return the highest build plate number
@pyqtProperty(int, notify = maxBuildPlateChanged)
def maxBuildPlate(self):
return self._max_build_plate
def setActiveBuildPlate(self, nr):
self._active_build_plate = nr
self.activeBuildPlateChanged.emit()
@pyqtProperty(int, notify = activeBuildPlateChanged)
def activeBuildPlate(self):
return self._active_build_plate
def _updateSelectedObjectBuildPlateNumbers(self, *args):
result = set()
for node in Selection.getAllSelectedObjects():
result.add(node.callDecoration("getBuildPlateNumber"))
self._selection_build_plates = list(result)
self.selectionChanged.emit()
@pyqtProperty("QVariantList", notify = selectionChanged)
def selectionBuildPlates(self):
return self._selection_build_plates

View file

@ -0,0 +1,56 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Util import parseBool
class NozzleModel(ListModel):
IdRole = Qt.UserRole + 1
HotendNameRole = Qt.UserRole + 2
ContainerNodeRole = Qt.UserRole + 3
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.HotendNameRole, "hotend_name")
self.addRoleName(self.ContainerNodeRole, "container_node")
Application.getInstance().globalContainerStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
def _update(self):
self.items.clear()
variant_manager = Application.getInstance()._variant_manager
active_global_stack = Application.getInstance().getMachineManager()._global_container_stack
if active_global_stack is None:
self.setItems([])
return
has_variants = parseBool(active_global_stack.getMetaDataEntry("has_variants", False))
if not has_variants:
self.setItems([])
return
variant_node_dict = variant_manager.getVariantNodes(active_global_stack)
if not variant_node_dict:
self.setItems([])
return
item_list = []
for hotend_name, container_node in sorted(variant_node_dict.items(), key = lambda i: i[0]):
item = {"id": hotend_name,
"hotend_name": hotend_name,
"container_node": container_node
}
item_list.append(item)
self.setItems(item_list)

View file

@ -0,0 +1,122 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSlot
from UM.Qt.ListModel import ListModel
#
# This the QML model for the quality management page.
#
class QualityManagementModel(ListModel):
NameRole = Qt.UserRole + 1
IsReadOnlyRole = Qt.UserRole + 2
QualityGroupRole = Qt.UserRole + 3
QualityChangesGroupRole = Qt.UserRole + 4
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IsReadOnlyRole, "is_read_only")
self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._quality_manager = CuraApplication.getInstance().getQualityManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._quality_manager.qualitiesUpdated.connect(self._update)
self._update()
def _update(self):
global_stack = self._machine_manager.activeMachine
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
if quality_group.is_available)
if not available_quality_types and not quality_changes_group_dict:
# Nothing to show
self.setItems([])
return
item_list = []
# Create quality group items
for quality_group in quality_group_dict.values():
if not quality_group.is_available:
continue
item = {"name": quality_group.name,
"is_read_only": True,
"quality_group": quality_group,
"quality_changes_group": None}
item_list.append(item)
# Sort by quality names
item_list = sorted(item_list, key = lambda x: x["name"])
# Create quality_changes group items
quality_changes_item_list = []
for quality_changes_group in quality_changes_group_dict.values():
if quality_changes_group.quality_type not in available_quality_types:
continue
quality_group = quality_group_dict[quality_changes_group.quality_type]
item = {"name": quality_changes_group.name,
"is_read_only": False,
"quality_group": quality_group,
"quality_changes_group": quality_changes_group}
quality_changes_item_list.append(item)
# Sort quality_changes items by names and append to the item list
quality_changes_item_list = sorted(quality_changes_item_list, key = lambda x: x["name"])
item_list += quality_changes_item_list
self.setItems(item_list)
# TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later.
#
## Gets a list of the possible file filters that the plugins have
# registered they can read or write. The convenience meta-filters
# "All Supported Types" and "All Files" are added when listing
# readers, but not when listing writers.
#
# \param io_type \type{str} name of the needed IO type
# \return A list of strings indicating file name filters for a file
# dialog.
@pyqtSlot(str, result = "QVariantList")
def getFileNameFilters(self, io_type):
from UM.i18n import i18nCatalog
catalog = i18nCatalog("uranium")
#TODO: This function should be in UM.Resources!
filters = []
all_types = []
for plugin_id, meta_data in self._getIOPlugins(io_type):
for io_plugin in meta_data[io_type]:
filters.append(io_plugin["description"] + " (*." + io_plugin["extension"] + ")")
all_types.append("*.{0}".format(io_plugin["extension"]))
if "_reader" in io_type:
# if we're listing readers, add the option to show all supported files as the default option
filters.insert(0, catalog.i18nc("@item:inlistbox", "All Supported Types ({0})", " ".join(all_types)))
filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers.
return filters
## Gets a list of profile reader or writer plugins
# \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type):
from UM.PluginRegistry import PluginRegistry
pr = PluginRegistry.getInstance()
active_plugin_ids = pr.getActivePlugins()
result = []
for plugin_id in active_plugin_ids:
meta_data = pr.getMetaData(plugin_id)
if io_type in meta_data:
result.append( (plugin_id, meta_data) )
return result

View file

@ -0,0 +1,106 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from cura.Machines.QualityManager import QualityGroup
#
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
#
class QualityProfilesDropDownMenuModel(ListModel):
NameRole = Qt.UserRole + 1
QualityTypeRole = Qt.UserRole + 2
LayerHeightRole = Qt.UserRole + 3
LayerHeightUnitRole = Qt.UserRole + 4
AvailableRole = Qt.UserRole + 5
QualityGroupRole = Qt.UserRole + 6
QualityChangesGroupRole = Qt.UserRole + 7
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.QualityTypeRole, "quality_type")
self.addRoleName(self.LayerHeightRole, "layer_height")
self.addRoleName(self.LayerHeightUnitRole, "layer_height_unit")
self.addRoleName(self.AvailableRole, "available")
self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self._quality_manager = Application.getInstance().getQualityManager()
self._application.globalContainerStackChanged.connect(self._update)
self._machine_manager.activeQualityGroupChanged.connect(self._update)
self._quality_manager.qualitiesUpdated.connect(self._update)
self._layer_height_unit = "" # This is cached
self._update()
def _update(self):
Logger.log("d", "Updating quality profile model ...")
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return
# Check for material compatibility
if not self._machine_manager.activeMaterialsCompatible():
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([])
return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
item_list = []
for key in sorted(quality_group_dict):
quality_group = quality_group_dict[key]
layer_height = self._fetchLayerHeight(quality_group)
item = {"name": quality_group.name,
"quality_type": quality_group.quality_type,
"layer_height": layer_height,
"layer_height_unit": self._layer_height_unit,
"available": quality_group.is_available,
"quality_group": quality_group}
item_list.append(item)
# Sort items based on layer_height
item_list = sorted(item_list, key = lambda x: x["layer_height"])
self.setItems(item_list)
def _fetchLayerHeight(self, quality_group: "QualityGroup"):
global_stack = self._machine_manager.activeMachine
if not self._layer_height_unit:
unit = global_stack.definition.getProperty("layer_height", "unit")
if not unit:
unit = ""
self._layer_height_unit = unit
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
# Get layer_height from the quality profile for the GlobalStack
container = quality_group.node_for_global.getContainer()
layer_height = default_layer_height
if container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
else:
# Look for layer_height in the GlobalStack from material -> definition
container = global_stack.definition
if container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
return float(layer_height)

View file

@ -0,0 +1,159 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Application import Application
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
#
# This model is used to show details settings of the selected quality in the quality management page.
#
class QualitySettingsModel(ListModel):
KeyRole = Qt.UserRole + 1
LabelRole = Qt.UserRole + 2
UnitRole = Qt.UserRole + 3
ProfileValueRole = Qt.UserRole + 4
ProfileValueSourceRole = Qt.UserRole + 5
UserValueRole = Qt.UserRole + 6
CategoryRole = Qt.UserRole + 7
def __init__(self, parent = None):
super().__init__(parent = parent)
self.addRoleName(self.KeyRole, "key")
self.addRoleName(self.LabelRole, "label")
self.addRoleName(self.UnitRole, "unit")
self.addRoleName(self.ProfileValueRole, "profile_value")
self.addRoleName(self.ProfileValueSourceRole, "profile_value_source")
self.addRoleName(self.UserValueRole, "user_value")
self.addRoleName(self.CategoryRole, "category")
self._container_registry = ContainerRegistry.getInstance()
self._application = Application.getInstance()
self._quality_manager = self._application.getQualityManager()
self._selected_position = "" # empty string means GlobalStack
# strings such as "0", "1", etc. mean extruder positions
self._selected_quality_item = None # The selected quality in the quality management page
self._i18n_catalog = None
self._quality_manager.qualitiesUpdated.connect(self._update)
self._update()
selectedPositionChanged = pyqtSignal()
selectedQualityItemChanged = pyqtSignal()
def setSelectedPosition(self, selected_position):
if selected_position != self._selected_position:
self._selected_position = selected_position
self.selectedPositionChanged.emit()
self._update()
@pyqtProperty(str, fset = setSelectedPosition, notify = selectedPositionChanged)
def selectedPosition(self):
return self._selected_position
def setSelectedQualityItem(self, selected_quality_item):
if selected_quality_item != self._selected_quality_item:
self._selected_quality_item = selected_quality_item
self.selectedQualityItemChanged.emit()
self._update()
@pyqtProperty("QVariantMap", fset = setSelectedQualityItem, notify = selectedQualityItemChanged)
def selectedQualityItem(self):
return self._selected_quality_item
def _update(self):
if self._selected_quality_item is None:
self.setItems([])
return
items = []
global_container_stack = self._application.getGlobalContainerStack()
definition_container = global_container_stack.definition
quality_group = self._selected_quality_item["quality_group"]
quality_changes_group = self._selected_quality_item["quality_changes_group"]
if self._selected_position == "":
quality_node = quality_group.node_for_global
else:
quality_node = quality_group.nodes_for_extruders.get(self._selected_position)
settings_keys = quality_group.getAllKeys()
quality_containers = [quality_node.getContainer()]
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
# the settings in that quality_changes_group.
if quality_changes_group is not None:
if self._selected_position == "":
quality_changes_node = quality_changes_group.node_for_global
else:
quality_changes_node = quality_changes_group.nodes_for_extruders.get(self._selected_position)
if quality_changes_node is not None: # it can be None if number of extruders are changed during runtime
try:
quality_containers.insert(0, quality_changes_node.getContainer())
except:
# FIXME: This is to prevent incomplete update of QualityManager
Logger.logException("d", "Failed to get container for quality changes node %s", quality_changes_node)
return
settings_keys.update(quality_changes_group.getAllKeys())
# We iterate over all definitions instead of settings in a quality/qualtiy_changes group is because in the GUI,
# the settings are grouped together by categories, and we had to go over all the definitions to figure out
# which setting belongs in which category.
current_category = ""
for definition in definition_container.findDefinitions():
if definition.type == "category":
current_category = definition.label
if self._i18n_catalog:
current_category = self._i18n_catalog.i18nc(definition.key + " label", definition.label)
continue
profile_value = None
profile_value_source = ""
for quality_container in quality_containers:
new_value = quality_container.getProperty(definition.key, "value")
if new_value is not None:
profile_value_source = quality_container.getMetaDataEntry("type")
profile_value = new_value
# Global tab should use resolve (if there is one)
if self._selected_position == "":
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
if resolve_value is not None and definition.key in settings_keys:
profile_value = resolve_value
if profile_value is not None:
break
if not self._selected_position:
user_value = global_container_stack.userChanges.getProperty(definition.key, "value")
else:
extruder_stack = global_container_stack.extruders[self._selected_position]
user_value = extruder_stack.userChanges.getProperty(definition.key, "value")
if profile_value is None and user_value is None:
continue
label = definition.label
if self._i18n_catalog:
label = self._i18n_catalog.i18nc(definition.key + " label", label)
items.append({
"key": definition.key,
"label": label,
"unit": definition.unit,
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
"profile_value_source": profile_value_source,
"user_value": "" if user_value is None else str(user_value),
"category": current_category
})
self.setItems(items)

View file

View file

@ -0,0 +1,53 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from .QualityGroup import QualityGroup
class QualityChangesGroup(QualityGroup):
def __init__(self, name: str, quality_type: str, parent = None):
super().__init__(name, quality_type, parent)
self._container_registry = Application.getInstance().getContainerRegistry()
def addNode(self, node: "QualityNode"):
# TODO: in 3.2 and earlier, a quality_changes container may have a field called "extruder" which contains the
# extruder definition ID it belongs to. But, in fact, we only need to know the following things:
# 1. which machine a custom profile is suitable for,
# 2. if this profile is for the GlobalStack,
# 3. if this profile is for an ExtruderStack and which one (the position).
#
# So, it is preferred to have a field like this:
# extruder_position = 1
# instead of this:
# extruder = custom_extruder_1
#
# An upgrade needs to be done if we want to do it this way. Before that, we use the extruder's definition
# to figure out its position.
#
extruder_definition_id = node.metadata.get("extruder")
if extruder_definition_id:
metadata_list = self._container_registry.findDefinitionContainersMetadata(id = extruder_definition_id)
if not metadata_list:
raise RuntimeError("%s cannot get metadata for extruder definition [%s]" %
(self, extruder_definition_id))
extruder_definition_metadata = metadata_list[0]
extruder_position = str(extruder_definition_metadata["position"])
if extruder_position in self.nodes_for_extruders:
raise RuntimeError("%s tries to overwrite the existing nodes_for_extruders position [%s] %s with %s" %
(self, extruder_position, self.node_for_global, node))
self.nodes_for_extruders[extruder_position] = node
else:
# This is a quality_changes for the GlobalStack
if self.node_for_global is not None:
raise RuntimeError("%s tries to overwrite the existing node_for_global %s with %s" %
(self, self.node_for_global, node))
self.node_for_global = node
def __str__(self) -> str:
return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)

View file

@ -0,0 +1,50 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, List
from PyQt5.QtCore import QObject, pyqtSlot
#
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
# must be applied to all stacks in a machine, although each stack can have different containers. Use an Ultimaker 3
# as an example, suppose we choose quality type "normal", the actual InstanceContainers on each stack may look
# as below:
# GlobalStack ExtruderStack 1 ExtruderStack 2
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
#
# This QualityGroup is mainly used in quality and quality_changes to group the containers that can be applied to
# a machine, so when a quality/custom quality is selected, the container can be directly applied to each stack instead
# of looking them up again.
#
class QualityGroup(QObject):
def __init__(self, name: str, quality_type: str, parent = None):
super().__init__(parent)
self.name = name
self.node_for_global = None # type: Optional["QualityGroup"]
self.nodes_for_extruders = dict() # position str -> QualityGroup
self.quality_type = quality_type
self.is_available = False
@pyqtSlot(result = str)
def getName(self) -> str:
return self.name
def getAllKeys(self) -> set:
result = set()
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None:
continue
result.update(node.getContainer().getAllKeys())
return result
def getAllNodes(self) -> List["QualityGroup"]:
result = []
if self.node_for_global is not None:
result.append(self.node_for_global)
for extruder_node in self.nodes_for_extruders.values():
result.append(extruder_node)
return result

View file

@ -0,0 +1,491 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
from UM.Application import Application
from UM.Logger import Logger
from UM.Util import parseBool
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.ExtruderStack import ExtruderStack
from .QualityGroup import QualityGroup
from .QualityNode import QualityNode
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup
#
# Similar to MaterialManager, QualityManager maintains a number of maps and trees for quality profile lookup.
# The models GUI and QML use are now only dependent on the QualityManager. That means as long as the data in
# QualityManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
#
# For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
# again. This means the update is exactly the same as initialization. There are performance concerns about this approach
# but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
# because it's simple.
#
class QualityManager(QObject):
qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry, parent = None):
super().__init__(parent)
self._application = Application.getInstance()
self._material_manager = self._application.getMaterialManager()
self._container_registry = container_registry
self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
self._default_machine_definition_id = "fdmprinter"
self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
# When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
# we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
# we don't react too many time.
self._update_timer = QTimer(self)
self._update_timer.setInterval(300)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps)
def initialize(self):
# Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <variant> -> <material>
# -> <material>
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
for metadata in quality_metadata_list:
if metadata["id"] == "empty_quality":
continue
definition_id = metadata["definition"]
quality_type = metadata["quality_type"]
root_material_id = metadata.get("material")
variant_name = metadata.get("variant")
is_global_quality = metadata.get("global_quality", False)
is_global_quality = is_global_quality or (root_material_id is None and variant_name is None)
# Sanity check: material+variant and is_global_quality cannot be present at the same time
if is_global_quality and (root_material_id or variant_name):
raise RuntimeError("Quality profile [%s] contains invalid data: it is a global quality but contains 'material' and 'nozzle' info." % metadata["id"])
if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id]
if is_global_quality:
# For global qualities, save data in the machine node
machine_node.addQualityMetadata(quality_type, metadata)
continue
if variant_name is not None:
# If variant_name is specified in the quality/quality_changes profile, check if material is specified,
# too.
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = QualityNode()
variant_node = machine_node.children_map[variant_name]
if root_material_id is None:
# If only variant_name is specified but material is not, add the quality/quality_changes metadata
# into the current variant node.
variant_node.addQualityMetadata(quality_type, metadata)
else:
# If only variant_name and material are both specified, go one level deeper: create a material node
# under the current variant node, and then add the quality/quality_changes metadata into the
# material node.
if root_material_id not in variant_node.children_map:
variant_node.children_map[root_material_id] = QualityNode()
material_node = variant_node.children_map[root_material_id]
material_node.addQualityMetadata(quality_type, metadata)
else:
# If variant_name is not specified, check if material is specified.
if root_material_id is not None:
if root_material_id not in machine_node.children_map:
machine_node.children_map[root_material_id] = QualityNode()
material_node = machine_node.children_map[root_material_id]
material_node.addQualityMetadata(quality_type, metadata)
# Initialize the lookup tree for quality_changes profiles with following structure:
# <machine> -> <quality_type> -> <name>
quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
for metadata in quality_changes_metadata_list:
if metadata["id"] == "empty_quality_changes":
continue
machine_definition_id = metadata["definition"]
quality_type = metadata["quality_type"]
if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
machine_node.addQualityChangesMetadata(quality_type, metadata)
Logger.log("d", "Lookup tables updated.")
self.qualitiesUpdated.emit()
def _updateMaps(self):
self.initialize()
def _onContainerMetadataChanged(self, container):
self._onContainerChanged(container)
def _onContainerChanged(self, container):
container_type = container.getMetaDataEntry("type")
if container_type not in ("quality", "quality_changes"):
return
# update the cache table
self._update_timer.start()
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
used_extruders = set()
# TODO: This will change after the Machine refactoring
for i in range(machine.getProperty("machine_extruder_count", "value")):
used_extruders.add(str(i))
# Update the "is_available" flag for each quality group.
for quality_group in quality_group_list:
is_available = True
if quality_group.node_for_global is None:
is_available = False
if is_available:
for position in used_extruders:
if position not in quality_group.nodes_for_extruders:
is_available = False
break
quality_group.is_available = is_available
# Returns a dict of "custom profile name" -> QualityChangesGroup
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
if not machine_node:
Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
return dict()
# Update availability for each QualityChangesGroup:
# A custom profile is always available as long as the quality_type it's based on is available
quality_group_dict = self.getQualityGroups(machine)
available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
# Iterate over all quality_types in the machine node
quality_changes_group_dict = dict()
for quality_type, quality_changes_node in machine_node.quality_type_map.items():
for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
quality_changes_group_dict[quality_changes_name] = quality_changes_group
quality_changes_group.is_available = quality_type in available_quality_type_list
return quality_changes_group_dict
#
# Gets all quality groups for the given machine. Both available and none available ones will be included.
# It returns a dictionary with "quality_type"s as keys and "QualityGroup"s as values.
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# For more details, see QualityGroup.
#
def getQualityGroups(self, machine: "GlobalStack") -> dict:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
# To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node
# (2) the generic node
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node]
# Iterate over all quality_types in the machine node
quality_group_dict = {}
for node in nodes_to_check:
if node and node.quality_type_map:
# Only include global qualities
if has_variant_materials:
quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
if not is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group
break
# Iterate over all extruders to find quality containers for each extruder
for position, extruder in machine.extruders.items():
variant_name = None
if extruder.variant.getId() != "empty_variant":
variant_name = extruder.variant.getName()
# This is a list of root material IDs to use for searching for suitable quality profiles.
# The root material IDs in this list are in prioritized order.
root_material_id_list = []
has_material = False # flag indicating whether this extruder has a material assigned
if extruder.material.getId() != "empty_material":
has_material = True
root_material_id = extruder.material.getMetaDataEntry("base_file")
# Convert possible generic_pla_175 -> generic_pla
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
root_material_id_list.append(root_material_id)
# Also try to get the fallback material
material_type = extruder.material.getMetaDataEntry("material")
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
if fallback_root_material_id:
root_material_id_list.append(fallback_root_material_id)
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
# The use case is that, when we look for qualities for a machine, we first want to search in the following
# order:
# 1. machine-variant-and-material-specific qualities if exist
# 2. machine-variant-specific qualities if exist
# 3. machine-material-specific qualities if exist
# 4. machine-specific qualities if exist
# 5. generic qualities if exist
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there.
nodes_to_check = []
if variant_name:
# In this case, we have both a specific variant and a specific material
variant_node = machine_node.getChildNode(variant_name)
if variant_node and has_material:
for root_material_id in root_material_id_list:
material_node = variant_node.getChildNode(root_material_id)
if material_node:
nodes_to_check.append(material_node)
break
nodes_to_check.append(variant_node)
# In this case, we only have a specific material but NOT a variant
if has_material:
for root_material_id in root_material_id_list:
material_node = machine_node.getChildNode(root_material_id)
if material_node:
nodes_to_check.append(material_node)
break
nodes_to_check += [machine_node, default_machine_node]
for node in nodes_to_check:
if node and node.quality_type_map:
if has_variant_materials:
# Only include variant qualities; skip non global qualities
quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
if is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items():
if quality_type not in quality_group_dict:
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type]
quality_group.nodes_for_extruders[position] = quality_node
break
# Update availabilities for each quality group
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
return quality_group_dict
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
# To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node
# (2) the generic node
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node]
# Iterate over all quality_types in the machine node
quality_group_dict = dict()
for node in nodes_to_check:
if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group
break
return quality_group_dict
#
# Methods for GUI
#
#
# Remove the given quality changes group.
#
@pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"):
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
for node in quality_changes_group.getAllNodes():
self._container_registry.removeContainer(node.metadata["id"])
#
# Rename a set of quality changes containers. Returns the new name.
#
@pyqtSlot(QObject, str, result = str)
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
Logger.log("i", "Renaming QualityChangesGroup[%s] to [%s]", quality_changes_group.name, new_name)
if new_name == quality_changes_group.name:
Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
return new_name
new_name = self._container_registry.uniqueName(new_name)
for node in quality_changes_group.getAllNodes():
node.getContainer().setName(new_name)
quality_changes_group.name = new_name
self._application.getMachineManager().activeQualityChanged.emit()
self._application.getMachineManager().activeQualityGroupChanged.emit()
return new_name
#
# Duplicates the given quality.
#
@pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, quality_changes_name, quality_model_item):
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.")
return
quality_group = quality_model_item["quality_group"]
quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None:
# create global quality changes only
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
global_stack, extruder_id = None)
self._container_registry.addContainer(new_quality_changes)
else:
new_name = self._container_registry.uniqueName(quality_changes_name)
for node in quality_changes_group.getAllNodes():
container = node.getContainer()
new_id = self._container_registry.uniqueName(container.getId())
self._container_registry.addContainer(container.duplicate(new_id, new_name))
## Create quality changes containers from the user containers in the active stacks.
#
# This will go through the global and extruder stacks and create quality_changes containers from
# the user containers in each stack. These then replace the quality_changes containers in the
# stack and clear the user settings.
@pyqtSlot(str)
def createQualityChanges(self, base_name):
machine_manager = Application.getInstance().getMachineManager()
global_stack = machine_manager.activeMachine
if not global_stack:
return
active_quality_name = machine_manager.activeQualityOrQualityChangesName
if active_quality_name == "":
Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
return
machine_manager.blurSettings.emit()
if base_name is None or base_name == "":
base_name = active_quality_name
unique_name = self._container_registry.uniqueName(base_name)
# Go through the active stacks and create quality_changes containers from the user containers.
stack_list = [global_stack] + list(global_stack.extruders.values())
for stack in stack_list:
user_container = stack.userChanges
quality_container = stack.quality
quality_changes_container = stack.qualityChanges
if not quality_container or not quality_changes_container:
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
continue
extruder_definition_id = None
if isinstance(stack, ExtruderStack):
extruder_definition_id = stack.definition.getId()
quality_type = quality_container.getMetaDataEntry("quality_type")
new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_definition_id)
from cura.Settings.ContainerManager import ContainerManager
ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
ContainerManager.getInstance()._performMerge(new_changes, user_container)
self._container_registry.addContainer(new_changes)
#
# Create a quality changes container with the given setup.
#
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
extruder_id: Optional[str]) -> "InstanceContainer":
base_id = machine.definition.getId() if extruder_id is None else extruder_id
new_id = base_id + "_" + new_name
new_id = new_id.lower().replace(" ", "_")
new_id = self._container_registry.uniqueName(new_id)
# Create a new quality_changes container for the quality.
quality_changes = InstanceContainer(new_id)
quality_changes.setName(new_name)
quality_changes.addMetaDataEntry("type", "quality_changes")
quality_changes.addMetaDataEntry("quality_type", quality_type)
# If we are creating a container for an extruder, ensure we add that to the container
if extruder_id is not None:
quality_changes.addMetaDataEntry("extruder", extruder_id)
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
quality_changes.setDefinition(machine_definition_id)
quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion)
return quality_changes
#
# Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
# machine. The rule is as follows:
# 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
# machine.
# 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
# own machine definition ID for quality search.
# Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
# 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
# definition ID specified in "quality_definition" should be used.
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
# shares the same set of qualities profiles as Ultimaker 3.
#
def getMachineDefinitionIDForQualitySearch(machine: "GlobalStack", default_definition_id: str = "fdmprinter") -> str:
machine_definition_id = default_definition_id
if parseBool(machine.getMetaDataEntry("has_machine_quality", False)):
# Only use the machine's own quality definition ID if this machine has machine quality.
machine_definition_id = machine.getMetaDataEntry("quality_definition")
if machine_definition_id is None:
machine_definition_id = machine.definition.getId()
return machine_definition_id

View file

@ -0,0 +1,35 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from .ContainerNode import ContainerNode
from .QualityChangesGroup import QualityChangesGroup
#
# QualityNode is used for BOTH quality and quality_changes containers.
#
class QualityNode(ContainerNode):
def __init__(self, metadata: Optional[dict] = None):
super().__init__(metadata = metadata)
self.quality_type_map = {} # quality_type -> QualityNode for InstanceContainer
def addQualityMetadata(self, quality_type: str, metadata: dict):
if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode(metadata)
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
return self.quality_type_map.get(quality_type)
def addQualityChangesMetadata(self, quality_type: str, metadata: dict):
if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode()
quality_type_node = self.quality_type_map[quality_type]
name = metadata["name"]
if name not in quality_type_node.children_map:
quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
quality_changes_group = quality_type_node.children_map[name]
quality_changes_group.addNode(QualityNode(metadata))

View file

@ -0,0 +1,111 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from enum import Enum
from collections import OrderedDict
from typing import Optional, TYPE_CHECKING
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer
class VariantType(Enum):
BUILD_PLATE = "buildplate"
NOZZLE = "nozzle"
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
#
# VariantManager is THE place to look for a specific variant. It maintains a variant lookup table with the following
# structure:
#
# [machine_definition_id] -> [variant_type] -> [variant_name] -> ContainerNode(metadata / container)
# Example: "ultimaker3" -> "buildplate" -> "Glass" (if present) -> ContainerNode
# -> ...
# -> "nozzle" -> "AA 0.4"
# -> "BB 0.8"
# -> ...
#
# Note that the "container" field is not loaded in the beginning because it would defeat the purpose of lazy-loading.
# A container is loaded when getVariant() is called to load a variant InstanceContainer.
#
class VariantManager:
def __init__(self, container_registry):
self._container_registry = container_registry # type: ContainerRegistry
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict>
self._exclude_variant_id_list = ["empty_variant"]
#
# Initializes the VariantManager including:
# - initializing the variant lookup table based on the metadata in ContainerRegistry.
#
def initialize(self):
self._machine_to_variant_dict_map = OrderedDict()
# Cache all variants from the container registry to a variant map for better searching and organization.
variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant")
for variant_metadata in variant_metadata_list:
if variant_metadata["id"] in self._exclude_variant_id_list:
Logger.log("d", "Exclude variant [%s]", variant_metadata["id"])
continue
variant_name = variant_metadata["name"]
variant_definition = variant_metadata["definition"]
if variant_definition not in self._machine_to_variant_dict_map:
self._machine_to_variant_dict_map[variant_definition] = OrderedDict()
for variant_type in ALL_VARIANT_TYPES:
self._machine_to_variant_dict_map[variant_definition][variant_type] = dict()
variant_type = variant_metadata["hardware_type"]
variant_type = VariantType(variant_type)
variant_dict = self._machine_to_variant_dict_map[variant_definition][variant_type]
if variant_name in variant_dict:
# ERROR: duplicated variant name.
raise RuntimeError("Found duplicated variant name [%s], type [%s] for machine [%s]" %
(variant_name, variant_type, variant_definition))
variant_dict[variant_name] = ContainerNode(metadata = variant_metadata)
#
# Gets the variant InstanceContainer with the given information.
# Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present.
#
def getVariantNode(self, machine_definition_id: str, variant_name: str,
variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> Optional["ContainerNode"]:
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
def getVariantNodes(self, machine: "GlobalStack",
variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> dict:
machine_definition_id = machine.definition.getId()
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})
#
# Gets the default variant for the given machine definition.
#
def getDefaultVariantNode(self, machine_definition: "DefinitionContainer",
variant_type: VariantType) -> Optional["ContainerNode"]:
machine_definition_id = machine_definition.getId()
preferred_variant_name = None
if variant_type == VariantType.BUILD_PLATE:
if parseBool(machine_definition.getMetaDataEntry("has_variant_buildplates", False)):
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_buildplate_name")
else:
if parseBool(machine_definition.getMetaDataEntry("has_variants", False)):
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_name")
node = None
if preferred_variant_name:
node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type)
return node

View file

View file

@ -2,24 +2,15 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.Arranging.Arrange import Arrange
from cura.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray
from cura.ShapeArray import ShapeArray
from typing import List
from UM.Application import Application from UM.Application import Application
from UM.Scene.Selection import Selection
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
@ -65,6 +56,10 @@ class MultiplyObjectsJob(Job):
new_location = new_location.set(z = 100 - i * 20) new_location = new_location.set(z = 100 - i * 20)
node.setPosition(new_location) node.setPosition(new_location)
# Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
node.callDecoration("setBuildPlateNumber", build_plate_number)
nodes.append(node) nodes.append(node)
current_progress += 1 current_progress += 1
status_message.setProgress((current_progress / total_progress) * 100) status_message.setProgress((current_progress / total_progress) * 100)

79
cura/ObjectsModel.py Normal file
View file

@ -0,0 +1,79 @@
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Preferences import Preferences
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Keep track of all objects in the project
class ObjectsModel(ListModel):
def __init__(self):
super().__init__()
Application.getInstance().getController().getScene().sceneChanged.connect(self._update)
Preferences.getInstance().preferenceChanged.connect(self._update)
self._build_plate_number = -1
self._stacks_have_errors = None # type:Optional[bool]
def setActiveBuildPlate(self, nr):
self._build_plate_number = nr
self._update()
def _update(self, *args):
nodes = []
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
active_build_plate_number = self._build_plate_number
group_nr = 1
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if not isinstance(node, SceneNode):
continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
continue
if not node.callDecoration("isGroup"):
name = node.getName()
else:
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
group_nr += 1
if hasattr(node, "isOutsideBuildArea"):
is_outside_build_area = node.isOutsideBuildArea()
else:
is_outside_build_area = False
nodes.append({
"name": name,
"isSelected": Selection.isSelected(node),
"isOutsideBuildArea": is_outside_build_area,
"buildPlateNumber": node_build_plate_number,
"node": node
})
nodes = sorted(nodes, key=lambda n: n["name"])
self.setItems(nodes)
self.itemsChanged.emit()
@staticmethod
def createObjectsModel():
return ObjectsModel()
## Check if none of the model's stacks contain error states
# The setting applied for the settings per model
def stacksHaveErrors(self) -> bool:
return bool(self._stacks_have_errors)
def setStacksHaveErrors(self, value):
self._stacks_have_errors = value

View file

@ -18,12 +18,13 @@ class OneAtATimeIterator(Iterator.Iterator):
def _fillStack(self): def _fillStack(self):
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not type(node) is SceneNode: if not issubclass(type(node), SceneNode):
continue continue
if node.callDecoration("getConvexHull"): if node.callDecoration("getConvexHull"):
node_list.append(node) node_list.append(node)
if len(node_list) < 2: if len(node_list) < 2:
self._node_stack = node_list[:] self._node_stack = node_list[:]
return return

View file

@ -0,0 +1,29 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.SceneNode import SceneNode
from UM.Operations.Operation import Operation
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the buildplate number of a scenenode.
class SetBuildPlateNumberOperation(Operation):
def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
super().__init__()
self._node = node
self._build_plate_nr = build_plate_nr
self._previous_build_plate_nr = None
self._decorator_added = False
def undo(self):
if self._previous_build_plate_nr:
self._node.callDecoration("setBuildPlateNumber", self._previous_build_plate_nr)
def redo(self):
stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
self._node.addDecorator(SettingOverrideDecorator())
self._previous_build_plate_nr = self._node.callDecoration("getBuildPlateNumber")
self._node.callDecoration("setBuildPlateNumber", self._build_plate_nr)

View file

View file

@ -10,10 +10,10 @@ from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from . import PlatformPhysicsOperation from cura.Operations import PlatformPhysicsOperation
from . import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
import random # used for list shuffling import random # used for list shuffling
@ -34,6 +34,7 @@ class PlatformPhysics:
self._change_timer.timeout.connect(self._onChangeTimerFinished) self._change_timer.timeout.connect(self._onChangeTimerFinished)
self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot? self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick? self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
Preferences.getInstance().addPreference("physics/automatic_push_free", True) Preferences.getInstance().addPreference("physics/automatic_push_free", True)
Preferences.getInstance().addPreference("physics/automatic_drop_down", True) Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
@ -41,7 +42,7 @@ class PlatformPhysics:
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
self._change_timer.start() self._change_timer.start()
def _onChangeTimerFinished(self, was_triggered_by_tool=False): def _onChangeTimerFinished(self):
if not self._enabled: if not self._enabled:
return return
@ -60,7 +61,7 @@ class PlatformPhysics:
random.shuffle(nodes) random.shuffle(nodes)
for node in nodes: for node in nodes:
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
continue continue
bbox = node.getBoundingBox() bbox = node.getBoundingBox()
@ -70,17 +71,18 @@ class PlatformPhysics:
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
move_vector = move_vector.set(y=-bbox.bottom + z_offset) move_vector = move_vector.set(y = -bbox.bottom + z_offset)
# If there is no convex hull for the node, start calculating it and continue. # If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator): if not node.getDecorator(ConvexHullDecorator):
node.addDecorator(ConvexHullDecorator()) node.addDecorator(ConvexHullDecorator())
if Preferences.getInstance().getValue("physics/automatic_push_free"): # only push away objects if this node is a printing mesh
if not node.callDecoration("isNonPrintingMesh") and Preferences.getInstance().getValue("physics/automatic_push_free"):
# Check for collisions between convex hulls # Check for collisions between convex hulls
for other_node in BreadthFirstIterator(root): for other_node in BreadthFirstIterator(root):
# Ignore root, ourselves and anything that is not a normal SceneNode. # Ignore root, ourselves and anything that is not a normal SceneNode.
if other_node is root or type(other_node) is not SceneNode or other_node is node: if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"):
continue continue
# Ignore collisions of a group with it's own children # Ignore collisions of a group with it's own children
@ -98,6 +100,9 @@ class PlatformPhysics:
if other_node in transformed_nodes: if other_node in transformed_nodes:
continue # Other node is already moving, wait for next pass. continue # Other node is already moving, wait for next pass.
if other_node.callDecoration("isNonPrintingMesh"):
continue
overlap = (0, 0) # Start loop with no overlap overlap = (0, 0) # Start loop with no overlap
current_overlap_checks = 0 current_overlap_checks = 0
# Continue to check the overlap until we no longer find one. # Continue to check the overlap until we no longer find one.
@ -112,26 +117,38 @@ class PlatformPhysics:
overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull) overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
if overlap: if overlap:
# Moving ensured that overlap was still there. Try anew! # Moving ensured that overlap was still there. Try anew!
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z=move_vector.z + overlap[1] * self._move_factor) z = move_vector.z + overlap[1] * self._move_factor)
else: else:
# Moving ensured that overlap was still there. Try anew! # Moving ensured that overlap was still there. Try anew!
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z=move_vector.z + overlap[1] * self._move_factor) z = move_vector.z + overlap[1] * self._move_factor)
else: else:
own_convex_hull = node.callDecoration("getConvexHull") own_convex_hull = node.callDecoration("getConvexHull")
other_convex_hull = other_node.callDecoration("getConvexHull") other_convex_hull = other_node.callDecoration("getConvexHull")
if own_convex_hull and other_convex_hull: if own_convex_hull and other_convex_hull:
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull) overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
if overlap: # Moving ensured that overlap was still there. Try anew! if overlap: # Moving ensured that overlap was still there. Try anew!
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor, temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z=move_vector.z + overlap[1] * self._move_factor) z = move_vector.z + overlap[1] * self._move_factor)
# if the distance between two models less than 2mm then try to find a new factor
if abs(temp_move_vector.x - overlap[0]) < self._minimum_gap and abs(temp_move_vector.y - overlap[1]) < self._minimum_gap:
temp_x_factor = (abs(overlap[0]) + self._minimum_gap) / overlap[0] if overlap[0] != 0 else 0 # find x move_factor, like (3.4 + 2) / 3.4 = 1.58
temp_y_factor = (abs(overlap[1]) + self._minimum_gap) / overlap[1] if overlap[1] != 0 else 0 # find y move_factor
temp_scale_factor = temp_x_factor if abs(temp_x_factor) > abs(temp_y_factor) else temp_y_factor
move_vector = move_vector.set(x = move_vector.x + overlap[0] * temp_scale_factor,
z = move_vector.z + overlap[1] * temp_scale_factor)
else:
move_vector = temp_move_vector
else: else:
# This can happen in some cases if the object is not yet done with being loaded. # This can happen in some cases if the object is not yet done with being loaded.
# Simply waiting for the next tick seems to resolve this correctly. # Simply waiting for the next tick seems to resolve this correctly.
overlap = None overlap = None
if not Vector.Null.equals(move_vector, epsilon=1e-5): if not Vector.Null.equals(move_vector, epsilon = 1e-5):
transformed_nodes.append(node) transformed_nodes.append(node)
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
op.push() op.push()
@ -160,4 +177,4 @@ class PlatformPhysics:
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
self._enabled = True self._enabled = True
self._onChangeTimerFinished(True) self._onChangeTimerFinished()

View file

@ -1,7 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
from UM.Math.Color import Color
from UM.Resources import Resources from UM.Resources import Resources
from UM.View.RenderPass import RenderPass from UM.View.RenderPass import RenderPass
@ -17,6 +17,18 @@ MYPY = False
if MYPY: if MYPY:
from UM.Scene.Camera import Camera from UM.Scene.Camera import Camera
# Make color brighter by normalizing it (maximum factor 2.5 brighter)
# color_list is a list of 4 elements: [r, g, b, a], each element is a float 0..1
def prettier_color(color_list):
maximum = max(color_list[:3])
if maximum > 0:
factor = min(1 / maximum, 2.5)
else:
factor = 1.0
return [min(i * factor, 1.0) for i in color_list]
## A render pass subclass that renders slicable objects with default parameters. ## A render pass subclass that renders slicable objects with default parameters.
# It uses the active camera by default, but it can be overridden to use a different camera. # It uses the active camera by default, but it can be overridden to use a different camera.
# #
@ -39,7 +51,14 @@ class PreviewPass(RenderPass):
def render(self) -> None: def render(self) -> None:
if not self._shader: if not self._shader:
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "object.shader")) self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
self._shader.setUniformValue("u_overhangAngle", 1.0)
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0])
self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0])
self._shader.setUniformValue("u_shininess", 20.0)
self._gl.glClearColor(0.0, 0.0, 0.0, 0.0)
self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT)
# Create a new batch to be rendered # Create a new batch to be rendered
batch = RenderBatch(self._shader) batch = RenderBatch(self._shader)
@ -47,7 +66,9 @@ class PreviewPass(RenderPass):
# Fill up the batch with objects that can be sliced. ` # Fill up the batch with objects that can be sliced. `
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData()) uniforms = {}
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
self.bind() self.bind()
if self._camera is None: if self._camera is None:
@ -55,3 +76,4 @@ class PreviewPass(RenderPass):
else: else:
batch.render(self._camera) batch.render(self._camera)
self.release() self.release()

View file

@ -1,24 +1,22 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty from typing import Dict
from UM.FlameProfiler import pyqtSlot 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.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.Duration import Duration from UM.Qt.Duration import Duration
from UM.Preferences import Preferences from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Scene.SceneNode import SceneNode
from cura.Settings.ExtruderManager import ExtruderManager
import math
import os.path
import unicodedata
import json
import re #To create abbreviations for printer names.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name ## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
@ -54,36 +52,45 @@ class PrintInformation(QObject):
self.initializeCuraMessagePrintTimeProperties() self.initializeCuraMessagePrintTimeProperties()
self._material_lengths = [] self._material_lengths = {} # indexed by build plate number
self._material_weights = [] self._material_weights = {}
self._material_costs = [] self._material_costs = {}
self._material_names = [] self._material_names = {}
self._pre_sliced = False self._pre_sliced = False
self._backend = Application.getInstance().getBackend() self._backend = Application.getInstance().getBackend()
if self._backend: if self._backend:
self._backend.printDurationMessage.connect(self._onPrintDurationMessage) self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
self._base_name = "" self._base_name = ""
self._abbr_machine = "" self._abbr_machine = ""
self._job_name = "" 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)
Application.getInstance().globalContainerStackChanged.connect(self._updateJobName)
Application.getInstance().fileLoaded.connect(self.setBaseName)
Application.getInstance().workspaceLoaded.connect(self.setProjectName)
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
self._active_material_container = None self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged) self._onActiveMaterialsChanged()
self._onActiveMaterialChanged()
self._material_amounts = [] self._material_amounts = []
# Crate cura message translations and using translation keys initialize empty time Duration object for total time # Crate cura message translations and using translation keys initialize empty time Duration object for total time
# and time for each feature # and time for each feature
def initializeCuraMessagePrintTimeProperties(self): def initializeCuraMessagePrintTimeProperties(self):
self._current_print_time = Duration(None, self) self._current_print_time = {} # Duration(None, self)
self._print_time_message_translations = { self._print_time_message_translations = {
"inset_0": catalog.i18nc("@tooltip", "Outer Wall"), "inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
@ -101,10 +108,25 @@ class PrintInformation(QObject):
self._print_time_message_values = {} self._print_time_message_values = {}
def _initPrintTimeMessageValues(self, build_plate_number):
# Full fill message values using keys from _print_time_message_translations # 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(): for key in self._print_time_message_translations.keys():
self._print_time_message_values[key] = Duration(None, self) 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() currentPrintTimeChanged = pyqtSignal()
@ -120,79 +142,84 @@ class PrintInformation(QObject):
@pyqtProperty(Duration, notify = currentPrintTimeChanged) @pyqtProperty(Duration, notify = currentPrintTimeChanged)
def currentPrintTime(self): def currentPrintTime(self):
return self._current_print_time return self._current_print_time[self._active_build_plate]
materialLengthsChanged = pyqtSignal() materialLengthsChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = materialLengthsChanged) @pyqtProperty("QVariantList", notify = materialLengthsChanged)
def materialLengths(self): def materialLengths(self):
return self._material_lengths return self._material_lengths[self._active_build_plate]
materialWeightsChanged = pyqtSignal() materialWeightsChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = materialWeightsChanged) @pyqtProperty("QVariantList", notify = materialWeightsChanged)
def materialWeights(self): def materialWeights(self):
return self._material_weights return self._material_weights[self._active_build_plate]
materialCostsChanged = pyqtSignal() materialCostsChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = materialCostsChanged) @pyqtProperty("QVariantList", notify = materialCostsChanged)
def materialCosts(self): def materialCosts(self):
return self._material_costs return self._material_costs[self._active_build_plate]
materialNamesChanged = pyqtSignal() materialNamesChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = materialNamesChanged) @pyqtProperty("QVariantList", notify = materialNamesChanged)
def materialNames(self): def materialNames(self):
return self._material_names return self._material_names[self._active_build_plate]
def _onPrintDurationMessage(self, print_time, material_amounts): def printTimes(self):
return self._print_time_message_values[self._active_build_plate]
self._updateTotalPrintTimePerFeature(print_time) 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.currentPrintTimeChanged.emit()
self._material_amounts = material_amounts self._material_amounts = material_amounts
self._calculateInformation() self._calculateInformation(build_plate_number)
def _updateTotalPrintTimePerFeature(self, print_time): def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
total_estimated_time = 0 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(): for feature, time in print_time.items():
if time != time: # Check for NaN. Engine can sometimes give us weird values. if time != time: # Check for NaN. Engine can sometimes give us weird values.
self._print_time_message_values.get(feature).setDuration(0) self._print_time_message_values[build_plate_number].get(feature).setDuration(0)
Logger.log("w", "Received NaN for print duration message") Logger.log("w", "Received NaN for print duration message")
continue continue
total_estimated_time += time total_estimated_time += time
self._print_time_message_values.get(feature).setDuration(time) self._print_time_message_values[build_plate_number].get(feature).setDuration(time)
self._current_print_time.setDuration(total_estimated_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): def _calculateInformation(self, build_plate_number):
if Application.getInstance().getGlobalContainerStack() is None: global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack is None:
return return
# Material amount is sent as an amount of mm^3, so calculate length from that self._material_lengths[build_plate_number] = []
radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2 self._material_weights[build_plate_number] = []
self._material_lengths = [] self._material_costs[build_plate_number] = []
self._material_weights = [] self._material_names[build_plate_number] = []
self._material_costs = []
self._material_names = []
material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings")) material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId())) extruder_stacks = global_stack.extruders
for index, amount in enumerate(self._material_amounts): 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 ## 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. # list comprehension filtering to solve this for us.
material = None density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
if extruder_stacks: # Multi extrusion machine material = extruder_stack.findContainer({"type": "material"})
extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0] radius = extruder_stack.getProperty("material_diameter", "value") / 2
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
material = extruder_stack.findContainer({"type": "material"})
else: # Machine with no extruder stacks
density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"})
weight = float(amount) * float(density) / 1000 weight = float(amount) * float(density) / 1000
cost = 0 cost = 0
@ -211,14 +238,15 @@ class PrintInformation(QObject):
else: else:
cost = 0 cost = 0
# Material amount is sent as an amount of mm^3, so calculate length from that
if radius != 0: if radius != 0:
length = round((amount / (math.pi * radius ** 2)) / 1000, 2) length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
else: else:
length = 0 length = 0
self._material_weights.append(weight) self._material_weights[build_plate_number].append(weight)
self._material_lengths.append(length) self._material_lengths[build_plate_number].append(length)
self._material_costs.append(cost) self._material_costs[build_plate_number].append(cost)
self._material_names.append(material_name) self._material_names[build_plate_number].append(material_name)
self.materialLengthsChanged.emit() self.materialLengthsChanged.emit()
self.materialWeightsChanged.emit() self.materialWeightsChanged.emit()
@ -229,24 +257,25 @@ class PrintInformation(QObject):
if preference != "cura/material_settings": if preference != "cura/material_settings":
return return
self._calculateInformation() for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
self._calculateInformation(build_plate_number)
def _onActiveMaterialChanged(self): def _onActiveBuildPlateChanged(self):
if self._active_material_container: new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
try: if new_active_build_plate != self._active_build_plate:
self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged) self._active_build_plate = new_active_build_plate
except TypeError: #pyQtSignal gives a TypeError when disconnecting from something that is already disconnected.
pass
active_material_id = Application.getInstance().getMachineManager().activeMaterialId self._initVariablesWithBuildPlate(self._active_build_plate)
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id = active_material_id)
if active_material_containers: self.materialLengthsChanged.emit()
self._active_material_container = active_material_containers[0] self.materialWeightsChanged.emit()
self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged) self.materialCostsChanged.emit()
self.materialNamesChanged.emit()
self.currentPrintTimeChanged.emit()
def _onMaterialMetaDataChanged(self, *args, **kwargs): def _onActiveMaterialsChanged(self, *args, **kwargs):
self._calculateInformation() for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
self._calculateInformation(build_plate_number)
@pyqtSlot(str) @pyqtSlot(str)
def setJobName(self, name): def setJobName(self, name):
@ -315,10 +344,10 @@ class PrintInformation(QObject):
if not global_container_stack: if not global_container_stack:
self._abbr_machine = "" self._abbr_machine = ""
return return
active_machine_type_name = global_container_stack.definition.getName()
global_stack_name = global_container_stack.getName()
abbr_machine = "" abbr_machine = ""
for word in re.findall(r"[\w']+", global_stack_name): for word in re.findall(r"[\w']+", active_machine_type_name):
if word.lower() == "ultimaker": if word.lower() == "ultimaker":
abbr_machine += "UM" abbr_machine += "UM"
elif word.isdigit(): elif word.isdigit():
@ -340,7 +369,9 @@ class PrintInformation(QObject):
@pyqtSlot(result = "QVariantMap") @pyqtSlot(result = "QVariantMap")
def getFeaturePrintTimes(self): def getFeaturePrintTimes(self):
result = {} result = {}
for feature, time in self._print_time_message_values.items(): 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: if feature in self._print_time_message_translations:
result[self._print_time_message_translations[feature]] = time result[self._print_time_message_translations[feature]] = time
else: else:
@ -348,10 +379,27 @@ class PrintInformation(QObject):
return result return result
# Simulate message with zero time duration # Simulate message with zero time duration
def setToZeroPrintInformation(self): def setToZeroPrintInformation(self, build_plate = None):
temp_message = {} if build_plate is None:
for key in self._print_time_message_values.keys(): build_plate = self._active_build_plate
temp_message[key] = 0
# 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] temp_material_amounts = [0]
self._onPrintDurationMessage(temp_message, temp_material_amounts)
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)

View file

@ -0,0 +1,70 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from UM.Logger import Logger
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
class ExtruderOutputModel(QObject):
hotendIDChanged = pyqtSignal()
targetHotendTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal()
activeMaterialChanged = pyqtSignal()
def __init__(self, printer: "PrinterOutputModel", parent=None):
super().__init__(parent)
self._printer = printer
self._target_hotend_temperature = 0
self._hotend_temperature = 0
self._hotend_id = ""
self._active_material = None # type: Optional[MaterialOutputModel]
@pyqtProperty(QObject, notify = activeMaterialChanged)
def activeMaterial(self) -> "MaterialOutputModel":
return self._active_material
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
if self._active_material != material:
self._active_material = material
self.activeMaterialChanged.emit()
## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float):
if self._hotend_temperature != temperature:
self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit()
def updateTargetHotendTemperature(self, temperature: float):
if self._target_hotend_temperature != temperature:
self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float)
def setTargetHotendTemperature(self, temperature: float):
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
self.updateTargetHotendTemperature(temperature)
@pyqtProperty(float, notify = targetHotendTemperatureChanged)
def targetHotendTemperature(self) -> float:
return self._target_hotend_temperature
@pyqtProperty(float, notify=hotendTemperatureChanged)
def hotendTemperature(self) -> float:
return self._hotend_temperature
@pyqtProperty(str, notify = hotendIDChanged)
def hotendID(self) -> str:
return self._hotend_id
def updateHotendID(self, id: str):
if self._hotend_id != id:
self._hotend_id = id
self.hotendIDChanged.emit()

View file

@ -0,0 +1,34 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
class MaterialOutputModel(QObject):
def __init__(self, guid, type, color, brand, name, parent = None):
super().__init__(parent)
self._guid = guid
self._type = type
self._color = color
self._brand = brand
self._name = name
@pyqtProperty(str, constant = True)
def guid(self):
return self._guid
@pyqtProperty(str, constant=True)
def type(self):
return self._type
@pyqtProperty(str, constant=True)
def brand(self):
return self._brand
@pyqtProperty(str, constant=True)
def color(self):
return self._color
@pyqtProperty(str, constant=True)
def name(self):
return self._name

View file

@ -0,0 +1,119 @@
from UM.Logger import Logger
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
from PyQt5.QtGui import QImage
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
class NetworkCamera(QObject):
newImage = pyqtSignal()
def __init__(self, target = None, parent = None):
super().__init__(parent)
self._stream_buffer = b""
self._stream_buffer_start_index = -1
self._manager = None
self._image_request = None
self._image_reply = None
self._image = QImage()
self._image_id = 0
self._target = target
self._started = False
@pyqtSlot(str)
def setTarget(self, target):
restart_required = False
if self._started:
self.stop()
restart_required = True
self._target = target
if restart_required:
self.start()
@pyqtProperty(QUrl, notify=newImage)
def latestImage(self):
self._image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://camera/" + str(self._image_id)
return QUrl(temp, QUrl.TolerantMode)
@pyqtSlot()
def start(self):
# Ensure that previous requests (if any) are stopped.
self.stop()
if self._target is None:
Logger.log("w", "Unable to start camera stream without target!")
return
self._started = True
url = QUrl(self._target)
self._image_request = QNetworkRequest(url)
if self._manager is None:
self._manager = QNetworkAccessManager()
self._image_reply = self._manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
@pyqtSlot()
def stop(self):
self._stream_buffer = b""
self._stream_buffer_start_index = -1
if self._image_reply:
try:
# disconnect the signal
try:
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
except Exception:
pass
# abort the request if it's not finished
if not self._image_reply.isFinished():
self._image_reply.close()
except Exception as e: # RuntimeError
pass # It can happen that the wrapped c++ object is already deleted.
self._image_reply = None
self._image_request = None
self._manager = None
self._started = False
def getImage(self):
return self._image
## Ensure that close gets called when object is destroyed
def __del__(self):
self.stop()
def _onStreamDownloadProgress(self, bytes_received, bytes_total):
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
if self._image_reply is None:
return
self._stream_buffer += self._image_reply.readAll()
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
self.stop() # resets stream buffer and start index
self.start()
return
if self._stream_buffer_start_index == -1:
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
# If this happens to be more than a single frame, then so be it; the JPG decoder will
# ignore the extra data. We do it like this in order not to get a buildup of frames
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
self._stream_buffer_start_index = -1
self._image.loadFromData(jpg_data)
self.newImage.emit()

View file

@ -0,0 +1,307 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication
from time import time
from typing import Callable, Any, Optional, Dict, Tuple
from enum import IntEnum
from typing import List
import os # To get the username
import gzip
class AuthState(IntEnum):
NotAuthenticated = 1
AuthenticationRequested = 2
Authenticated = 3
AuthenticationDenied = 4
AuthenticationReceived = 5
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
authenticationStateChanged = pyqtSignal()
def __init__(self, device_id, address: str, properties, parent = None) -> None:
super().__init__(device_id = device_id, parent = parent)
self._manager = None # type: QNetworkAccessManager
self._last_manager_create_time = None # type: float
self._recreate_network_manager_time = 30
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
self._last_response_time = None # type: float
self._last_request_time = None # type: float
self._api_prefix = ""
self._address = address
self._properties = properties
self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion())
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
self._authentication_state = AuthState.NotAuthenticated
# QHttpMultiPart objects need to be kept alive and not garbage collected during the
# HTTP which uses them. We hold references to these QHttpMultiPart objects here.
self._kept_alive_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart]
self._sending_gcode = False
self._compressing_gcode = False
self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None:
raise NotImplementedError("requestWrite needs to be implemented")
def setAuthenticationState(self, authentication_state) -> None:
if self._authentication_state != authentication_state:
self._authentication_state = authentication_state
self.authenticationStateChanged.emit()
@pyqtProperty(int, notify=authenticationStateChanged)
def authenticationState(self) -> int:
return self._authentication_state
def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
# If we don't do this, the device might trigger a timeout.
self._last_response_time = time()
return compressed_data
def _compressGCode(self) -> Optional[bytes]:
self._compressing_gcode = True
## Mash the data into single string
max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
file_data_bytes_list = []
batched_lines = []
batched_lines_count = 0
for line in self._gcode:
if not self._compressing_gcode:
self._progress_message.hide()
# Stop trying to zip / send as abort was called.
return None
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
batched_lines.append(line)
batched_lines_count += len(line)
if batched_lines_count >= max_chars_per_line:
file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
batched_lines = []
batched_lines_count = 0
# Don't miss the last batch (If any)
if len(batched_lines) != 0:
file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
self._compressing_gcode = False
return b"".join(file_data_bytes_list)
def _update(self) -> bool:
if self._last_response_time:
time_since_last_response = time() - self._last_response_time
else:
time_since_last_response = 0
if self._last_request_time:
time_since_last_request = time() - self._last_request_time
else:
time_since_last_request = float("inf") # An irrelevantly large number of seconds
if time_since_last_response > self._timeout_time >= time_since_last_request:
# Go (or stay) into timeout.
if self._connection_state_before_timeout is None:
self._connection_state_before_timeout = self._connection_state
self.setConnectionState(ConnectionState.closed)
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep.
if time_since_last_response > self._recreate_network_manager_time:
if self._last_manager_create_time is None:
self._createNetworkManager()
if time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager()
elif self._connection_state == ConnectionState.closed:
# Go out of timeout.
self.setConnectionState(self._connection_state_before_timeout)
self._connection_state_before_timeout = None
return True
def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json") -> QNetworkRequest:
url = QUrl("http://" + self._address + self._api_prefix + target)
request = QNetworkRequest(url)
if content_type is not None:
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
return request
def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart:
part = QHttpPart()
if not content_header.startswith("form-data;"):
content_header = "form_data; " + content_header
part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
if content_type is not None:
part.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
part.setBody(data)
return part
## Convenience function to get the username from the OS.
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
def _getUserName(self) -> str:
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
user = os.environ.get(name)
if user:
return user
return "Unknown User" # Couldn't find out username.
def _clearCachedMultiPart(self, reply: QNetworkReply) -> None:
if reply in self._kept_alive_multiparts:
del self._kept_alive_multiparts[reply]
def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, onFinished)
def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, onFinished)
def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.post(request, data)
if onProgress is not None:
reply.uploadProgress.connect(onProgress)
self._registerOnFinishedCallback(reply, onFinished)
def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
for part in parts:
multi_post_part.append(part)
self._last_request_time = time()
reply = self._manager.post(request, multi_post_part)
self._kept_alive_multiparts[reply] = multi_post_part
if onProgress is not None:
reply.uploadProgress.connect(onProgress)
self._registerOnFinishedCallback(reply, onFinished)
return reply
def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
post_part = QHttpPart()
post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
post_part.setBody(body_data)
self.postFormWithParts(target, [post_part], onFinished, onProgress)
def _onAuthenticationRequired(self, reply, authenticator) -> None:
Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
def _createNetworkManager(self) -> None:
Logger.log("d", "Creating network manager")
if self._manager:
self._manager.finished.disconnect(self.__handleOnFinished)
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
self._manager = QNetworkAccessManager()
self._manager.finished.connect(self.__handleOnFinished)
self._last_manager_create_time = time()
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if onFinished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
def __handleOnFinished(self, reply: QNetworkReply) -> None:
# Due to garbage collection, we need to cache certain bits of post operations.
# As we don't want to keep them around forever, delete them if we get a reply.
if reply.operation() == QNetworkAccessManager.PostOperation:
self._clearCachedMultiPart(reply)
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
# No status code means it never even reached remote.
return
self._last_response_time = time()
if self._connection_state == ConnectionState.connecting:
self.setConnectionState(ConnectionState.connected)
callback_key = reply.url().toString() + str(reply.operation())
try:
if callback_key in self._onFinishedCallbacks:
self._onFinishedCallbacks[callback_key](reply)
except Exception:
Logger.logException("w", "something went wrong with callback")
@pyqtSlot(str, result=str)
def getProperty(self, key: str) -> str:
bytes_key = key.encode("utf-8")
if bytes_key in self._properties:
return self._properties.get(bytes_key, b"").decode("utf-8")
else:
return ""
def getProperties(self):
return self._properties
## Get the unique key of this machine
# \return key String containing the key of the machine.
@pyqtProperty(str, constant=True)
def key(self) -> str:
return self._id
## The IP address of the printer.
@pyqtProperty(str, constant=True)
def address(self) -> str:
return self._properties.get(b"address", b"").decode("utf-8")
## Name of the printer (as returned from the ZeroConf properties)
@pyqtProperty(str, constant=True)
def name(self) -> str:
return self._properties.get(b"name", b"").decode("utf-8")
## Firmware version (as returned from the ZeroConf properties)
@pyqtProperty(str, constant=True)
def firmwareVersion(self) -> str:
return self._properties.get(b"firmware_version", b"").decode("utf-8")
## IPadress of this printer
@pyqtProperty(str, constant=True)
def ipAddress(self) -> str:
return self._address

View file

@ -0,0 +1,101 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class PrintJobOutputModel(QObject):
stateChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
timeElapsedChanged = pyqtSignal()
nameChanged = pyqtSignal()
keyChanged = pyqtSignal()
assignedPrinterChanged = pyqtSignal()
ownerChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None):
super().__init__(parent)
self._output_controller = output_controller
self._state = ""
self._time_total = 0
self._time_elapsed = 0
self._name = name # Human readable name
self._key = key # Unique identifier
self._assigned_printer = None # type: Optional[PrinterOutputModel]
self._owner = "" # Who started/owns the print job?
@pyqtProperty(str, notify=ownerChanged)
def owner(self):
return self._owner
def updateOwner(self, owner):
if self._owner != owner:
self._owner = owner
self.ownerChanged.emit()
@pyqtProperty(QObject, notify=assignedPrinterChanged)
def assignedPrinter(self):
return self._assigned_printer
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"):
if self._assigned_printer != assigned_printer:
old_printer = self._assigned_printer
self._assigned_printer = assigned_printer
if old_printer is not None:
# If the previously assigned printer is set, this job is moved away from it.
old_printer.updateActivePrintJob(None)
self.assignedPrinterChanged.emit()
@pyqtProperty(str, notify=keyChanged)
def key(self):
return self._key
def updateKey(self, key: str):
if self._key != key:
self._key = key
self.keyChanged.emit()
@pyqtProperty(str, notify = nameChanged)
def name(self):
return self._name
def updateName(self, name: str):
if self._name != name:
self._name = name
self.nameChanged.emit()
@pyqtProperty(int, notify = timeTotalChanged)
def timeTotal(self):
return self._time_total
@pyqtProperty(int, notify = timeElapsedChanged)
def timeElapsed(self):
return self._time_elapsed
@pyqtProperty(str, notify=stateChanged)
def state(self):
return self._state
def updateTimeTotal(self, new_time_total):
if self._time_total != new_time_total:
self._time_total = new_time_total
self.timeTotalChanged.emit()
def updateTimeElapsed(self, new_time_elapsed):
if self._time_elapsed != new_time_elapsed:
self._time_elapsed = new_time_elapsed
self.timeElapsedChanged.emit()
def updateState(self, new_state):
if self._state != new_state:
self._state = new_state
self.stateChanged.emit()
@pyqtSlot(str)
def setState(self, state):
self._output_controller.setJobState(self, state)

View file

@ -0,0 +1,46 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class PrinterOutputController:
def __init__(self, output_device):
self.can_pause = True
self.can_abort = True
self.can_pre_heat_bed = True
self.can_control_manually = True
self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int):
Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str):
Logger.log("w", "Set job state not implemented in controller")
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
Logger.log("w", "Cancel preheat bed not implemented in controller")
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
Logger.log("w", "Preheat bed not implemented in controller")
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
Logger.log("w", "Set head position not implemented in controller")
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
Logger.log("w", "Move head not implemented in controller")
def homeBed(self, printer):
Logger.log("w", "Home bed not implemented in controller")
def homeHead(self, printer):
Logger.log("w", "Home head not implemented in controller")

View file

@ -0,0 +1,240 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from UM.Logger import Logger
from typing import Optional, List
from UM.Math.Vector import Vector
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
class PrinterOutputModel(QObject):
bedTemperatureChanged = pyqtSignal()
targetBedTemperatureChanged = pyqtSignal()
isPreheatingChanged = pyqtSignal()
stateChanged = pyqtSignal()
activePrintJobChanged = pyqtSignal()
nameChanged = pyqtSignal()
headPositionChanged = pyqtSignal()
keyChanged = pyqtSignal()
typeChanged = pyqtSignal()
cameraChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""):
super().__init__(parent)
self._bed_temperature = -1 # Use -1 for no heated bed.
self._target_bed_temperature = 0
self._name = ""
self._key = "" # Unique identifier
self._controller = output_controller
self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)]
self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version
self._printer_state = "unknown"
self._is_preheating = False
self._type = ""
self._camera = None
@pyqtProperty(str, constant = True)
def firmwareVersion(self):
return self._firmware_version
def setCamera(self, camera):
if self._camera is not camera:
self._camera = camera
self.cameraChanged.emit()
def updateIsPreheating(self, pre_heating):
if self._is_preheating != pre_heating:
self._is_preheating = pre_heating
self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self):
return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged)
def camera(self):
return self._camera
@pyqtProperty(str, notify = typeChanged)
def type(self):
return self._type
def updateType(self, type):
if self._type != type:
self._type = type
self.typeChanged.emit()
@pyqtProperty(str, notify=keyChanged)
def key(self):
return self._key
def updateKey(self, key: str):
if self._key != key:
self._key = key
self.keyChanged.emit()
@pyqtSlot()
def homeHead(self):
self._controller.homeHead(self)
@pyqtSlot()
def homeBed(self):
self._controller.homeBed(self)
@pyqtProperty("QVariantList", constant = True)
def extruders(self):
return self._extruders
@pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self):
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z}
def updateHeadPosition(self, x, y, z):
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
self._head_position = Vector(x, y, z)
self.headPositionChanged.emit()
@pyqtProperty("long", "long", "long")
@pyqtProperty("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self.updateHeadPosition(x, y, z)
self._controller.setHeadPosition(self, x, y, z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadX(self, x, speed = 3000):
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadY(self, y, speed = 3000):
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadZ(self, z, speed = 3000):
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
self._controller.preheatBed(self, temperature, duration)
@pyqtSlot()
def cancelPreheatBed(self):
self._controller.cancelPreheatBed(self)
def getController(self):
return self._controller
@pyqtProperty(str, notify=nameChanged)
def name(self):
return self._name
def setName(self, name):
self._setName(name)
self.updateName(name)
def updateName(self, name):
if self._name != name:
self._name = name
self.nameChanged.emit()
## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature):
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature)
def updateActivePrintJob(self, print_job):
if self._active_print_job != print_job:
old_print_job = self._active_print_job
if print_job is not None:
print_job.updateAssignedPrinter(self)
self._active_print_job = print_job
if old_print_job is not None:
old_print_job.updateAssignedPrinter(None)
self.activePrintJobChanged.emit()
def updateState(self, printer_state):
if self._printer_state != printer_state:
self._printer_state = printer_state
self.stateChanged.emit()
@pyqtProperty(QObject, notify = activePrintJobChanged)
def activePrintJob(self):
return self._active_print_job
@pyqtProperty(str, notify=stateChanged)
def state(self):
return self._printer_state
@pyqtProperty(int, notify = bedTemperatureChanged)
def bedTemperature(self):
return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged)
def targetBedTemperature(self):
return self._target_bed_temperature
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
if self._controller:
return self._controller.can_pre_heat_bed
return False
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
if self._controller:
return self._controller.can_pause
return False
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
if self._controller:
return self._controller.can_abort
return False
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
if self._controller:
return self._controller.can_control_manually
return False

View file

View file

@ -3,15 +3,21 @@
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Application import Application from UM.Application import Application
from enum import IntEnum # For the connection state tracking.
from typing import List, Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device. ## Printer output device adds extra interface options on top of output device.
@ -25,662 +31,149 @@ i18n_catalog = i18nCatalog("cura")
# For all other uses it should be used in the same way as a "regular" OutputDevice. # For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter @signalemitter
class PrinterOutputDevice(QObject, OutputDevice): class PrinterOutputDevice(QObject, OutputDevice):
printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal()
# Signal to indicate that the material of the active printer on the remote changed.
materialIdChanged = pyqtSignal()
# # Signal to indicate that the hotend of the active printer on the remote changed.
hotendIdChanged = pyqtSignal()
# Signal to indicate that the info text about the connection has changed.
connectionTextChanged = pyqtSignal()
def __init__(self, device_id, parent = None): def __init__(self, device_id, parent = None):
super().__init__(device_id = device_id, parent = parent) super().__init__(device_id = device_id, parent = parent)
self._container_registry = ContainerRegistry.getInstance() self._printers = [] # type: List[PrinterOutputModel]
self._target_bed_temperature = 0
self._bed_temperature = 0
self._num_extruders = 1
self._hotend_temperatures = [0] * self._num_extruders
self._target_hotend_temperatures = [0] * self._num_extruders
self._material_ids = [""] * self._num_extruders
self._hotend_ids = [""] * self._num_extruders
self._progress = 0
self._head_x = 0
self._head_y = 0
self._head_z = 0
self._connection_state = ConnectionState.closed
self._connection_text = ""
self._time_elapsed = 0
self._time_total = 0
self._job_state = ""
self._job_name = ""
self._error_text = ""
self._accepts_commands = True
self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds.
self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still.
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed)
self._printer_state = ""
self._printer_type = "unknown"
self._camera_active = False
self._monitor_view_qml_path = "" self._monitor_view_qml_path = ""
self._monitor_component = None
self._monitor_item = None self._monitor_item = None
self._control_view_qml_path = "" self._control_view_qml_path = ""
self._control_component = None
self._control_item = None self._control_item = None
self._qml_context = None self._qml_context = None
self._can_pause = True self._accepts_commands = False
self._can_abort = True
self._can_pre_heat_bed = True
self._can_control_manually = True
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): self._update_timer = QTimer()
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
self._update_timer.setSingleShot(False)
self._update_timer.timeout.connect(self._update)
self._connection_state = ConnectionState.closed
self._address = ""
self._connection_text = ""
@pyqtProperty(str, notify = connectionTextChanged)
def address(self):
return self._address
def setConnectionText(self, connection_text):
if self._connection_text != connection_text:
self._connection_text = connection_text
self.connectionTextChanged.emit()
@pyqtProperty(str, constant=True)
def connectionText(self):
return self._connection_text
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
def isConnected(self):
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
def _update(self):
pass
def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]:
for printer in self._printers:
if printer.key == key:
return printer
return None
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
## Signals @pyqtProperty(QObject, notify = printersChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
if len(self._printers):
return self._printers[0]
return None
# Signal to be emitted when bed temp is changed @pyqtProperty("QVariantList", notify = printersChanged)
bedTemperatureChanged = pyqtSignal() def printers(self):
return self._printers
# Signal to be emitted when target bed temp is changed
targetBedTemperatureChanged = pyqtSignal()
# Signal when the progress is changed (usually when this output device is printing / sending lots of data)
progressChanged = pyqtSignal()
# Signal to be emitted when hotend temp is changed
hotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when target hotend temp is changed
targetHotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when head position is changed (x,y,z)
headPositionChanged = pyqtSignal()
# Signal to be emitted when either of the material ids is changed
materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal to be emitted when either of the hotend ids is changed
hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal that is emitted every time connection state is changed.
# it also sends it's own device_id (for convenience sake)
connectionStateChanged = pyqtSignal(str)
connectionTextChanged = pyqtSignal()
timeElapsedChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
jobStateChanged = pyqtSignal()
jobNameChanged = pyqtSignal()
errorTextChanged = pyqtSignal()
acceptsCommandsChanged = pyqtSignal()
printerStateChanged = pyqtSignal()
printerTypeChanged = pyqtSignal()
# Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally).
preheatBedRemainingTimeChanged = pyqtSignal()
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
return self._can_pre_heat_bed
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
return self._can_pause
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
return self._can_abort
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
return self._can_control_manually
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant=True)
def monitorItem(self): def monitorItem(self):
# Note that we specifically only check if the monitor component is created. # Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to # It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time. # create the item (and fail) every time.
if not self._monitor_item: if not self._monitor_component:
self._createMonitorViewFromQML() self._createMonitorViewFromQML()
return self._monitor_item return self._monitor_item
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant=True)
def controlItem(self): def controlItem(self):
if not self._control_item: if not self._control_component:
self._createControlViewFromQML() self._createControlViewFromQML()
return self._control_item return self._control_item
def _createControlViewFromQML(self): def _createControlViewFromQML(self):
if not self._control_view_qml_path: if not self._control_view_qml_path:
return return
if self._control_item is None:
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, { self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
"OutputDevice": self
})
def _createMonitorViewFromQML(self): def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path: if not self._monitor_view_qml_path:
return return
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, { if self._monitor_item is None:
"OutputDevice": self self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
})
@pyqtProperty(str, notify=printerTypeChanged)
def printerType(self):
return self._printer_type
@pyqtProperty(str, notify=printerStateChanged)
def printerState(self):
return self._printer_state
@pyqtProperty(str, notify = jobStateChanged)
def jobState(self):
return self._job_state
def _updatePrinterType(self, printer_type):
if self._printer_type != printer_type:
self._printer_type = printer_type
self.printerTypeChanged.emit()
def _updatePrinterState(self, printer_state):
if self._printer_state != printer_state:
self._printer_state = printer_state
self.printerStateChanged.emit()
def _updateJobState(self, job_state):
if self._job_state != job_state:
self._job_state = job_state
self.jobStateChanged.emit()
@pyqtSlot(str)
def setJobState(self, job_state):
self._setJobState(job_state)
def _setJobState(self, job_state):
Logger.log("w", "_setJobState is not implemented by this output device")
@pyqtSlot()
def startCamera(self):
self._camera_active = True
self._startCamera()
def _startCamera(self):
Logger.log("w", "_startCamera is not implemented by this output device")
@pyqtSlot()
def stopCamera(self):
self._camera_active = False
self._stopCamera()
def _stopCamera(self):
Logger.log("w", "_stopCamera is not implemented by this output device")
@pyqtProperty(str, notify = jobNameChanged)
def jobName(self):
return self._job_name
def setJobName(self, name):
if self._job_name != name:
self._job_name = name
self.jobNameChanged.emit()
## Gives a human-readable address where the device can be found.
@pyqtProperty(str, constant = True)
def address(self):
Logger.log("w", "address is not implemented by this output device.")
## A human-readable name for the device.
@pyqtProperty(str, constant = True)
def name(self):
Logger.log("w", "name is not implemented by this output device.")
return ""
@pyqtProperty(str, notify = errorTextChanged)
def errorText(self):
return self._error_text
## Set the error-text that is shown in the print monitor in case of an error
def setErrorText(self, error_text):
if self._error_text != error_text:
self._error_text = error_text
self.errorTextChanged.emit()
@pyqtProperty(bool, notify = acceptsCommandsChanged)
def acceptsCommands(self):
return self._accepts_commands
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def setAcceptsCommands(self, accepts_commands):
if self._accepts_commands != accepts_commands:
self._accepts_commands = accepts_commands
self.acceptsCommandsChanged.emit()
## Get the bed temperature of the bed (if any)
# This function is "final" (do not re-implement)
# /sa _getBedTemperature implementation function
@pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self):
return self._bed_temperature
## Set the (target) bed temperature
# This function is "final" (do not re-implement)
# /param temperature new target temperature of the bed (in deg C)
# /sa _setTargetBedTemperature implementation function
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
self._setTargetBedTemperature(temperature)
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## The total duration of the time-out to pre-heat the bed, in seconds.
#
# \return The duration of the time-out to pre-heat the bed, in seconds.
@pyqtProperty(int, constant = True)
def preheatBedTimeout(self):
return self._preheat_bed_timeout
## The remaining duration of the pre-heating of the bed.
#
# This is formatted in M:SS format.
# \return The duration of the time-out to pre-heat the bed, formatted.
@pyqtProperty(str, notify = preheatBedRemainingTimeChanged)
def preheatBedRemainingTime(self):
if not self._preheat_bed_timer.isActive():
return ""
period = self._preheat_bed_timer.remainingTime()
if period <= 0:
return ""
minutes, period = divmod(period, 60000) #60000 milliseconds in a minute.
seconds, _ = divmod(period, 1000) #1000 milliseconds in a second.
if minutes <= 0 and seconds <= 0:
return ""
return "%d:%02d" % (minutes, seconds)
## Time the print has been printing.
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify = timeElapsedChanged)
def timeElapsed(self):
return self._time_elapsed
## Total time of the print
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify=timeTotalChanged)
def timeTotal(self):
return self._time_total
@pyqtSlot(float)
def setTimeTotal(self, new_total):
if self._time_total != new_total:
self._time_total = new_total
self.timeTotalChanged.emit()
@pyqtSlot(float)
def setTimeElapsed(self, time_elapsed):
if self._time_elapsed != time_elapsed:
self._time_elapsed = time_elapsed
self.timeElapsedChanged.emit()
## Home the head of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeHead implementation function
@pyqtSlot()
def homeHead(self):
self._homeHead()
## Home the head of the connected printer
# This is an implementation function and should be overriden by children.
def _homeHead(self):
Logger.log("w", "_homeHead is not implemented by this output device")
## Home the bed of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeBed implementation function
@pyqtSlot()
def homeBed(self):
self._homeBed()
## Home the bed of the connected printer
# This is an implementation function and should be overriden by children.
# /sa homeBed
def _homeBed(self):
Logger.log("w", "_homeBed is not implemented by this output device")
## Protected setter for the bed temperature of the connected printer (if any).
# /parameter temperature Temperature bed needs to go to (in deg celsius)
# /sa setTargetBedTemperature
def _setTargetBedTemperature(self, temperature):
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
Logger.log("w", "preheatBed is not implemented by this output device.")
## Cancels pre-heating the heated bed of the printer.
#
# If the bed is not pre-heated, nothing happens.
@pyqtSlot()
def cancelPreheatBed(self):
Logger.log("w", "cancelPreheatBed is not implemented by this output device.")
## Protected setter for the current bed temperature.
# This simply sets the bed temperature, but ensures that a signal is emitted.
# /param temperature temperature of the bed.
def _setBedTemperature(self, temperature):
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
## Get the target bed temperature if connected printer (if any)
@pyqtProperty(int, notify = targetBedTemperatureChanged)
def targetBedTemperature(self):
return self._target_bed_temperature
## Set the (target) hotend temperature
# This function is "final" (do not re-implement)
# /param index the index of the hotend that needs to change temperature
# /param temperature The temperature it needs to change to (in deg celsius).
# /sa _setTargetHotendTemperature implementation function
@pyqtSlot(int, int)
def setTargetHotendTemperature(self, index, temperature):
self._setTargetHotendTemperature(index, temperature)
if self._target_hotend_temperatures[index] != temperature:
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
## Implementation function of setTargetHotendTemperature.
# /param index Index of the hotend to set the temperature of
# /param temperature Temperature to set the hotend to (in deg C)
# /sa setTargetHotendTemperature
def _setTargetHotendTemperature(self, index, temperature):
Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device")
@pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged)
def targetHotendTemperatures(self):
return self._target_hotend_temperatures
@pyqtProperty("QVariantList", notify = hotendTemperaturesChanged)
def hotendTemperatures(self):
return self._hotend_temperatures
## Protected setter for the current hotend temperature.
# This simply sets the hotend temperature, but ensures that a signal is emitted.
# /param index Index of the hotend
# /param temperature temperature of the hotend (in deg C)
def _setHotendTemperature(self, index, temperature):
if self._hotend_temperatures[index] != temperature:
self._hotend_temperatures[index] = temperature
self.hotendTemperaturesChanged.emit()
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialIds(self):
return self._material_ids
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialNames(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["name"])
else:
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
return result
## List of the colours of the currently loaded materials.
#
# The list is in order of extruders. If there is no material in an
# extruder, the colour is shown as transparent.
#
# The colours are returned in hex-format AARRGGBB or RRGGBB
# (e.g. #800000ff for transparent blue or #00ff00 for pure green).
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialColors(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append("#00000000") #No material.
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["color_code"])
else:
result.append("#00000000") #Unknown material.
return result
## Protected setter for the current material id.
# /param index Index of the extruder
# /param material_id id of the material
def _setMaterialId(self, index, material_id):
if material_id and material_id != "" and material_id != self._material_ids[index]:
Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id))
self._material_ids[index] = material_id
self.materialIdChanged.emit(index, material_id)
@pyqtProperty("QVariantList", notify = hotendIdChanged)
def hotendIds(self):
return self._hotend_ids
## Protected setter for the current hotend id.
# /param index Index of the extruder
# /param hotend_id id of the hotend
def _setHotendId(self, index, hotend_id):
if hotend_id and hotend_id != self._hotend_ids[index]:
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
self._hotend_ids[index] = hotend_id
self.hotendIdChanged.emit(index, hotend_id)
elif not hotend_id:
Logger.log("d", "Removing hotend id of hotend %d.", index)
self._hotend_ids[index] = None
self.hotendIdChanged.emit(index, None)
## Let the user decide if the hotends and/or material should be synced with the printer
# NB: the UX needs to be implemented by the plugin
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
## Attempt to establish connection ## Attempt to establish connection
def connect(self): def connect(self):
raise NotImplementedError("connect needs to be implemented") self.setConnectionState(ConnectionState.connecting)
self._update_timer.start()
## Attempt to close the connection ## Attempt to close the connection
def close(self): def close(self):
raise NotImplementedError("close needs to be implemented") self._update_timer.stop()
self.setConnectionState(ConnectionState.closed)
@pyqtProperty(bool, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
## Set the connection state of this output device.
# /param connection_state ConnectionState enum.
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionTextChanged)
def connectionText(self):
return self._connection_text
## Set a text that is shown on top of the print monitor tab
def setConnectionText(self, connection_text):
if self._connection_text != connection_text:
self._connection_text = connection_text
self.connectionTextChanged.emit()
## Ensure that close gets called when object is destroyed ## Ensure that close gets called when object is destroyed
def __del__(self): def __del__(self):
self.close() self.close()
## Get the x position of the head. @pyqtProperty(bool, notify=acceptsCommandsChanged)
# This function is "final" (do not re-implement) def acceptsCommands(self):
@pyqtProperty(float, notify = headPositionChanged) return self._accepts_commands
def headX(self):
return self._head_x
## Get the y position of the head. ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
# This function is "final" (do not re-implement) def _setAcceptsCommands(self, accepts_commands):
@pyqtProperty(float, notify = headPositionChanged) if self._accepts_commands != accepts_commands:
def headY(self): self._accepts_commands = accepts_commands
return self._head_y
## Get the z position of the head. self.acceptsCommandsChanged.emit()
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headZ(self):
return self._head_z
## Update the saved position of the head
# This function should be called when a new position for the head is received.
def _updateHeadPosition(self, x, y ,z):
position_changed = False
if self._head_x != x:
self._head_x = x
position_changed = True
if self._head_y != y:
self._head_y = y
position_changed = True
if self._head_z != z:
self._head_z = z
position_changed = True
if position_changed:
self.headPositionChanged.emit()
## Set the position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadPosition implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self._setHeadPosition(x, y , z, speed)
## Set the X position of the head.
# This function is "final" (do not re-implement)
# /param x x position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadx implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadX(self, x, speed = 3000):
self._setHeadX(x, speed)
## Set the Y position of the head.
# This function is "final" (do not re-implement)
# /param y y position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadY(self, y, speed = 3000):
self._setHeadY(y, speed)
## Set the Z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param z z position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadZ(self, z, speed = 3000):
self._setHeadZ(z, speed)
## Move the head of the printer.
# Note that this is a relative move. If you want to move the head to a specific position you can use
# setHeadPosition
# This function is "final" (do not re-implement)
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _moveHead implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._moveHead(x, y, z, speed)
## Implementation function of moveHead.
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa moveHead
def _moveHead(self, x, y, z, speed):
Logger.log("w", "_moveHead is not implemented by this output device")
## Implementation function of setHeadPosition.
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadPosition
def _setHeadPosition(self, x, y, z, speed):
Logger.log("w", "_setHeadPosition is not implemented by this output device")
## Implementation function of setHeadX.
# /param x new x location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadX
def _setHeadX(self, x, speed):
Logger.log("w", "_setHeadX is not implemented by this output device")
## Implementation function of setHeadY.
# /param y new y location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY
def _setHeadY(self, y, speed):
Logger.log("w", "_setHeadY is not implemented by this output device")
## Implementation function of setHeadZ.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ
def _setHeadZ(self, z, speed):
Logger.log("w", "_setHeadZ is not implemented by this output device")
## Get the progress of any currently active process.
# This function is "final" (do not re-implement)
# /sa _getProgress
# /returns float progress of the process. -1 indicates that there is no process.
@pyqtProperty(float, notify = progressChanged)
def progress(self):
return self._progress
## Set the progress of any currently active process
# /param progress Progress of the process.
def setProgress(self, progress):
if self._progress != progress:
self._progress = progress
self.progressChanged.emit()
## The current processing state of the backend. ## The current processing state of the backend.
@ -689,4 +182,4 @@ class ConnectionState(IntEnum):
connecting = 1 connecting = 1
connected = 2 connected = 2
busy = 3 busy = 3
error = 4 error = 4

View file

@ -1,332 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# This collects a lot of quality and quality changes related code which was split between ContainerManager
# and the MachineManager and really needs to usable from both.
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.ExtruderManager import ExtruderManager
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.DefinitionContainer import DefinitionContainerInterface
class QualityManager:
## Get the singleton instance for this class.
@classmethod
def getInstance(cls) -> "QualityManager":
# Note: Explicit use of class name to prevent issues with inheritance.
if not QualityManager.__instance:
QualityManager.__instance = cls()
return QualityManager.__instance
__instance = None # type: "QualityManager"
## Find a quality by name for a specific machine definition and materials.
#
# \param quality_name
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
# specified then the currently selected machine definition is used.
# \param material_containers_metadata If nothing is specified then the
# current set of selected materials is used.
# \return the matching quality container \type{InstanceContainer}
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers_metadata: Optional[List[Dict[str, Any]]] = None) -> Optional[InstanceContainer]:
criteria = {"type": "quality", "name": quality_name}
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
# Fall back to using generic materials and qualities if nothing could be found.
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
return result[0] if result else None
## Find a quality changes container by name.
#
# \param quality_changes_name \type{str} the name of the quality changes container.
# \param machine_definition (Optional) \type{DefinitionContainer} If nothing is
# specified then the currently selected machine definition is used..
# \return the matching quality changes containers \type{List[InstanceContainer]}
def findQualityChangesByName(self, quality_changes_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None):
if not machine_definition:
global_stack = Application.getGlobalContainerStack()
if not global_stack:
return [] #No stack, so no current definition could be found, so there are no quality changes either.
machine_definition = global_stack.definition
result = self.findAllQualityChangesForMachine(machine_definition)
result = [quality_change for quality_change in result if quality_change.getName() == quality_changes_name]
return result
## Fetch the list of available quality types for this combination of machine definition and materials.
#
# \param machine_definition \type{DefinitionContainer}
# \param material_containers \type{List[InstanceContainer]}
# \return \type{List[str]}
def findAllQualityTypesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[str]:
# Determine the common set of quality types which can be
# applied to all of the materials for this machine.
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
common_quality_types = set(quality_type_dict.keys())
for material_container in material_containers[1:]:
next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
common_quality_types.intersection_update(set(next_quality_type_dict.keys()))
return list(common_quality_types)
def findAllQualitiesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[InstanceContainer]:
# Determine the common set of quality types which can be
# applied to all of the materials for this machine.
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
qualities = set(quality_type_dict.values())
for material_container in material_containers[1:]:
next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
qualities.intersection_update(set(next_quality_type_dict.values()))
return list(qualities)
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
#
# \param machine_definition \type{DefinitionContainer} the machine definition.
# \param material \type{InstanceContainer} the material.
# \return \type{Dict[str, InstanceContainer]} the dict of suitable quality type names mapping to qualities.
def __fetchQualityTypeDictForMaterial(self, machine_definition: "DefinitionContainerInterface", material: InstanceContainer) -> Dict[str, InstanceContainer]:
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
quality_type_dict = {}
for quality in qualities:
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
return quality_type_dict
## Find a quality container by quality type.
#
# \param quality_type \type{str} the name of the quality type to search for.
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
# specified then the currently selected machine definition is used.
# \param material_containers_metadata If nothing is specified then the
# current set of selected materials is used.
# \return the matching quality container \type{InstanceContainer}
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs) -> InstanceContainer:
criteria = kwargs
criteria["type"] = "quality"
if quality_type:
criteria["quality_type"] = quality_type
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
# Fall back to using generic materials and qualities if nothing could be found.
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
if basic_materials:
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
return result[0] if result else None
## Find all suitable qualities for a combination of machine and material.
#
# \param machine_definition \type{DefinitionContainer} the machine definition.
# \param material_container \type{InstanceContainer} the material.
# \return \type{List[InstanceContainer]} the list of suitable qualities.
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
criteria = {"type": "quality"}
result = self._getFilteredContainersForStack(machine_definition, [material_container.getMetaData()], **criteria)
if not result:
basic_materials = self._getBasicMaterialMetadatas(material_container.getMetaData())
if basic_materials:
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
return result
## Find all quality changes for a machine.
#
# \param machine_definition \type{DefinitionContainer} the machine definition.
# \return \type{List[InstanceContainer]} the list of quality changes
def findAllQualityChangesForMachine(self, machine_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
if machine_definition.getMetaDataEntry("has_machine_quality"):
definition_id = machine_definition.getId()
else:
definition_id = "fdmprinter"
filter_dict = { "type": "quality_changes", "definition": definition_id }
quality_changes_list = ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
return quality_changes_list
def findAllExtruderDefinitionsForMachine(self, machine_definition: "DefinitionContainerInterface") -> List["DefinitionContainerInterface"]:
filter_dict = { "machine": machine_definition.getId() }
return ContainerRegistry.getInstance().findDefinitionContainers(**filter_dict)
## Find all quality changes for a given extruder.
#
# \param extruder_definition The extruder to find the quality changes for.
# \return The list of quality changes for the given extruder.
def findAllQualityChangesForExtruder(self, extruder_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
filter_dict = {"type": "quality_changes", "extruder": extruder_definition.getId()}
return ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
## Find all usable qualities for a machine and extruders.
#
# Finds all of the qualities for this combination of machine and extruders.
# Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
# then only one of then is returned (at random).
#
# \param global_container_stack \type{GlobalStack} the global machine definition
# \param extruder_stacks \type{List[ExtruderStack]} the list of extruder stacks
# \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
# return come from the first extruder in the given list of extruders.
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack: "GlobalStack", extruder_stacks: List["ExtruderStack"]) -> List[InstanceContainer]:
global_machine_definition = global_container_stack.getBottom()
machine_manager = Application.getInstance().getMachineManager()
active_stack_id = machine_manager.activeStackId
materials = []
for stack in extruder_stacks:
if stack.getId() == active_stack_id and machine_manager.newMaterial:
materials.append(machine_manager.newMaterial)
else:
materials.append(stack.material)
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
# Map the list of quality_types to InstanceContainers
qualities = self.findAllQualitiesForMachineMaterial(global_machine_definition, materials[0])
quality_type_dict = {}
for quality in qualities:
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
return [quality_type_dict[quality_type] for quality_type in quality_types]
## Fetch more basic versions of a material.
#
# This tries to find a generic or basic version of the given material.
# \param material_container \type{Dict[str, Any]} The metadata of a
# material to find the basic versions of.
# \return \type{List[Dict[str, Any]]} A list of the metadata of basic
# materials, or an empty list if none could be found.
def _getBasicMaterialMetadatas(self, material_container: Dict[str, Any]) -> List[Dict[str, Any]]:
if "definition" not in material_container:
definition_id = "fdmprinter"
else:
material_container_definition = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = material_container["definition"])
if not material_container_definition:
definition_id = "fdmprinter"
else:
material_container_definition = material_container_definition[0]
if "has_machine_quality" not in material_container_definition:
definition_id = "fdmprinter"
else:
definition_id = material_container_definition.get("quality_definition", material_container_definition["id"])
base_material = material_container.get("material")
if base_material:
# There is a basic material specified
criteria = {
"type": "material",
"name": base_material,
"definition": definition_id,
"variant": material_container.get("variant")
}
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
return containers
return []
def _getFilteredContainers(self, **kwargs):
return self._getFilteredContainersForStack(None, None, **kwargs)
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs):
# Fill in any default values.
if machine_definition is None:
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition_id is not None:
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition_id)[0]
if not material_metadata:
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks:
material_metadata = [stack.material.getMetaData() for stack in active_stacks]
criteria = kwargs
filter_by_material = False
machine_definition = self.getParentMachineDefinition(machine_definition)
criteria["definition"] = machine_definition.getId()
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
criteria["definition"] = definition_id
filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
# only fall back to "fdmprinter" when there is no container for this machine
elif not found_containers_with_machine_definition:
criteria["definition"] = "fdmprinter"
# Stick the material IDs in a set
material_ids = set()
for material_instance in material_metadata:
if material_instance is not None:
# Add the parent material too.
for basic_material in self._getBasicMaterialMetadatas(material_instance):
material_ids.add(basic_material["id"])
material_ids.add(material_instance["id"])
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
result = []
for container in containers:
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and "global_quality" not in kwargs:
continue
result.append(container)
return result
## Get the parent machine definition of a machine definition.
#
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
# an extruder definition.
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
# definition doesn't have a parent then it is simply returned.
def getParentMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
container_registry = ContainerRegistry.getInstance()
machine_entry = machine_definition.getMetaDataEntry("machine")
if machine_entry is None:
# We have a normal (whole) machine defintion
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition is not None:
parent_machine_definition = container_registry.findDefinitionContainers(id = quality_definition)[0]
return self.getParentMachineDefinition(parent_machine_definition)
else:
return machine_definition
else:
# This looks like an extruder. Find the rest of the machine.
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
parent_machine = self.getParentMachineDefinition(whole_machine)
if whole_machine is parent_machine:
# This extruder already belongs to a 'parent' machine def.
return machine_definition
else:
# Look up the corresponding extruder definition in the parent machine definition.
extruder_position = machine_definition.getMetaDataEntry("position")
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
return container_registry.findDefinitionContainers(id = parent_extruder_id)[0]
## Get the whole/global machine definition from an extruder definition.
#
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
# an extruder definition.
# \return \type{DefinitionContainerInterface}
def getWholeMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
machine_entry = machine_definition.getMetaDataEntry("machine")
if machine_entry is None:
# This already is a 'global' machine definition.
return machine_definition
else:
container_registry = ContainerRegistry.getInstance()
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
return whole_machine

View file

@ -0,0 +1,26 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
class BuildPlateDecorator(SceneNodeDecorator):
def __init__(self, build_plate_number = -1):
super().__init__()
self._build_plate_number = None
self.setBuildPlateNumber(build_plate_number)
def setBuildPlateNumber(self, nr):
# Make sure that groups are set correctly
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
self._build_plate_number = nr
if isinstance(self._node, CuraSceneNode):
self._node.transformChanged() # trigger refresh node without introducing a new signal
if self._node and self._node.callDecoration("isGroup"):
for child in self._node.getChildren():
child.callDecoration("setBuildPlateNumber", nr)
def getBuildPlateNumber(self):
return self._build_plate_number
def __deepcopy__(self, memo):
return BuildPlateDecorator()

View file

@ -7,7 +7,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from . import ConvexHullNode from cura.Scene import ConvexHullNode
import numpy import numpy

View file

@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode
from UM.Resources import Resources from UM.Resources import Resources
from UM.Math.Color import Color from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
@ -25,7 +24,10 @@ class ConvexHullNode(SceneNode):
self._original_parent = parent self._original_parent = parent
# Color of the drawn convex hull # Color of the drawn convex hull
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb()) if Application.getInstance().hasGui():
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
else:
self._color = Color(0, 0, 0)
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting. # The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
self._mesh_height = 0.1 self._mesh_height = 0.1
@ -66,7 +68,7 @@ class ConvexHullNode(SceneNode):
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
if self.getParent(): if self.getParent():
if self.getMeshData(): if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate:
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
if self._convex_hull_head_mesh: if self._convex_hull_head_mesh:
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)

View file

@ -0,0 +1,115 @@
from UM.Logger import Logger
from PyQt5.QtCore import Qt, pyqtSlot, QObject
from PyQt5.QtWidgets import QApplication
from cura.ObjectsModel import ObjectsModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from UM.Application import Application
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Signal import Signal
class CuraSceneController(QObject):
activeBuildPlateChanged = Signal()
def __init__(self, objects_model: ObjectsModel, multi_build_plate_model: MultiBuildPlateModel):
super().__init__()
self._objects_model = objects_model
self._multi_build_plate_model = multi_build_plate_model
self._active_build_plate = -1
self._last_selected_index = 0
self._max_build_plate = 1 # default
Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously
def updateMaxBuildPlate(self, *args):
if args:
source = args[0]
else:
source = None
if not isinstance(source, SceneNode):
return
max_build_plate = self._calcMaxBuildPlate()
changed = False
if max_build_plate != self._max_build_plate:
self._max_build_plate = max_build_plate
changed = True
if changed:
self._multi_build_plate_model.setMaxBuildPlate(self._max_build_plate)
build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)]
self._multi_build_plate_model.setItems(build_plates)
if self._active_build_plate > self._max_build_plate:
build_plate_number = 0
if self._last_selected_index >= 0: # go to the buildplate of the item you last selected
item = self._objects_model.getItem(self._last_selected_index)
if "node" in item:
node = item["node"]
build_plate_number = node.callDecoration("getBuildPlateNumber")
self.setActiveBuildPlate(build_plate_number)
# self.buildPlateItemsChanged.emit() # TODO: necessary after setItems?
def _calcMaxBuildPlate(self):
max_build_plate = 0
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if node.callDecoration("isSliceable"):
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is None:
build_plate_number = 0
max_build_plate = max(build_plate_number, max_build_plate)
return max_build_plate
## Either select or deselect an item
@pyqtSlot(int)
def changeSelection(self, index):
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier
shift_is_active = modifiers & Qt.ShiftModifier
if ctrl_is_active:
item = self._objects_model.getItem(index)
node = item["node"]
if Selection.isSelected(node):
Selection.remove(node)
else:
Selection.add(node)
elif shift_is_active:
polarity = 1 if index + 1 > self._last_selected_index else -1
for i in range(self._last_selected_index, index + polarity, polarity):
item = self._objects_model.getItem(i)
node = item["node"]
Selection.add(node)
else:
# Single select
item = self._objects_model.getItem(index)
node = item["node"]
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != -1:
self.setActiveBuildPlate(build_plate_number)
Selection.clear()
Selection.add(node)
self._last_selected_index = index
@pyqtSlot(int)
def setActiveBuildPlate(self, nr):
if nr == self._active_build_plate:
return
Logger.log("d", "Select build plate: %s" % nr)
self._active_build_plate = nr
Selection.clear()
self._multi_build_plate_model.setActiveBuildPlate(nr)
self._objects_model.setActiveBuildPlate(nr)
self.activeBuildPlateChanged.emit()
@staticmethod
def createCuraSceneController():
objects_model = Application.getInstance().getObjectsModel()
multi_build_plate_model = Application.getInstance().getMultiBuildPlateModel()
return CuraSceneController(objects_model = objects_model, multi_build_plate_model = multi_build_plate_model)

View file

@ -0,0 +1,92 @@
from typing import List
from UM.Application import Application
from UM.Scene.SceneNode import SceneNode
from copy import deepcopy
from cura.Settings.ExtrudersModel import ExtrudersModel
## Scene nodes that are models are only seen when selecting the corresponding build plate
# Note that many other nodes can just be UM SceneNode objects.
class CuraSceneNode(SceneNode):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._outside_buildarea = False
def setOutsideBuildArea(self, new_value):
self._outside_buildarea = new_value
def isOutsideBuildArea(self):
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
def isVisible(self):
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
def isSelectable(self) -> bool:
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
def getPrintingExtruder(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
per_mesh_stack = self.callDecoration("getStack")
extruders = list(global_container_stack.extruders.values())
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
return extruders[int(global_container_stack.getProperty("support_extruder_nr", "value"))]
# It's only set if you explicitly choose an extruder
extruder_id = self.callDecoration("getActiveExtruder")
for extruder in extruders:
# Find out the extruder if we know the id.
if extruder_id is not None:
if extruder_id == extruder.getId():
return extruder
else: # If the id is unknown, then return the extruder in the position 0
try:
if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero
return extruder
except ValueError:
continue
# This point should never be reached
return None
## Return the color of the material used to print this model
def getDiffuseColor(self) -> List[float]:
printing_extruder = self.getPrintingExtruder()
material_color = "#808080" # Fallback color
if printing_extruder is not None and printing_extruder.material:
material_color = printing_extruder.material.getMetaDataEntry("color_code", default = material_color)
# Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
# an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
return [
int(material_color[1:3], 16) / 255,
int(material_color[3:5], 16) / 255,
int(material_color[5:7], 16) / 255,
1.0
]
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo):
copy = CuraSceneNode()
copy.setTransformation(self.getLocalTransformation())
copy.setMeshData(self._mesh_data)
copy.setVisible(deepcopy(self._visible, memo))
copy._selectable = deepcopy(self._selectable, memo)
copy._name = deepcopy(self._name, memo)
for decorator in self._decorators:
copy.addDecorator(deepcopy(decorator, memo))
for child in self._children:
copy.addChild(deepcopy(child, memo))
self.calculateBoundingBoxMesh()
return copy
def transformChanged(self) -> None:
self._transformChanged()

0
cura/Scene/__init__.py Normal file
View file

View file

@ -1,16 +1,14 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy
import os.path import os.path
import urllib import urllib.parse
import uuid import uuid
from typing import Any, Dict, List, Union from typing import Dict, Union
from PyQt5.QtCore import QObject, QUrl, QVariant from PyQt5.QtCore import QObject, QUrl, QVariant
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Util import parseBool
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.SaveFile import SaveFile from UM.SaveFile import SaveFile
@ -22,7 +20,6 @@ from UM.Application import Application
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from cura.QualityManager import QualityManager
from UM.MimeTypeDatabase import MimeTypeNotFoundError from UM.MimeTypeDatabase import MimeTypeNotFoundError
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -30,9 +27,11 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Manager class that contains common actions to deal with containers in Cura. ## Manager class that contains common actions to deal with containers in Cura.
# #
# This is primarily intended as a class to be able to perform certain actions # This is primarily intended as a class to be able to perform certain actions
@ -42,154 +41,12 @@ class ContainerManager(QObject):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance()
self._container_registry = ContainerRegistry.getInstance() self._container_registry = ContainerRegistry.getInstance()
self._machine_manager = Application.getInstance().getMachineManager() self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
self._container_name_filters = {} self._container_name_filters = {}
## Create a duplicate of the specified container
#
# This will create and add a duplicate of the container corresponding
# to the container ID.
#
# \param container_id \type{str} The ID of the container to duplicate.
#
# \return The ID of the new container, or an empty string if duplication failed.
@pyqtSlot(str, result = str)
def duplicateContainer(self, container_id):
#TODO: It should be able to duplicate a container of which only the metadata is known.
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
return ""
container = containers[0]
new_container = self.duplicateContainerInstance(container)
return new_container.getId()
## Create a duplicate of the given container instance
#
# This will create and add a duplicate of the container that was passed.
#
# \param container \type{ContainerInterface} The container to duplicate.
#
# \return The duplicated container, or None if duplication failed.
def duplicateContainerInstance(self, container):
new_container = None
new_name = self._container_registry.uniqueName(container.getName())
# Only InstanceContainer has a duplicate method at the moment.
# So fall back to serialize/deserialize when no duplicate method exists.
if hasattr(container, "duplicate"):
new_container = container.duplicate(new_name)
else:
new_container = container.__class__(new_name)
new_container.deserialize(container.serialize())
new_container.setName(new_name)
# TODO: we probably don't want to add it to the registry here!
if new_container:
self._container_registry.addContainer(new_container)
return new_container
## Change the name of a specified container to a new name.
#
# \param container_id \type{str} The ID of the container to change the name of.
# \param new_id \type{str} The new ID of the container.
# \param new_name \type{str} The new name of the specified container.
#
# \return True if successful, False if not.
@pyqtSlot(str, str, str, result = bool)
def renameContainer(self, container_id, new_id, new_name):
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could rename container %s because it was not found.", container_id)
return False
container = containers[0]
# First, remove the container from the registry. This will clean up any files related to the container.
self._container_registry.removeContainer(container_id)
# Ensure we have a unique name for the container
new_name = self._container_registry.uniqueName(new_name)
# Then, update the name and ID of the container
container.setName(new_name)
container._id = new_id # TODO: Find a nicer way to set a new, unique ID
# Finally, re-add the container so it will be properly serialized again.
self._container_registry.addContainer(container)
return True
## Remove the specified container.
#
# \param container_id \type{str} The ID of the container to remove.
#
# \return True if the container was successfully removed, False if not.
@pyqtSlot(str, result = bool)
def removeContainer(self, container_id):
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could not remove container %s because it was not found.", container_id)
return False
self._container_registry.removeContainer(containers[0].getId())
return True
## Merge a container with another.
#
# This will try to merge one container into the other, by going through the container
# and setting the right properties on the other container.
#
# \param merge_into_id \type{str} The ID of the container to merge into.
# \param merge_id \type{str} The ID of the container to merge.
#
# \return True if successfully merged, False if not.
@pyqtSlot(str, result = bool)
def mergeContainers(self, merge_into_id, merge_id):
containers = self._container_registry.findContainers(id = merge_into_id)
if not containers:
Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
return False
merge_into = containers[0]
containers = self._container_registry.findContainers(id = merge_id)
if not containers:
Logger.log("w", "Could not merge container %s because it was not found", merge_id)
return False
merge = containers[0]
if not isinstance(merge, type(merge_into)):
Logger.log("w", "Cannot merge two containers of different types")
return False
self._performMerge(merge_into, merge)
return True
## Clear the contents of a container.
#
# \param container_id \type{str} The ID of the container to clear.
#
# \return True if successful, False if not.
@pyqtSlot(str, result = bool)
def clearContainer(self, container_id):
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot clear read-only container %s", container_id)
return False
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could clear container %s because it was not found.", container_id)
return False
containers[0].clear()
return True
@pyqtSlot(str, str, result=str) @pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id, entry_name): def getContainerMetaDataEntry(self, container_id, entry_name):
metadatas = self._container_registry.findContainersMetadata(id = container_id) metadatas = self._container_registry.findContainersMetadata(id = container_id)
@ -211,18 +68,15 @@ class ContainerManager(QObject):
# \param entry_value The new value of the entry. # \param entry_value The new value of the entry.
# #
# \return True if successful, False if not. # \return True if successful, False if not.
@pyqtSlot(str, str, str, result = bool) # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value): @pyqtSlot("QVariant", str, str)
if self._container_registry.isReadOnly(container_id): def setContainerMetaDataEntry(self, container_node, entry_name, entry_value):
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id) root_material_id = container_node.metadata["base_file"]
if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
return False return False
containers = self._container_registry.findContainers(id = container_id) #We need the complete container, since we need to know whether the container is read-only or not. material_group = self._material_manager.getMaterialGroup(root_material_id)
if not containers:
Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
return False
container = containers[0]
entries = entry_name.split("/") entries = entry_name.split("/")
entry_name = entries.pop() entry_name = entries.pop()
@ -230,7 +84,7 @@ class ContainerManager(QObject):
sub_item_changed = False sub_item_changed = False
if entries: if entries:
root_name = entries.pop(0) root_name = entries.pop(0)
root = container.getMetaDataEntry(root_name) root = material_group.root_material_node.metadata.get(root_name)
item = root item = root
for _ in range(len(entries)): for _ in range(len(entries)):
@ -243,12 +97,11 @@ class ContainerManager(QObject):
entry_name = root_name entry_name = root_name
entry_value = root entry_value = root
container = material_group.root_material_node.getContainer()
container.setMetaDataEntry(entry_name, entry_value) container.setMetaDataEntry(entry_name, entry_value)
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed. if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
container.metaDataChanged.emit(container) container.metaDataChanged.emit(container)
return True
## Set a setting property of the specified container. ## Set a setting property of the specified container.
# #
# This will set the specified property of the specified setting of the container # This will set the specified property of the specified setting of the container
@ -306,60 +159,6 @@ class ContainerManager(QObject):
return container.getProperty(setting_key, property_name) return container.getProperty(setting_key, property_name)
## Set the name of the specified container.
@pyqtSlot(str, str, result = bool)
def setContainerName(self, container_id, new_name):
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot set name of read-only container %s.", container_id)
return False
containers = self._container_registry.findContainers(id = container_id) #We need to get the full container, not just metadata, since we need to know whether it's read-only.
if not containers:
Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
return False
containers[0].setName(new_name)
return True
## Find instance containers matching certain criteria.
#
# This effectively forwards to
# ContainerRegistry::findInstanceContainersMetadata.
#
# \param criteria A dict of key - value pairs to search for.
#
# \return A list of container IDs that match the given criteria.
@pyqtSlot("QVariantMap", result = "QVariantList")
def findInstanceContainers(self, criteria):
return [entry["id"] for entry in self._container_registry.findInstanceContainersMetadata(**criteria)]
@pyqtSlot(str, result = bool)
def isContainerUsed(self, container_id):
Logger.log("d", "Checking if container %s is currently used", container_id)
# check if this is a material container. If so, check if any material with the same base is being used by any
# stacks.
container_ids_to_check = [container_id]
container_results = self._container_registry.findInstanceContainersMetadata(id = container_id, type = "material")
if container_results:
this_container = container_results[0]
material_base_file = this_container["id"]
if "base_file" in this_container:
material_base_file = this_container["base_file"]
# check all material container IDs with the same base
material_containers = self._container_registry.findInstanceContainersMetadata(base_file = material_base_file,
type = "material")
if material_containers:
container_ids_to_check = [container["id"] for container in material_containers]
all_stacks = self._container_registry.findContainerStacks()
for stack in all_stacks:
for used_container_id in container_ids_to_check:
if used_container_id in [child.getId() for child in stack.getContainers()]:
Logger.log("d", "The container is in use by %s", stack.getId())
return True
return False
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def makeUniqueName(self, original_name): def makeUniqueName(self, original_name):
return self._container_registry.uniqueName(original_name) return self._container_registry.uniqueName(original_name)
@ -399,7 +198,7 @@ class ContainerManager(QObject):
@pyqtSlot(str, str, QUrl, result = "QVariantMap") @pyqtSlot(str, str, QUrl, result = "QVariantMap")
def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
if not container_id or not file_type or not file_url_or_string: if not container_id or not file_type or not file_url_or_string:
return { "status": "error", "message": "Invalid arguments"} return {"status": "error", "message": "Invalid arguments"}
if isinstance(file_url_or_string, QUrl): if isinstance(file_url_or_string, QUrl):
file_url = file_url_or_string.toLocalFile() file_url = file_url_or_string.toLocalFile()
@ -407,20 +206,20 @@ class ContainerManager(QObject):
file_url = file_url_or_string file_url = file_url_or_string
if not file_url: if not file_url:
return { "status": "error", "message": "Invalid path"} return {"status": "error", "message": "Invalid path"}
mime_type = None mime_type = None
if not file_type in self._container_name_filters: if file_type not in self._container_name_filters:
try: try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url) mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
except MimeTypeNotFoundError: except MimeTypeNotFoundError:
return { "status": "error", "message": "Unknown File Type" } return {"status": "error", "message": "Unknown File Type"}
else: else:
mime_type = self._container_name_filters[file_type]["mime"] mime_type = self._container_name_filters[file_type]["mime"]
containers = self._container_registry.findContainers(id = container_id) containers = self._container_registry.findContainers(id = container_id)
if not containers: if not containers:
return { "status": "error", "message": "Container not found"} return {"status": "error", "message": "Container not found"}
container = containers[0] container = containers[0]
if Platform.isOSX() and "." in file_url: if Platform.isOSX() and "." in file_url:
@ -437,12 +236,12 @@ class ContainerManager(QObject):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url)) catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
if result == QMessageBox.No: if result == QMessageBox.No:
return { "status": "cancelled", "message": "User cancelled"} return {"status": "cancelled", "message": "User cancelled"}
try: try:
contents = container.serialize() contents = container.serialize()
except NotImplementedError: except NotImplementedError:
return { "status": "error", "message": "Unable to serialize container"} return {"status": "error", "message": "Unable to serialize container"}
if contents is None: if contents is None:
return {"status": "error", "message": "Serialization returned None. Unable to write to file"} return {"status": "error", "message": "Serialization returned None. Unable to write to file"}
@ -450,7 +249,7 @@ class ContainerManager(QObject):
with SaveFile(file_url, "w") as f: with SaveFile(file_url, "w") as f:
f.write(contents) f.write(contents)
return { "status": "success", "message": "Succesfully exported container", "path": file_url} return {"status": "success", "message": "Successfully exported container", "path": file_url}
## Imports a profile from a file ## Imports a profile from a file
# #
@ -459,9 +258,9 @@ class ContainerManager(QObject):
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key # \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
# containing a message for the user # containing a message for the user
@pyqtSlot(QUrl, result = "QVariantMap") @pyqtSlot(QUrl, result = "QVariantMap")
def importContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
if not file_url_or_string: if not file_url_or_string:
return { "status": "error", "message": "Invalid path"} return {"status": "error", "message": "Invalid path"}
if isinstance(file_url_or_string, QUrl): if isinstance(file_url_or_string, QUrl):
file_url = file_url_or_string.toLocalFile() file_url = file_url_or_string.toLocalFile()
@ -469,16 +268,16 @@ class ContainerManager(QObject):
file_url = file_url_or_string file_url = file_url_or_string
if not file_url or not os.path.exists(file_url): if not file_url or not os.path.exists(file_url):
return { "status": "error", "message": "Invalid path" } return {"status": "error", "message": "Invalid path"}
try: try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url) mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
except MimeTypeNotFoundError: except MimeTypeNotFoundError:
return { "status": "error", "message": "Could not determine mime type of file" } return {"status": "error", "message": "Could not determine mime type of file"}
container_type = self._container_registry.getContainerForMimeType(mime_type) container_type = self._container_registry.getContainerForMimeType(mime_type)
if not container_type: if not container_type:
return { "status": "error", "message": "Could not find a container to handle the specified file."} return {"status": "error", "message": "Could not find a container to handle the specified file."}
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url))) container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
container_id = self._container_registry.uniqueName(container_id) container_id = self._container_registry.uniqueName(container_id)
@ -486,16 +285,18 @@ class ContainerManager(QObject):
container = container_type(container_id) container = container_type(container_id)
try: try:
with open(file_url, "rt") as f: with open(file_url, "rt", encoding = "utf-8") as f:
container.deserialize(f.read()) container.deserialize(f.read())
except PermissionError: except PermissionError:
return { "status": "error", "message": "Permission denied when trying to read the file"} return {"status": "error", "message": "Permission denied when trying to read the file"}
except Exception as ex:
return {"status": "error", "message": str(ex)}
container.setName(container_id) container.setDirty(True)
self._container_registry.addContainer(container) self._container_registry.addContainer(container)
return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) } return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
## Update the current active quality changes container with the settings from the user container. ## Update the current active quality changes container with the settings from the user container.
# #
@ -514,13 +315,13 @@ class ContainerManager(QObject):
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks(): for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
# Find the quality_changes container for this stack and merge the contents of the top container into it. # Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges quality_changes = stack.qualityChanges
if not quality_changes or quality_changes.isReadOnly(): if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId()) Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue continue
self._performMerge(quality_changes, stack.getTop()) self._performMerge(quality_changes, stack.getTop())
self._machine_manager.activeQualityChanged.emit() self._machine_manager.activeQualityChangesGroupChanged.emit()
return True return True
@ -540,391 +341,37 @@ class ContainerManager(QObject):
for container in send_emits_containers: for container in send_emits_containers:
container.sendPostponedEmits() container.sendPostponedEmits()
## Create quality changes containers from the user containers in the active stacks.
#
# This will go through the global and extruder stacks and create quality_changes containers from
# the user containers in each stack. These then replace the quality_changes containers in the
# stack and clear the user settings.
#
# \return \type{bool} True if the operation was successfully, False if not.
@pyqtSlot(str, result = bool)
def createQualityChanges(self, base_name):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return False
active_quality_name = self._machine_manager.activeQualityName
if active_quality_name == "":
Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
return False
self._machine_manager.blurSettings.emit()
if base_name is None or base_name == "":
base_name = active_quality_name
unique_name = self._container_registry.uniqueName(base_name)
# Go through the active stacks and create quality_changes containers from the user containers.
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
user_container = stack.getTop()
quality_container = stack.quality
quality_changes_container = stack.qualityChanges
if not quality_container or not quality_changes_container:
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
continue
extruder_id = None if stack is global_stack else QualityManager.getInstance().getParentMachineDefinition(stack.getBottom()).getId()
new_changes = self._createQualityChanges(quality_container, unique_name,
Application.getInstance().getGlobalContainerStack().getBottom(),
extruder_id)
self._performMerge(new_changes, quality_changes_container, clear_settings = False)
self._performMerge(new_changes, user_container)
self._container_registry.addContainer(new_changes)
stack.replaceContainer(stack.getContainerIndex(quality_changes_container), new_changes)
self._machine_manager.activeQualityChanged.emit()
return True
## Remove all quality changes containers matching a specified name.
#
# This will search for quality_changes containers matching the supplied name and remove them.
# Note that if the machine specifies that qualities should be filtered by machine and/or material
# only the containers related to the active machine/material are removed.
#
# \param quality_name The name of the quality changes to remove.
#
# \return \type{bool} True if successful, False if not.
@pyqtSlot(str, result = bool)
def removeQualityChanges(self, quality_name):
Logger.log("d", "Attempting to remove the quality change containers with name %s", quality_name)
containers_found = False
if not quality_name:
return containers_found # Without a name we will never find a container to remove.
# If the container that is being removed is the currently active quality, set another quality as the active quality
activate_quality = quality_name == self._machine_manager.activeQualityName
activate_quality_type = None
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack or not quality_name:
return ""
machine_definition = QualityManager.getInstance().getParentMachineDefinition(global_stack.getBottom())
for container in QualityManager.getInstance().findQualityChangesByName(quality_name, machine_definition):
containers_found = True
if activate_quality and not activate_quality_type:
activate_quality_type = container.getMetaDataEntry("quality")
self._container_registry.removeContainer(container.getId())
if not containers_found:
Logger.log("d", "Unable to remove quality containers, as we did not find any by the name of %s", quality_name)
elif activate_quality:
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
containers = self._container_registry.findInstanceContainersMetadata(type = "quality", definition = definition_id, quality_type = activate_quality_type)
if containers:
self._machine_manager.setActiveQuality(containers[0]["id"])
self._machine_manager.activeQualityChanged.emit()
return containers_found
## Rename a set of quality changes containers.
#
# This will search for quality_changes containers matching the supplied name and rename them.
# Note that if the machine specifies that qualities should be filtered by machine and/or material
# only the containers related to the active machine/material are renamed.
#
# \param quality_name The name of the quality changes containers to rename.
# \param new_name The new name of the quality changes.
#
# \return True if successful, False if not.
@pyqtSlot(str, str, result = bool)
def renameQualityChanges(self, quality_name, new_name):
Logger.log("d", "User requested QualityChanges container rename of %s to %s", quality_name, new_name)
if not quality_name or not new_name:
return False
if quality_name == new_name:
Logger.log("w", "Unable to rename %s to %s, because they are the same.", quality_name, new_name)
return True
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return False
self._machine_manager.blurSettings.emit()
new_name = self._container_registry.uniqueName(new_name)
container_registry = self._container_registry
containers_to_rename = self._container_registry.findInstanceContainersMetadata(type = "quality_changes", name = quality_name)
for container in containers_to_rename:
stack_id = global_stack.getId()
if "extruder" in container:
stack_id = container["extruder"]
container_registry.renameContainer(container["id"], new_name, self._createUniqueId(stack_id, new_name))
if not containers_to_rename:
Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
self._machine_manager.activeQualityChanged.emit()
return True
## Duplicate a specified set of quality or quality_changes containers.
#
# This will search for containers matching the specified name. If the container is a "quality" type container, a new
# quality_changes container will be created with the specified quality as base. If the container is a "quality_changes"
# container, it is simply duplicated and renamed.
#
# \param quality_name The name of the quality to duplicate.
#
# \return A string containing the name of the duplicated containers, or an empty string if it failed.
@pyqtSlot(str, str, result = str)
def duplicateQualityOrQualityChanges(self, quality_name, base_name):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack or not quality_name:
return ""
machine_definition = global_stack.getBottom()
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks is None:
return ""
material_metadatas = [stack.material.getMetaData() for stack in active_stacks]
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
material_metadatas)
return result[0].getName() if result else ""
## Duplicate a quality or quality changes profile specific to a machine type
#
# \param quality_name The name of the quality or quality changes container to duplicate.
# \param base_name The desired name for the new container.
# \param machine_definition The machine with the specific machine type.
# \param material_metadatas Metadata of materials
# \return List of duplicated quality profiles.
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name: str, base_name: str, machine_definition: DefinitionContainer, material_metadatas: List[Dict[str, Any]]) -> List[InstanceContainer]:
Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
if base_name is None:
base_name = quality_name
# Try to find a Quality with the name.
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_metadatas)
if container:
Logger.log("d", "We found a quality to duplicate.")
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
Logger.log("d", "We found a quality_changes to duplicate.")
# Assume it is a quality changes.
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
# Duplicate a quality profile
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition) -> List[InstanceContainer]:
if base_name is None:
base_name = quality_container.getName()
new_name = self._container_registry.uniqueName(base_name)
new_change_instances = []
# Handle the global stack first.
global_changes = self._createQualityChanges(quality_container, new_name, machine_definition, None)
new_change_instances.append(global_changes)
self._container_registry.addContainer(global_changes)
# Handle the extruders if present.
extruders = machine_definition.getMetaDataEntry("machine_extruder_trains")
if extruders:
for extruder_id in extruders:
extruder = extruders[extruder_id]
new_changes = self._createQualityChanges(quality_container, new_name, machine_definition, extruder)
new_change_instances.append(new_changes)
self._container_registry.addContainer(new_changes)
return new_change_instances
# Duplicate a quality changes container
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition) -> List[InstanceContainer]:
new_change_instances = []
for container in QualityManager.getInstance().findQualityChangesByName(quality_changes_name,
machine_definition):
base_id = container.getMetaDataEntry("extruder")
if not base_id:
base_id = container.getDefinition().getId()
new_unique_id = self._createUniqueId(base_id, base_name)
new_container = container.duplicate(new_unique_id, base_name)
new_change_instances.append(new_container)
self._container_registry.addContainer(new_container)
return new_change_instances
## Create a duplicate of a material, which has the same GUID and base_file metadata
#
# \return \type{str} the id of the newly created container.
@pyqtSlot(str, result = str)
def duplicateMaterial(self, material_id: str) -> str:
original = self._container_registry.findContainersMetadata(id = material_id)
if not original:
Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
return ""
original = original[0]
base_container_id = original.get("base_file")
base_container = self._container_registry.findContainers(id = base_container_id)
if not base_container:
Logger.log("d", "Unable to duplicate the material with id {material_id}, because base_file {base_container_id} doesn't exist.".format(material_id = material_id, base_container_id = base_container_id))
return ""
base_container = base_container[0]
#We'll copy all containers with the same base.
#This way the correct variant and machine still gets assigned when loading the copy of the material.
containers_to_copy = self._container_registry.findInstanceContainers(base_file = base_container_id)
# Ensure all settings are saved.
Application.getInstance().saveSettings()
# Create a new ID & container to hold the data.
new_containers = []
new_base_id = self._container_registry.uniqueName(base_container.getId())
new_base_container = copy.deepcopy(base_container)
new_base_container.getMetaData()["id"] = new_base_id
new_base_container.getMetaData()["base_file"] = new_base_id
new_containers.append(new_base_container)
#Clone all of them.
clone_of_original = None #Keeping track of which one is the clone of the original material, since we need to return that.
for container_to_copy in containers_to_copy:
#Create unique IDs for every clone.
current_id = container_to_copy.getId()
new_id = new_base_id
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant"):
variant = self._container_registry.findContainers(id = container_to_copy.getMetaDataEntry("variant"))[0]
new_id += "_" + variant.getName().replace(" ", "_")
if current_id == material_id:
clone_of_original = new_id
new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id
new_container.getMetaData()["base_file"] = new_base_id
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
ContainerRegistry.getInstance().addContainer(container_to_add)
return self._getMaterialContainerIdForActiveMachine(clone_of_original)
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
#
# \return \type{str} the id of the newly created container.
@pyqtSlot(result = str)
def createMaterial(self) -> str:
# Ensure all settings are saved.
Application.getInstance().saveSettings()
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return ""
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
containers = self._container_registry.findInstanceContainersMetadata(id = "generic_pla*", approximate_diameter = approximate_diameter)
if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
return ""
base_file = containers[0].get("base_file")
containers = self._container_registry.findInstanceContainers(id = base_file)
if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
return ""
# Create a new ID & container to hold the data.
new_id = self._container_registry.uniqueName("custom_material")
container_type = type(containers[0]) # Always XMLMaterialProfile, since we specifically clone the base_file
duplicated_container = container_type(new_id)
# Instead of duplicating we load the data from the basefile again.
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
# are also correctly created.
with open(containers[0].getPath(), encoding="utf-8") as f:
duplicated_container.deserialize(f.read())
duplicated_container.setMetaDataEntry("GUID", str(uuid.uuid4()))
duplicated_container.setMetaDataEntry("brand", catalog.i18nc("@label", "Custom"))
# We're defaulting to PLA, as machines with material profiles don't like material types they don't know.
# TODO: This is a hack, the only reason this is in now is to bandaid the problem as we're close to a release!
duplicated_container.setMetaDataEntry("material", "PLA")
duplicated_container.setName(catalog.i18nc("@label", "Custom Material"))
self._container_registry.addContainer(duplicated_container)
return self._getMaterialContainerIdForActiveMachine(new_id)
## Find the id of a material container based on the new material
# Utilty function that is shared between duplicateMaterial and createMaterial
#
# \param base_file \type{str} the id of the created container.
def _getMaterialContainerIdForActiveMachine(self, base_file):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return base_file
has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False))
has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False))
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
if has_machine_materials or has_variant_materials:
if has_variants:
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
else:
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
if materials:
return materials[0]["id"]
Logger.log("w", "Unable to find a suitable container based on %s for the current machine.", base_file)
return "" # do not activate a new material if a container can not be found
return base_file
## Get a list of materials that have the same GUID as the reference material ## Get a list of materials that have the same GUID as the reference material
# #
# \param material_id \type{str} the id of the material for which to get the linked materials. # \param material_id \type{str} the id of the material for which to get the linked materials.
# \return \type{list} a list of names of materials with the same GUID # \return \type{list} a list of names of materials with the same GUID
@pyqtSlot(str, result = "QStringList") @pyqtSlot("QVariant", result = "QStringList")
def getLinkedMaterials(self, material_id: str): def getLinkedMaterials(self, material_node):
containers = self._container_registry.findInstanceContainersMetadata(id = material_id) guid = material_node.metadata["GUID"]
if not containers:
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
return []
material_container = containers[0] material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
material_base_file = material_container.get("base_file", "")
material_guid = material_container.get("GUID", "")
if not material_guid:
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
return []
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_guid)
linked_material_names = [] linked_material_names = []
for container in containers: if material_group_list:
if container["id"] in [material_id, material_base_file] or container.get("base_file") != container["id"]: for material_group in material_group_list:
continue linked_material_names.append(material_group.root_material_node.metadata["name"])
linked_material_names.append(container["name"])
return linked_material_names return linked_material_names
## Unlink a material from all other materials by creating a new GUID ## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for. # \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot(str) @pyqtSlot("QVariant")
def unlinkMaterial(self, material_id: str): def unlinkMaterial(self, material_node):
containers = self._container_registry.findInstanceContainers(id=material_id) # Get the material group
if not containers: material_group = self._material_manager.getMaterialGroup(material_node.metadata["base_file"])
Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id)
return ""
containers[0].setMetaDataEntry("GUID", str(uuid.uuid4())) # Generate a new GUID
new_guid = str(uuid.uuid4())
# Update the GUID
# NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will
# take care of the derived containers too
container = material_group.root_material_node.getContainer()
container.setMetaDataEntry("GUID", new_guid)
## Get the singleton instance for this class. ## Get the singleton instance for this class.
@classmethod @classmethod
@ -997,81 +444,6 @@ class ContainerManager(QObject):
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list) name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
self._container_name_filters[name_filter] = entry self._container_name_filters[name_filter] = entry
## Creates a unique ID for a container by prefixing the name with the stack ID.
#
# This method creates a unique ID for a container by prefixing it with a specified stack ID.
# This is done to ensure we have an easily identified ID for quality changes, which have the
# same name across several stacks.
#
# \param stack_id The ID of the stack to prepend.
# \param container_name The name of the container that we are creating a unique ID for.
#
# \return Container name prefixed with stack ID, in lower case with spaces replaced by underscores.
def _createUniqueId(self, stack_id, container_name):
result = stack_id + "_" + container_name
result = result.lower()
result.replace(" ", "_")
return result
## Create a quality changes container for a specified quality container.
#
# \param quality_container The quality container to create a changes container for.
# \param new_name The name of the new quality_changes container.
# \param machine_definition The machine definition this quality changes container is specific to.
# \param extruder_id
#
# \return A new quality_changes container with the specified container as base.
def _createQualityChanges(self, quality_container, new_name, machine_definition, extruder_id):
base_id = machine_definition.getId() if extruder_id is None else extruder_id
# Create a new quality_changes container for the quality.
quality_changes = InstanceContainer(self._createUniqueId(base_id, new_name))
quality_changes.setName(new_name)
quality_changes.addMetaDataEntry("type", "quality_changes")
quality_changes.addMetaDataEntry("quality_type", quality_container.getMetaDataEntry("quality_type"))
# If we are creating a container for an extruder, ensure we add that to the container
if extruder_id is not None:
quality_changes.addMetaDataEntry("extruder", extruder_id)
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
if not machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes.setDefinition("fdmprinter")
else:
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition).getId())
from cura.CuraApplication import CuraApplication
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
return quality_changes
## Import profiles from a list of file_urls.
# Each QUrl item must end with .curaprofile, or it will not be imported.
#
# \param QVariant<QUrl>, essentially a list with QUrl objects.
# \return Dict with keys status, text
@pyqtSlot("QVariantList", result="QVariantMap")
def importProfiles(self, file_urls):
status = "ok"
results = {"ok": [], "error": []}
for file_url in file_urls:
if not file_url.isValid():
continue
path = file_url.toLocalFile()
if not path:
continue
if not path.endswith(".curaprofile"):
continue
single_result = self._container_registry.importProfile(path)
if single_result["status"] == "error":
status = "error"
results[single_result["status"]].append(single_result["message"])
return {
"status": status,
"message": "\n".join(results["ok"] + results["error"])}
## Import single profile, file_url does not have to end with curaprofile ## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result="QVariantMap") @pyqtSlot(QUrl, result="QVariantMap")
def importProfile(self, file_url): def importProfile(self, file_url):
@ -1082,11 +454,13 @@ class ContainerManager(QObject):
return return
return self._container_registry.importProfile(path) return self._container_registry.importProfile(path)
@pyqtSlot("QVariantList", QUrl, str) @pyqtSlot(QObject, QUrl, str)
def exportProfile(self, instance_id: str, file_url: QUrl, file_type: str) -> None: def exportQualityChangesGroup(self, quality_changes_group, file_url: QUrl, file_type: str):
if not file_url.isValid(): if not file_url.isValid():
return return
path = file_url.toLocalFile() path = file_url.toLocalFile()
if not path: if not path:
return return
self._container_registry.exportProfile(instance_id, path, file_type)
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes()]
self._container_registry.exportQualityProfile(container_list, path, file_type)

View file

@ -1,97 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction
class ContainerSettingsModel(ListModel):
LabelRole = Qt.UserRole + 1
CategoryRole = Qt.UserRole + 2
UnitRole = Qt.UserRole + 3
ValuesRole = Qt.UserRole + 4
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.LabelRole, "label")
self.addRoleName(self.CategoryRole, "category")
self.addRoleName(self.UnitRole, "unit")
self.addRoleName(self.ValuesRole, "values")
self._container_ids = []
self._containers = []
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
self._update()
def _update(self):
items = []
if len(self._container_ids) == 0:
return
keys = []
for container in self._containers:
keys = keys + list(container.getAllKeys())
keys = list(set(keys)) # remove duplicate keys
for key in keys:
definition = None
category = None
values = []
for container in self._containers:
instance = container.getInstance(key)
if instance:
definition = instance.definition
# Traverse up to find the category
category = definition
while category.type != "category":
category = category.parent
value = container.getProperty(key, "value")
if type(value) == SettingFunction:
values.append("=\u0192")
else:
values.append(container.getProperty(key, "value"))
else:
values.append("")
items.append({
"key": key,
"values": values,
"label": definition.label,
"unit": definition.unit,
"category": category.label
})
items.sort(key = lambda k: (k["category"], k["key"]))
self.setItems(items)
## Set the ids of the containers which have the settings this model should list.
# Also makes sure the model updates when the containers have property changes
def setContainers(self, container_ids):
for container in self._containers:
container.propertyChanged.disconnect(self._onPropertyChanged)
self._container_ids = container_ids
self._containers = []
for container_id in self._container_ids:
containers = ContainerRegistry.getInstance().findContainers(id = container_id)
if containers:
containers[0].propertyChanged.connect(self._onPropertyChanged)
self._containers.append(containers[0])
self._update()
containersChanged = pyqtSignal()
@pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
def containers(self):
return self.container_ids

View file

@ -14,6 +14,7 @@ from UM.Decorators import override
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingInstance import SettingInstance
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
@ -24,14 +25,15 @@ from UM.Resources import Resources
from . import ExtruderStack from . import ExtruderStack
from . import GlobalStack from . import GlobalStack
from .ContainerManager import ContainerManager
from .ExtruderManager import ExtruderManager from .ExtruderManager import ExtruderManager
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class CuraContainerRegistry(ContainerRegistry): class CuraContainerRegistry(ContainerRegistry):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -93,7 +95,7 @@ class CuraContainerRegistry(ContainerRegistry):
def _containerExists(self, container_type, container_name): def _containerExists(self, container_type, container_name):
container_class = ContainerStack if container_type == "machine" else InstanceContainer container_class = ContainerStack if container_type == "machine" else InstanceContainer
return self.findContainersMetadata(id = container_name, type = container_type, ignore_case = True) or \ return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
## Exports an profile to a file ## Exports an profile to a file
@ -101,7 +103,7 @@ class CuraContainerRegistry(ContainerRegistry):
# \param instance_ids \type{list} the IDs of the profiles to export. # \param instance_ids \type{list} the IDs of the profiles to export.
# \param file_name \type{str} the full path and filename to export to. # \param file_name \type{str} the full path and filename to export to.
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)" # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
def exportProfile(self, instance_ids, file_name, file_type): def exportQualityProfile(self, container_list, file_name, file_type):
# Parse the fileType to deduce what plugin can save the file format. # Parse the fileType to deduce what plugin can save the file format.
# fileType has the format "<description> (*.<extension>)" # fileType has the format "<description> (*.<extension>)"
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
@ -120,31 +122,10 @@ class CuraContainerRegistry(ContainerRegistry):
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name)) catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
if result == QMessageBox.No: if result == QMessageBox.No:
return return
found_containers = []
extruder_positions = []
for instance_id in instance_ids:
containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
if containers:
found_containers.append(containers[0])
# Determine the position of the extruder of this container
extruder_id = containers[0].getMetaDataEntry("extruder", "")
if extruder_id == "":
# Global stack
extruder_positions.append(-1)
else:
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = extruder_id)
if extruder_containers:
extruder_positions.append(int(extruder_containers[0].get("position", 0)))
else:
extruder_positions.append(0)
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]
profile_writer = self._findProfileWriter(extension, description) profile_writer = self._findProfileWriter(extension, description)
try: try:
success = profile_writer.write(file_name, found_containers) success = profile_writer.write(file_name, container_list)
except Exception as e: except Exception as e:
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)),
@ -201,16 +182,43 @@ class CuraContainerRegistry(ContainerRegistry):
for plugin_id, meta_data in self._getIOPlugins("profile_reader"): for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
if meta_data["profile_reader"][0]["extension"] != extension: if meta_data["profile_reader"][0]["extension"] != extension:
continue continue
profile_reader = plugin_registry.getPluginObject(plugin_id) profile_reader = plugin_registry.getPluginObject(plugin_id)
try: try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except Exception as e: except Exception as e:
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e)) Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
if profile_or_list: if profile_or_list:
# Ensure it is always a list of profiles
if not isinstance(profile_or_list, list):
profile_or_list = [profile_or_list]
# First check if this profile is suitable for this machine
global_profile = None
if len(profile_or_list) == 1:
global_profile = profile_or_list[0]
else:
for profile in profile_or_list:
if not profile.getMetaDataEntry("extruder"):
global_profile = profile
break
if not global_profile:
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
profile_definition = global_profile.getMetaDataEntry("definition")
expected_machine_definition = "fdmprinter"
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
expected_machine_definition = global_container_stack.getMetaDataEntry("quality_definition")
if not expected_machine_definition:
expected_machine_definition = global_container_stack.definition.getId()
if expected_machine_definition is not None and profile_definition is not None and profile_definition != expected_machine_definition:
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
name_seed = os.path.splitext(os.path.basename(file_name))[0] name_seed = os.path.splitext(os.path.basename(file_name))[0]
new_name = self.uniqueName(name_seed) new_name = self.uniqueName(name_seed)
@ -218,24 +226,40 @@ class CuraContainerRegistry(ContainerRegistry):
if type(profile_or_list) is not list: if type(profile_or_list) is not list:
profile_or_list = [profile_or_list] profile_or_list = [profile_or_list]
# Make sure that there are also extruder stacks' quality_changes, not just one for the global stack
if len(profile_or_list) == 1: if len(profile_or_list) == 1:
# If there is only 1 stack file it means we're loading a legacy (pre-3.1) .curaprofile. global_profile = profile_or_list[0]
# In that case we find the per-extruder settings and put those in a new quality_changes container extruder_profiles = []
# so that it is compatible with the new stack setup. for idx, extruder in enumerate(global_container_stack.extruders.values()):
profile = profile_or_list[0] profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1))
extruder_stack_quality_changes_container = ContainerManager.getInstance().duplicateContainerInstance(profile) profile = InstanceContainer(profile_id)
extruder_stack_quality_changes_container.addMetaDataEntry("extruder", "fdmextruder") profile.setName(global_profile.getName())
profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
profile.addMetaDataEntry("type", "quality_changes")
profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition"))
profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type"))
profile.addMetaDataEntry("extruder", extruder.getId())
profile.setDirty(True)
if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_container_stack.getProperty(qc_setting_key,
"settable_per_extruder")
if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value")
for quality_changes_setting_key in extruder_stack_quality_changes_container.getAllKeys(): setting_definition = global_container_stack.getSettingDefinition(qc_setting_key)
settable_per_extruder = extruder_stack_quality_changes_container.getProperty(quality_changes_setting_key, "settable_per_extruder") new_instance = SettingInstance(setting_definition, profile)
if settable_per_extruder: new_instance.setProperty("value", setting_value)
profile.removeInstance(quality_changes_setting_key, postpone_emit = True) new_instance.resetState() # Ensure that the state is not seen as a user state.
else: profile.addInstance(new_instance)
extruder_stack_quality_changes_container.removeInstance(quality_changes_setting_key, postpone_emit = True) profile.setDirty(True)
# We add the new container to the profile list so things like extruder positions are taken care of global_profile.removeInstance(qc_setting_key, postpone_emit=True)
# in the next code segment. extruder_profiles.append(profile)
profile_or_list.append(extruder_stack_quality_changes_container)
for profile in extruder_profiles:
profile_or_list.append(profile)
# Import all profiles # Import all profiles
for profile_index, profile in enumerate(profile_or_list): for profile_index, profile in enumerate(profile_or_list):
@ -243,15 +267,18 @@ class CuraContainerRegistry(ContainerRegistry):
# This is assumed to be the global profile # This is assumed to be the global profile
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
elif len(machine_extruders) > profile_index: elif profile_index < len(machine_extruders) + 1:
# This is assumed to be an extruder profile # This is assumed to be an extruder profile
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom()) extruder_id = machine_extruders[profile_index - 1].definition.getId()
if not profile.getMetaDataEntry("extruder"): if not profile.getMetaDataEntry("extruder"):
profile.addMetaDataEntry("extruder", extruder_id) profile.addMetaDataEntry("extruder", extruder_id)
else: else:
profile.setMetaDataEntry("extruder", extruder_id) profile.setMetaDataEntry("extruder", extruder_id)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
else: #More extruders in the imported file than in the machine.
continue #Delete the additional profiles.
result = self._configureProfile(profile, profile_id, new_name) result = self._configureProfile(profile, profile_id, new_name)
if result is not None: if result is not None:
return {"status": "error", "message": catalog.i18nc( return {"status": "error", "message": catalog.i18nc(
@ -261,6 +288,9 @@ class CuraContainerRegistry(ContainerRegistry):
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
# This message is throw when the profile reader doesn't find any profile in the file
return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
# If it hasn't returned by now, none of the plugins loaded the profile successfully. # If it hasn't returned by now, none of the plugins loaded the profile successfully.
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
@ -281,9 +311,13 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setDirty(True) # Ensure the profiles are correctly saved profile.setDirty(True) # Ensure the profiles are correctly saved
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
profile._id = new_id profile.setMetaDataEntry("id", new_id)
profile.setName(new_name) profile.setName(new_name)
# Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile
# It also solves an issue with importing profiles from G-Codes
profile.setMetaDataEntry("id", new_id)
if "type" in profile.getMetaData(): if "type" in profile.getMetaData():
profile.setMetaDataEntry("type", "quality_changes") profile.setMetaDataEntry("type", "quality_changes")
else: else:
@ -294,39 +328,16 @@ class CuraContainerRegistry(ContainerRegistry):
return catalog.i18nc("@info:status", "Profile is missing a quality type.") return catalog.i18nc("@info:status", "Profile is missing a quality type.")
quality_type_criteria = {"quality_type": quality_type} quality_type_criteria = {"quality_type": quality_type}
if self._machineHasOwnQualities(): global_stack = Application.getInstance().getGlobalContainerStack()
profile.setDefinition(self._activeQualityDefinition().getId()) definition_id = getMachineDefinitionIDForQualitySearch(global_stack)
if self._machineHasOwnMaterials(): profile.setDefinition(definition_id)
active_material_id = self._activeMaterialId()
if active_material_id and active_material_id != "empty": # only update if there is an active material
profile.addMetaDataEntry("material", active_material_id)
quality_type_criteria["material"] = active_material_id
quality_type_criteria["definition"] = profile.getDefinition().getId()
else:
profile.setDefinition(fdmprinter)
quality_type_criteria["definition"] = "fdmprinter"
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
del quality_type_criteria["definition"]
# materials = None
if "material" in quality_type_criteria:
# materials = ContainerRegistry.getInstance().findInstanceContainers(id = quality_type_criteria["material"])
del quality_type_criteria["material"]
# Do not filter quality containers here with materials because we are trying to import a profile, so it should
# NOT be restricted by the active materials on the current machine.
materials = None
# Check to make sure the imported profile actually makes sense in context of the current configuration. # Check to make sure the imported profile actually makes sense in context of the current configuration.
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
# successfully imported but then fail to show up. # successfully imported but then fail to show up.
from cura.QualityManager import QualityManager quality_manager = CuraApplication.getInstance()._quality_manager
qualities = QualityManager.getInstance()._getFilteredContainersForStack(machine_definition, materials, **quality_type_criteria) quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
if not qualities: if quality_type not in quality_group_dict:
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
ContainerRegistry.getInstance().addContainer(profile) ContainerRegistry.getInstance().addContainer(profile)
@ -346,18 +357,6 @@ class CuraContainerRegistry(ContainerRegistry):
result.append( (plugin_id, meta_data) ) result.append( (plugin_id, meta_data) )
return result return result
## Get the definition to use to select quality profiles for the active machine
# \return the active quality definition object or None if there is no quality definition
def _activeQualityDefinition(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
definition = self.findDefinitionContainers(id = definition_id)[0]
if definition:
return definition
return None
## Returns true if the current machine requires its own materials ## Returns true if the current machine requires its own materials
# \return True if the current machine requires its own materials # \return True if the current machine requires its own materials
def _machineHasOwnMaterials(self): def _machineHasOwnMaterials(self):
@ -422,11 +421,22 @@ class CuraContainerRegistry(ContainerRegistry):
if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine":
return return
machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains")
if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}:
return
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId())
if not extruder_stacks: if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id): #
# new_global_quality_changes is optional. It is only used in project loading for a scenario like this:
# - override the current machine
# - create new for custom quality profile
# new_global_quality_changes is the new global quality changes container in this scenario.
# create_new_ids indicates if new unique ids must be created
#
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True):
new_extruder_id = extruder_id new_extruder_id = extruder_id
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
@ -435,20 +445,50 @@ class CuraContainerRegistry(ContainerRegistry):
return return
extruder_definition = extruder_definitions[0] extruder_definition = extruder_definitions[0]
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id
extruder_stack = ExtruderStack.ExtruderStack(unique_name) extruder_stack = ExtruderStack.ExtruderStack(unique_name)
extruder_stack.setName(extruder_definition.getName()) extruder_stack.setName(extruder_definition.getName())
extruder_stack.setDefinition(extruder_definition) extruder_stack.setDefinition(extruder_definition)
extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
# create a new definition_changes container for the extruder stack
definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings"
definition_changes_name = definition_changes_id
definition_changes = InstanceContainer(definition_changes_id)
definition_changes.setName(definition_changes_name)
definition_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
definition_changes.addMetaDataEntry("type", "definition_changes")
definition_changes.addMetaDataEntry("definition", extruder_definition.getId())
# move definition_changes settings if exist
for setting_key in definition_changes.getAllKeys():
if machine.definition.getProperty(setting_key, "settable_per_extruder"):
setting_value = machine.definitionChanges.getProperty(setting_key, "value")
if setting_value is not None:
# move it to the extruder stack's definition_changes
setting_definition = machine.getSettingDefinition(setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
definition_changes.addInstance(new_instance)
definition_changes.setDirty(True)
machine.definitionChanges.removeInstance(setting_key, postpone_emit = True)
self.addContainer(definition_changes)
extruder_stack.setDefinitionChanges(definition_changes)
# create empty user changes container otherwise # create empty user changes container otherwise
user_container = InstanceContainer(extruder_stack.id + "_user") user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user"
user_container_name = user_container_id
user_container = InstanceContainer(user_container_id)
user_container.setName(user_container_name)
user_container.addMetaDataEntry("type", "user") user_container.addMetaDataEntry("type", "user")
user_container.addMetaDataEntry("machine", extruder_stack.getId()) user_container.addMetaDataEntry("machine", machine.getId())
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(machine.definition.getId()) user_container.setDefinition(machine.definition.getId())
user_container.setMetaDataEntry("extruder", extruder_stack.getId())
if machine.userChanges: if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes # for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
@ -456,56 +496,152 @@ class CuraContainerRegistry(ContainerRegistry):
for user_setting_key in machine.userChanges.getAllKeys(): for user_setting_key in machine.userChanges.getAllKeys():
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
user_container.addInstance(machine.userChanges.getInstance(user_setting_key)) setting_value = machine.getProperty(user_setting_key, "value")
setting_definition = machine.getSettingDefinition(user_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
user_container.addInstance(new_instance)
user_container.setDirty(True)
machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
self.addContainer(user_container) self.addContainer(user_container)
extruder_stack.setUserChanges(user_container) extruder_stack.setUserChanges(user_container)
variant_id = "default" application = CuraApplication.getInstance()
empty_variant = application.empty_variant_container
empty_material = application.empty_material_container
empty_quality = application.empty_quality_container
if machine.variant.getId() not in ("empty", "empty_variant"): if machine.variant.getId() not in ("empty", "empty_variant"):
variant_id = machine.variant.getId() variant = machine.variant
else: else:
variant_id = "empty_variant" variant = empty_variant
extruder_stack.setVariantById(variant_id) extruder_stack.variant = variant
material_id = "default"
if machine.material.getId() not in ("empty", "empty_material"): if machine.material.getId() not in ("empty", "empty_material"):
material_id = machine.material.getId() material = machine.material
else: else:
material_id = "empty_material" material = empty_material
extruder_stack.setMaterialById(material_id) extruder_stack.material = material
quality_id = "default"
if machine.quality.getId() not in ("empty", "empty_quality"): if machine.quality.getId() not in ("empty", "empty_quality"):
quality_id = machine.quality.getId() quality = machine.quality
else: else:
quality_id = "empty_quality" quality = empty_quality
extruder_stack.setQualityById(quality_id) extruder_stack.quality = quality
if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"): machine_quality_changes = machine.qualityChanges
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id) if new_global_quality_changes is not None:
machine_quality_changes = new_global_quality_changes
if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id)
if extruder_quality_changes_container: if extruder_quality_changes_container:
extruder_quality_changes_container = extruder_quality_changes_container[0] extruder_quality_changes_container = extruder_quality_changes_container[0]
quality_changes_id = extruder_quality_changes_container.getId() quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id) extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
else: else:
# Some extruder quality_changes containers can be created at runtime as files in the qualities # Some extruder quality_changes containers can be created at runtime as files in the qualities
# folder. Those files won't be loaded in the registry immediately. So we also need to search # folder. Those files won't be loaded in the registry immediately. So we also need to search
# the folder to see if the quality_changes exists. # the folder to see if the quality_changes exists.
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName()) extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if extruder_quality_changes_container: if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId() quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id) extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
else:
# if we still cannot find a quality changes container for the extruder, create a new one
container_name = machine_quality_changes.getName()
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
extruder_quality_changes_container = InstanceContainer(container_id)
extruder_quality_changes_container.setName(container_name)
extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes")
extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_quality_changes_container.addMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
self.addContainer(extruder_quality_changes_container)
extruder_stack.qualityChanges = extruder_quality_changes_container
if not extruder_quality_changes_container: if not extruder_quality_changes_container:
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine.qualityChanges.getName(), extruder_stack.getId()) machine_quality_changes.getName(), extruder_stack.getId())
else:
# move all per-extruder settings to the extruder's quality changes
for qc_setting_key in machine_quality_changes.getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = machine_quality_changes.getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
extruder_quality_changes_container.addInstance(new_instance)
extruder_quality_changes_container.setDirty(True)
machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True)
else: else:
extruder_stack.setQualityChangesById("empty_quality_changes") extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0]
self.addContainer(extruder_stack) self.addContainer(extruder_stack)
# Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have
# per-extruder settings in the container for the machine instead of the extruder.
if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId()
else:
whole_machine_definition = machine.definition
machine_entry = machine.definition.getMetaDataEntry("machine")
if machine_entry is not None:
container_registry = ContainerRegistry.getInstance()
whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0]
quality_changes_machine_definition_id = "fdmprinter"
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition",
whole_machine_definition.getId())
qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id)
qc_groups = {} # map of qc names -> qc containers
for qc in qcs:
qc_name = qc.getName()
if qc_name not in qc_groups:
qc_groups[qc_name] = []
qc_groups[qc_name].append(qc)
# try to find from the quality changes cura directory too
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if quality_changes_container:
qc_groups[qc_name].append(quality_changes_container)
for qc_name, qc_list in qc_groups.items():
qc_dict = {"global": None, "extruders": []}
for qc in qc_list:
extruder_def_id = qc.getMetaDataEntry("extruder")
if extruder_def_id is not None:
qc_dict["extruders"].append(qc)
else:
qc_dict["global"] = qc
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
# move per-extruder settings
for qc_setting_key in qc_dict["global"].getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = qc_dict["global"].getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
qc_dict["extruders"][0].addInstance(new_instance)
qc_dict["extruders"][0].setDirty(True)
qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True)
# Set next stack at the end # Set next stack at the end
extruder_stack.setNextStack(machine) extruder_stack.setNextStack(machine)
@ -521,7 +657,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
continue continue
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation=None)
try: try:
parser.read([file_path]) parser.read([file_path])
except: except:
@ -534,9 +670,12 @@ class CuraContainerRegistry(ContainerRegistry):
if parser["general"]["name"] == name: if parser["general"]["name"] == name:
# load the container # load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "") container_id = os.path.basename(file_path).replace(".inst.cfg", "")
if self.findInstanceContainers(id = container_id):
# this container is already in the registry, skip it
continue
instance_container = InstanceContainer(container_id) instance_container = InstanceContainer(container_id)
with open(file_path, "r") as f: with open(file_path, "r", encoding = "utf-8") as f:
serialized = f.read() serialized = f.read()
instance_container.deserialize(serialized, file_path) instance_container.deserialize(serialized, file_path)
self.addContainer(instance_container) self.addContainer(instance_container)

View file

@ -83,20 +83,6 @@ class CuraContainerStack(ContainerStack):
def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
## Set the quality changes container by an ID.
#
# This will search for the specified container and set it. If no container was found, an error will be raised.
#
# \param new_quality_changes_id The ID of the new quality changes container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setQualityChangesById(self, new_quality_changes_id: str) -> None:
quality_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_changes_id)
if quality_changes:
self.setQualityChanges(quality_changes[0])
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_changes_id))
## Get the quality changes container. ## Get the quality changes container.
# #
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -110,31 +96,6 @@ class CuraContainerStack(ContainerStack):
def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None: def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None:
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
## Set the quality container by an ID.
#
# This will search for the specified container and set it. If no container was found, an error will be raised.
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultQuality
# for details.
#
# \param new_quality_id The ID of the new quality container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setQualityById(self, new_quality_id: str) -> None:
quality = self._empty_quality
if new_quality_id == "default":
new_quality = self.findDefaultQuality()
if new_quality:
quality = new_quality
else:
qualities = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_id)
if qualities:
quality = qualities[0]
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_id))
self.setQuality(quality)
## Get the quality container. ## Get the quality container.
# #
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -144,35 +105,10 @@ class CuraContainerStack(ContainerStack):
## Set the material container. ## Set the material container.
# #
# \param new_quality_changes The new material container. It is expected to have a "type" metadata entry with the value "quality_changes". # \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None: def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
## Set the material container by an ID.
#
# This will search for the specified container and set it. If no container was found, an error will be raised.
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultMaterial
# for details.
#
# \param new_quality_changes_id The ID of the new material container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setMaterialById(self, new_material_id: str) -> None:
material = self._empty_material
if new_material_id == "default":
new_material = self.findDefaultMaterial()
if new_material:
material = new_material
else:
materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
if materials:
material = materials[0]
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_material_id))
self.setMaterial(material)
## Get the material container. ## Get the material container.
# #
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -182,35 +118,10 @@ class CuraContainerStack(ContainerStack):
## Set the variant container. ## Set the variant container.
# #
# \param new_quality_changes The new variant container. It is expected to have a "type" metadata entry with the value "quality_changes". # \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant".
def setVariant(self, new_variant: InstanceContainer) -> None: def setVariant(self, new_variant: InstanceContainer) -> None:
self.replaceContainer(_ContainerIndexes.Variant, new_variant) self.replaceContainer(_ContainerIndexes.Variant, new_variant)
## Set the variant container by an ID.
#
# This will search for the specified container and set it. If no container was found, an error will be raised.
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultVariant
# for details.
#
# \param new_quality_changes_id The ID of the new variant container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setVariantById(self, new_variant_id: str) -> None:
variant = self._empty_variant
if new_variant_id == "default":
new_variant = self.findDefaultVariant()
if new_variant:
variant = new_variant
else:
variants = ContainerRegistry.getInstance().findInstanceContainers(id = new_variant_id)
if variants:
variant = variants[0]
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_variant_id))
self.setVariant(variant)
## Get the variant container. ## Get the variant container.
# #
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -220,22 +131,10 @@ class CuraContainerStack(ContainerStack):
## Set the definition changes container. ## Set the definition changes container.
# #
# \param new_quality_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "quality_changes". # \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
## Set the definition changes container by an ID.
#
# \param new_quality_changes_id The ID of the new definition changes container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setDefinitionChangesById(self, new_definition_changes_id: str) -> None:
new_definition_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_definition_changes_id)
if new_definition_changes:
self.setDefinitionChanges(new_definition_changes[0])
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_changes_id))
## Get the definition changes container. ## Get the definition changes container.
# #
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -245,22 +144,10 @@ class CuraContainerStack(ContainerStack):
## Set the definition container. ## Set the definition container.
# #
# \param new_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes". # \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition".
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
self.replaceContainer(_ContainerIndexes.Definition, new_definition) self.replaceContainer(_ContainerIndexes.Definition, new_definition)
## Set the definition container by an ID.
#
# \param new_quality_changes_id The ID of the new definition container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setDefinitionById(self, new_definition_id: str) -> None:
new_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = new_definition_id)
if new_definition:
self.setDefinition(new_definition[0])
else:
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_id))
## Get the definition container. ## Get the definition container.
# #
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer. # \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@ -348,6 +235,10 @@ class CuraContainerStack(ContainerStack):
elif container != self._empty_instance_container and container.getMetaDataEntry("type") != expected_type: elif container != self._empty_instance_container and container.getMetaDataEntry("type") != expected_type:
raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not of {type} type, but {actual_type} type.".format(index = index, type = expected_type, actual_type = container.getMetaDataEntry("type"))) raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not of {type} type, but {actual_type} type.".format(index = index, type = expected_type, actual_type = container.getMetaDataEntry("type")))
current_container = self._containers[index]
if current_container.getId() == container.getId():
return
super().replaceContainer(index, container, postpone_emit) super().replaceContainer(index, container, postpone_emit)
## Overridden from ContainerStack ## Overridden from ContainerStack
@ -391,198 +282,6 @@ class CuraContainerStack(ContainerStack):
self._containers = new_containers self._containers = new_containers
## Find the variant that should be used as "default" variant.
#
# This will search for variants that match the current definition and pick the preferred one,
# if specified by the machine definition.
#
# The following criteria are used to find the default variant:
# - If the machine definition does not have a metadata entry "has_variants" set to True, return None
# - The definition of the variant should be the same as the machine definition for this stack.
# - The container should have a metadata entry "type" with value "variant".
# - If the machine definition has a metadata entry "preferred_variant", filter the variant IDs based on that.
#
# \return The container that should be used as default, or None if nothing was found or the machine does not use variants.
#
# \note This method assumes the stack has a valid machine definition.
def findDefaultVariant(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition()
# has_variants can be overridden in other containers and stacks.
# In the case of UM2, it is overridden in the GlobalStack
if not self.getMetaDataEntry("has_variants"):
# If the machine does not use variants, we should never set a variant.
return None
# First add any variant. Later, overwrite with preference if the preference is valid.
variant = None
definition_id = self._findInstanceContainerDefinitionId(definition)
variants = ContainerRegistry.getInstance().findInstanceContainers(definition = definition_id, type = "variant")
if variants:
variant = variants[0]
preferred_variant_id = definition.getMetaDataEntry("preferred_variant")
if preferred_variant_id:
preferred_variants = ContainerRegistry.getInstance().findInstanceContainers(id = preferred_variant_id, definition = definition_id, type = "variant")
if preferred_variants:
variant = preferred_variants[0]
else:
Logger.log("w", "The preferred variant \"{variant}\" of stack {stack} does not exist or is not a variant.", variant = preferred_variant_id, stack = self.id)
# And leave it at the default variant.
if variant:
return variant
Logger.log("w", "Could not find a valid default variant for stack {stack}", stack = self.id)
return None
## Find the material that should be used as "default" material.
#
# This will search for materials that match the current definition and pick the preferred one,
# if specified by the machine definition.
#
# The following criteria are used to find the default material:
# - If the machine definition does not have a metadata entry "has_materials" set to True, return None
# - If the machine definition has a metadata entry "has_machine_materials", the definition of the material should
# be the same as the machine definition for this stack. Otherwise, the definition should be "fdmprinter".
# - The container should have a metadata entry "type" with value "material".
# - The material should have an approximate diameter that matches the machine
# - If the machine definition has a metadata entry "has_variants" and set to True, the "variant" metadata entry of
# the material should be the same as the ID of the variant in the stack. Only applies if "has_machine_materials" is also True.
# - If the stack currently has a material set, try to find a material that matches the current material by name.
# - Otherwise, if the machine definition has a metadata entry "preferred_material", try to find a material that matches the specified ID.
#
# \return The container that should be used as default, or None if nothing was found or the machine does not use materials.
def findDefaultMaterial(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition()
if not definition.getMetaDataEntry("has_materials"):
# Machine does not use materials, never try to set it.
return None
search_criteria = {"type": "material"}
if definition.getMetaDataEntry("has_machine_materials"):
search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
if definition.getMetaDataEntry("has_variants"):
search_criteria["variant"] = self.variant.id
else:
search_criteria["definition"] = "fdmprinter"
if self.material != self._empty_material:
search_criteria["name"] = self.material.name
else:
preferred_material = definition.getMetaDataEntry("preferred_material")
if preferred_material:
search_criteria["id"] = preferred_material
approximate_material_diameter = str(round(self.getProperty("material_diameter", "value")))
search_criteria["approximate_diameter"] = approximate_material_diameter
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if not materials:
Logger.log("w", "The preferred material \"{material}\" could not be found for stack {stack}", material = preferred_material, stack = self.id)
# We failed to find any materials matching the specified criteria, drop some specific criteria and try to find
# a material that sort-of matches what we want.
search_criteria.pop("variant", None)
search_criteria.pop("id", None)
search_criteria.pop("name", None)
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if not materials:
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None
for material in materials:
# Prefer a read-only material
if ContainerRegistry.getInstance().isReadOnly(material.getId()):
return material
return materials[0]
## Find the quality that should be used as "default" quality.
#
# This will search for qualities that match the current definition and pick the preferred one,
# if specified by the machine definition.
#
# \return The container that should be used as default, or None if nothing was found.
def findDefaultQuality(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition()
registry = ContainerRegistry.getInstance()
material_container = self.material if self.material.getId() not in (self._empty_material.getId(), self._empty_instance_container.getId()) else None
search_criteria = {"type": "quality"}
if definition.getMetaDataEntry("has_machine_quality"):
search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
if definition.getMetaDataEntry("has_materials") and material_container:
search_criteria["material"] = material_container.id
else:
search_criteria["definition"] = "fdmprinter"
if self.quality != self._empty_quality:
search_criteria["name"] = self.quality.name
else:
preferred_quality = definition.getMetaDataEntry("preferred_quality")
if preferred_quality:
search_criteria["id"] = preferred_quality
containers = registry.findInstanceContainers(**search_criteria)
if containers:
return containers[0]
if "material" in search_criteria:
# First check if we can solve our material not found problem by checking if we can find quality containers
# that are assigned to the parents of this material profile.
try:
inherited_files = material_container.getInheritedFiles()
except AttributeError: # Material_container does not support inheritance.
inherited_files = []
if inherited_files:
for inherited_file in inherited_files:
# Extract the ID from the path we used to load the file.
search_criteria["material"] = os.path.basename(inherited_file).split(".")[0]
containers = registry.findInstanceContainers(**search_criteria)
if containers:
return containers[0]
# We still weren't able to find a quality for this specific material.
# Try to find qualities for a generic version of the material.
material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
if definition.getMetaDataEntry("has_machine_quality"):
if self.material != self._empty_instance_container:
material_search_criteria["definition"] = material_container.getMetaDataEntry("definition")
if definition.getMetaDataEntry("has_variants"):
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
else:
material_search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
if definition.getMetaDataEntry("has_variants") and self.variant != self._empty_instance_container:
material_search_criteria["variant"] = self.variant.id
else:
material_search_criteria["definition"] = "fdmprinter"
material_containers = registry.findInstanceContainersMetadata(**material_search_criteria)
# Try all materials to see if there is a quality profile available.
for material_container in material_containers:
search_criteria["material"] = material_container["id"]
containers = registry.findInstanceContainers(**search_criteria)
if containers:
return containers[0]
if "name" in search_criteria or "id" in search_criteria:
# If a quality by this name can not be found, try a wider set of search criteria
search_criteria.pop("name", None)
search_criteria.pop("id", None)
containers = registry.findInstanceContainers(**search_criteria)
if containers:
return containers[0]
return None
## protected: ## protected:
# Helper to make sure we emit a PyQt signal on container changes. # Helper to make sure we emit a PyQt signal on container changes.

View file

@ -1,15 +1,18 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger from typing import Optional
from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction
from UM.Util import parseBool
from cura.Machines.VariantManager import VariantType
from .GlobalStack import GlobalStack from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack from .ExtruderStack import ExtruderStack
from typing import Optional
## Contains helper functions to create new machines. ## Contains helper functions to create new machines.
@ -22,7 +25,13 @@ class CuraStackBuilder:
# \return The new global stack or None if an error occurred. # \return The new global stack or None if an error occurred.
@classmethod @classmethod
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager()
registry = ContainerRegistry.getInstance() registry = ContainerRegistry.getInstance()
definitions = registry.findDefinitionContainers(id = definition_id) definitions = registry.findDefinitionContainers(id = definition_id)
if not definitions: if not definitions:
Logger.log("w", "Definition {definition} was not found!", definition = definition_id) Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
@ -30,7 +39,21 @@ class CuraStackBuilder:
machine_definition = definitions[0] machine_definition = definitions[0]
generated_name = registry.createUniqueName("machine", "", name, machine_definition.name) # get variant container for the global stack
global_variant_container = application.empty_variant_container
global_variant_node = variant_manager.getDefaultVariantNode(machine_definition, VariantType.BUILD_PLATE)
if global_variant_node:
global_variant_container = global_variant_node.getContainer()
# get variant container for extruders
extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(machine_definition, VariantType.NOZZLE)
extruder_variant_name = None
if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer()
extruder_variant_name = extruder_variant_container.getName()
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
# Make sure the new name does not collide with any definition or (quality) profile # Make sure the new name does not collide with any definition or (quality) profile
# createUniqueName() only looks at other stacks, but not at definitions or quality profiles # createUniqueName() only looks at other stacks, but not at definitions or quality profiles
# Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true # Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true
@ -40,47 +63,59 @@ class CuraStackBuilder:
new_global_stack = cls.createGlobalStack( new_global_stack = cls.createGlobalStack(
new_stack_id = generated_name, new_stack_id = generated_name,
definition = machine_definition, definition = machine_definition,
quality = "default", variant_container = global_variant_container,
material = "default", material_container = application.empty_material_container,
variant = "default", quality_container = application.empty_quality_container,
) )
new_global_stack.setName(generated_name) new_global_stack.setName(generated_name)
extruder_definition = registry.findDefinitionContainers(machine = machine_definition.getId()) # get material container for extruders
material_container = application.empty_material_container
material_node = material_manager.getDefaultMaterial(new_global_stack, extruder_variant_name)
if material_node:
material_container = material_node.getContainer()
if not extruder_definition: # Create ExtruderStacks
# create extruder stack for single extrusion machines that have no separate extruder definition files extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
extruder_definition = registry.findDefinitionContainers(id = "fdmextruder")[0]
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id) for position, extruder_definition_id in extruder_dict.items():
# Sanity check: make sure that the positions in the extruder definitions are same as in the machine
# definition
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
position_in_extruder_def = extruder_definition.getMetaDataEntry("position")
if position_in_extruder_def != position:
raise RuntimeError("Extruder position [%s] defined in extruder definition [%s] is not the same as in machine definition [%s] position [%s]" %
(position_in_extruder_def, extruder_definition_id, definition_id, position))
new_extruder_id = registry.uniqueName(extruder_definition_id)
new_extruder = cls.createExtruderStack( new_extruder = cls.createExtruderStack(
new_extruder_id, new_extruder_id,
definition = extruder_definition, extruder_definition = extruder_definition,
machine_definition_id = machine_definition.getId(), machine_definition_id = definition_id,
quality = "default", position = position,
material = "default", variant_container = extruder_variant_container,
variant = "default", material_container = material_container,
next_stack = new_global_stack quality_container = application.empty_quality_container,
global_stack = new_global_stack,
) )
new_extruder.setNextStack(new_global_stack)
new_global_stack.addExtruder(new_extruder) new_global_stack.addExtruder(new_extruder)
else: registry.addContainer(new_extruder)
# create extruder stack for each found extruder definition
for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id):
position = extruder_definition.getMetaDataEntry("position", None)
if not position:
Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.id)
new_extruder_id = registry.uniqueName(extruder_definition.id) preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
new_extruder = cls.createExtruderStack( quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
new_extruder_id, quality_group = quality_group_dict.get(preferred_quality_type)
definition = extruder_definition,
machine_definition_id = machine_definition.getId(), new_global_stack.quality = quality_group.node_for_global.getContainer()
quality = "default", for position, extruder_stack in new_global_stack.extruders.items():
material = "default", if position in quality_group.nodes_for_extruders:
variant = "default", extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
next_stack = new_global_stack else:
) extruder_stack.quality = application.empty_quality_container
new_global_stack.addExtruder(new_extruder)
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
registry.addContainer(new_global_stack)
return new_global_stack return new_global_stack
@ -94,50 +129,32 @@ class CuraStackBuilder:
# #
# \return A new Global stack instance with the specified parameters. # \return A new Global stack instance with the specified parameters.
@classmethod @classmethod
def createExtruderStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, machine_definition_id: str, **kwargs) -> ExtruderStack: def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str,
stack = ExtruderStack(new_stack_id) position: int,
stack.setName(definition.getName()) variant_container, material_container, quality_container, global_stack) -> ExtruderStack:
stack.setDefinition(definition)
stack.addMetaDataEntry("position", definition.getMetaDataEntry("position"))
if "next_stack" in kwargs:
# Add stacks before containers are added, since they may trigger a setting update.
stack.setNextStack(kwargs["next_stack"])
user_container = InstanceContainer(new_stack_id + "_user")
user_container.addMetaDataEntry("type", "user")
user_container.addMetaDataEntry("extruder", new_stack_id)
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) application = CuraApplication.getInstance()
user_container.setDefinition(machine_definition_id)
stack.setUserChanges(user_container) stack = ExtruderStack(new_stack_id, parent = global_stack)
stack.setName(extruder_definition.getName())
stack.setDefinition(extruder_definition)
# Important! The order here matters, because that allows the stack to stack.addMetaDataEntry("position", position)
# assume the material and variant have already been set.
if "definition_changes" in kwargs:
stack.setDefinitionChangesById(kwargs["definition_changes"])
else:
stack.setDefinitionChanges(cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings"))
if "variant" in kwargs: user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id,
stack.setVariantById(kwargs["variant"]) is_global_stack = False)
if "material" in kwargs: stack.definitionChanges = cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings")
stack.setMaterialById(kwargs["material"]) stack.variant = variant_container
stack.material = material_container
if "quality" in kwargs: stack.quality = quality_container
stack.setQualityById(kwargs["quality"]) stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container
if "quality_changes" in kwargs:
stack.setQualityChangesById(kwargs["quality_changes"])
# Only add the created containers to the registry after we have set all the other # Only add the created containers to the registry after we have set all the other
# properties. This makes the create operation more transactional, since any problems # properties. This makes the create operation more transactional, since any problems
# setting properties will not result in incomplete containers being added. # setting properties will not result in incomplete containers being added.
registry = ContainerRegistry.getInstance() ContainerRegistry.getInstance().addContainer(user_container)
registry.addContainer(stack)
registry.addContainer(user_container)
return stack return stack
@ -149,46 +166,48 @@ class CuraStackBuilder:
# #
# \return A new Global stack instance with the specified parameters. # \return A new Global stack instance with the specified parameters.
@classmethod @classmethod
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, **kwargs) -> GlobalStack: def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
variant_container, material_container, quality_container) -> GlobalStack:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
stack = GlobalStack(new_stack_id) stack = GlobalStack(new_stack_id)
stack.setDefinition(definition) stack.setDefinition(definition)
user_container = InstanceContainer(new_stack_id + "_user") # Create user container
user_container.addMetaDataEntry("type", "user") user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id,
user_container.addMetaDataEntry("machine", new_stack_id) is_global_stack = True)
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(definition.getId())
stack.setUserChanges(user_container) stack.definitionChanges = cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings")
stack.variant = variant_container
stack.material = material_container
stack.quality = quality_container
stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container
# Important! The order here matters, because that allows the stack to ContainerRegistry.getInstance().addContainer(user_container)
# assume the material and variant have already been set.
if "definition_changes" in kwargs:
stack.setDefinitionChangesById(kwargs["definition_changes"])
else:
stack.setDefinitionChanges(cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings"))
if "variant" in kwargs:
stack.setVariantById(kwargs["variant"])
if "material" in kwargs:
stack.setMaterialById(kwargs["material"])
if "quality" in kwargs:
stack.setQualityById(kwargs["quality"])
if "quality_changes" in kwargs:
stack.setQualityChangesById(kwargs["quality_changes"])
registry = ContainerRegistry.getInstance()
registry.addContainer(stack)
registry.addContainer(user_container)
return stack return stack
@classmethod @classmethod
def createDefinitionChangesContainer(cls, container_stack, container_name, container_index = None): def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
is_global_stack: bool) -> "InstanceContainer":
from cura.CuraApplication import CuraApplication
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
container = InstanceContainer(unique_container_name)
container.setDefinition(definition_id)
container.addMetaDataEntry("type", "user")
container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
metadata_key_to_add = "machine" if is_global_stack else "extruder"
container.addMetaDataEntry(metadata_key_to_add, stack_id)
return container
@classmethod
def createDefinitionChangesContainer(cls, container_stack, container_name):
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name) unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)

View file

@ -12,6 +12,7 @@ from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.SettingInstance import SettingInstance
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Optional, List, TYPE_CHECKING, Union from typing import Optional, List, TYPE_CHECKING, Union
@ -30,22 +31,19 @@ class ExtruderManager(QObject):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance()
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = [] self._selected_object_extruders = []
self._global_container_stack_definition_id = None
self._addCurrentMachineExtruders() self._addCurrentMachineExtruders()
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged) #Application.getInstance().globalContainerStackChanged.connect(self._globalContainerStackChanged)
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes. ## Signal to notify other components when the list of extruders for a machine definition changes.
extrudersChanged = pyqtSignal(QVariant) extrudersChanged = pyqtSignal(QVariant)
## Signal to notify other components when the global container stack is switched to a definition
# that has different extruders than the previous global container stack
globalContainerStackDefinitionChanged = pyqtSignal()
## Notify when the user switches the currently active extruder. ## Notify when the user switches the currently active extruder.
activeExtruderChanged = pyqtSignal() activeExtruderChanged = pyqtSignal()
@ -181,6 +179,7 @@ class ExtruderManager(QObject):
self._selected_object_extruders = [] self._selected_object_extruders = []
self.selectedObjectExtrudersChanged.emit() self.selectedObjectExtrudersChanged.emit()
@pyqtSlot(result = QObject)
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
@ -270,7 +269,7 @@ class ExtruderManager(QObject):
return [] return []
# Get the extruders of all printable meshes in the scene # Get the extruders of all printable meshes in the scene
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()] meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()]
for mesh in meshes: for mesh in meshes:
extruder_stack_id = mesh.callDecoration("getActiveExtruder") extruder_stack_id = mesh.callDecoration("getActiveExtruder")
if not extruder_stack_id: if not extruder_stack_id:
@ -368,12 +367,7 @@ class ExtruderManager(QObject):
return result[:machine_extruder_count] return result[:machine_extruder_count]
def __globalContainerStackChanged(self) -> None: def _globalContainerStackChanged(self) -> None:
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and global_container_stack.getBottom() and global_container_stack.getBottom().getId() != self._global_container_stack_definition_id:
self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
self.globalContainerStackDefinitionChanged.emit()
# If the global container changed, the machine changed and might have extruders that were not registered yet # If the global container changed, the machine changed and might have extruders that were not registered yet
self._addCurrentMachineExtruders() self._addCurrentMachineExtruders()
@ -381,7 +375,7 @@ class ExtruderManager(QObject):
## Adds the extruders of the currently active machine. ## Adds the extruders of the currently active machine.
def _addCurrentMachineExtruders(self) -> None: def _addCurrentMachineExtruders(self) -> None:
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
extruders_changed = False extruders_changed = False
if global_stack: if global_stack:
@ -401,13 +395,82 @@ class ExtruderManager(QObject):
self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train
# regardless of what the next stack is, we have to set it again, because of signal routing. ??? # regardless of what the next stack is, we have to set it again, because of signal routing. ???
extruder_train.setParent(global_stack)
extruder_train.setNextStack(global_stack) extruder_train.setNextStack(global_stack)
extruders_changed = True extruders_changed = True
self._fixMaterialDiameterAndNozzleSize(global_stack, extruder_trains)
if extruders_changed: if extruders_changed:
self.extrudersChanged.emit(global_stack_id) self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0) self.setActiveExtruderIndex(0)
#
# This function tries to fix the problem with per-extruder-settable nozzle size and material diameter problems
# in early versions (3.0 - 3.2.1).
#
# In earlier versions, "nozzle size" and "material diameter" are only applicable to the complete machine, so all
# extruders share the same values. In this case, "nozzle size" and "material diameter" are saved in the
# GlobalStack's DefinitionChanges container.
#
# Later, we could have different "nozzle size" for each extruder, but "material diameter" could only be set for
# the entire machine. In this case, "nozzle size" should be saved in each ExtruderStack's DefinitionChanges, but
# "material diameter" still remains in the GlobalStack's DefinitionChanges.
#
# Lateer, both "nozzle size" and "material diameter" are settable per-extruder, and both settings should be saved
# in the ExtruderStack's DefinitionChanges.
#
# There were some bugs in upgrade so the data weren't saved correct as described above. This function tries fix
# this.
#
# One more thing is about material diameter and single-extrusion machines. Most single-extrusion machines don't
# specifically define their extruder definition, so they reuse "fdmextruder", but for those machines, they may
# define "material diameter = 1.75" in their machine definition, but in "fdmextruder", it's still "2.85". This
# causes a problem with incorrect default values.
#
# This is also fixed here in this way: If no "material diameter" is specified, it will look for the default value
# in both the Extruder's definition and the Global's definition. If 2 values don't match, we will use the value
# from the Global definition by setting it in the Extruder's DefinitionChanges container.
#
def _fixMaterialDiameterAndNozzleSize(self, global_stack, extruder_stack_list):
keys_to_copy = ["material_diameter", "machine_nozzle_size"] # these will be copied over to all extruders
extruder_positions_to_update = set()
for extruder_stack in extruder_stack_list:
for key in keys_to_copy:
# Only copy the value when this extruder doesn't have the value.
if extruder_stack.definitionChanges.hasProperty(key, "value"):
continue
setting_value_in_global_def_changes = global_stack.definitionChanges.getProperty(key, "value")
setting_value_in_global_def = global_stack.definition.getProperty(key, "value")
setting_value = setting_value_in_global_def
if setting_value_in_global_def_changes is not None:
setting_value = setting_value_in_global_def_changes
if setting_value == extruder_stack.definition.getProperty(key, "value"):
continue
setting_definition = global_stack.getSettingDefinition(key)
new_instance = SettingInstance(setting_definition, extruder_stack.definitionChanges)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
extruder_stack.definitionChanges.addInstance(new_instance)
extruder_stack.definitionChanges.setDirty(True)
# Make sure the material diameter is up to date for the extruder stack.
if key == "material_diameter":
position = int(extruder_stack.getMetaDataEntry("position"))
extruder_positions_to_update.add(position)
# We have to remove those settings here because we know that those values have been copied to all
# the extruders at this point.
for key in keys_to_copy:
if global_stack.definitionChanges.hasProperty(key, "value"):
global_stack.definitionChanges.removeInstance(key, postpone_emit = True)
# Update material diameter for extruders
for position in extruder_positions_to_update:
self.updateMaterialForDiameter(position, global_stack = global_stack)
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.
# #
# This is exposed to SettingFunction so it can be used in value functions. # This is exposed to SettingFunction so it can be used in value functions.
@ -492,6 +555,96 @@ class ExtruderManager(QObject):
def getInstanceExtruderValues(self, key): def getInstanceExtruderValues(self, key):
return ExtruderManager.getExtruderValues(key) return ExtruderManager.getExtruderValues(key)
## Updates the material container to a material that matches the material diameter set for the printer
def updateMaterialForDiameter(self, extruder_position: int, global_stack = None):
if not global_stack:
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return
if not global_stack.getMetaDataEntry("has_materials", False):
return
extruder_stack = global_stack.extruders[str(extruder_position)]
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
setting_provider = extruder_stack
if not material_diameter:
if extruder_stack.definition.hasProperty("material_diameter", "value"):
material_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
else:
material_diameter = global_stack.definition.getProperty("material_diameter", "value")
setting_provider = global_stack
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(setting_provider)
machine_approximate_diameter = str(round(material_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if global_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = global_stack.definition.getId()
has_material_variants = global_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
old_material = extruder_stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"brand": old_material.getMetaDataEntry("brand", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = extruder_stack.variant.getId()
container_registry = Application.getInstance().getContainerRegistry()
empty_material = container_registry.findInstanceContainers(id = "empty_material")[0]
if old_material == empty_material:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria.pop("definition", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic"
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [empty_material]
Logger.log("i", "Selecting new material: %s", materials[0].getId())
extruder_stack.material = materials[0]
## Get the value for a setting from a specific extruder. ## Get the value for a setting from a specific extruder.
# #
# This is exposed to SettingFunction to use in value functions. # This is exposed to SettingFunction to use in value functions.

View file

@ -3,6 +3,8 @@
from typing import Any, TYPE_CHECKING, Optional from typing import Any, TYPE_CHECKING, Optional
from PyQt5.QtCore import pyqtProperty
from UM.Decorators import override from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
@ -10,12 +12,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
from . import Exceptions from . import Exceptions
from .CuraContainerStack import CuraContainerStack from .CuraContainerStack import CuraContainerStack, _ContainerIndexes
from .ExtruderManager import ExtruderManager from .ExtruderManager import ExtruderManager
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
## Represents an Extruder and its related containers. ## Represents an Extruder and its related containers.
# #
# #
@ -31,7 +34,7 @@ class ExtruderStack(CuraContainerStack):
# #
# This will set the next stack and ensure that we register this stack as an extruder. # This will set the next stack and ensure that we register this stack as an extruder.
@override(ContainerStack) @override(ContainerStack)
def setNextStack(self, stack: ContainerStack) -> None: def setNextStack(self, stack: CuraContainerStack) -> None:
super().setNextStack(stack) super().setNextStack(stack)
stack.addExtruder(self) stack.addExtruder(self)
self.addMetaDataEntry("machine", stack.id) self.addMetaDataEntry("machine", stack.id)
@ -47,6 +50,29 @@ class ExtruderStack(CuraContainerStack):
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 3 return 3
## Return the filament diameter that the machine requires.
#
# If the machine has no requirement for the diameter, -1 is returned.
# \return The filament diameter for the printer
@property
def materialDiameter(self) -> float:
context = PropertyEvaluationContext(self)
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
return self.getProperty("material_diameter", "value", context = context)
## Return the approximate filament diameter that the machine requires.
#
# The approximate material diameter is the material diameter rounded to
# the nearest millimetre.
#
# If the machine has no requirement for the diameter, -1 is returned.
#
# \return The approximate filament diameter for the printer
@pyqtProperty(float)
def approximateMaterialDiameter(self) -> float:
return round(float(self.materialDiameter))
## Overridden from ContainerStack ## Overridden from ContainerStack
# #
# It will perform a few extra checks when trying to get properties. # It will perform a few extra checks when trying to get properties.
@ -115,11 +141,6 @@ class ExtruderStack(CuraContainerStack):
if has_global_dependencies: if has_global_dependencies:
self.getNextStack().propertiesChanged.emit(key, properties) self.getNextStack().propertiesChanged.emit(key, properties)
def findDefaultVariant(self):
# The default variant is defined in the machine stack and/or definition, so use the machine stack to find
# the default variant.
return self.getNextStack().findDefaultVariant()
extruder_stack_mime = MimeType( extruder_stack_mime = MimeType(
name = "application/x-cura-extruderstack", name = "application/x-cura-extruderstack",

View file

@ -1,6 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
import threading
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from PyQt5.QtCore import pyqtProperty from PyQt5.QtCore import pyqtProperty
@ -30,7 +32,8 @@ class GlobalStack(CuraContainerStack):
# This property is used to track which settings we are calculating the "resolve" for # This property is used to track which settings we are calculating the "resolve" for
# and if so, to bypass the resolve to prevent an infinite recursion that would occur # and if so, to bypass the resolve to prevent an infinite recursion that would occur
# if the resolve function tried to access the same property it is a resolve for. # if the resolve function tried to access the same property it is a resolve for.
self._resolving_settings = set() # Per thread we have our own resolving_settings, or strange things sometimes occur.
self._resolving_settings = defaultdict(set) # keys are thread names
## Get the list of extruders of this stack. ## Get the list of extruders of this stack.
# #
@ -91,9 +94,10 @@ class GlobalStack(CuraContainerStack):
# Handle the "resolve" property. # Handle the "resolve" property.
if self._shouldResolve(key, property_name, context): if self._shouldResolve(key, property_name, context):
self._resolving_settings.add(key) current_thread = threading.current_thread()
self._resolving_settings[current_thread.name].add(key)
resolve = super().getProperty(key, "resolve", context) resolve = super().getProperty(key, "resolve", context)
self._resolving_settings.remove(key) self._resolving_settings[current_thread.name].remove(key)
if resolve is not None: if resolve is not None:
return resolve return resolve
@ -121,21 +125,6 @@ class GlobalStack(CuraContainerStack):
def setNextStack(self, next_stack: ContainerStack) -> None: def setNextStack(self, next_stack: ContainerStack) -> None:
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!") raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
## Gets the approximate filament diameter that the machine requires.
#
# The approximate material diameter is the material diameter rounded to
# the nearest millimetre.
#
# If the machine has no requirement for the diameter, -1 is returned.
#
# \return The approximate filament diameter for the printer, as a string.
@pyqtProperty(str)
def approximateMaterialDiameter(self) -> str:
material_diameter = self.definition.getProperty("material_diameter", "value")
if material_diameter is None:
return "-1"
return str(round(float(material_diameter))) #Round, then convert back to string.
# protected: # protected:
# Determine whether or not we should try to get the "resolve" property instead of the # Determine whether or not we should try to get the "resolve" property instead of the
@ -145,7 +134,8 @@ class GlobalStack(CuraContainerStack):
# Do not try to resolve anything but the "value" property # Do not try to resolve anything but the "value" property
return False return False
if key in self._resolving_settings: current_thread = threading.current_thread()
if key in self._resolving_settings[current_thread.name]:
# To prevent infinite recursion, if getProperty is called with the same key as # To prevent infinite recursion, if getProperty is called with the same key as
# we are already trying to resolve, we should not try to resolve again. Since # we are already trying to resolve, we should not try to resolve again. Since
# this can happen multiple times when trying to resolve a value, we need to # this can happen multiple times when trying to resolve a value, we need to

File diff suppressed because it is too large Load diff

View file

@ -1,57 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSlot #To expose data to QML.
from cura.Settings.ContainerManager import ContainerManager
from UM.Logger import Logger
from UM.Message import Message #To create a warning message about material diameter.
from UM.i18n import i18nCatalog #Translated strings.
catalog = i18nCatalog("cura")
## Handles material-related data, processing requests to change them and
# providing data for the GUI.
#
# TODO: Move material-related managing over from the machine manager to here.
class MaterialManager(QObject):
## Creates the global values for the material manager to use.
def __init__(self, parent = None):
super().__init__(parent)
#Material diameter changed warning message.
self._material_diameter_warning_message = Message(catalog.i18nc("@info:status Has a cancel button next to it.",
"The selected material diameter causes the material to become incompatible with the current printer."), title = catalog.i18nc("@info:title", "Incompatible Material"))
self._material_diameter_warning_message.addAction("Undo", catalog.i18nc("@action:button", "Undo"), None, catalog.i18nc("@action", "Undo changing the material diameter."))
self._material_diameter_warning_message.actionTriggered.connect(self._materialWarningMessageAction)
## Creates an instance of the MaterialManager.
#
# This should only be called by PyQt to create the singleton instance of
# this class.
@staticmethod
def createMaterialManager(engine = None, script_engine = None):
return MaterialManager()
@pyqtSlot(str, str)
def showMaterialWarningMessage(self, material_id, previous_diameter):
self._material_diameter_warning_message.previous_diameter = previous_diameter #Make sure that the undo button can properly undo the action.
self._material_diameter_warning_message.material_id = material_id
self._material_diameter_warning_message.show()
## Called when clicking "undo" on the warning dialogue for disappeared
# materials.
#
# This executes the undo action, restoring the material diameter.
#
# \param button The identifier of the button that was pressed.
def _materialWarningMessageAction(self, message, button):
if button == "Undo":
container_manager = ContainerManager.getInstance()
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "properties/diameter", self._material_diameter_warning_message.previous_diameter)
approximate_previous_diameter = str(round(float(self._material_diameter_warning_message.previous_diameter)))
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "approximate_diameter", approximate_previous_diameter)
container_manager.setContainerProperty(self._material_diameter_warning_message.material_id, "material_diameter", "value", self._material_diameter_warning_message.previous_diameter);
message.hide()
else:
Logger.log("w", "Unknown button action for material diameter warning message: {action}".format(action = button))

View file

@ -3,13 +3,14 @@
import UM.Settings.Models.SettingVisibilityHandler import UM.Settings.Models.SettingVisibilityHandler
class MaterialSettingsVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler): class MaterialSettingsVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
def __init__(self, parent = None, *args, **kwargs): def __init__(self, parent = None, *args, **kwargs):
super().__init__(parent = parent, *args, **kwargs) super().__init__(parent = parent, *args, **kwargs)
material_settings = { material_settings = {
"default_material_print_temperature", "default_material_print_temperature",
"material_bed_temperature", "default_material_bed_temperature",
"material_standby_temperature", "material_standby_temperature",
#"material_flow_temp_graph", #"material_flow_temp_graph",
"cool_fan_speed", "cool_fan_speed",

View file

@ -1,37 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, List
from UM.Settings.ContainerRegistry import ContainerRegistry #To listen for changes to the materials.
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel #We're extending this class.
## A model that shows a list of currently valid materials.
class MaterialsModel(InstanceContainersModel):
def __init__(self, parent = None):
super().__init__(parent)
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerMetaDataChanged)
## Called when the metadata of the container was changed.
#
# This makes sure that we only update when it was a material that changed.
#
# \param container The container whose metadata was changed.
def _onContainerMetaDataChanged(self, container):
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
self._container_change_timer.start()
def _onContainerChanged(self, container):
if container.getMetaDataEntry("type", "") == "material":
super()._onContainerChanged(container)
## Group brand together
def _sortKey(self, item) -> List[Any]:
result = []
result.append(item["metadata"]["brand"])
result.append(item["metadata"]["material"])
result.append(item["metadata"]["name"])
result.append(item["metadata"]["color_name"])
result.append(item["metadata"]["id"])
result.extend(super()._sortKey(item))
return result

View file

@ -1,224 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import OrderedDict
from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
from cura.QualityManager import QualityManager
from cura.Settings.ExtruderManager import ExtruderManager
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
## QML Model for listing the current list of valid quality profiles.
#
class ProfilesModel(InstanceContainersModel):
LayerHeightRole = Qt.UserRole + 1001
LayerHeightWithoutUnitRole = Qt.UserRole + 1002
AvailableRole = Qt.UserRole + 1003
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.LayerHeightRole, "layer_height")
self.addRoleName(self.LayerHeightWithoutUnitRole, "layer_height_without_unit")
self.addRoleName(self.AvailableRole, "available")
Application.getInstance().globalContainerStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
# Factory function, used by QML
@staticmethod
def createProfilesModel(engine, js_engine):
return ProfilesModel.getInstance()
## Get the singleton instance for this class.
@classmethod
def getInstance(cls) -> "ProfilesModel":
# Note: Explicit use of class name to prevent issues with inheritance.
if not ProfilesModel.__instance:
ProfilesModel.__instance = cls()
return ProfilesModel.__instance
@classmethod
def hasInstance(cls) -> bool:
return ProfilesModel.__instance is not None
__instance = None # type: "ProfilesModel"
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return {}, {}
global_stack_definition = global_container_stack.definition
# Get the list of extruders and place the selected extruder at the front of the list.
extruder_stacks = self._getOrderedExtruderStacksList()
materials = [extruder.material for extruder in extruder_stacks]
# Fetch the list of usable qualities across all extruders.
# The actual list of quality profiles come from the first extruder in the extruder list.
result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
# The usable quality types are set
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in result])
# Fetch all qualities available for this machine and the materials selected in extruders
all_qualities = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition, materials)
# If in the all qualities there is some of them that are not available due to incompatibility with materials
# we also add it so that they will appear in the slide quality bar. However in recomputeItems will be marked as
# not available so they will be shown in gray
for quality in all_qualities:
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
result.append(quality)
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
if len(result) == 0:
# If not qualities are found we dynamically create a not supported container for this machine + material combination
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
result.append(not_supported_container)
return {item.getId():item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
## Re-computes the items in this model, and adds the layer height role.
def _recomputeItems(self):
# Some globals that we can re-use.
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return
extruder_stacks = self._getOrderedExtruderStacksList()
container_registry = ContainerRegistry.getInstance()
# Get a list of usable/available qualities for this machine and material
qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
if not unit:
unit = ""
# group all quality items according to quality_types, so we know which profile suits the currently
# active machine and material, and later yield the right ones.
tmp_all_quality_items = OrderedDict()
for item in super()._recomputeItems():
profiles = container_registry.findContainersMetadata(id = item["id"])
if not profiles or "quality_type" not in profiles[0]:
quality_type = ""
else:
quality_type = profiles[0]["quality_type"]
if quality_type not in tmp_all_quality_items:
tmp_all_quality_items[quality_type] = {"suitable_container": None, "all_containers": []}
tmp_all_quality_items[quality_type]["all_containers"].append(item)
if tmp_all_quality_items[quality_type]["suitable_container"] is None:
tmp_all_quality_items[quality_type]["suitable_container"] = item
# reverse the ordering (finest first, coarsest last)
all_quality_items = OrderedDict()
for key in reversed(tmp_all_quality_items.keys()):
all_quality_items[key] = tmp_all_quality_items[key]
# First the suitable containers are set in the model
containers = []
for data_item in all_quality_items.values():
suitable_item = data_item["suitable_container"]
if suitable_item is not None:
containers.append(suitable_item)
# Once the suitable containers are collected, the rest of the containers are appended
for data_item in all_quality_items.values():
for item in data_item["all_containers"]:
if item not in containers:
containers.append(item)
# Now all the containers are set
for item in containers:
profile = container_registry.findContainers(id = item["id"])
# When for some reason there is no profile container in the registry
if not profile:
self._setItemLayerHeight(item, "", "")
item["available"] = False
yield item
continue
profile = profile[0]
# When there is a profile but it's an empty quality should. It's shown in the list (they are "Not Supported" profiles)
if profile.getId() == "empty_quality":
self._setItemLayerHeight(item, "", "")
item["available"] = True
yield item
continue
item["available"] = profile in qualities
# Easy case: This profile defines its own layer height.
if profile.hasProperty("layer_height", "value"):
self._setItemLayerHeight(item, profile.getProperty("layer_height", "value"), unit)
yield item
continue
machine_manager = Application.getInstance().getMachineManager()
# Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
quality_type = profile.getMetaDataEntry("quality_type", None)
if quality_type:
quality_results = machine_manager.determineQualityAndQualityChangesForQualityType(quality_type)
for quality_result in quality_results:
if quality_result["stack"] is global_container_stack:
quality = quality_result["quality"]
break
else:
# No global container stack in the results:
if quality_results:
# Take any of the extruders.
quality = quality_results[0]["quality"]
else:
quality = None
if quality and quality.hasProperty("layer_height", "value"):
self._setItemLayerHeight(item, quality.getProperty("layer_height", "value"), unit)
yield item
continue
# Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
skip_until_container = global_container_stack.material
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No material in stack.
skip_until_container = global_container_stack.variant
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No variant in stack.
skip_until_container = global_container_stack.getBottom()
self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material.
yield item
## Get a list of extruder stacks with the active extruder at the front of the list.
@staticmethod
def _getOrderedExtruderStacksList() -> List["ExtruderStack"]:
extruder_manager = ExtruderManager.getInstance()
extruder_stacks = extruder_manager.getActiveExtruderStacks()
active_extruder = extruder_manager.getActiveExtruderStack()
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
extruder_stacks = [active_extruder] + extruder_stacks
return extruder_stacks
@staticmethod
def _setItemLayerHeight(item, value, unit):
item["layer_height"] = str(value) + unit
item["layer_height_without_unit"] = str(value)

View file

@ -1,46 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from cura.QualityManager import QualityManager
from cura.Settings.ProfilesModel import ProfilesModel
from cura.Settings.ExtruderManager import ExtruderManager
## QML Model for listing the current list of valid quality and quality changes profiles.
#
class QualityAndUserProfilesModel(ProfilesModel):
def __init__(self, parent = None):
super().__init__(parent)
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
return {}, {}
# Fetch the list of quality changes.
quality_manager = QualityManager.getInstance()
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = self._getOrderedExtruderStacksList()
# Fetch the list of usable qualities across all extruders.
# The actual list of quality profiles come from the first extruder in the extruder list.
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
result = filtered_quality_changes
result.update({q.getId():q for q in quality_list})
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.

View file

@ -1,240 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Logger import Logger
import UM.Qt
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
import os
from UM.i18n import i18nCatalog
class QualitySettingsModel(UM.Qt.ListModel.ListModel):
KeyRole = Qt.UserRole + 1
LabelRole = Qt.UserRole + 2
UnitRole = Qt.UserRole + 3
ProfileValueRole = Qt.UserRole + 4
ProfileValueSourceRole = Qt.UserRole + 5
UserValueRole = Qt.UserRole + 6
CategoryRole = Qt.UserRole + 7
def __init__(self, parent = None):
super().__init__(parent = parent)
self._container_registry = ContainerRegistry.getInstance()
self._extruder_id = None
self._extruder_definition_id = None
self._quality_id = None
self._material_id = None
self._i18n_catalog = None
self.addRoleName(self.KeyRole, "key")
self.addRoleName(self.LabelRole, "label")
self.addRoleName(self.UnitRole, "unit")
self.addRoleName(self.ProfileValueRole, "profile_value")
self.addRoleName(self.ProfileValueSourceRole, "profile_value_source")
self.addRoleName(self.UserValueRole, "user_value")
self.addRoleName(self.CategoryRole, "category")
def setExtruderId(self, extruder_id):
if extruder_id != self._extruder_id:
self._extruder_id = extruder_id
self._update()
self.extruderIdChanged.emit()
extruderIdChanged = pyqtSignal()
@pyqtProperty(str, fset = setExtruderId, notify = extruderIdChanged)
def extruderId(self):
return self._extruder_id
def setExtruderDefinition(self, extruder_definition):
if extruder_definition != self._extruder_definition_id:
self._extruder_definition_id = extruder_definition
self._update()
self.extruderDefinitionChanged.emit()
extruderDefinitionChanged = pyqtSignal()
@pyqtProperty(str, fset = setExtruderDefinition, notify = extruderDefinitionChanged)
def extruderDefinition(self):
return self._extruder_definition_id
def setQuality(self, quality):
if quality != self._quality_id:
self._quality_id = quality
self._update()
self.qualityChanged.emit()
qualityChanged = pyqtSignal()
@pyqtProperty(str, fset = setQuality, notify = qualityChanged)
def quality(self):
return self._quality_id
def setMaterial(self, material):
if material != self._material_id:
self._material_id = material
self._update()
self.materialChanged.emit()
materialChanged = pyqtSignal()
@pyqtProperty(str, fset = setMaterial, notify = materialChanged)
def material(self):
return self._material_id
def _update(self):
if not self._quality_id:
return
items = []
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
if not containers:
Logger.log("w", "Could not find a quality container with id %s", self._quality_id)
return
quality_container = None
quality_changes_container = None
if containers[0].getMetaDataEntry("type") == "quality":
quality_container = containers[0]
else:
quality_changes_container = containers[0]
criteria = {
"type": "quality",
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
"definition": quality_changes_container.getDefinition().getId()
}
quality_container = self._container_registry.findInstanceContainers(**criteria)
if not quality_container:
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
return
quality_container = quality_container[0]
quality_type = quality_container.getMetaDataEntry("quality_type")
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
definition = quality_container.getDefinition()
# Check if the definition container has a translation file.
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
if catalog.hasTranslationLoaded():
self._i18n_catalog = catalog
for file_name in quality_container.getDefinition().getInheritedFiles():
catalog = i18nCatalog(os.path.basename(file_name))
if catalog.hasTranslationLoaded():
self._i18n_catalog = catalog
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
if self._material_id and self._material_id != "empty_material":
criteria["material"] = self._material_id
criteria["extruder"] = self._extruder_id
containers = self._container_registry.findInstanceContainers(**criteria)
if not containers:
# Try again, this time without extruder
new_criteria = criteria.copy()
new_criteria.pop("extruder")
containers = self._container_registry.findInstanceContainers(**new_criteria)
if not containers and "material" in criteria:
# Try again, this time without material
criteria.pop("material", None)
containers = self._container_registry.findInstanceContainers(**criteria)
if not containers:
# Try again, this time without material or extruder
criteria.pop("extruder") # "material" has already been popped
containers = self._container_registry.findInstanceContainers(**criteria)
if not containers:
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
return
if quality_changes_container:
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
if self._extruder_definition_id != "":
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
if extruder_definitions:
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
criteria["name"] = quality_changes_container.getName()
else:
criteria["extruder"] = None
changes = self._container_registry.findInstanceContainers(**criteria)
if changes:
containers.extend(changes)
global_container_stack = Application.getInstance().getGlobalContainerStack()
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
current_category = ""
for definition in definition_container.findDefinitions():
if definition.type == "category":
current_category = definition.label
if self._i18n_catalog:
current_category = self._i18n_catalog.i18nc(definition.key + " label", definition.label)
continue
profile_value = None
profile_value_source = ""
for container in containers:
new_value = container.getProperty(definition.key, "value")
if new_value is not None:
profile_value_source = container.getMetaDataEntry("type")
profile_value = new_value
# Global tab should use resolve (if there is one)
if not self._extruder_id:
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
if resolve_value is not None and profile_value is not None and profile_value_source != "quality_changes":
profile_value = resolve_value
user_value = None
if not self._extruder_id:
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
else:
extruder_stack = self._container_registry.findContainerStacks(id = self._extruder_id)
if extruder_stack:
user_value = extruder_stack[0].getTop().getProperty(definition.key, "value")
if profile_value is None and user_value is None:
continue
if is_multi_extrusion:
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
if self._extruder_id != "" and not settable_per_extruder:
continue
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
if self._extruder_id == "" and settable_per_extruder:
continue
label = definition.label
if self._i18n_catalog:
label = self._i18n_catalog.i18nc(definition.key + " label", label)
items.append({
"key": definition.key,
"label": label,
"unit": definition.unit,
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
"profile_value_source": profile_value_source,
"user_value": "" if user_value is None else str(user_value),
"category": current_category
})
self.setItems(items)

View file

@ -3,12 +3,14 @@
import copy import copy
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.Validator import ValidatorState
from PyQt5.QtCore import QTimer
from UM.Application import Application from UM.Application import Application
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
@ -32,14 +34,20 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._stack = PerObjectContainerStack(stack_id = id(self)) self._stack = PerObjectContainerStack(stack_id = "per_object_stack_" + str(id(self)))
self._stack.setDirty(False) # This stack does not need to be saved. self._stack.setDirty(False) # This stack does not need to be saved.
self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer")) self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer"))
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
self._is_non_printing_mesh = False
self._error_check_timer = QTimer()
self._error_check_timer.setInterval(250)
self._error_check_timer.setSingleShot(True)
self._error_check_timer.timeout.connect(self._checkStackForErrors)
self._stack.propertyChanged.connect(self._onSettingChanged) self._stack.propertyChanged.connect(self._onSettingChanged)
ContainerRegistry.getInstance().addContainer(self._stack) Application.getInstance().getContainerRegistry().addContainer(self._stack)
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack) Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
self.activeExtruderChanged.connect(self._updateNextStack) self.activeExtruderChanged.connect(self._updateNextStack)
@ -57,6 +65,10 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# Properly set the right extruder on the copy # Properly set the right extruder on the copy
deep_copy.setActiveExtruder(self._extruder_stack) deep_copy.setActiveExtruder(self._extruder_stack)
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet.
deep_copy._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
return deep_copy return deep_copy
## Gets the currently active extruder to print this object with. ## Gets the currently active extruder to print this object with.
@ -80,13 +92,28 @@ class SettingOverrideDecorator(SceneNodeDecorator):
container_stack = containers[0] container_stack = containers[0]
return container_stack.getMetaDataEntry("position", default=None) return container_stack.getMetaDataEntry("position", default=None)
def isNonPrintingMesh(self):
return self._is_non_printing_mesh
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
# Trigger slice/need slicing if the value has changed. # Trigger slice/need slicing if the value has changed.
if property_name == "value": if property_name == "value":
Application.getInstance().getBackend().needsSlicing() self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
Application.getInstance().getBackend().tickle() if not self._is_non_printing_mesh:
# self._error_check_timer.start()
self._checkStackForErrors()
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
self._node._non_printing_mesh = any(self._stack.getProperty(setting, "value") for setting in self._non_printing_mesh_settings) def _checkStackForErrors(self):
hasErrors = False;
for key in self._stack.getAllKeys():
validation_state = self._stack.getProperty(key, "validationState")
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
Logger.log("w", "Setting Per Object %s is not valid.", key)
hasErrors = True
break
Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors)
## Makes sure that the stack upon which the container stack is placed is ## Makes sure that the stack upon which the container stack is placed is
# kept up to date. # kept up to date.

View file

@ -1,80 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from cura.QualityManager import QualityManager
from cura.Settings.ProfilesModel import ProfilesModel
from cura.Settings.ExtruderManager import ExtruderManager
## QML Model for listing the current list of valid quality changes profiles.
#
class UserProfilesModel(ProfilesModel):
def __init__(self, parent = None):
super().__init__(parent)
#Need to connect to the metaDataChanged signal of the active materials.
self.__current_extruders = []
self.__current_materials = []
Application.getInstance().getExtruderManager().extrudersChanged.connect(self.__onExtrudersChanged)
self.__onExtrudersChanged()
self.__current_materials = [extruder.material for extruder in self.__current_extruders]
for material in self.__current_materials:
material.metaDataChanged.connect(self._onContainerChanged)
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
return {}, {}
# Fetch the list of quality changes.
quality_manager = QualityManager.getInstance()
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = self._getOrderedExtruderStacksList()
# Fetch the list of usable qualities across all extruders.
# The actual list of quality profiles come from the first extruder in the extruder list.
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
return filtered_quality_changes, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
## Called when a container changed on an extruder stack.
#
# If it's the material we need to connect to the metaDataChanged signal of
# that.
def __onContainerChanged(self, new_container):
#Careful not to update when a quality or quality changes profile changed!
#If you then update you're going to have an infinite recursion because the update may change the container.
if new_container.getMetaDataEntry("type") == "material":
for material in self.__current_materials:
material.metaDataChanged.disconnect(self._onContainerChanged)
self.__current_materials = [extruder.material for extruder in self.__current_extruders]
for material in self.__current_materials:
material.metaDataChanged.connect(self._onContainerChanged)
## Called when the current set of extruders change.
#
# This makes sure that we are listening to the signal for when the
# materials change.
def __onExtrudersChanged(self):
for extruder in self.__current_extruders:
extruder.containersChanged.disconnect(self.__onContainerChanged)
self.__current_extruders = Application.getInstance().getExtruderManager().getExtruderStacks()
for extruder in self.__current_extruders:
extruder.containersChanged.connect(self.__onContainerChanged)

116
cura/Snapshot.py Normal file
View file

@ -0,0 +1,116 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy
from PyQt5 import QtCore
from PyQt5.QtGui import QImage
from cura.PreviewPass import PreviewPass
from cura.Scene import ConvexHullNode
from UM.Application import Application
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshData import transformVertices
from UM.Scene.Camera import Camera
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class Snapshot:
@staticmethod
def getImageBoundaries(image: QImage):
# Look at the resulting image to get a good crop.
# Get the pixels as byte array
pixel_array = image.bits().asarray(image.byteCount())
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)
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1)
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1)
return min_x, max_x, min_y, max_y
## Return a QImage of the scene
# Uses PreviewPass that leaves out some elements
# Aspect ratio assumes a square
@staticmethod
def snapshot(width = 300, height = 300):
scene = Application.getInstance().getController().getScene()
active_camera = scene.getActiveCamera()
render_width, render_height = active_camera.getWindowSize()
render_width = int(render_width)
render_height = int(render_height)
preview_pass = PreviewPass(render_width, render_height)
root = scene.getRoot()
camera = Camera("snapshot", root)
# determine zoom and look at
bbox = None
for node in DepthFirstIterator(root):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
if bbox is None:
bbox = node.getBoundingBox()
else:
bbox = bbox + node.getBoundingBox()
# If there is no bounding box, it means that there is no model in the buildplate
if bbox is None:
return None
look_at = bbox.center
# guessed size so the objects are hopefully big
size = max(bbox.width, bbox.height, bbox.depth * 0.5)
# Looking from this direction (x, y, z) in OGL coordinates
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.3
camera.setPosition(look_at + looking_from_offset)
camera.lookAt(look_at)
satisfied = False
size = None
fovy = 30
while not satisfied:
if size is not None:
satisfied = True # always be satisfied after second try
projection_matrix = Matrix()
# Somehow the aspect ratio is also influenced in reverse by the screen width/height
# So you have to set it to render_width/render_height to get 1
projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500)
camera.setProjectionMatrix(projection_matrix)
preview_pass.setCamera(camera)
preview_pass.render()
pixel_output = preview_pass.getOutput()
min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
if size > 0.5 or satisfied:
satisfied = True
else:
# make it big and allow for some empty space around
fovy *= 0.5 # strangely enough this messes up the aspect ratio: fovy *= size * 1.1
# make it a square
if max_x - min_x >= max_y - min_y:
# make y bigger
min_y, max_y = int((max_y + min_y) / 2 - (max_x - min_x) / 2), int((max_y + min_y) / 2 + (max_x - min_x) / 2)
else:
# make x bigger
min_x, max_x = int((max_x + min_x) / 2 - (max_y - min_y) / 2), int((max_x + min_x) / 2 + (max_y - min_y) / 2)
cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y)
# Scale it to the correct size
scaled_image = cropped_image.scaled(
width, height,
aspectRatioMode = QtCore.Qt.IgnoreAspectRatio,
transformMode = QtCore.Qt.SmoothTransformation)
return scaled_image

View file

@ -16,6 +16,12 @@ parser.add_argument('--debug',
default = False, default = False,
help = "Turn on the debug mode by setting this option." help = "Turn on the debug mode by setting this option."
) )
parser.add_argument('--trigger-early-crash',
dest = 'trigger_early_crash',
action = 'store_true',
default = False,
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
)
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]: if not known_args["debug"]:
@ -26,12 +32,12 @@ if not known_args["debug"]:
return os.path.expanduser("~/.local/share/cura") return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX(): elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura") return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"): if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path() dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True) os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w") sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w") sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8")
import platform import platform
import faulthandler import faulthandler
@ -40,11 +46,11 @@ import faulthandler
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
linux_distro_name = platform.linux_distribution()[0].lower() linux_distro_name = platform.linux_distribution()[0].lower()
if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix. # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes import ctypes
from ctypes.util import find_library from ctypes.util import find_library
libGL = find_library("GL") libGL = find_library("GL")
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL) ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
# When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs. # When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs.
if Platform.isWindows() and hasattr(sys, "frozen"): if Platform.isWindows() and hasattr(sys, "frozen"):
@ -71,8 +77,45 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u
def exceptHook(hook_type, value, traceback): def exceptHook(hook_type, value, traceback):
from cura.CrashHandler import CrashHandler from cura.CrashHandler import CrashHandler
_crash_handler = CrashHandler(hook_type, value, traceback) from cura.CuraApplication import CuraApplication
_crash_handler.show() has_started = False
if CuraApplication.Created:
has_started = CuraApplication.getInstance().started
#
# When the exception hook is triggered, the QApplication may not have been initialized yet. In this case, we don't
# have an QApplication to handle the event loop, which is required by the Crash Dialog.
# The flag "CuraApplication.Created" is set to True when CuraApplication finishes its constructor call.
#
# Before the "started" flag is set to True, the Qt event loop has not started yet. The event loop is a blocking
# call to the QApplication.exec_(). In this case, we need to:
# 1. Remove all scheduled events so no more unnecessary events will be processed, such as loading the main dialog,
# loading the machine, etc.
# 2. Start the Qt event loop with exec_() and show the Crash Dialog.
#
# If the application has finished its initialization and was running fine, and then something causes a crash,
# we run the old routine to show the Crash Dialog.
#
from PyQt5.Qt import QApplication
if CuraApplication.Created:
_crash_handler = CrashHandler(hook_type, value, traceback, has_started)
if CuraApplication.splash is not None:
CuraApplication.splash.close()
if not has_started:
CuraApplication.getInstance().removePostedEvents(None)
_crash_handler.early_crash_dialog.show()
sys.exit(CuraApplication.getInstance().exec_())
else:
_crash_handler.show()
else:
application = QApplication(sys.argv)
application.removePostedEvents(None)
_crash_handler = CrashHandler(hook_type, value, traceback, has_started)
# This means the QtApplication could be created and so the splash screen. Then Cura closes it
if CuraApplication.splash is not None:
CuraApplication.splash.close()
_crash_handler.early_crash_dialog.show()
sys.exit(application.exec_())
if not known_args["debug"]: if not known_args["debug"]:
sys.excepthook = exceptHook sys.excepthook = exceptHook

View file

@ -1,29 +1,31 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os.path import os.path
import zipfile import zipfile
from UM.Job import Job import numpy
import Savitar
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshReader import MeshReader
from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.GroupDecorator import GroupDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.QualityManager import QualityManager from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.Scene.SceneNode import SceneNode from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
MYPY = False MYPY = False
import Savitar
import numpy
try: try:
if not MYPY: if not MYPY:
import xml.etree.cElementTree as ET import xml.etree.cElementTree as ET
@ -37,12 +39,9 @@ class ThreeMFReader(MeshReader):
super().__init__() super().__init__()
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._root = None self._root = None
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._base_name = "" self._base_name = ""
self._unit = None self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
def _createMatrixFromTransformationString(self, transformation): def _createMatrixFromTransformationString(self, transformation):
if transformation == "": if transformation == "":
@ -77,7 +76,14 @@ class ThreeMFReader(MeshReader):
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node. ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
# \returns Uranium scene node. # \returns Uranium scene node.
def _convertSavitarNodeToUMNode(self, savitar_node): def _convertSavitarNodeToUMNode(self, savitar_node):
um_node = SceneNode() self._object_count += 1
node_name = "Object %s" % self._object_count
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode()
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation) um_node.setTransformation(transformation)
mesh_builder = MeshBuilder() mesh_builder = MeshBuilder()
@ -116,8 +122,8 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId()) um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it # Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack)
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId()) um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop() setting_container = um_node.callDecoration("getStack").getTop()
@ -147,6 +153,7 @@ class ThreeMFReader(MeshReader):
def read(self, file_name): def read(self, file_name):
result = [] result = []
self._object_count = 0 # Used to name objects as there is no node name yet.
# The base object of 3mf is a zipped archive. # The base object of 3mf is a zipped archive.
try: try:
archive = zipfile.ZipFile(file_name, "r") archive = zipfile.ZipFile(file_name, "r")

View file

@ -23,17 +23,49 @@ from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerStack import _ContainerIndexes from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.QualityManager import QualityManager from cura.CuraApplication import CuraApplication
from configparser import ConfigParser from configparser import ConfigParser
import zipfile import zipfile
import io import io
import configparser import configparser
import os import os
import threading
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
#
# HACK:
#
# In project loading, when override the existing machine is selected, the stacks and containers that are correctly
# active in the system will be overridden at runtime. Because the project loading is done in a different thread than
# the Qt thread, something else can kick in the middle of the process. One of them is the rendering. It will access
# the current stacks and container, which have not completely been updated yet, so Cura will crash in this case.
#
# This "@call_on_qt_thread" decorator makes sure that a function will always be called on the Qt thread (blocking).
# It is applied to the read() function of project loading so it can be guaranteed that only after the project loading
# process is completely done, everything else that needs to occupy the QT thread will be executed.
#
class InterCallObject:
def __init__(self):
self.finish_event = threading.Event()
self.result = None
def call_on_qt_thread(func):
def _call_on_qt_thread_wrapper(*args, **kwargs):
def _handle_call(ico, *args, **kwargs):
ico.result = func(*args, **kwargs)
ico.finish_event.set()
inter_call_object = InterCallObject()
new_args = tuple([inter_call_object] + list(args)[:])
CuraApplication.getInstance().callLater(_handle_call, *new_args, **kwargs)
inter_call_object.finish_event.wait()
return inter_call_object.result
return _call_on_qt_thread_wrapper
## Base implementation for reading 3MF workspace files. ## Base implementation for reading 3MF workspace files.
class ThreeMFWorkspaceReader(WorkspaceReader): class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self): def __init__(self):
@ -89,7 +121,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The default ContainerStack.deserialize() will connect signals, which is not desired in this case. # The default ContainerStack.deserialize() will connect signals, which is not desired in this case.
# Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them. # Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them.
serialized = archive.open(file_name).read().decode("utf-8") serialized = archive.open(file_name).read().decode("utf-8")
stack_config = ConfigParser() stack_config = ConfigParser(interpolation = None)
stack_config.read_string(serialized) stack_config.read_string(serialized)
# sanity check # sanity check
@ -168,11 +200,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Unknown definition container type %s for %s", Logger.log("w", "Unknown definition container type %s for %s",
definition_container_type, each_definition_container_file) definition_container_type, each_definition_container_file)
Job.yieldThread() Job.yieldThread()
# sanity check
if machine_definition_container_count != 1: if machine_definition_container_count != 1:
msg = "Expecting one machine definition container but got %s" % machine_definition_container_count return WorkspaceReader.PreReadResult.failed #Not a workspace file but ordinary 3MF.
Logger.log("e", msg)
raise RuntimeError(msg)
material_labels = [] material_labels = []
material_conflict = False material_conflict = False
@ -271,9 +301,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# if the global stack is found, we check if there are conflicts in the extruder stacks # if the global stack is found, we check if there are conflicts in the extruder stacks
if containers_found_dict["machine"] and not machine_conflict: if containers_found_dict["machine"] and not machine_conflict:
for extruder_stack_file in extruder_stack_files: for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file)
serialized = archive.open(extruder_stack_file).read().decode("utf-8") serialized = archive.open(extruder_stack_file).read().decode("utf-8")
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
# The check should be done for the extruder stack that's associated with the existing global stack, # The check should be done for the extruder stack that's associated with the existing global stack,
@ -303,7 +332,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
break break
num_visible_settings = 0 num_visible_settings = 0
has_visible_settings_string = False
try: try:
temp_preferences = Preferences() temp_preferences = Preferences()
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8") serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
@ -378,7 +406,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack. ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file): def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first # Get extruder position first
extruder_config = configparser.ConfigParser() extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content) extruder_config.read_string(extruder_file_content)
if not extruder_config.has_option("metadata", "position"): if not extruder_config.has_option("metadata", "position"):
msg = "Could not find 'metadata/position' in extruder stack file" msg = "Could not find 'metadata/position' in extruder stack file"
@ -404,6 +432,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# containing global.cfg / extruder.cfg # containing global.cfg / extruder.cfg
# #
# \param file_name # \param file_name
@call_on_qt_thread
def read(self, file_name): def read(self, file_name):
archive = zipfile.ZipFile(file_name, "r") archive = zipfile.ZipFile(file_name, "r")
@ -423,6 +452,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged") Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
else: else:
global_preferences.setValue("general/visible_settings", visible_settings) global_preferences.setValue("general/visible_settings", visible_settings)
global_preferences.setValue("general/preset_setting_visibility_choice", "Custom")
categories_expanded = temp_preferences.getValue("cura/categories_expanded") categories_expanded = temp_preferences.getValue("cura/categories_expanded")
if categories_expanded is None: if categories_expanded is None:
@ -461,7 +491,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack_id_new = self.getNewId(global_stack_id_original) global_stack_id_new = self.getNewId(global_stack_id_original)
global_stack_need_rename = True global_stack_need_rename = True
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original) if self._container_registry.findContainerStacksMetadata(name = global_stack_id_original):
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original)
for each_extruder_stack_file in extruder_stack_files: for each_extruder_stack_file in extruder_stack_files:
old_container_id = self._stripFileToId(each_extruder_stack_file) old_container_id = self._stripFileToId(each_extruder_stack_file)
@ -527,12 +558,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
user_instance_containers = [] user_instance_containers = []
quality_and_definition_changes_instance_containers = [] quality_and_definition_changes_instance_containers = []
quality_changes_instance_containers = []
for instance_container_file in instance_container_files: for instance_container_file in instance_container_files:
container_id = self._stripFileToId(instance_container_file) container_id = self._stripFileToId(instance_container_file)
serialized = archive.open(instance_container_file).read().decode("utf-8") serialized = archive.open(instance_container_file).read().decode("utf-8")
# HACK! we ignore "quality" and "variant" instance containers! # HACK! we ignore "quality" and "variant" instance containers!
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
if not parser.has_option("metadata", "type"): if not parser.has_option("metadata", "type"):
Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file) Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file)
@ -583,7 +615,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_id: if machine_id:
new_machine_id = self.getNewId(machine_id) new_machine_id = self.getNewId(machine_id)
new_id = new_machine_id + "_current_settings" new_id = new_machine_id + "_current_settings"
instance_container.setMetadataEntry("id", new_id) instance_container.setMetaDataEntry("id", new_id)
instance_container.setName(new_id) instance_container.setName(new_id)
instance_container.setMetaDataEntry("machine", new_machine_id) instance_container.setMetaDataEntry("machine", new_machine_id)
containers_to_add.append(instance_container) containers_to_add.append(instance_container)
@ -608,7 +640,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container.setName(self._container_registry.uniqueName(instance_container.getName())) instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
new_changes_container_id = self.getNewId(instance_container.getId()) new_changes_container_id = self.getNewId(instance_container.getId())
instance_container._id = new_changes_container_id instance_container.setMetaDataEntry("id", new_changes_container_id)
# TODO: we don't know the following is correct or not, need to verify # TODO: we don't know the following is correct or not, need to verify
# AND REFACTOR!!! # AND REFACTOR!!!
@ -632,6 +664,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The ID already exists, but nothing in the values changed, so do nothing. # The ID already exists, but nothing in the values changed, so do nothing.
pass pass
quality_and_definition_changes_instance_containers.append(instance_container) quality_and_definition_changes_instance_containers.append(instance_container)
if container_type == "quality_changes":
quality_changes_instance_containers.append(instance_container)
if container_type == "definition_changes": if container_type == "definition_changes":
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value") definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
@ -681,18 +715,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
file_name = global_stack_file) file_name = global_stack_file)
# Ensure a unique ID and name # Ensure a unique ID and name
stack._id = global_stack_id_new stack.setMetaDataEntry("id", global_stack_id_new)
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
# bound machine also needs to change.
if stack.getMetaDataEntry("machine", None):
stack.setMetaDataEntry("machine", global_stack_id_new)
# Only machines need a new name, stacks may be non-unique # Only machines need a new name, stacks may be non-unique
stack.setName(global_stack_name_new) stack.setName(global_stack_name_new)
container_stacks_added.append(stack) container_stacks_added.append(stack)
self._container_registry.addContainer(stack) # self._container_registry.addContainer(stack)
containers_added.append(stack) containers_added.append(stack)
else: else:
Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
@ -710,6 +739,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return return
# load extruder stack files # load extruder stack files
has_extruder_stack_files = len(extruder_stack_files) > 0
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
try: try:
for extruder_stack_file in extruder_stack_files: for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file) container_id = self._stripFileToId(extruder_stack_file)
@ -730,7 +762,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize() # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
# also does addExtruder() to its machine stack, so we have to make sure that it's pointing # also does addExtruder() to its machine stack, so we have to make sure that it's pointing
# to the right machine BEFORE deserialization. # to the right machine BEFORE deserialization.
extruder_config = configparser.ConfigParser() extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content) extruder_config.read_string(extruder_file_content)
extruder_config.set("metadata", "machine", global_stack_id_new) extruder_config.set("metadata", "machine", global_stack_id_new)
tmp_string_io = io.StringIO() tmp_string_io = io.StringIO()
@ -740,7 +772,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
stack.deserialize(extruder_file_content, file_name = extruder_stack_file) stack.deserialize(extruder_file_content, file_name = extruder_stack_file)
# Ensure a unique ID and name # Ensure a unique ID and name
stack._id = new_id stack.setMetaDataEntry("id", new_id)
self._container_registry.addContainer(stack) self._container_registry.addContainer(stack)
extruder_stacks_added.append(stack) extruder_stacks_added.append(stack)
@ -758,17 +790,33 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# If not extruder stacks were saved in the project file (pre 3.1) create one manually # If not extruder stacks were saved in the project file (pre 3.1) create one manually
# We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this # We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this
if not extruder_stacks: if not extruder_stacks:
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder") # If we choose to override a machine but to create a new custom quality profile, the custom quality
if stack: # profile is not immediately applied to the global_stack, so this fix for single extrusion machines
if self._resolve_strategies["machine"] == "override": # will use the current custom quality profile on the existing machine. The extra optional argument
# in case the extruder is newly created (for a single-extrusion machine), we need to override # in that function is used in this case to specify a new global stack quality_changes container so
# the existing extruder stack. # the fix can correctly create and copy over the custom quality settings to the newly created extruder.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")] new_global_quality_changes = None
for idx in range(len(_ContainerIndexes.IndexTypeMap)): if self._resolve_strategies["quality_changes"] == "new" and len(quality_changes_instance_containers) > 0:
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True) new_global_quality_changes = quality_changes_instance_containers[0]
extruder_stacks.append(existing_extruder_stack)
else: # Depending if the strategy is to create a new or override, the ids must be or not be unique
extruder_stacks.append(stack) stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder",
new_global_quality_changes,
create_new_ids = self._resolve_strategies["machine"] == "new")
if new_global_quality_changes is not None:
quality_changes_instance_containers.append(stack.qualityChanges)
quality_and_definition_changes_instance_containers.append(stack.qualityChanges)
if global_stack.quality.getId() in ("empty", "empty_quality"):
stack.quality = empty_quality_container
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
extruder_stacks.append(existing_extruder_stack)
else:
extruder_stacks.append(stack)
except: except:
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.") Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
@ -777,6 +825,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId()) self._container_registry.removeContainer(container.getId())
return return
## In case there is a new machine and once the extruders are created, the global stack is added to the registry,
# otherwise the addContainers function in CuraContainerRegistry will create an extruder stack and then creating
# useless files
if self._resolve_strategies["machine"] == "new":
self._container_registry.addContainer(global_stack)
# Check quality profiles to make sure that if one stack has the "not supported" quality profile, # Check quality profiles to make sure that if one stack has the "not supported" quality profile,
# all others should have the same. # all others should have the same.
@ -801,30 +854,24 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_extruder_count is not None: if machine_extruder_count is not None:
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count] extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack, quality_manager = CuraApplication.getInstance()._quality_manager
extruder_stacks_in_use) all_quality_groups = quality_manager.getQualityGroups(global_stack)
available_quality_types = [qt for qt, qg in all_quality_groups.items() if qg.is_available]
if not has_not_supported: if not has_not_supported:
has_not_supported = not available_quality has_not_supported = not available_quality_types
quality_has_been_changed = False quality_has_been_changed = False
if has_not_supported: if has_not_supported:
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
for stack in [global_stack] + extruder_stacks_in_use: for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container) stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container) stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True quality_has_been_changed = True
else: else:
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
# The machine in the project has non-empty quality and there are usable qualities for this machine. # The machine in the project has non-empty quality and there are usable qualities for this machine.
# We need to check if the current quality_type is still usable for this machine, if not, then the quality # We need to check if the current quality_type is still usable for this machine, if not, then the quality
# will be reset to the "preferred quality" if present, otherwise "normal". # will be reset to the "preferred quality" if present, otherwise "normal".
available_quality_types = [q.getMetaDataEntry("quality_type") for q in available_quality]
if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types: if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types:
# We are here because the quality_type specified in the project is not supported any more, # We are here because the quality_type specified in the project is not supported any more,
# so we need to switch it to the "preferred quality" if present, otherwise "normal". # so we need to switch it to the "preferred quality" if present, otherwise "normal".
@ -888,6 +935,34 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.quality = new_quality_container extruder_stack.quality = new_quality_container
global_stack.quality = new_quality_container global_stack.quality = new_quality_container
# Now we are checking if the quality in the extruder stacks is the same as in the global. In other case,
# the quality is set to be the same.
definition_id = global_stack.definition.getId()
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
definition_id = "fdmprinter"
for extruder_stack in extruder_stacks_in_use:
# If the quality is different in the stacks, then the quality in the global stack is trusted
if extruder_stack.quality.getMetaDataEntry("quality_type") != global_stack.quality.getMetaDataEntry("quality_type"):
search_criteria = {"id": global_stack.quality.getId(),
"type": "quality",
"definition": definition_id}
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
extruder_stack.quality = containers[0]
extruder_stack.qualityChanges = empty_quality_changes_container
else:
Logger.log("e", "Cannot find a suitable quality for extruder [%s].", extruder_stack.getId())
quality_has_been_changed = True
else:
Logger.log("i", "The quality is the same for the global and the extruder stack [%s]", global_stack.quality.getId())
# Replacing the old containers if resolve is "new". # Replacing the old containers if resolve is "new".
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those # When resolve is "new", some containers will get renamed, so all the other containers that reference to those
# MUST get updated too. # MUST get updated too.
@ -930,9 +1005,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# sanity checks # sanity checks
# NOTE: The following cases SHOULD NOT happen!!!! # NOTE: The following cases SHOULD NOT happen!!!!
if not old_container: if old_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!", Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!",
changes_container_type, global_stack.getId()) changes_container_type, global_stack.getId())
continue
# Replace the quality/definition changes container if it's in the GlobalStack # Replace the quality/definition changes container if it's in the GlobalStack
# NOTE: we can get an empty container here, but the IDs will not match, # NOTE: we can get an empty container here, but the IDs will not match,
@ -945,26 +1021,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
continue continue
# Replace the quality/definition changes container if it's in one of the ExtruderStacks # Replace the quality/definition changes container if it's in one of the ExtruderStacks
for each_extruder_stack in extruder_stacks: # Only apply the change if we have loaded extruder stacks from the project
changes_container = None if has_extruder_stack_files:
if changes_container_type == "quality_changes": for each_extruder_stack in extruder_stacks:
changes_container = each_extruder_stack.qualityChanges changes_container = None
elif changes_container_type == "definition_changes":
changes_container = each_extruder_stack.definitionChanges
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if not changes_container:
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
changes_container_type, each_extruder_stack.getId())
# NOTE: we can get an empty container here, but the IDs will not match,
# so this comparison is fine.
if self._id_mapping.get(changes_container.getId()) == new_id:
if changes_container_type == "quality_changes": if changes_container_type == "quality_changes":
each_extruder_stack.qualityChanges = each_changes_container changes_container = each_extruder_stack.qualityChanges
elif changes_container_type == "definition_changes": elif changes_container_type == "definition_changes":
each_extruder_stack.definitionChanges = each_changes_container changes_container = each_extruder_stack.definitionChanges
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if changes_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
changes_container_type, each_extruder_stack.getId())
continue
# NOTE: we can get an empty container here, but the IDs will not match,
# so this comparison is fine.
if self._id_mapping.get(changes_container.getId()) == new_id:
if changes_container_type == "quality_changes":
each_extruder_stack.qualityChanges = each_changes_container
elif changes_container_type == "definition_changes":
each_extruder_stack.definitionChanges = each_changes_container
if self._resolve_strategies["material"] == "new": if self._resolve_strategies["material"] == "new":
# the actual material instance container can have an ID such as # the actual material instance container can have an ID such as
@ -999,12 +1078,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for stack in extruder_stacks: for stack in extruder_stacks:
stack.setNextStack(global_stack) stack.setNextStack(global_stack)
stack.containersChanged.emit(stack.getTop()) stack.containersChanged.emit(stack.getTop())
else:
CuraApplication.getInstance().getMachineManager().activeQualityChanged.emit()
# Actually change the active machine. # Actually change the active machine.
Application.getInstance().setGlobalContainerStack(global_stack) #
# This is scheduled for later is because it depends on the Variant/Material/Qualitiy Managers to have the latest
# Notify everything/one that is to notify about changes. # data, but those managers will only update upon a container/container metadata changed signal. Because this
global_stack.containersChanged.emit(global_stack.getTop()) # function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
# they won't take effect until this function is done.
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
CuraApplication.getInstance().callLater(self._updateActiveMachine, global_stack)
# Load all the nodes / meshdata of the workspace # Load all the nodes / meshdata of the workspace
nodes = self._3mf_mesh_reader.read(file_name) nodes = self._3mf_mesh_reader.read(file_name)
@ -1017,6 +1101,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
def _updateActiveMachine(self, global_stack):
# Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager()
machine_manager.setActiveMachine(global_stack.getId())
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())
## HACK: Replaces the material container in the given stack with a newly created material container. ## HACK: Replaces the material container in the given stack with a newly created material container.
# This function is used when the user chooses to resolve material conflicts by creating new ones. # This function is used when the user chooses to resolve material conflicts by creating new ones.
def _replaceStackMaterialWithNew(self, stack, old_new_material_dict): def _replaceStackMaterialWithNew(self, stack, old_new_material_dict):
@ -1048,13 +1140,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# find the old material ID # find the old material ID
old_material_id_in_stack = stack.material.getId() old_material_id_in_stack = stack.material.getId()
best_matching_old_material_id = None best_matching_old_material_id = None
best_matching_old_meterial_prefix_length = -1 best_matching_old_material_prefix_length = -1
for old_parent_material_id in old_new_material_dict: for old_parent_material_id in old_new_material_dict:
if len(old_parent_material_id) < best_matching_old_meterial_prefix_length: if len(old_parent_material_id) < best_matching_old_material_prefix_length:
continue continue
if len(old_parent_material_id) <= len(old_material_id_in_stack): if len(old_parent_material_id) <= len(old_material_id_in_stack):
if old_parent_material_id == old_material_id_in_stack[0:len(old_parent_material_id)]: if old_parent_material_id == old_material_id_in_stack[0:len(old_parent_material_id)]:
best_matching_old_meterial_prefix_length = len(old_parent_material_id) best_matching_old_material_prefix_length = len(old_parent_material_id)
best_matching_old_material_id = old_parent_material_id best_matching_old_material_id = old_parent_material_id
if best_matching_old_material_id is None: if best_matching_old_material_id is None:

View file

@ -390,6 +390,13 @@ UM.Dialog
} }
} }
function accept() {
manager.closeBackend();
manager.onOkButtonClicked();
base.visible = false;
base.accept();
}
function reject() { function reject() {
manager.onCancelButtonClicked(); manager.onCancelButtonClicked();
base.visible = false; base.visible = false;

View file

@ -57,7 +57,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version # Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini") version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser() version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions") version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion()) version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType()) version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
@ -97,7 +97,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
file_in_archive.compress_type = zipfile.ZIP_DEFLATED file_in_archive.compress_type = zipfile.ZIP_DEFLATED
# Do not include the network authentication keys # Do not include the network authentication keys
ignore_keys = {"network_authentication_id", "network_authentication_key"} ignore_keys = {"network_authentication_id", "network_authentication_key", "octoprint_api_key"}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys) serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
archive.writestr(file_in_archive, serialized_data) archive.writestr(file_in_archive, serialized_data)

View file

@ -6,7 +6,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Application import Application from UM.Application import Application
import UM.Scene.SceneNode from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
import Savitar import Savitar
@ -61,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive self._store_archive = store_archive
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
# \returns Uranium Scenen node. # \returns Uranium Scene node.
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
if type(um_node) is not UM.Scene.SceneNode.SceneNode: if not isinstance(um_node, SceneNode):
return None return None
active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
return
savitar_node = Savitar.SceneNode() savitar_node = Savitar.SceneNode()
node_matrix = um_node.getLocalTransformation() node_matrix = um_node.getLocalTransformation()
@ -96,6 +102,9 @@ class ThreeMFWriter(MeshWriter):
savitar_node.setSetting(key, str(stack.getProperty(key, "value"))) savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
for child_node in um_node.getChildren(): for child_node in um_node.getChildren():
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = self._convertUMNodeToSavitarNode(child_node) savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None: if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node) savitar_node.addChild(savitar_child_node)

View file

@ -9,6 +9,7 @@ from UM.Application import Application
from UM.Resources import Resources from UM.Resources import Resources
from UM.Logger import Logger from UM.Logger import Logger
class AutoSave(Extension): class AutoSave(Extension):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -16,18 +17,40 @@ class AutoSave(Extension):
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer() self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True) self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False self._saving = False
# At this point, the Application instance has not finished its constructor call yet, so directly using something
# like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
# application finishes its start up successfully.
self._init_timer = QTimer()
self._init_timer.setInterval(1000)
self._init_timer.setSingleShot(True)
self._init_timer.timeout.connect(self.initialize)
self._init_timer.start()
def initialize(self):
# only initialise if the application is created and has started
from cura.CuraApplication import CuraApplication
if not CuraApplication.Created:
self._init_timer.start()
return
if not CuraApplication.getInstance().started:
self._init_timer.start()
return
self._change_timer.timeout.connect(self._onTimeout)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._triggerTimer()
def _triggerTimer(self, *args): def _triggerTimer(self, *args):
if not self._saving: if not self._saving:
self._change_timer.start() self._change_timer.start()

View file

@ -1,3 +1,49 @@
[3.2.0]
*Tree support
Experimental tree-like support structure that uses branches to support prints. Branches grow and multiply towards the model, with fewer contact points than alternative support methods. This results in better surface finishes for organic-shaped prints.
*Adaptive layers
Prints with a variable layer thickness which adapts to the angle of the models surfaces. The result is high-quality surface finishes with a marginally increased print time. This setting can be found under the experimental category.
*Faster startup
Printer definitions are now loaded when adding a printer, instead of loading all available printers on startup.
*Backface culling in layer view
Doubled frame rate by only rendering visible surfaces of the model in the layer view, instead of rendering the entire model. Good for lower spec GPUs as it is less resource-intensive.
*Multi build plate
Experimental feature that creates separate build plates with shared settings in a single session, eliminating the need to clear the build plate multiple times. Multiple build plates can be sliced and sent to a printer or printer group in Cura Connect. This feature must be enabled manually in the preferences general tab.
*Improved mesh type selection
New button in the left toolbar to edit per model settings, giving the user more control over where to place support. Objects can be used as meshes, with a drop down list where Print as support, Don't overlap support with other models, Modify settings for overlap with other models, or Modify settings for infill of other models can be specified. Contributed by fieldOfView.
*View optimization
Quick camera controls introduced in version 3.1 have been revised to create more accurate isometric, front, left, and right views.
*Updated sidebar to QtQuick 2.0
Application framework updated to increase speed, achieve a better width and style fit, and gives users dropdown menus that are styled to fit the enabled Ultimaker Cura theme, instead of the operating systems theme.
*Hide sidebar
The sidebar can now be hidden/shown by selecting View > Expand/Collapse Sidebar, or with the hotkey CMD + E (Mac) or CTRL + E (PC and Linux).
*Disable Send slice information
A shortcut to disable Send slice information has been added to the first launch to make it easier for privacy-conscious users to keep slice information private.
*Signed binaries (Windows)
For security-conscious users, the Windows installer and Windows binaries have been digitally signed to prevent “Unknown application” warnings and virus scanner false-positives.
*Start/end gcode script per extruder
Variables from both extruders in the start and end gcode snippets can now be accessed and edited, creating uniformity between profiles in different slicing environments. Contributed by fieldOfView.
*OctoPrint plugin added to plugin browser
This plugin enables printers managed with OctoPrint to print via Ultimaker Cura interface (version 3.2 or later).
*Bugfixes
- Fixed a bug where the mirror tool and center model options when used together would reset the model transformations
- Updated config file path to fix crashes caused by user config files that are located on remote drives
- Updated Arduino drivers to fix triggering errors during OTA updates in shared environments. This also fixes an issue when upgrading the firmware of the Ultimaker Original.
- Fixed an issue where arranging small models would fail, due to conflict with small model files combined with the “Ensure models are kept apart” option
[3.1.0] [3.1.0]
*Profile added for 0.25 mm print core *Profile added for 0.25 mm print core
This new print core gives extra fine line widths which gives prints extra definition and surface quality. This new print core gives extra fine line widths which gives prints extra definition and surface quality.

View file

@ -16,6 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from collections import defaultdict
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from . import ProcessSlicedLayersJob from . import ProcessSlicedLayersJob
from . import StartSliceJob from . import StartSliceJob
@ -69,9 +70,10 @@ class CuraEngineBackend(QObject, Backend):
# Workaround to disable layer view processing if layer view is not active. # Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged() self._onActiveViewChanged()
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = [] self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged) self._scene.sceneChanged.connect(self._onSceneChanged)
@ -86,7 +88,6 @@ class CuraEngineBackend(QObject, Backend):
# #
self._global_container_stack = None self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -105,17 +106,18 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None self._start_slice_job = None
self._start_slice_job_build_plate = None
self._slicing = False # Are we currently slicing? self._slicing = False # Are we currently slicing?
self._restart = False # Back-end is currently restarting? self._restart = False # Back-end is currently restarting?
self._tool_active = False # If a tool is active, some tasks do not have to do anything self._tool_active = False # If a tool is active, some tasks do not have to do anything
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
self._need_slicing = False self._build_plates_to_be_sliced = [] # what needs slicing?
self._engine_is_fresh = True # Is the newly started engine used before or not? self._engine_is_fresh = True # Is the newly started engine used before or not?
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
self._error_message = None # Pop-up message that shows errors. self._error_message = None # Pop-up message that shows errors.
self._last_num_objects = 0 # Count number of objects to see if there is something changed self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
self.backendQuit.connect(self._onBackendQuit) self.backendQuit.connect(self._onBackendQuit)
@ -174,6 +176,7 @@ class CuraEngineBackend(QObject, Backend):
self._createSocket() self._createSocket()
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon. if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
Logger.log("d", "Aborting process layers job...")
self._process_layers_job.abort() self._process_layers_job.abort()
self._process_layers_job = None self._process_layers_job = None
@ -183,24 +186,42 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice ## Manually triggers a reslice
@pyqtSlot() @pyqtSlot()
def forceSlice(self): def forceSlice(self):
if self._use_timer: self.markSliceAll()
self._change_timer.start() self.slice()
else:
self.slice()
## Perform a slice of the scene. ## Perform a slice of the scene.
def slice(self): def slice(self):
Logger.log("d", "Starting to slice...")
self._slice_start_time = time() self._slice_start_time = time()
if not self._need_slicing: if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0) self.processingProgress.emit(1.0)
self.backendStateChange.emit(BackendState.Done)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
return return
if Application.getInstance().getPrintInformation():
Application.getInstance().getPrintInformation().setToZeroPrintInformation() if self._process_layers_job:
Logger.log("d", "Process layers job still busy, trying later.")
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
self._scene.gcode_dict[build_plate_to_be_sliced] = []
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
if self._build_plates_to_be_sliced:
self.slice()
return
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._process is None: if self._process is None:
self._createSocket() self._createSocket()
@ -210,12 +231,16 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0) self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_list = [] self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._slicing = True self._slicing = True
self.slicingStarted.emit() self.slicingStarted.emit()
self.determineAutoSlicing() # Switch timer on or off if appropriate
slice_message = self._socket.createMessage("cura.proto.Slice") slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = build_plate_to_be_sliced
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
self._start_slice_job.start() self._start_slice_job.start()
self._start_slice_job.finished.connect(self._onStartSliceCompleted) self._start_slice_job.finished.connect(self._onStartSliceCompleted)
@ -224,7 +249,8 @@ class CuraEngineBackend(QObject, Backend):
def _terminate(self): def _terminate(self):
self._slicing = False self._slicing = False
self._stored_layer_data = [] self._stored_layer_data = []
self._stored_optimized_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
if self._start_slice_job is not None: if self._start_slice_job is not None:
self._start_slice_job.cancel() self._start_slice_job.cancel()
@ -262,6 +288,7 @@ class CuraEngineBackend(QObject, Backend):
self._start_slice_job = None self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
return return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -339,7 +366,9 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Error) self.backendStateChange.emit(BackendState.Error)
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
self._invokeSlice()
return return
# Preparation completed, send it to the backend. # Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage()) self._socket.sendMessage(job.getSliceMessage())
@ -363,7 +392,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Disabled) self.backendStateChange.emit(BackendState.Disabled)
gcode_list = node.callDecoration("getGCodeList") gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None: if gcode_list is not None:
self._scene.gcode_list = gcode_list self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
if self._use_timer == enable_timer: if self._use_timer == enable_timer:
return self._use_timer return self._use_timer
@ -375,33 +404,53 @@ class CuraEngineBackend(QObject, Backend):
self.disableTimer() self.disableTimer()
return False return False
## Return a dict with number of objects per build plate
def _numObjects(self):
num_objects = defaultdict(int)
for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects
if node.callDecoration("isSliceable"):
build_plate_number = node.callDecoration("getBuildPlateNumber")
num_objects[build_plate_number] += 1
return num_objects
## Listener for when the scene has changed. ## Listener for when the scene has changed.
# #
# This should start a slice if the scene is now ready to slice. # This should start a slice if the scene is now ready to slice.
# #
# \param source The scene node that was changed. # \param source The scene node that was changed.
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if type(source) is not SceneNode: if not isinstance(source, SceneNode):
return return
root_scene_nodes_changed = False # This case checks if the source node is a node that contains GCode. In this case the
if source == self._scene.getRoot(): # current layer data is removed so the previous data is not rendered - CURA-4821
num_objects = 0 if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"):
for node in DepthFirstIterator(self._scene.getRoot()): self._stored_optimized_layer_data = {}
# Only count sliceable objects
if node.callDecoration("isSliceable"):
num_objects += 1
if num_objects != self._last_num_objects:
self._last_num_objects = num_objects
root_scene_nodes_changed = True
else:
return
if not source.callDecoration("isGroup") and not root_scene_nodes_changed: build_plate_changed = set()
if source.getMeshData() is None: source_build_plate_number = source.callDecoration("getBuildPlateNumber")
return if source == self._scene.getRoot():
if source.getMeshData().getVertices() is None: # we got the root node
return num_objects = self._numObjects()
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
build_plate_changed.add(build_plate_number)
else:
# we got a single scenenode
if not source.callDecoration("isGroup"):
if source.getMeshData() is None:
return
if source.getMeshData().getVertices() is None:
return
build_plate_changed.add(source_build_plate_number)
build_plate_changed.discard(None)
build_plate_changed.discard(-1) # object not on build plate
if not build_plate_changed:
return
if self._tool_active: if self._tool_active:
# do it later, each source only has to be done once # do it later, each source only has to be done once
@ -409,9 +458,18 @@ class CuraEngineBackend(QObject, Backend):
self._postponed_scene_change_sources.append(source) self._postponed_scene_change_sources.append(source)
return return
self.needsSlicing()
self.stopSlicing() self.stopSlicing()
self._onChanged() for build_plate_number in build_plate_changed:
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
# if not self._use_timer:
# With manually having to slice, we want to clear the old invalid layer data.
self._clearLayerData(build_plate_changed)
self._invokeSlice()
## Called when an error occurs in the socket connection towards the engine. ## Called when an error occurs in the socket connection towards the engine.
# #
@ -431,16 +489,21 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("w", "A socket error caused the connection to be reset") Logger.log("w", "A socket error caused the connection to be reset")
## Remove old layer data (if any) ## Remove old layer data (if any)
def _clearLayerData(self): def _clearLayerData(self, build_plate_numbers = set()):
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"): if node.callDecoration("getLayerData"):
node.getParent().removeChild(node) if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
break node.getParent().removeChild(node)
## Convenient function: set need_slicing, emit state and clear layer data def markSliceAll(self):
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
## Convenient function: mark everything to slice, emit state and clear layer data
def needsSlicing(self): def needsSlicing(self):
self.stopSlicing() self.stopSlicing()
self._need_slicing = True self.markSliceAll()
self.processingProgress.emit(0.0) self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
if not self._use_timer: if not self._use_timer:
@ -462,7 +525,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self): def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False self._is_error_check_scheduled = False
if not self._slicing and self._need_slicing: if not self._slicing and self._build_plates_to_be_sliced:
self.needsSlicing() self.needsSlicing()
self._onChanged() self._onChanged()
@ -476,7 +539,9 @@ class CuraEngineBackend(QObject, Backend):
# #
# \param message The protobuf message containing sliced layer data. # \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message): def _onOptimizedLayerMessage(self, message):
self._stored_optimized_layer_data.append(message) if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine. ## Called when a progress message is received from the engine.
# #
@ -485,6 +550,16 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(message.amount) self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing) self.backendStateChange.emit(BackendState.Processing)
# testing
def _invokeSlice(self):
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
# otherwise business as usual
if self._is_error_check_scheduled:
self._change_timer.stop()
else:
self._change_timer.start()
## Called when the engine sends a message that slicing is finished. ## Called when the engine sends a message that slicing is finished.
# #
# \param message The protobuf message signalling that slicing is finished. # \param message The protobuf message signalling that slicing is finished.
@ -492,36 +567,46 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Done) self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0) self.processingProgress.emit(1.0)
for line in self._scene.gcode_list: gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths)) replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights)) replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts)) replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName)) replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced gcode_list[index] = replaced
self._slicing = False self._slicing = False
self._need_slicing = False
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) # See if we need to process the sliced layers job.
self._process_layers_job.finished.connect(self._onProcessLayersFinished) active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
self._process_layers_job.start() if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
self._stored_optimized_layer_data = [] self._startProcessSlicedLayersJob(active_build_plate)
# self._onActiveViewChanged()
self._start_slice_job_build_plate = None
Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error
# self.slice()
# Call slice again using the timer, allowing the backend to restart
if self._build_plates_to_be_sliced:
self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
self._invokeSlice()
## Called when a g-code message is received from the engine. ## Called when a g-code message is received from the engine.
# #
# \param message The protobuf message containing g-code, encoded as UTF-8. # \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message): def _onGCodeLayerMessage(self, message):
self._scene.gcode_list.append(message.data.decode("utf-8", "replace")) self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
## Called when a g-code prefix message is received from the engine. ## Called when a g-code prefix message is received from the engine.
# #
# \param message The protobuf message containing the g-code prefix, # \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8. # encoded as UTF-8.
def _onGCodePrefixMessage(self, message): def _onGCodePrefixMessage(self, message):
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace")) self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
## Creates a new socket connection. ## Creates a new socket connection.
def _createSocket(self): def _createSocket(self):
@ -551,7 +636,7 @@ class CuraEngineBackend(QObject, Backend):
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
times = self._parseMessagePrintTimes(message) times = self._parseMessagePrintTimes(message)
self.printDurationMessage.emit(times, material_amounts) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts)
## Called for parsing message to retrieve estimated time per feature ## Called for parsing message to retrieve estimated time per feature
# #
@ -605,19 +690,25 @@ class CuraEngineBackend(QObject, Backend):
source = self._postponed_scene_change_sources.pop(0) source = self._postponed_scene_change_sources.pop(0)
self._onSceneChanged(source) self._onSceneChanged(source)
def _startProcessSlicedLayersJob(self, build_plate_number):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
self._process_layers_job.setBuildPlate(build_plate_number)
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
self._process_layers_job.start()
## Called when the user changes the active view mode. ## Called when the user changes the active view mode.
def _onActiveViewChanged(self): def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView(): application = Application.getInstance()
view = Application.getInstance().getController().getActiveView() view = application.getController().getActiveView()
if view:
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True self._layer_view_active = True
# There is data and we're not slicing at the moment # There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
if self._stored_optimized_layer_data and not self._slicing: # TODO: what build plate I am slicing
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job:
self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._startProcessSlicedLayersJob(active_build_plate)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
else: else:
self._layer_view_active = False self._layer_view_active = False
@ -635,7 +726,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.propertyChanged.disconnect(self._onSettingChanged)
@ -646,14 +737,17 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged) self._global_container_stack.containersChanged.connect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged) extruder.propertyChanged.connect(self._onSettingChanged)
extruder.containersChanged.connect(self._onChanged) extruder.containersChanged.connect(self._onChanged)
self._onChanged() self._onChanged()
def _onProcessLayersFinished(self, job): def _onProcessLayersFinished(self, job):
del self._stored_optimized_layer_data[job.getBuildPlate()]
self._process_layers_job = None self._process_layers_job = None
Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice()
## Connect slice function to timer. ## Connect slice function to timer.
def enableTimer(self): def enableTimer(self):

View file

@ -12,4 +12,6 @@ class ProcessGCodeLayerJob(Job):
self._message = message self._message = message
def run(self): def run(self):
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace")) active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = self._scene.gcode_dict[active_build_plate_id]
gcode_list.append(self._message.data.decode("utf-8", "replace"))

View file

@ -4,8 +4,6 @@
import gc import gc
from UM.Job import Job from UM.Job import Job
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application from UM.Application import Application
from UM.Mesh.MeshData import MeshData from UM.Mesh.MeshData import MeshData
from UM.Preferences import Preferences from UM.Preferences import Preferences
@ -17,6 +15,8 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder from cura import LayerDataBuilder
from cura import LayerDataDecorator from cura import LayerDataDecorator
@ -49,6 +49,7 @@ class ProcessSlicedLayersJob(Job):
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
self._abort_requested = False self._abort_requested = False
self._build_plate_number = None
## Aborts the processing of layers. ## Aborts the processing of layers.
# #
@ -59,7 +60,14 @@ class ProcessSlicedLayersJob(Job):
def abort(self): def abort(self):
self._abort_requested = True self._abort_requested = True
def setBuildPlate(self, new_value):
self._build_plate_number = new_value
def getBuildPlate(self):
return self._build_plate_number
def run(self): def run(self):
Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number)
start_time = time() start_time = time()
view = Application.getInstance().getController().getActiveView() view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "SimulationView": if view.getPluginId() == "SimulationView":
@ -73,17 +81,8 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode() new_node = CuraSceneNode()
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
## Remove old layer data (if any)
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
node.getParent().removeChild(node)
break
if self._abort_requested:
if self._progress_message:
self._progress_message.hide()
return
# Force garbage collection. # Force garbage collection.
# For some reason, Python has a tendency to keep the layer data # For some reason, Python has a tendency to keep the layer data

View file

@ -5,20 +5,25 @@ import numpy
from string import Formatter from string import Formatter
from enum import IntEnum from enum import IntEnum
import time import time
import re
from UM.Job import Job from UM.Job import Job
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType from UM.Settings.SettingRelation import RelationType
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
NON_PRINTING_MESH_SETTINGS = ["anti_overhang_mesh", "infill_mesh", "cutting_mesh"]
class StartJobResult(IntEnum): class StartJobResult(IntEnum):
Finished = 1 Finished = 1
Error = 2 Error = 2
@ -32,14 +37,37 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcod ## Formatter class that handles token expansion in start/end gcod
class GcodeStartEndFormatter(Formatter): class GcodeStartEndFormatter(Formatter):
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class] def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified
if isinstance(key, str): if isinstance(key, str):
try: try:
return kwargs[key] extruder_nr = kwargs["default_extruder_nr"]
except ValueError:
extruder_nr = -1
key_fragments = [fragment.strip() for fragment in key.split(',')]
if len(key_fragments) == 2:
try:
extruder_nr = int(key_fragments[1])
except ValueError:
try:
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
except (KeyError, ValueError):
# either the key does not exist, or the value is not an int
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
elif len(key_fragments) != 1:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
key = key_fragments[0]
try:
return kwargs[str(extruder_nr)][key]
except KeyError: except KeyError:
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key) Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
return "{" + key + "}" return "{" + key + "}"
else: else:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key) Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}" return "{" + str(key) + "}"
@ -51,10 +79,16 @@ class StartSliceJob(Job):
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._slice_message = slice_message self._slice_message = slice_message
self._is_cancelled = False self._is_cancelled = False
self._build_plate_number = None
self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine
def getSliceMessage(self): def getSliceMessage(self):
return self._slice_message return self._slice_message
def setBuildPlate(self, build_plate_number):
self._build_plate_number = build_plate_number
## Check if a stack has any errors. ## Check if a stack has any errors.
## returns true if it has errors, false otherwise. ## returns true if it has errors, false otherwise.
def _checkStackForErrors(self, stack): def _checkStackForErrors(self, stack):
@ -71,6 +105,10 @@ class StartSliceJob(Job):
## Runs the job that initiates the slicing. ## Runs the job that initiates the slicing.
def run(self): def run(self):
if self._build_plate_number is None:
self.setResult(StartJobResult.Error)
return
stack = Application.getInstance().getGlobalContainerStack() stack = Application.getInstance().getGlobalContainerStack()
if not stack: if not stack:
self.setResult(StartJobResult.Error) self.setResult(StartJobResult.Error)
@ -85,6 +123,12 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.BuildPlateError) self.setResult(StartJobResult.BuildPlateError)
return return
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \
not Application.getInstance().getMachineManager().variantBuildplateUsable:
self.setResult(StartJobResult.MaterialIncompatible)
return
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
material = extruder_stack.findContainer({"type": "material"}) material = extruder_stack.findContainer({"type": "material"})
if material: if material:
@ -92,9 +136,14 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible) self.setResult(StartJobResult.MaterialIncompatible)
return return
# Validate settings per selectable model
if Application.getInstance().getObjectsModel().stacksHaveErrors():
self.setResult(StartJobResult.ObjectSettingError)
return
# Don't slice if there is a per object setting with an error value. # Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable(): if node.isSelectable():
continue continue
if self._checkStackForErrors(node.callDecoration("getStack")): if self._checkStackForErrors(node.callDecoration("getStack")):
@ -104,7 +153,7 @@ class StartSliceJob(Job):
with self._scene.getSceneLock(): with self._scene.getSceneLock():
# Remove old layer data. # Remove old layer data.
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"): if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
node.getParent().removeChild(node) node.getParent().removeChild(node)
break break
@ -118,10 +167,15 @@ class StartSliceJob(Job):
if getattr(node, "_outside_buildarea", False): if getattr(node, "_outside_buildarea", False):
continue continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
children = node.getAllChildren() children = node.getAllChildren()
children.append(node) children.append(node)
for child_node in children: for child_node in children:
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None: if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node) temp_list.append(child_node)
if temp_list: if temp_list:
@ -133,12 +187,18 @@ class StartSliceJob(Job):
temp_list = [] temp_list = []
has_printing_mesh = False has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
_non_printing_mesh = getattr(node, "_non_printing_mesh", False) per_object_stack = node.callDecoration("getStack")
if not getattr(node, "_outside_buildarea", False) or _non_printing_mesh: is_non_printing_mesh = False
temp_list.append(node) if per_object_stack:
if not _non_printing_mesh: is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
has_printing_mesh = True
if node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
Job.yieldThread() Job.yieldThread()
#If the list doesn't have any model with suitable settings then clean the list #If the list doesn't have any model with suitable settings then clean the list
@ -224,19 +284,36 @@ class StartSliceJob(Job):
result["date"] = time.strftime("%d-%m-%Y") result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
result["initial_extruder_nr"] = initial_extruder_nr
return result return result
## Replace setting tokens in a piece of g-code. ## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in. # \param value A piece of g-code to replace tokens in.
# \param settings A dictionary of tokens to replace and their respective # \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
# replacement strings. def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1):
def _expandGcodeTokens(self, value: str, settings: dict): if not self._all_extruders_settings:
global_stack = Application.getInstance().getGlobalContainerStack()
# NB: keys must be strings for the string formatter
self._all_extruders_settings = {
"-1": self._buildReplacementTokens(global_stack)
}
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
try: try:
# any setting can be used as a token # any setting can be used as a token
fmt = GcodeStartEndFormatter() fmt = GcodeStartEndFormatter()
settings = self._all_extruders_settings.copy()
settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings)) return str(fmt.format(value, **settings))
except: except:
Logger.logException("w", "Unable to do token replacement on start/end gcode") Logger.logException("w", "Unable to do token replacement on start/end g-code")
return str(value) return str(value)
## Create extruder message from stack ## Create extruder message from stack
@ -250,8 +327,9 @@ class StartSliceJob(Job):
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
# Replace the setting tokens in start and end g-code. # Replace the setting tokens in start and end g-code.
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings) extruder_nr = stack.getProperty("extruder_nr", "value")
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings) settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr)
for key, value in settings.items(): for key, value in settings.items():
# Do not send settings that are not settable_per_extruder. # Do not send settings that are not settable_per_extruder.
@ -271,18 +349,20 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"] start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings)) pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"} settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings)) print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
# Find the correct temperatures from the first used extruder settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
# Replace the setting tokens in start and end g-code. # Replace the setting tokens in start and end g-code.
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings) # Use values from the first used extruder by default so we get the expected temperatures
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings) initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
# Add all sub-messages for each individual setting. # Add all sub-messages for each individual setting.
for key, value in settings.items(): for key, value in settings.items():

View file

@ -39,7 +39,7 @@ class CuraProfileReader(ProfileReader):
except zipfile.BadZipFile: except zipfile.BadZipFile:
# It must be an older profile from Cura 2.1. # It must be an older profile from Cura 2.1.
with open(file_name, encoding="utf-8") as fhandle: with open(file_name, encoding = "utf-8") as fhandle:
serialized = fhandle.read() serialized = fhandle.read()
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)] return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
@ -52,10 +52,10 @@ class CuraProfileReader(ProfileReader):
parser = configparser.ConfigParser(interpolation=None) parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized) parser.read_string(serialized)
if not "general" in parser: if "general" not in parser:
Logger.log("w", "Missing required section 'general'.") Logger.log("w", "Missing required section 'general'.")
return [] return []
if not "version" in parser["general"]: if "version" not in parser["general"]:
Logger.log("w", "Missing required 'version' property") Logger.log("w", "Missing required 'version' property")
return [] return []

View file

@ -20,7 +20,7 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy # The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications. # to change it to work for other applications.
class FirmwareUpdateChecker(Extension): class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version" JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -49,7 +49,6 @@ class FirmwareUpdateChecker(Extension):
def _onContainerAdded(self, container): def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added # Only take care when a new GlobalStack was added
if isinstance(container, GlobalStack): if isinstance(container, GlobalStack):
Logger.log("i", "You have a '%s' in printer list. Let's check the firmware!", container.getId())
self.checkFirmwareVersion(container, True) self.checkFirmwareVersion(container, True)
## Connect with software.ultimaker.com, load latest.version and check version info. ## Connect with software.ultimaker.com, load latest.version and check version info.

Some files were not shown because too many files have changed in this diff Show more