mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-11-23 10:51:16 -07:00
Merge remote-tracking branch 'refs/remotes/Ultimaker/master'
This commit is contained in:
commit
33ba9a274d
434 changed files with 38422 additions and 9936 deletions
36
.github/ISSUE_TEMPLATE.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!--
|
||||||
|
The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out.
|
||||||
|
Before filing, please check if the issue already exists (either open or closed).
|
||||||
|
|
||||||
|
It is also helpful to attach a project (.3MF) 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.
|
||||||
|
|
||||||
|
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) -->
|
||||||
22
.gitignore
vendored
22
.gitignore
vendored
|
|
@ -33,18 +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/CuraSolidWorksPlugin
|
||||||
|
plugins/CuraVariSlicePlugin
|
||||||
|
plugins/Doodle3D-cura-plugin
|
||||||
|
plugins/FlatProfileExporter
|
||||||
|
plugins/GodMode
|
||||||
|
plugins/OctoPrintPlugin
|
||||||
|
plugins/ProfileFlattener
|
||||||
|
plugins/X3GWriter
|
||||||
|
|
||||||
#Build stuff
|
#Build stuff
|
||||||
CMakeCache.txt
|
CMakeCache.txt
|
||||||
|
|
|
||||||
70
Jenkinsfile
vendored
70
Jenkinsfile
vendored
|
|
@ -1,45 +1,47 @@
|
||||||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
timeout(time: 2, unit: "HOURS") {
|
||||||
// Prepare building
|
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||||
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.'])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
README.md
71
README.md
|
|
@ -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 a 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,53 +18,26 @@ 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)
|
Plugins
|
||||||
|
|
||||||
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/Plugin-Directory) for details about creating and using plugins.
|
||||||
* [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
|
Supported printers
|
||||||
----------------------------------
|
-------------
|
||||||
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/Adding-new-machine-profiles-to-Cura) for guidelines about adding support for new machines.
|
||||||
|
|
||||||
* Change the machine ID to something unique
|
Configuring Cura
|
||||||
* 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 out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
|
||||||
* 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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
Translating Cura
|
Translating Cura
|
||||||
----------------
|
----------------
|
||||||
|
|
@ -93,3 +60,7 @@ To submit your translation, ideally you would make two pull requests where all `
|
||||||
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.
|
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.
|
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.
|
||||||
|
|
||||||
|
License
|
||||||
|
----------------
|
||||||
|
Cura is released under the terms of the LGPLv3 or higher. A copy of this license should be included with the software.
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Cura
|
Name=Ultimaker Cura
|
||||||
Name[de]=Cura
|
Name[de]=Ultimaker Cura
|
||||||
GenericName=3D Printing Software
|
GenericName=3D Printing Software
|
||||||
GenericName[de]=3D-Druck-Software
|
GenericName[de]=3D-Druck-Software
|
||||||
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
||||||
|
|
|
||||||
27
cura/Arrange.py → cura/Arranging/Arrange.py
Executable file → Normal file
27
cura/Arrange.py → cura/Arranging/Arrange.py
Executable file → Normal 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:
|
||||||
|
|
@ -52,6 +53,8 @@ class Arrange:
|
||||||
# Place all objects fixed nodes
|
# Place all objects fixed nodes
|
||||||
for fixed_node in fixed_nodes:
|
for fixed_node in fixed_nodes:
|
||||||
vertices = fixed_node.callDecoration("getConvexHull")
|
vertices = fixed_node.callDecoration("getConvexHull")
|
||||||
|
if not vertices:
|
||||||
|
continue
|
||||||
points = copy.deepcopy(vertices._points)
|
points = copy.deepcopy(vertices._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)
|
||||||
|
|
@ -62,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)
|
||||||
|
|
@ -166,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
|
||||||
|
|
@ -179,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
|
||||||
154
cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py
Normal file
154
cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py
Normal 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()
|
||||||
7
cura/ArrangeObjectsJob.py → cura/Arranging/ArrangeObjectsJob.py
Executable file → Normal file
7
cura/ArrangeObjectsJob.py → cura/Arranging/ArrangeObjectsJob.py
Executable file → Normal 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
|
||||||
|
|
||||||
13
cura/ShapeArray.py → cura/Arranging/ShapeArray.py
Executable file → Normal file
13
cura/ShapeArray.py → cura/Arranging/ShapeArray.py
Executable file → Normal 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)
|
||||||
0
cura/Arranging/__init__.py
Normal file
0
cura/Arranging/__init__.py
Normal file
53
cura/BuildPlateModel.py
Normal file
53
cura/BuildPlateModel.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Application import Application
|
||||||
|
|
||||||
|
|
||||||
|
class BuildPlateModel(ListModel):
|
||||||
|
maxBuildPlateChanged = pyqtSignal()
|
||||||
|
activeBuildPlateChanged = pyqtSignal()
|
||||||
|
selectionChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
|
||||||
|
Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
|
||||||
|
|
||||||
|
self._max_build_plate = 1 # default
|
||||||
|
self._active_build_plate = -1
|
||||||
|
self._selection_build_plates = []
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def createBuildPlateModel():
|
||||||
|
return BuildPlateModel()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -513,14 +513,13 @@ class BuildVolume(SceneNode):
|
||||||
update_disallowed_areas = False
|
update_disallowed_areas = False
|
||||||
update_raft_thickness = False
|
update_raft_thickness = False
|
||||||
update_extra_z_clearance = True
|
update_extra_z_clearance = True
|
||||||
|
|
||||||
for setting_key in self._changed_settings_since_last_rebuild:
|
for setting_key in self._changed_settings_since_last_rebuild:
|
||||||
|
|
||||||
if setting_key == "print_sequence":
|
if setting_key == "print_sequence":
|
||||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence",
|
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||||
"value") == "one_at_a_time" and len(
|
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
|
||||||
self._scene_objects) > 1:
|
|
||||||
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"),
|
|
||||||
machine_height)
|
|
||||||
if self._height < machine_height:
|
if self._height < machine_height:
|
||||||
self._build_volume_message.show()
|
self._build_volume_message.show()
|
||||||
else:
|
else:
|
||||||
|
|
@ -528,9 +527,20 @@ class BuildVolume(SceneNode):
|
||||||
else:
|
else:
|
||||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
self._build_volume_message.hide()
|
self._build_volume_message.hide()
|
||||||
|
update_disallowed_areas = True
|
||||||
rebuild_me = True
|
rebuild_me = True
|
||||||
|
|
||||||
if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings:
|
# sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
|
||||||
|
if setting_key in self._machine_settings:
|
||||||
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
|
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
|
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||||
|
update_extra_z_clearance = True
|
||||||
|
update_disallowed_areas = True
|
||||||
|
rebuild_me = True
|
||||||
|
|
||||||
|
if setting_key in self._skirt_settings + self._prime_settings + self._tower_settings + self._ooze_shield_settings + self._distance_settings + self._extruder_settings:
|
||||||
update_disallowed_areas = True
|
update_disallowed_areas = True
|
||||||
rebuild_me = True
|
rebuild_me = True
|
||||||
|
|
||||||
|
|
@ -969,6 +979,7 @@ class BuildVolume(SceneNode):
|
||||||
def _clamp(self, value, min_value, max_value):
|
def _clamp(self, value, min_value, max_value):
|
||||||
return max(min(value, max_value), min_value)
|
return max(min(value, max_value), min_value)
|
||||||
|
|
||||||
|
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
|
||||||
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
|
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
|
||||||
_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"]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -53,13 +53,13 @@ class CrashHandler:
|
||||||
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.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()
|
||||||
self.data["time_stamp"] = time.time()
|
self.data["time_stamp"] = time.time()
|
||||||
|
|
||||||
Logger.log("c", "An uncaught exception has occurred!")
|
Logger.log("c", "An uncaught error has occurred!")
|
||||||
for line in traceback.format_exception(exception_type, value, tb):
|
for line in traceback.format_exception(exception_type, value, tb):
|
||||||
for part in line.rstrip("\n").split("\n"):
|
for part in line.rstrip("\n").split("\n"):
|
||||||
Logger.log("c", part)
|
Logger.log("c", part)
|
||||||
|
|
@ -71,6 +71,7 @@ class CrashHandler:
|
||||||
if not application:
|
if not application:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.dialog = QDialog()
|
||||||
self._createDialog()
|
self._createDialog()
|
||||||
|
|
||||||
## Creates a modal dialog.
|
## Creates a modal dialog.
|
||||||
|
|
@ -90,7 +91,7 @@ class CrashHandler:
|
||||||
|
|
||||||
def _messageWidget(self):
|
def _messageWidget(self):
|
||||||
label = QLabel()
|
label = QLabel()
|
||||||
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal exception 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>
|
||||||
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
|
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
|
@ -143,7 +144,7 @@ class CrashHandler:
|
||||||
|
|
||||||
def _exceptionInfoWidget(self):
|
def _exceptionInfoWidget(self):
|
||||||
group = QGroupBox()
|
group = QGroupBox()
|
||||||
group.setTitle(catalog.i18nc("@title:groupbox", "Exception traceback"))
|
group.setTitle(catalog.i18nc("@title:groupbox", "Error traceback"))
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
text_area = QTextEdit()
|
text_area = QTextEdit()
|
||||||
|
|
@ -288,4 +289,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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -124,5 +134,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)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# 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 PyQt5.QtNetwork import QLocalServer
|
from PyQt5.QtNetwork import QLocalServer
|
||||||
from PyQt5.QtNetwork import QLocalSocket
|
from PyQt5.QtNetwork import QLocalSocket
|
||||||
|
|
||||||
|
|
@ -17,7 +17,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
from UM.SaveFile import SaveFile
|
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
|
@ -32,15 +31,20 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
from cura.Arrange import Arrange
|
|
||||||
from cura.ShapeArray import ShapeArray
|
|
||||||
from cura.ConvexHullDecorator import ConvexHullDecorator
|
|
||||||
from cura.SetParentOperation import SetParentOperation
|
|
||||||
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
|
||||||
from cura.BlockSlicingDecorator import BlockSlicingDecorator
|
|
||||||
|
|
||||||
from cura.ArrangeObjectsJob import ArrangeObjectsJob
|
from cura.Arranging.Arrange import Arrange
|
||||||
|
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
|
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||||
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||||
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
from cura.Scene.CuraSceneController import CuraSceneController
|
||||||
|
|
||||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
@ -53,12 +57,13 @@ from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||||
from cura.Settings.UserProfilesModel import UserProfilesModel
|
from cura.Settings.UserProfilesModel import UserProfilesModel
|
||||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
|
|
||||||
|
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import BuildVolume
|
from . import BuildVolume
|
||||||
from . import CameraAnimation
|
from . import CameraAnimation
|
||||||
from . import PrintInformation
|
from . import PrintInformation
|
||||||
from . import CuraActions
|
from . import CuraActions
|
||||||
from . import ZOffsetDecorator
|
from cura.Scene import ZOffsetDecorator
|
||||||
from . import CuraSplashScreen
|
from . import CuraSplashScreen
|
||||||
from . import CameraImageProvider
|
from . import CameraImageProvider
|
||||||
from . import MachineActionManager
|
from . import MachineActionManager
|
||||||
|
|
@ -72,8 +77,9 @@ from cura.Settings.ContainerSettingsModel import ContainerSettingsModel
|
||||||
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||||
from cura.Settings.QualitySettingsModel import QualitySettingsModel
|
from cura.Settings.QualitySettingsModel import QualitySettingsModel
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.ObjectsModel import ObjectsModel
|
||||||
|
from cura.BuildPlateModel import BuildPlateModel
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
|
from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
@ -85,7 +91,6 @@ import sys
|
||||||
import os.path
|
import os.path
|
||||||
import numpy
|
import numpy
|
||||||
import copy
|
import copy
|
||||||
import urllib.parse
|
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
|
@ -95,10 +100,11 @@ numpy.seterr(all="ignore")
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if not MYPY:
|
if not MYPY:
|
||||||
try:
|
try:
|
||||||
from cura.CuraVersion import CuraVersion, CuraBuildType
|
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||||
CuraBuildType = ""
|
CuraBuildType = ""
|
||||||
|
CuraDebugMode = False
|
||||||
|
|
||||||
|
|
||||||
class CuraApplication(QtApplication):
|
class CuraApplication(QtApplication):
|
||||||
|
|
@ -126,7 +132,8 @@ class CuraApplication(QtApplication):
|
||||||
# Cura will always show the Add Machine Dialog upon start.
|
# Cura will always show the Add Machine Dialog upon start.
|
||||||
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
|
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
# this list of dir names will be used by UM to detect an old cura directory
|
# this list of dir names will be used by UM to detect an old cura directory
|
||||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
|
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
|
||||||
Resources.addExpectedDirNameInData(dir_name)
|
Resources.addExpectedDirNameInData(dir_name)
|
||||||
|
|
@ -155,7 +162,6 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
||||||
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
||||||
|
|
||||||
SettingDefinition.addSettingType("[int]", None, str, None)
|
SettingDefinition.addSettingType("[int]", None, str, None)
|
||||||
|
|
||||||
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
|
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
|
||||||
|
|
@ -171,16 +177,18 @@ class CuraApplication(QtApplication):
|
||||||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||||
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer, "variant")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer, "material")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer, "user")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.DefinitionChangesContainer)
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack, "machine")
|
||||||
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||||
|
|
||||||
## Initialise the version upgrade manager with Cura's storage paths.
|
## Initialise the version upgrade manager with Cura's storage paths.
|
||||||
import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
|
# Needs to be here to prevent circular dependencies.
|
||||||
|
import UM.VersionUpgradeManager
|
||||||
|
|
||||||
UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
|
UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
|
||||||
{
|
{
|
||||||
|
|
@ -200,13 +208,20 @@ class CuraApplication(QtApplication):
|
||||||
self._machine_manager = None # This is initialized on demand.
|
self._machine_manager = None # This is initialized on demand.
|
||||||
self._extruder_manager = None
|
self._extruder_manager = None
|
||||||
self._material_manager = None
|
self._material_manager = None
|
||||||
|
self._object_manager = None
|
||||||
|
self._build_plate_model = None
|
||||||
self._setting_inheritance_manager = None
|
self._setting_inheritance_manager = None
|
||||||
self._simple_mode_settings_manager = None
|
self._simple_mode_settings_manager = None
|
||||||
|
self._cura_scene_controller = None
|
||||||
|
|
||||||
self._additional_components = {} # Components to add to certain areas in the interface
|
self._additional_components = {} # Components to add to certain areas in the interface
|
||||||
|
|
||||||
super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType,
|
super().__init__(name = "cura",
|
||||||
tray_icon_name = "cura-icon-32.png")
|
version = CuraVersion,
|
||||||
|
buildtype = CuraBuildType,
|
||||||
|
is_debug_mode = CuraDebugMode,
|
||||||
|
tray_icon_name = "cura-icon-32.png",
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
self.default_theme = "cura-light"
|
self.default_theme = "cura-light"
|
||||||
|
|
||||||
|
|
@ -225,7 +240,9 @@ class CuraApplication(QtApplication):
|
||||||
"TranslateTool",
|
"TranslateTool",
|
||||||
"FileLogger",
|
"FileLogger",
|
||||||
"XmlMaterialProfile",
|
"XmlMaterialProfile",
|
||||||
"PluginBrowser"
|
"PluginBrowser",
|
||||||
|
"PrepareStage",
|
||||||
|
"MonitorStage"
|
||||||
])
|
])
|
||||||
self._physics = None
|
self._physics = None
|
||||||
self._volume = None
|
self._volume = None
|
||||||
|
|
@ -249,6 +266,7 @@ class CuraApplication(QtApplication):
|
||||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
||||||
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||||
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
|
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
|
||||||
|
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
|
||||||
|
|
||||||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||||
|
|
@ -261,17 +279,17 @@ class CuraApplication(QtApplication):
|
||||||
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||||
|
|
||||||
empty_variant_container = copy.deepcopy(empty_container)
|
empty_variant_container = copy.deepcopy(empty_container)
|
||||||
empty_variant_container._id = "empty_variant"
|
empty_variant_container.setMetaDataEntry("id", "empty_variant")
|
||||||
empty_variant_container.addMetaDataEntry("type", "variant")
|
empty_variant_container.addMetaDataEntry("type", "variant")
|
||||||
ContainerRegistry.getInstance().addContainer(empty_variant_container)
|
ContainerRegistry.getInstance().addContainer(empty_variant_container)
|
||||||
|
|
||||||
empty_material_container = copy.deepcopy(empty_container)
|
empty_material_container = copy.deepcopy(empty_container)
|
||||||
empty_material_container._id = "empty_material"
|
empty_material_container.setMetaDataEntry("id", "empty_material")
|
||||||
empty_material_container.addMetaDataEntry("type", "material")
|
empty_material_container.addMetaDataEntry("type", "material")
|
||||||
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
||||||
|
|
||||||
empty_quality_container = copy.deepcopy(empty_container)
|
empty_quality_container = copy.deepcopy(empty_container)
|
||||||
empty_quality_container._id = "empty_quality"
|
empty_quality_container.setMetaDataEntry("id", "empty_quality")
|
||||||
empty_quality_container.setName("Not Supported")
|
empty_quality_container.setName("Not Supported")
|
||||||
empty_quality_container.addMetaDataEntry("quality_type", "not_supported")
|
empty_quality_container.addMetaDataEntry("quality_type", "not_supported")
|
||||||
empty_quality_container.addMetaDataEntry("type", "quality")
|
empty_quality_container.addMetaDataEntry("type", "quality")
|
||||||
|
|
@ -279,12 +297,12 @@ class CuraApplication(QtApplication):
|
||||||
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
||||||
|
|
||||||
empty_quality_changes_container = copy.deepcopy(empty_container)
|
empty_quality_changes_container = copy.deepcopy(empty_container)
|
||||||
empty_quality_changes_container._id = "empty_quality_changes"
|
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
|
||||||
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
|
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
|
||||||
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
|
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
|
||||||
|
|
||||||
with ContainerRegistry.getInstance().lockFile():
|
with ContainerRegistry.getInstance().lockFile():
|
||||||
ContainerRegistry.getInstance().load()
|
ContainerRegistry.getInstance().loadAllMetadata()
|
||||||
|
|
||||||
# set the setting version for Preferences
|
# set the setting version for Preferences
|
||||||
preferences = Preferences.getInstance()
|
preferences = Preferences.getInstance()
|
||||||
|
|
@ -302,11 +320,15 @@ class CuraApplication(QtApplication):
|
||||||
preferences.addPreference("cura/asked_dialog_on_project_save", False)
|
preferences.addPreference("cura/asked_dialog_on_project_save", False)
|
||||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||||
|
preferences.addPreference("cura/not_arrange_objects_on_load", False)
|
||||||
|
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||||
|
|
||||||
preferences.addPreference("cura/currency", "€")
|
preferences.addPreference("cura/currency", "€")
|
||||||
preferences.addPreference("cura/material_settings", "{}")
|
preferences.addPreference("cura/material_settings", "{}")
|
||||||
|
|
||||||
preferences.addPreference("view/invert_zoom", False)
|
preferences.addPreference("view/invert_zoom", False)
|
||||||
|
preferences.addPreference("view/filter_current_build_plate", False)
|
||||||
|
preferences.addPreference("cura/sidebar_collapsed", False)
|
||||||
|
|
||||||
self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement")
|
self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement")
|
||||||
|
|
||||||
|
|
@ -379,6 +401,8 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin")
|
self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin")
|
||||||
|
|
||||||
|
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
|
||||||
|
|
||||||
def _onEngineCreated(self):
|
def _onEngineCreated(self):
|
||||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||||
|
|
||||||
|
|
@ -386,7 +410,6 @@ class CuraApplication(QtApplication):
|
||||||
def needToShowUserAgreement(self):
|
def needToShowUserAgreement(self):
|
||||||
return self._need_to_show_user_agreement
|
return self._need_to_show_user_agreement
|
||||||
|
|
||||||
|
|
||||||
def setNeedToShowUserAgreement(self, set_value = True):
|
def setNeedToShowUserAgreement(self, set_value = True):
|
||||||
self._need_to_show_user_agreement = set_value
|
self._need_to_show_user_agreement = set_value
|
||||||
|
|
||||||
|
|
@ -394,7 +417,19 @@ class CuraApplication(QtApplication):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def closeApplication(self):
|
def closeApplication(self):
|
||||||
Logger.log("i", "Close application")
|
Logger.log("i", "Close application")
|
||||||
self._main_window.close()
|
main_window = self.getMainWindow()
|
||||||
|
if main_window is not None:
|
||||||
|
main_window.close()
|
||||||
|
else:
|
||||||
|
self.exit(0)
|
||||||
|
|
||||||
|
## Signal to connect preferences action in QML
|
||||||
|
showPreferencesWindow = pyqtSignal()
|
||||||
|
|
||||||
|
## Show the preferences window
|
||||||
|
@pyqtSlot()
|
||||||
|
def showPreferences(self):
|
||||||
|
self.showPreferencesWindow.emit()
|
||||||
|
|
||||||
## A reusable dialogbox
|
## A reusable dialogbox
|
||||||
#
|
#
|
||||||
|
|
@ -465,69 +500,10 @@ class CuraApplication(QtApplication):
|
||||||
if not self._started: # Do not do saving during application start
|
if not self._started: # Do not do saving during application start
|
||||||
return
|
return
|
||||||
|
|
||||||
# Lock file for "more" atomically loading and saving to/from config dir.
|
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||||
with ContainerRegistry.getInstance().lockFile():
|
|
||||||
for instance in ContainerRegistry.getInstance().findInstanceContainers():
|
|
||||||
if not instance.isDirty():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = instance.serialize()
|
|
||||||
except NotImplementedError:
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
|
||||||
continue
|
|
||||||
|
|
||||||
mime_type = ContainerRegistry.getMimeTypeForContainer(type(instance))
|
|
||||||
file_name = urllib.parse.quote_plus(instance.getId()) + "." + mime_type.preferredSuffix
|
|
||||||
instance_type = instance.getMetaDataEntry("type")
|
|
||||||
path = None
|
|
||||||
if instance_type == "material":
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
|
|
||||||
elif instance_type == "quality" or instance_type == "quality_changes":
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
|
|
||||||
elif instance_type == "user":
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
|
|
||||||
elif instance_type == "variant":
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
|
|
||||||
elif instance_type == "definition_changes":
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.DefinitionChangesContainer, file_name)
|
|
||||||
|
|
||||||
if path:
|
|
||||||
instance.setPath(path)
|
|
||||||
with SaveFile(path, "wt") as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
for stack in ContainerRegistry.getInstance().findContainerStacks():
|
|
||||||
self.saveStack(stack)
|
|
||||||
|
|
||||||
def saveStack(self, stack):
|
def saveStack(self, stack):
|
||||||
if not stack.isDirty():
|
ContainerRegistry.getInstance().saveContainer(stack)
|
||||||
return
|
|
||||||
try:
|
|
||||||
data = stack.serialize()
|
|
||||||
except NotImplementedError:
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
Logger.logException("e", "An exception occurred when serializing container %s", stack.getId())
|
|
||||||
return
|
|
||||||
|
|
||||||
mime_type = ContainerRegistry.getMimeTypeForContainer(type(stack))
|
|
||||||
file_name = urllib.parse.quote_plus(stack.getId()) + "." + mime_type.preferredSuffix
|
|
||||||
|
|
||||||
path = None
|
|
||||||
if isinstance(stack, GlobalStack):
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
|
|
||||||
elif isinstance(stack, ExtruderStack):
|
|
||||||
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
|
|
||||||
else:
|
|
||||||
path = Resources.getStoragePath(Resources.ContainerStacks, file_name)
|
|
||||||
|
|
||||||
stack.setPath(path)
|
|
||||||
with SaveFile(path, "wt") as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = QUrl)
|
@pyqtSlot(str, result = QUrl)
|
||||||
def getDefaultPath(self, key):
|
def getDefaultPath(self, key):
|
||||||
|
|
@ -561,11 +537,10 @@ class CuraApplication(QtApplication):
|
||||||
self._plugins_loaded = True
|
self._plugins_loaded = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def addCommandLineOptions(self, parser):
|
def addCommandLineOptions(self, parser, parsed_command_line = {}):
|
||||||
super().addCommandLineOptions(parser)
|
super().addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
|
||||||
parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
|
parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
|
||||||
parser.add_argument("--single-instance", action="store_true", default=False)
|
parser.add_argument("--single-instance", action="store_true", default=False)
|
||||||
parser.add_argument("--headless", action = "store_true", default=False)
|
|
||||||
|
|
||||||
# Set up a local socket server which listener which coordinates single instances Curas and accepts commands.
|
# Set up a local socket server which listener which coordinates single instances Curas and accepts commands.
|
||||||
def _setUpSingleInstanceServer(self):
|
def _setUpSingleInstanceServer(self):
|
||||||
|
|
@ -619,13 +594,16 @@ class CuraApplication(QtApplication):
|
||||||
# This should be called directly before creating an instance of CuraApplication.
|
# This should be called directly before creating an instance of CuraApplication.
|
||||||
# \returns \type{bool} True if the whole Cura app should continue running.
|
# \returns \type{bool} True if the whole Cura app should continue running.
|
||||||
@classmethod
|
@classmethod
|
||||||
def preStartUp(cls):
|
def preStartUp(cls, parser = None, parsed_command_line = {}):
|
||||||
# Peek the arguments and look for the 'single-instance' flag.
|
# Peek the arguments and look for the 'single-instance' flag.
|
||||||
parser = argparse.ArgumentParser(prog="cura") # pylint: disable=bad-whitespace
|
if not parser:
|
||||||
CuraApplication.addCommandLineOptions(parser)
|
parser = argparse.ArgumentParser(prog = "cura", add_help = False) # pylint: disable=bad-whitespace
|
||||||
parsed_command_line = vars(parser.parse_args())
|
CuraApplication.addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
|
||||||
|
# Important: It is important to keep this line here!
|
||||||
|
# In Uranium we allow to pass unknown arguments to the final executable or script.
|
||||||
|
parsed_command_line.update(vars(parser.parse_known_args()[0]))
|
||||||
|
|
||||||
if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]:
|
if parsed_command_line["single_instance"]:
|
||||||
Logger.log("i", "Checking for the presence of an ready running Cura instance.")
|
Logger.log("i", "Checking for the presence of an ready running Cura instance.")
|
||||||
single_instance_socket = QLocalSocket()
|
single_instance_socket = QLocalSocket()
|
||||||
Logger.log("d", "preStartUp(): full server name: " + single_instance_socket.fullServerName())
|
Logger.log("d", "preStartUp(): full server name: " + single_instance_socket.fullServerName())
|
||||||
|
|
@ -657,21 +635,36 @@ class CuraApplication(QtApplication):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def preRun(self):
|
||||||
|
# Last check for unknown commandline arguments
|
||||||
|
parser = self.getCommandlineParser()
|
||||||
|
parser.add_argument("--help", "-h",
|
||||||
|
action='store_true',
|
||||||
|
default = False,
|
||||||
|
help = "Show this help message and exit."
|
||||||
|
)
|
||||||
|
parsed_args = vars(parser.parse_args()) # This won't allow unknown arguments
|
||||||
|
if parsed_args["help"]:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
self.preRun()
|
||||||
|
|
||||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
|
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
|
||||||
|
|
||||||
self._setUpSingleInstanceServer()
|
self._setUpSingleInstanceServer()
|
||||||
|
|
||||||
controller = self.getController()
|
controller = self.getController()
|
||||||
|
|
||||||
|
controller.setActiveStage("PrepareStage")
|
||||||
controller.setActiveView("SolidView")
|
controller.setActiveView("SolidView")
|
||||||
|
|
||||||
controller.setCameraTool("CameraTool")
|
controller.setCameraTool("CameraTool")
|
||||||
controller.setSelectionTool("SelectionTool")
|
controller.setSelectionTool("SelectionTool")
|
||||||
|
|
||||||
t = controller.getTool("TranslateTool")
|
t = controller.getTool("TranslateTool")
|
||||||
if t:
|
if t:
|
||||||
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis])
|
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
|
||||||
|
|
||||||
Selection.selectionChanged.connect(self.onSelectionChanged)
|
Selection.selectionChanged.connect(self.onSelectionChanged)
|
||||||
|
|
||||||
|
|
@ -705,18 +698,27 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
|
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
|
||||||
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
||||||
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
|
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
|
||||||
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
|
|
||||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
|
||||||
|
self.getSettingInheritanceManager)
|
||||||
|
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager",
|
||||||
|
self.getSimpleModeSettingsManager)
|
||||||
|
|
||||||
|
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
|
||||||
|
qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
|
||||||
|
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
|
||||||
|
|
||||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||||
|
|
||||||
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
|
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
|
||||||
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
||||||
|
|
||||||
run_headless = self.getCommandLineOption("headless", False)
|
run_without_gui = self.getCommandLineOption("headless", False)
|
||||||
if not run_headless:
|
if not run_without_gui:
|
||||||
self.initializeEngine()
|
self.initializeEngine()
|
||||||
|
controller.setActiveStage("PrepareStage")
|
||||||
|
|
||||||
if run_headless or self._engine.rootObjects:
|
if run_without_gui or self._engine.rootObjects:
|
||||||
self.closeSplash()
|
self.closeSplash()
|
||||||
|
|
||||||
for file_name in self.getCommandLineOption("file", []):
|
for file_name in self.getCommandLineOption("file", []):
|
||||||
|
|
@ -728,7 +730,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def getMachineManager(self, *args):
|
def getMachineManager(self, *args) -> MachineManager:
|
||||||
if self._machine_manager is None:
|
if self._machine_manager is None:
|
||||||
self._machine_manager = MachineManager.createMachineManager()
|
self._machine_manager = MachineManager.createMachineManager()
|
||||||
return self._machine_manager
|
return self._machine_manager
|
||||||
|
|
@ -743,6 +745,22 @@ class CuraApplication(QtApplication):
|
||||||
self._material_manager = MaterialManager.createMaterialManager()
|
self._material_manager = MaterialManager.createMaterialManager()
|
||||||
return self._material_manager
|
return self._material_manager
|
||||||
|
|
||||||
|
def getObjectsModel(self, *args):
|
||||||
|
if self._object_manager is None:
|
||||||
|
self._object_manager = ObjectsModel.createObjectsModel()
|
||||||
|
return self._object_manager
|
||||||
|
|
||||||
|
def getBuildPlateModel(self, *args):
|
||||||
|
if self._build_plate_model is None:
|
||||||
|
self._build_plate_model = BuildPlateModel.createBuildPlateModel()
|
||||||
|
|
||||||
|
return self._build_plate_model
|
||||||
|
|
||||||
|
def getCuraSceneController(self, *args):
|
||||||
|
if self._cura_scene_controller is None:
|
||||||
|
self._cura_scene_controller = CuraSceneController.createCuraSceneController()
|
||||||
|
return self._cura_scene_controller
|
||||||
|
|
||||||
def getSettingInheritanceManager(self, *args):
|
def getSettingInheritanceManager(self, *args):
|
||||||
if self._setting_inheritance_manager is None:
|
if self._setting_inheritance_manager is None:
|
||||||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||||
|
|
@ -787,6 +805,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||||
|
|
||||||
|
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||||
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||||
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
|
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
|
||||||
|
|
@ -873,12 +892,18 @@ class CuraApplication(QtApplication):
|
||||||
def getSceneBoundingBoxString(self):
|
def getSceneBoundingBoxString(self):
|
||||||
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
|
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
|
||||||
|
|
||||||
|
## Update scene bounding box for current build plate
|
||||||
def updatePlatformActivity(self, node = None):
|
def updatePlatformActivity(self, node = None):
|
||||||
count = 0
|
count = 0
|
||||||
scene_bounding_box = None
|
scene_bounding_box = None
|
||||||
is_block_slicing_node = False
|
is_block_slicing_node = False
|
||||||
|
active_build_plate = self.getBuildPlateModel().activeBuildPlate
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")):
|
if (
|
||||||
|
not issubclass(type(node), CuraSceneNode) or
|
||||||
|
(not node.getMeshData() and not node.callDecoration("getLayerData")) or
|
||||||
|
(node.callDecoration("getBuildPlateNumber") != active_build_plate)):
|
||||||
|
|
||||||
continue
|
continue
|
||||||
if node.callDecoration("isBlockSlicing"):
|
if node.callDecoration("isBlockSlicing"):
|
||||||
is_block_slicing_node = True
|
is_block_slicing_node = True
|
||||||
|
|
@ -898,7 +923,7 @@ class CuraApplication(QtApplication):
|
||||||
if not scene_bounding_box:
|
if not scene_bounding_box:
|
||||||
scene_bounding_box = AxisAlignedBox.Null
|
scene_bounding_box = AxisAlignedBox.Null
|
||||||
|
|
||||||
if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid():
|
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
|
||||||
self._scene_bounding_box = scene_bounding_box
|
self._scene_bounding_box = scene_bounding_box
|
||||||
self.sceneBoundingBoxChanged.emit()
|
self.sceneBoundingBoxChanged.emit()
|
||||||
|
|
||||||
|
|
@ -995,7 +1020,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
Selection.clear()
|
Selection.clear()
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
|
@ -1003,6 +1028,9 @@ class CuraApplication(QtApplication):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
Selection.add(node)
|
Selection.add(node)
|
||||||
|
|
||||||
## Delete all nodes containing mesh data in the scene.
|
## Delete all nodes containing mesh data in the scene.
|
||||||
|
|
@ -1014,10 +1042,12 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
if not node.isSelectable():
|
||||||
|
continue # Only remove nodes that are selectable.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
@ -1030,13 +1060,18 @@ class CuraApplication(QtApplication):
|
||||||
op.push()
|
op.push()
|
||||||
Selection.clear()
|
Selection.clear()
|
||||||
|
|
||||||
|
Logger.log("i", "Reseting print information")
|
||||||
|
self._print_information = PrintInformation.PrintInformation()
|
||||||
|
|
||||||
|
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||||
|
|
||||||
## Reset all translation on nodes with mesh data.
|
## Reset all translation on nodes with mesh data.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def resetAllTranslation(self):
|
def resetAllTranslation(self):
|
||||||
Logger.log("i", "Resetting all scene translations")
|
Logger.log("i", "Resetting all scene translations")
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
|
@ -1064,13 +1099,13 @@ class CuraApplication(QtApplication):
|
||||||
Logger.log("i", "Resetting all scene transformations")
|
Logger.log("i", "Resetting all scene transformations")
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.isSelectable():
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
||||||
|
|
@ -1088,10 +1123,31 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
## Arrange all objects.
|
## Arrange all objects.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeAll(self):
|
def arrangeObjectsToAllBuildPlates(self):
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
|
continue
|
||||||
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
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 # i.e. node with layer data
|
||||||
|
# Skip nodes that are too big
|
||||||
|
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||||
|
nodes.append(node)
|
||||||
|
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
||||||
|
job.start()
|
||||||
|
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||||
|
|
||||||
|
# Single build plate
|
||||||
|
@pyqtSlot()
|
||||||
|
def arrangeAll(self):
|
||||||
|
nodes = []
|
||||||
|
active_build_plate = self.getBuildPlateModel().activeBuildPlate
|
||||||
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
|
@ -1099,9 +1155,12 @@ class CuraApplication(QtApplication):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
# Skip nodes that are too big
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
continue # i.e. node with layer data
|
||||||
nodes.append(node)
|
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||||
|
# Skip nodes that are too big
|
||||||
|
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||||
|
nodes.append(node)
|
||||||
self.arrange(nodes, fixed_nodes = [])
|
self.arrange(nodes, fixed_nodes = [])
|
||||||
|
|
||||||
## Arrange Selection
|
## Arrange Selection
|
||||||
|
|
@ -1112,7 +1171,7 @@ class CuraApplication(QtApplication):
|
||||||
# What nodes are on the build plate and are not being moved
|
# What nodes are on the build plate and are not being moved
|
||||||
fixed_nodes = []
|
fixed_nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode:
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
|
|
@ -1120,6 +1179,8 @@ class CuraApplication(QtApplication):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
|
continue # i.e. node with layer data
|
||||||
if node in nodes: # exclude selected node from fixed_nodes
|
if node in nodes: # exclude selected node from fixed_nodes
|
||||||
continue
|
continue
|
||||||
fixed_nodes.append(node)
|
fixed_nodes.append(node)
|
||||||
|
|
@ -1138,7 +1199,7 @@ class CuraApplication(QtApplication):
|
||||||
Logger.log("i", "Reloading all loaded mesh data.")
|
Logger.log("i", "Reloading all loaded mesh data.")
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if type(node) is not SceneNode or not node.getMeshData():
|
if not isinstance(node, SceneNode) or not node.getMeshData():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
@ -1229,10 +1290,11 @@ class CuraApplication(QtApplication):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def groupSelected(self):
|
def groupSelected(self):
|
||||||
# Create a group-node
|
# Create a group-node
|
||||||
group_node = SceneNode()
|
group_node = CuraSceneNode()
|
||||||
group_decorator = GroupDecorator()
|
group_decorator = GroupDecorator()
|
||||||
group_node.addDecorator(group_decorator)
|
group_node.addDecorator(group_decorator)
|
||||||
group_node.addDecorator(ConvexHullDecorator())
|
group_node.addDecorator(ConvexHullDecorator())
|
||||||
|
group_node.addDecorator(BuildPlateDecorator(self.getBuildPlateModel().activeBuildPlate))
|
||||||
group_node.setParent(self.getController().getScene().getRoot())
|
group_node.setParent(self.getController().getScene().getRoot())
|
||||||
group_node.setSelectable(True)
|
group_node.setSelectable(True)
|
||||||
center = Selection.getSelectionCenter()
|
center = Selection.getSelectionCenter()
|
||||||
|
|
@ -1372,13 +1434,26 @@ class CuraApplication(QtApplication):
|
||||||
filename = job.getFileName()
|
filename = job.getFileName()
|
||||||
self._currently_loading_files.remove(filename)
|
self._currently_loading_files.remove(filename)
|
||||||
|
|
||||||
|
self.fileLoaded.emit(filename)
|
||||||
|
arrange_objects_on_load = (
|
||||||
|
not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
|
||||||
|
not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load"))
|
||||||
|
target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
|
||||||
|
|
||||||
root = self.getController().getScene().getRoot()
|
root = self.getController().getScene().getRoot()
|
||||||
arranger = Arrange.create(scene_root = root)
|
fixed_nodes = []
|
||||||
|
for node_ in DepthFirstIterator(root):
|
||||||
|
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||||
|
fixed_nodes.append(node_)
|
||||||
|
arranger = Arrange.create(fixed_nodes = fixed_nodes)
|
||||||
min_offset = 8
|
min_offset = 8
|
||||||
|
|
||||||
self.fileLoaded.emit(filename)
|
for original_node in nodes:
|
||||||
|
|
||||||
|
# Create a CuraSceneNode just if the original node is not that type
|
||||||
|
node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode()
|
||||||
|
node.setMeshData(original_node.getMeshData())
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
node.setSelectable(True)
|
node.setSelectable(True)
|
||||||
node.setName(os.path.basename(filename))
|
node.setName(os.path.basename(filename))
|
||||||
|
|
||||||
|
|
@ -1405,20 +1480,30 @@ class CuraApplication(QtApplication):
|
||||||
if not child.getDecorator(ConvexHullDecorator):
|
if not child.getDecorator(ConvexHullDecorator):
|
||||||
child.addDecorator(ConvexHullDecorator())
|
child.addDecorator(ConvexHullDecorator())
|
||||||
|
|
||||||
if node.callDecoration("isSliceable"):
|
if arrange_objects_on_load:
|
||||||
# Only check position if it's not already blatantly obvious that it won't fit.
|
if node.callDecoration("isSliceable"):
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
# Only check position if it's not already blatantly obvious that it won't fit.
|
||||||
# Find node location
|
if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
|
# Find node location
|
||||||
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
|
||||||
|
|
||||||
# If a model is to small then it will not contain any points
|
# If a model is to small then it will not contain any points
|
||||||
if offset_shape_arr is None and hull_shape_arr is None:
|
if offset_shape_arr is None and hull_shape_arr is None:
|
||||||
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
|
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
|
||||||
title=self._i18n_catalog.i18nc("@info:title", "Warning")).show()
|
title=self._i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
|
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
|
||||||
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
|
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
|
||||||
|
|
||||||
|
# This node is deepcopied from some other node which already has a BuildPlateDecorator, but the deepcopy
|
||||||
|
# of BuildPlateDecorator produces one that's assoicated with build plate -1. So, here we need to check if
|
||||||
|
# the BuildPlateDecorator exists or not and always set the correct build plate number.
|
||||||
|
build_plate_decorator = node.getDecorator(BuildPlateDecorator)
|
||||||
|
if build_plate_decorator is None:
|
||||||
|
build_plate_decorator = BuildPlateDecorator(target_build_plate)
|
||||||
|
node.addDecorator(build_plate_decorator)
|
||||||
|
build_plate_decorator.setBuildPlateNumber(target_build_plate)
|
||||||
|
|
||||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
op = AddSceneNodeOperation(node, scene.getRoot())
|
||||||
op.push()
|
op.push()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
63
cura/ObjectsModel.py
Normal file
63
cura/ObjectsModel.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"name": name,
|
||||||
|
"isSelected": Selection.isSelected(node),
|
||||||
|
"isOutsideBuildArea": node.isOutsideBuildArea(),
|
||||||
|
"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()
|
||||||
|
|
@ -18,7 +18,7 @@ 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 isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.callDecoration("getConvexHull"):
|
if node.callDecoration("getConvexHull"):
|
||||||
|
|
|
||||||
29
cura/Operations/SetBuildPlateNumberOperation.py
Normal file
29
cura/Operations/SetBuildPlateNumberOperation.py
Normal 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)
|
||||||
0
cura/Operations/__init__.py
Normal file
0
cura/Operations/__init__.py
Normal 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()
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ from UM.Preferences import Preferences
|
||||||
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 typing import Dict
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import os.path
|
import os.path
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import json
|
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")
|
||||||
|
|
@ -53,10 +55,10 @@ 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
|
||||||
|
|
||||||
|
|
@ -67,10 +69,15 @@ class PrintInformation(QObject):
|
||||||
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)
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._updateJobName)
|
Application.getInstance().globalContainerStackChanged.connect(self._updateJobName)
|
||||||
Application.getInstance().fileLoaded.connect(self.setBaseName)
|
Application.getInstance().fileLoaded.connect(self.setBaseName)
|
||||||
|
Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||||
Application.getInstance().workspaceLoaded.connect(self.setProjectName)
|
Application.getInstance().workspaceLoaded.connect(self.setProjectName)
|
||||||
|
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._active_material_container = None
|
self._active_material_container = None
|
||||||
|
|
@ -82,7 +89,7 @@ class PrintInformation(QObject):
|
||||||
# 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"),
|
||||||
|
|
@ -100,10 +107,26 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
self._print_time_message_values = {}
|
self._print_time_message_values = {}
|
||||||
|
|
||||||
# Full fill message values using keys from _print_time_message_translations
|
|
||||||
for key in self._print_time_message_translations.keys():
|
|
||||||
self._print_time_message_values[key] = Duration(None, self)
|
|
||||||
|
|
||||||
|
def _initPrintTimeMessageValues(self, build_plate_number):
|
||||||
|
# Full fill message values using keys from _print_time_message_translations
|
||||||
|
self._print_time_message_values[build_plate_number] = {}
|
||||||
|
for key in self._print_time_message_translations.keys():
|
||||||
|
self._print_time_message_values[build_plate_number][key] = Duration(None, self)
|
||||||
|
|
||||||
|
def _initVariablesWithBuildPlate(self, build_plate_number):
|
||||||
|
if build_plate_number not in self._print_time_message_values:
|
||||||
|
self._initPrintTimeMessageValues(build_plate_number)
|
||||||
|
if self._active_build_plate not in self._material_lengths:
|
||||||
|
self._material_lengths[self._active_build_plate] = []
|
||||||
|
if self._active_build_plate not in self._material_weights:
|
||||||
|
self._material_weights[self._active_build_plate] = []
|
||||||
|
if self._active_build_plate not in self._material_costs:
|
||||||
|
self._material_costs[self._active_build_plate] = []
|
||||||
|
if self._active_build_plate not in self._material_names:
|
||||||
|
self._material_names[self._active_build_plate] = []
|
||||||
|
if self._active_build_plate not in self._current_print_time:
|
||||||
|
self._current_print_time[self._active_build_plate] = Duration(None, self)
|
||||||
|
|
||||||
currentPrintTimeChanged = pyqtSignal()
|
currentPrintTimeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
|
@ -119,64 +142,71 @@ 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, material_amounts):
|
||||||
|
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:
|
if Application.getInstance().getGlobalContainerStack() is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Material amount is sent as an amount of mm^3, so calculate length from that
|
# Material amount is sent as an amount of mm^3, so calculate length from that
|
||||||
radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
||||||
self._material_lengths = []
|
self._material_lengths[build_plate_number] = []
|
||||||
self._material_weights = []
|
self._material_weights[build_plate_number] = []
|
||||||
self._material_costs = []
|
self._material_costs[build_plate_number] = []
|
||||||
self._material_names = []
|
self._material_names[build_plate_number] = []
|
||||||
|
|
||||||
material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
|
material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
|
||||||
|
|
||||||
|
|
@ -214,10 +244,10 @@ class PrintInformation(QObject):
|
||||||
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()
|
||||||
|
|
@ -228,7 +258,8 @@ class PrintInformation(QObject):
|
||||||
if preference != "cura/material_settings":
|
if preference != "cura/material_settings":
|
||||||
return
|
return
|
||||||
|
|
||||||
self._calculateInformation()
|
for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1):
|
||||||
|
self._calculateInformation(build_plate_number)
|
||||||
|
|
||||||
def _onActiveMaterialChanged(self):
|
def _onActiveMaterialChanged(self):
|
||||||
if self._active_material_container:
|
if self._active_material_container:
|
||||||
|
|
@ -238,14 +269,28 @@ class PrintInformation(QObject):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
active_material_id = Application.getInstance().getMachineManager().activeMaterialId
|
active_material_id = Application.getInstance().getMachineManager().activeMaterialId
|
||||||
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
|
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id = active_material_id)
|
||||||
|
|
||||||
if active_material_containers:
|
if active_material_containers:
|
||||||
self._active_material_container = active_material_containers[0]
|
self._active_material_container = active_material_containers[0]
|
||||||
self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
|
self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
|
||||||
|
|
||||||
|
def _onActiveBuildPlateChanged(self):
|
||||||
|
new_active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||||
|
if new_active_build_plate != self._active_build_plate:
|
||||||
|
self._active_build_plate = new_active_build_plate
|
||||||
|
|
||||||
|
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||||
|
|
||||||
|
self.materialLengthsChanged.emit()
|
||||||
|
self.materialWeightsChanged.emit()
|
||||||
|
self.materialCostsChanged.emit()
|
||||||
|
self.materialNamesChanged.emit()
|
||||||
|
self.currentPrintTimeChanged.emit()
|
||||||
|
|
||||||
def _onMaterialMetaDataChanged(self, *args, **kwargs):
|
def _onMaterialMetaDataChanged(self, *args, **kwargs):
|
||||||
self._calculateInformation()
|
for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1):
|
||||||
|
self._calculateInformation(build_plate_number)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setJobName(self, name):
|
def setJobName(self, name):
|
||||||
|
|
@ -316,15 +361,14 @@ class PrintInformation(QObject):
|
||||||
return
|
return
|
||||||
|
|
||||||
global_stack_name = global_container_stack.getName()
|
global_stack_name = global_container_stack.getName()
|
||||||
split_name = global_stack_name.split(" ")
|
|
||||||
abbr_machine = ""
|
abbr_machine = ""
|
||||||
for word in split_name:
|
for word in re.findall(r"[\w']+", global_stack_name):
|
||||||
if word.lower() == "ultimaker":
|
if word.lower() == "ultimaker":
|
||||||
abbr_machine += "UM"
|
abbr_machine += "UM"
|
||||||
elif word.isdigit():
|
elif word.isdigit():
|
||||||
abbr_machine += word
|
abbr_machine += word
|
||||||
else:
|
else:
|
||||||
stripped_word = self._stripAccents(word.strip("()[]{}#").upper())
|
stripped_word = self._stripAccents(word.upper())
|
||||||
# - use only the first character if the word is too long (> 3 characters)
|
# - use only the first character if the word is too long (> 3 characters)
|
||||||
# - use the whole word if it's not too long (<= 3 characters)
|
# - use the whole word if it's not too long (<= 3 characters)
|
||||||
if len(stripped_word) > 3:
|
if len(stripped_word) > 3:
|
||||||
|
|
@ -340,7 +384,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 +394,12 @@ 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_number):
|
||||||
temp_message = {}
|
temp_message = {}
|
||||||
for key in self._print_time_message_values.keys():
|
if build_plate_number not in self._print_time_message_values:
|
||||||
|
self._print_time_message_values[build_plate_number] = {}
|
||||||
|
for key in self._print_time_message_values[build_plate_number].keys():
|
||||||
temp_message[key] = 0
|
temp_message[key] = 0
|
||||||
|
|
||||||
temp_material_amounts = [0]
|
temp_material_amounts = [0]
|
||||||
self._onPrintDurationMessage(temp_message, temp_material_amounts)
|
self._onPrintDurationMessage(build_plate_number, temp_message, temp_material_amounts)
|
||||||
|
|
|
||||||
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal file
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal 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()
|
||||||
34
cura/PrinterOutput/MaterialOutputModel.py
Normal file
34
cura/PrinterOutput/MaterialOutputModel.py
Normal 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
|
||||||
119
cura/PrinterOutput/NetworkCamera.py
Normal file
119
cura/PrinterOutput/NetworkCamera.py
Normal 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()
|
||||||
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal file
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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
|
||||||
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal file
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal 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)
|
||||||
46
cura/PrinterOutput/PrinterOutputController.py
Normal file
46
cura/PrinterOutput/PrinterOutputController.py
Normal 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")
|
||||||
240
cura/PrinterOutput/PrinterOutputModel.py
Normal file
240
cura/PrinterOutput/PrinterOutputModel.py
Normal 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
|
||||||
0
cura/PrinterOutput/__init__.py
Normal file
0
cura/PrinterOutput/__init__.py
Normal 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,150 @@ 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.findInstanceContainers(type = "material", GUID = material_id)
|
|
||||||
if containers:
|
|
||||||
result.append(containers[0].getName())
|
|
||||||
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.findInstanceContainers(type = "material", GUID = material_id)
|
|
||||||
if containers:
|
|
||||||
result.append(containers[0].getMetaDataEntry("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.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
# Copyright (c) 2016 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.
|
||||||
|
|
||||||
# This collects a lot of quality and quality changes related code which was split between ContainerManager
|
# 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.
|
# and the MachineManager and really needs to usable from both.
|
||||||
from typing import List, Optional, Dict, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
|
@ -33,16 +32,16 @@ class QualityManager:
|
||||||
# \param quality_name
|
# \param quality_name
|
||||||
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
|
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
|
||||||
# specified then the currently selected machine definition is used.
|
# specified then the currently selected machine definition is used.
|
||||||
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
# \param material_containers_metadata If nothing is specified then the
|
||||||
# the current set of selected materials is used.
|
# current set of selected materials is used.
|
||||||
# \return the matching quality container \type{InstanceContainer}
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[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}
|
criteria = {"type": "quality", "name": quality_name}
|
||||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
|
||||||
|
|
||||||
# Fall back to using generic materials and qualities if nothing could be found.
|
# Fall back to using generic materials and qualities if nothing could be found.
|
||||||
if not result and material_containers and len(material_containers) == 1:
|
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
|
||||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
|
||||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
|
|
@ -108,18 +107,18 @@ class QualityManager:
|
||||||
# \param quality_type \type{str} the name of the quality type to search for.
|
# \param quality_type \type{str} the name of the quality type to search for.
|
||||||
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
|
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
|
||||||
# specified then the currently selected machine definition is used.
|
# specified then the currently selected machine definition is used.
|
||||||
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
# \param material_containers_metadata If nothing is specified then the
|
||||||
# the current set of selected materials is used.
|
# current set of selected materials is used.
|
||||||
# \return the matching quality container \type{InstanceContainer}
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> 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 = kwargs
|
||||||
criteria["type"] = "quality"
|
criteria["type"] = "quality"
|
||||||
if quality_type:
|
if quality_type:
|
||||||
criteria["quality_type"] = quality_type
|
criteria["quality_type"] = quality_type
|
||||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
|
||||||
# Fall back to using generic materials and qualities if nothing could be found.
|
# Fall back to using generic materials and qualities if nothing could be found.
|
||||||
if not result and material_containers and len(material_containers) == 1:
|
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
|
||||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
|
||||||
if basic_materials:
|
if basic_materials:
|
||||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
|
|
@ -131,12 +130,15 @@ class QualityManager:
|
||||||
# \return \type{List[InstanceContainer]} the list of suitable qualities.
|
# \return \type{List[InstanceContainer]} the list of suitable qualities.
|
||||||
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
|
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
|
||||||
criteria = {"type": "quality"}
|
criteria = {"type": "quality"}
|
||||||
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, [material_container.getMetaData()], **criteria)
|
||||||
if not result:
|
if not result:
|
||||||
basic_materials = self._getBasicMaterials(material_container)
|
basic_materials = self._getBasicMaterialMetadatas(material_container.getMetaData())
|
||||||
if basic_materials:
|
if basic_materials:
|
||||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
|
empty_quality = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
|
||||||
|
result.append(empty_quality)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Find all quality changes for a machine.
|
## Find all quality changes for a machine.
|
||||||
|
|
@ -202,22 +204,34 @@ class QualityManager:
|
||||||
## Fetch more basic versions of a material.
|
## Fetch more basic versions of a material.
|
||||||
#
|
#
|
||||||
# This tries to find a generic or basic version of the given material.
|
# This tries to find a generic or basic version of the given material.
|
||||||
# \param material_container \type{InstanceContainer} the material
|
# \param material_container \type{Dict[str, Any]} The metadata of a
|
||||||
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
|
# material to find the basic versions of.
|
||||||
def _getBasicMaterials(self, material_container: InstanceContainer):
|
# \return \type{List[Dict[str, Any]]} A list of the metadata of basic
|
||||||
base_material = material_container.getMetaDataEntry("material")
|
# materials, or an empty list if none could be found.
|
||||||
material_container_definition = material_container.getDefinition()
|
def _getBasicMaterialMetadatas(self, material_container: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
if "definition" not in material_container:
|
||||||
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
|
||||||
else:
|
|
||||||
definition_id = "fdmprinter"
|
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:
|
if base_material:
|
||||||
# There is a basic material specified
|
# There is a basic material specified
|
||||||
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
criteria = {
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
"type": "material",
|
||||||
containers = [basic_material for basic_material in containers if
|
"name": base_material,
|
||||||
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
|
"definition": definition_id,
|
||||||
"variant")]
|
"variant": material_container.get("variant")
|
||||||
|
}
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
|
||||||
return containers
|
return containers
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
@ -225,29 +239,25 @@ class QualityManager:
|
||||||
def _getFilteredContainers(self, **kwargs):
|
def _getFilteredContainers(self, **kwargs):
|
||||||
return self._getFilteredContainersForStack(None, None, **kwargs)
|
return self._getFilteredContainersForStack(None, None, **kwargs)
|
||||||
|
|
||||||
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
|
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
||||||
# Fill in any default values.
|
# Fill in any default values.
|
||||||
if machine_definition is None:
|
if machine_definition is None:
|
||||||
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
if quality_definition_id is not None:
|
if quality_definition_id is not None:
|
||||||
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition_id)[0]
|
||||||
|
|
||||||
# for convenience
|
if not material_metadata:
|
||||||
if material_containers is None:
|
|
||||||
material_containers = []
|
|
||||||
|
|
||||||
if not material_containers:
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
if active_stacks:
|
if active_stacks:
|
||||||
material_containers = [stack.material for stack in active_stacks]
|
material_metadata = [stack.material.getMetaData() for stack in active_stacks]
|
||||||
|
|
||||||
criteria = kwargs
|
criteria = kwargs
|
||||||
filter_by_material = False
|
filter_by_material = False
|
||||||
|
|
||||||
machine_definition = self.getParentMachineDefinition(machine_definition)
|
machine_definition = self.getParentMachineDefinition(machine_definition)
|
||||||
criteria["definition"] = machine_definition.getId()
|
criteria["definition"] = machine_definition.getId()
|
||||||
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
|
||||||
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
|
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
|
||||||
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
||||||
|
|
@ -261,12 +271,12 @@ class QualityManager:
|
||||||
# Stick the material IDs in a set
|
# Stick the material IDs in a set
|
||||||
material_ids = set()
|
material_ids = set()
|
||||||
|
|
||||||
for material_instance in material_containers:
|
for material_instance in material_metadata:
|
||||||
if material_instance is not None:
|
if material_instance is not None:
|
||||||
# Add the parent material too.
|
# Add the parent material too.
|
||||||
for basic_material in self._getBasicMaterials(material_instance):
|
for basic_material in self._getBasicMaterialMetadatas(material_instance):
|
||||||
material_ids.add(basic_material.getId())
|
material_ids.add(basic_material["id"])
|
||||||
material_ids.add(material_instance.getId())
|
material_ids.add(material_instance["id"])
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -292,13 +302,13 @@ class QualityManager:
|
||||||
# We have a normal (whole) machine defintion
|
# We have a normal (whole) machine defintion
|
||||||
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
if quality_definition is not None:
|
if quality_definition is not None:
|
||||||
parent_machine_definition = container_registry.findDefinitionContainers(id=quality_definition)[0]
|
parent_machine_definition = container_registry.findDefinitionContainers(id = quality_definition)[0]
|
||||||
return self.getParentMachineDefinition(parent_machine_definition)
|
return self.getParentMachineDefinition(parent_machine_definition)
|
||||||
else:
|
else:
|
||||||
return machine_definition
|
return machine_definition
|
||||||
else:
|
else:
|
||||||
# This looks like an extruder. Find the rest of the machine.
|
# This looks like an extruder. Find the rest of the machine.
|
||||||
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
|
||||||
parent_machine = self.getParentMachineDefinition(whole_machine)
|
parent_machine = self.getParentMachineDefinition(whole_machine)
|
||||||
if whole_machine is parent_machine:
|
if whole_machine is parent_machine:
|
||||||
# This extruder already belongs to a 'parent' machine def.
|
# This extruder already belongs to a 'parent' machine def.
|
||||||
|
|
@ -307,7 +317,7 @@ class QualityManager:
|
||||||
# Look up the corresponding extruder definition in the parent machine definition.
|
# Look up the corresponding extruder definition in the parent machine definition.
|
||||||
extruder_position = machine_definition.getMetaDataEntry("position")
|
extruder_position = machine_definition.getMetaDataEntry("position")
|
||||||
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
|
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
|
||||||
return container_registry.findDefinitionContainers(id=parent_extruder_id)[0]
|
return container_registry.findDefinitionContainers(id = parent_extruder_id)[0]
|
||||||
|
|
||||||
## Get the whole/global machine definition from an extruder definition.
|
## Get the whole/global machine definition from an extruder definition.
|
||||||
#
|
#
|
||||||
|
|
@ -321,5 +331,5 @@ class QualityManager:
|
||||||
return machine_definition
|
return machine_definition
|
||||||
else:
|
else:
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
|
||||||
return whole_machine
|
return whole_machine
|
||||||
|
|
|
||||||
26
cura/Scene/BuildPlateDecorator.py
Normal file
26
cura/Scene/BuildPlateDecorator.py
Normal 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()
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,7 +65,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().getBuildPlateModel().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)
|
||||||
113
cura/Scene/CuraSceneController.py
Normal file
113
cura/Scene/CuraSceneController.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from cura.ObjectsModel import ObjectsModel
|
||||||
|
from cura.BuildPlateModel import BuildPlateModel
|
||||||
|
|
||||||
|
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, build_plate_model: BuildPlateModel):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._objects_model = objects_model
|
||||||
|
self._build_plate_model = 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._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._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")
|
||||||
|
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._build_plate_model.setActiveBuildPlate(nr)
|
||||||
|
self._objects_model.setActiveBuildPlate(nr)
|
||||||
|
self.activeBuildPlateChanged.emit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def createCuraSceneController():
|
||||||
|
objects_model = Application.getInstance().getObjectsModel()
|
||||||
|
build_plate_model = Application.getInstance().getBuildPlateModel()
|
||||||
|
return CuraSceneController(objects_model = objects_model, build_plate_model = build_plate_model)
|
||||||
43
cura/Scene/CuraSceneNode.py
Normal file
43
cura/Scene/CuraSceneNode.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
## 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 = True
|
||||||
|
|
||||||
|
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().getBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
|
def isSelectable(self) -> bool:
|
||||||
|
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
|
## 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
0
cura/Scene/__init__.py
Normal file
|
|
@ -1,10 +1,11 @@
|
||||||
# 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
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Union
|
from typing import Any, Dict, List, 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
|
||||||
|
|
@ -55,13 +56,24 @@ class ContainerManager(QObject):
|
||||||
# \return The ID of the new container, or an empty string if duplication failed.
|
# \return The ID of the new container, or an empty string if duplication failed.
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def duplicateContainer(self, container_id):
|
def duplicateContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = 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:
|
if not containers:
|
||||||
Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
|
Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
container = containers[0]
|
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_container = None
|
||||||
new_name = self._container_registry.uniqueName(container.getName())
|
new_name = self._container_registry.uniqueName(container.getName())
|
||||||
# Only InstanceContainer has a duplicate method at the moment.
|
# Only InstanceContainer has a duplicate method at the moment.
|
||||||
|
|
@ -73,10 +85,11 @@ class ContainerManager(QObject):
|
||||||
new_container.deserialize(container.serialize())
|
new_container.deserialize(container.serialize())
|
||||||
new_container.setName(new_name)
|
new_container.setName(new_name)
|
||||||
|
|
||||||
|
# TODO: we probably don't want to add it to the registry here!
|
||||||
if new_container:
|
if new_container:
|
||||||
self._container_registry.addContainer(new_container)
|
self._container_registry.addContainer(new_container)
|
||||||
|
|
||||||
return new_container.getId()
|
return new_container
|
||||||
|
|
||||||
## Change the name of a specified container to a new name.
|
## Change the name of a specified container to a new name.
|
||||||
#
|
#
|
||||||
|
|
@ -87,14 +100,14 @@ class ContainerManager(QObject):
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
@pyqtSlot(str, str, str, result = bool)
|
@pyqtSlot(str, str, str, result = bool)
|
||||||
def renameContainer(self, container_id, new_id, new_name):
|
def renameContainer(self, container_id, new_id, new_name):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could rename container %s because it was not found.", container_id)
|
Logger.log("w", "Could rename container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
# First, remove the container from the registry. This will clean up any files related to the container.
|
# First, remove the container from the registry. This will clean up any files related to the container.
|
||||||
self._container_registry.removeContainer(container)
|
self._container_registry.removeContainer(container_id)
|
||||||
|
|
||||||
# Ensure we have a unique name for the container
|
# Ensure we have a unique name for the container
|
||||||
new_name = self._container_registry.uniqueName(new_name)
|
new_name = self._container_registry.uniqueName(new_name)
|
||||||
|
|
@ -115,9 +128,9 @@ class ContainerManager(QObject):
|
||||||
# \return True if the container was successfully removed, False if not.
|
# \return True if the container was successfully removed, False if not.
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def removeContainer(self, container_id):
|
def removeContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could remove container %s because it was not found.", container_id)
|
Logger.log("w", "Could not remove container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._container_registry.removeContainer(containers[0].getId())
|
self._container_registry.removeContainer(containers[0].getId())
|
||||||
|
|
@ -135,14 +148,14 @@ class ContainerManager(QObject):
|
||||||
# \return True if successfully merged, False if not.
|
# \return True if successfully merged, False if not.
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def mergeContainers(self, merge_into_id, merge_id):
|
def mergeContainers(self, merge_into_id, merge_id):
|
||||||
containers = self._container_registry.findContainers(None, id = merge_into_id)
|
containers = self._container_registry.findContainers(id = merge_into_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
|
Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
merge_into = containers[0]
|
merge_into = containers[0]
|
||||||
|
|
||||||
containers = self._container_registry.findContainers(None, id = merge_id)
|
containers = self._container_registry.findContainers(id = merge_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could not merge container %s because it was not found", merge_id)
|
Logger.log("w", "Could not merge container %s because it was not found", merge_id)
|
||||||
return False
|
return False
|
||||||
|
|
@ -164,13 +177,13 @@ class ContainerManager(QObject):
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def clearContainer(self, container_id):
|
def clearContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
if self._container_registry.isReadOnly(container_id):
|
||||||
if not containers:
|
Logger.log("w", "Cannot clear read-only container %s", container_id)
|
||||||
Logger.log("w", "Could clear container %s because it was not found.", container_id)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if containers[0].isReadOnly():
|
containers = self._container_registry.findContainers(id = container_id)
|
||||||
Logger.log("w", "Cannot clear read-only container %s", container_id)
|
if not containers:
|
||||||
|
Logger.log("w", "Could clear container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
containers[0].clear()
|
containers[0].clear()
|
||||||
|
|
@ -179,16 +192,12 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(str, str, result=str)
|
@pyqtSlot(str, str, result=str)
|
||||||
def getContainerMetaDataEntry(self, container_id, entry_name):
|
def getContainerMetaDataEntry(self, container_id, entry_name):
|
||||||
containers = self._container_registry.findContainers(None, id=container_id)
|
metadatas = self._container_registry.findContainersMetadata(id = container_id)
|
||||||
if not containers:
|
if not metadatas:
|
||||||
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
result = containers[0].getMetaDataEntry(entry_name)
|
return str(metadatas[0].get(entry_name, ""))
|
||||||
if result is not None:
|
|
||||||
return str(result)
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
## Set a metadata entry of the specified container.
|
## Set a metadata entry of the specified container.
|
||||||
#
|
#
|
||||||
|
|
@ -204,17 +213,17 @@ class ContainerManager(QObject):
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
@pyqtSlot(str, str, str, result = bool)
|
@pyqtSlot(str, str, str, result = bool)
|
||||||
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
|
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
if self._container_registry.isReadOnly(container_id):
|
||||||
|
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
|
||||||
|
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.
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
||||||
if container.isReadOnly():
|
|
||||||
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
entries = entry_name.split("/")
|
entries = entry_name.split("/")
|
||||||
entry_name = entries.pop()
|
entry_name = entries.pop()
|
||||||
|
|
||||||
|
|
@ -254,17 +263,17 @@ class ContainerManager(QObject):
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
@pyqtSlot(str, str, str, str, result = bool)
|
@pyqtSlot(str, str, str, str, result = bool)
|
||||||
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
|
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
if self._container_registry.isReadOnly(container_id):
|
||||||
|
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
containers = self._container_registry.findContainers(id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
||||||
if container.isReadOnly():
|
|
||||||
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
container.setProperty(setting_key, property_name, property_value)
|
container.setProperty(setting_key, property_name, property_value)
|
||||||
|
|
||||||
basefile = container.getMetaDataEntry("base_file", container_id)
|
basefile = container.getMetaDataEntry("base_file", container_id)
|
||||||
|
|
@ -300,35 +309,30 @@ class ContainerManager(QObject):
|
||||||
## Set the name of the specified container.
|
## Set the name of the specified container.
|
||||||
@pyqtSlot(str, str, result = bool)
|
@pyqtSlot(str, str, result = bool)
|
||||||
def setContainerName(self, container_id, new_name):
|
def setContainerName(self, container_id, new_name):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
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:
|
if not containers:
|
||||||
Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
containers[0].setName(new_name)
|
||||||
|
|
||||||
if container.isReadOnly():
|
|
||||||
Logger.log("w", "Cannot set name of read-only container %s.", container_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
container.setName(new_name)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
## Find instance containers matching certain criteria.
|
## Find instance containers matching certain criteria.
|
||||||
#
|
#
|
||||||
# This effectively forwards to ContainerRegistry::findInstanceContainers.
|
# This effectively forwards to
|
||||||
|
# ContainerRegistry::findInstanceContainersMetadata.
|
||||||
#
|
#
|
||||||
# \param criteria A dict of key - value pairs to search for.
|
# \param criteria A dict of key - value pairs to search for.
|
||||||
#
|
#
|
||||||
# \return A list of container IDs that match the given criteria.
|
# \return A list of container IDs that match the given criteria.
|
||||||
@pyqtSlot("QVariantMap", result = "QVariantList")
|
@pyqtSlot("QVariantMap", result = "QVariantList")
|
||||||
def findInstanceContainers(self, criteria):
|
def findInstanceContainers(self, criteria):
|
||||||
result = []
|
return [entry["id"] for entry in self._container_registry.findInstanceContainersMetadata(**criteria)]
|
||||||
for entry in self._container_registry.findInstanceContainers(**criteria):
|
|
||||||
result.append(entry.getId())
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def isContainerUsed(self, container_id):
|
def isContainerUsed(self, container_id):
|
||||||
|
|
@ -336,15 +340,17 @@ class ContainerManager(QObject):
|
||||||
# check if this is a material container. If so, check if any material with the same base is being used by any
|
# check if this is a material container. If so, check if any material with the same base is being used by any
|
||||||
# stacks.
|
# stacks.
|
||||||
container_ids_to_check = [container_id]
|
container_ids_to_check = [container_id]
|
||||||
container_results = self._container_registry.findInstanceContainers(id = container_id, type = "material")
|
container_results = self._container_registry.findInstanceContainersMetadata(id = container_id, type = "material")
|
||||||
if container_results:
|
if container_results:
|
||||||
this_container = container_results[0]
|
this_container = container_results[0]
|
||||||
material_base_file = this_container.getMetaDataEntry("base_file", this_container.getId())
|
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
|
# check all material container IDs with the same base
|
||||||
material_containers = self._container_registry.findInstanceContainers(base_file = material_base_file,
|
material_containers = self._container_registry.findInstanceContainersMetadata(base_file = material_base_file,
|
||||||
type = "material")
|
type = "material")
|
||||||
if material_containers:
|
if material_containers:
|
||||||
container_ids_to_check = [container.getId() for container in material_containers]
|
container_ids_to_check = [container["id"] for container in material_containers]
|
||||||
|
|
||||||
all_stacks = self._container_registry.findContainerStacks()
|
all_stacks = self._container_registry.findContainerStacks()
|
||||||
for stack in all_stacks:
|
for stack in all_stacks:
|
||||||
|
|
@ -412,7 +418,7 @@ class ContainerManager(QObject):
|
||||||
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(None, 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]
|
||||||
|
|
@ -508,7 +514,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -616,9 +622,9 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
elif activate_quality:
|
elif activate_quality:
|
||||||
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
|
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
|
||||||
containers = self._container_registry.findInstanceContainers(type = "quality", definition = definition_id, quality_type = activate_quality_type)
|
containers = self._container_registry.findInstanceContainersMetadata(type = "quality", definition = definition_id, quality_type = activate_quality_type)
|
||||||
if containers:
|
if containers:
|
||||||
self._machine_manager.setActiveQuality(containers[0].getId())
|
self._machine_manager.setActiveQuality(containers[0]["id"])
|
||||||
self._machine_manager.activeQualityChanged.emit()
|
self._machine_manager.activeQualityChanged.emit()
|
||||||
|
|
||||||
return containers_found
|
return containers_found
|
||||||
|
|
@ -653,11 +659,13 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
container_registry = self._container_registry
|
container_registry = self._container_registry
|
||||||
|
|
||||||
containers_to_rename = self._container_registry.findInstanceContainers(type = "quality_changes", name = quality_name)
|
containers_to_rename = self._container_registry.findInstanceContainersMetadata(type = "quality_changes", name = quality_name)
|
||||||
|
|
||||||
for container in containers_to_rename:
|
for container in containers_to_rename:
|
||||||
stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
|
stack_id = global_stack.getId()
|
||||||
container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
|
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:
|
if not containers_to_rename:
|
||||||
Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
|
Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
|
||||||
|
|
@ -679,30 +687,32 @@ class ContainerManager(QObject):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack or not quality_name:
|
if not global_stack or not quality_name:
|
||||||
return ""
|
return ""
|
||||||
machine_definition = global_stack.getBottom()
|
machine_definition = global_stack.definition
|
||||||
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
material_containers = [stack.material for stack in active_stacks]
|
if active_stacks is None:
|
||||||
|
return ""
|
||||||
|
material_metadatas = [stack.material.getMetaData() for stack in active_stacks]
|
||||||
|
|
||||||
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
|
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
|
||||||
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
|
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
|
||||||
material_containers)
|
material_metadatas)
|
||||||
return result[0].getName() if result else ""
|
return result[0].getName() if result else ""
|
||||||
|
|
||||||
## Duplicate a quality or quality changes profile specific to a machine type
|
## Duplicate a quality or quality changes profile specific to a machine type
|
||||||
#
|
#
|
||||||
# \param quality_name \type{str} the name of the quality or quality changes container to duplicate.
|
# \param quality_name The name of the quality or quality changes container to duplicate.
|
||||||
# \param base_name \type{str} the desired name for the new container.
|
# \param base_name The desired name for the new container.
|
||||||
# \param machine_definition \type{DefinitionContainer}
|
# \param machine_definition The machine with the specific machine type.
|
||||||
# \param material_instances \type{List[InstanceContainer]}
|
# \param material_metadatas Metadata of materials
|
||||||
# \return \type{str} the name of the newly created container.
|
# \return List of duplicated quality profiles.
|
||||||
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
|
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)
|
Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
|
||||||
|
|
||||||
if base_name is None:
|
if base_name is None:
|
||||||
base_name = quality_name
|
base_name = quality_name
|
||||||
# Try to find a Quality with the name.
|
# Try to find a Quality with the name.
|
||||||
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
|
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_metadatas)
|
||||||
if container:
|
if container:
|
||||||
Logger.log("d", "We found a quality to duplicate.")
|
Logger.log("d", "We found a quality to duplicate.")
|
||||||
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
|
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
|
||||||
|
|
@ -711,7 +721,7 @@ class ContainerManager(QObject):
|
||||||
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
|
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
|
||||||
|
|
||||||
# Duplicate a quality profile
|
# Duplicate a quality profile
|
||||||
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition):
|
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition) -> List[InstanceContainer]:
|
||||||
if base_name is None:
|
if base_name is None:
|
||||||
base_name = quality_container.getName()
|
base_name = quality_container.getName()
|
||||||
new_name = self._container_registry.uniqueName(base_name)
|
new_name = self._container_registry.uniqueName(base_name)
|
||||||
|
|
@ -735,7 +745,7 @@ class ContainerManager(QObject):
|
||||||
return new_change_instances
|
return new_change_instances
|
||||||
|
|
||||||
# Duplicate a quality changes container
|
# Duplicate a quality changes container
|
||||||
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition):
|
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition) -> List[InstanceContainer]:
|
||||||
new_change_instances = []
|
new_change_instances = []
|
||||||
for container in QualityManager.getInstance().findQualityChangesByName(quality_changes_name,
|
for container in QualityManager.getInstance().findQualityChangesByName(quality_changes_name,
|
||||||
machine_definition):
|
machine_definition):
|
||||||
|
|
@ -754,27 +764,73 @@ class ContainerManager(QObject):
|
||||||
# \return \type{str} the id of the newly created container.
|
# \return \type{str} the id of the newly created container.
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def duplicateMaterial(self, material_id: str) -> str:
|
def duplicateMaterial(self, material_id: str) -> str:
|
||||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
original = self._container_registry.findContainersMetadata(id = material_id)
|
||||||
if not containers:
|
if not original:
|
||||||
Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
|
Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
|
||||||
return ""
|
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.
|
# Ensure all settings are saved.
|
||||||
Application.getInstance().saveSettings()
|
Application.getInstance().saveSettings()
|
||||||
|
|
||||||
# Create a new ID & container to hold the data.
|
# Create a new ID & container to hold the data.
|
||||||
new_id = self._container_registry.uniqueName(material_id)
|
new_containers = []
|
||||||
container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer
|
new_base_id = self._container_registry.uniqueName(base_container.getId())
|
||||||
duplicated_container = container_type(new_id)
|
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)
|
||||||
|
|
||||||
# Instead of duplicating we load the data from the basefile again.
|
#Clone all of them.
|
||||||
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
|
clone_of_original = None #Keeping track of which one is the clone of the original material, since we need to return that.
|
||||||
# are also correctly created.
|
for container_to_copy in containers_to_copy:
|
||||||
with open(containers[0].getPath(), encoding="utf-8") as f:
|
#Create unique IDs for every clone.
|
||||||
duplicated_container.deserialize(f.read())
|
current_id = container_to_copy.getId()
|
||||||
duplicated_container.setDirty(True)
|
new_id = new_base_id
|
||||||
self._container_registry.addContainer(duplicated_container)
|
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
|
||||||
return self._getMaterialContainerIdForActiveMachine(new_id)
|
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 duplicate of a material or it's original entry
|
||||||
|
#
|
||||||
|
# \return \type{str} the id of the newly created container.
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def duplicateOriginalMaterial(self, material_id):
|
||||||
|
|
||||||
|
# check if the given material has a base file (i.e. was shipped by default)
|
||||||
|
base_file = self.getContainerMetaDataEntry(material_id, "base_file")
|
||||||
|
|
||||||
|
if base_file == "":
|
||||||
|
# there is no base file, so duplicate by ID
|
||||||
|
return self.duplicateMaterial(material_id)
|
||||||
|
else:
|
||||||
|
# there is a base file, so duplicate the original material
|
||||||
|
return self.duplicateMaterial(base_file)
|
||||||
|
|
||||||
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
|
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
|
||||||
#
|
#
|
||||||
|
|
@ -789,12 +845,12 @@ class ContainerManager(QObject):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
|
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
|
||||||
containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter)
|
containers = self._container_registry.findInstanceContainersMetadata(id = "generic_pla*", approximate_diameter = approximate_diameter)
|
||||||
if not containers:
|
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.")
|
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 ""
|
return ""
|
||||||
|
|
||||||
base_file = containers[0].getMetaDataEntry("base_file")
|
base_file = containers[0].get("base_file")
|
||||||
containers = self._container_registry.findInstanceContainers(id = base_file)
|
containers = self._container_registry.findInstanceContainers(id = base_file)
|
||||||
if not containers:
|
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.")
|
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.")
|
||||||
|
|
@ -835,14 +891,14 @@ class ContainerManager(QObject):
|
||||||
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
|
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
|
||||||
if has_machine_materials or has_variant_materials:
|
if has_machine_materials or has_variant_materials:
|
||||||
if has_variants:
|
if has_variants:
|
||||||
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
|
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
|
||||||
else:
|
else:
|
||||||
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
|
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
|
||||||
|
|
||||||
if materials:
|
if materials:
|
||||||
return materials[0].getId()
|
return materials[0]["id"]
|
||||||
|
|
||||||
Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file)
|
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 "" # do not activate a new material if a container can not be found
|
||||||
|
|
||||||
return base_file
|
return base_file
|
||||||
|
|
@ -853,25 +909,25 @@ class ContainerManager(QObject):
|
||||||
# \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(str, result = "QStringList")
|
||||||
def getLinkedMaterials(self, material_id: str):
|
def getLinkedMaterials(self, material_id: str):
|
||||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
containers = self._container_registry.findInstanceContainersMetadata(id = material_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
|
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
material_container = containers[0]
|
material_container = containers[0]
|
||||||
material_base_file = material_container.getMetaDataEntry("base_file", "")
|
material_base_file = material_container.get("base_file", "")
|
||||||
material_guid = material_container.getMetaDataEntry("GUID", "")
|
material_guid = material_container.get("GUID", "")
|
||||||
if not material_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)
|
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid)
|
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_guid)
|
||||||
linked_material_names = []
|
linked_material_names = []
|
||||||
for container in containers:
|
for container in containers:
|
||||||
if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId():
|
if container["id"] in [material_id, material_base_file] or container.get("base_file") != container["id"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
linked_material_names.append(container.getName())
|
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
|
||||||
|
|
@ -957,14 +1013,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
|
||||||
|
|
||||||
## Get containers filtered by machine type and material if required.
|
|
||||||
#
|
|
||||||
# \param kwargs Initial search criteria that the containers need to match.
|
|
||||||
#
|
|
||||||
# \return A list of containers matching the search criteria.
|
|
||||||
def _getFilteredContainers(self, **kwargs):
|
|
||||||
return QualityManager.getInstance()._getFilteredContainers(**kwargs)
|
|
||||||
|
|
||||||
## Creates a unique ID for a container by prefixing the name with the stack ID.
|
## 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 method creates a unique ID for a container by prefixing it with a specified stack ID.
|
||||||
|
|
@ -1004,9 +1052,9 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||||
if not machine_definition.getMetaDataEntry("has_machine_quality"):
|
if not machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
|
quality_changes.setDefinition("fdmprinter")
|
||||||
else:
|
else:
|
||||||
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
|
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition).getId())
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -36,6 +37,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
||||||
|
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
||||||
|
# is added, we check to see if an extruder stack needs to be added.
|
||||||
|
self.containerAdded.connect(self._onContainerAdded)
|
||||||
|
|
||||||
## Overridden from ContainerRegistry
|
## Overridden from ContainerRegistry
|
||||||
#
|
#
|
||||||
# Adds a container to the registry.
|
# Adds a container to the registry.
|
||||||
|
|
@ -44,7 +50,6 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# Global stack based on metadata information.
|
# Global stack based on metadata information.
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def addContainer(self, container):
|
def addContainer(self, container):
|
||||||
|
|
||||||
# Note: Intentional check with type() because we want to ignore subclasses
|
# Note: Intentional check with type() because we want to ignore subclasses
|
||||||
if type(container) == ContainerStack:
|
if type(container) == ContainerStack:
|
||||||
container = self._convertContainerStack(container)
|
container = self._convertContainerStack(container)
|
||||||
|
|
@ -89,8 +94,8 @@ 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.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
|
return self.findContainersMetadata(id = container_name, type = container_type, ignore_case = True) or \
|
||||||
self.findContainers(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
|
||||||
#
|
#
|
||||||
|
|
@ -119,7 +124,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
found_containers = []
|
found_containers = []
|
||||||
extruder_positions = []
|
extruder_positions = []
|
||||||
for instance_id in instance_ids:
|
for instance_id in instance_ids:
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
|
containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
|
||||||
if containers:
|
if containers:
|
||||||
found_containers.append(containers[0])
|
found_containers.append(containers[0])
|
||||||
|
|
||||||
|
|
@ -129,9 +134,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# Global stack
|
# Global stack
|
||||||
extruder_positions.append(-1)
|
extruder_positions.append(-1)
|
||||||
else:
|
else:
|
||||||
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id)
|
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = extruder_id)
|
||||||
if extruder_containers:
|
if extruder_containers:
|
||||||
extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0)))
|
extruder_positions.append(int(extruder_containers[0].get("position", 0)))
|
||||||
else:
|
else:
|
||||||
extruder_positions.append(0)
|
extruder_positions.append(0)
|
||||||
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
|
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
|
||||||
|
|
@ -197,7 +202,6 @@ 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.
|
||||||
|
|
@ -205,53 +209,41 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# 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, str(e))}
|
||||||
if profile_or_list: # Success!
|
|
||||||
|
if profile_or_list:
|
||||||
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)
|
||||||
|
|
||||||
|
# Ensure it is always a list of profiles
|
||||||
if type(profile_or_list) is not list:
|
if type(profile_or_list) is not list:
|
||||||
profile = profile_or_list
|
profile_or_list = [profile_or_list]
|
||||||
|
|
||||||
result = self._configureProfile(profile, name_seed, new_name)
|
# Import all profiles
|
||||||
if result is not None:
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
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, result)}
|
if profile_index == 0:
|
||||||
|
# This is assumed to be the global profile
|
||||||
|
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName())}
|
elif profile_index < len(machine_extruders) + 1:
|
||||||
else:
|
# This is assumed to be an extruder profile
|
||||||
profile_index = -1
|
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom())
|
||||||
global_profile = None
|
if not profile.getMetaDataEntry("extruder"):
|
||||||
|
profile.addMetaDataEntry("extruder", extruder_id)
|
||||||
for profile in profile_or_list:
|
|
||||||
if profile_index >= 0:
|
|
||||||
if len(machine_extruders) > profile_index:
|
|
||||||
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
|
|
||||||
# Ensure the extruder profiles get non-conflicting names
|
|
||||||
# NB: these are not user-facing
|
|
||||||
if "extruder" in profile.getMetaData():
|
|
||||||
profile.setMetaDataEntry("extruder", extruder_id)
|
|
||||||
else:
|
|
||||||
profile.addMetaDataEntry("extruder", extruder_id)
|
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
|
||||||
elif profile_index == 0:
|
|
||||||
# Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
|
|
||||||
profile._id = self.uniqueName("temporary_profile")
|
|
||||||
self.addContainer(profile)
|
|
||||||
ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
|
|
||||||
self.removeContainer(profile.getId())
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
global_profile = profile
|
profile.setMetaDataEntry("extruder", extruder_id)
|
||||||
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
result = self._configureProfile(profile, profile_id, new_name)
|
else: #More extruders in the imported file than in the machine.
|
||||||
if result is not None:
|
continue #Delete the additional profiles.
|
||||||
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, result)}
|
|
||||||
|
|
||||||
profile_index += 1
|
result = self._configureProfile(profile, profile_id, new_name)
|
||||||
|
if result is not None:
|
||||||
|
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, result)}
|
||||||
|
|
||||||
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())}
|
||||||
|
|
||||||
# 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)}
|
||||||
|
|
@ -270,13 +262,16 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
#
|
#
|
||||||
# \return None if configuring was successful or an error message if an error occurred.
|
# \return None if configuring was successful or an error message if an error occurred.
|
||||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
|
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
|
||||||
profile.setReadOnly(False)
|
|
||||||
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._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:
|
||||||
|
|
@ -288,7 +283,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
quality_type_criteria = {"quality_type": quality_type}
|
quality_type_criteria = {"quality_type": quality_type}
|
||||||
if self._machineHasOwnQualities():
|
if self._machineHasOwnQualities():
|
||||||
profile.setDefinition(self._activeQualityDefinition())
|
profile.setDefinition(self._activeQualityDefinition().getId())
|
||||||
if self._machineHasOwnMaterials():
|
if self._machineHasOwnMaterials():
|
||||||
active_material_id = self._activeMaterialId()
|
active_material_id = self._activeMaterialId()
|
||||||
if active_material_id and active_material_id != "empty": # only update if there is an active material
|
if active_material_id and active_material_id != "empty": # only update if there is an active material
|
||||||
|
|
@ -298,7 +293,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
quality_type_criteria["definition"] = profile.getDefinition().getId()
|
quality_type_criteria["definition"] = profile.getDefinition().getId()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
|
profile.setDefinition("fdmprinter")
|
||||||
quality_type_criteria["definition"] = "fdmprinter"
|
quality_type_criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
|
@ -345,7 +340,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
|
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
|
||||||
definition = self.findDefinitionContainers(id=definition_id)[0]
|
definition = self.findDefinitionContainers(id = definition_id)[0]
|
||||||
|
|
||||||
if definition:
|
if definition:
|
||||||
return definition
|
return definition
|
||||||
|
|
@ -408,6 +403,21 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if not extruder_stacks:
|
if not extruder_stacks:
|
||||||
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
||||||
|
|
||||||
|
def _onContainerAdded(self, container):
|
||||||
|
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
||||||
|
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
||||||
|
# is added, we check to see if an extruder stack needs to be added.
|
||||||
|
if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine":
|
||||||
|
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())
|
||||||
|
if not extruder_stacks:
|
||||||
|
self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
|
||||||
|
|
||||||
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
|
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
|
||||||
new_extruder_id = extruder_id
|
new_extruder_id = extruder_id
|
||||||
|
|
||||||
|
|
@ -423,15 +433,45 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
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"))
|
||||||
extruder_stack.setNextStack(machine)
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
# create a new definition_changes container for the extruder stack
|
||||||
|
definition_changes_id = self.uniqueName(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")
|
||||||
|
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", extruder_stack.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)
|
user_container.setDefinition(machine.definition.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
|
||||||
|
|
@ -439,11 +479,19 @@ 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)
|
||||||
|
|
||||||
extruder_stack.setUserChanges(user_container)
|
|
||||||
self.addContainer(user_container)
|
self.addContainer(user_container)
|
||||||
|
extruder_stack.setUserChanges(user_container)
|
||||||
|
|
||||||
variant_id = "default"
|
variant_id = "default"
|
||||||
if machine.variant.getId() not in ("empty", "empty_variant"):
|
if machine.variant.getId() not in ("empty", "empty_variant"):
|
||||||
|
|
@ -470,6 +518,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
|
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.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.setQualityChangesById(quality_changes_id)
|
||||||
else:
|
else:
|
||||||
|
|
@ -480,15 +529,95 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
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_stack.setQualityChangesById(quality_changes_id)
|
||||||
|
else:
|
||||||
|
# if we still cannot find a quality changes container for the extruder, create a new one
|
||||||
|
container_id = self.uniqueName(extruder_stack.getId() + "_user")
|
||||||
|
container_name = machine.qualityChanges.getName()
|
||||||
|
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.qualityChanges.getMetaDataEntry("quality_type"))
|
||||||
|
extruder_quality_changes_container.setDefinition(machine.qualityChanges.getDefinition().getId())
|
||||||
|
|
||||||
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.qualityChanges.getName(), extruder_stack.getId())
|
||||||
|
else:
|
||||||
|
# move all per-extruder settings to the extruder's quality changes
|
||||||
|
for qc_setting_key in machine.qualityChanges.getAllKeys():
|
||||||
|
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||||
|
if settable_per_extruder:
|
||||||
|
setting_value = machine.qualityChanges.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.qualityChanges.removeInstance(qc_setting_key, postpone_emit=True)
|
||||||
else:
|
else:
|
||||||
extruder_stack.setQualityChangesById("empty_quality_changes")
|
extruder_stack.setQualityChangesById("empty_quality_changes")
|
||||||
|
|
||||||
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.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
|
||||||
|
quality_changes_machine_definition_id = machine.qualityChanges.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.qualityChanges.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
|
||||||
|
extruder_stack.setNextStack(machine)
|
||||||
|
|
||||||
return extruder_stack
|
return extruder_stack
|
||||||
|
|
||||||
def _findQualityChangesContainerInCuraFolder(self, name):
|
def _findQualityChangesContainerInCuraFolder(self, name):
|
||||||
|
|
@ -514,6 +643,9 @@ 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") as f:
|
||||||
|
|
@ -530,13 +662,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# set after upgrading, because the proper global stack was not yet loaded. This method
|
# set after upgrading, because the proper global stack was not yet loaded. This method
|
||||||
# makes sure those extruders also get the right stack set.
|
# makes sure those extruders also get the right stack set.
|
||||||
def _connectUpgradedExtruderStacksToMachines(self):
|
def _connectUpgradedExtruderStacksToMachines(self):
|
||||||
extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack)
|
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks:
|
||||||
if extruder_stack.getNextStack():
|
if extruder_stack.getNextStack():
|
||||||
# Has the right next stack, so ignore it.
|
# Has the right next stack, so ignore it.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
machines = ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack.getMetaDataEntry("machine", ""))
|
machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", ""))
|
||||||
if machines:
|
if machines:
|
||||||
extruder_stack.setNextStack(machines[0])
|
extruder_stack.setNextStack(machines[0])
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackErro
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import ContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
|
|
||||||
|
|
@ -246,7 +246,7 @@ 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_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
def setDefinition(self, new_definition: DefinitionContainer) -> 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.
|
## Set the definition container by an ID.
|
||||||
|
|
@ -377,7 +377,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
if not container or not isinstance(container, DefinitionContainer):
|
if not container or not isinstance(container, DefinitionContainer):
|
||||||
definition = self.findContainer(container_type = DefinitionContainer)
|
definition = self.findContainer(container_type = DefinitionContainer)
|
||||||
if not definition:
|
if not definition:
|
||||||
raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self._id))
|
raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self.getId()))
|
||||||
|
|
||||||
new_containers[index] = definition
|
new_containers[index] = definition
|
||||||
continue
|
continue
|
||||||
|
|
@ -487,11 +487,17 @@ class CuraContainerStack(ContainerStack):
|
||||||
search_criteria.pop("name", None)
|
search_criteria.pop("name", None)
|
||||||
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||||
|
|
||||||
if materials:
|
if not materials:
|
||||||
return materials[0]
|
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]
|
||||||
|
|
||||||
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
## Find the quality that should be used as "default" quality.
|
## Find the quality that should be used as "default" quality.
|
||||||
#
|
#
|
||||||
|
|
@ -502,7 +508,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
def findDefaultQuality(self) -> Optional[ContainerInterface]:
|
def findDefaultQuality(self) -> Optional[ContainerInterface]:
|
||||||
definition = self._getMachineDefinition()
|
definition = self._getMachineDefinition()
|
||||||
registry = ContainerRegistry.getInstance()
|
registry = ContainerRegistry.getInstance()
|
||||||
material_container = self.material if self.material != self._empty_instance_container else None
|
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"}
|
search_criteria = {"type": "quality"}
|
||||||
|
|
||||||
|
|
@ -546,7 +552,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
|
material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
|
||||||
if definition.getMetaDataEntry("has_machine_quality"):
|
if definition.getMetaDataEntry("has_machine_quality"):
|
||||||
if self.material != self._empty_instance_container:
|
if self.material != self._empty_instance_container:
|
||||||
material_search_criteria["definition"] = material_container.getDefinition().id
|
material_search_criteria["definition"] = material_container.getMetaDataEntry("definition")
|
||||||
|
|
||||||
if definition.getMetaDataEntry("has_variants"):
|
if definition.getMetaDataEntry("has_variants"):
|
||||||
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
|
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
|
||||||
|
|
@ -557,10 +563,10 @@ class CuraContainerStack(ContainerStack):
|
||||||
material_search_criteria["variant"] = self.variant.id
|
material_search_criteria["variant"] = self.variant.id
|
||||||
else:
|
else:
|
||||||
material_search_criteria["definition"] = "fdmprinter"
|
material_search_criteria["definition"] = "fdmprinter"
|
||||||
material_containers = registry.findInstanceContainers(**material_search_criteria)
|
material_containers = registry.findInstanceContainersMetadata(**material_search_criteria)
|
||||||
# Try all materials to see if there is a quality profile available.
|
# Try all materials to see if there is a quality profile available.
|
||||||
for material_container in material_containers:
|
for material_container in material_containers:
|
||||||
search_criteria["material"] = material_container.getId()
|
search_criteria["material"] = material_container["id"]
|
||||||
|
|
||||||
containers = registry.findInstanceContainers(**search_criteria)
|
containers = registry.findInstanceContainers(**search_criteria)
|
||||||
if containers:
|
if containers:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
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
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class CuraStackBuilder:
|
||||||
# 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
|
||||||
if registry.findContainers(id = generated_name):
|
if registry.findContainersMetadata(id = generated_name):
|
||||||
generated_name = registry.uniqueName(generated_name)
|
generated_name = registry.uniqueName(generated_name)
|
||||||
|
|
||||||
new_global_stack = cls.createGlobalStack(
|
new_global_stack = cls.createGlobalStack(
|
||||||
|
|
@ -55,12 +55,12 @@ class CuraStackBuilder:
|
||||||
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id)
|
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id)
|
||||||
new_extruder = cls.createExtruderStack(
|
new_extruder = cls.createExtruderStack(
|
||||||
new_extruder_id,
|
new_extruder_id,
|
||||||
definition=extruder_definition,
|
definition = extruder_definition,
|
||||||
machine_definition=machine_definition,
|
machine_definition_id = machine_definition.getId(),
|
||||||
quality="default",
|
quality = "default",
|
||||||
material="default",
|
material = "default",
|
||||||
variant="default",
|
variant = "default",
|
||||||
next_stack=new_global_stack
|
next_stack = new_global_stack
|
||||||
)
|
)
|
||||||
new_global_stack.addExtruder(new_extruder)
|
new_global_stack.addExtruder(new_extruder)
|
||||||
else:
|
else:
|
||||||
|
|
@ -74,7 +74,7 @@ class CuraStackBuilder:
|
||||||
new_extruder = cls.createExtruderStack(
|
new_extruder = cls.createExtruderStack(
|
||||||
new_extruder_id,
|
new_extruder_id,
|
||||||
definition = extruder_definition,
|
definition = extruder_definition,
|
||||||
machine_definition = machine_definition,
|
machine_definition_id = machine_definition.getId(),
|
||||||
quality = "default",
|
quality = "default",
|
||||||
material = "default",
|
material = "default",
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
|
@ -88,12 +88,13 @@ class CuraStackBuilder:
|
||||||
#
|
#
|
||||||
# \param new_stack_id The ID of the new stack.
|
# \param new_stack_id The ID of the new stack.
|
||||||
# \param definition The definition to base the new stack on.
|
# \param definition The definition to base the new stack on.
|
||||||
# \param machine_definition The machine definition to use for the user container.
|
# \param machine_definition_id The ID of the machine definition to use for
|
||||||
|
# the user container.
|
||||||
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
||||||
#
|
#
|
||||||
# \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: DefinitionContainer, machine_definition: DefinitionContainer, **kwargs) -> ExtruderStack:
|
def createExtruderStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, machine_definition_id: str, **kwargs) -> ExtruderStack:
|
||||||
stack = ExtruderStack(new_stack_id)
|
stack = ExtruderStack(new_stack_id)
|
||||||
stack.setName(definition.getName())
|
stack.setName(definition.getName())
|
||||||
stack.setDefinition(definition)
|
stack.setDefinition(definition)
|
||||||
|
|
@ -108,7 +109,7 @@ class CuraStackBuilder:
|
||||||
user_container.addMetaDataEntry("extruder", new_stack_id)
|
user_container.addMetaDataEntry("extruder", new_stack_id)
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
user_container.setDefinition(machine_definition)
|
user_container.setDefinition(machine_definition_id)
|
||||||
|
|
||||||
stack.setUserChanges(user_container)
|
stack.setUserChanges(user_container)
|
||||||
|
|
||||||
|
|
@ -148,7 +149,7 @@ 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: DefinitionContainer, **kwargs) -> GlobalStack:
|
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, **kwargs) -> GlobalStack:
|
||||||
stack = GlobalStack(new_stack_id)
|
stack = GlobalStack(new_stack_id)
|
||||||
stack.setDefinition(definition)
|
stack.setDefinition(definition)
|
||||||
|
|
||||||
|
|
@ -157,7 +158,7 @@ class CuraStackBuilder:
|
||||||
user_container.addMetaDataEntry("machine", new_stack_id)
|
user_container.addMetaDataEntry("machine", new_stack_id)
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
user_container.setDefinition(definition)
|
user_container.setDefinition(definition.getId())
|
||||||
|
|
||||||
stack.setUserChanges(user_container)
|
stack.setUserChanges(user_container)
|
||||||
|
|
||||||
|
|
@ -193,8 +194,7 @@ class CuraStackBuilder:
|
||||||
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
|
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
|
||||||
|
|
||||||
definition_changes_container = InstanceContainer(unique_container_name)
|
definition_changes_container = InstanceContainer(unique_container_name)
|
||||||
definition = container_stack.getBottom()
|
definition_changes_container.setDefinition(container_stack.getBottom().getId())
|
||||||
definition_changes_container.setDefinition(definition)
|
|
||||||
definition_changes_container.addMetaDataEntry("type", "definition_changes")
|
definition_changes_container.addMetaDataEntry("type", "definition_changes")
|
||||||
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,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:
|
||||||
|
|
@ -356,14 +356,16 @@ class ExtruderManager(QObject):
|
||||||
# \return \type{List[ContainerStack]} a list of
|
# \return \type{List[ContainerStack]} a list of
|
||||||
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return None
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
if global_stack.getId() in self._extruder_trains:
|
||||||
|
|
||||||
if global_stack and global_stack.getId() in self._extruder_trains:
|
|
||||||
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
|
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
|
||||||
result.append(self._extruder_trains[global_stack.getId()][extruder])
|
result.append(self._extruder_trains[global_stack.getId()][extruder])
|
||||||
|
|
||||||
|
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
return result[:machine_extruder_count]
|
return result[:machine_extruder_count]
|
||||||
|
|
||||||
def __globalContainerStackChanged(self) -> None:
|
def __globalContainerStackChanged(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
||||||
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
from .CuraContainerStack import CuraContainerStack
|
from .CuraContainerStack import CuraContainerStack
|
||||||
|
|
@ -16,6 +17,11 @@ from .ExtruderManager import ExtruderManager
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
|
||||||
|
_EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS = ["machine_nozzle_size",
|
||||||
|
"material_diameter"]
|
||||||
|
|
||||||
|
|
||||||
## Represents an Extruder and its related containers.
|
## Represents an Extruder and its related containers.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
@ -39,6 +45,29 @@ class ExtruderStack(CuraContainerStack):
|
||||||
# For backward compatibility: Register the extruder with the Extruder Manager
|
# For backward compatibility: Register the extruder with the Extruder Manager
|
||||||
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
||||||
|
|
||||||
|
# Now each machine will have at least one extruder stack. If this is the first extruder, the extruder-specific
|
||||||
|
# settings such as nozzle size and material diameter should be moved from the machine's definition_changes to
|
||||||
|
# the this extruder's definition_changes.
|
||||||
|
#
|
||||||
|
# We do this here because it is tooooo expansive to do it in the version upgrade: During the version upgrade,
|
||||||
|
# when we are upgrading a definition_changes container file, there is NO guarantee that other files such as
|
||||||
|
# machine an extruder stack files are upgraded before this, so we cannot read those files assuming they are in
|
||||||
|
# the latest format.
|
||||||
|
if self.getMetaDataEntry("position") == "0":
|
||||||
|
for key in _EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS:
|
||||||
|
setting_value = stack.definitionChanges.getProperty(key, "value")
|
||||||
|
if setting_value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
setting_definition = stack.getSettingDefinition(key)
|
||||||
|
new_instance = SettingInstance(setting_definition, self.definitionChanges)
|
||||||
|
new_instance.setProperty("value", setting_value)
|
||||||
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
|
self.definitionChanges.addInstance(new_instance)
|
||||||
|
self.definitionChanges.setDirty(True)
|
||||||
|
|
||||||
|
stack.definitionChanges.removeInstance(key, postpone_emit = True)
|
||||||
|
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def getNextStack(self) -> Optional["GlobalStack"]:
|
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||||
return super().getNextStack()
|
return super().getNextStack()
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ from UM.i18n import i18nCatalog
|
||||||
import UM.Qt.ListModel
|
import UM.Qt.ListModel
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
import UM.FlameProfiler
|
import UM.FlameProfiler
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack #To listen to changes on the extruders.
|
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
@ -68,7 +68,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
self._update_extruder_timer.setSingleShot(True)
|
self._update_extruder_timer.setSingleShot(True)
|
||||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||||
|
|
||||||
self._add_global = False
|
|
||||||
self._simple_names = False
|
self._simple_names = False
|
||||||
|
|
||||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||||
|
|
@ -76,21 +75,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
# Listen to changes
|
# Listen to changes
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) # When the machine is swapped we must update the active machine extruders
|
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) # When the machine is swapped we must update the active machine extruders
|
||||||
ExtruderManager.getInstance().extrudersChanged.connect(self._extrudersChanged) # When the extruders change we must link to the stack-changed signal of the new extruder
|
Application.getInstance().getExtruderManager().extrudersChanged.connect(self._extrudersChanged) # When the extruders change we must link to the stack-changed signal of the new extruder
|
||||||
|
Application.getInstance().getContainerRegistry().containerMetaDataChanged.connect(self._onExtruderStackContainersChanged) # When meta data from a material container changes we must update
|
||||||
self._extrudersChanged() # Also calls _updateExtruders
|
self._extrudersChanged() # Also calls _updateExtruders
|
||||||
|
|
||||||
def setAddGlobal(self, add):
|
|
||||||
if add != self._add_global:
|
|
||||||
self._add_global = add
|
|
||||||
self._updateExtruders()
|
|
||||||
self.addGlobalChanged.emit()
|
|
||||||
|
|
||||||
addGlobalChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
|
|
||||||
def addGlobal(self):
|
|
||||||
return self._add_global
|
|
||||||
|
|
||||||
addOptionalExtruderChanged = pyqtSignal()
|
addOptionalExtruderChanged = pyqtSignal()
|
||||||
|
|
||||||
def setAddOptionalExtruder(self, add_optional_extruder):
|
def setAddOptionalExtruder(self, add_optional_extruder):
|
||||||
|
|
@ -140,8 +128,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
# Link to new extruders
|
# Link to new extruders
|
||||||
self._active_machine_extruders = []
|
self._active_machine_extruders = []
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
extruder_manager = Application.getInstance().getExtruderManager()
|
||||||
for extruder in extruder_manager.getExtruderStacks():
|
for extruder in extruder_manager.getExtruderStacks():
|
||||||
|
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
|
||||||
|
continue
|
||||||
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||||
self._active_machine_extruders.append(extruder)
|
self._active_machine_extruders.append(extruder)
|
||||||
|
|
||||||
|
|
@ -173,24 +163,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
|
|
||||||
# TODO: remove this - CURA-4482
|
|
||||||
if self._add_global:
|
|
||||||
material = global_container_stack.material
|
|
||||||
color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0]
|
|
||||||
item = {
|
|
||||||
"id": global_container_stack.getId(),
|
|
||||||
"name": catalog.i18nc("@menuitem", "Global"),
|
|
||||||
"color": color,
|
|
||||||
"index": -1,
|
|
||||||
"definition": ""
|
|
||||||
}
|
|
||||||
items.append(item)
|
|
||||||
extruders_changed = True
|
|
||||||
|
|
||||||
# get machine extruder count for verification
|
# get machine extruder count for verification
|
||||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
|
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()):
|
||||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||||
try:
|
try:
|
||||||
position = int(position)
|
position = int(position)
|
||||||
|
|
|
||||||
|
|
@ -43,15 +43,11 @@ class GlobalStack(CuraContainerStack):
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
|
@classmethod
|
||||||
configuration_type = None
|
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
|
||||||
try:
|
configuration_type = super().getConfigurationTypeFromSerialized(serialized)
|
||||||
parser = self._readAndValidateSerialized(serialized)
|
if configuration_type == "machine":
|
||||||
configuration_type = parser["metadata"].get("type")
|
return "machine_stack"
|
||||||
if configuration_type == "machine":
|
|
||||||
configuration_type = "machine_stack"
|
|
||||||
except Exception as e:
|
|
||||||
Logger.log("e", "Could not get configuration type: %s", e)
|
|
||||||
return configuration_type
|
return configuration_type
|
||||||
|
|
||||||
## Add an extruder to the list of extruders of this stack.
|
## Add an extruder to the list of extruders of this stack.
|
||||||
|
|
@ -67,7 +63,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
return
|
return
|
||||||
|
|
||||||
if any(item.getId() == extruder.id for item in self._extruders.values()):
|
if any(item.getId() == extruder.id for item in self._extruders.values()):
|
||||||
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self._id)
|
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self.getId())
|
||||||
return
|
return
|
||||||
|
|
||||||
self._extruders[position] = extruder
|
self._extruders[position] = extruder
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from .CuraStackBuilder import CuraStackBuilder
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
from cura.Settings.ProfilesModel import ProfilesModel
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -60,9 +61,11 @@ class MachineManager(QObject):
|
||||||
self._instance_container_timer = QTimer()
|
self._instance_container_timer = QTimer()
|
||||||
self._instance_container_timer.setInterval(250)
|
self._instance_container_timer.setInterval(250)
|
||||||
self._instance_container_timer.setSingleShot(True)
|
self._instance_container_timer.setSingleShot(True)
|
||||||
self._instance_container_timer.timeout.connect(self.__onInstanceContainersChanged)
|
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
|
Application.getInstance().getContainerRegistry().containerLoadComplete.connect(self._onInstanceContainersChanged)
|
||||||
|
self._connected_to_profiles_model = False
|
||||||
|
|
||||||
## When the global container is changed, active material probably needs to be updated.
|
## When the global container is changed, active material probably needs to be updated.
|
||||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||||
|
|
@ -104,7 +107,7 @@ class MachineManager(QObject):
|
||||||
# There might already be some output devices by the time the signal is connected
|
# There might already be some output devices by the time the signal is connected
|
||||||
self._onOutputDevicesChanged()
|
self._onOutputDevicesChanged()
|
||||||
|
|
||||||
if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacks(id = active_machine_id):
|
if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id):
|
||||||
# An active machine was saved, so restore it.
|
# An active machine was saved, so restore it.
|
||||||
self.setActiveMachine(active_machine_id)
|
self.setActiveMachine(active_machine_id)
|
||||||
# Make sure _active_container_stack is properly initiated
|
# Make sure _active_container_stack is properly initiated
|
||||||
|
|
@ -114,9 +117,13 @@ class MachineManager(QObject):
|
||||||
self._auto_hotends_changed = {}
|
self._auto_hotends_changed = {}
|
||||||
|
|
||||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||||
"The selected material is incompatible with the selected machine or configuration."),
|
"The selected material is incompatible with the selected machine or configuration."),
|
||||||
title = catalog.i18nc("@info:title", "Incompatible Material"))
|
title = catalog.i18nc("@info:title", "Incompatible Material"))
|
||||||
|
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId)
|
||||||
|
if containers:
|
||||||
|
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||||
|
|
||||||
globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value)
|
globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value)
|
||||||
activeMaterialChanged = pyqtSignal()
|
activeMaterialChanged = pyqtSignal()
|
||||||
activeVariantChanged = pyqtSignal()
|
activeVariantChanged = pyqtSignal()
|
||||||
|
|
@ -128,7 +135,7 @@ class MachineManager(QObject):
|
||||||
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||||
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed
|
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed
|
||||||
|
|
||||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||||
|
|
||||||
outputDevicesChanged = pyqtSignal()
|
outputDevicesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
|
@ -137,8 +144,7 @@ class MachineManager(QObject):
|
||||||
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
|
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
|
||||||
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
|
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
|
||||||
|
|
||||||
self._printer_output_devices.clear()
|
self._printer_output_devices = []
|
||||||
|
|
||||||
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
||||||
if isinstance(printer_output_device, PrinterOutputDevice):
|
if isinstance(printer_output_device, PrinterOutputDevice):
|
||||||
self._printer_output_devices.append(printer_output_device)
|
self._printer_output_devices.append(printer_output_device)
|
||||||
|
|
@ -163,58 +169,70 @@ class MachineManager(QObject):
|
||||||
def totalNumberOfSettings(self) -> int:
|
def totalNumberOfSettings(self) -> int:
|
||||||
return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys())
|
return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys())
|
||||||
|
|
||||||
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None:
|
def _onHotendIdChanged(self):
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack or not self._printer_output_devices:
|
||||||
return
|
return
|
||||||
|
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type="variant", definition=self._global_container_stack.getBottom().getId(), name=hotend_id)
|
active_printer_model = self._printer_output_devices[0].activePrinter
|
||||||
if containers: # New material ID is known
|
if not active_printer_model:
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
|
||||||
machine_id = self.activeMachineId
|
|
||||||
extruders = extruder_manager.getMachineExtruders(machine_id)
|
|
||||||
matching_extruder = None
|
|
||||||
for extruder in extruders:
|
|
||||||
if str(index) == extruder.getMetaDataEntry("position"):
|
|
||||||
matching_extruder = extruder
|
|
||||||
break
|
|
||||||
if matching_extruder and matching_extruder.variant.getName() != hotend_id:
|
|
||||||
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
|
|
||||||
self._auto_hotends_changed[str(index)] = containers[0].getId()
|
|
||||||
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
|
|
||||||
else:
|
|
||||||
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
|
|
||||||
|
|
||||||
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
|
|
||||||
if not self._global_container_stack:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
definition_id = "fdmprinter"
|
change_found = False
|
||||||
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
|
machine_id = self.activeMachineId
|
||||||
definition_id = self.activeQualityDefinitionId
|
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
key=lambda k: k.getMetaDataEntry("position"))
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
|
|
||||||
if containers: # New material ID is known
|
|
||||||
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
|
|
||||||
matching_extruder = None
|
|
||||||
for extruder in extruders:
|
|
||||||
if str(index) == extruder.getMetaDataEntry("position"):
|
|
||||||
matching_extruder = extruder
|
|
||||||
break
|
|
||||||
|
|
||||||
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id:
|
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
|
||||||
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="variant",
|
||||||
if self._global_container_stack.getBottom().getMetaDataEntry("has_variants") and matching_extruder.variant:
|
definition=self._global_container_stack.definition.getId(),
|
||||||
variant_id = self.getQualityVariantId(self._global_container_stack.getBottom(), matching_extruder.variant)
|
name=extruder_model.hotendID)
|
||||||
for container in containers:
|
if containers:
|
||||||
if container.getMetaDataEntry("variant") == variant_id:
|
# The hotend ID is known.
|
||||||
self._auto_materials_changed[str(index)] = container.getId()
|
machine_id = self.activeMachineId
|
||||||
break
|
if extruder.variant.getName() != extruder_model.hotendID:
|
||||||
else:
|
change_found = True
|
||||||
# Just use the first result we found.
|
self._auto_hotends_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"]
|
||||||
self._auto_materials_changed[str(index)] = containers[0].getId()
|
|
||||||
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
|
if change_found:
|
||||||
else:
|
# A change was found, let the output device handle this.
|
||||||
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
|
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
|
||||||
|
|
||||||
|
def _onMaterialIdChanged(self):
|
||||||
|
if not self._global_container_stack or not self._printer_output_devices:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_printer_model = self._printer_output_devices[0].activePrinter
|
||||||
|
if not active_printer_model:
|
||||||
|
return
|
||||||
|
|
||||||
|
change_found = False
|
||||||
|
machine_id = self.activeMachineId
|
||||||
|
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
|
||||||
|
key=lambda k: k.getMetaDataEntry("position"))
|
||||||
|
|
||||||
|
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
|
||||||
|
if extruder_model.activeMaterial is None:
|
||||||
|
continue
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="material",
|
||||||
|
definition=self._global_container_stack.definition.getId(),
|
||||||
|
GUID=extruder_model.activeMaterial.guid)
|
||||||
|
if containers:
|
||||||
|
# The material is known.
|
||||||
|
if extruder.material.getMetaDataEntry("GUID") != extruder_model.activeMaterial.guid:
|
||||||
|
change_found = True
|
||||||
|
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and extruder.variant:
|
||||||
|
variant_id = self.getQualityVariantId(self._global_container_stack.definition,
|
||||||
|
extruder.variant)
|
||||||
|
for container in containers:
|
||||||
|
if container.get("variant") == variant_id:
|
||||||
|
self._auto_materials_changed[extruder.getMetaDataEntry("position")] = container["id"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Just use the first result we found.
|
||||||
|
self._auto_materials_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"]
|
||||||
|
if change_found:
|
||||||
|
# A change was found, let the output device handle this.
|
||||||
|
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
|
||||||
|
|
||||||
def _materialHotendChangedCallback(self, button):
|
def _materialHotendChangedCallback(self, button):
|
||||||
if button == QMessageBox.No:
|
if button == QMessageBox.No:
|
||||||
|
|
@ -329,14 +347,24 @@ class MachineManager(QObject):
|
||||||
# on _active_container_stack. If it changes, then the properties change.
|
# on _active_container_stack. If it changes, then the properties change.
|
||||||
self.activeQualityChanged.emit()
|
self.activeQualityChanged.emit()
|
||||||
|
|
||||||
def __onInstanceContainersChanged(self):
|
def __emitChangedSignals(self):
|
||||||
self.activeQualityChanged.emit()
|
self.activeQualityChanged.emit()
|
||||||
self.activeVariantChanged.emit()
|
self.activeVariantChanged.emit()
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
self._updateStacksHaveErrors() # Prevents unwanted re-slices after changing machine
|
self._updateStacksHaveErrors() # Prevents unwanted re-slices after changing machine
|
||||||
self._error_check_timer.start()
|
self._error_check_timer.start()
|
||||||
|
|
||||||
|
def _onProfilesModelChanged(self, *args):
|
||||||
|
self.__emitChangedSignals()
|
||||||
|
|
||||||
def _onInstanceContainersChanged(self, container):
|
def _onInstanceContainersChanged(self, container):
|
||||||
|
# This should not trigger the ProfilesModel to be created, or there will be an infinite recursion
|
||||||
|
if not self._connected_to_profiles_model and ProfilesModel.hasInstance():
|
||||||
|
# This triggers updating the qualityModel in SidebarSimple whenever ProfilesModel is updated
|
||||||
|
Logger.log("d", "Connecting profiles model...")
|
||||||
|
ProfilesModel.getInstance().itemsChanged.connect(self._onProfilesModelChanged)
|
||||||
|
self._connected_to_profiles_model = True
|
||||||
|
|
||||||
self._instance_container_timer.start()
|
self._instance_container_timer.start()
|
||||||
|
|
||||||
def _onPropertyChanged(self, key: str, property_name: str):
|
def _onPropertyChanged(self, key: str, property_name: str):
|
||||||
|
|
@ -352,11 +380,13 @@ class MachineManager(QObject):
|
||||||
self.blurSettings.emit() # Ensure no-one has focus.
|
self.blurSettings.emit() # Ensure no-one has focus.
|
||||||
self._cancelDelayedActiveContainerStackChanges()
|
self._cancelDelayedActiveContainerStackChanges()
|
||||||
|
|
||||||
containers = ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
containers = container_registry.findContainerStacks(id = stack_id)
|
||||||
if containers:
|
if containers:
|
||||||
Application.getInstance().setGlobalContainerStack(containers[0])
|
Application.getInstance().setGlobalContainerStack(containers[0])
|
||||||
|
|
||||||
self.__onInstanceContainersChanged()
|
self.__emitChangedSignals()
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def addMachine(self, name: str, definition_id: str) -> None:
|
def addMachine(self, name: str, definition_id: str) -> None:
|
||||||
|
|
@ -498,6 +528,7 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty("QVariantList", notify=activeVariantChanged)
|
@pyqtProperty("QVariantList", notify=activeVariantChanged)
|
||||||
def activeVariantNames(self) -> List[str]:
|
def activeVariantNames(self) -> List[str]:
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
if active_stacks is not None:
|
if active_stacks is not None:
|
||||||
for stack in active_stacks:
|
for stack in active_stacks:
|
||||||
|
|
@ -510,6 +541,7 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty("QVariantList", notify = activeMaterialChanged)
|
@pyqtProperty("QVariantList", notify = activeMaterialChanged)
|
||||||
def activeMaterialNames(self) -> List[str]:
|
def activeMaterialNames(self) -> List[str]:
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
if active_stacks is not None:
|
if active_stacks is not None:
|
||||||
for stack in active_stacks:
|
for stack in active_stacks:
|
||||||
|
|
@ -530,6 +562,7 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty("QVariantMap", notify = activeVariantChanged)
|
@pyqtProperty("QVariantMap", notify = activeVariantChanged)
|
||||||
def allActiveVariantIds(self) -> Dict[str, str]:
|
def allActiveVariantIds(self) -> Dict[str, str]:
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
if active_stacks is not None: #If we have a global stack.
|
if active_stacks is not None: #If we have a global stack.
|
||||||
for stack in active_stacks:
|
for stack in active_stacks:
|
||||||
|
|
@ -548,10 +581,8 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
|
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
|
||||||
def allActiveMaterialIds(self) -> Dict[str, str]:
|
def allActiveMaterialIds(self) -> Dict[str, str]:
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
|
|
||||||
result[self._global_container_stack.getId()] = self._global_container_stack.material.getId()
|
|
||||||
|
|
||||||
if active_stacks is not None: # If we have extruder stacks
|
if active_stacks is not None: # If we have extruder stacks
|
||||||
for stack in active_stacks:
|
for stack in active_stacks:
|
||||||
material_container = stack.material
|
material_container = stack.material
|
||||||
|
|
@ -703,10 +734,7 @@ class MachineManager(QObject):
|
||||||
## Check if a container is read_only
|
## Check if a container is read_only
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def isReadOnly(self, container_id: str) -> bool:
|
def isReadOnly(self, container_id: str) -> bool:
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
return ContainerRegistry.getInstance().isReadOnly(container_id)
|
||||||
if not containers or not self._active_container_stack:
|
|
||||||
return True
|
|
||||||
return containers[0].isReadOnly()
|
|
||||||
|
|
||||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
|
|
@ -733,6 +761,9 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
old_material = self._active_container_stack.material
|
old_material = self._active_container_stack.material
|
||||||
old_quality = self._active_container_stack.quality
|
old_quality = self._active_container_stack.quality
|
||||||
|
old_quality_type = None
|
||||||
|
if old_quality and old_quality.getId() != self._empty_quality_container.getId():
|
||||||
|
old_quality_type = old_quality.getMetaDataEntry("quality_type")
|
||||||
old_quality_changes = self._active_container_stack.qualityChanges
|
old_quality_changes = self._active_container_stack.qualityChanges
|
||||||
if not old_material:
|
if not old_material:
|
||||||
Logger.log("w", "While trying to set the active material, no material was found to replace it.")
|
Logger.log("w", "While trying to set the active material, no material was found to replace it.")
|
||||||
|
|
@ -771,15 +802,30 @@ class MachineManager(QObject):
|
||||||
if quality_type:
|
if quality_type:
|
||||||
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
|
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
|
||||||
quality_manager.getWholeMachineDefinition(global_stack.definition),
|
quality_manager.getWholeMachineDefinition(global_stack.definition),
|
||||||
[material_container])
|
[material_container.getMetaData()])
|
||||||
|
|
||||||
if not candidate_quality or isinstance(candidate_quality, type(self._empty_quality_changes_container)):
|
if not candidate_quality or candidate_quality.getId() == self._empty_quality_changes_container:
|
||||||
Logger.log("d", "Attempting to find fallback quality")
|
Logger.log("d", "Attempting to find fallback quality")
|
||||||
# Fall back to a quality (which must be compatible with all other extruders)
|
# Fall back to a quality (which must be compatible with all other extruders)
|
||||||
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders(
|
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders(
|
||||||
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks())
|
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks())
|
||||||
if new_qualities:
|
|
||||||
new_quality_id = new_qualities[0].getId() # Just pick the first available one
|
quality_types = sorted([q.getMetaDataEntry("quality_type") for q in new_qualities], reverse = True)
|
||||||
|
quality_type_to_use = None
|
||||||
|
if quality_types:
|
||||||
|
# try to use the same quality as before, otherwise the first one in the quality_types
|
||||||
|
quality_type_to_use = quality_types[0]
|
||||||
|
if old_quality_type is not None and old_quality_type in quality_type_to_use:
|
||||||
|
quality_type_to_use = old_quality_type
|
||||||
|
|
||||||
|
new_quality = None
|
||||||
|
for q in new_qualities:
|
||||||
|
if quality_type_to_use is not None and q.getMetaDataEntry("quality_type") == quality_type_to_use:
|
||||||
|
new_quality = q
|
||||||
|
break
|
||||||
|
|
||||||
|
if new_quality is not None:
|
||||||
|
new_quality_id = new_quality.getId() # Just pick the first available one
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "No quality profile found that matches the current machine and extruders.")
|
Logger.log("w", "No quality profile found that matches the current machine and extruders.")
|
||||||
else:
|
else:
|
||||||
|
|
@ -804,7 +850,7 @@ class MachineManager(QObject):
|
||||||
preferred_material_name = None
|
preferred_material_name = None
|
||||||
if old_material:
|
if old_material:
|
||||||
preferred_material_name = old_material.getName()
|
preferred_material_name = old_material.getName()
|
||||||
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.getBottom(), self._global_container_stack, containers[0], preferred_material_name).id
|
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.definition, self._global_container_stack, containers[0], preferred_material_name).id
|
||||||
self.setActiveMaterial(preferred_material_id)
|
self.setActiveMaterial(preferred_material_id)
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
|
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
|
||||||
|
|
@ -816,15 +862,15 @@ class MachineManager(QObject):
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
|
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id)
|
||||||
if not containers or not self._global_container_stack:
|
if not containers or not self._global_container_stack:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Quality profile come in two flavours: type=quality and type=quality_changes
|
# Quality profile come in two flavours: type=quality and type=quality_changes
|
||||||
# If we found a quality_changes profile then look up its parent quality profile.
|
# If we found a quality_changes profile then look up its parent quality profile.
|
||||||
container_type = containers[0].getMetaDataEntry("type")
|
container_type = containers[0].get("type")
|
||||||
quality_name = containers[0].getName()
|
quality_name = containers[0]["name"]
|
||||||
quality_type = containers[0].getMetaDataEntry("quality_type")
|
quality_type = containers[0].get("quality_type")
|
||||||
|
|
||||||
# Get quality container and optionally the quality_changes container.
|
# Get quality container and optionally the quality_changes container.
|
||||||
if container_type == "quality":
|
if container_type == "quality":
|
||||||
|
|
@ -832,7 +878,7 @@ class MachineManager(QObject):
|
||||||
elif container_type == "quality_changes":
|
elif container_type == "quality_changes":
|
||||||
new_quality_settings_list = self._determineQualityAndQualityChangesForQualityChanges(quality_name)
|
new_quality_settings_list = self._determineQualityAndQualityChangesForQualityChanges(quality_name)
|
||||||
else:
|
else:
|
||||||
Logger.log("e", "Tried to set quality to a container that is not of the right type")
|
Logger.log("e", "Tried to set quality to a container that is not of the right type: {container_id}".format(container_id = containers[0]["id"]))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if it was at all possible to find new settings
|
# Check if it was at all possible to find new settings
|
||||||
|
|
@ -921,18 +967,18 @@ class MachineManager(QObject):
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
|
|
||||||
# find qualities for extruders
|
# find qualities for extruders
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks:
|
||||||
material = extruder_stack.material
|
material_metadata = extruder_stack.material.getMetaData()
|
||||||
|
|
||||||
# TODO: fix this
|
# TODO: fix this
|
||||||
if self._new_material_container and extruder_stack.getId() == self._active_container_stack.getId():
|
if self._new_material_container and extruder_stack.getId() == self._active_container_stack.getId():
|
||||||
material = self._new_material_container
|
material_metadata = self._new_material_container.getMetaData()
|
||||||
|
|
||||||
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
|
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material_metadata])
|
||||||
|
|
||||||
if not quality:
|
if not quality:
|
||||||
# No quality profile is found for this quality type.
|
# No quality profile is found for this quality type.
|
||||||
|
|
@ -982,12 +1028,6 @@ class MachineManager(QObject):
|
||||||
Logger.log("e", "Could not find the global quality changes container with name %s", quality_changes_name)
|
Logger.log("e", "Could not find the global quality changes container with name %s", quality_changes_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
material = global_container_stack.material
|
|
||||||
|
|
||||||
# find a quality type that matches both machine and materials
|
|
||||||
if self._new_material_container and self._active_container_stack.getId() == global_container_stack.getId():
|
|
||||||
material = self._new_material_container
|
|
||||||
|
|
||||||
# For the global stack, find a quality which matches the quality_type in
|
# For the global stack, find a quality which matches the quality_type in
|
||||||
# the quality changes profile and also satisfies any material constraints.
|
# the quality changes profile and also satisfies any material constraints.
|
||||||
quality_type = global_quality_changes.getMetaDataEntry("quality_type")
|
quality_type = global_quality_changes.getMetaDataEntry("quality_type")
|
||||||
|
|
@ -1007,12 +1047,12 @@ class MachineManager(QObject):
|
||||||
if not quality_changes:
|
if not quality_changes:
|
||||||
quality_changes = self._empty_quality_changes_container
|
quality_changes = self._empty_quality_changes_container
|
||||||
|
|
||||||
material = extruder_stack.material
|
material_metadata = extruder_stack.material.getMetaData()
|
||||||
|
|
||||||
if self._new_material_container and self._active_container_stack.getId() == extruder_stack.getId():
|
if self._new_material_container and self._active_container_stack.getId() == extruder_stack.getId():
|
||||||
material = self._new_material_container
|
material_metadata = self._new_material_container.getMetaData()
|
||||||
|
|
||||||
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
|
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material_metadata])
|
||||||
|
|
||||||
if not quality:
|
if not quality:
|
||||||
# No quality profile found for this quality type.
|
# No quality profile found for this quality type.
|
||||||
|
|
@ -1025,7 +1065,7 @@ class MachineManager(QObject):
|
||||||
})
|
})
|
||||||
|
|
||||||
# append the global quality changes
|
# append the global quality changes
|
||||||
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material], global_quality = "True")
|
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, global_quality = "True")
|
||||||
|
|
||||||
# if there is not global quality but we're using a single extrusion machine, copy the quality of the first extruder - CURA-4482
|
# if there is not global quality but we're using a single extrusion machine, copy the quality of the first extruder - CURA-4482
|
||||||
if not global_quality and len(extruder_stacks) == 1:
|
if not global_quality and len(extruder_stacks) == 1:
|
||||||
|
|
@ -1079,18 +1119,14 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
def activeDefinitionId(self) -> str:
|
def activeDefinitionId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
definition = self._global_container_stack.getBottom()
|
return self._global_container_stack.definition.id
|
||||||
if definition:
|
|
||||||
return definition.id
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify=globalContainerChanged)
|
@pyqtProperty(str, notify=globalContainerChanged)
|
||||||
def activeDefinitionName(self) -> str:
|
def activeDefinitionName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
definition = self._global_container_stack.getBottom()
|
return self._global_container_stack.definition.getName()
|
||||||
if definition:
|
|
||||||
return definition.getName()
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
@ -1100,7 +1136,7 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
def activeQualityDefinitionId(self) -> str:
|
def activeQualityDefinitionId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self.getQualityDefinitionId(self._global_container_stack.getBottom())
|
return self.getQualityDefinitionId(self._global_container_stack.definition)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
## Get the Definition ID to use to select quality profiles for machines of the specified definition
|
## Get the Definition ID to use to select quality profiles for machines of the specified definition
|
||||||
|
|
@ -1118,7 +1154,7 @@ class MachineManager(QObject):
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
variant = self._active_container_stack.variant
|
variant = self._active_container_stack.variant
|
||||||
if variant:
|
if variant:
|
||||||
return self.getQualityVariantId(self._global_container_stack.getBottom(), variant)
|
return self.getQualityVariantId(self._global_container_stack.definition, variant)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
## Get the Variant ID to use to select quality profiles for variants of the specified definitions
|
## Get the Variant ID to use to select quality profiles for variants of the specified definitions
|
||||||
|
|
@ -1142,7 +1178,7 @@ class MachineManager(QObject):
|
||||||
def activeDefinitionVariantsName(self) -> str:
|
def activeDefinitionVariantsName(self) -> str:
|
||||||
fallback_title = catalog.i18nc("@label", "Nozzle")
|
fallback_title = catalog.i18nc("@label", "Nozzle")
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getBottom().getMetaDataEntry("variants_name", fallback_title)
|
return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title)
|
||||||
|
|
||||||
return fallback_title
|
return fallback_title
|
||||||
|
|
||||||
|
|
@ -1151,7 +1187,7 @@ class MachineManager(QObject):
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
machine_stack = container_registry.findContainerStacks(id = machine_id)
|
machine_stack = container_registry.findContainerStacks(id = machine_id)
|
||||||
if machine_stack:
|
if machine_stack:
|
||||||
new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].getBottom().getName())
|
new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].definition.getName())
|
||||||
machine_stack[0].setName(new_name)
|
machine_stack[0].setName(new_name)
|
||||||
self.globalContainerChanged.emit()
|
self.globalContainerChanged.emit()
|
||||||
|
|
||||||
|
|
@ -1162,15 +1198,15 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
# activate a new machine before removing a machine because this is safer
|
# activate a new machine before removing a machine because this is safer
|
||||||
if activate_new_machine:
|
if activate_new_machine:
|
||||||
machine_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
machine_stacks = ContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
||||||
other_machine_stacks = [s for s in machine_stacks if s.getId() != machine_id]
|
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
||||||
if other_machine_stacks:
|
if other_machine_stacks:
|
||||||
self.setActiveMachine(other_machine_stacks[0].getId())
|
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||||
|
|
||||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", machine = machine_id)
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
ContainerRegistry.getInstance().removeContainer(container.getId())
|
ContainerRegistry.getInstance().removeContainer(container["id"])
|
||||||
ContainerRegistry.getInstance().removeContainer(machine_id)
|
ContainerRegistry.getInstance().removeContainer(machine_id)
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
|
|
@ -1207,9 +1243,9 @@ class MachineManager(QObject):
|
||||||
# \returns DefinitionID (string) if found, None otherwise
|
# \returns DefinitionID (string) if found, None otherwise
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def getDefinitionByMachineId(self, machine_id: str) -> str:
|
def getDefinitionByMachineId(self, machine_id: str) -> str:
|
||||||
containers = ContainerRegistry.getInstance().findContainerStacks(id=machine_id)
|
containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||||
if containers:
|
if containers:
|
||||||
return containers[0].getBottom().getId()
|
return containers[0].definition.getId()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def createMachineManager():
|
def createMachineManager():
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# 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 typing import Any, List
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry #To listen for changes to the materials.
|
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.
|
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel #We're extending this class.
|
||||||
|
|
||||||
|
|
@ -18,8 +19,19 @@ class MaterialsModel(InstanceContainersModel):
|
||||||
# \param container The container whose metadata was changed.
|
# \param container The container whose metadata was changed.
|
||||||
def _onContainerMetaDataChanged(self, container):
|
def _onContainerMetaDataChanged(self, container):
|
||||||
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
|
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
|
||||||
self._update()
|
self._container_change_timer.start()
|
||||||
|
|
||||||
def _onContainerChanged(self, container):
|
def _onContainerChanged(self, container):
|
||||||
if container.getMetaDataEntry("type", "") == "material":
|
if container.getMetaDataEntry("type", "") == "material":
|
||||||
super()._onContainerChanged(container)
|
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
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
||||||
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
||||||
|
|
||||||
|
self._empty_quality = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
|
||||||
|
|
||||||
# Factory function, used by QML
|
# Factory function, used by QML
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def createProfilesModel(engine, js_engine):
|
def createProfilesModel(engine, js_engine):
|
||||||
|
|
@ -49,6 +51,10 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
ProfilesModel.__instance = cls()
|
ProfilesModel.__instance = cls()
|
||||||
return ProfilesModel.__instance
|
return ProfilesModel.__instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def hasInstance(cls) -> bool:
|
||||||
|
return ProfilesModel.__instance is not None
|
||||||
|
|
||||||
__instance = None # type: "ProfilesModel"
|
__instance = None # type: "ProfilesModel"
|
||||||
|
|
||||||
## Fetch the list of containers to display.
|
## Fetch the list of containers to display.
|
||||||
|
|
@ -57,8 +63,7 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
def _fetchInstanceContainers(self):
|
def _fetchInstanceContainers(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack is None:
|
if global_container_stack is None:
|
||||||
return []
|
return {}, {}
|
||||||
|
|
||||||
global_stack_definition = global_container_stack.definition
|
global_stack_definition = global_container_stack.definition
|
||||||
|
|
||||||
# Get the list of extruders and place the selected extruder at the front of the list.
|
# Get the list of extruders and place the selected extruder at the front of the list.
|
||||||
|
|
@ -69,11 +74,18 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
# The actual list of quality profiles come from the first extruder in the extruder list.
|
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||||
result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
||||||
|
|
||||||
|
# append empty quality if it's not there
|
||||||
|
if not any(q.getId() == self._empty_quality.getId() for q in result):
|
||||||
|
result.append(self._empty_quality)
|
||||||
|
|
||||||
# The usable quality types are set
|
# The usable quality types are set
|
||||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in result])
|
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
|
# Fetch all qualities available for this machine and the materials selected in extruders
|
||||||
all_qualities = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition, materials)
|
all_qualities = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition, materials)
|
||||||
|
# append empty quality if it's not there
|
||||||
|
if not any(q.getId() == self._empty_quality.getId() for q in all_qualities):
|
||||||
|
all_qualities.append(self._empty_quality)
|
||||||
|
|
||||||
# If in the all qualities there is some of them that are not available due to incompatibility with 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
|
# we also add it so that they will appear in the slide quality bar. However in recomputeItems will be marked as
|
||||||
|
|
@ -82,17 +94,10 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
|
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
|
||||||
result.append(quality)
|
result.append(quality)
|
||||||
|
|
||||||
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
|
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.
|
||||||
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 result
|
|
||||||
|
|
||||||
## Re-computes the items in this model, and adds the layer height role.
|
## Re-computes the items in this model, and adds the layer height role.
|
||||||
def _recomputeItems(self):
|
def _recomputeItems(self):
|
||||||
|
|
||||||
# Some globals that we can re-use.
|
# Some globals that we can re-use.
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack is None:
|
if global_container_stack is None:
|
||||||
|
|
@ -112,8 +117,11 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
# active machine and material, and later yield the right ones.
|
# active machine and material, and later yield the right ones.
|
||||||
tmp_all_quality_items = OrderedDict()
|
tmp_all_quality_items = OrderedDict()
|
||||||
for item in super()._recomputeItems():
|
for item in super()._recomputeItems():
|
||||||
profile = container_registry.findContainers(id=item["id"])
|
profiles = container_registry.findContainersMetadata(id = item["id"])
|
||||||
quality_type = profile[0].getMetaDataEntry("quality_type") if profile else ""
|
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:
|
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] = {"suitable_container": None, "all_containers": []}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class QualityAndUserProfilesModel(ProfilesModel):
|
||||||
def _fetchInstanceContainers(self):
|
def _fetchInstanceContainers(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return []
|
return {}, {}
|
||||||
|
|
||||||
# Fetch the list of quality changes.
|
# Fetch the list of quality changes.
|
||||||
quality_manager = QualityManager.getInstance()
|
quality_manager = QualityManager.getInstance()
|
||||||
|
|
@ -35,10 +35,14 @@ class QualityAndUserProfilesModel(ProfilesModel):
|
||||||
|
|
||||||
# Filter the quality_change by the list of available quality_types
|
# Filter the quality_change by the list of available quality_types
|
||||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||||
filtered_quality_changes = [qc for qc in quality_changes_list if
|
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
|
||||||
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
||||||
qc.getMetaDataEntry("extruder") is not None and
|
qc.getMetaDataEntry("extruder") is not None and
|
||||||
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
||||||
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
|
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
|
||||||
|
|
||||||
return quality_list + filtered_quality_changes
|
result = filtered_quality_changes
|
||||||
|
for q in quality_list:
|
||||||
|
if q.getId() != "empty_quality":
|
||||||
|
result[q.getId()] = q
|
||||||
|
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +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 collections
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
@ -42,6 +40,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
self.addRoleName(self.UserValueRole, "user_value")
|
self.addRoleName(self.UserValueRole, "user_value")
|
||||||
self.addRoleName(self.CategoryRole, "category")
|
self.addRoleName(self.CategoryRole, "category")
|
||||||
|
|
||||||
|
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||||
|
|
||||||
def setExtruderId(self, extruder_id):
|
def setExtruderId(self, extruder_id):
|
||||||
if extruder_id != self._extruder_id:
|
if extruder_id != self._extruder_id:
|
||||||
self._extruder_id = extruder_id
|
self._extruder_id = extruder_id
|
||||||
|
|
@ -92,7 +92,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
settings = collections.OrderedDict()
|
|
||||||
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
|
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
|
||||||
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
||||||
|
|
@ -108,77 +107,87 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
else:
|
else:
|
||||||
quality_changes_container = containers[0]
|
quality_changes_container = containers[0]
|
||||||
|
|
||||||
criteria = {
|
if quality_changes_container.getMetaDataEntry("quality_type") == "not_supported":
|
||||||
"type": "quality",
|
quality_container = self._empty_quality
|
||||||
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
else:
|
||||||
"definition": quality_changes_container.getDefinition().getId()
|
criteria = {
|
||||||
}
|
"type": "quality",
|
||||||
|
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
||||||
|
"definition": quality_changes_container.getDefinition().getId()
|
||||||
|
}
|
||||||
|
|
||||||
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||||
if not quality_container:
|
if not quality_container:
|
||||||
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||||
return
|
return
|
||||||
quality_container = quality_container[0]
|
|
||||||
|
quality_container = quality_container[0]
|
||||||
|
|
||||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
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.
|
if quality_type == "not_supported":
|
||||||
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
containers = []
|
||||||
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
|
else:
|
||||||
if catalog.hasTranslationLoaded():
|
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
||||||
self._i18n_catalog = catalog
|
definition = quality_container.getDefinition()
|
||||||
|
|
||||||
for file_name in quality_container.getDefinition().getInheritedFiles():
|
# Check if the definition container has a translation file.
|
||||||
catalog = i18nCatalog(os.path.basename(file_name))
|
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
||||||
|
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
|
||||||
if catalog.hasTranslationLoaded():
|
if catalog.hasTranslationLoaded():
|
||||||
self._i18n_catalog = catalog
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
for file_name in quality_container.getDefinition().getInheritedFiles():
|
||||||
|
catalog = i18nCatalog(os.path.basename(file_name))
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
if self._material_id and self._material_id != "empty_material":
|
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
||||||
criteria["material"] = self._material_id
|
|
||||||
|
|
||||||
criteria["extruder"] = self._extruder_id
|
if self._material_id and self._material_id != "empty_material":
|
||||||
|
criteria["material"] = self._material_id
|
||||||
|
|
||||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
criteria["extruder"] = self._extruder_id
|
||||||
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)
|
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:
|
if not containers and "material" in criteria:
|
||||||
# Try again, this time without material or extruder
|
# Try again, this time without material
|
||||||
criteria.pop("extruder") # "material" has already been popped
|
criteria.pop("material", None)
|
||||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
|
||||||
if not containers:
|
if not containers:
|
||||||
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
# Try again, this time without material or extruder
|
||||||
return
|
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:
|
if quality_changes_container:
|
||||||
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
|
if quality_type == "not_supported":
|
||||||
if self._extruder_definition_id != "":
|
criteria = {"type": "quality_changes", "quality_type": quality_type, "name": quality_changes_container.getName()}
|
||||||
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:
|
else:
|
||||||
criteria["extruder"] = None
|
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)
|
changes = self._container_registry.findInstanceContainers(**criteria)
|
||||||
if changes:
|
if changes:
|
||||||
containers.extend(changes)
|
containers.extend(changes)
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
|
||||||
|
|
||||||
current_category = ""
|
current_category = ""
|
||||||
for definition in definition_container.findDefinitions():
|
for definition in definition_container.findDefinitions():
|
||||||
|
|
@ -214,15 +223,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
if profile_value is None and user_value is None:
|
if profile_value is None and user_value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if is_multi_extrusion:
|
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
|
||||||
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 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:
|
||||||
if self._extruder_id != "" and not settable_per_extruder:
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
|
# 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:
|
if self._extruder_id == "" and settable_per_extruder:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
label = definition.label
|
label = definition.label
|
||||||
if self._i18n_catalog:
|
if self._i18n_catalog:
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,26 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
## Event indicating that the user selected a different extruder.
|
## Event indicating that the user selected a different extruder.
|
||||||
activeExtruderChanged = Signal()
|
activeExtruderChanged = Signal()
|
||||||
|
|
||||||
|
## Non-printing meshes
|
||||||
|
#
|
||||||
|
# If these settings are True for any mesh, the mesh does not need a convex hull,
|
||||||
|
# and is sent to the slicer regardless of whether it fits inside the build volume.
|
||||||
|
# Note that Support Mesh is not in here because it actually generates
|
||||||
|
# g-code in the volume of the mesh.
|
||||||
|
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||||
|
|
||||||
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._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)
|
||||||
|
|
@ -49,6 +59,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.
|
||||||
|
|
@ -72,9 +86,14 @@ 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":
|
||||||
|
self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# 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 UM.Application import Application
|
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
from cura.Settings.ProfilesModel import ProfilesModel
|
from cura.Settings.ProfilesModel import ProfilesModel
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
@ -12,13 +12,23 @@ class UserProfilesModel(ProfilesModel):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
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.
|
## Fetch the list of containers to display.
|
||||||
#
|
#
|
||||||
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||||
def _fetchInstanceContainers(self):
|
def _fetchInstanceContainers(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return []
|
return {}, {}
|
||||||
|
|
||||||
# Fetch the list of quality changes.
|
# Fetch the list of quality changes.
|
||||||
quality_manager = QualityManager.getInstance()
|
quality_manager = QualityManager.getInstance()
|
||||||
|
|
@ -35,10 +45,36 @@ class UserProfilesModel(ProfilesModel):
|
||||||
|
|
||||||
# Filter the quality_change by the list of available quality_types
|
# Filter the quality_change by the list of available quality_types
|
||||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||||
filtered_quality_changes = [qc for qc in quality_changes_list if
|
|
||||||
|
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
|
||||||
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
||||||
qc.getMetaDataEntry("extruder") is not None and
|
qc.getMetaDataEntry("extruder") is not None and
|
||||||
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
||||||
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
|
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
|
||||||
|
|
||||||
return filtered_quality_changes
|
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)
|
||||||
22
cura/Stages/CuraStage.py
Normal file
22
cura/Stages/CuraStage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QUrl, QObject
|
||||||
|
|
||||||
|
from UM.Stage import Stage
|
||||||
|
|
||||||
|
class CuraStage(Stage):
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def stageId(self):
|
||||||
|
return self.getPluginId()
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def mainComponent(self):
|
||||||
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def sidebarComponent(self):
|
||||||
|
return self.getDisplayComponent("sidebar")
|
||||||
2
cura/Stages/__init__.py
Normal file
2
cura/Stages/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
71
cura_app.py
71
cura_app.py
|
|
@ -2,29 +2,58 @@
|
||||||
|
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 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 argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
|
||||||
import faulthandler
|
|
||||||
|
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog = "cura",
|
||||||
|
add_help = False)
|
||||||
|
parser.add_argument('--debug',
|
||||||
|
action='store_true',
|
||||||
|
default = False,
|
||||||
|
help = "Turn on the debug mode by setting this option."
|
||||||
|
)
|
||||||
|
known_args = vars(parser.parse_known_args()[0])
|
||||||
|
|
||||||
|
if not known_args["debug"]:
|
||||||
|
def get_cura_dir_path():
|
||||||
|
if Platform.isWindows():
|
||||||
|
return os.path.expanduser("~/AppData/Roaming/cura/")
|
||||||
|
elif Platform.isLinux():
|
||||||
|
return os.path.expanduser("~/.local/share/cura")
|
||||||
|
elif Platform.isOSX():
|
||||||
|
return os.path.expanduser("~/Library/Logs/cura")
|
||||||
|
|
||||||
|
if hasattr(sys, "frozen"):
|
||||||
|
dirpath = get_cura_dir_path()
|
||||||
|
os.makedirs(dirpath, exist_ok = True)
|
||||||
|
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
||||||
|
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
|
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
|
||||||
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
|
||||||
if platform.linux_distribution()[0] in ("debian", "Ubuntu", "LinuxMint"): # 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.
|
linux_distro_name = platform.linux_distribution()[0].lower()
|
||||||
import ctypes
|
# 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.
|
||||||
from ctypes.util import find_library
|
import ctypes
|
||||||
libGL = find_library("GL")
|
from ctypes.util import find_library
|
||||||
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
libGL = find_library("GL")
|
||||||
|
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"):
|
||||||
try:
|
try:
|
||||||
del os.environ["PYTHONPATH"]
|
del os.environ["PYTHONPATH"]
|
||||||
except KeyError: pass
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
#WORKAROUND: GITHUB-704 GITHUB-708
|
# WORKAROUND: GITHUB-704 GITHUB-708
|
||||||
# It looks like setuptools creates a .pth file in
|
# It looks like setuptools creates a .pth file in
|
||||||
# the default /usr/lib which causes the default site-packages
|
# the default /usr/lib which causes the default site-packages
|
||||||
# to be inserted into sys.path before PYTHONPATH.
|
# to be inserted into sys.path before PYTHONPATH.
|
||||||
|
|
@ -45,7 +74,8 @@ def exceptHook(hook_type, value, traceback):
|
||||||
_crash_handler = CrashHandler(hook_type, value, traceback)
|
_crash_handler = CrashHandler(hook_type, value, traceback)
|
||||||
_crash_handler.show()
|
_crash_handler.show()
|
||||||
|
|
||||||
sys.excepthook = exceptHook
|
if not known_args["debug"]:
|
||||||
|
sys.excepthook = exceptHook
|
||||||
|
|
||||||
# Workaround for a race condition on certain systems where there
|
# Workaround for a race condition on certain systems where there
|
||||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||||
|
|
@ -55,29 +85,14 @@ import Arcus #@UnusedImport
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
import cura.Settings.CuraContainerRegistry
|
import cura.Settings.CuraContainerRegistry
|
||||||
|
|
||||||
def get_cura_dir_path():
|
|
||||||
if Platform.isWindows():
|
|
||||||
return os.path.expanduser("~/AppData/Local/cura/")
|
|
||||||
elif Platform.isLinux():
|
|
||||||
return os.path.expanduser("~/.local/share/cura")
|
|
||||||
elif Platform.isOSX():
|
|
||||||
return os.path.expanduser("~/Library/Logs/cura")
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(sys, "frozen"):
|
|
||||||
dirpath = get_cura_dir_path()
|
|
||||||
os.makedirs(dirpath, exist_ok = True)
|
|
||||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
|
||||||
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
|
||||||
|
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
|
|
||||||
# Force an instance of CuraContainerRegistry to be created and reused later.
|
# Force an instance of CuraContainerRegistry to be created and reused later.
|
||||||
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
|
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
|
||||||
|
|
||||||
# This prestart up check is needed to determine if we should start the application at all.
|
# This pre-start up check is needed to determine if we should start the application at all.
|
||||||
if not cura.CuraApplication.CuraApplication.preStartUp():
|
if not cura.CuraApplication.CuraApplication.preStartUp(parser = parser, parsed_command_line = known_args):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
app = cura.CuraApplication.CuraApplication.getInstance()
|
app = cura.CuraApplication.CuraApplication.getInstance(parser = parser, parsed_command_line = known_args)
|
||||||
app.run()
|
app.run()
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,7 @@ Section "Install Arduino Drivers"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Open STL files with Cura"
|
Section "Open STL files with Cura"
|
||||||
WriteRegStr HKCR .stl "" "Cura STL model file"
|
${registerExtension} "$INSTDIR\Cura.exe" ".stl" "STL_File"
|
||||||
DeleteRegValue HKCR .stl "Content Type"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\shell" "" "open"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section /o "Open OBJ files with Cura"
|
Section /o "Open OBJ files with Cura"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
import os.path
|
import os.path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from UM.Job import Job
|
|
||||||
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
|
||||||
|
|
@ -15,9 +14,10 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
from cura.ZOffsetDecorator import ZOffsetDecorator
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
|
|
||||||
|
|
@ -43,6 +43,7 @@ class ThreeMFReader(MeshReader):
|
||||||
}
|
}
|
||||||
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 +78,12 @@ 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
|
||||||
|
|
||||||
|
um_node = CuraSceneNode()
|
||||||
|
um_node.addDecorator(BuildPlateDecorator(0))
|
||||||
|
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()
|
||||||
|
|
@ -117,7 +123,7 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
# Get the definition & set it
|
# Get the definition & set it
|
||||||
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
||||||
um_node.callDecoration("getStack").getTop().setDefinition(definition)
|
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
|
||||||
|
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
|
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
|
||||||
return WorkspaceReader.PreReadResult.failed
|
return WorkspaceReader.PreReadResult.failed
|
||||||
|
|
||||||
machine_name = ""
|
|
||||||
machine_type = ""
|
machine_type = ""
|
||||||
variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
|
variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
|
||||||
|
|
||||||
|
|
@ -133,9 +132,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
# A few lists of containers in this project files.
|
# A few lists of containers in this project files.
|
||||||
# When loading the global stack file, it may be associated with those containers, which may or may not be
|
# When loading the global stack file, it may be associated with those containers, which may or may not be
|
||||||
# in Cura already, so we need to provide them as alternative search lists.
|
# in Cura already, so we need to provide them as alternative search lists.
|
||||||
definition_container_list = []
|
|
||||||
instance_container_list = []
|
instance_container_list = []
|
||||||
material_container_list = []
|
|
||||||
|
|
||||||
resolve_strategy_keys = ["machine", "material", "quality_changes"]
|
resolve_strategy_keys = ["machine", "material", "quality_changes"]
|
||||||
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
|
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
|
||||||
|
|
@ -149,21 +146,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
for each_definition_container_file in definition_container_files:
|
for each_definition_container_file in definition_container_files:
|
||||||
container_id = self._stripFileToId(each_definition_container_file)
|
container_id = self._stripFileToId(each_definition_container_file)
|
||||||
definitions = self._container_registry.findDefinitionContainers(id=container_id)
|
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
||||||
|
|
||||||
if not definitions:
|
if not definitions:
|
||||||
definition_container = DefinitionContainer(container_id)
|
definition_container = DefinitionContainer(container_id)
|
||||||
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"),
|
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"), file_name = each_definition_container_file)
|
||||||
file_name = each_definition_container_file)
|
definition_container = definition_container.getMetaData()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
definition_container = definitions[0]
|
definition_container = definitions[0]
|
||||||
definition_container_list.append(definition_container)
|
|
||||||
|
|
||||||
definition_container_type = definition_container.getMetaDataEntry("type")
|
definition_container_type = definition_container.get("type")
|
||||||
if definition_container_type == "machine":
|
if definition_container_type == "machine":
|
||||||
machine_type = definition_container.getName()
|
machine_type = definition_container["name"]
|
||||||
variant_type_name = definition_container.getMetaDataEntry("variants_name", variant_type_name)
|
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
||||||
|
|
||||||
machine_definition_container_count += 1
|
machine_definition_container_count += 1
|
||||||
elif definition_container_type == "extruder":
|
elif definition_container_type == "extruder":
|
||||||
|
|
@ -187,11 +183,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||||
for material_container_file in material_container_files:
|
for material_container_file in material_container_files:
|
||||||
container_id = self._stripFileToId(material_container_file)
|
container_id = self._stripFileToId(material_container_file)
|
||||||
materials = self._container_registry.findInstanceContainers(id=container_id)
|
|
||||||
material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
|
material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
|
||||||
if materials:
|
if self._container_registry.findContainersMetadata(id = container_id): #This material already exists.
|
||||||
containers_found_dict["material"] = True
|
containers_found_dict["material"] = True
|
||||||
if not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
|
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
|
||||||
material_conflict = True
|
material_conflict = True
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
|
@ -449,6 +444,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
extruder_stacks = []
|
extruder_stacks = []
|
||||||
extruder_stacks_added = []
|
extruder_stacks_added = []
|
||||||
container_stacks_added = []
|
container_stacks_added = []
|
||||||
|
machine_extruder_count = None
|
||||||
|
|
||||||
containers_added = []
|
containers_added = []
|
||||||
|
|
||||||
|
|
@ -461,16 +457,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
extruder_stack_id_map = {} # new and old ExtruderStack IDs map
|
extruder_stack_id_map = {} # new and old ExtruderStack IDs map
|
||||||
if self._resolve_strategies["machine"] == "new":
|
if self._resolve_strategies["machine"] == "new":
|
||||||
# We need a new id if the id already exists
|
# We need a new id if the id already exists
|
||||||
if self._container_registry.findContainerStacks(id = global_stack_id_original):
|
if self._container_registry.findContainerStacksMetadata(id = global_stack_id_original):
|
||||||
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)
|
||||||
new_container_id = old_container_id
|
new_container_id = old_container_id
|
||||||
if self._container_registry.findContainerStacks(id = old_container_id):
|
if self._container_registry.findContainerStacksMetadata(id = old_container_id):
|
||||||
# get a new name for this extruder
|
# get a new name for this extruder
|
||||||
new_container_id = self.getNewId(old_container_id)
|
new_container_id = self.getNewId(old_container_id)
|
||||||
|
|
||||||
|
|
@ -484,7 +481,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
for definition_container_file in definition_container_files:
|
for definition_container_file in definition_container_files:
|
||||||
container_id = self._stripFileToId(definition_container_file)
|
container_id = self._stripFileToId(definition_container_file)
|
||||||
definitions = self._container_registry.findDefinitionContainers(id = container_id)
|
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
definition_container = DefinitionContainer(container_id)
|
definition_container = DefinitionContainer(container_id)
|
||||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
||||||
|
|
@ -511,7 +508,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
containers_to_add.append(material_container)
|
containers_to_add.append(material_container)
|
||||||
else:
|
else:
|
||||||
material_container = materials[0]
|
material_container = materials[0]
|
||||||
if not material_container.isReadOnly(): # Only create new materials if they are not read only.
|
if not self._container_registry.isReadOnly(container_id): # Only create new materials if they are not read only.
|
||||||
if self._resolve_strategies["material"] == "override":
|
if self._resolve_strategies["material"] == "override":
|
||||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
||||||
file_name = material_container_file)
|
file_name = material_container_file)
|
||||||
|
|
@ -578,7 +575,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if old_extruder_id:
|
if old_extruder_id:
|
||||||
new_extruder_id = extruder_stack_id_map[old_extruder_id]
|
new_extruder_id = extruder_stack_id_map[old_extruder_id]
|
||||||
new_id = new_extruder_id + "_current_settings"
|
new_id = new_extruder_id + "_current_settings"
|
||||||
instance_container._id = new_id
|
instance_container.setMetaDataEntry("id", new_id)
|
||||||
instance_container.setName(new_id)
|
instance_container.setName(new_id)
|
||||||
instance_container.setMetaDataEntry("extruder", new_extruder_id)
|
instance_container.setMetaDataEntry("extruder", new_extruder_id)
|
||||||
containers_to_add.append(instance_container)
|
containers_to_add.append(instance_container)
|
||||||
|
|
@ -587,7 +584,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._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)
|
||||||
|
|
@ -612,7 +609,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!!!
|
||||||
|
|
@ -636,8 +633,14 @@ 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 == "definition_changes":
|
||||||
|
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
|
||||||
|
if definition_changes_extruder_count is not None:
|
||||||
|
machine_extruder_count = definition_changes_extruder_count
|
||||||
|
|
||||||
else:
|
else:
|
||||||
existing_container = self._container_registry.findInstanceContainers(id = container_id)
|
existing_container = self._container_registry.findInstanceContainersMetadata(id = container_id)
|
||||||
if not existing_container:
|
if not existing_container:
|
||||||
containers_to_add.append(instance_container)
|
containers_to_add.append(instance_container)
|
||||||
if global_stack_need_rename:
|
if global_stack_need_rename:
|
||||||
|
|
@ -663,14 +666,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
|
|
||||||
# HACK
|
# HACK
|
||||||
# There is a machine, check if it has authentication data. If so, keep that data.
|
# There is a machine, check if it has authentication data. If so, keep that data.
|
||||||
network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
|
network_authentication_id = stack.getMetaDataEntry("network_authentication_id")
|
||||||
network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
|
network_authentication_key = stack.getMetaDataEntry("network_authentication_key")
|
||||||
container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"),
|
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"), file_name = global_stack_file)
|
||||||
file_name = global_stack_file)
|
|
||||||
if network_authentication_id:
|
if network_authentication_id:
|
||||||
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
|
stack.addMetaDataEntry("network_authentication_id", network_authentication_id)
|
||||||
if network_authentication_key:
|
if network_authentication_key:
|
||||||
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
|
stack.addMetaDataEntry("network_authentication_key", network_authentication_key)
|
||||||
|
|
||||||
elif self._resolve_strategies["machine"] == "new":
|
elif self._resolve_strategies["machine"] == "new":
|
||||||
# create a new global stack
|
# create a new global stack
|
||||||
|
|
@ -680,12 +682,7 @@ 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)
|
||||||
|
|
@ -739,7 +736,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)
|
||||||
|
|
@ -794,8 +791,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if stack.quality.getId() in ("empty", "empty_quality"):
|
if stack.quality.getId() in ("empty", "empty_quality"):
|
||||||
has_not_supported = True
|
has_not_supported = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# We filter out extruder stacks that are not actually used, for example the UM3 and custom FDM printer extruder count setting.
|
||||||
|
extruder_stacks_in_use = extruder_stacks
|
||||||
|
if machine_extruder_count is not None:
|
||||||
|
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
|
||||||
|
|
||||||
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
|
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
|
||||||
extruder_stacks)
|
extruder_stacks_in_use)
|
||||||
if not has_not_supported:
|
if not has_not_supported:
|
||||||
has_not_supported = not available_quality
|
has_not_supported = not available_quality
|
||||||
|
|
||||||
|
|
@ -803,10 +806,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
|
|
||||||
if has_not_supported:
|
if has_not_supported:
|
||||||
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||||
for stack in [global_stack] + extruder_stacks:
|
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]
|
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
|
||||||
for stack in [global_stack] + extruder_stacks:
|
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
|
||||||
|
|
||||||
|
|
@ -839,7 +842,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
global_stack.quality = containers[0]
|
global_stack.quality = containers[0]
|
||||||
global_stack.qualityChanges = empty_quality_changes_container
|
global_stack.qualityChanges = empty_quality_changes_container
|
||||||
# also find the quality containers for the extruders
|
# also find the quality containers for the extruders
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks_in_use:
|
||||||
search_criteria = {"id": preferred_quality_id,
|
search_criteria = {"id": preferred_quality_id,
|
||||||
"type": "quality",
|
"type": "quality",
|
||||||
"definition": definition_id}
|
"definition": definition_id}
|
||||||
|
|
@ -868,8 +871,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if len(extruder_stacks) == 1:
|
if len(extruder_stacks) == 1:
|
||||||
extruder_stack = extruder_stacks[0]
|
extruder_stack = extruder_stacks[0]
|
||||||
|
|
||||||
search_criteria = {"type": "quality",
|
search_criteria = {"type": "quality", "quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
|
||||||
"quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
|
|
||||||
search_criteria["definition"] = global_stack.definition.getId()
|
search_criteria["definition"] = global_stack.definition.getId()
|
||||||
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
|
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
|
||||||
search_criteria["definition"] = "fdmprinter"
|
search_criteria["definition"] = "fdmprinter"
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||||
version_config_parser = configparser.ConfigParser()
|
version_config_parser = configparser.ConfigParser()
|
||||||
version_config_parser.add_section("versions")
|
version_config_parser.add_section("versions")
|
||||||
version_config_parser.set("versions", "cura_version", Application.getStaticVersion())
|
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", "is_debug_mode", str(Application.getInstance().getIsDebugMode()))
|
||||||
|
|
||||||
version_file_string = StringIO()
|
version_file_string = StringIO()
|
||||||
version_config_parser.write(version_file_string)
|
version_config_parser.write(version_file_string)
|
||||||
|
|
@ -95,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"}
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -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().getBuildPlateModel().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)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,72 @@
|
||||||
|
[3.1.0]
|
||||||
|
*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.
|
||||||
|
|
||||||
|
*Profile added for Breakaway material
|
||||||
|
New material profile for Breakaway material, a new dry post processing support material, which can be used for models with flat surface area overhangs.
|
||||||
|
|
||||||
|
*Layer view
|
||||||
|
The existing Layer View has been updated in order to see a live simulation of all the paths within a layer.
|
||||||
|
|
||||||
|
*Quick camera controls
|
||||||
|
New buttons have been added to the interface that can quickly reposition the camera view of the buildplate.
|
||||||
|
|
||||||
|
*Lock model on platform
|
||||||
|
The move tool has a new option to lock a selected model to the platform.
|
||||||
|
|
||||||
|
*Faster profile switching speed
|
||||||
|
Duplicating and removing a profile could take Ultimaker Cura quite some time, it now happens instantly.
|
||||||
|
|
||||||
|
*Faster printer selection
|
||||||
|
Removing a printer from the library is now instant. No more unresponsive screens.
|
||||||
|
|
||||||
|
*Faster processing speed
|
||||||
|
A 5 - 10 % speed increase when calculating normals, loading models, and slicing.
|
||||||
|
|
||||||
|
*Feedrate visualization
|
||||||
|
Feedrate visualization has been added to the Layer view. Using this gives the user an idea of the print speeds per model part, allowing for better control over prints.
|
||||||
|
|
||||||
|
*Jogging
|
||||||
|
It allows the printhead to be moved with on-screen controls. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Large model loading
|
||||||
|
A new feature has been added which unloads the layer view when switching to solid mode, speeding Ultimaker Cura back up without losing your G-code/layer view information.
|
||||||
|
|
||||||
|
*Scripts folder
|
||||||
|
A scripts folder is now available in the Ultimaker Cura configuration folder. This folder can be loaded with post processing plugins scripts, which will automatically show in Ultimaker Cura. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Optimized workflow for crash reporting
|
||||||
|
Crash reports are automatically generated and allow the user, in case of a crash, to easily send their report with a description to developers.
|
||||||
|
|
||||||
|
*Floating models enabled
|
||||||
|
In previous releases, models were dropped to the build plate when support was disabled. Models now float when the setting is enabled (even if creates an impossible-to-print situation). This can be used to stack separate models on top of each other.
|
||||||
|
|
||||||
|
*Slicing tolerance
|
||||||
|
A new setting that affects the intersect point to influence the dimensional accuracy for diagonal surfaces. The user can select the behaviour: ‘Inclusive’ makes gaps narrower, ‘Exclusive’ makes gaps wider, and ‘Middle’ is the fastest to process. This can be used to create better tolerances for printed screw holes. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Optimized zig zag patterns
|
||||||
|
Zig zag patterns now print more consistently. Lines now have a 5 micron tolerance in which they are printed any way, resulting in longer connected lines. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Aligned z-seam inner wall moves
|
||||||
|
Inner wall travel moves are aligned with the z-seam. This reduces the number of travel moves and reduces the chance of more unwanted seams.
|
||||||
|
|
||||||
|
*Relative positioning of infill patterns
|
||||||
|
Infill patterns are now positioned relative to the center of loaded models and an offset can be applied to control the infill more precisely and adjust it to preference or strength. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Line resolution
|
||||||
|
Enables the user to specify the minimum allowed distance value between two points in G-code to create lower or higher resolution polygons.
|
||||||
|
|
||||||
|
*Custom mode changes
|
||||||
|
If profile settings have been modified in recommended mode under custom mode, a reset icon will appear to notify the user. Click the icon to show the changes that have been made, and revert back to the default profile settings.
|
||||||
|
|
||||||
|
*Bugfixes
|
||||||
|
- Fix for layer numbers being displayed incorrectly when switching between solid and layer mode
|
||||||
|
- Fix for Ultimaker Cura engine crashes on certain models
|
||||||
|
- Fix for Uninstalling previous versions of Cura on Windows platforms
|
||||||
|
- Fix for displaying visible settings
|
||||||
|
- Fix for importing legacy .ini files
|
||||||
|
- Prevent skipping user agreement dialog by pressing escape
|
||||||
|
|
||||||
[3.0.4]
|
[3.0.4]
|
||||||
*Bug fixes
|
*Bug fixes
|
||||||
- Fixed OpenGL issue that prevents Cura from starting.
|
- Fixed OpenGL issue that prevents Cura from starting.
|
||||||
|
|
@ -40,7 +109,7 @@ The build plate now shows graduations of 10 mm and 1 mm for easy model positioni
|
||||||
Extruder tabs have become buttons and icons have been updated.
|
Extruder tabs have become buttons and icons have been updated.
|
||||||
|
|
||||||
*Add an "Export to Cura" button in SOLIDWORKS
|
*Add an "Export to Cura" button in SOLIDWORKS
|
||||||
SOLIDWORKS plugin can now be installed using an automatic installer.
|
A macro can be added to your SOLIDWORKS installation that loads your model into Ultimaker Cura.
|
||||||
|
|
||||||
*Siemens NX macro
|
*Siemens NX macro
|
||||||
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
|
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
|
||||||
|
|
|
||||||
|
|
@ -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().getBuildPlateModel().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)
|
||||||
|
|
@ -104,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)
|
||||||
|
|
@ -173,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
|
||||||
|
|
||||||
|
|
@ -189,17 +193,37 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
## 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().getBuildPlateModel().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()
|
||||||
|
|
@ -209,12 +233,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)
|
||||||
|
|
||||||
|
|
@ -223,7 +251,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()
|
||||||
|
|
||||||
|
|
@ -338,7 +367,10 @@ 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)
|
||||||
|
pass
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
@ -362,7 +394,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
|
||||||
|
|
@ -374,33 +406,48 @@ 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
|
build_plate_changed = set()
|
||||||
|
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
|
||||||
if source == self._scene.getRoot():
|
if source == self._scene.getRoot():
|
||||||
num_objects = 0
|
# we got the root node
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
num_objects = self._numObjects()
|
||||||
# Only count sliceable objects
|
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
|
||||||
if node.callDecoration("isSliceable"):
|
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
|
||||||
num_objects += 1
|
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
|
||||||
if num_objects != self._last_num_objects:
|
build_plate_changed.add(build_plate_number)
|
||||||
self._last_num_objects = num_objects
|
else:
|
||||||
root_scene_nodes_changed = True
|
# we got a single scenenode
|
||||||
else:
|
if not source.callDecoration("isGroup"):
|
||||||
return
|
if source.getMeshData() is None:
|
||||||
|
return
|
||||||
|
if source.getMeshData().getVertices() is None:
|
||||||
|
return
|
||||||
|
|
||||||
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
|
build_plate_changed.add(source_build_plate_number)
|
||||||
if source.getMeshData() is None:
|
|
||||||
return
|
build_plate_changed.discard(None)
|
||||||
if source.getMeshData().getVertices() is None:
|
build_plate_changed.discard(-1) # object not on build plate
|
||||||
return
|
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
|
||||||
|
|
@ -408,9 +455,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.
|
||||||
#
|
#
|
||||||
|
|
@ -430,16 +486,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().getBuildPlateModel().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:
|
||||||
|
|
@ -461,7 +522,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()
|
||||||
|
|
||||||
|
|
@ -475,7 +536,7 @@ 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)
|
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.
|
||||||
#
|
#
|
||||||
|
|
@ -484,6 +545,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.
|
||||||
|
|
@ -491,36 +562,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().getBuildPlateModel().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):
|
||||||
|
|
@ -550,7 +631,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
|
||||||
#
|
#
|
||||||
|
|
@ -604,19 +685,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.getBuildPlateModel().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
|
||||||
|
|
||||||
|
|
@ -634,7 +721,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)
|
||||||
|
|
@ -645,14 +732,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):
|
||||||
|
|
|
||||||
|
|
@ -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().getBuildPlateModel().activeBuildPlate
|
||||||
|
gcode_list = self._scene.gcode_dict[active_build_plate_id]
|
||||||
|
gcode_list.append(self._message.data.decode("utf-8", "replace"))
|
||||||
|
|
|
||||||
|
|
@ -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,9 +60,18 @@ 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()
|
||||||
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
|
view = Application.getInstance().getController().getActiveView()
|
||||||
|
if view.getPluginId() == "SimulationView":
|
||||||
|
view.resetLayerData()
|
||||||
self._progress_message.show()
|
self._progress_message.show()
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
if self._abort_requested:
|
if self._abort_requested:
|
||||||
|
|
@ -71,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
|
||||||
|
|
@ -226,10 +227,6 @@ class ProcessSlicedLayersJob(Job):
|
||||||
if self._progress_message:
|
if self._progress_message:
|
||||||
self._progress_message.setProgress(100)
|
self._progress_message.setProgress(100)
|
||||||
|
|
||||||
view = Application.getInstance().getController().getActiveView()
|
|
||||||
if view.getPluginId() == "SimulationView":
|
|
||||||
view.resetLayerData()
|
|
||||||
|
|
||||||
if self._progress_message:
|
if self._progress_message:
|
||||||
self._progress_message.hide()
|
self._progress_message.hide()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,19 @@ 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 as SceneNode
|
||||||
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,9 +36,32 @@ 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 gcode, using global stack", key_fragments[1], key_fragments[0])
|
||||||
|
elif len(key_fragments) != 1:
|
||||||
|
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", 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 gcode", key)
|
||||||
return "{" + key + "}"
|
return "{" + key + "}"
|
||||||
|
|
@ -45,24 +72,22 @@ class GcodeStartEndFormatter(Formatter):
|
||||||
|
|
||||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||||
class StartSliceJob(Job):
|
class StartSliceJob(Job):
|
||||||
## Meshes that are sent to the engine regardless of being outside of the
|
|
||||||
# build volume.
|
|
||||||
#
|
|
||||||
# If these settings are True for any mesh, the build volume is ignored.
|
|
||||||
# Note that Support Mesh is not in here because it actually generates
|
|
||||||
# g-code in the volume of the mesh.
|
|
||||||
_not_printed_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
|
||||||
|
|
||||||
def __init__(self, slice_message):
|
def __init__(self, slice_message):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
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):
|
||||||
|
|
@ -79,6 +104,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)
|
||||||
|
|
@ -112,7 +141,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
|
||||||
|
|
||||||
|
|
@ -139,13 +168,27 @@ class StartSliceJob(Job):
|
||||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||||
else:
|
else:
|
||||||
temp_list = []
|
temp_list = []
|
||||||
|
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 type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||||
if not getattr(node, "_outside_buildarea", False)\
|
per_object_stack = node.callDecoration("getStack")
|
||||||
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
|
is_non_printing_mesh = False
|
||||||
temp_list.append(node)
|
if per_object_stack:
|
||||||
|
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
|
||||||
|
|
||||||
|
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
|
||||||
|
# otherwise CuraEngine will crash
|
||||||
|
if not has_printing_mesh:
|
||||||
|
temp_list.clear()
|
||||||
|
|
||||||
if temp_list:
|
if temp_list:
|
||||||
object_groups.append(temp_list)
|
object_groups.append(temp_list)
|
||||||
|
|
||||||
|
|
@ -205,32 +248,71 @@ class StartSliceJob(Job):
|
||||||
def isCancelled(self):
|
def isCancelled(self):
|
||||||
return self._is_cancelled
|
return self._is_cancelled
|
||||||
|
|
||||||
def _expandGcodeTokens(self, key, value, settings):
|
## Creates a dictionary of tokens to replace in g-code pieces.
|
||||||
|
#
|
||||||
|
# This indicates what should be replaced in the start and end g-codes.
|
||||||
|
# \param stack The stack to get the settings from to replace the tokens
|
||||||
|
# with.
|
||||||
|
# \return A dictionary of replacement tokens to the values they should be
|
||||||
|
# replaced with.
|
||||||
|
def _buildReplacementTokens(self, stack) -> dict:
|
||||||
|
result = {}
|
||||||
|
for key in stack.getAllKeys():
|
||||||
|
result[key] = stack.getProperty(key, "value")
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
|
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
||||||
|
result["print_temperature"] = result["material_print_temperature"]
|
||||||
|
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||||
|
result["date"] = time.strftime("%d-%m-%Y")
|
||||||
|
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
|
||||||
|
|
||||||
|
## Replace setting tokens in a piece of g-code.
|
||||||
|
# \param value A piece of g-code to replace tokens in.
|
||||||
|
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||||
|
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1):
|
||||||
|
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()
|
||||||
return str(fmt.format(value, **settings)).encode("utf-8")
|
settings = self._all_extruders_settings.copy()
|
||||||
|
settings["default_extruder_nr"] = default_extruder_nr
|
||||||
|
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 gcode")
|
||||||
return str(value).encode("utf-8")
|
return str(value)
|
||||||
|
|
||||||
## Create extruder message from stack
|
## Create extruder message from stack
|
||||||
def _buildExtruderMessage(self, stack):
|
def _buildExtruderMessage(self, stack):
|
||||||
message = self._slice_message.addRepeatedMessage("extruders")
|
message = self._slice_message.addRepeatedMessage("extruders")
|
||||||
message.id = int(stack.getMetaDataEntry("position"))
|
message.id = int(stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
material_instance_container = stack.findContainer({"type": "material"})
|
settings = self._buildReplacementTokens(stack)
|
||||||
|
|
||||||
settings = {}
|
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
||||||
for key in stack.getAllKeys():
|
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
||||||
settings[key] = stack.getProperty(key, "value")
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
settings["print_bed_temperature"] = settings["material_bed_temperature"] #Renamed settings.
|
# Replace the setting tokens in start and end g-code.
|
||||||
settings["print_temperature"] = settings["material_print_temperature"]
|
extruder_nr = stack.getProperty("extruder_nr", "value")
|
||||||
settings["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr)
|
||||||
settings["date"] = time.strftime("%d-%m-%Y")
|
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr)
|
||||||
settings["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -238,13 +320,7 @@ class StartSliceJob(Job):
|
||||||
continue
|
continue
|
||||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||||
setting.name = key
|
setting.name = key
|
||||||
if key == "material_guid" and material_instance_container:
|
setting.value = str(value).encode("utf-8")
|
||||||
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
|
||||||
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
|
|
||||||
elif key == "machine_extruder_start_code" or key == "machine_extruder_end_code":
|
|
||||||
setting.value = self._expandGcodeTokens(key, value, settings)
|
|
||||||
else:
|
|
||||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Sends all global settings to the engine.
|
## Sends all global settings to the engine.
|
||||||
|
|
@ -252,33 +328,28 @@ class StartSliceJob(Job):
|
||||||
# The settings are taken from the global stack. This does not include any
|
# The settings are taken from the global stack. This does not include any
|
||||||
# per-extruder settings or per-object settings.
|
# per-extruder settings or per-object settings.
|
||||||
def _buildGlobalSettingsMessage(self, stack):
|
def _buildGlobalSettingsMessage(self, stack):
|
||||||
keys = stack.getAllKeys()
|
settings = self._buildReplacementTokens(stack)
|
||||||
settings = {}
|
|
||||||
for key in keys:
|
|
||||||
settings[key] = stack.getProperty(key, "value")
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
|
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||||
start_gcode = settings["machine_start_gcode"]
|
start_gcode = settings["machine_start_gcode"]
|
||||||
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
|
||||||
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))
|
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_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"}
|
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_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
|
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
|
||||||
|
|
||||||
settings["print_bed_temperature"] = settings["material_bed_temperature"]
|
# Replace the setting tokens in start and end g-code.
|
||||||
settings["print_temperature"] = settings["material_print_temperature"]
|
# Use values from the first used extruder by default so we get the expected temperatures
|
||||||
|
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||||
|
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||||
|
|
||||||
settings["time"] = time.strftime('%H:%M:%S')
|
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
|
||||||
settings["date"] = time.strftime('%d-%m-%Y')
|
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
|
||||||
settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
|
|
||||||
|
|
||||||
for key, value in settings.items(): #Add all submessages for each individual setting.
|
# Add all sub-messages for each individual setting.
|
||||||
|
for key, value in settings.items():
|
||||||
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
||||||
setting_message.name = key
|
setting_message.name = key
|
||||||
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
|
setting_message.value = str(value).encode("utf-8")
|
||||||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
|
||||||
else:
|
|
||||||
setting_message.value = str(value).encode("utf-8")
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Sends for some settings which extruder they should fallback to if not
|
## Sends for some settings which extruder they should fallback to if not
|
||||||
|
|
@ -348,3 +419,4 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
relations_set.add(relation.target.key)
|
relations_set.add(relation.target.key)
|
||||||
self._addRelations(relations_set, relation.target.relations)
|
self._addRelations(relations_set, relation.target.relations)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,16 @@ from UM.Logger import Logger
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura import LayerDataBuilder
|
from cura import LayerDataBuilder
|
||||||
from cura import LayerDataDecorator
|
from cura.LayerDataDecorator import LayerDataDecorator
|
||||||
from cura.LayerPolygon import LayerPolygon
|
from cura.LayerPolygon import LayerPolygon
|
||||||
from cura.GCodeListDecorator import GCodeListDecorator
|
from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -292,7 +292,7 @@ class FlavorParser:
|
||||||
# We obtain the filament diameter from the selected printer to calculate line widths
|
# We obtain the filament diameter from the selected printer to calculate line widths
|
||||||
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
|
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
|
||||||
|
|
||||||
scene_node = SceneNode()
|
scene_node = CuraSceneNode()
|
||||||
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
|
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
|
||||||
# real data to calculate it from.
|
# real data to calculate it from.
|
||||||
scene_node.getBoundingBox = self._getNullBoundingBox
|
scene_node.getBoundingBox = self._getNullBoundingBox
|
||||||
|
|
@ -418,11 +418,17 @@ class FlavorParser:
|
||||||
self._layer_number += 1
|
self._layer_number += 1
|
||||||
current_path.clear()
|
current_path.clear()
|
||||||
|
|
||||||
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
|
material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
|
||||||
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
|
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
|
||||||
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
|
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
|
||||||
|
material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
|
||||||
|
material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
|
||||||
|
material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
|
||||||
|
material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
|
||||||
|
material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
|
||||||
|
material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
|
||||||
layer_mesh = self._layer_data_builder.build(material_color_map)
|
layer_mesh = self._layer_data_builder.build(material_color_map)
|
||||||
decorator = LayerDataDecorator.LayerDataDecorator()
|
decorator = LayerDataDecorator()
|
||||||
decorator.setLayerData(layer_mesh)
|
decorator.setLayerData(layer_mesh)
|
||||||
scene_node.addDecorator(decorator)
|
scene_node.addDecorator(decorator)
|
||||||
|
|
||||||
|
|
@ -430,7 +436,10 @@ class FlavorParser:
|
||||||
gcode_list_decorator.setGCodeList(gcode_list)
|
gcode_list_decorator.setGCodeList(gcode_list)
|
||||||
scene_node.addDecorator(gcode_list_decorator)
|
scene_node.addDecorator(gcode_list_decorator)
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().gcode_list = gcode_list
|
# gcode_dict stores gcode_lists for a number of build plates.
|
||||||
|
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||||
|
gcode_dict = {active_build_plate_id: gcode_list}
|
||||||
|
Application.getInstance().getController().getScene().gcode_dict = gcode_dict
|
||||||
|
|
||||||
Logger.log("d", "Finished parsing %s" % file_name)
|
Logger.log("d", "Finished parsing %s" % file_name)
|
||||||
self._message.hide()
|
self._message.hide()
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,13 @@ class GCodeWriter(MeshWriter):
|
||||||
Logger.log("e", "GCode Writer does not support non-text mode.")
|
Logger.log("e", "GCode Writer does not support non-text mode.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||||
scene = Application.getInstance().getController().getScene()
|
scene = Application.getInstance().getController().getScene()
|
||||||
gcode_list = getattr(scene, "gcode_list")
|
gcode_dict = getattr(scene, "gcode_dict")
|
||||||
if gcode_list:
|
if not gcode_dict:
|
||||||
|
return False
|
||||||
|
gcode_list = gcode_dict.get(active_build_plate, None)
|
||||||
|
if gcode_list is not None:
|
||||||
for gcode in gcode_list:
|
for gcode in gcode_list:
|
||||||
stream.write(gcode)
|
stream.write(gcode)
|
||||||
# Serialise the current container stack and put it at the end of the file.
|
# Serialise the current container stack and put it at the end of the file.
|
||||||
|
|
@ -74,12 +78,13 @@ class GCodeWriter(MeshWriter):
|
||||||
## Create a new container with container 2 as base and container 1 written over it.
|
## Create a new container with container 2 as base and container 1 written over it.
|
||||||
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
|
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
|
||||||
flat_container = InstanceContainer(instance_container2.getName())
|
flat_container = InstanceContainer(instance_container2.getName())
|
||||||
if instance_container1.getDefinition():
|
|
||||||
flat_container.setDefinition(instance_container1.getDefinition())
|
# The metadata includes id, name and definition
|
||||||
else:
|
|
||||||
flat_container.setDefinition(instance_container2.getDefinition())
|
|
||||||
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
|
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
|
||||||
|
|
||||||
|
if instance_container1.getDefinition():
|
||||||
|
flat_container.setDefinition(instance_container1.getDefinition().getId())
|
||||||
|
|
||||||
for key in instance_container2.getAllKeys():
|
for key in instance_container2.getAllKeys():
|
||||||
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
|
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ UM.Dialog
|
||||||
TextField {
|
TextField {
|
||||||
id: peak_height
|
id: peak_height
|
||||||
objectName: "Peak_Height"
|
objectName: "Peak_Height"
|
||||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
|
validator: RegExpValidator {regExp: /^-?\d{1,3}([\,|\.]\d*)?$/}
|
||||||
width: 180 * screenScaleFactor
|
width: 180 * screenScaleFactor
|
||||||
onTextChanged: { manager.onPeakHeightChanged(text) }
|
onTextChanged: { manager.onPeakHeightChanged(text) }
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,7 @@ UM.Dialog
|
||||||
TextField {
|
TextField {
|
||||||
id: base_height
|
id: base_height
|
||||||
objectName: "Base_Height"
|
objectName: "Base_Height"
|
||||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
|
validator: RegExpValidator {regExp: /^\d{1,3}([\,|\.]\d*)?$/}
|
||||||
width: 180 * screenScaleFactor
|
width: 180 * screenScaleFactor
|
||||||
onTextChanged: { manager.onBaseHeightChanged(text) }
|
onTextChanged: { manager.onBaseHeightChanged(text) }
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +90,7 @@ UM.Dialog
|
||||||
id: width
|
id: width
|
||||||
objectName: "Width"
|
objectName: "Width"
|
||||||
focus: true
|
focus: true
|
||||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
|
||||||
width: 180 * screenScaleFactor
|
width: 180 * screenScaleFactor
|
||||||
onTextChanged: { manager.onWidthChanged(text) }
|
onTextChanged: { manager.onWidthChanged(text) }
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ UM.Dialog
|
||||||
id: depth
|
id: depth
|
||||||
objectName: "Depth"
|
objectName: "Depth"
|
||||||
focus: true
|
focus: true
|
||||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
|
||||||
width: 180 * screenScaleFactor
|
width: 180 * screenScaleFactor
|
||||||
onTextChanged: { manager.onDepthChanged(text) }
|
onTextChanged: { manager.onDepthChanged(text) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@ from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from UM.Mesh.MeshReader import MeshReader
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from .ImageReaderUI import ImageReaderUI
|
from .ImageReaderUI import ImageReaderUI
|
||||||
|
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
|
||||||
|
|
||||||
|
|
||||||
class ImageReader(MeshReader):
|
class ImageReader(MeshReader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ImageReaderUI(QObject):
|
||||||
def onWidthChanged(self, value):
|
def onWidthChanged(self, value):
|
||||||
if self._ui_view and not self._disable_size_callbacks:
|
if self._ui_view and not self._disable_size_callbacks:
|
||||||
if len(value) > 0:
|
if len(value) > 0:
|
||||||
self._width = float(value)
|
self._width = float(value.replace(",", "."))
|
||||||
else:
|
else:
|
||||||
self._width = 0
|
self._width = 0
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ class ImageReaderUI(QObject):
|
||||||
def onDepthChanged(self, value):
|
def onDepthChanged(self, value):
|
||||||
if self._ui_view and not self._disable_size_callbacks:
|
if self._ui_view and not self._disable_size_callbacks:
|
||||||
if len(value) > 0:
|
if len(value) > 0:
|
||||||
self._depth = float(value)
|
self._depth = float(value.replace(",", "."))
|
||||||
else:
|
else:
|
||||||
self._depth = 0
|
self._depth = 0
|
||||||
|
|
||||||
|
|
@ -126,14 +126,14 @@ class ImageReaderUI(QObject):
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def onBaseHeightChanged(self, value):
|
def onBaseHeightChanged(self, value):
|
||||||
if (len(value) > 0):
|
if (len(value) > 0):
|
||||||
self.base_height = float(value)
|
self.base_height = float(value.replace(",", "."))
|
||||||
else:
|
else:
|
||||||
self.base_height = 0
|
self.base_height = 0
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def onPeakHeightChanged(self, value):
|
def onPeakHeightChanged(self, value):
|
||||||
if (len(value) > 0):
|
if (len(value) > 0):
|
||||||
self.peak_height = float(value)
|
self.peak_height = float(value.replace(",", "."))
|
||||||
else:
|
else:
|
||||||
self.peak_height = 0
|
self.peak_height = 0
|
||||||
|
|
||||||
|
|
@ -143,7 +143,4 @@ class ImageReaderUI(QObject):
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def onImageColorInvertChanged(self, value):
|
def onImageColorInvertChanged(self, value):
|
||||||
if (value == 1):
|
self.image_color_invert = (value == 1)
|
||||||
self.image_color_invert = True
|
|
||||||
else:
|
|
||||||
self.image_color_invert = False
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2015 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 configparser # For reading the legacy profile INI files.
|
import configparser # For reading the legacy profile INI files.
|
||||||
|
import io
|
||||||
import json # For reading the Dictionary of Doom.
|
import json # For reading the Dictionary of Doom.
|
||||||
import math # For mathematical operations included in the Dictionary of Doom.
|
import math # For mathematical operations included in the Dictionary of Doom.
|
||||||
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
|
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
|
||||||
|
|
@ -9,8 +10,10 @@ import os.path # For concatenating the path to the plugin and the relative path
|
||||||
from UM.Application import Application # To get the machine manager to create the new profile in.
|
from UM.Application import Application # To get the machine manager to create the new profile in.
|
||||||
from UM.Logger import Logger # Logging errors.
|
from UM.Logger import Logger # Logging errors.
|
||||||
from UM.PluginRegistry import PluginRegistry # For getting the path to this plugin's directory.
|
from UM.PluginRegistry import PluginRegistry # For getting the path to this plugin's directory.
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry #To create unique profile IDs.
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
||||||
from cura.ProfileReader import ProfileReader # The plug-in type to implement.
|
from cura.ProfileReader import ProfileReader # The plug-in type to implement.
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager #To get the current extruder definition.
|
||||||
|
|
||||||
|
|
||||||
## A plugin that reads profile data from legacy Cura versions.
|
## A plugin that reads profile data from legacy Cura versions.
|
||||||
|
|
@ -76,12 +79,13 @@ class LegacyProfileReader(ProfileReader):
|
||||||
raise Exception("Unable to import legacy profile. Multi extrusion is not supported")
|
raise Exception("Unable to import legacy profile. Multi extrusion is not supported")
|
||||||
|
|
||||||
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
||||||
profile = InstanceContainer("Imported Legacy Profile") # Create an empty profile.
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
profile_id = container_registry.uniqueName("Imported Legacy Profile")
|
||||||
|
profile = InstanceContainer(profile_id) # Create an empty profile.
|
||||||
|
|
||||||
parser = configparser.ConfigParser(interpolation = None)
|
parser = configparser.ConfigParser(interpolation = None)
|
||||||
try:
|
try:
|
||||||
with open(file_name) as f:
|
parser.read([file_name]) # Parse the INI file.
|
||||||
parser.readfp(f) # Parse the INI file.
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
|
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
|
||||||
return None
|
return None
|
||||||
|
|
@ -120,8 +124,8 @@ class LegacyProfileReader(ProfileReader):
|
||||||
if "translation" not in dict_of_doom:
|
if "translation" not in dict_of_doom:
|
||||||
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
|
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
|
||||||
return None
|
return None
|
||||||
current_printer_definition = global_container_stack.getBottom()
|
current_printer_definition = global_container_stack.definition
|
||||||
profile.setDefinition(current_printer_definition)
|
profile.setDefinition(current_printer_definition.getId())
|
||||||
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
|
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
|
||||||
old_setting_expression = dict_of_doom["translation"][new_setting]
|
old_setting_expression = dict_of_doom["translation"][new_setting]
|
||||||
compiled = compile(old_setting_expression, new_setting, "eval")
|
compiled = compile(old_setting_expression, new_setting, "eval")
|
||||||
|
|
@ -138,7 +142,40 @@ class LegacyProfileReader(ProfileReader):
|
||||||
|
|
||||||
if len(profile.getAllKeys()) == 0:
|
if len(profile.getAllKeys()) == 0:
|
||||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||||
profile.setDirty(True)
|
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.addMetaDataEntry("type", "profile")
|
||||||
|
# don't know what quality_type it is based on, so use "normal" by default
|
||||||
profile.addMetaDataEntry("quality_type", "normal")
|
profile.addMetaDataEntry("quality_type", "normal")
|
||||||
return profile
|
profile.setName(profile_id)
|
||||||
|
profile.setDirty(True)
|
||||||
|
|
||||||
|
#Serialise and deserialise in order to perform the version upgrade.
|
||||||
|
parser = configparser.ConfigParser(interpolation=None)
|
||||||
|
data = profile.serialize()
|
||||||
|
parser.read_string(data)
|
||||||
|
parser["general"]["version"] = "1"
|
||||||
|
if parser.has_section("values"):
|
||||||
|
parser["settings"] = parser["values"]
|
||||||
|
del parser["values"]
|
||||||
|
stream = io.StringIO()
|
||||||
|
parser.write(stream)
|
||||||
|
data = stream.getvalue()
|
||||||
|
profile.deserialize(data)
|
||||||
|
|
||||||
|
#We need to return one extruder stack and one global stack.
|
||||||
|
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
|
||||||
|
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
|
||||||
|
global_profile.setDirty(True)
|
||||||
|
|
||||||
|
#Only the extruder stack has an extruder metadata entry.
|
||||||
|
profile.addMetaDataEntry("extruder", ExtruderManager.getInstance().getActiveExtruderStack().definition.getId())
|
||||||
|
|
||||||
|
#Split all settings into per-extruder and global settings.
|
||||||
|
for setting_key in profile.getAllKeys():
|
||||||
|
settable_per_extruder = global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
||||||
|
if settable_per_extruder:
|
||||||
|
global_profile.removeInstance(setting_key)
|
||||||
|
else:
|
||||||
|
profile.removeInstance(setting_key)
|
||||||
|
|
||||||
|
return [global_profile, profile]
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,11 @@ from UM.FlameProfiler import pyqtSlot
|
||||||
from cura.MachineAction import MachineAction
|
from cura.MachineAction import MachineAction
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
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 cura.CuraApplication import CuraApplication
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
|
||||||
|
|
@ -30,13 +27,14 @@ class MachineSettingsAction(MachineAction):
|
||||||
self._qml_url = "MachineSettingsAction.qml"
|
self._qml_url = "MachineSettingsAction.qml"
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
self._container_index = 0
|
|
||||||
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
|
self._container_index = _ContainerIndexes.DefinitionChanges
|
||||||
|
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
||||||
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
|
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
|
||||||
|
|
||||||
self._empty_container = self._container_registry.getEmptyInstanceContainer()
|
self._empty_container = self._container_registry.getEmptyInstanceContainer()
|
||||||
|
|
||||||
|
|
@ -67,7 +65,9 @@ class MachineSettingsAction(MachineAction):
|
||||||
self._global_container_stack, self._global_container_stack.getName() + "_settings")
|
self._global_container_stack, self._global_container_stack.getName() + "_settings")
|
||||||
|
|
||||||
# Notify the UI in which container to store the machine settings data
|
# Notify the UI in which container to store the machine settings data
|
||||||
container_index = self._global_container_stack.getContainerIndex(definition_changes_container)
|
from cura.Settings.CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
||||||
|
|
||||||
|
container_index = _ContainerIndexes.DefinitionChanges
|
||||||
if container_index != self._container_index:
|
if container_index != self._container_index:
|
||||||
self._container_index = container_index
|
self._container_index = container_index
|
||||||
self.containerIndexChanged.emit()
|
self.containerIndexChanged.emit()
|
||||||
|
|
@ -82,17 +82,6 @@ class MachineSettingsAction(MachineAction):
|
||||||
if self._backend and self._backend.determineAutoSlicing():
|
if self._backend and self._backend.determineAutoSlicing():
|
||||||
self._backend.tickle()
|
self._backend.tickle()
|
||||||
|
|
||||||
def _onActiveExtruderStackChanged(self):
|
|
||||||
extruder_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
|
||||||
if not self._global_container_stack or not extruder_container_stack:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Make sure there is a definition_changes container to store the machine settings
|
|
||||||
definition_changes_container = extruder_container_stack.definitionChanges
|
|
||||||
if definition_changes_container == self._empty_container:
|
|
||||||
definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
|
|
||||||
extruder_container_stack, extruder_container_stack.getId() + "_settings")
|
|
||||||
|
|
||||||
containerIndexChanged = pyqtSignal()
|
containerIndexChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = containerIndexChanged)
|
@pyqtProperty(int, notify = containerIndexChanged)
|
||||||
|
|
@ -188,34 +177,37 @@ class MachineSettingsAction(MachineAction):
|
||||||
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||||
return
|
return
|
||||||
|
|
||||||
|
stacks = ExtruderManager.getInstance().getExtruderStacks()
|
||||||
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||||
|
|
||||||
material_container = self._global_container_stack.material
|
|
||||||
|
|
||||||
if has_materials:
|
if has_materials:
|
||||||
if "has_materials" in self._global_container_stack.getMetaData():
|
if "has_materials" in self._global_container_stack.getMetaData():
|
||||||
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
||||||
else:
|
else:
|
||||||
self._global_container_stack.addMetaDataEntry("has_materials", True)
|
self._global_container_stack.addMetaDataEntry("has_materials", True)
|
||||||
|
|
||||||
# Set the material container to a sane default
|
# Set the material container for each extruder to a sane default
|
||||||
if material_container == self._empty_container:
|
for stack in stacks:
|
||||||
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")}
|
material_container = stack.material
|
||||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
if material_container == self._empty_container:
|
||||||
if materials:
|
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
|
||||||
self._global_container_stack.material = materials[0]
|
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
|
||||||
|
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||||
|
if materials:
|
||||||
|
stack.material = materials[0]
|
||||||
else:
|
else:
|
||||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||||
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||||
if "has_materials" in self._global_container_stack.getMetaData():
|
if "has_materials" in self._global_container_stack.getMetaData():
|
||||||
self._global_container_stack.removeMetaDataEntry("has_materials")
|
self._global_container_stack.removeMetaDataEntry("has_materials")
|
||||||
|
|
||||||
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
for stack in stacks:
|
||||||
|
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.emit()
|
Application.getInstance().globalContainerStackChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot(int)
|
||||||
def updateMaterialForDiameter(self):
|
def updateMaterialForDiameter(self, extruder_position: int):
|
||||||
# Updates the material container to a material that matches the material diameter set for the printer
|
# Updates the material container to a material that matches the material diameter set for the printer
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return
|
return
|
||||||
|
|
@ -223,24 +215,22 @@ class MachineSettingsAction(MachineAction):
|
||||||
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
|
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
|
||||||
return
|
return
|
||||||
|
|
||||||
material = ExtruderManager.getInstance().getActiveExtruderStack().material
|
extruder_stack = self._global_container_stack.extruders[str(extruder_position)]
|
||||||
material_diameter = material.getProperty("material_diameter", "value")
|
|
||||||
|
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
|
||||||
if not material_diameter:
|
if not material_diameter:
|
||||||
# in case of "empty" material
|
# in case of "empty" material
|
||||||
material_diameter = 0
|
material_diameter = 0
|
||||||
|
|
||||||
material_approximate_diameter = str(round(material_diameter))
|
material_approximate_diameter = str(round(material_diameter))
|
||||||
definition_changes = self._global_container_stack.definitionChanges
|
machine_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
|
||||||
machine_diameter = definition_changes.getProperty("material_diameter", "value")
|
|
||||||
if not machine_diameter:
|
if not machine_diameter:
|
||||||
machine_diameter = self._global_container_stack.definition.getProperty("material_diameter", "value")
|
machine_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
|
||||||
machine_approximate_diameter = str(round(machine_diameter))
|
machine_approximate_diameter = str(round(machine_diameter))
|
||||||
|
|
||||||
if material_approximate_diameter != machine_approximate_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.")
|
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
|
||||||
|
|
||||||
stacks = ExtruderManager.getInstance().getExtruderStacks()
|
|
||||||
|
|
||||||
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
|
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
|
||||||
materials_definition = self._global_container_stack.definition.getId()
|
materials_definition = self._global_container_stack.definition.getId()
|
||||||
has_material_variants = self._global_container_stack.getMetaDataEntry("has_variants", False)
|
has_material_variants = self._global_container_stack.getMetaDataEntry("has_variants", False)
|
||||||
|
|
@ -248,45 +238,47 @@ class MachineSettingsAction(MachineAction):
|
||||||
materials_definition = "fdmprinter"
|
materials_definition = "fdmprinter"
|
||||||
has_material_variants = False
|
has_material_variants = False
|
||||||
|
|
||||||
for stack in stacks:
|
old_material = extruder_stack.material
|
||||||
old_material = stack.material
|
search_criteria = {
|
||||||
search_criteria = {
|
"type": "material",
|
||||||
"type": "material",
|
"approximate_diameter": machine_approximate_diameter,
|
||||||
"approximate_diameter": machine_approximate_diameter,
|
"material": old_material.getMetaDataEntry("material", "value"),
|
||||||
"material": old_material.getMetaDataEntry("material", "value"),
|
"brand": old_material.getMetaDataEntry("brand", "value"),
|
||||||
"supplier": old_material.getMetaDataEntry("supplier", "value"),
|
"supplier": old_material.getMetaDataEntry("supplier", "value"),
|
||||||
"color_name": old_material.getMetaDataEntry("color_name", "value"),
|
"color_name": old_material.getMetaDataEntry("color_name", "value"),
|
||||||
"definition": materials_definition
|
"definition": materials_definition
|
||||||
}
|
}
|
||||||
if has_material_variants:
|
if has_material_variants:
|
||||||
search_criteria["variant"] = stack.variant.getId()
|
search_criteria["variant"] = extruder_stack.variant.getId()
|
||||||
|
|
||||||
if old_material == self._empty_container:
|
if old_material == self._empty_container:
|
||||||
search_criteria.pop("material", None)
|
search_criteria.pop("material", None)
|
||||||
search_criteria.pop("supplier", None)
|
search_criteria.pop("supplier", None)
|
||||||
search_criteria.pop("definition", None)
|
search_criteria.pop("brand", None)
|
||||||
search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
|
search_criteria.pop("definition", None)
|
||||||
|
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
|
||||||
|
|
||||||
|
materials = self._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 = self._container_registry.findInstanceContainers(**search_criteria)
|
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||||
if not materials:
|
if not materials:
|
||||||
# Same material with new diameter is not found, search for generic version of the same material type
|
# Generic material with new diameter is not found, search for preferred material
|
||||||
search_criteria.pop("supplier", None)
|
search_criteria.pop("color_name", None)
|
||||||
search_criteria["color_name"] = "Generic"
|
search_criteria.pop("material", None)
|
||||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
|
||||||
if not materials:
|
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||||
# Generic material with new diameter is not found, search for preferred material
|
if not materials:
|
||||||
search_criteria.pop("color_name", None)
|
# Preferred material with new diameter is not found, search for any material
|
||||||
search_criteria.pop("material", None)
|
search_criteria.pop("id", None)
|
||||||
search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
|
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
||||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
if not materials:
|
||||||
if not materials:
|
# Just use empty material as a final fallback
|
||||||
# Preferred material with new diameter is not found, search for any material
|
materials = [self._empty_container]
|
||||||
search_criteria.pop("id", None)
|
|
||||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if not materials:
|
|
||||||
# Just use empty material as a final fallback
|
|
||||||
materials = [self._empty_container]
|
|
||||||
|
|
||||||
Logger.log("i", "Selecting new material: %s" % materials[0].getId())
|
Logger.log("i", "Selecting new material: %s", materials[0].getId())
|
||||||
|
|
||||||
stack.material = materials[0]
|
extruder_stack.material = materials[0]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Cura.MachineAction
|
||||||
onModelChanged:
|
onModelChanged:
|
||||||
{
|
{
|
||||||
var extruderCount = base.extrudersModel.rowCount();
|
var extruderCount = base.extrudersModel.rowCount();
|
||||||
base.extruderTabsCount = extruderCount > 1 ? extruderCount : 0;
|
base.extruderTabsCount = extruderCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,7 +241,6 @@ Cura.MachineAction
|
||||||
|
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
visible: manager.definedExtruderCount > 1
|
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
text: machineExtruderCountProvider.properties.description
|
text: machineExtruderCountProvider.properties.description
|
||||||
|
|
@ -271,6 +270,20 @@ Cura.MachineAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: manager
|
||||||
|
onDefinedExtruderCountChanged:
|
||||||
|
{
|
||||||
|
extruderCountModel.clear();
|
||||||
|
for(var i = 0; i < manager.definedExtruderCount; ++i)
|
||||||
|
{
|
||||||
|
extruderCountModel.append({text: String(i + 1), value: i});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex: machineExtruderCountProvider.properties.value - 1
|
currentIndex: machineExtruderCountProvider.properties.value - 1
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
|
|
@ -279,26 +292,6 @@ Cura.MachineAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: materialDiameterField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "material_diameter"
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
|
|
||||||
property var afterOnEditingFinished: manager.updateMaterialForDiameter
|
|
||||||
property string label: catalog.i18nc("@label", "Material diameter")
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: nozzleSizeField
|
|
||||||
visible: !Cura.MachineManager.hasVariants && machineExtruderCountProvider.properties.value == 1
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_nozzle_size"
|
|
||||||
property string label: catalog.i18nc("@label", "Nozzle size")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,7 +348,6 @@ Cura.MachineAction
|
||||||
if(currentIndex > 0)
|
if(currentIndex > 0)
|
||||||
{
|
{
|
||||||
contentItem.forceActiveFocus();
|
contentItem.forceActiveFocus();
|
||||||
Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,6 +384,25 @@ Cura.MachineAction
|
||||||
property bool isExtruderSetting: true
|
property bool isExtruderSetting: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader
|
||||||
|
{
|
||||||
|
id: materialDiameterField
|
||||||
|
visible: Cura.MachineManager.hasMaterials
|
||||||
|
sourceComponent: numericTextFieldWithUnit
|
||||||
|
property string settingKey: "material_diameter"
|
||||||
|
property string label: catalog.i18nc("@label", "Material diameter")
|
||||||
|
property string unit: catalog.i18nc("@label", "mm")
|
||||||
|
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
|
||||||
|
function afterOnEditingFinished()
|
||||||
|
{
|
||||||
|
if (settingsTabs.currentIndex > 0)
|
||||||
|
{
|
||||||
|
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
property bool isExtruderSetting: true
|
||||||
|
}
|
||||||
|
|
||||||
Loader
|
Loader
|
||||||
{
|
{
|
||||||
id: extruderOffsetXField
|
id: extruderOffsetXField
|
||||||
|
|
@ -441,7 +452,7 @@ Cura.MachineAction
|
||||||
property int areaHeight: parent.height - y
|
property int areaHeight: parent.height - y
|
||||||
property string settingKey: "machine_extruder_start_code"
|
property string settingKey: "machine_extruder_start_code"
|
||||||
property bool isExtruderSetting: true
|
property bool isExtruderSetting: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
|
@ -490,7 +501,7 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
if(settingsTabs.currentIndex > 0)
|
if(settingsTabs.currentIndex > 0)
|
||||||
{
|
{
|
||||||
return Cura.MachineManager.activeStackId;
|
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -508,11 +519,11 @@ Cura.MachineAction
|
||||||
checked: String(propertyProvider.properties.value).toLowerCase() != 'false'
|
checked: String(propertyProvider.properties.value).toLowerCase() != 'false'
|
||||||
onClicked:
|
onClicked:
|
||||||
{
|
{
|
||||||
propertyProvider.setPropertyValue("value", checked);
|
propertyProvider.setPropertyValue("value", checked);
|
||||||
if(_forceUpdateOnChange)
|
if(_forceUpdateOnChange)
|
||||||
{
|
{
|
||||||
manager.forceUpdate();
|
manager.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +554,7 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
if(settingsTabs.currentIndex > 0)
|
if(settingsTabs.currentIndex > 0)
|
||||||
{
|
{
|
||||||
return Cura.MachineManager.activeStackId;
|
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -576,7 +587,10 @@ Cura.MachineAction
|
||||||
TextField
|
TextField
|
||||||
{
|
{
|
||||||
id: textField
|
id: textField
|
||||||
text: (propertyProvider.properties.value) ? propertyProvider.properties.value : ""
|
text: {
|
||||||
|
const value = propertyProvider.properties.value;
|
||||||
|
return value ? value : "";
|
||||||
|
}
|
||||||
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
|
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
|
||||||
onEditingFinished:
|
onEditingFinished:
|
||||||
{
|
{
|
||||||
|
|
@ -585,12 +599,7 @@ Cura.MachineAction
|
||||||
propertyProvider.setPropertyValue("value", text);
|
propertyProvider.setPropertyValue("value", text);
|
||||||
if(_forceUpdateOnChange)
|
if(_forceUpdateOnChange)
|
||||||
{
|
{
|
||||||
var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
|
|
||||||
manager.forceUpdate();
|
manager.forceUpdate();
|
||||||
if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
|
|
||||||
{
|
|
||||||
Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(_afterOnEditingFinished)
|
if(_afterOnEditingFinished)
|
||||||
{
|
{
|
||||||
|
|
@ -636,7 +645,7 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
if(settingsTabs.currentIndex > 0)
|
if(settingsTabs.currentIndex > 0)
|
||||||
{
|
{
|
||||||
return Cura.MachineManager.activeStackId;
|
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -723,7 +732,7 @@ Cura.MachineAction
|
||||||
width: gcodeArea.width
|
width: gcodeArea.width
|
||||||
text: _tooltip
|
text: _tooltip
|
||||||
|
|
||||||
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false: isExtruderSetting
|
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false : isExtruderSetting
|
||||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
UM.SettingPropertyProvider
|
||||||
|
|
@ -735,7 +744,7 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
if(settingsTabs.currentIndex > 0)
|
if(settingsTabs.currentIndex > 0)
|
||||||
{
|
{
|
||||||
return Cura.MachineManager.activeStackId;
|
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
plugins/MonitorStage/MonitorMainView.qml
Normal file
45
plugins/MonitorStage/MonitorMainView.qml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
|
||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 1.1
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
// parent could be undefined as this component is not visible at all times
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: parent ? parent.height : 0
|
||||||
|
|
||||||
|
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: viewportOverlay
|
||||||
|
|
||||||
|
color: UM.Theme.getColor("viewport_overlay")
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
MouseArea
|
||||||
|
{
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.AllButtons
|
||||||
|
onWheel: wheel.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader
|
||||||
|
{
|
||||||
|
id: monitorViewComponent
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
property real maximumWidth: parent.width
|
||||||
|
property real maximumHeight: parent.height
|
||||||
|
|
||||||
|
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem: null
|
||||||
|
visible: sourceComponent != null
|
||||||
|
}
|
||||||
|
}
|
||||||
135
plugins/MonitorStage/MonitorStage.py
Normal file
135
plugins/MonitorStage/MonitorStage.py
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import os.path
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from cura.Stages.CuraStage import CuraStage
|
||||||
|
|
||||||
|
|
||||||
|
## Stage for monitoring a 3D printing while it's printing.
|
||||||
|
class MonitorStage(CuraStage):
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||||
|
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
|
self._printer_output_device = None
|
||||||
|
|
||||||
|
self._active_print_job = None
|
||||||
|
self._active_printer = None
|
||||||
|
|
||||||
|
def _setActivePrintJob(self, print_job):
|
||||||
|
if self._active_print_job != print_job:
|
||||||
|
if self._active_print_job:
|
||||||
|
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
|
||||||
|
self._active_print_job = print_job
|
||||||
|
if self._active_print_job:
|
||||||
|
self._active_print_job.stateChanged.connect(self._updateIconSource)
|
||||||
|
|
||||||
|
# Ensure that the right icon source is returned.
|
||||||
|
self._updateIconSource()
|
||||||
|
|
||||||
|
def _setActivePrinter(self, printer):
|
||||||
|
if self._active_printer != printer:
|
||||||
|
if self._active_printer:
|
||||||
|
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||||
|
self._active_printer = printer
|
||||||
|
if self._active_printer:
|
||||||
|
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||||
|
# Jobs might change, so we need to listen to it's changes.
|
||||||
|
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||||
|
else:
|
||||||
|
self._setActivePrintJob(None)
|
||||||
|
|
||||||
|
# Ensure that the right icon source is returned.
|
||||||
|
self._updateIconSource()
|
||||||
|
|
||||||
|
def _onActivePrintJobChanged(self):
|
||||||
|
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||||
|
|
||||||
|
def _onActivePrinterChanged(self):
|
||||||
|
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||||
|
|
||||||
|
def _onOutputDevicesChanged(self):
|
||||||
|
try:
|
||||||
|
# We assume that you are monitoring the device with the highest priority.
|
||||||
|
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||||
|
if new_output_device != self._printer_output_device:
|
||||||
|
if self._printer_output_device:
|
||||||
|
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
|
||||||
|
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
|
||||||
|
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||||
|
|
||||||
|
self._printer_output_device = new_output_device
|
||||||
|
|
||||||
|
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
|
||||||
|
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||||
|
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
|
||||||
|
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||||
|
|
||||||
|
# Force an update of the icon source
|
||||||
|
self._updateIconSource()
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _onEngineCreated(self):
|
||||||
|
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||||
|
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||||
|
self._onOutputDevicesChanged()
|
||||||
|
self._updateMainOverlay()
|
||||||
|
self._updateSidebar()
|
||||||
|
self._updateIconSource()
|
||||||
|
|
||||||
|
def _updateMainOverlay(self):
|
||||||
|
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
|
||||||
|
self.addDisplayComponent("main", main_component_path)
|
||||||
|
|
||||||
|
def _updateSidebar(self):
|
||||||
|
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
|
||||||
|
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
|
||||||
|
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||||
|
|
||||||
|
def _updateIconSource(self):
|
||||||
|
if Application.getInstance().getTheme() is not None:
|
||||||
|
icon_name = self._getActiveOutputDeviceStatusIcon()
|
||||||
|
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
|
||||||
|
|
||||||
|
## Find the correct status icon depending on the active output device state
|
||||||
|
def _getActiveOutputDeviceStatusIcon(self):
|
||||||
|
# We assume that you are monitoring the device with the highest priority.
|
||||||
|
try:
|
||||||
|
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||||
|
except IndexError:
|
||||||
|
return "tab_status_unknown"
|
||||||
|
|
||||||
|
if not output_device.acceptsCommands:
|
||||||
|
return "tab_status_unknown"
|
||||||
|
|
||||||
|
if output_device.activePrinter is None:
|
||||||
|
return "tab_status_connected"
|
||||||
|
|
||||||
|
# TODO: refactor to use enum instead of hardcoded strings?
|
||||||
|
if output_device.activePrinter.state == "maintenance":
|
||||||
|
return "tab_status_busy"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob is None:
|
||||||
|
return "tab_status_connected"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
|
||||||
|
return "tab_status_busy"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
|
||||||
|
return "tab_status_finished"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
|
||||||
|
return "tab_status_connected"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob.state == "paused":
|
||||||
|
return "tab_status_paused"
|
||||||
|
|
||||||
|
if output_device.activePrinter.activePrintJob.state == "error":
|
||||||
|
return "tab_status_stopped"
|
||||||
|
|
||||||
|
return "tab_status_unknown"
|
||||||
20
plugins/MonitorStage/__init__.py
Normal file
20
plugins/MonitorStage/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from . import MonitorStage
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {
|
||||||
|
"stage": {
|
||||||
|
"name": i18n_catalog.i18nc("@item:inmenu", "Monitor"),
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return {
|
||||||
|
"stage": MonitorStage.MonitorStage()
|
||||||
|
}
|
||||||
8
plugins/MonitorStage/plugin.json
Normal file
8
plugins/MonitorStage/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Monitor Stage",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Provides a monitor stage in Cura.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
# 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, pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
@ -22,6 +23,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
||||||
self._node = None
|
self._node = None
|
||||||
self._stack = None
|
self._stack = None
|
||||||
|
|
||||||
|
# this is a set of settings that will be skipped if the user chooses to reset.
|
||||||
|
self._skip_reset_setting_set = set()
|
||||||
|
|
||||||
def setSelectedObjectId(self, id):
|
def setSelectedObjectId(self, id):
|
||||||
if id != self._selected_object_id:
|
if id != self._selected_object_id:
|
||||||
self._selected_object_id = id
|
self._selected_object_id = id
|
||||||
|
|
@ -36,6 +40,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
||||||
def selectedObjectId(self):
|
def selectedObjectId(self):
|
||||||
return self._selected_object_id
|
return self._selected_object_id
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def addSkipResetSetting(self, setting_name):
|
||||||
|
self._skip_reset_setting_set.add(setting_name)
|
||||||
|
|
||||||
def setVisible(self, visible):
|
def setVisible(self, visible):
|
||||||
if not self._node:
|
if not self._node:
|
||||||
return
|
return
|
||||||
|
|
@ -50,6 +58,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
||||||
|
|
||||||
# Remove all instances that are not in visibility list
|
# Remove all instances that are not in visibility list
|
||||||
for instance in all_instances:
|
for instance in all_instances:
|
||||||
|
# exceptionally skip setting
|
||||||
|
if instance.definition.key in self._skip_reset_setting_set:
|
||||||
|
continue
|
||||||
if instance.definition.key not in visible:
|
if instance.definition.key not in visible:
|
||||||
settings.removeInstance(instance.definition.key)
|
settings.removeInstance(instance.definition.key)
|
||||||
visibility_changed = True
|
visibility_changed = True
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ Item {
|
||||||
width: childrenRect.width;
|
width: childrenRect.width;
|
||||||
height: childrenRect.height;
|
height: childrenRect.height;
|
||||||
|
|
||||||
|
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
|
||||||
|
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
id: items
|
id: items
|
||||||
|
|
@ -26,17 +29,128 @@ Item {
|
||||||
|
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@label","Mesh Type")
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
height: UM.Theme.getSize("setting").height
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.SettingPropertyProvider
|
||||||
|
{
|
||||||
|
id: meshTypePropertyProvider
|
||||||
|
containerStackId: Cura.MachineManager.activeMachineId
|
||||||
|
watchedProperties: [ "enabled" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox
|
||||||
|
{
|
||||||
|
id: meshTypeSelection
|
||||||
|
style: UM.Theme.styles.combobox
|
||||||
|
onActivated: {
|
||||||
|
UM.ActiveTool.setProperty("MeshType", model.get(index).type)
|
||||||
|
}
|
||||||
|
model: ListModel
|
||||||
|
{
|
||||||
|
id: meshTypeModel
|
||||||
|
Component.onCompleted: meshTypeSelection.populateModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateModel()
|
||||||
|
{
|
||||||
|
meshTypeModel.append({
|
||||||
|
type: "",
|
||||||
|
text: catalog.i18nc("@label", "Normal model")
|
||||||
|
});
|
||||||
|
meshTypePropertyProvider.key = "support_mesh";
|
||||||
|
if(meshTypePropertyProvider.properties.enabled == "True")
|
||||||
|
{
|
||||||
|
meshTypeModel.append({
|
||||||
|
type: "support_mesh",
|
||||||
|
text: catalog.i18nc("@label", "Print as support")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
meshTypePropertyProvider.key = "anti_overhang_mesh";
|
||||||
|
if(meshTypePropertyProvider.properties.enabled == "True")
|
||||||
|
{
|
||||||
|
meshTypeModel.append({
|
||||||
|
type: "anti_overhang_mesh",
|
||||||
|
text: catalog.i18nc("@label", "Don't support overlap with other models")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
meshTypePropertyProvider.key = "cutting_mesh";
|
||||||
|
if(meshTypePropertyProvider.properties.enabled == "True")
|
||||||
|
{
|
||||||
|
meshTypeModel.append({
|
||||||
|
type: "cutting_mesh",
|
||||||
|
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
meshTypePropertyProvider.key = "infill_mesh";
|
||||||
|
if(meshTypePropertyProvider.properties.enabled == "True")
|
||||||
|
{
|
||||||
|
meshTypeModel.append({
|
||||||
|
type: "infill_mesh",
|
||||||
|
text: catalog.i18nc("@label", "Modify settings for infill of other models")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
meshTypeSelection.updateCurrentIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentIndex()
|
||||||
|
{
|
||||||
|
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
|
||||||
|
meshTypeSelection.currentIndex = -1;
|
||||||
|
for(var index=0; index < meshTypeSelection.model.count; index++)
|
||||||
|
{
|
||||||
|
if(meshTypeSelection.model.get(index).type == mesh_type)
|
||||||
|
{
|
||||||
|
meshTypeSelection.currentIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meshTypeSelection.currentIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: Cura.MachineManager
|
||||||
|
onGlobalContainerChanged:
|
||||||
|
{
|
||||||
|
meshTypeSelection.model.clear();
|
||||||
|
meshTypeSelection.populateModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: UM.Selection
|
||||||
|
onSelectionChanged: meshTypeSelection.updateCurrentIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
|
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
|
||||||
// It kinda looks ugly otherwise (big panel, no content on it)
|
// It kinda looks ugly otherwise (big panel, no content on it)
|
||||||
|
id: currentSettings
|
||||||
property int maximumHeight: 200 * screenScaleFactor
|
property int maximumHeight: 200 * screenScaleFactor
|
||||||
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
|
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
|
||||||
|
visible: meshTypeSelection.model.get(meshTypeSelection.currentIndex).type != "anti_overhang_mesh"
|
||||||
|
|
||||||
ScrollView
|
ScrollView
|
||||||
{
|
{
|
||||||
height: parent.height
|
height: parent.height
|
||||||
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("setting").height
|
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("default_margin").width
|
||||||
style: UM.Theme.styles.scrollview
|
style: UM.Theme.styles.scrollview
|
||||||
|
|
||||||
ListView
|
ListView
|
||||||
|
|
@ -49,6 +163,15 @@ Item {
|
||||||
id: addedSettingsModel;
|
id: addedSettingsModel;
|
||||||
containerId: Cura.MachineManager.activeDefinitionId
|
containerId: Cura.MachineManager.activeDefinitionId
|
||||||
expanded: [ "*" ]
|
expanded: [ "*" ]
|
||||||
|
exclude: {
|
||||||
|
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
|
||||||
|
|
||||||
|
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
|
||||||
|
{
|
||||||
|
excluded_settings = excluded_settings.concat(base.all_categories_except_support);
|
||||||
|
}
|
||||||
|
return excluded_settings;
|
||||||
|
}
|
||||||
|
|
||||||
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
|
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +181,7 @@ Item {
|
||||||
|
|
||||||
delegate: Row
|
delegate: Row
|
||||||
{
|
{
|
||||||
|
spacing: - UM.Theme.getSize("default_margin").width
|
||||||
Loader
|
Loader
|
||||||
{
|
{
|
||||||
id: settingLoader
|
id: settingLoader
|
||||||
|
|
@ -68,6 +192,7 @@ Item {
|
||||||
property var settingDefinitionsModel: addedSettingsModel
|
property var settingDefinitionsModel: addedSettingsModel
|
||||||
property var propertyProvider: provider
|
property var propertyProvider: provider
|
||||||
property var globalPropertyProvider: inheritStackProvider
|
property var globalPropertyProvider: inheritStackProvider
|
||||||
|
property var externalResetHandler: false
|
||||||
|
|
||||||
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
|
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
|
||||||
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
|
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
|
||||||
|
|
@ -112,7 +237,7 @@ Item {
|
||||||
|
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
width: (UM.Theme.getSize("setting").height / 2) | 0
|
width: Math.floor(UM.Theme.getSize("setting").height / 2)
|
||||||
height: UM.Theme.getSize("setting").height
|
height: UM.Theme.getSize("setting").height
|
||||||
|
|
||||||
onClicked: addedSettingsModel.setVisible(model.key, false)
|
onClicked: addedSettingsModel.setVisible(model.key, false)
|
||||||
|
|
@ -125,7 +250,7 @@ Item {
|
||||||
{
|
{
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height / 2
|
height: width
|
||||||
sourceSize.width: width
|
sourceSize.width: width
|
||||||
sourceSize.height: width
|
sourceSize.height: width
|
||||||
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
|
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
|
||||||
|
|
@ -201,9 +326,9 @@ Item {
|
||||||
|
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
id: customise_settings_button;
|
id: customiseSettingsButton;
|
||||||
height: UM.Theme.getSize("setting").height;
|
height: UM.Theme.getSize("setting_control").height;
|
||||||
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
|
visible: currentSettings.visible
|
||||||
|
|
||||||
text: catalog.i18nc("@action:button", "Select settings");
|
text: catalog.i18nc("@action:button", "Select settings");
|
||||||
|
|
||||||
|
|
@ -223,19 +348,21 @@ Item {
|
||||||
{
|
{
|
||||||
text: control.text;
|
text: control.text;
|
||||||
color: UM.Theme.getColor("setting_control_text");
|
color: UM.Theme.getColor("setting_control_text");
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: settingPickDialog.visible = true;
|
onClicked:
|
||||||
|
|
||||||
Connections
|
|
||||||
{
|
{
|
||||||
target: UM.Preferences;
|
settingPickDialog.visible = true;
|
||||||
|
if (meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
|
||||||
onPreferenceChanged:
|
|
||||||
{
|
{
|
||||||
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
|
settingPickDialog.additional_excluded_settings = base.all_categories_except_support;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settingPickDialog.additional_excluded_settings = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,15 +373,18 @@ Item {
|
||||||
id: settingPickDialog
|
id: settingPickDialog
|
||||||
|
|
||||||
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
|
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
|
||||||
width: screenScaleFactor * 360;
|
width: screenScaleFactor * 360
|
||||||
|
|
||||||
property string labelFilter: ""
|
property string labelFilter: ""
|
||||||
|
property var additional_excluded_settings
|
||||||
|
|
||||||
onVisibilityChanged:
|
onVisibilityChanged:
|
||||||
{
|
{
|
||||||
// force updating the model to sync it with addedSettingsModel
|
// force updating the model to sync it with addedSettingsModel
|
||||||
if(visible)
|
if(visible)
|
||||||
{
|
{
|
||||||
|
// Set skip setting, it will prevent from resetting selected mesh_type
|
||||||
|
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
|
||||||
listview.model.forceUpdate()
|
listview.model.forceUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,7 +455,12 @@ Item {
|
||||||
}
|
}
|
||||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||||
expanded: [ "*" ]
|
expanded: [ "*" ]
|
||||||
exclude: [ "machine_settings", "command_line_settings" ]
|
exclude:
|
||||||
|
{
|
||||||
|
var excluded_settings = [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
|
||||||
|
excluded_settings = excluded_settings.concat(settingPickDialog.additional_excluded_settings);
|
||||||
|
return excluded_settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delegate:Loader
|
delegate:Loader
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from UM.Application import Application
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
from UM.Event import Event
|
from UM.Event import Event
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ class PerObjectSettingsTool(Tool):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._model = None
|
self._model = None
|
||||||
|
|
||||||
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
|
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder", "MeshType")
|
||||||
|
|
||||||
self._advanced_mode = False
|
self._advanced_mode = False
|
||||||
self._multi_extrusion = False
|
self._multi_extrusion = False
|
||||||
|
|
@ -70,6 +71,39 @@ class PerObjectSettingsTool(Tool):
|
||||||
selected_object.addDecorator(SettingOverrideDecorator())
|
selected_object.addDecorator(SettingOverrideDecorator())
|
||||||
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
||||||
|
|
||||||
|
def setMeshType(self, mesh_type):
|
||||||
|
selected_object = Selection.getSelectedObject(0)
|
||||||
|
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||||
|
if not stack:
|
||||||
|
selected_object.addDecorator(SettingOverrideDecorator())
|
||||||
|
stack = selected_object.callDecoration("getStack")
|
||||||
|
|
||||||
|
settings = stack.getTop()
|
||||||
|
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
|
||||||
|
if property_key != mesh_type:
|
||||||
|
if settings.getInstance(property_key):
|
||||||
|
settings.removeInstance(property_key)
|
||||||
|
else:
|
||||||
|
if not (settings.getInstance(property_key) and settings.getProperty(property_key, "value")):
|
||||||
|
definition = stack.getSettingDefinition(property_key)
|
||||||
|
new_instance = SettingInstance(definition, settings)
|
||||||
|
new_instance.setProperty("value", True)
|
||||||
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
|
settings.addInstance(new_instance)
|
||||||
|
|
||||||
|
def getMeshType(self):
|
||||||
|
selected_object = Selection.getSelectedObject(0)
|
||||||
|
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||||
|
if not stack:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
settings = stack.getTop()
|
||||||
|
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
|
||||||
|
if settings.getInstance(property_key) and settings.getProperty(property_key, "value"):
|
||||||
|
return property_key
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
def _onPreferenceChanged(self, preference):
|
def _onPreferenceChanged(self, preference):
|
||||||
if preference == "cura/active_mode":
|
if preference == "cura/active_mode":
|
||||||
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
|
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
|
||||||
|
|
|
||||||
210
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
210
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
|
||||||
|
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Extension import Extension
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
|
||||||
|
# g-code files.
|
||||||
|
class PostProcessingPlugin(QObject, Extension):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
|
||||||
|
self._view = None
|
||||||
|
|
||||||
|
# Loaded scripts are all scripts that can be used
|
||||||
|
self._loaded_scripts = {}
|
||||||
|
self._script_labels = {}
|
||||||
|
|
||||||
|
# Script list contains instances of scripts in loaded_scripts.
|
||||||
|
# There can be duplicates, which will be executed in sequence.
|
||||||
|
self._script_list = []
|
||||||
|
self._selected_script_index = -1
|
||||||
|
|
||||||
|
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
|
||||||
|
|
||||||
|
selectedIndexChanged = pyqtSignal()
|
||||||
|
@pyqtProperty("QVariant", notify = selectedIndexChanged)
|
||||||
|
def selectedScriptDefinitionId(self):
|
||||||
|
try:
|
||||||
|
return self._script_list[self._selected_script_index].getDefinitionId()
|
||||||
|
except:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@pyqtProperty("QVariant", notify=selectedIndexChanged)
|
||||||
|
def selectedScriptStackId(self):
|
||||||
|
try:
|
||||||
|
return self._script_list[self._selected_script_index].getStackId()
|
||||||
|
except:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
## Execute all post-processing scripts on the gcode.
|
||||||
|
def execute(self, output_device):
|
||||||
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
gcode_dict = None
|
||||||
|
|
||||||
|
if hasattr(scene, "gcode_dict"):
|
||||||
|
gcode_dict = getattr(scene, "gcode_dict")
|
||||||
|
|
||||||
|
if not gcode_dict:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get gcode list for the active build plate
|
||||||
|
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
|
||||||
|
gcode_list = gcode_dict[active_build_plate_id]
|
||||||
|
if not gcode_list:
|
||||||
|
return
|
||||||
|
|
||||||
|
if ";POSTPROCESSED" not in gcode_list[0]:
|
||||||
|
for script in self._script_list:
|
||||||
|
try:
|
||||||
|
gcode_list = script.execute(gcode_list)
|
||||||
|
except Exception:
|
||||||
|
Logger.logException("e", "Exception in post-processing script.")
|
||||||
|
if len(self._script_list): # Add comment to g-code if any changes were made.
|
||||||
|
gcode_list[0] += ";POSTPROCESSED\n"
|
||||||
|
gcode_dict[active_build_plate_id] = gcode_list
|
||||||
|
setattr(scene, "gcode_dict", gcode_dict)
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Already post processed")
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def setSelectedScriptIndex(self, index):
|
||||||
|
self._selected_script_index = index
|
||||||
|
self.selectedIndexChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = selectedIndexChanged)
|
||||||
|
def selectedScriptIndex(self):
|
||||||
|
return self._selected_script_index
|
||||||
|
|
||||||
|
@pyqtSlot(int, int)
|
||||||
|
def moveScript(self, index, new_index):
|
||||||
|
if new_index < 0 or new_index > len(self._script_list) - 1:
|
||||||
|
return # nothing needs to be done
|
||||||
|
else:
|
||||||
|
# Magical switch code.
|
||||||
|
self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index]
|
||||||
|
self.scriptListChanged.emit()
|
||||||
|
self.selectedIndexChanged.emit() #Ensure that settings are updated
|
||||||
|
self._propertyChanged()
|
||||||
|
|
||||||
|
## Remove a script from the active script list by index.
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def removeScriptByIndex(self, index):
|
||||||
|
self._script_list.pop(index)
|
||||||
|
if len(self._script_list) - 1 < self._selected_script_index:
|
||||||
|
self._selected_script_index = len(self._script_list) - 1
|
||||||
|
self.scriptListChanged.emit()
|
||||||
|
self.selectedIndexChanged.emit() # Ensure that settings are updated
|
||||||
|
self._propertyChanged()
|
||||||
|
|
||||||
|
## Load all scripts from provided path.
|
||||||
|
# This should probably only be done on init.
|
||||||
|
# \param path Path to check for scripts.
|
||||||
|
def loadAllScripts(self, path):
|
||||||
|
scripts = pkgutil.iter_modules(path = [path])
|
||||||
|
for loader, script_name, ispkg in scripts:
|
||||||
|
# Iterate over all scripts.
|
||||||
|
if script_name not in sys.modules:
|
||||||
|
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
|
||||||
|
loaded_script = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(loaded_script)
|
||||||
|
sys.modules[script_name] = loaded_script
|
||||||
|
|
||||||
|
loaded_class = getattr(loaded_script, script_name)
|
||||||
|
temp_object = loaded_class()
|
||||||
|
Logger.log("d", "Begin loading of script: %s", script_name)
|
||||||
|
try:
|
||||||
|
setting_data = temp_object.getSettingData()
|
||||||
|
if "name" in setting_data and "key" in setting_data:
|
||||||
|
self._script_labels[setting_data["key"]] = setting_data["name"]
|
||||||
|
self._loaded_scripts[setting_data["key"]] = loaded_class
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Script %s.py has no name or key", script_name)
|
||||||
|
self._script_labels[script_name] = script_name
|
||||||
|
self._loaded_scripts[script_name] = loaded_class
|
||||||
|
except AttributeError:
|
||||||
|
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
|
||||||
|
except NotImplementedError:
|
||||||
|
Logger.log("e", "Script %s.py has no implemented settings", script_name)
|
||||||
|
self.loadedScriptListChanged.emit()
|
||||||
|
|
||||||
|
loadedScriptListChanged = pyqtSignal()
|
||||||
|
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
|
||||||
|
def loadedScriptList(self):
|
||||||
|
return sorted(list(self._loaded_scripts.keys()))
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def getScriptLabelByKey(self, key):
|
||||||
|
return self._script_labels[key]
|
||||||
|
|
||||||
|
scriptListChanged = pyqtSignal()
|
||||||
|
@pyqtProperty("QVariantList", notify = scriptListChanged)
|
||||||
|
def scriptList(self):
|
||||||
|
script_list = [script.getSettingData()["key"] for script in self._script_list]
|
||||||
|
return script_list
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def addScriptToList(self, key):
|
||||||
|
Logger.log("d", "Adding script %s to list.", key)
|
||||||
|
new_script = self._loaded_scripts[key]()
|
||||||
|
self._script_list.append(new_script)
|
||||||
|
self.setSelectedScriptIndex(len(self._script_list) - 1)
|
||||||
|
self.scriptListChanged.emit()
|
||||||
|
self._propertyChanged()
|
||||||
|
|
||||||
|
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||||
|
def _createView(self):
|
||||||
|
Logger.log("d", "Creating post processing plugin view.")
|
||||||
|
|
||||||
|
## Load all scripts in the scripts folders
|
||||||
|
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
|
||||||
|
try:
|
||||||
|
path = os.path.join(root, "scripts")
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError:
|
||||||
|
Logger.log("w", "Unable to create a folder for scripts: " + path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.loadAllScripts(path)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
|
||||||
|
|
||||||
|
# Create the plugin dialog component
|
||||||
|
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
|
||||||
|
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
|
Logger.log("d", "Post processing view created.")
|
||||||
|
|
||||||
|
# Create the save button component
|
||||||
|
Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
|
||||||
|
|
||||||
|
## Show the (GUI) popup of the post processing plugin.
|
||||||
|
def showPopup(self):
|
||||||
|
if self._view is None:
|
||||||
|
self._createView()
|
||||||
|
self._view.show()
|
||||||
|
|
||||||
|
## Property changed: trigger re-slice
|
||||||
|
# To do this we use the global container stack propertyChanged.
|
||||||
|
# Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||||
|
# are applied only once per "fresh" gcode
|
||||||
|
def _propertyChanged(self):
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
|
||||||
|
|
||||||
|
|
||||||
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
|
|
@ -0,0 +1,501 @@
|
||||||
|
// Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
|
||||||
|
// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 1.1
|
||||||
|
import QtQuick.Controls.Styles 1.1
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtQuick.Dialogs 1.1
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.2 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
UM.Dialog
|
||||||
|
{
|
||||||
|
id: dialog
|
||||||
|
|
||||||
|
title: catalog.i18nc("@title:window", "Post Processing Plugin")
|
||||||
|
width: 700 * screenScaleFactor;
|
||||||
|
height: 500 * screenScaleFactor;
|
||||||
|
minimumWidth: 400 * screenScaleFactor;
|
||||||
|
minimumHeight: 250 * screenScaleFactor;
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
UM.I18nCatalog{id: catalog; name:"cura"}
|
||||||
|
id: base
|
||||||
|
property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width)
|
||||||
|
property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
|
||||||
|
property string activeScriptName
|
||||||
|
SystemPalette{ id: palette }
|
||||||
|
SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled }
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
ExclusiveGroup
|
||||||
|
{
|
||||||
|
id: selectedScriptGroup
|
||||||
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: activeScripts
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: base.columnWidth
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: activeScriptsHeader
|
||||||
|
text: catalog.i18nc("@label", "Post Processing Scripts")
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: base.textMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: base.textMargin
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: base.textMargin
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
}
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: activeScriptsList
|
||||||
|
anchors.top: activeScriptsHeader.bottom
|
||||||
|
anchors.topMargin: base.textMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: base.textMargin
|
||||||
|
height: childrenRect.height
|
||||||
|
model: manager.scriptList
|
||||||
|
delegate: Item
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: activeScriptButton.height
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: activeScriptButton
|
||||||
|
text: manager.getScriptLabelByKey(modelData.toString())
|
||||||
|
exclusiveGroup: selectedScriptGroup
|
||||||
|
checkable: true
|
||||||
|
checked: {
|
||||||
|
if (manager.selectedScriptIndex == index)
|
||||||
|
{
|
||||||
|
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
forceActiveFocus()
|
||||||
|
manager.setSelectedScriptIndex(index)
|
||||||
|
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
|
||||||
|
}
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("setting").height
|
||||||
|
style: ButtonStyle
|
||||||
|
{
|
||||||
|
background: Rectangle
|
||||||
|
{
|
||||||
|
color: activeScriptButton.checked ? palette.highlight : "transparent"
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
label: Label
|
||||||
|
{
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
text: control.text
|
||||||
|
color: activeScriptButton.checked ? palette.highlightedText : palette.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: removeButton
|
||||||
|
text: "x"
|
||||||
|
width: 20 * screenScaleFactor
|
||||||
|
height: 20 * screenScaleFactor
|
||||||
|
anchors.right:parent.right
|
||||||
|
anchors.rightMargin: base.textMargin
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: manager.removeScriptByIndex(index)
|
||||||
|
style: ButtonStyle
|
||||||
|
{
|
||||||
|
label: Item
|
||||||
|
{
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.floor(control.width / 2.7)
|
||||||
|
height: Math.floor(control.height / 2.7)
|
||||||
|
sourceSize.width: width
|
||||||
|
sourceSize.height: width
|
||||||
|
color: palette.text
|
||||||
|
source: UM.Theme.getIcon("cross1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: downButton
|
||||||
|
text: ""
|
||||||
|
anchors.right: removeButton.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
enabled: index != manager.scriptList.length - 1
|
||||||
|
width: 20 * screenScaleFactor
|
||||||
|
height: 20 * screenScaleFactor
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
if (manager.selectedScriptIndex == index)
|
||||||
|
{
|
||||||
|
manager.setSelectedScriptIndex(index + 1)
|
||||||
|
}
|
||||||
|
return manager.moveScript(index, index + 1)
|
||||||
|
}
|
||||||
|
style: ButtonStyle
|
||||||
|
{
|
||||||
|
label: Item
|
||||||
|
{
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.floor(control.width / 2.5)
|
||||||
|
height: Math.floor(control.height / 2.5)
|
||||||
|
sourceSize.width: width
|
||||||
|
sourceSize.height: width
|
||||||
|
color: control.enabled ? palette.text : disabledPalette.text
|
||||||
|
source: UM.Theme.getIcon("arrow_bottom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: upButton
|
||||||
|
text: ""
|
||||||
|
enabled: index != 0
|
||||||
|
width: 20 * screenScaleFactor
|
||||||
|
height: 20 * screenScaleFactor
|
||||||
|
anchors.right: downButton.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
if (manager.selectedScriptIndex == index)
|
||||||
|
{
|
||||||
|
manager.setSelectedScriptIndex(index - 1)
|
||||||
|
}
|
||||||
|
return manager.moveScript(index, index - 1)
|
||||||
|
}
|
||||||
|
style: ButtonStyle
|
||||||
|
{
|
||||||
|
label: Item
|
||||||
|
{
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.floor(control.width / 2.5)
|
||||||
|
height: Math.floor(control.height / 2.5)
|
||||||
|
sourceSize.width: width
|
||||||
|
sourceSize.height: width
|
||||||
|
color: control.enabled ? palette.text : disabledPalette.text
|
||||||
|
source: UM.Theme.getIcon("arrow_top")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: addButton
|
||||||
|
text: catalog.i18nc("@action", "Add a script")
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: base.textMargin
|
||||||
|
anchors.top: activeScriptsList.bottom
|
||||||
|
anchors.topMargin: base.textMargin
|
||||||
|
menu: scriptsMenu
|
||||||
|
style: ButtonStyle
|
||||||
|
{
|
||||||
|
label: Label
|
||||||
|
{
|
||||||
|
text: control.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Menu
|
||||||
|
{
|
||||||
|
id: scriptsMenu
|
||||||
|
|
||||||
|
Instantiator
|
||||||
|
{
|
||||||
|
model: manager.loadedScriptList
|
||||||
|
|
||||||
|
MenuItem
|
||||||
|
{
|
||||||
|
text: manager.getScriptLabelByKey(modelData.toString())
|
||||||
|
onTriggered: manager.addScriptToList(modelData.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectAdded: scriptsMenu.insertItem(index, object);
|
||||||
|
onObjectRemoved: scriptsMenu.removeItem(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
color: UM.Theme.getColor("sidebar")
|
||||||
|
anchors.left: activeScripts.right
|
||||||
|
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: parent.height
|
||||||
|
id: settingsPanel
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: scriptSpecsHeader
|
||||||
|
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: base.textMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: base.textMargin
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: base.textMargin
|
||||||
|
height: 20 * screenScaleFactor
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView
|
||||||
|
{
|
||||||
|
id: scrollView
|
||||||
|
anchors.top: scriptSpecsHeader.bottom
|
||||||
|
anchors.topMargin: settingsPanel.textMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
visible: manager.selectedScriptDefinitionId != ""
|
||||||
|
style: UM.Theme.styles.scrollview;
|
||||||
|
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: listview
|
||||||
|
spacing: UM.Theme.getSize("default_lining").height
|
||||||
|
model: UM.SettingDefinitionsModel
|
||||||
|
{
|
||||||
|
id: definitionsModel;
|
||||||
|
containerId: manager.selectedScriptDefinitionId
|
||||||
|
showAll: true
|
||||||
|
}
|
||||||
|
delegate:Loader
|
||||||
|
{
|
||||||
|
id: settingLoader
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height:
|
||||||
|
{
|
||||||
|
if(provider.properties.enabled == "True")
|
||||||
|
{
|
||||||
|
if(model.type != undefined)
|
||||||
|
{
|
||||||
|
return UM.Theme.getSize("section").height;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Behavior on height { NumberAnimation { duration: 100 } }
|
||||||
|
opacity: provider.properties.enabled == "True" ? 1 : 0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||||
|
enabled: opacity > 0
|
||||||
|
property var definition: model
|
||||||
|
property var settingDefinitionsModel: definitionsModel
|
||||||
|
property var propertyProvider: provider
|
||||||
|
property var globalPropertyProvider: inheritStackProvider
|
||||||
|
|
||||||
|
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
|
||||||
|
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
|
||||||
|
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
|
||||||
|
asynchronous: model.type != "enum" && model.type != "extruder"
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
settingLoader.item.showRevertButton = false
|
||||||
|
settingLoader.item.showInheritButton = false
|
||||||
|
settingLoader.item.showLinkedSettingIcon = false
|
||||||
|
settingLoader.item.doDepthIndentation = true
|
||||||
|
settingLoader.item.doQualityUserSettingEmphasis = false
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent:
|
||||||
|
{
|
||||||
|
switch(model.type)
|
||||||
|
{
|
||||||
|
case "int":
|
||||||
|
return settingTextField
|
||||||
|
case "float":
|
||||||
|
return settingTextField
|
||||||
|
case "enum":
|
||||||
|
return settingComboBox
|
||||||
|
case "extruder":
|
||||||
|
return settingExtruder
|
||||||
|
case "bool":
|
||||||
|
return settingCheckBox
|
||||||
|
case "str":
|
||||||
|
return settingTextField
|
||||||
|
case "category":
|
||||||
|
return settingCategory
|
||||||
|
default:
|
||||||
|
return settingUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.SettingPropertyProvider
|
||||||
|
{
|
||||||
|
id: provider
|
||||||
|
containerStackId: manager.selectedScriptStackId
|
||||||
|
key: model.key ? model.key : "None"
|
||||||
|
watchedProperties: [ "value", "enabled", "state", "validationState" ]
|
||||||
|
storeIndex: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
|
||||||
|
// so we bypass that to make a dedicated provider).
|
||||||
|
UM.SettingPropertyProvider
|
||||||
|
{
|
||||||
|
id: inheritStackProvider
|
||||||
|
containerStackId: Cura.MachineManager.activeMachineId
|
||||||
|
key: model.key ? model.key : "None"
|
||||||
|
watchedProperties: [ "limit_to_extruder" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: item
|
||||||
|
|
||||||
|
onShowTooltip:
|
||||||
|
{
|
||||||
|
tooltip.text = text;
|
||||||
|
var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
|
||||||
|
tooltip.show(position);
|
||||||
|
tooltip.target.x = position.x + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
onHideTooltip:
|
||||||
|
{
|
||||||
|
tooltip.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SidebarTooltip
|
||||||
|
{
|
||||||
|
id: tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingTextField;
|
||||||
|
|
||||||
|
Cura.SettingTextField { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingComboBox;
|
||||||
|
|
||||||
|
Cura.SettingComboBox { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingExtruder;
|
||||||
|
|
||||||
|
Cura.SettingExtruder { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingCheckBox;
|
||||||
|
|
||||||
|
Cura.SettingCheckBox { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingCategory;
|
||||||
|
|
||||||
|
Cura.SettingCategory { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Component
|
||||||
|
{
|
||||||
|
id: settingUnknown;
|
||||||
|
|
||||||
|
Cura.SettingUnknown { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rightButtons: Button
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:button", "Close")
|
||||||
|
iconName: "dialog-close"
|
||||||
|
onClicked: dialog.accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
objectName: "postProcessingSaveAreaButton"
|
||||||
|
visible: activeScriptsList.count > 0
|
||||||
|
height: UM.Theme.getSize("save_button_save_to_button").height
|
||||||
|
width: height
|
||||||
|
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
|
||||||
|
onClicked: dialog.show()
|
||||||
|
|
||||||
|
style: ButtonStyle {
|
||||||
|
background: Rectangle {
|
||||||
|
id: deviceSelectionIcon
|
||||||
|
border.width: UM.Theme.getSize("default_lining").width
|
||||||
|
border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
|
||||||
|
control.pressed ? UM.Theme.getColor("action_button_active_border") :
|
||||||
|
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
|
||||||
|
color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
|
||||||
|
control.pressed ? UM.Theme.getColor("action_button_active") :
|
||||||
|
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
|
||||||
|
Behavior on color { ColorAnimation { duration: 50; } }
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2);
|
||||||
|
width: parent.height
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
UM.RecolorImage {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.floor(parent.width / 2)
|
||||||
|
height: Math.floor(parent.height / 2)
|
||||||
|
sourceSize.width: width
|
||||||
|
sourceSize.height: height
|
||||||
|
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
|
||||||
|
control.pressed ? UM.Theme.getColor("action_button_active_text") :
|
||||||
|
control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
|
||||||
|
source: "postprocessing.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label: Label{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
plugins/PostProcessingPlugin/README.md
Normal file
2
plugins/PostProcessingPlugin/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# PostProcessingPlugin
|
||||||
|
A post processing plugin for Cura
|
||||||
111
plugins/PostProcessingPlugin/Script.py
Normal file
111
plugins/PostProcessingPlugin/Script.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Copyright (c) 2015 Jaime van Kessel
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Signal import Signal, signalemitter
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
# Setting stuff import
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import collections
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Base class for scripts. All scripts should inherit the script class.
|
||||||
|
@signalemitter
|
||||||
|
class Script:
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._settings = None
|
||||||
|
self._stack = None
|
||||||
|
|
||||||
|
setting_data = self.getSettingData()
|
||||||
|
self._stack = ContainerStack(stack_id = str(id(self)))
|
||||||
|
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||||
|
|
||||||
|
|
||||||
|
## Check if the definition of this script already exists. If not, add it to the registry.
|
||||||
|
if "key" in setting_data:
|
||||||
|
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"])
|
||||||
|
if definitions:
|
||||||
|
# Definition was found
|
||||||
|
self._definition = definitions[0]
|
||||||
|
else:
|
||||||
|
self._definition = DefinitionContainer(setting_data["key"])
|
||||||
|
self._definition.deserialize(json.dumps(setting_data))
|
||||||
|
ContainerRegistry.getInstance().addContainer(self._definition)
|
||||||
|
self._stack.addContainer(self._definition)
|
||||||
|
self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
|
||||||
|
self._instance.setDefinition(self._definition.getId())
|
||||||
|
self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0))
|
||||||
|
self._stack.addContainer(self._instance)
|
||||||
|
self._stack.propertyChanged.connect(self._onPropertyChanged)
|
||||||
|
|
||||||
|
ContainerRegistry.getInstance().addContainer(self._stack)
|
||||||
|
|
||||||
|
settingsLoaded = Signal()
|
||||||
|
valueChanged = Signal() # Signal emitted whenever a value of a setting is changed
|
||||||
|
|
||||||
|
def _onPropertyChanged(self, key, property_name):
|
||||||
|
if property_name == "value":
|
||||||
|
self.valueChanged.emit()
|
||||||
|
|
||||||
|
# Property changed: trigger reslice
|
||||||
|
# To do this we use the global container stack propertyChanged.
|
||||||
|
# Reslicing is necessary for setting changes in this plugin, because the changes
|
||||||
|
# are applied only once per "fresh" gcode
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
global_container_stack.propertyChanged.emit(key, property_name)
|
||||||
|
|
||||||
|
## Needs to return a dict that can be used to construct a settingcategory file.
|
||||||
|
# See the example script for an example.
|
||||||
|
# It follows the same style / guides as the Uranium settings.
|
||||||
|
# Scripts can either override getSettingData directly, or use getSettingDataString
|
||||||
|
# to return a string that will be parsed as json. The latter has the benefit over
|
||||||
|
# returning a dict in that the order of settings is maintained.
|
||||||
|
def getSettingData(self):
|
||||||
|
setting_data = self.getSettingDataString()
|
||||||
|
if type(setting_data) == str:
|
||||||
|
setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
|
||||||
|
return setting_data
|
||||||
|
|
||||||
|
def getSettingDataString(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def getDefinitionId(self):
|
||||||
|
if self._stack:
|
||||||
|
return self._stack.getBottom().getId()
|
||||||
|
|
||||||
|
def getStackId(self):
|
||||||
|
if self._stack:
|
||||||
|
return self._stack.getId()
|
||||||
|
|
||||||
|
## Convenience function that retrieves value of a setting from the stack.
|
||||||
|
def getSettingValueByKey(self, key):
|
||||||
|
return self._stack.getProperty(key, "value")
|
||||||
|
|
||||||
|
## Convenience function that finds the value in a line of g-code.
|
||||||
|
# When requesting key = x from line "G1 X100" the value 100 is returned.
|
||||||
|
def getValue(self, line, key, default = None):
|
||||||
|
if not key in line or (';' in line and line.find(key) > line.find(';')):
|
||||||
|
return default
|
||||||
|
sub_part = line[line.find(key) + 1:]
|
||||||
|
m = re.search('^-?[0-9]+\.?[0-9]*', sub_part)
|
||||||
|
if m is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return float(m.group(0))
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
## This is called when the script is executed.
|
||||||
|
# It gets a list of g-code strings and needs to return a (modified) list.
|
||||||
|
def execute(self, data):
|
||||||
|
raise NotImplementedError()
|
||||||
11
plugins/PostProcessingPlugin/__init__.py
Normal file
11
plugins/PostProcessingPlugin/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
|
||||||
|
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
|
from . import PostProcessingPlugin
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
def getMetaData():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return {"extension": PostProcessingPlugin.PostProcessingPlugin()}
|
||||||
8
plugins/PostProcessingPlugin/plugin.json
Normal file
8
plugins/PostProcessingPlugin/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Post Processing",
|
||||||
|
"author": "Ultimaker",
|
||||||
|
"version": "2.2",
|
||||||
|
"api": 4,
|
||||||
|
"description": "Extension that allows for user created scripts for post processing",
|
||||||
|
"catalog": "cura"
|
||||||
|
}
|
||||||
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="512px"
|
||||||
|
height="512px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="enable-background:new 0 0 512 512;"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="postprocessing.svg"><metadata
|
||||||
|
id="metadata9"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs7" /><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1104"
|
||||||
|
inkscape:window-height="1006"
|
||||||
|
id="namedview5"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.3359375"
|
||||||
|
inkscape:cx="256"
|
||||||
|
inkscape:cy="256"
|
||||||
|
inkscape:window-x="701"
|
||||||
|
inkscape:window-y="121"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="Layer_1" /><path
|
||||||
|
d="M 402.15234 0 C 371.74552 4.7369516e-015 345.79114 10.752017 324.21875 32.324219 C 302.57788 53.89652 291.82617 79.851497 291.82617 110.18945 C 291.82617 127.34315 295.26662 143.09419 302.16602 157.44531 L 238.38477 221.20312 C 227.77569 210.95036 218.04331 201.50935 209.66016 193.32422 C 207.33386 190.99792 202.68042 189.48707 198.60938 191.92969 L 191.74609 196.11719 C 165.34252 169.24836 154.17609 158.42965 150.57031 145.40234 C 146.84822 131.79345 150.22148 113.64862 153.71094 106.90234 C 156.61882 101.55183 165.69233 96.550326 173.36914 95.96875 L 183.37109 106.20508 C 185.69739 108.53139 189.30456 108.53139 191.63086 106.20508 L 227.57227 69.681641 C 229.89858 67.355335 229.89858 63.517712 227.57227 61.191406 L 169.53125 2.21875 C 167.20494 -0.10755598 163.48147 -0.10755598 161.27148 2.21875 L 125.33008 38.742188 C 123.00378 41.068494 123.00378 44.906116 125.33008 47.232422 L 129.16992 51.1875 C 129.16992 56.88695 128.35573 65.727167 123.70312 70.496094 C 116.49157 77.823958 102.18413 69.332919 92.878906 75.962891 C 83.689998 82.476548 72.05746 92.944493 64.613281 100.38867 C 57.285417 107.83285 29.138171 137.37722 9.015625 187.16016 C -11.106922 236.94311 4.3632369 283.12 15.296875 295.2168 C 21.11264 301.61414 31.696982 308.12804 29.835938 296.03125 C 27.974892 283.81815 24.951448 241.47942 38.792969 224.14844 C 52.634489 206.81746 70.894726 192.62799 94.623047 191.46484 C 117.42084 190.30169 130.56529 198.09417 160.10938 228.10352 L 156.85156 234.15234 C 154.75788 238.10706 155.92175 243.10728 158.24805 245.43359 C 161.95717 248.74082 172.37305 258.96006 186.52539 273.04297 L 6.9511719 452.54883 C 2.2984329 457.14417 1.1842379e-015 462.71497 0 469.14844 C -1.1842379e-015 475.69681 2.2984329 481.15473 6.9511719 485.51953 L 26.308594 505.22266 C 31.018838 509.76054 36.589603 512 42.908203 512 C 49.341623 512 54.800053 509.76054 59.337891 505.22266 L 238.96875 325.6582 C 317.6609 404.95524 424.21289 513.40234 424.21289 513.40234 L 482.25391 454.43164 C 437.71428 411.9686 358.71135 336.76293 291.93164 272.71484 L 354.68945 209.98047 C 369.08663 216.91399 384.90203 220.37891 402.15234 220.37891 C 425.29988 220.37891 446.52947 213.53073 465.77344 199.83398 C 485.08493 186.1372 498.57775 168.33291 506.31641 146.34961 C 510.08303 135.39222 512 126.69334 512 120.25586 C 512 117.79044 511.24662 115.80572 509.87695 114.16211 C 508.50726 112.5185 506.59041 111.69531 504.125 111.69531 C 502.61835 111.69531 496.86414 114.5734 486.72852 120.39453 C 476.6614 126.21564 465.50054 132.85752 453.37891 140.32227 C 441.18878 147.78698 434.7515 151.75888 433.92969 152.23828 L 386.40234 125.94141 L 386.40234 70.8125 L 458.51562 29.242188 C 461.18649 27.461587 462.48633 25.202356 462.48633 22.394531 C 462.48633 19.586706 461.1865 17.325625 458.51562 15.476562 C 451.32484 10.545729 442.4896 6.780346 432.08008 4.0410156 C 421.60206 1.3701797 411.67159 0 402.15234 0 z "
|
||||||
|
id="path3" /></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from ..Script import Script
|
||||||
|
class BQ_PauseAtHeight(Script):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getSettingDataString(self):
|
||||||
|
return """{
|
||||||
|
"name":"Pause at height (BQ Printers)",
|
||||||
|
"key": "BQ_PauseAtHeight",
|
||||||
|
"metadata":{},
|
||||||
|
"version": 2,
|
||||||
|
"settings":
|
||||||
|
{
|
||||||
|
"pause_height":
|
||||||
|
{
|
||||||
|
"label": "Pause height",
|
||||||
|
"description": "At what height should the pause occur",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 5.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
def execute(self, data):
|
||||||
|
x = 0.
|
||||||
|
y = 0.
|
||||||
|
current_z = 0.
|
||||||
|
pause_z = self.getSettingValueByKey("pause_height")
|
||||||
|
for layer in data:
|
||||||
|
lines = layer.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
|
||||||
|
current_z = self.getValue(line, 'Z')
|
||||||
|
if current_z != None:
|
||||||
|
if current_z >= pause_z:
|
||||||
|
prepend_gcode = ";TYPE:CUSTOM\n"
|
||||||
|
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
|
||||||
|
|
||||||
|
# Insert Pause gcode
|
||||||
|
prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n"
|
||||||
|
|
||||||
|
index = data.index(layer)
|
||||||
|
layer = prepend_gcode + layer
|
||||||
|
data[index] = layer # Override the data of this layer with the modified data
|
||||||
|
return data
|
||||||
|
break
|
||||||
|
return data
|
||||||
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# This PostProcessing Plugin script is released
|
||||||
|
# under the terms of the AGPLv3 or higher
|
||||||
|
|
||||||
|
from ..Script import Script
|
||||||
|
#from UM.Logger import Logger
|
||||||
|
# from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
class ColorChange(Script):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getSettingDataString(self):
|
||||||
|
return """{
|
||||||
|
"name":"Color Change",
|
||||||
|
"key": "ColorChange",
|
||||||
|
"metadata": {},
|
||||||
|
"version": 2,
|
||||||
|
"settings":
|
||||||
|
{
|
||||||
|
"layer_number":
|
||||||
|
{
|
||||||
|
"label": "Layer",
|
||||||
|
"description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.",
|
||||||
|
"unit": "",
|
||||||
|
"type": "str",
|
||||||
|
"default_value": "1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"initial_retract":
|
||||||
|
{
|
||||||
|
"label": "Initial Retraction",
|
||||||
|
"description": "Initial filament retraction distance",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 300.0
|
||||||
|
},
|
||||||
|
"later_retract":
|
||||||
|
{
|
||||||
|
"label": "Later Retraction Distance",
|
||||||
|
"description": "Later filament retraction distance for removal",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 30.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
def execute(self, data: list):
|
||||||
|
|
||||||
|
"""data is a list. Each index contains a layer"""
|
||||||
|
layer_nums = self.getSettingValueByKey("layer_number")
|
||||||
|
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||||
|
later_retract = self.getSettingValueByKey("later_retract")
|
||||||
|
|
||||||
|
color_change = "M600"
|
||||||
|
|
||||||
|
if initial_retract is not None and initial_retract > 0.:
|
||||||
|
color_change = color_change + (" E%.2f" % initial_retract)
|
||||||
|
|
||||||
|
if later_retract is not None and later_retract > 0.:
|
||||||
|
color_change = color_change + (" L%.2f" % later_retract)
|
||||||
|
|
||||||
|
color_change = color_change + " ; Generated by ColorChange plugin"
|
||||||
|
|
||||||
|
layer_targets = layer_nums.split(',')
|
||||||
|
if len(layer_targets) > 0:
|
||||||
|
for layer_num in layer_targets:
|
||||||
|
layer_num = int( layer_num.strip() )
|
||||||
|
if layer_num < len(data):
|
||||||
|
layer = data[ layer_num - 1 ]
|
||||||
|
lines = layer.split("\n")
|
||||||
|
lines.insert(2, color_change )
|
||||||
|
final_line = "\n".join( lines )
|
||||||
|
data[ layer_num - 1 ] = final_line
|
||||||
|
|
||||||
|
return data
|
||||||
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
|
||||||
|
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
from ..Script import Script
|
||||||
|
|
||||||
|
class ExampleScript(Script):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getSettingDataString(self):
|
||||||
|
return """{
|
||||||
|
"name":"Example script",
|
||||||
|
"key": "ExampleScript",
|
||||||
|
"metadata": {},
|
||||||
|
"version": 2,
|
||||||
|
"settings":
|
||||||
|
{
|
||||||
|
"test":
|
||||||
|
{
|
||||||
|
"label": "Test",
|
||||||
|
"description": "None",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 0.5,
|
||||||
|
"minimum_value": "0",
|
||||||
|
"minimum_value_warning": "0.1",
|
||||||
|
"maximum_value_warning": "1"
|
||||||
|
},
|
||||||
|
"derp":
|
||||||
|
{
|
||||||
|
"label": "zomg",
|
||||||
|
"description": "afgasgfgasfgasf",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 0.5,
|
||||||
|
"minimum_value": "0",
|
||||||
|
"minimum_value_warning": "0.1",
|
||||||
|
"maximum_value_warning": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
def execute(self, data):
|
||||||
|
return data
|
||||||
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
from ..Script import Script
|
||||||
|
# from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
class PauseAtHeight(Script):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getSettingDataString(self):
|
||||||
|
return """{
|
||||||
|
"name":"Pause at height",
|
||||||
|
"key": "PauseAtHeight",
|
||||||
|
"metadata": {},
|
||||||
|
"version": 2,
|
||||||
|
"settings":
|
||||||
|
{
|
||||||
|
"pause_height":
|
||||||
|
{
|
||||||
|
"label": "Pause Height",
|
||||||
|
"description": "At what height should the pause occur",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 5.0
|
||||||
|
},
|
||||||
|
"head_park_x":
|
||||||
|
{
|
||||||
|
"label": "Park Print Head X",
|
||||||
|
"description": "What X location does the head move to when pausing.",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 190
|
||||||
|
},
|
||||||
|
"head_park_y":
|
||||||
|
{
|
||||||
|
"label": "Park Print Head Y",
|
||||||
|
"description": "What Y location does the head move to when pausing.",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 190
|
||||||
|
},
|
||||||
|
"retraction_amount":
|
||||||
|
{
|
||||||
|
"label": "Retraction",
|
||||||
|
"description": "How much filament must be retracted at pause.",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 0
|
||||||
|
},
|
||||||
|
"retraction_speed":
|
||||||
|
{
|
||||||
|
"label": "Retraction Speed",
|
||||||
|
"description": "How fast to retract the filament.",
|
||||||
|
"unit": "mm/s",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 25
|
||||||
|
},
|
||||||
|
"extrude_amount":
|
||||||
|
{
|
||||||
|
"label": "Extrude Amount",
|
||||||
|
"description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
|
||||||
|
"unit": "mm",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 0
|
||||||
|
},
|
||||||
|
"extrude_speed":
|
||||||
|
{
|
||||||
|
"label": "Extrude Speed",
|
||||||
|
"description": "How fast to extrude the material after pause.",
|
||||||
|
"unit": "mm/s",
|
||||||
|
"type": "float",
|
||||||
|
"default_value": 3.3333
|
||||||
|
},
|
||||||
|
"redo_layers":
|
||||||
|
{
|
||||||
|
"label": "Redo Layers",
|
||||||
|
"description": "Redo a number of previous layers after a pause to increases adhesion.",
|
||||||
|
"unit": "layers",
|
||||||
|
"type": "int",
|
||||||
|
"default_value": 0
|
||||||
|
},
|
||||||
|
"standby_temperature":
|
||||||
|
{
|
||||||
|
"label": "Standby Temperature",
|
||||||
|
"description": "Change the temperature during the pause",
|
||||||
|
"unit": "°C",
|
||||||
|
"type": "int",
|
||||||
|
"default_value": 0
|
||||||
|
},
|
||||||
|
"resume_temperature":
|
||||||
|
{
|
||||||
|
"label": "Resume Temperature",
|
||||||
|
"description": "Change the temperature after the pause",
|
||||||
|
"unit": "°C",
|
||||||
|
"type": "int",
|
||||||
|
"default_value": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
def execute(self, data: list):
|
||||||
|
|
||||||
|
"""data is a list. Each index contains a layer"""
|
||||||
|
|
||||||
|
x = 0.
|
||||||
|
y = 0.
|
||||||
|
current_z = 0.
|
||||||
|
pause_height = self.getSettingValueByKey("pause_height")
|
||||||
|
retraction_amount = self.getSettingValueByKey("retraction_amount")
|
||||||
|
retraction_speed = self.getSettingValueByKey("retraction_speed")
|
||||||
|
extrude_amount = self.getSettingValueByKey("extrude_amount")
|
||||||
|
extrude_speed = self.getSettingValueByKey("extrude_speed")
|
||||||
|
park_x = self.getSettingValueByKey("head_park_x")
|
||||||
|
park_y = self.getSettingValueByKey("head_park_y")
|
||||||
|
layers_started = False
|
||||||
|
redo_layers = self.getSettingValueByKey("redo_layers")
|
||||||
|
standby_temperature = self.getSettingValueByKey("standby_temperature")
|
||||||
|
resume_temperature = self.getSettingValueByKey("resume_temperature")
|
||||||
|
|
||||||
|
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
|
||||||
|
# with open("out.txt", "w") as f:
|
||||||
|
# f.write(T)
|
||||||
|
|
||||||
|
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
|
||||||
|
layer_0_z = 0.
|
||||||
|
got_first_g_cmd_on_layer_0 = False
|
||||||
|
for layer in data:
|
||||||
|
lines = layer.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if ";LAYER:0" in line:
|
||||||
|
layers_started = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not layers_started:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
|
||||||
|
current_z = self.getValue(line, 'Z')
|
||||||
|
if not got_first_g_cmd_on_layer_0:
|
||||||
|
layer_0_z = current_z
|
||||||
|
got_first_g_cmd_on_layer_0 = True
|
||||||
|
|
||||||
|
x = self.getValue(line, 'X', x)
|
||||||
|
y = self.getValue(line, 'Y', y)
|
||||||
|
if current_z is not None:
|
||||||
|
current_height = current_z - layer_0_z
|
||||||
|
if current_height >= pause_height:
|
||||||
|
index = data.index(layer)
|
||||||
|
prevLayer = data[index - 1]
|
||||||
|
prevLines = prevLayer.split("\n")
|
||||||
|
current_e = 0.
|
||||||
|
for prevLine in reversed(prevLines):
|
||||||
|
current_e = self.getValue(prevLine, 'E', -1)
|
||||||
|
if current_e >= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# include a number of previous layers
|
||||||
|
for i in range(1, redo_layers + 1):
|
||||||
|
prevLayer = data[index - i]
|
||||||
|
layer = prevLayer + layer
|
||||||
|
|
||||||
|
prepend_gcode = ";TYPE:CUSTOM\n"
|
||||||
|
prepend_gcode += ";added code by post processing\n"
|
||||||
|
prepend_gcode += ";script: PauseAtHeight.py\n"
|
||||||
|
prepend_gcode += ";current z: %f \n" % current_z
|
||||||
|
prepend_gcode += ";current height: %f \n" % current_height
|
||||||
|
|
||||||
|
# Retraction
|
||||||
|
prepend_gcode += "M83\n"
|
||||||
|
if retraction_amount != 0:
|
||||||
|
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
|
||||||
|
|
||||||
|
# Move the head away
|
||||||
|
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
|
||||||
|
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
|
||||||
|
if current_z < 15:
|
||||||
|
prepend_gcode += "G1 Z15 F300\n"
|
||||||
|
|
||||||
|
# Disable the E steppers
|
||||||
|
prepend_gcode += "M84 E0\n"
|
||||||
|
|
||||||
|
# Set extruder standby temperature
|
||||||
|
prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature)
|
||||||
|
|
||||||
|
# Wait till the user continues printing
|
||||||
|
prepend_gcode += "M0 ;Do the actual pause\n"
|
||||||
|
|
||||||
|
# Set extruder resume temperature
|
||||||
|
prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature)
|
||||||
|
|
||||||
|
# Push the filament back,
|
||||||
|
if retraction_amount != 0:
|
||||||
|
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
|
||||||
|
|
||||||
|
# Optionally extrude material
|
||||||
|
if extrude_amount != 0:
|
||||||
|
prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60)
|
||||||
|
|
||||||
|
# and retract again, the properly primes the nozzle
|
||||||
|
# when changing filament.
|
||||||
|
if retraction_amount != 0:
|
||||||
|
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
|
||||||
|
|
||||||
|
# Move the head back
|
||||||
|
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
|
||||||
|
prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y)
|
||||||
|
if retraction_amount != 0:
|
||||||
|
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
|
||||||
|
prepend_gcode += "G1 F9000\n"
|
||||||
|
prepend_gcode += "M82\n"
|
||||||
|
|
||||||
|
# reset extrude value to pre pause value
|
||||||
|
prepend_gcode += "G92 E%f\n" % (current_e)
|
||||||
|
|
||||||
|
layer = prepend_gcode + layer
|
||||||
|
|
||||||
|
|
||||||
|
# Override the data of this layer with the
|
||||||
|
# modified data
|
||||||
|
data[index] = layer
|
||||||
|
return data
|
||||||
|
break
|
||||||
|
return data
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue