Merge remote-tracking branch 'refs/remotes/Ultimaker/master'

This commit is contained in:
Brecht Nuyttens 2018-01-15 16:29:21 +01:00
commit 33ba9a274d
434 changed files with 38422 additions and 9936 deletions

36
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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
View file

@ -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

2
Jenkinsfile vendored
View file

@ -1,3 +1,4 @@
timeout(time: 2, unit: "HOURS") {
parallel_nodes(['linux && cura', 'windows && cura']) { parallel_nodes(['linux && cura', 'windows && cura']) {
// Prepare building // Prepare building
stage('Prepare') { stage('Prepare') {
@ -43,3 +44,4 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.']) notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
} }
} }
}

View file

@ -1,22 +1,16 @@
Cura Cura
==== ====
This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
This is the new, shiny frontend for Cura. [daid/Cura](https://github.com/daid/Cura.git) is the old legacy Cura that everyone knows and loves/hates.
We re-worked the whole GUI code at Ultimaker, because the old code started to become 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.

View file

@ -24,16 +24,23 @@ function(cura_add_test)
if(WIN32) if(WIN32)
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH}) string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}\\;$ENV{PYTHONPATH}")
else() else()
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH}) string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}:$ENV{PYTHONPATH}")
endif() endif()
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).
if (NOT ${test_exists})
add_test( add_test(
NAME ${_NAME} NAME ${_NAME}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
) )
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
else()
message(WARNING "Duplicate test ${_NAME}!")
endif()
endfunction() endfunction()
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}") cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")

View file

@ -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
View file

@ -1,8 +1,8 @@
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from collections import namedtuple from collections import namedtuple
@ -30,6 +30,7 @@ class Arrange:
self._offset_x = offset_x self._offset_x = offset_x
self._offset_y = offset_y self._offset_y = offset_y
self._last_priority = 0 self._last_priority = 0
self._is_empty = True
## Helper to create an Arranger instance ## Helper to create an Arranger instance
# #
@ -38,8 +39,8 @@ class Arrange:
# \param scene_root Root for finding all scene nodes # \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed # \param fixed_nodes Scene nodes to be placed
@classmethod @classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
arranger = Arrange(220, 220, 110, 110, scale = scale) arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
if fixed_nodes is None: if fixed_nodes is None:
@ -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

View file

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

View file

@ -4,7 +4,6 @@
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Logger import Logger from UM.Logger import Logger
@ -12,9 +11,9 @@ from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from typing import List from typing import List

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

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

View file

53
cura/BuildPlateModel.py Normal file
View 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

View file

@ -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"]

View file

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

View file

@ -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()

View file

@ -13,12 +13,18 @@ from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Operations.TranslateOperation import TranslateOperation
from cura.SetParentOperation import SetParentOperation from cura.Operations.SetParentOperation import SetParentOperation
from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
from UM.Logger import Logger
class CuraActions(QObject): class CuraActions(QObject):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
@ -54,7 +60,11 @@ class CuraActions(QObject):
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
current_node = current_node.getParent() current_node = current_node.getParent()
center_operation = SetTransformOperation(current_node, Vector()) # This was formerly done with SetTransformOperation but because of
# unpredictable matrix deconstruction it was possible that mirrors
# could manifest as rotations. Centering is therefore done by
# moving the node to negative whatever its position is:
center_operation = TranslateOperation(current_node, -current_node._position)
operation.addOperation(center_operation) operation.addOperation(center_operation)
operation.push() operation.push()
@ -63,7 +73,7 @@ class CuraActions(QObject):
# \param count The number of times to multiply the selection. # \param count The number of times to multiply the selection.
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8) job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
job.start() job.start()
## Delete all selected objects. ## Delete all selected objects.
@ -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)

View file

@ -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,15 +635,30 @@ 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")
@ -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,6 +1155,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
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
nodes.append(node) nodes.append(node)
@ -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,9 +1480,10 @@ class CuraApplication(QtApplication):
if not child.getDecorator(ConvexHullDecorator): if not child.getDecorator(ConvexHullDecorator):
child.addDecorator(ConvexHullDecorator()) child.addDecorator(ConvexHullDecorator())
if arrange_objects_on_load:
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
# Only check position if it's not already blatantly obvious that it won't fit. # Only check position if it's not already blatantly obvious that it won't fit.
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: 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:
# Find node location # Find node location
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
@ -1420,6 +1496,15 @@ class CuraApplication(QtApplication):
# 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()
scene.sceneChanged.emit(node) scene.sceneChanged.emit(node)

View file

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

63
cura/ObjectsModel.py Normal file
View 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()

View file

@ -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"):

View file

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

View file

View file

@ -10,10 +10,10 @@ from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from . import PlatformPhysicsOperation from cura.Operations import PlatformPhysicsOperation
from . import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
import random # used for list shuffling import random # used for list shuffling
@ -34,6 +34,7 @@ class PlatformPhysics:
self._change_timer.timeout.connect(self._onChangeTimerFinished) self._change_timer.timeout.connect(self._onChangeTimerFinished)
self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot? self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick? self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
Preferences.getInstance().addPreference("physics/automatic_push_free", True) Preferences.getInstance().addPreference("physics/automatic_push_free", True)
Preferences.getInstance().addPreference("physics/automatic_drop_down", True) Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
@ -41,7 +42,7 @@ class PlatformPhysics:
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
self._change_timer.start() self._change_timer.start()
def _onChangeTimerFinished(self, was_triggered_by_tool=False): def _onChangeTimerFinished(self):
if not self._enabled: if not self._enabled:
return return
@ -60,7 +61,7 @@ class PlatformPhysics:
random.shuffle(nodes) random.shuffle(nodes)
for node in nodes: for node in nodes:
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
continue continue
bbox = node.getBoundingBox() bbox = node.getBoundingBox()
@ -76,11 +77,12 @@ class PlatformPhysics:
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.
@ -124,8 +129,20 @@ class PlatformPhysics:
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.
@ -160,4 +177,4 @@ class PlatformPhysics:
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
self._enabled = True self._enabled = True
self._onChangeTimerFinished(True) self._onChangeTimerFinished()

View file

@ -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:
@ -244,8 +275,22 @@ class PrintInformation(QObject):
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)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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

View file

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

View file

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

View file

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

View file

View file

@ -3,15 +3,21 @@
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Application import Application from UM.Application import Application
from enum import IntEnum # For the connection state tracking.
from typing import List, Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device. ## Printer output device adds extra interface options on top of output device.
@ -25,663 +31,151 @@ 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) ## Attempt to establish connection
def printerType(self): def connect(self):
return self._printer_type self.setConnectionState(ConnectionState.connecting)
self._update_timer.start()
@pyqtProperty(str, notify=printerStateChanged) ## Attempt to close the connection
def printerState(self): def close(self):
return self._printer_state self._update_timer.stop()
self.setConnectionState(ConnectionState.closed)
@pyqtProperty(str, notify = jobStateChanged) ## Ensure that close gets called when object is destroyed
def jobState(self): def __del__(self):
return self._job_state self.close()
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) @pyqtProperty(bool, notify=acceptsCommandsChanged)
def acceptsCommands(self): def acceptsCommands(self):
return self._accepts_commands return self._accepts_commands
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def setAcceptsCommands(self, accepts_commands): def _setAcceptsCommands(self, accepts_commands):
if self._accepts_commands != accepts_commands: if self._accepts_commands != accepts_commands:
self._accepts_commands = accepts_commands self._accepts_commands = accepts_commands
self.acceptsCommandsChanged.emit() 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
def connect(self):
raise NotImplementedError("connect needs to be implemented")
## Attempt to close the connection
def close(self):
raise NotImplementedError("close needs to be implemented")
@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
def __del__(self):
self.close()
## Get the x position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headX(self):
return self._head_x
## Get the y position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headY(self):
return self._head_y
## Get 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)
@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.
class ConnectionState(IntEnum): class ConnectionState(IntEnum):

View file

@ -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,7 +239,7 @@ 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()
@ -233,21 +247,17 @@ class QualityManager:
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 = []

View file

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

View file

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

View file

@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode
from UM.Resources import Resources from UM.Resources import Resources
from UM.Math.Color import Color from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
@ -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)

View 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)

View 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
View file

View 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,12 +891,12 @@ 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
@ -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)

View file

@ -14,6 +14,7 @@ from UM.Decorators import override
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingInstance import SettingInstance
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
@ -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
# #
@ -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,51 +209,39 @@ 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
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName())}
else:
profile_index = -1
global_profile = None
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:
global_profile = profile
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
elif profile_index < len(machine_extruders) + 1:
# This is assumed to be an extruder profile
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom())
if not profile.getMetaDataEntry("extruder"):
profile.addMetaDataEntry("extruder", extruder_id)
else:
profile.setMetaDataEntry("extruder", extruder_id)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
else: #More extruders in the imported file than in the machine.
continue #Delete the additional profiles.
result = self._configureProfile(profile, profile_id, new_name) result = self._configureProfile(profile, profile_id, new_name)
if result is not None: if result is not None:
return {"status": "error", "message": catalog.i18nc("@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": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!",
profile_index += 1 "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())}
@ -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()
@ -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,7 +662,7 @@ 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.

View file

@ -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,12 +487,18 @@ 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) Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None return None
for material in materials:
# Prefer a read-only material
if ContainerRegistry.getInstance().isReadOnly(material.getId()):
return material
return materials[0]
## Find the quality that should be used as "default" quality. ## Find the quality that should be used as "default" quality.
# #
# This will search for qualities that match the current definition and pick the preferred one, # This will search for qualities that match the current definition and pick the preferred one,
@ -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:

View file

@ -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(
@ -56,7 +56,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",
@ -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)

View file

@ -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:

View file

@ -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()

View file

@ -8,7 +8,7 @@ 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)

View file

@ -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)
configuration_type = parser["metadata"].get("type")
if configuration_type == "machine": if configuration_type == "machine":
configuration_type = "machine_stack" return "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

View file

@ -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
@ -117,6 +120,10 @@ class MachineManager(QObject):
"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()
@ -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() return
change_found = False
machine_id = self.activeMachineId machine_id = self.activeMachineId
extruders = extruder_manager.getMachineExtruders(machine_id) extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
matching_extruder = None key=lambda k: k.getMetaDataEntry("position"))
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): for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
if not self._global_container_stack: containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="variant",
definition=self._global_container_stack.definition.getId(),
name=extruder_model.hotendID)
if containers:
# The hotend ID is known.
machine_id = self.activeMachineId
if extruder.variant.getName() != extruder_model.hotendID:
change_found = True
self._auto_hotends_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 _onMaterialIdChanged(self):
if not self._global_container_stack or not self._printer_output_devices:
return return
definition_id = "fdmprinter" active_printer_model = self._printer_output_devices[0].activePrinter
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False): if not active_printer_model:
definition_id = self.activeQualityDefinitionId return
extruder_manager = ExtruderManager.getInstance()
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: change_found = False
# Save the material that needs to be changed. Multiple changes will be handled by the callback. machine_id = self.activeMachineId
if self._global_container_stack.getBottom().getMetaDataEntry("has_variants") and matching_extruder.variant: extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
variant_id = self.getQualityVariantId(self._global_container_stack.getBottom(), matching_extruder.variant) 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: for container in containers:
if container.getMetaDataEntry("variant") == variant_id: if container.get("variant") == variant_id:
self._auto_materials_changed[str(index)] = container.getId() self._auto_materials_changed[extruder.getMetaDataEntry("position")] = container["id"]
break break
else: else:
# Just use the first result we found. # Just use the first result we found.
self._auto_materials_changed[str(index)] = containers[0].getId() 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) self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
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)
@ -1209,7 +1245,7 @@ class MachineManager(QObject):
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():

View file

@ -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

View file

@ -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": []}

View file

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

View file

@ -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,6 +107,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
else: else:
quality_changes_container = containers[0] quality_changes_container = containers[0]
if quality_changes_container.getMetaDataEntry("quality_type") == "not_supported":
quality_container = self._empty_quality
else:
criteria = { criteria = {
"type": "quality", "type": "quality",
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"), "quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
@ -118,9 +120,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
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")
if quality_type == "not_supported":
containers = []
else:
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition()) definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
definition = quality_container.getDefinition() definition = quality_container.getDefinition()
@ -164,6 +171,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
return return
if quality_changes_container: if quality_changes_container:
if quality_type == "not_supported":
criteria = {"type": "quality_changes", "quality_type": quality_type, "name": quality_changes_container.getName()}
else:
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()} criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
if self._extruder_definition_id != "": if self._extruder_definition_id != "":
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id) extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
@ -178,7 +188,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
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,7 +223,6 @@ 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:

View file

@ -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()

View file

@ -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
View 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
View file

@ -0,0 +1,2 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

View file

@ -2,17 +2,45 @@
# 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()
# TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes import ctypes
from ctypes.util import find_library from ctypes.util import find_library
libGL = find_library("GL") libGL = find_library("GL")
@ -22,7 +50,8 @@ if Platform.isLinux(): # Needed for platform.linux_distribution, which is not av
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
@ -45,6 +74,7 @@ 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()
if not known_args["debug"]:
sys.excepthook = exceptHook sys.excepthook = exceptHook
# Workaround for a race condition on certain systems where there # Workaround for a race condition on certain systems where there
@ -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()

View file

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

View file

@ -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")

View file

@ -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
if self._container_registry.findContainerStacksMetadata(name = global_stack_id_original):
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_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"

View file

@ -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)

View file

@ -6,7 +6,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Application import Application from UM.Application import Application
import UM.Scene.SceneNode from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
import Savitar import Savitar
@ -61,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive self._store_archive = store_archive
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
# \returns Uranium Scenen node. # \returns Uranium Scene node.
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
if type(um_node) is not UM.Scene.SceneNode.SceneNode: if not isinstance(um_node, SceneNode):
return None return None
active_build_plate_nr = CuraApplication.getInstance().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)

View file

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

View file

@ -16,6 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from collections import defaultdict
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from . import ProcessSlicedLayersJob from . import ProcessSlicedLayersJob
from . import StartSliceJob from . import StartSliceJob
@ -69,9 +70,10 @@ class CuraEngineBackend(QObject, Backend):
# Workaround to disable layer view processing if layer view is not active. # Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().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,43 +406,67 @@ 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
root_scene_nodes_changed = True
else: else:
return # we got a single scenenode
if not source.callDecoration("isGroup"):
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
if source.getMeshData() is None: if source.getMeshData() is None:
return return
if source.getMeshData().getVertices() is None: if source.getMeshData().getVertices() is None:
return return
build_plate_changed.add(source_build_plate_number)
build_plate_changed.discard(None)
build_plate_changed.discard(-1) # object not on build plate
if not build_plate_changed:
return
if self._tool_active: if self._tool_active:
# do it later, each source only has to be done once # do it later, each source only has to be done once
if source not in self._postponed_scene_change_sources: if source not in self._postponed_scene_change_sources:
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"):
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
node.getParent().removeChild(node) node.getParent().removeChild(node)
break
## 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):

View file

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

View file

@ -4,8 +4,6 @@
import gc import gc
from UM.Job import Job from UM.Job import Job
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application from UM.Application import Application
from UM.Mesh.MeshData import MeshData from UM.Mesh.MeshData import MeshData
from UM.Preferences import Preferences from UM.Preferences import Preferences
@ -17,6 +15,8 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder from cura import LayerDataBuilder
from cura import LayerDataDecorator from cura import LayerDataDecorator
@ -49,6 +49,7 @@ class ProcessSlicedLayersJob(Job):
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
self._abort_requested = False self._abort_requested = False
self._build_plate_number = None
## Aborts the processing of layers. ## Aborts the processing of layers.
# #
@ -59,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()

View file

@ -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
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) 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,32 +328,27 @@ 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()
start_gcode = settings["machine_start_gcode"]
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
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 = self._expandGcodeTokens(key, value, settings)
else:
setting_message.value = str(value).encode("utf-8") setting_message.value = str(value).encode("utf-8")
Job.yieldThread() Job.yieldThread()
@ -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)

View file

@ -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()

View file

@ -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"))

View file

@ -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) }
} }

View file

@ -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):

View file

@ -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

View file

@ -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]

View file

@ -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
for stack in stacks:
material_container = stack.material
if material_container == self._empty_container: if material_container == self._empty_container:
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")} machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
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) materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials: if materials:
self._global_container_stack.material = materials[0] 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,36 +238,38 @@ 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("brand", None)
search_criteria.pop("definition", None) search_criteria.pop("definition", None)
search_criteria["id"] = stack.getMetaDataEntry("preferred_material") search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
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 # 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("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic" 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:
# Generic material with new diameter is not found, search for preferred material # Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None) search_criteria.pop("color_name", None)
search_criteria.pop("material", None) search_criteria.pop("material", None)
search_criteria["id"] = stack.getMetaDataEntry("preferred_material") search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria) materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials: if not materials:
# Preferred material with new diameter is not found, search for any material # Preferred material with new diameter is not found, search for any material
@ -287,6 +279,6 @@ class MachineSettingsAction(MachineAction):
# Just use empty material as a final fallback # Just use empty material as a final fallback
materials = [self._empty_container] 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]

View file

@ -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
@ -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 "";
} }
@ -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 "";
} }
@ -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 "";
} }

View 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
}
}

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

View 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()
}

View 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"
}

View file

@ -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

View file

@ -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
{ {

View file

@ -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

View 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")

View 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{ }
}
}
}

View file

@ -0,0 +1,2 @@
# PostProcessingPlugin
A post processing plugin for Cura

View 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()

View 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()}

View 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"
}

View 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

View 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

View 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

View 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

View 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