Merge branch 'master' into feature_xmlmaterials_cura_settings

# Conflicts:
#	plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
fieldOfView 2017-12-18 12:47:57 +01:00
commit 266be74569
312 changed files with 46856 additions and 17766 deletions

4
.gitignore vendored
View file

@ -43,6 +43,10 @@ plugins/ProfileFlattener
plugins/cura-god-mode-plugin plugins/cura-god-mode-plugin
plugins/cura-big-flame-graph plugins/cura-big-flame-graph
plugins/cura-siemensnx-plugin plugins/cura-siemensnx-plugin
plugins/CuraVariSlicePlugin
plugins/CuraLiveScriptingPlugin
plugins/CuraPrintProfileCreator
plugins/OctoPrintPlugin
#Build stuff #Build stuff
CMakeCache.txt CMakeCache.txt

173
CHANGES
View file

@ -1,173 +0,0 @@
Cura 15.06 Beta
===============
This is the *Beta* version of Cura 15.06.
Cura 15.06 is a new release built from the ground up on a completely new
framework called Uranium. This framework has been designed to make it easier to
extend Cura with additional functionality as well as provide a cleaner UI.
Changes since 15.05.95
----------------------
* Fixed: Selection ghost remains visible after deleting an object
* Fixed: Window does not show up immediately after starting application on OSX
* Fixed: Added display of rotation angle during rotation
* Fixed: Object changes position while rotating/scaling
* Fixed: Loading improvements in the layer view
* Fixed: Added application icons
* Fixed: Improved feedback when loading models
* Fixed: Eject device on MacOSX now provides proper feedback
* Fixed: Make it possible to show retraction settings for UM2
* Fixed: Opening the machine preferences page will switch to the first available machine
* Fixed: Improved tool handle hit area size
* Fixed: Render lines with a thickness based on screen DPI
Changes since 15.05.94
----------------------
* Added Russian translations
* Fixed: Infill not displayed in layer view
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
* Fixed: Improved font rendering on Windows
* Fixed: Help > Show Documentation crashes Cura on Windows
* Fixed: "There is no disk in the drive" repeating messages on Windows
* Fixed: Retraction settings not visible for Ultimaker2
* Fixed: Display rotation angle when rotating an object
* Fixed: Time/Quality slider values are properly rounded
* Fixed: Improved clarity of buttons and text
* Fixed: No indication that anything is happening when loading a model
* Fixed: Eject device now works on Windows
Changes since 15.05.93
----------------------
* Fixed: No shortcuts for moving up/down layers in layer view.
* Fixed: Last view layers could not be scrolled through in layer view.
* Fixed: Files provided on command line would not actually show up on the build
platform.
* Fixed: Render a ghost of the selection in Layer view to make the actual object
position clear.
* Fixed: Showing a menu would clear the selection.
* Fixed: Size and scaling factor display for scale tool.
* Fixed: Missing background for additional tool controls.
* Fixed: Loading message times out when loading large files.
* Fixed: Show recent files in the file menu.
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
prevent issues with missing DLL's.
* Fixed: Collapsed/expanded state of setting categories not stored.
Changes since 15.05.91
----------------------
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
higher.
* Fixed: Need to deselect before selecting a different object.
* Fixed: Object can be moved on Z axis.
* Fixed: Error values should be considered invalid values and will not trigger a
slice.
* Fixed: Text fields used a locale-aware validator while the underlying code did
not.
* Fixed: Text fields will trigger a slice on text change, not only after focus
change/enter press.
* Fixed: Rotate Tool snaps to incorrect value.
* Fixed: Object Collision would only moved objects to the right.
* Fixed: Object Collision would move the selected object when it should not.
* Fixed: Camera panning now works correctly instead of doing nothing.
* Fixed: Camera would flip around center point at maximum rotation.
* Fixed: Build platform grid blocked view from below objects.
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
window
Changes since 15.05.90
----------------------
* Fixed: Additional UI elements for tools and views not loading.
* Fixed: Double click needed to change setting dialog page.
* Fixed: Context menu entries (reload, center object, etc.) not working.
* Fixed: "Open With" or passing files from command line not working.
* Fixed: "Reload All" would not reload files.
In addition, a lot of work has gone into getting a usable Mac OSX version.
New Features
------------
* Plugin based system
The Uranium framework provides us with a plugin-based system
that provides additional flexibility when extending Cura. Think
of new views, tools, file formats, etc. This is probably the
biggest new feature.
* Improved UI
The UI has received a complete overhaul.
* Time-Quality Slider
The 4 static quick print profiles have been replaced with
a slider that should make it easier to find the right spot
between print time and print quality.
* More Settings
The Advanced mode is now configurable and can show many
additional settings that were previously not available, while at
the same time not overwhelming new users with too many settings.
Custom set of visible settings can be created by the user.
* Support for high-DPI screens
The refreshed UI has been designed with high-DPI screens in
mind which should improve the experience of Cura on such
devices.
* Improved language support
(Not yet available for the Beta release.)
* Improved support structure generation
The new version of the CuraEngine now features improved
support generation algorithms and additional options for support
structure generation.
* Experimental Feature: Wire Printing
Wire Printing has been added as an experimental new feature. It
will print objects as a structure of lines. It can be enabled by
from Advanced Mode -> Fixes -> Wire Printing.
* Undo/Redo
It is now possible to undo and redo most scene operations, like
moving or rotating objects.
Features from earlier versions not (yet) in this release
--------------------------------------------------------
* The All-at-once/One-at-a-time toggle is not available.
We are working on an improved implementation of this mechanism
but it will not be available for this release.
* No dual extrusion features are available yet.
We are working on a completely new workflow for this but this
needs additional time.
* “Lay Flat” has been removed.
The existing implementation was unfortunately not salvageable.
We will be looking into an improved implementation for this
feature.
* "Split Object Into Parts" has been removed.
Due to the same reason as Lay Flat.
* Support for AMF and DAE file formats has been removed.
Both of these will be implemented as plugins in the future.
* Support for directly loading a GCode file is not yet available.
This will be implemented as a plugin in the future.
* Support for PNG, JPG and other image formats has been removed.
These can be supported by a plugin with an improved UI.
* Support for loading Minecraft levels has been removed.
This can be implemented as a plugin.
* Windows XP support has been dropped.
Microsoft is no longer supporting xp, so they no longer back
port certain features that we require.
* X-Ray view is missing.
Will be implemented as a (you might have guessed it) plugin.
* Fixes: Follow Mesh Surface
Has been removed from the engine, the same result can be
achieved using no infill or top/bottom layers.
Known Issues
------------
For an up to date list of all known issues, please see
https://github.com/Ultimaker/Cura/issues and
https://github.com/Ultimaker/Uranium/issues .
* Some OBJ files are rendered as black objects due to missing
normals.
* Disabling plugins does not work correctly yet.
* Unicorn occasionally still requires feeding. Do not feed it
after midnight.

View file

@ -39,7 +39,7 @@ find_package(PythonInterp 3.5.0 REQUIRED)
install(DIRECTORY resources install(DIRECTORY resources
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
install(DIRECTORY plugins install(DIRECTORY plugins
DESTINATION lib/cura) DESTINATION lib${LIB_SUFFIX}/cura)
if(NOT APPLE AND NOT WIN32) if(NOT APPLE AND NOT WIN32)
install(FILES cura_app.py install(FILES cura_app.py
DESTINATION ${CMAKE_INSTALL_BINDIR} DESTINATION ${CMAKE_INSTALL_BINDIR}
@ -47,16 +47,16 @@ if(NOT APPLE AND NOT WIN32)
RENAME cura) RENAME cura)
if(EXISTS /etc/debian_version) if(EXISTS /etc/debian_version)
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
else() else()
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
endif() endif()
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
@ -72,8 +72,8 @@ else()
DESTINATION ${CMAKE_INSTALL_BINDIR} DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
endif() endif()

View file

@ -3,7 +3,7 @@ Cura
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. 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. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
Logging Issues Logging Issues
@ -20,6 +20,8 @@ For crashes and similar issues, please attach the following information:
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
For additional support, you could also ask in the #cura channel on FreeNode IRC. For help with development, there is also the #cura-dev channel.
Dependencies Dependencies
------------ ------------

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.

View file

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

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
@ -876,15 +886,6 @@ class BuildVolume(SceneNode):
return result return result
## Private convenience function to get a setting from the adhesion
# extruder.
#
# \param setting_key The key of the setting to get.
# \param property The property to get from the setting.
# \return The property of the specified setting in the adhesion extruder.
def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"):
return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property)
## Private convenience function to get a setting from every extruder. ## Private convenience function to get a setting from every extruder.
# #
# For single extrusion machines, this gets the setting from the global # For single extrusion machines, this gets the setting from the global
@ -899,44 +900,6 @@ class BuildVolume(SceneNode):
all_values[i] = 0 all_values[i] = 0
return all_values return all_values
## Private convenience function to get a setting from the support infill
# extruder.
#
# \param setting_key The key of the setting to get.
# \param property The property to get from the setting.
# \return The property of the specified setting in the support infill
# extruder.
def _getSettingFromSupportInfillExtruder(self, setting_key, property = "value"):
return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property)
## Helper function to get a setting from an extruder specified in another
# setting.
#
# \param setting_key The key of the setting to get.
# \param extruder_setting_key The key of the setting that specifies from
# which extruder to get the setting, if there are multiple extruders.
# \param property The property to get from the setting.
# \return The property of the specified setting in the specified extruder.
def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"):
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
if not multi_extrusion:
stack = self._global_container_stack
else:
extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value")
if str(extruder_index) == "-1": # If extruder index is -1 use global instead
stack = self._global_container_stack
else:
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
value = stack.getProperty(setting_key, property)
setting_type = stack.getProperty(setting_key, "type")
if not value and (setting_type == "int" or setting_type == "float"):
return 0
return value
## Convenience function to calculate the disallowed radius around the edge. ## Convenience function to calculate the disallowed radius around the edge.
# #
# This disallowed radius is to allow for space around the models that is # This disallowed radius is to allow for space around the models that is
@ -945,6 +908,7 @@ class BuildVolume(SceneNode):
def _getEdgeDisallowedSize(self): def _getEdgeDisallowedSize(self):
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
container_stack = self._global_container_stack container_stack = self._global_container_stack
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
@ -953,32 +917,44 @@ class BuildVolume(SceneNode):
return 0.1 # Return a very small value, so we do draw disallowed area's near the edges. return 0.1 # Return a very small value, so we do draw disallowed area's near the edges.
adhesion_type = container_stack.getProperty("adhesion_type", "value") adhesion_type = container_stack.getProperty("adhesion_type", "value")
skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
if adhesion_type == "skirt": if adhesion_type == "skirt":
skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap") skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count") skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
bed_adhesion_size = skirt_distance + (self._getSettingFromAdhesionExtruder("skirt_brim_line_width") * skirt_line_count) * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor") / 100.0
if len(used_extruders) > 1: bed_adhesion_size = skirt_distance + (skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0 for extruder_stack in used_extruders:
#We don't create an additional line for the extruder we're printing the skirt with. bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
bed_adhesion_size -= self._getSettingFromAdhesionExtruder("skirt_brim_line_width", "value") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor", "value") / 100.0
# We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "brim": elif adhesion_type == "brim":
bed_adhesion_size = self._getSettingFromAdhesionExtruder("skirt_brim_line_width") * self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor") / 100.0 brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0 for extruder_stack in used_extruders:
#We don't create an additional line for the extruder we're printing the brim with. bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
bed_adhesion_size -= self._getSettingFromAdhesionExtruder("skirt_brim_line_width", "value") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor", "value") / 100.0
# We don't create an additional line for the extruder we're printing the brim with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "raft": elif adhesion_type == "raft":
bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin") bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
elif adhesion_type == "none": elif adhesion_type == "none":
bed_adhesion_size = 0 bed_adhesion_size = 0
else: else:
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?") raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
support_expansion = 0 support_expansion = 0
if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"): support_enabled = self._global_container_stack.getProperty("support_enable", "value")
support_expansion += self._getSettingFromSupportInfillExtruder("support_offset") support_offset = self._global_container_stack.getProperty("support_offset", "value")
if support_enabled and support_offset:
support_expansion += support_offset
farthest_shield_distance = 0 farthest_shield_distance = 0
if container_stack.getProperty("draft_shield_enabled", "value"): if container_stack.getProperty("draft_shield_enabled", "value"):
@ -1003,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

@ -302,24 +302,23 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._onChanged() self._onChanged()
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
def _getSettingProperty(self, setting_key, property = "value"): def _getSettingProperty(self, setting_key, prop = "value"):
per_mesh_stack = self._node.callDecoration("getStack") per_mesh_stack = self._node.callDecoration("getStack")
if per_mesh_stack: if per_mesh_stack:
return per_mesh_stack.getProperty(setting_key, property) return per_mesh_stack.getProperty(setting_key, prop)
multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1
if not multi_extrusion:
return self._global_stack.getProperty(setting_key, property)
extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder") extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder")
if extruder_index == "-1": #No limit_to_extruder. if extruder_index == "-1":
# No limit_to_extruder
extruder_stack_id = self._node.callDecoration("getActiveExtruder") extruder_stack_id = self._node.callDecoration("getActiveExtruder")
if not extruder_stack_id: #Decoration doesn't exist. if not extruder_stack_id:
# Decoration doesn't exist
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"] extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
return extruder_stack.getProperty(setting_key, property) return extruder_stack.getProperty(setting_key, prop)
else: #Limit_to_extruder is set. The global stack handles this then. else:
return self._global_stack.getProperty(setting_key, property) # Limit_to_extruder is set. The global stack handles this then
return self._global_stack.getProperty(setting_key, prop)
## Returns true if node is a descendant or the same as the root node. ## Returns true if node is a descendant or the same as the root node.
def __isDescendant(self, root, node): def __isDescendant(self, root, node):

View file

@ -59,7 +59,7 @@ class CrashHandler:
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)
@ -90,7 +90,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>
""")) """))
@ -108,11 +108,11 @@ class CrashHandler:
except: except:
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown") self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
crash_info = catalog.i18nc("@label Cura version", "<b>Cura version:</b> {version}<br/>").format(version = self.cura_version) crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>"
crash_info += catalog.i18nc("@label Platform", "<b>Platform:</b> {platform}<br/>").format(platform = platform.platform()) crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>"
crash_info += catalog.i18nc("@label Qt version", "<b>Qt version:</b> {qt}<br/>").format(qt = QT_VERSION_STR) crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
crash_info += catalog.i18nc("@label PyQt version", "<b>PyQt version:</b> {pyqt}<br/>").format(pyqt = PYQT_VERSION_STR) crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>"
crash_info += catalog.i18nc("@label OpenGL", "<b>OpenGL:</b> {opengl}<br/>").format(opengl = self._getOpenGLInfo()) crash_info += "<b>" + catalog.i18nc("@label OpenGL version", "OpenGL") + ":</b> " + str(self._getOpenGLInfo()) + "<br/>"
label.setText(crash_info) label.setText(crash_info)
layout.addWidget(label) layout.addWidget(label)
@ -126,19 +126,24 @@ class CrashHandler:
return group return group
def _getOpenGLInfo(self): def _getOpenGLInfo(self):
opengl_instance = OpenGL.getInstance()
if not opengl_instance:
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
return catalog.i18nc("@label", "not yet initialised<br/>")
info = "<ul>" info = "<ul>"
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = OpenGL.getInstance().getOpenGLVersion()) info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
info += catalog.i18nc("@label OpenGL vendor", "<li>OpenGL Vendor: {vendor}</li>").format(vendor = OpenGL.getInstance().getGPUVendorName()) info += catalog.i18nc("@label OpenGL vendor", "<li>OpenGL Vendor: {vendor}</li>").format(vendor = opengl_instance.getGPUVendorName())
info += catalog.i18nc("@label OpenGL renderer", "<li>OpenGL Renderer: {renderer}</li>").format(renderer = OpenGL.getInstance().getGPUType()) info += catalog.i18nc("@label OpenGL renderer", "<li>OpenGL Renderer: {renderer}</li>").format(renderer = opengl_instance.getGPUType())
info += "</ul>" info += "</ul>"
self.data["opengl"] = {"version": OpenGL.getInstance().getOpenGLVersion(), "vendor": OpenGL.getInstance().getGPUVendorName(), "type": OpenGL.getInstance().getGPUType()} self.data["opengl"] = {"version": opengl_instance.getOpenGLVersion(), "vendor": opengl_instance.getGPUVendorName(), "type": opengl_instance.getGPUType()}
return info return info
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()
@ -280,5 +285,7 @@ class CrashHandler:
Application.getInstance().callLater(self._show) Application.getInstance().callLater(self._show)
def _show(self): def _show(self):
self.dialog.exec_() # When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
os._exit(1) if self.dialog:
self.dialog.exec_()
os._exit(1)

View file

@ -1,6 +1,5 @@
# 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
@ -32,6 +31,7 @@ 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.Arrange import Arrange
from cura.ShapeArray import ShapeArray from cura.ShapeArray import ShapeArray
from cura.ConvexHullDecorator import ConvexHullDecorator from cura.ConvexHullDecorator import ConvexHullDecorator
@ -95,10 +95,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,9 +127,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
projectFileLoaded = pyqtSignal(str) # Emitted whenever a project file is loaded def __init__(self, **kwargs):
def __init__(self):
# 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)
@ -157,7 +157,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)
@ -173,16 +172,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,14 +201,19 @@ class CuraApplication(QtApplication):
self._machine_action_manager = MachineActionManager.MachineActionManager() self._machine_action_manager = MachineActionManager.MachineActionManager()
self._machine_manager = None # This is initialized on demand. self._machine_manager = None # This is initialized on demand.
self._extruder_manager = None
self._material_manager = None self._material_manager = None
self._setting_inheritance_manager = None self._setting_inheritance_manager = None
self._simple_mode_settings_manager = None self._simple_mode_settings_manager = 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"
@ -217,7 +223,7 @@ class CuraApplication(QtApplication):
"CuraEngineBackend", "CuraEngineBackend",
"UserAgreement", "UserAgreement",
"SolidView", "SolidView",
"LayerView", "SimulationView",
"STLReader", "STLReader",
"SelectionTool", "SelectionTool",
"CameraTool", "CameraTool",
@ -226,7 +232,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
@ -260,28 +268,32 @@ class CuraApplication(QtApplication):
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # We need them to simplify the switching between materials.
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")
empty_quality_container.addMetaDataEntry("supported", False) empty_quality_container.addMetaDataEntry("supported", False)
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()
@ -304,6 +316,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("cura/material_settings", "{}")
preferences.addPreference("view/invert_zoom", False) preferences.addPreference("view/invert_zoom", False)
preferences.addPreference("cura/sidebar_collapse", 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")
@ -383,7 +396,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
@ -391,7 +403,11 @@ 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)
## A reusable dialogbox ## A reusable dialogbox
# #
@ -425,7 +441,7 @@ class CuraApplication(QtApplication):
def discardOrKeepProfileChangesClosed(self, option): def discardOrKeepProfileChangesClosed(self, option):
if option == "discard": if option == "discard":
global_stack = self.getGlobalContainerStack() global_stack = self.getGlobalContainerStack()
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
extruder.getTop().clear() extruder.getTop().clear()
global_stack.getTop().clear() global_stack.getTop().clear()
@ -433,7 +449,7 @@ class CuraApplication(QtApplication):
# before slicing. To ensure that slicer uses right settings values # before slicing. To ensure that slicer uses right settings values
elif option == "keep": elif option == "keep":
global_stack = self.getGlobalContainerStack() global_stack = self.getGlobalContainerStack()
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
user_extruder_container = extruder.getTop() user_extruder_container = extruder.getTop()
if user_extruder_container: if user_extruder_container:
user_extruder_container.update() user_extruder_container.update()
@ -462,69 +478,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):
@ -558,11 +515,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):
@ -616,13 +572,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())
@ -654,21 +613,36 @@ class CuraApplication(QtApplication):
return False return False
return True return True
def preRun(self):
# Last check for unknown commandline arguments
parser = self.getCommandlineParser()
parser.add_argument("--help", "-h",
action='store_true',
default = False,
help = "Show this help message and exit."
)
parsed_args = vars(parser.parse_args()) # This won't allow unknown arguments
if parsed_args["help"]:
parser.print_help()
sys.exit(0)
def run(self): def run(self):
self.preRun()
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
self._setUpSingleInstanceServer() self._setUpSingleInstanceServer()
controller = self.getController() controller = self.getController()
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView") controller.setActiveView("SolidView")
controller.setCameraTool("CameraTool") controller.setCameraTool("CameraTool")
controller.setSelectionTool("SelectionTool") controller.setSelectionTool("SelectionTool")
t = controller.getTool("TranslateTool") t = controller.getTool("TranslateTool")
if t: if t:
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis]) t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
Selection.selectionChanged.connect(self.onSelectionChanged) Selection.selectionChanged.connect(self.onSelectionChanged)
@ -699,24 +673,22 @@ class CuraApplication(QtApplication):
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface...")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set. qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
ExtruderManager.getInstance()
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", qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
self.getSettingInheritanceManager) qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager",
self.getSimpleModeSettingsManager)
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) or self.getCommandLineOption("invisible", 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,11 +700,16 @@ 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
def getExtruderManager(self, *args):
if self._extruder_manager is None:
self._extruder_manager = ExtruderManager.createExtruderManager()
return self._extruder_manager
def getMaterialManager(self, *args): def getMaterialManager(self, *args):
if self._material_manager is None: if self._material_manager is None:
self._material_manager = MaterialManager.createMaterialManager() self._material_manager = MaterialManager.createMaterialManager()
@ -783,7 +760,6 @@ class CuraApplication(QtApplication):
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
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)
qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel") qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel")
@ -793,15 +769,12 @@ class CuraApplication(QtApplication):
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel") qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions") qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.getInstance())
for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles): for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
type_name = os.path.splitext(os.path.basename(path))[0] type_name = os.path.splitext(os.path.basename(path))[0]
if type_name in ("Cura", "Actions"): if type_name in ("Cura", "Actions"):
@ -1383,7 +1356,7 @@ class CuraApplication(QtApplication):
extension = os.path.splitext(filename)[1] extension = os.path.splitext(filename)[1]
if extension.lower() in self._non_sliceable_extensions: if extension.lower() in self._non_sliceable_extensions:
self.getController().setActiveView("LayerView") self.getController().setActiveView("SimulationView")
view = self.getController().getActiveView() view = self.getController().getActiveView()
view.resetLayerData() view.resetLayerData()
view.setLayer(9999999) view.setLayer(9999999)
@ -1413,7 +1386,8 @@ class CuraApplication(QtApplication):
# If a model is to small then it will not contain any points # If a model is to small then it will not contain any points
if offset_shape_arr is None and hull_shape_arr is None: if offset_shape_arr is None and hull_shape_arr is None:
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."), Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
title=self._i18n_catalog.i18nc("@info:title", "Warning")).show() title=self._i18n_catalog.i18nc("@info:title", "Warning")
).show()
return return
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher

View file

@ -47,12 +47,12 @@ class Layer:
return result return result
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices): def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
result_vertex_offset = vertex_offset result_vertex_offset = vertex_offset
result_index_offset = index_offset result_index_offset = index_offset
self._element_count = 0 self._element_count = 0
for polygon in self._polygons: for polygon in self._polygons:
polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, extruders, line_types, indices) polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
result_vertex_offset += polygon.lineMeshVertexCount() result_vertex_offset += polygon.lineMeshVertexCount()
result_index_offset += polygon.lineMeshElementCount() result_index_offset += polygon.lineMeshElementCount()
self._element_count += polygon.elementCount self._element_count += polygon.elementCount

View file

@ -20,11 +20,11 @@ class LayerDataBuilder(MeshBuilder):
if layer not in self._layers: if layer not in self._layers:
self._layers[layer] = Layer(layer) self._layers[layer] = Layer(layer)
def addPolygon(self, layer, polygon_type, data, line_width): def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate):
if layer not in self._layers: if layer not in self._layers:
self.addLayer(layer) self.addLayer(layer)
p = LayerPolygon(self, polygon_type, data, line_width) p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate)
self._layers[layer].polygons.append(p) self._layers[layer].polygons.append(p)
def getLayer(self, layer): def getLayer(self, layer):
@ -64,13 +64,14 @@ class LayerDataBuilder(MeshBuilder):
line_dimensions = numpy.empty((vertex_count, 2), numpy.float32) line_dimensions = numpy.empty((vertex_count, 2), numpy.float32)
colors = numpy.empty((vertex_count, 4), numpy.float32) colors = numpy.empty((vertex_count, 4), numpy.float32)
indices = numpy.empty((index_count, 2), numpy.int32) indices = numpy.empty((index_count, 2), numpy.int32)
feedrates = numpy.empty((vertex_count), numpy.float32)
extruders = numpy.empty((vertex_count), numpy.float32) extruders = numpy.empty((vertex_count), numpy.float32)
line_types = numpy.empty((vertex_count), numpy.float32) line_types = numpy.empty((vertex_count), numpy.float32)
vertex_offset = 0 vertex_offset = 0
index_offset = 0 index_offset = 0
for layer, data in sorted(self._layers.items()): for layer, data in sorted(self._layers.items()):
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices) ( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
self._element_counts[layer] = data.elementCount self._element_counts[layer] = data.elementCount
self.addVertices(vertices) self.addVertices(vertices)
@ -107,6 +108,11 @@ class LayerDataBuilder(MeshBuilder):
"value": line_types, "value": line_types,
"opengl_name": "a_line_type", "opengl_name": "a_line_type",
"opengl_type": "float" "opengl_type": "float"
},
"feedrates": {
"value": feedrates,
"opengl_name": "a_feedrate",
"opengl_type": "float"
} }
} }

View file

@ -28,7 +28,8 @@ class LayerPolygon:
# \param data new_points # \param data new_points
# \param line_widths array with line widths # \param line_widths array with line widths
# \param line_thicknesses: array with type as index and thickness as value # \param line_thicknesses: array with type as index and thickness as value
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses): # \param line_feedrates array with line feedrates
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates):
self._extruder = extruder self._extruder = extruder
self._types = line_types self._types = line_types
for i in range(len(self._types)): for i in range(len(self._types)):
@ -37,6 +38,7 @@ class LayerPolygon:
self._data = data self._data = data
self._line_widths = line_widths self._line_widths = line_widths
self._line_thicknesses = line_thicknesses self._line_thicknesses = line_thicknesses
self._line_feedrates = line_feedrates
self._vertex_begin = 0 self._vertex_begin = 0
self._vertex_end = 0 self._vertex_end = 0
@ -84,10 +86,11 @@ class LayerPolygon:
# \param vertices : vertex numpy array to be filled # \param vertices : vertex numpy array to be filled
# \param colors : vertex numpy array to be filled # \param colors : vertex numpy array to be filled
# \param line_dimensions : vertex numpy array to be filled # \param line_dimensions : vertex numpy array to be filled
# \param feedrates : vertex numpy array to be filled
# \param extruders : vertex numpy array to be filled # \param extruders : vertex numpy array to be filled
# \param line_types : vertex numpy array to be filled # \param line_types : vertex numpy array to be filled
# \param indices : index numpy array to be filled # \param indices : index numpy array to be filled
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices): def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
self.buildCache() self.buildCache()
@ -109,10 +112,13 @@ class LayerPolygon:
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away. # Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()] colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
# Create an array with line widths for each vertex. # Create an array with line widths and thicknesses for each vertex.
line_dimensions[self._vertex_begin:self._vertex_end, 0] = numpy.tile(self._line_widths, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] line_dimensions[self._vertex_begin:self._vertex_end, 0] = numpy.tile(self._line_widths, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
line_dimensions[self._vertex_begin:self._vertex_end, 1] = numpy.tile(self._line_thicknesses, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] line_dimensions[self._vertex_begin:self._vertex_end, 1] = numpy.tile(self._line_thicknesses, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
# Create an array with feedrates for each line
feedrates[self._vertex_begin:self._vertex_end] = numpy.tile(self._line_feedrates, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
extruders[self._vertex_begin:self._vertex_end] = self._extruder extruders[self._vertex_begin:self._vertex_end] = self._extruder
# Convert type per vertex to type per line # Convert type per vertex to type per line
@ -166,6 +172,14 @@ class LayerPolygon:
@property @property
def lineWidths(self): def lineWidths(self):
return self._line_widths return self._line_widths
@property
def lineThicknesses(self):
return self._line_thicknesses
@property
def lineFeedrates(self):
return self._line_feedrates
@property @property
def jumpMask(self): def jumpMask(self):

View file

@ -1,12 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V. # Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginObject import PluginObject from UM.PluginObject import PluginObject
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.Application import Application from UM.Application import Application
import os import os
@ -26,9 +24,6 @@ class MachineAction(QObject, PluginObject):
self._key = key self._key = key
self._label = label self._label = label
self._qml_url = "" self._qml_url = ""
self._component = None
self._context = None
self._view = None self._view = None
self._finished = False self._finished = False
@ -52,7 +47,6 @@ class MachineAction(QObject, PluginObject):
# /sa _reset # /sa _reset
@pyqtSlot() @pyqtSlot()
def reset(self): def reset(self):
self._component = None
self._finished = False self._finished = False
self._reset() self._reset()
@ -73,18 +67,11 @@ class MachineAction(QObject, PluginObject):
## Protected helper to create a view object based on provided QML. ## Protected helper to create a view object based on provided QML.
def _createViewFromQML(self): def _createViewFromQML(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
self._component = QQmlComponent(Application.getInstance()._engine, path) self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
self._context.setContextProperty("manager", self)
self._view = self._component.create(self._context)
if self._view is None:
Logger.log("c", "QQmlComponent status %s", self._component.status())
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
def displayItem(self): def displayItem(self):
if not self._component: if not self._view:
self._createViewFromQML() self._createViewFromQML()
return self._view
return self._view

View file

@ -16,6 +16,7 @@ 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")
@ -67,11 +68,10 @@ class PrintInformation(QObject):
self._base_name = "" self._base_name = ""
self._abbr_machine = "" self._abbr_machine = ""
self._job_name = "" self._job_name = ""
self._project_name = ""
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().projectFileLoaded.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
@ -239,7 +239,7 @@ class PrintInformation(QObject):
pass pass
active_material_id = Application.getInstance().getMachineManager().activeMaterialId active_material_id = Application.getInstance().getMachineManager().activeMaterialId
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id) active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id = active_material_id)
if active_material_containers: if active_material_containers:
self._active_material_container = active_material_containers[0] self._active_material_container = active_material_containers[0]
@ -253,11 +253,6 @@ class PrintInformation(QObject):
self._job_name = name self._job_name = name
self.jobNameChanged.emit() self.jobNameChanged.emit()
@pyqtSlot(str)
def setProjectName(self, name):
self._project_name = name
self.setJobName(name)
jobNameChanged = pyqtSignal() jobNameChanged = pyqtSignal()
@pyqtProperty(str, notify = jobNameChanged) @pyqtProperty(str, notify = jobNameChanged)
@ -265,11 +260,6 @@ class PrintInformation(QObject):
return self._job_name return self._job_name
def _updateJobName(self): def _updateJobName(self):
# if the project name is set, we use the project name as the job name, so the job name should not get updated
# if a model file is loaded after that.
if self._project_name != "":
return
if self._base_name == "": if self._base_name == "":
self._job_name = "" self._job_name = ""
self.jobNameChanged.emit() self.jobNameChanged.emit()
@ -295,7 +285,11 @@ class PrintInformation(QObject):
return self._base_name return self._base_name
@pyqtSlot(str) @pyqtSlot(str)
def setBaseName(self, base_name): def setProjectName(self, name):
self.setBaseName(name, is_project_file = True)
@pyqtSlot(str)
def setBaseName(self, base_name, is_project_file = False):
# Ensure that we don't use entire path but only filename # Ensure that we don't use entire path but only filename
name = os.path.basename(base_name) name = os.path.basename(base_name)
@ -303,11 +297,17 @@ class PrintInformation(QObject):
# extension. This cuts the extension off if necessary. # extension. This cuts the extension off if necessary.
name = os.path.splitext(name)[0] name = os.path.splitext(name)[0]
# if this is a profile file, always update the job name
# name is "" when I first had some meshes and afterwards I deleted them so the naming should start again # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
if name == "" or (self._base_name == "" and self._base_name != name): is_empty = name == ""
if is_project_file or (is_empty or (self._base_name == "" and self._base_name != name)):
# remove ".curaproject" suffix from (imported) the file name
if name.endswith(".curaproject"):
name = name[:name.rfind(".curaproject")]
self._base_name = name self._base_name = name
self._updateJobName() self._updateJobName()
## Created an acronymn-like abbreviated machine name from the currently active machine name ## Created an acronymn-like abbreviated machine name from the currently active machine name
# Called each time the global stack is switched # Called each time the global stack is switched
def _setAbbreviatedMachineName(self): def _setAbbreviatedMachineName(self):
@ -317,15 +317,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:

View file

@ -3,19 +3,15 @@
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, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking. from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry 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.PluginRegistry import PluginRegistry
from UM.Application import Application from UM.Application import Application
import os
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.
@ -63,11 +59,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._camera_active = False 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
@ -155,54 +149,31 @@ class PrinterOutputDevice(QObject, OutputDevice):
# 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_component: if not self._monitor_item:
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_component: if not self._control_item:
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
path = QUrl.fromLocalFile(self._control_view_qml_path) self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
"OutputDevice": self
# Because of garbage collection we need to keep this referenced by python. })
self._control_component = QQmlComponent(Application.getInstance()._engine, path)
# Check if the context was already requested before (Printer output device might have multiple items in the future)
if self._qml_context is None:
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("OutputDevice", self)
self._control_item = self._control_component.create(self._qml_context)
if self._control_item is None:
Logger.log("e", "QQmlComponent status %s", self._control_component.status())
Logger.log("e", "QQmlComponent error string %s", self._control_component.errorString())
def _createMonitorViewFromQML(self): def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path: if not self._monitor_view_qml_path:
return return
path = QUrl.fromLocalFile(self._monitor_view_qml_path)
# Because of garbage collection we need to keep this referenced by python. self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
self._monitor_component = QQmlComponent(Application.getInstance()._engine, path) "OutputDevice": self
})
# Check if the context was already requested before (Printer output device might have multiple items in the future)
if self._qml_context is None:
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("OutputDevice", self)
self._monitor_item = self._monitor_component.create(self._qml_context)
if self._monitor_item is None:
Logger.log("e", "QQmlComponent status %s", self._monitor_component.status())
Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString())
@pyqtProperty(str, notify=printerTypeChanged) @pyqtProperty(str, notify=printerTypeChanged)
def printerType(self): def printerType(self):
@ -469,9 +440,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
result.append(i18n_catalog.i18nc("@item:material", "No material loaded")) result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
continue continue
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers: if containers:
result.append(containers[0].getName()) result.append(containers[0]["name"])
else: else:
result.append(i18n_catalog.i18nc("@item:material", "Unknown material")) result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
return result return result
@ -491,9 +462,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
result.append("#00000000") #No material. result.append("#00000000") #No material.
continue continue
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers: if containers:
result.append(containers[0].getMetaDataEntry("color_code")) result.append(containers[0]["color_code"])
else: else:
result.append("#00000000") #Unknown material. result.append("#00000000") #Unknown material.
return result return result

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,9 +130,9 @@ 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)
@ -183,20 +182,11 @@ class QualityManager:
materials = [] materials = []
# TODO: fix this for stack in extruder_stacks:
if extruder_stacks: if stack.getId() == active_stack_id and machine_manager.newMaterial:
# Multi-extruder machine detected
for stack in extruder_stacks:
if stack.getId() == active_stack_id and machine_manager.newMaterial:
materials.append(machine_manager.newMaterial)
else:
materials.append(stack.material)
else:
# Machine with one extruder
if global_container_stack.getId() == active_stack_id and machine_manager.newMaterial:
materials.append(machine_manager.newMaterial) materials.append(machine_manager.newMaterial)
else: else:
materials.append(global_container_stack.material) materials.append(stack.material)
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials) quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
@ -211,22 +201,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 []
@ -234,29 +236,25 @@ class QualityManager:
def _getFilteredContainers(self, **kwargs): def _getFilteredContainers(self, **kwargs):
return self._getFilteredContainersForStack(None, None, **kwargs) return self._getFilteredContainersForStack(None, None, **kwargs)
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs): def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs):
# Fill in any default values. # Fill in any default values.
if machine_definition is None: if machine_definition is None:
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom() machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition") quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition_id is not None: if quality_definition_id is not None:
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0] machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition_id)[0]
# for convenience if not material_metadata:
if material_containers is None:
material_containers = []
if not material_containers:
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks() active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks: if active_stacks:
material_containers = [stack.material for stack in active_stacks] material_metadata = [stack.material.getMetaData() for stack in active_stacks]
criteria = kwargs criteria = kwargs
filter_by_material = False filter_by_material = False
machine_definition = self.getParentMachineDefinition(machine_definition) machine_definition = self.getParentMachineDefinition(machine_definition)
criteria["definition"] = machine_definition.getId() criteria["definition"] = machine_definition.getId()
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainers(**criteria) found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
whole_machine_definition = self.getWholeMachineDefinition(machine_definition) whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
if whole_machine_definition.getMetaDataEntry("has_machine_quality"): if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId()) definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
@ -270,12 +268,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 = []
@ -301,13 +299,13 @@ class QualityManager:
# We have a normal (whole) machine defintion # We have a normal (whole) machine defintion
quality_definition = machine_definition.getMetaDataEntry("quality_definition") quality_definition = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition is not None: if quality_definition is not None:
parent_machine_definition = container_registry.findDefinitionContainers(id=quality_definition)[0] parent_machine_definition = container_registry.findDefinitionContainers(id = quality_definition)[0]
return self.getParentMachineDefinition(parent_machine_definition) return self.getParentMachineDefinition(parent_machine_definition)
else: else:
return machine_definition return machine_definition
else: else:
# This looks like an extruder. Find the rest of the machine. # This looks like an extruder. Find the rest of the machine.
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0] whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
parent_machine = self.getParentMachineDefinition(whole_machine) parent_machine = self.getParentMachineDefinition(whole_machine)
if whole_machine is parent_machine: if whole_machine is parent_machine:
# This extruder already belongs to a 'parent' machine def. # This extruder already belongs to a 'parent' machine def.
@ -316,7 +314,7 @@ class QualityManager:
# Look up the corresponding extruder definition in the parent machine definition. # Look up the corresponding extruder definition in the parent machine definition.
extruder_position = machine_definition.getMetaDataEntry("position") extruder_position = machine_definition.getMetaDataEntry("position")
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position] parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
return container_registry.findDefinitionContainers(id=parent_extruder_id)[0] return container_registry.findDefinitionContainers(id = parent_extruder_id)[0]
## Get the whole/global machine definition from an extruder definition. ## Get the whole/global machine definition from an extruder definition.
# #
@ -330,5 +328,5 @@ class QualityManager:
return machine_definition return machine_definition
else: else:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0] whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
return whole_machine return whole_machine

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]
@ -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)
@ -682,27 +690,29 @@ class ContainerManager(QObject):
machine_definition = global_stack.getBottom() machine_definition = global_stack.getBottom()
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,57 @@ 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 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 +829,12 @@ class ContainerManager(QObject):
return "" return ""
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value"))) approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter) containers = self._container_registry.findInstanceContainersMetadata(id = "generic_pla*", approximate_diameter = approximate_diameter)
if not containers: if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.") Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
return "" return ""
base_file = containers[0].getMetaDataEntry("base_file") base_file = containers[0].get("base_file")
containers = self._container_registry.findInstanceContainers(id = base_file) containers = self._container_registry.findInstanceContainers(id = base_file)
if not containers: if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.") Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
@ -835,14 +875,14 @@ class ContainerManager(QObject):
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False)) has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
if has_machine_materials or has_variant_materials: if has_machine_materials or has_variant_materials:
if has_variants: if has_variants:
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId) materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
else: else:
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId()) materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
if materials: if materials:
return materials[0].getId() return materials[0]["id"]
Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file) Logger.log("w", "Unable to find a suitable container based on %s for the current machine.", base_file)
return "" # do not activate a new material if a container can not be found return "" # do not activate a new material if a container can not be found
return base_file return base_file
@ -853,25 +893,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 +997,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 +1036,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

@ -4,6 +4,7 @@
import os import os
import os.path import os.path
import re import re
import configparser
from typing import Optional from typing import Optional
@ -17,8 +18,9 @@ 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
from UM.Platform import Platform from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry #For getting the possible profile writers to write with. from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
from UM.Util import parseBool from UM.Util import parseBool
from UM.Resources import Resources
from . import ExtruderStack from . import ExtruderStack
from . import GlobalStack from . import GlobalStack
@ -34,6 +36,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.
@ -47,7 +54,7 @@ class CuraContainerRegistry(ContainerRegistry):
container = self._convertContainerStack(container) container = self._convertContainerStack(container)
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
#Check against setting version of the definition. # Check against setting version of the definition.
required_setting_version = CuraApplication.SettingVersion required_setting_version = CuraApplication.SettingVersion
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
if required_setting_version != actual_setting_version: if required_setting_version != actual_setting_version:
@ -86,8 +93,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
# #
@ -116,7 +123,7 @@ class CuraContainerRegistry(ContainerRegistry):
found_containers = [] found_containers = []
extruder_positions = [] extruder_positions = []
for instance_id in instance_ids: for instance_id in instance_ids:
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id) containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
if containers: if containers:
found_containers.append(containers[0]) found_containers.append(containers[0])
@ -126,9 +133,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, ...)
@ -202,53 +209,57 @@ 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) if len(profile_or_list) == 1:
if result is not None: # If there is only 1 stack file it means we're loading a legacy (pre-3.1) .curaprofile.
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)} # In that case we find the per-extruder settings and put those in a new quality_changes container
# so that it is compatible with the new stack setup.
profile = profile_or_list[0]
extruder_stack_quality_changes_container = ContainerManager.getInstance().duplicateContainerInstance(profile)
extruder_stack_quality_changes_container.addMetaDataEntry("extruder", "fdmextruder")
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName())} for quality_changes_setting_key in extruder_stack_quality_changes_container.getAllKeys():
else: settable_per_extruder = extruder_stack_quality_changes_container.getProperty(quality_changes_setting_key, "settable_per_extruder")
profile_index = -1 if settable_per_extruder:
global_profile = None profile.removeInstance(quality_changes_setting_key, postpone_emit = True)
for profile in profile_or_list:
if profile_index >= 0:
if len(machine_extruders) > profile_index:
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
# Ensure the extruder profiles get non-conflicting names
# NB: these are not user-facing
if "extruder" in profile.getMetaData():
profile.setMetaDataEntry("extruder", extruder_id)
else:
profile.addMetaDataEntry("extruder", extruder_id)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
elif profile_index == 0:
# Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
profile._id = self.uniqueName("temporary_profile")
self.addContainer(profile)
ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
self.removeContainer(profile.getId())
break
else:
# The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
break
else: else:
global_profile = profile extruder_stack_quality_changes_container.removeInstance(quality_changes_setting_key, postpone_emit = True)
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
result = self._configureProfile(profile, profile_id, new_name) # We add the new container to the profile list so things like extruder positions are taken care of
if result is not None: # in the next code segment.
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, result)} profile_or_list.append(extruder_stack_quality_changes_container)
profile_index += 1 # Import all profiles
for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0:
# This is assumed to be the global profile
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} elif len(machine_extruders) > profile_index:
# 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(" ", "_")
result = self._configureProfile(profile, profile_id, new_name)
if result is not None:
return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!",
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
file_name, result)}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
# If it hasn't returned by now, none of the plugins loaded the profile successfully. # If it hasn't returned by now, none of the plugins loaded the profile successfully.
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
@ -256,7 +267,8 @@ class CuraContainerRegistry(ContainerRegistry):
@override(ContainerRegistry) @override(ContainerRegistry)
def load(self): def load(self):
super().load() super().load()
self._fixupExtruders() self._registerSingleExtrusionMachinesExtruderStacks()
self._connectUpgradedExtruderStacksToMachines()
## Update an imported profile to match the current machine configuration. ## Update an imported profile to match the current machine configuration.
# #
@ -266,7 +278,6 @@ 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"))
@ -284,7 +295,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
@ -294,15 +305,18 @@ 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()
del quality_type_criteria["definition"] del quality_type_criteria["definition"]
materials = None
# materials = None
if "material" in quality_type_criteria: if "material" in quality_type_criteria:
materials = ContainerRegistry.getInstance().findInstanceContainers(id = quality_type_criteria["material"]) # materials = ContainerRegistry.getInstance().findInstanceContainers(id = quality_type_criteria["material"])
del quality_type_criteria["material"] del quality_type_criteria["material"]
# Do not filter quality containers here with materials because we are trying to import a profile, so it should # Do not filter quality containers here with materials because we are trying to import a profile, so it should
# NOT be restricted by the active materials on the current machine. # NOT be restricted by the active materials on the current machine.
materials = None materials = None
@ -338,7 +352,7 @@ class CuraContainerRegistry(ContainerRegistry):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom()) definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
definition = self.findDefinitionContainers(id=definition_id)[0] definition = self.findDefinitionContainers(id = definition_id)[0]
if definition: if definition:
return definition return definition
@ -360,8 +374,8 @@ class CuraContainerRegistry(ContainerRegistry):
return global_container_stack.material.getId() return global_container_stack.material.getId()
return "" return ""
## Returns true if the current machien requires its own quality profiles ## Returns true if the current machine requires its own quality profiles
# \return true if the current machien requires its own quality profiles # \return true if the current machine requires its own quality profiles
def _machineHasOwnQualities(self): def _machineHasOwnQualities(self):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
@ -394,19 +408,155 @@ class CuraContainerRegistry(ContainerRegistry):
return new_stack return new_stack
def _registerSingleExtrusionMachinesExtruderStacks(self):
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
for machine in machines:
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
if not extruder_stacks:
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
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId())
if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
new_extruder_id = extruder_id
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
if not extruder_definitions:
Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id)
return
extruder_definition = extruder_definitions[0]
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id)
extruder_stack = ExtruderStack.ExtruderStack(unique_name)
extruder_stack.setName(extruder_definition.getName())
extruder_stack.setDefinition(extruder_definition)
extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
# create empty user changes container otherwise
user_container = InstanceContainer(extruder_stack.id + "_user")
user_container.addMetaDataEntry("type", "user")
user_container.addMetaDataEntry("machine", extruder_stack.getId())
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(machine.definition.getId())
if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
# container to the extruder stack.
for user_setting_key in machine.userChanges.getAllKeys():
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
if settable_per_extruder:
user_container.addInstance(machine.userChanges.getInstance(user_setting_key))
machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
self.addContainer(user_container)
extruder_stack.setUserChanges(user_container)
variant_id = "default"
if machine.variant.getId() not in ("empty", "empty_variant"):
variant_id = machine.variant.getId()
else:
variant_id = "empty_variant"
extruder_stack.setVariantById(variant_id)
material_id = "default"
if machine.material.getId() not in ("empty", "empty_material"):
material_id = machine.material.getId()
else:
material_id = "empty_material"
extruder_stack.setMaterialById(material_id)
quality_id = "default"
if machine.quality.getId() not in ("empty", "empty_quality"):
quality_id = machine.quality.getId()
else:
quality_id = "empty_quality"
extruder_stack.setQualityById(quality_id)
if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
if extruder_quality_changes_container:
extruder_quality_changes_container = extruder_quality_changes_container[0]
quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id)
else:
# Some extruder quality_changes containers can be created at runtime as files in the qualities
# folder. Those files won't be loaded in the registry immediately. So we also need to search
# the folder to see if the quality_changes exists.
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName())
if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id)
if not extruder_quality_changes_container:
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine.qualityChanges.getName(), extruder_stack.getId())
else:
extruder_stack.setQualityChangesById("empty_quality_changes")
self.addContainer(extruder_stack)
# Set next stack at the end
extruder_stack.setNextStack(machine)
return extruder_stack
def _findQualityChangesContainerInCuraFolder(self, name):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
instance_container = None
for item in os.listdir(quality_changes_dir):
file_path = os.path.join(quality_changes_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser()
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("general", "name"):
continue
if parser["general"]["name"] == name:
# load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
instance_container = InstanceContainer(container_id)
with open(file_path, "r") as f:
serialized = f.read()
instance_container.deserialize(serialized, file_path)
self.addContainer(instance_container)
break
return instance_container
# Fix the extruders that were upgraded to ExtruderStack instances during addContainer. # Fix the extruders that were upgraded to ExtruderStack instances during addContainer.
# The stacks are now responsible for setting the next stack on deserialize. However, # The stacks are now responsible for setting the next stack on deserialize. However,
# due to problems with loading order, some stacks may not have the proper next stack # due to problems with loading order, some stacks may not have the proper next stack
# 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 _fixupExtruders(self): def _connectUpgradedExtruderStacksToMachines(self):
extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack) extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
for extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
if extruder_stack.getNextStack(): if extruder_stack.getNextStack():
# Has the right next stack, so ignore it. # Has the right next stack, so ignore it.
continue continue
machines = ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack.getMetaDataEntry("machine", "")) machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", ""))
if machines: if machines:
extruder_stack.setNextStack(machines[0]) extruder_stack.setNextStack(machines[0])
else: else:

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
@ -41,9 +41,20 @@ class CuraContainerStack(ContainerStack):
def __init__(self, container_id: str, *args, **kwargs): def __init__(self, container_id: str, *args, **kwargs):
super().__init__(container_id, *args, **kwargs) super().__init__(container_id, *args, **kwargs)
self._empty_instance_container = ContainerRegistry.getInstance().getEmptyInstanceContainer() self._container_registry = ContainerRegistry.getInstance()
self._empty_instance_container = self._container_registry.getEmptyInstanceContainer()
self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0]
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))]
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
self._containers[_ContainerIndexes.Quality] = self._empty_quality
self._containers[_ContainerIndexes.Material] = self._empty_material
self._containers[_ContainerIndexes.Variant] = self._empty_variant
self.containersChanged.connect(self._onContainersChanged) self.containersChanged.connect(self._onContainersChanged)
@ -110,7 +121,7 @@ class CuraContainerStack(ContainerStack):
# #
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setQualityById(self, new_quality_id: str) -> None: def setQualityById(self, new_quality_id: str) -> None:
quality = self._empty_instance_container quality = self._empty_quality
if new_quality_id == "default": if new_quality_id == "default":
new_quality = self.findDefaultQuality() new_quality = self.findDefaultQuality()
if new_quality: if new_quality:
@ -148,7 +159,7 @@ class CuraContainerStack(ContainerStack):
# #
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setMaterialById(self, new_material_id: str) -> None: def setMaterialById(self, new_material_id: str) -> None:
material = self._empty_instance_container material = self._empty_material
if new_material_id == "default": if new_material_id == "default":
new_material = self.findDefaultMaterial() new_material = self.findDefaultMaterial()
if new_material: if new_material:
@ -186,7 +197,7 @@ class CuraContainerStack(ContainerStack):
# #
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setVariantById(self, new_variant_id: str) -> None: def setVariantById(self, new_variant_id: str) -> None:
variant = self._empty_instance_container variant = self._empty_variant
if new_variant_id == "default": if new_variant_id == "default":
new_variant = self.findDefaultVariant() new_variant = self.findDefaultVariant()
if new_variant: if new_variant:
@ -235,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.
@ -348,8 +359,8 @@ class CuraContainerStack(ContainerStack):
# #
# \throws InvalidContainerStackError Raised when no definition can be found for the stack. # \throws InvalidContainerStackError Raised when no definition can be found for the stack.
@override(ContainerStack) @override(ContainerStack)
def deserialize(self, contents: str) -> None: def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
super().deserialize(contents) super().deserialize(contents, file_name)
new_containers = self._containers.copy() new_containers = self._containers.copy()
while len(new_containers) < len(_ContainerIndexes.IndexTypeMap): while len(new_containers) < len(_ContainerIndexes.IndexTypeMap):
@ -366,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
@ -396,7 +407,9 @@ class CuraContainerStack(ContainerStack):
# \note This method assumes the stack has a valid machine definition. # \note This method assumes the stack has a valid machine definition.
def findDefaultVariant(self) -> Optional[ContainerInterface]: def findDefaultVariant(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition() definition = self._getMachineDefinition()
if not definition.getMetaDataEntry("has_variants"): # has_variants can be overridden in other containers and stacks.
# In the case of UM2, it is overridden in the GlobalStack
if not self.getMetaDataEntry("has_variants"):
# If the machine does not use variants, we should never set a variant. # If the machine does not use variants, we should never set a variant.
return None return None
@ -454,7 +467,7 @@ class CuraContainerStack(ContainerStack):
else: else:
search_criteria["definition"] = "fdmprinter" search_criteria["definition"] = "fdmprinter"
if self.material != self._empty_instance_container: if self.material != self._empty_material:
search_criteria["name"] = self.material.name search_criteria["name"] = self.material.name
else: else:
preferred_material = definition.getMetaDataEntry("preferred_material") preferred_material = definition.getMetaDataEntry("preferred_material")
@ -474,11 +487,17 @@ class CuraContainerStack(ContainerStack):
search_criteria.pop("name", None) search_criteria.pop("name", None)
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria) materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if materials: if not materials:
return materials[0] Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None
for material in materials:
# Prefer a read-only material
if ContainerRegistry.getInstance().isReadOnly(material.getId()):
return material
return materials[0]
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None
## Find the quality that should be used as "default" quality. ## Find the quality that should be used as "default" quality.
# #
@ -489,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"}
@ -501,7 +520,7 @@ class CuraContainerStack(ContainerStack):
else: else:
search_criteria["definition"] = "fdmprinter" search_criteria["definition"] = "fdmprinter"
if self.quality != self._empty_instance_container: if self.quality != self._empty_quality:
search_criteria["name"] = self.quality.name search_criteria["name"] = self.quality.name
else: else:
preferred_quality = definition.getMetaDataEntry("preferred_quality") preferred_quality = definition.getMetaDataEntry("preferred_quality")
@ -533,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")
@ -544,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(
@ -47,21 +47,40 @@ class CuraStackBuilder:
new_global_stack.setName(generated_name) new_global_stack.setName(generated_name)
for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id): extruder_definition = registry.findDefinitionContainers(machine = machine_definition.getId())
position = extruder_definition.getMetaDataEntry("position", None)
if not position:
Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.id)
new_extruder_id = registry.uniqueName(extruder_definition.id) if not extruder_definition:
# create extruder stack for single extrusion machines that have no separate extruder definition files
extruder_definition = registry.findDefinitionContainers(id = "fdmextruder")[0]
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id)
new_extruder = cls.createExtruderStack( new_extruder = cls.createExtruderStack(
new_extruder_id, new_extruder_id,
definition = extruder_definition, definition = extruder_definition,
machine_definition = machine_definition, machine_definition_id = machine_definition.getId(),
quality = "default", quality = "default",
material = "default", material = "default",
variant = "default", variant = "default",
next_stack = new_global_stack next_stack = new_global_stack
) )
new_global_stack.addExtruder(new_extruder)
else:
# create extruder stack for each found extruder definition
for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id):
position = extruder_definition.getMetaDataEntry("position", None)
if not position:
Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.id)
new_extruder_id = registry.uniqueName(extruder_definition.id)
new_extruder = cls.createExtruderStack(
new_extruder_id,
definition = extruder_definition,
machine_definition_id = machine_definition.getId(),
quality = "default",
material = "default",
variant = "default",
next_stack = new_global_stack
)
new_global_stack.addExtruder(new_extruder)
return new_global_stack return new_global_stack
@ -69,17 +88,20 @@ 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)
stack.addMetaDataEntry("position", definition.getMetaDataEntry("position")) stack.addMetaDataEntry("position", definition.getMetaDataEntry("position"))
if "next_stack" in kwargs: #Add stacks before containers are added, since they may trigger a setting update.
if "next_stack" in kwargs:
# Add stacks before containers are added, since they may trigger a setting update.
stack.setNextStack(kwargs["next_stack"]) stack.setNextStack(kwargs["next_stack"])
user_container = InstanceContainer(new_stack_id + "_user") user_container = InstanceContainer(new_stack_id + "_user")
@ -87,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)
@ -127,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)
@ -136,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)
@ -172,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

@ -1,21 +1,18 @@
# 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.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant #For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Application import Application #To get the global container stack to find the current machine. from UM.Application import Application # To get the global container stack to find the current machine.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Decorators import deprecated
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Optional, List, TYPE_CHECKING, Union from typing import Optional, List, TYPE_CHECKING, Union
@ -28,6 +25,20 @@ if TYPE_CHECKING:
# #
# This keeps a list of extruder stacks for each machine. # This keeps a list of extruder stacks for each machine.
class ExtruderManager(QObject): class ExtruderManager(QObject):
## Registers listeners and such to listen to changes to the extruders.
def __init__(self, parent = None):
super().__init__(parent)
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = []
self._global_container_stack_definition_id = None
self._addCurrentMachineExtruders()
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes. ## Signal to notify other components when the list of extruders for a machine definition changes.
extrudersChanged = pyqtSignal(QVariant) extrudersChanged = pyqtSignal(QVariant)
@ -38,18 +49,6 @@ class ExtruderManager(QObject):
## Notify when the user switches the currently active extruder. ## Notify when the user switches the currently active extruder.
activeExtruderChanged = pyqtSignal() activeExtruderChanged = pyqtSignal()
## Registers listeners and such to listen to changes to the extruders.
def __init__(self, parent = None):
super().__init__(parent)
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = []
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
self._global_container_stack_definition_id = None
self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Gets the unique identifier of the currently active extruder stack. ## Gets the unique identifier of the currently active extruder stack.
# #
# The currently active extruder stack is the stack that is currently being # The currently active extruder stack is the stack that is currently being
@ -59,10 +58,10 @@ class ExtruderManager(QObject):
@pyqtProperty(str, notify = activeExtruderChanged) @pyqtProperty(str, notify = activeExtruderChanged)
def activeExtruderStackId(self) -> Optional[str]: def activeExtruderStackId(self) -> Optional[str]:
if not Application.getInstance().getGlobalContainerStack(): if not Application.getInstance().getGlobalContainerStack():
return None # No active machine, so no active extruder. return None # No active machine, so no active extruder.
try: try:
return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId() return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None return None
## Return extruder count according to extruder trains. ## Return extruder count according to extruder trains.
@ -76,23 +75,23 @@ class ExtruderManager(QObject):
return 0 return 0
## Gets a dict with the extruder stack ids with the extruder number as the key. ## Gets a dict with the extruder stack ids with the extruder number as the key.
# The key "-1" indicates the global stack id.
#
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self): def extruderIds(self):
extruder_stack_ids = {} extruder_stack_ids = {}
global_stack_id = Application.getInstance().getGlobalContainerStack().getId() global_stack_id = Application.getInstance().getGlobalContainerStack().getId()
extruder_stack_ids["-1"] = global_stack_id
if global_stack_id in self._extruder_trains: if global_stack_id in self._extruder_trains:
for position in self._extruder_trains[global_stack_id]: for position in self._extruder_trains[global_stack_id]:
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId() extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
return extruder_stack_ids return extruder_stack_ids
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getQualityChangesIdByExtruderStackId(self, id: str) -> str: def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]: for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position] extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
if extruder.getId() == id: if extruder.getId() == extruder_stack_id:
return extruder.qualityChanges.getId() return extruder.qualityChanges.getId()
## The instance of the singleton pattern. ## The instance of the singleton pattern.
@ -100,6 +99,10 @@ class ExtruderManager(QObject):
# It's None if the extruder manager hasn't been created yet. # It's None if the extruder manager hasn't been created yet.
__instance = None __instance = None
@staticmethod
def createExtruderManager():
return ExtruderManager().getInstance()
## Gets an instance of the extruder manager, or creates one if no instance ## Gets an instance of the extruder manager, or creates one if no instance
# exists yet. # exists yet.
# #
@ -185,6 +188,7 @@ class ExtruderManager(QObject):
if global_container_stack.getId() in self._extruder_trains: if global_container_stack.getId() in self._extruder_trains:
if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]: if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)] return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)]
return None return None
## Get an extruder stack by index ## Get an extruder stack by index
@ -203,40 +207,6 @@ class ExtruderManager(QObject):
result.append(self.getExtruderStack(i)) result.append(self.getExtruderStack(i))
return result return result
## Adds all extruders of a specific machine definition to the extruder
# manager.
#
# \param machine_definition The machine definition to add the extruders for.
# \param machine_id The machine_id to add the extruders for.
@deprecated("Use CuraStackBuilder", "2.6")
def addMachineExtruders(self, machine_definition: DefinitionContainerInterface, machine_id: str) -> None:
changed = False
machine_definition_id = machine_definition.getId()
if machine_id not in self._extruder_trains:
self._extruder_trains[machine_id] = { }
changed = True
container_registry = ContainerRegistry.getInstance()
if container_registry:
# Add the extruder trains that don't exist yet.
for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition_id):
position = extruder_definition.getMetaDataEntry("position", None)
if not position:
Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
if not container_registry.findContainerStacks(machine = machine_id, position = position): # Doesn't exist yet.
self.createExtruderTrain(extruder_definition, machine_definition, position, machine_id)
changed = True
# Gets the extruder trains that we just created as well as any that still existed.
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_id)
for extruder_train in extruder_trains:
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
# regardless of what the next stack is, we have to set it again, because of signal routing.
extruder_train.setNextStack(Application.getInstance().getGlobalContainerStack())
changed = True
if changed:
self.extrudersChanged.emit(machine_id)
def registerExtruder(self, extruder_train, machine_id): def registerExtruder(self, extruder_train, machine_id):
changed = False changed = False
@ -256,138 +226,6 @@ class ExtruderManager(QObject):
if changed: if changed:
self.extrudersChanged.emit(machine_id) self.extrudersChanged.emit(machine_id)
## Creates a container stack for an extruder train.
#
# The container stack has an extruder definition at the bottom, which is
# linked to a machine definition. Then it has a variant profile, a material
# profile, a quality profile and a user profile, in that order.
#
# The resulting container stack is added to the registry.
#
# \param extruder_definition The extruder to create the extruder train for.
# \param machine_definition The machine that the extruder train belongs to.
# \param position The position of this extruder train in the extruder slots of the machine.
# \param machine_id The id of the "global" stack this extruder is linked to.
@deprecated("Use CuraStackBuilder::createExtruderStack", "2.6")
def createExtruderTrain(self, extruder_definition: DefinitionContainerInterface, machine_definition: DefinitionContainerInterface,
position, machine_id: str) -> None:
# Cache some things.
container_registry = ContainerRegistry.getInstance()
machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_definition)
# Create a container stack for this extruder.
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
container_stack = ContainerStack(extruder_stack_id)
container_stack.setName(extruder_definition.getName()) # Take over the display name to display the stack with.
container_stack.addMetaDataEntry("type", "extruder_train")
container_stack.addMetaDataEntry("machine", machine_id)
container_stack.addMetaDataEntry("position", position)
container_stack.addContainer(extruder_definition)
# Find the variant to use for this extruder.
variant = container_registry.findInstanceContainers(id = "empty_variant")[0]
if machine_definition.getMetaDataEntry("has_variants"):
# First add any variant. Later, overwrite with preference if the preference is valid.
variants = container_registry.findInstanceContainers(definition = machine_definition_id, type = "variant")
if len(variants) >= 1:
variant = variants[0]
preferred_variant_id = machine_definition.getMetaDataEntry("preferred_variant")
if preferred_variant_id:
preferred_variants = container_registry.findInstanceContainers(id = preferred_variant_id, definition = machine_definition_id, type = "variant")
if len(preferred_variants) >= 1:
variant = preferred_variants[0]
else:
Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
# And leave it at the default variant.
container_stack.addContainer(variant)
# Find a material to use for this variant.
material = container_registry.findInstanceContainers(id = "empty_material")[0]
if machine_definition.getMetaDataEntry("has_materials"):
# First add any material. Later, overwrite with preference if the preference is valid.
machine_has_variant_materials = machine_definition.getMetaDataEntry("has_variant_materials", default = False)
if machine_has_variant_materials or machine_has_variant_materials == "True":
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id, variant = variant.getId())
else:
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id)
if len(materials) >= 1:
material = materials[0]
preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
if preferred_material_id:
global_stack = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
if global_stack:
approximate_material_diameter = str(round(global_stack[0].getProperty("material_diameter", "value")))
else:
approximate_material_diameter = str(round(machine_definition.getProperty("material_diameter", "value")))
search_criteria = { "type": "material", "id": preferred_material_id, "approximate_diameter": approximate_material_diameter}
if machine_definition.getMetaDataEntry("has_machine_materials"):
search_criteria["definition"] = machine_definition_id
if machine_definition.getMetaDataEntry("has_variants") and variant:
search_criteria["variant"] = variant.id
else:
search_criteria["definition"] = "fdmprinter"
preferred_materials = container_registry.findInstanceContainers(**search_criteria)
if len(preferred_materials) >= 1:
# In some cases we get multiple materials. In that case, prefer materials that are marked as read only.
read_only_preferred_materials = [preferred_material for preferred_material in preferred_materials if preferred_material.isReadOnly()]
if len(read_only_preferred_materials) >= 1:
material = read_only_preferred_materials[0]
else:
material = preferred_materials[0]
else:
Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
# And leave it at the default material.
container_stack.addContainer(material)
# Find a quality to use for this extruder.
quality = container_registry.getEmptyInstanceContainer()
search_criteria = { "type": "quality" }
if machine_definition.getMetaDataEntry("has_machine_quality"):
search_criteria["definition"] = machine_definition_id
if machine_definition.getMetaDataEntry("has_materials") and material:
search_criteria["material"] = material.id
else:
search_criteria["definition"] = "fdmprinter"
preferred_quality = machine_definition.getMetaDataEntry("preferred_quality")
if preferred_quality:
search_criteria["id"] = preferred_quality
containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if not containers and preferred_quality:
Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
search_criteria.pop("id", None)
containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if containers:
quality = containers[0]
container_stack.addContainer(quality)
empty_quality_changes = container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
container_stack.addContainer(empty_quality_changes)
user_profile = container_registry.findInstanceContainers(type = "user", extruder = extruder_stack_id)
if user_profile: # There was already a user profile, loaded from settings.
user_profile = user_profile[0]
else:
user_profile = InstanceContainer(extruder_stack_id + "_current_settings") # Add an empty user profile.
user_profile.addMetaDataEntry("type", "user")
user_profile.addMetaDataEntry("extruder", extruder_stack_id)
from cura.CuraApplication import CuraApplication
user_profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_profile.setDefinition(machine_definition)
container_registry.addContainer(user_profile)
container_stack.addContainer(user_profile)
# regardless of what the next stack is, we have to set it again, because of signal routing.
container_stack.setNextStack(Application.getInstance().getGlobalContainerStack())
container_registry.addContainer(container_stack)
def getAllExtruderValues(self, setting_key): def getAllExtruderValues(self, setting_key):
return self.getAllExtruderSettings(setting_key, "value") return self.getAllExtruderSettings(setting_key, "value")
@ -396,16 +234,12 @@ class ExtruderManager(QObject):
# \param setting_key \type{str} The setting to get the property of. # \param setting_key \type{str} The setting to get the property of.
# \param property \type{str} The property to get. # \param property \type{str} The property to get.
# \return \type{List} the list of results # \return \type{List} the list of results
def getAllExtruderSettings(self, setting_key, property): def getAllExtruderSettings(self, setting_key: str, prop: str):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
return [global_container_stack.getProperty(setting_key, property)]
result = [] result = []
for index in self.extruderIds: for index in self.extruderIds:
extruder_stack_id = self.extruderIds[str(index)] extruder_stack_id = self.extruderIds[str(index)]
stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
result.append(stack.getProperty(setting_key, property)) result.append(extruder_stack.getProperty(setting_key, prop))
return result return result
## Gets the extruder stacks that are actually being used at the moment. ## Gets the extruder stacks that are actually being used at the moment.
@ -422,20 +256,25 @@ class ExtruderManager(QObject):
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
if global_stack.getProperty("machine_extruder_count", "value") <= 1: #For single extrusion.
return [global_stack]
used_extruder_stack_ids = set() used_extruder_stack_ids = set()
#Get the extruders of all meshes in the scene. # Get the extruders of all meshes in the scene
support_enabled = False support_enabled = False
support_bottom_enabled = False support_bottom_enabled = False
support_roof_enabled = False support_roof_enabled = False
scene_root = Application.getInstance().getController().getScene().getRoot() scene_root = Application.getInstance().getController().getScene().getRoot()
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()] #Only use the nodes that will be printed.
# If no extruders are registered in the extruder manager yet, return an empty array
if len(self.extruderIds) == 0:
return []
# 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()]
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: #No per-object settings for this node. if not extruder_stack_id:
# No per-object settings for this node
extruder_stack_id = self.extruderIds["0"] extruder_stack_id = self.extruderIds["0"]
used_extruder_stack_ids.add(extruder_stack_id) used_extruder_stack_ids.add(extruder_stack_id)
@ -471,9 +310,10 @@ class ExtruderManager(QObject):
if support_roof_enabled: if support_roof_enabled:
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))]) used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))])
#The platform adhesion extruder. Not used if using none. # The platform adhesion extruder. Not used if using none.
if global_stack.getProperty("adhesion_type", "value") != "none": if global_stack.getProperty("adhesion_type", "value") != "none":
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))]) used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
try: try:
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids] return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
except IndexError: # One or more of the extruders was not found. except IndexError: # One or more of the extruders was not found.
@ -516,18 +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:
# In case the printer is using one extruder, shouldn't exist active extruder stacks
if machine_extruder_count == 1:
return result
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:
@ -536,24 +374,39 @@ class ExtruderManager(QObject):
self._global_container_stack_definition_id = global_container_stack.getBottom().getId() self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
self.globalContainerStackDefinitionChanged.emit() self.globalContainerStackDefinitionChanged.emit()
# If the global container changed, the number of extruders could be changed and so the active_extruder_index is updated # If the global container changed, the machine changed and might have extruders that were not registered yet
extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") self._addCurrentMachineExtruders()
if extruder_count > 1:
if self._active_extruder_index == -1:
self.setActiveExtruderIndex(0)
else:
if self._active_extruder_index != -1:
self.setActiveExtruderIndex(-1)
self.activeExtruderChanged.emit()
self.resetSelectedObjectExtruders() self.resetSelectedObjectExtruders()
## Adds the extruders of the currently active machine. ## Adds the extruders of the currently active machine.
def _addCurrentMachineExtruders(self) -> None: def _addCurrentMachineExtruders(self) -> None:
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack and global_stack.getBottom(): extruders_changed = False
self.addMachineExtruders(global_stack.getBottom(), global_stack.getId())
if global_stack:
container_registry = ContainerRegistry.getInstance()
global_stack_id = global_stack.getId()
# Gets the extruder trains that we just created as well as any that still existed.
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
# Make sure the extruder trains for the new machine can be placed in the set of sets
if global_stack_id not in self._extruder_trains:
self._extruder_trains[global_stack_id] = {}
extruders_changed = True
# Register the extruder trains by position
for extruder_train in extruder_trains:
self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train
# regardless of what the next stack is, we have to set it again, because of signal routing. ???
extruder_train.setNextStack(global_stack)
extruders_changed = True
if extruders_changed:
self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0)
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.
# #
@ -632,7 +485,7 @@ class ExtruderManager(QObject):
# #
# This is exposed to qml for display purposes # This is exposed to qml for display purposes
# #
# \param key The key of the setting to retieve values for. # \param key The key of the setting to retrieve values for.
# #
# \return String representing the extruder values # \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
@ -656,7 +509,8 @@ class ExtruderManager(QObject):
value = extruder.getRawProperty(key, "value") value = extruder.getRawProperty(key, "value")
if isinstance(value, SettingFunction): if isinstance(value, SettingFunction):
value = value(extruder) value = value(extruder)
else: #Just a value from global. else:
# Just a value from global.
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value") value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
return value return value

View file

@ -92,8 +92,8 @@ class ExtruderStack(CuraContainerStack):
return self.getNextStack()._getMachineDefinition() return self.getNextStack()._getMachineDefinition()
@override(CuraContainerStack) @override(CuraContainerStack)
def deserialize(self, contents: str) -> None: def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
super().deserialize(contents) super().deserialize(contents, file_name)
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", "")) stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
if stacks: if stacks:
self.setNextStack(stacks[0]) self.setNextStack(stacks[0])
@ -115,6 +115,11 @@ class ExtruderStack(CuraContainerStack):
if has_global_dependencies: if has_global_dependencies:
self.getNextStack().propertiesChanged.emit(key, properties) self.getNextStack().propertiesChanged.emit(key, properties)
def findDefaultVariant(self):
# The default variant is defined in the machine stack and/or definition, so use the machine stack to find
# the default variant.
return self.getNextStack().findDefaultVariant()
extruder_stack_mime = MimeType( extruder_stack_mime = MimeType(
name = "application/x-cura-extruderstack", name = "application/x-cura-extruderstack",

View file

@ -8,8 +8,8 @@ from UM.i18n import i18nCatalog
import UM.Qt.ListModel import UM.Qt.ListModel
from UM.Application import Application from UM.Application import Application
import UM.FlameProfiler import UM.FlameProfiler
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack #To listen to changes on the extruders. from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -68,28 +68,16 @@ 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]
self._add_optional_extruder = False self._add_optional_extruder = False
#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
self._extrudersChanged() #Also calls _updateExtruders. Application.getInstance().getContainerRegistry().containerMetaDataChanged.connect(self._onExtruderStackContainersChanged) # When meta data from a material container changes we must update
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()
@ -128,21 +116,26 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
def _extrudersChanged(self, machine_id = None): def _extrudersChanged(self, machine_id = None):
if machine_id is not None: if machine_id is not None:
if Application.getInstance().getGlobalContainerStack() is None: if Application.getInstance().getGlobalContainerStack() is None:
return #No machine, don't need to update the current machine's extruders. # No machine, don't need to update the current machine's extruders
return
if machine_id != Application.getInstance().getGlobalContainerStack().getId(): if machine_id != Application.getInstance().getGlobalContainerStack().getId():
return #Not the current machine. # Not the current machine
#Unlink from old extruders. return
# Unlink from old extruders
for extruder in self._active_machine_extruders: for extruder in self._active_machine_extruders:
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged) extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
#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)
self._updateExtruders() #Since the new extruders may have different properties, update our own model. self._updateExtruders() # Since the new extruders may have different properties, update our own model.
def _onExtruderStackContainersChanged(self, container): def _onExtruderStackContainersChanged(self, container):
# Update when there is an empty container or material change # Update when there is an empty container or material change
@ -150,7 +143,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name # The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
self._updateExtruders() self._updateExtruders()
modelChanged = pyqtSignal() modelChanged = pyqtSignal()
def _updateExtruders(self): def _updateExtruders(self):
@ -161,59 +153,52 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# This should be called whenever the list of extruders changes. # This should be called whenever the list of extruders changes.
@UM.FlameProfiler.profile @UM.FlameProfiler.profile
def __updateExtruders(self): def __updateExtruders(self):
changed = False extruders_changed = False
if self.rowCount() != 0: if self.rowCount() != 0:
changed = True extruders_changed = True
items = [] items = []
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
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)
changed = True
# 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")
manager = ExtruderManager.getInstance()
for extruder in manager.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)
except ValueError: #Not a proper int. except ValueError:
# Not a proper int.
position = -1 position = -1
if position >= machine_extruder_count: if position >= machine_extruder_count:
continue continue
extruder_name = extruder.getName()
material = extruder.material
variant = extruder.variant
default_color = self.defaultColors[position] if position >= 0 and position < len(self.defaultColors) else self.defaultColors[0] default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
item = { #Construct an item with only the relevant information.
# construct an item with only the relevant information
item = {
"id": extruder.getId(), "id": extruder.getId(),
"name": extruder_name, "name": extruder.getName(),
"color": color, "color": color,
"index": position, "index": position,
"definition": extruder.getBottom().getId(), "definition": extruder.getBottom().getId(),
"material": material.getName() if material else "", "material": extruder.material.getName() if extruder.material else "",
"variant": variant.getName() if variant else "", "variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
} }
items.append(item)
changed = True
if changed: items.append(item)
extruders_changed = True
if extruders_changed:
# sort by extruder index
items.sort(key = lambda i: i["index"]) items.sort(key = lambda i: i["index"])
# We need optional extruder to be last, so add it after we do sorting. # We need optional extruder to be last, so add it after we do sorting.
# This way we can simply intrepret the -1 of the index as the last item (which it now always is) # This way we can simply interpret the -1 of the index as the last item (which it now always is)
if self._add_optional_extruder: if self._add_optional_extruder:
item = { item = {
"id": "", "id": "",
@ -223,5 +208,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"definition": "" "definition": ""
} }
items.append(item) items.append(item)
self.setItems(items) self.setItems(items)
self.modelChanged.emit() self.modelChanged.emit()

View file

@ -23,9 +23,9 @@ class GlobalStack(CuraContainerStack):
def __init__(self, container_id: str, *args, **kwargs): def __init__(self, container_id: str, *args, **kwargs):
super().__init__(container_id, *args, **kwargs) super().__init__(container_id, *args, **kwargs)
self.addMetaDataEntry("type", "machine") # For backward compatibility self.addMetaDataEntry("type", "machine") # For backward compatibility
self._extruders = {} self._extruders = {} # type: Dict[str, "ExtruderStack"]
# This property is used to track which settings we are calculating the "resolve" for # This property is used to track which settings we are calculating the "resolve" for
# and if so, to bypass the resolve to prevent an infinite recursion that would occur # and if so, to bypass the resolve to prevent an infinite recursion that would occur
@ -43,15 +43,11 @@ class GlobalStack(CuraContainerStack):
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 2 return 2
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]: @classmethod
configuration_type = None def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
try: configuration_type = super().getConfigurationTypeFromSerialized(serialized)
parser = self._readAndValidateSerialized(serialized) if configuration_type == "machine":
configuration_type = parser["metadata"].get("type") return "machine_stack"
if configuration_type == "machine":
configuration_type = "machine_stack"
except Exception as e:
Logger.log("e", "Could not get configuration type: %s", e)
return configuration_type return configuration_type
## Add an extruder to the list of extruders of this stack. ## Add an extruder to the list of extruders of this stack.
@ -61,20 +57,13 @@ class GlobalStack(CuraContainerStack):
# \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we
# already have the maximum number of extruders. # already have the maximum number of extruders.
def addExtruder(self, extruder: ContainerStack) -> None: def addExtruder(self, extruder: ContainerStack) -> None:
extruder_count = self.getProperty("machine_extruder_count", "value")
if extruder_count <= 1:
Logger.log("i", "Not adding extruder[%s] to [%s] because it is a single-extrusion machine.",
extruder.id, self.id)
return
position = extruder.getMetaDataEntry("position") position = extruder.getMetaDataEntry("position")
if position is None: if position is None:
Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id) Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
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,12 +107,11 @@ 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)
if self._global_container_stack and self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: # Make sure _active_container_stack is properly initiated
# Make sure _active_container_stack is properly initiated ExtruderManager.getInstance().setActiveExtruderIndex(0)
ExtruderManager.getInstance().setActiveExtruderIndex(0)
self._auto_materials_changed = {} self._auto_materials_changed = {}
self._auto_hotends_changed = {} self._auto_hotends_changed = {}
@ -118,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()
@ -162,13 +168,13 @@ class MachineManager(QObject):
@pyqtProperty(int, constant=True) @pyqtProperty(int, constant=True)
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, index: Union[str, int], hotend_id: str) -> None:
if not self._global_container_stack: if not self._global_container_stack:
return return
containers = ContainerRegistry.getInstance().findInstanceContainers(type="variant", definition=self._global_container_stack.getBottom().getId(), name=hotend_id) containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id)
if containers: # New material ID is known if containers: # New material ID is known
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
machine_id = self.activeMachineId machine_id = self.activeMachineId
@ -180,10 +186,10 @@ class MachineManager(QObject):
break break
if matching_extruder and matching_extruder.variant.getName() != hotend_id: 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. # 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._auto_hotends_changed[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else: else:
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id)) Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.definition.getId(), hotend_id))
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str): def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
if not self._global_container_stack: if not self._global_container_stack:
@ -193,7 +199,7 @@ class MachineManager(QObject):
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False): if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
definition_id = self.activeQualityDefinitionId definition_id = self.activeQualityDefinitionId
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id) containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", definition = definition_id, GUID = material_id)
if containers: # New material ID is known if containers: # New material ID is known
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId)) extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
matching_extruder = None matching_extruder = None
@ -204,15 +210,15 @@ class MachineManager(QObject):
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id: if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback. # Save the material that needs to be changed. Multiple changes will be handled by the callback.
if self._global_container_stack.getBottom().getMetaDataEntry("has_variants") and matching_extruder.variant: if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.getBottom(), matching_extruder.variant) variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_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[str(index)] = 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[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else: else:
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id)) Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
@ -258,13 +264,13 @@ class MachineManager(QObject):
if old_index is not None: if old_index is not None:
extruder_manager.setActiveExtruderIndex(old_index) extruder_manager.setActiveExtruderIndex(old_index)
self._auto_hotends_changed = {} #Processed all of them now. self._auto_hotends_changed = {} # Processed all of them now.
def _onGlobalContainerChanged(self): def _onGlobalContainerChanged(self):
if self._global_container_stack: if self._global_container_stack:
try: try:
self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged) self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
except TypeError: #pyQtSignal gives a TypeError when disconnecting from something that was already disconnected. except TypeError: # pyQtSignal gives a TypeError when disconnecting from something that was already disconnected.
pass pass
try: try:
self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged) self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
@ -274,52 +280,38 @@ class MachineManager(QObject):
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
except TypeError: except TypeError:
pass pass
material = self._global_container_stack.material
material.nameChanged.disconnect(self._onMaterialNameChanged)
quality = self._global_container_stack.quality for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
quality.nameChanged.disconnect(self._onQualityNameChanged) extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
extruder_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
extruder_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
# update the local global container stack reference
self._global_container_stack = Application.getInstance().getGlobalContainerStack() self._global_container_stack = Application.getInstance().getGlobalContainerStack()
self.globalContainerChanged.emit() self.globalContainerChanged.emit()
# after switching the global stack we reconnect all the signals and set the variant and material references
if self._global_container_stack: if self._global_container_stack:
Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId()) Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId())
self._global_container_stack.nameChanged.connect(self._onMachineNameChanged) self._global_container_stack.nameChanged.connect(self._onMachineNameChanged)
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged) self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: # set the global variant to empty as we now use the extruder stack at all times - CURA-4482
# For multi-extrusion machines, we do not want variant or material profiles in the stack, global_variant = self._global_container_stack.variant
# because these are extruder specific and may cause wrong values to be used for extruders if global_variant != self._empty_variant_container:
# that did not specify a value in the extruder. self._global_container_stack.setVariant(self._empty_variant_container)
global_variant = self._global_container_stack.variant
if global_variant != self._empty_variant_container:
self._global_container_stack.setVariant(self._empty_variant_container)
global_material = self._global_container_stack.material # set the global material to empty as we now use the extruder stack at all times - CURA-4482
if global_material != self._empty_material_container: global_material = self._global_container_stack.material
self._global_container_stack.setMaterial(self._empty_material_container) if global_material != self._empty_material_container:
self._global_container_stack.setMaterial(self._empty_material_container)
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks(): #Listen for changes on all extruder stacks. # Listen for changes on all extruder stacks
extruder_stack.propertyChanged.connect(self._onPropertyChanged) for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.containersChanged.connect(self._onInstanceContainersChanged) extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onInstanceContainersChanged)
else:
material = self._global_container_stack.material
material.nameChanged.connect(self._onMaterialNameChanged)
quality = self._global_container_stack.quality
quality.nameChanged.connect(self._onQualityNameChanged)
self._active_container_stack = self._global_container_stack
self.activeStackChanged.emit()
self._error_check_timer.start() self._error_check_timer.start()
@ -336,8 +328,6 @@ class MachineManager(QObject):
old_active_container_stack = self._active_container_stack old_active_container_stack = self._active_container_stack
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack() self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not self._active_container_stack:
self._active_container_stack = self._global_container_stack
self._error_check_timer.start() self._error_check_timer.start()
@ -346,14 +336,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):
@ -369,11 +369,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:
@ -384,15 +386,6 @@ class MachineManager(QObject):
else: else:
Logger.log("w", "Failed creating a new machine!") Logger.log("w", "Failed creating a new machine!")
## Create a name that is not empty and unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param current_name \type{} Current name of the container, which may be an acceptable option
# \param new_name \type{string} Base name, which may not be unique
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
# \return \type{string} Name that is unique for the specified type and name/id
def _createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
return ContainerRegistry.getInstance().createUniqueName(container_type, current_name, new_name, fallback_name)
def _checkStacksHaveErrors(self) -> bool: def _checkStacksHaveErrors(self) -> bool:
if self._global_container_stack is None: #No active machine. if self._global_container_stack is None: #No active machine.
return False return False
@ -524,6 +517,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:
@ -536,6 +530,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:
@ -556,6 +551,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:
@ -574,10 +570,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
@ -646,12 +640,17 @@ class MachineManager(QObject):
@pyqtProperty(str, notify=activeQualityChanged) @pyqtProperty(str, notify=activeQualityChanged)
def activeQualityId(self) -> str: def activeQualityId(self) -> str:
if self._active_container_stack: if self._active_container_stack:
quality = self._active_container_stack.qualityChanges
if quality and not isinstance(quality, type(self._empty_quality_changes_container)):
return quality.getId()
quality = self._active_container_stack.quality quality = self._active_container_stack.quality
if quality: if isinstance(quality, type(self._empty_quality_container)):
return quality.getId() return ""
quality_changes = self._active_container_stack.qualityChanges
if quality and quality_changes:
if isinstance(quality_changes, type(self._empty_quality_changes_container)):
# It's a built-in profile
return quality.getId()
else:
# Custom profile
return quality_changes.getId()
return "" return ""
@pyqtProperty(str, notify=activeQualityChanged) @pyqtProperty(str, notify=activeQualityChanged)
@ -716,31 +715,26 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = activeQualityChanged) @pyqtProperty(str, notify = activeQualityChanged)
def activeQualityChangesId(self) -> str: def activeQualityChangesId(self) -> str:
if self._active_container_stack: if self._active_container_stack:
changes = self._active_container_stack.qualityChanges quality_changes = self._active_container_stack.qualityChanges
if changes and changes.getId() != "empty": if quality_changes and not isinstance(quality_changes, type(self._empty_quality_changes_container)):
return changes.getId() return quality_changes.getId()
return "" return ""
## 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)
def copyValueToExtruders(self, key: str): def copyValueToExtruders(self, key: str):
if not self._active_container_stack or self._global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
return
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())]
stacks.append(self._global_container_stack)
for extruder_stack in stacks: # check in which stack the value has to be replaced
for extruder_stack in extruder_stacks:
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
extruder_stack.getTop().setProperty(key, "value", new_value) extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
## Set the active material by switching out a container ## Set the active material by switching out a container
# Depending on from/to material+current variant, a quality profile is chosen and set. # Depending on from/to material+current variant, a quality profile is chosen and set.
@ -756,6 +750,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.")
@ -786,29 +783,45 @@ class MachineManager(QObject):
quality_type = old_quality_changes.getMetaDataEntry("quality_type") quality_type = old_quality_changes.getMetaDataEntry("quality_type")
new_quality_id = old_quality_changes.getId() new_quality_id = old_quality_changes.getId()
# See if the requested quality type is available in the new situation. global_stack = Application.getInstance().getGlobalContainerStack()
machine_definition = self._active_container_stack.getBottom() if global_stack:
quality_manager = QualityManager.getInstance() quality_manager = QualityManager.getInstance()
candidate_quality = None
if quality_type:
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
quality_manager.getWholeMachineDefinition(machine_definition),
[material_container])
if not candidate_quality or isinstance(candidate_quality, type(self._empty_quality_changes_container)): candidate_quality = None
Logger.log("d", "Attempting to find fallback quality") if quality_type:
# Fall back to a quality (which must be compatible with all other extruders) candidate_quality = quality_manager.findQualityByQualityType(quality_type,
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders( quality_manager.getWholeMachineDefinition(global_stack.definition),
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks()) [material_container.getMetaData()])
if new_qualities:
new_quality_id = new_qualities[0].getId() # Just pick the first available one if not candidate_quality or candidate_quality.getId() == self._empty_quality_changes_container:
Logger.log("d", "Attempting to find fallback quality")
# Fall back to a quality (which must be compatible with all other extruders)
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders(
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks())
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:
Logger.log("w", "No quality profile found that matches the current machine and extruders.")
else: else:
Logger.log("w", "No quality profile found that matches the current machine and extruders.") if not old_quality_changes:
else: new_quality_id = candidate_quality.getId()
if not old_quality_changes:
new_quality_id = candidate_quality.getId()
self.setActiveQuality(new_quality_id) self.setActiveQuality(new_quality_id)
@pyqtSlot(str) @pyqtSlot(str)
def setActiveVariant(self, variant_id: str): def setActiveVariant(self, variant_id: str):
@ -826,7 +839,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.")
@ -838,15 +851,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":
@ -854,7 +867,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
@ -892,17 +905,12 @@ class MachineManager(QObject):
"quality_changes": stack_quality_changes "quality_changes": stack_quality_changes
}) })
has_user_interaction = False # show the keep/discard dialog after the containers have been switched. Otherwise, the default values on
# the dialog will be the those before the switching.
self._executeDelayedActiveContainerStackChanges()
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
# Show the keep/discard user settings dialog Application.getInstance().discardOrKeepProfileChanges()
has_user_interaction = Application.getInstance().discardOrKeepProfileChanges()
# If there is no interaction with the user (it means the dialog showing "keep" or "discard" was not shown)
# because either there are not user changes or because the used already decided to always keep or discard,
# then the quality instance container is replaced, in which case, the activeQualityChanged signal is emitted.
if not has_user_interaction:
self._executeDelayedActiveContainerStackChanges()
## Used to update material and variant in the active container stack with a delay. ## Used to update material and variant in the active container stack with a delay.
# This delay prevents the stack from triggering a lot of signals (eventually resulting in slicing) # This delay prevents the stack from triggering a lot of signals (eventually resulting in slicing)
@ -947,35 +955,46 @@ class MachineManager(QObject):
global_container_stack = self._global_container_stack global_container_stack = self._global_container_stack
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()
if extruder_stacks:
stacks = extruder_stacks
else:
stacks = [global_container_stack]
for stack in stacks: # find qualities for extruders
material = stack.material for extruder_stack in extruder_stacks:
material_metadata = extruder_stack.material.getMetaData()
# TODO: fix this # TODO: fix this
if self._new_material_container and 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_metadata])
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
if not quality: if not quality:
# No quality profile is found for this quality type. # No quality profile is found for this quality type.
quality = self._empty_quality_container quality = self._empty_quality_container
result.append({"stack": stack, "quality": quality, "quality_changes": empty_quality_changes})
if extruder_stacks: result.append({
# Add an extra entry for the global stack. "stack": extruder_stack,
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = "True") "quality": quality,
"quality_changes": empty_quality_changes
})
if not global_quality: # also find a global quality for the machine
global_quality = self._empty_quality_container global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = "True")
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": empty_quality_changes}) # 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:
global_quality = result[0]["quality"]
# if there is still no global quality, set it to empty (not supported)
if not global_quality:
global_quality = self._empty_quality_container
result.append({
"stack": global_container_stack,
"quality": global_quality,
"quality_changes": empty_quality_changes
})
return result return result
@ -988,10 +1007,8 @@ class MachineManager(QObject):
quality_manager = QualityManager.getInstance() quality_manager = QualityManager.getInstance()
global_container_stack = self._global_container_stack global_container_stack = self._global_container_stack
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom()) global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
quality_changes_profiles = quality_manager.findQualityChangesByName(quality_changes_name, global_machine_definition)
quality_changes_profiles = quality_manager.findQualityChangesByName(quality_changes_name,
global_machine_definition)
global_quality_changes = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") is None] global_quality_changes = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") is None]
if global_quality_changes: if global_quality_changes:
@ -1000,29 +1017,18 @@ 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
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")
if global_container_stack.getProperty("machine_extruder_count", "value") > 1:
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = True)
else:
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
if not global_quality:
global_quality = self._empty_quality_container
# Find the values for each extruder.
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in extruder_stacks: # append the extruder quality changes
extruder_definition = quality_manager.getParentMachineDefinition(stack.getBottom()) for extruder_stack in extruder_stacks:
extruder_definition = quality_manager.getParentMachineDefinition(extruder_stack.definition)
quality_changes_list = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") == extruder_definition.getId()]
quality_changes_list = [qcp for qcp in quality_changes_profiles
if qcp.getMetaDataEntry("extruder") == extruder_definition.getId()]
if quality_changes_list: if quality_changes_list:
quality_changes = quality_changes_list[0] quality_changes = quality_changes_list[0]
else: else:
@ -1030,24 +1036,39 @@ 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 = stack.material material_metadata = extruder_stack.material.getMetaData()
if self._new_material_container and self._active_container_stack.getId() == 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: #No quality profile found for this quality type.
if not quality:
# No quality profile found for this quality type.
quality = self._empty_quality_container quality = self._empty_quality_container
result.append({"stack": stack, "quality": quality, "quality_changes": quality_changes}) result.append({
"stack": extruder_stack,
"quality": quality,
"quality_changes": quality_changes
})
if extruder_stacks: # 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 not global_quality:
global_quality = self._empty_quality_container # if there is not global quality but we're using a single extrusion machine, copy the quality of the first extruder - CURA-4482
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes}) if not global_quality and len(extruder_stacks) == 1:
else: global_quality = result[0]["quality"]
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes})
# if still no global quality changes are found we set it to empty (not supported)
if not global_quality:
global_quality = self._empty_quality_container
result.append({
"stack": global_container_stack,
"quality": global_quality,
"quality_changes": global_quality_changes
})
return result return result
@ -1087,18 +1108,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 ""
@ -1108,7 +1125,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
@ -1126,7 +1143,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
@ -1150,16 +1167,17 @@ 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
@pyqtSlot(str, str) @pyqtSlot(str, str)
def renameMachine(self, machine_id: str, new_name: str): def renameMachine(self, machine_id: str, new_name: str):
containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id) container_registry = ContainerRegistry.getInstance()
if containers: machine_stack = container_registry.findContainerStacks(id = machine_id)
new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName()) if machine_stack:
containers[0].setName(new_name) new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].definition.getName())
machine_stack[0].setName(new_name)
self.globalContainerChanged.emit() self.globalContainerChanged.emit()
@pyqtSlot(str) @pyqtSlot(str)
@ -1169,29 +1187,28 @@ 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)
def hasMaterials(self) -> bool: def hasMaterials(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_materials", False)) return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
return False return False
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def hasVariants(self) -> bool: def hasVariants(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_variants", False)) return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
return False return False
## Property to indicate if a machine has "specialized" material profiles. ## Property to indicate if a machine has "specialized" material profiles.
@ -1199,8 +1216,7 @@ class MachineManager(QObject):
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def filterMaterialsByMachine(self) -> bool: def filterMaterialsByMachine(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False)) return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
return False return False
## Property to indicate if a machine has "specialized" quality profiles. ## Property to indicate if a machine has "specialized" quality profiles.
@ -1208,7 +1224,7 @@ class MachineManager(QObject):
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def filterQualityByMachine(self) -> bool: def filterQualityByMachine(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False)) return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
return False return False
## Get the Definition ID of a machine (specified by ID) ## Get the Definition ID of a machine (specified by ID)
@ -1216,12 +1232,12 @@ class MachineManager(QObject):
# \returns DefinitionID (string) if found, None otherwise # \returns DefinitionID (string) if found, None otherwise
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getDefinitionByMachineId(self, machine_id: str) -> str: def getDefinitionByMachineId(self, machine_id: str) -> str:
containers = ContainerRegistry.getInstance().findContainerStacks(id=machine_id) containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
if containers: if containers:
return containers[0].getBottom().getId() return containers[0].definition.getId()
@staticmethod @staticmethod
def createMachineManager(engine=None, script_engine=None): def createMachineManager():
return MachineManager() return MachineManager()
@deprecated("Use ExtruderStack.material = ... and it won't be necessary", "2.7") @deprecated("Use ExtruderStack.material = ... and it won't be necessary", "2.7")

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,4 +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):
if container.getMetaDataEntry("type", "") == "material":
super()._onContainerChanged(container)
## Group brand together
def _sortKey(self, item) -> List[Any]:
result = []
result.append(item["metadata"]["brand"])
result.append(item["metadata"]["material"])
result.append(item["metadata"]["name"])
result.append(item["metadata"]["color_name"])
result.append(item["metadata"]["id"])
result.extend(super()._sortKey(item))
return result

View file

@ -12,6 +12,11 @@ from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
from cura.QualityManager import QualityManager from cura.QualityManager import QualityManager
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
## QML Model for listing the current list of valid quality profiles. ## QML Model for listing the current list of valid quality profiles.
# #
@ -27,7 +32,6 @@ class ProfilesModel(InstanceContainersModel):
self.addRoleName(self.AvailableRole, "available") self.addRoleName(self.AvailableRole, "available")
Application.getInstance().globalContainerStackChanged.connect(self._update) Application.getInstance().globalContainerStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update) Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
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)
@ -45,6 +49,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.
@ -53,19 +61,12 @@ 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.getBottom() 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.
extruder_manager = ExtruderManager.getInstance() extruder_stacks = self._getOrderedExtruderStacksList()
active_extruder = extruder_manager.getActiveExtruderStack() materials = [extruder.material for extruder in extruder_stacks]
extruder_stacks = extruder_manager.getActiveExtruderStacks()
materials = [global_container_stack.material]
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
extruder_stacks = [active_extruder] + extruder_stacks
materials = [extruder.material for extruder in extruder_stacks]
# Fetch the list of usable qualities across all extruders. # Fetch the list of usable qualities across all extruders.
# 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.
@ -90,42 +91,21 @@ class ProfilesModel(InstanceContainersModel):
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
result.append(not_supported_container) result.append(not_supported_container)
return result return {item.getId():item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
## Re-computes the items in this model, and adds the layer height role. ## 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:
return return
# Detecting if the machine has multiple extrusion extruder_stacks = self._getOrderedExtruderStacksList()
multiple_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 container_registry = ContainerRegistry.getInstance()
# Get the list of extruders and place the selected extruder at the front of the list.
extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = extruder_manager.getActiveExtruderStacks()
if multiple_extrusion:
# Place the active extruder at the front of the list.
# This is a workaround checking if there is an active_extruder or not before moving it to the front of the list.
# Actually, when a printer has multiple extruders, should exist always an active_extruder. However, in some
# cases the active_extruder is still None.
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
new_extruder_stacks = []
if active_extruder is not None:
new_extruder_stacks = [active_extruder]
extruder_stacks = new_extruder_stacks + extruder_stacks
# Get a list of usable/available qualities for this machine and material # Get a list of usable/available qualities for this machine and material
qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks) qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
container_registry = ContainerRegistry.getInstance()
machine_manager = Application.getInstance().getMachineManager()
unit = global_container_stack.getBottom().getProperty("layer_height", "unit") unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
if not unit: if not unit:
unit = "" unit = ""
@ -134,8 +114,12 @@ 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"])
quality_type = profile[0].getMetaDataEntry("quality_type") if profile else "" profiles = container_registry.findContainersMetadata(id = item["id"])
if not profiles or "quality_type" not in profiles[0]:
quality_type = ""
else:
quality_type = profiles[0]["quality_type"]
if quality_type not in tmp_all_quality_items: 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": []}
@ -190,6 +174,8 @@ class ProfilesModel(InstanceContainersModel):
yield item yield item
continue continue
machine_manager = Application.getInstance().getMachineManager()
# Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile. # Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
quality_type = profile.getMetaDataEntry("quality_type", None) quality_type = profile.getMetaDataEntry("quality_type", None)
if quality_type: if quality_type:
@ -201,7 +187,8 @@ class ProfilesModel(InstanceContainersModel):
else: else:
# No global container stack in the results: # No global container stack in the results:
if quality_results: if quality_results:
quality = quality_results[0]["quality"] # Take any of the extruders. # Take any of the extruders.
quality = quality_results[0]["quality"]
else: else:
quality = None quality = None
if quality and quality.hasProperty("layer_height", "value"): if quality and quality.hasProperty("layer_height", "value"):
@ -211,13 +198,27 @@ class ProfilesModel(InstanceContainersModel):
# Quality has no value for layer height either. Get the layer height from somewhere lower in the stack. # Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
skip_until_container = global_container_stack.material skip_until_container = global_container_stack.material
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): #No material in stack. if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No material in stack.
skip_until_container = global_container_stack.variant skip_until_container = global_container_stack.variant
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): #No variant in stack. if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No variant in stack.
skip_until_container = global_container_stack.getBottom() skip_until_container = global_container_stack.getBottom()
self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material. self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material.
yield item yield item
def _setItemLayerHeight(self, item, value, unit): ## Get a list of extruder stacks with the active extruder at the front of the list.
@staticmethod
def _getOrderedExtruderStacksList() -> List["ExtruderStack"]:
extruder_manager = ExtruderManager.getInstance()
extruder_stacks = extruder_manager.getActiveExtruderStacks()
active_extruder = extruder_manager.getActiveExtruderStack()
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
extruder_stacks = [active_extruder] + extruder_stacks
return extruder_stacks
@staticmethod
def _setItemLayerHeight(item, value, unit):
item["layer_height"] = str(value) + unit item["layer_height"] = str(value) + unit
item["layer_height_without_unit"] = str(value) item["layer_height_without_unit"] = str(value)

View file

@ -18,51 +18,29 @@ 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()
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom()) machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition) quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
# Detecting if the machine has multiple extrusion
multiple_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
# Get the list of extruders
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack() active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = extruder_manager.getActiveExtruderStacks() extruder_stacks = self._getOrderedExtruderStacksList()
if multiple_extrusion:
# Place the active extruder at the front of the list.
# This is a workaround checking if there is an active_extruder or not before moving it to the front of the list.
# Actually, when a printer has multiple extruders, should exist always an active_extruder. However, in some
# cases the active_extruder is still None.
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
new_extruder_stacks = []
if active_extruder is not None:
new_extruder_stacks = [active_extruder]
else:
# if there is no active extruder, use the first one in the active extruder stacks
active_extruder = extruder_stacks[0]
extruder_stacks = new_extruder_stacks + extruder_stacks
# Fetch the list of useable qualities across all extruders. # Fetch the list of usable qualities across all extruders.
# 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.
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
extruder_stacks)
# 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.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
if multiple_extrusion: result = filtered_quality_changes
# If the printer has multiple extruders then quality changes related to the current extruder are kept result.update({q.getId():q for q in quality_list})
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set and return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
else:
# If not, the quality changes of the global stack are selected
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is None]
return quality_list + filtered_quality_changes

View file

@ -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)
@ -224,7 +223,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
if self._extruder_id == "" and settable_per_extruder: if self._extruder_id == "" and settable_per_extruder:
continue continue
label = definition.label label = definition.label
if self._i18n_catalog: if self._i18n_catalog:
label = self._i18n_catalog.i18nc(definition.key + " label", label) label = self._i18n_catalog.i18nc(definition.key + " label", label)

View file

@ -47,21 +47,20 @@ class SettingInheritanceManager(QObject):
@pyqtSlot(str, str, result = "QStringList") @pyqtSlot(str, str, result = "QStringList")
def getOverridesForExtruder(self, key, extruder_index): def getOverridesForExtruder(self, key, extruder_index):
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1 result = []
if not multi_extrusion:
return self._settings_with_inheritance_warning
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if not extruder:
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
return []
definitions = self._global_container_stack.definition.findDefinitions(key=key) extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if not extruder_stack:
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
return result
definitions = self._global_container_stack.definition.findDefinitions(key = key)
if not definitions: if not definitions:
Logger.log("w", "Could not find definition for key [%s] (2)", key) Logger.log("w", "Could not find definition for key [%s] (2)", key)
return [] return result
result = []
for key in definitions[0].getAllKeys(): for key in definitions[0].getAllKeys():
if self._settingIsOverwritingInheritance(key, extruder): if self._settingIsOverwritingInheritance(key, extruder_stack):
result.append(key) result.append(key)
return result return result
@ -78,8 +77,8 @@ class SettingInheritanceManager(QObject):
def _onActiveExtruderChanged(self): def _onActiveExtruderChanged(self):
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack() new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not new_active_stack: # if not new_active_stack:
new_active_stack = self._global_container_stack # new_active_stack = self._global_container_stack
if new_active_stack != self._active_container_stack: # Check if changed if new_active_stack != self._active_container_stack: # Check if changed
if self._active_container_stack: # Disconnect signal from old container (if any) if self._active_container_stack: # Disconnect signal from old container (if any)

View file

@ -22,16 +22,20 @@ 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 = 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()
if ExtruderManager.getInstance().extruderCount > 1:
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
else:
self._extruder_stack = None
self._stack.propertyChanged.connect(self._onSettingChanged) self._stack.propertyChanged.connect(self._onSettingChanged)
@ -82,6 +86,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle() Application.getInstance().getBackend().tickle()
self._node._non_printing_mesh = any(self._stack.getProperty(setting, "value") for setting in self._non_printing_mesh_settings)
## Makes sure that the stack upon which the container stack is placed is ## Makes sure that the stack upon which the container stack is placed is
# kept up to date. # kept up to date.
def _updateNextStack(self): def _updateNextStack(self):

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,57 +12,69 @@ 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()
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom()) machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition) quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
# Detecting if the machine has multiple extrusion
multiple_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
# Get the list of extruders and place the selected extruder at the front of the list.
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack() active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = extruder_manager.getActiveExtruderStacks() extruder_stacks = self._getOrderedExtruderStacksList()
if multiple_extrusion:
# Place the active extruder at the front of the list.
# This is a workaround checking if there is an active_extruder or not before moving it to the front of the list.
# Actually, when a printer has multiple extruders, should exist always an active_extruder. However, in some
# cases the active_extruder is still None.
if active_extruder in extruder_stacks:
extruder_stacks.remove(active_extruder)
new_extruder_stacks = []
if active_extruder is not None:
new_extruder_stacks = [active_extruder]
else:
# if there is no active extruder, use the first one in the active extruder stacks
active_extruder = extruder_stacks[0]
extruder_stacks = new_extruder_stacks + extruder_stacks
# Fetch the list of useable qualities across all extruders. # Fetch the list of usable qualities across all extruders.
# 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.
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
extruder_stacks)
# 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])
if multiple_extrusion: filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
# If the printer has multiple extruders then quality changes related to the current extruder are kept qc.getMetaDataEntry("quality_type") in quality_type_set and
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set and qc.getMetaDataEntry("extruder") is not None and
qc.getMetaDataEntry("extruder") 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())]
else:
# If not, the quality changes of the global stack are selected
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is None]
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()
if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
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,9 +50,10 @@ 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
# the default /usr/lib which causes the default site-packages # the default /usr/lib which causes the default site-packages
# to be inserted into sys.path before PYTHONPATH. # to be inserted into sys.path before PYTHONPATH.
@ -45,7 +74,8 @@ def exceptHook(hook_type, value, traceback):
_crash_handler = CrashHandler(hook_type, value, traceback) _crash_handler = CrashHandler(hook_type, value, traceback)
_crash_handler.show() _crash_handler.show()
sys.excepthook = exceptHook if not known_args["debug"]:
sys.excepthook = exceptHook
# Workaround for a race condition on certain systems where there # Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus # is a race condition between Arcus and PyQt. Importing Arcus
@ -55,29 +85,14 @@ import Arcus #@UnusedImport
import cura.CuraApplication import cura.CuraApplication
import cura.Settings.CuraContainerRegistry import cura.Settings.CuraContainerRegistry
def get_cura_dir_path():
if Platform.isWindows():
return os.path.expanduser("~/AppData/Local/cura/")
elif Platform.isLinux():
return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
faulthandler.enable() faulthandler.enable()
# Force an instance of CuraContainerRegistry to be created and reused later. # Force an instance of CuraContainerRegistry to be created and reused later.
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance() cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
# This prestart up check is needed to determine if we should start the application at all. # This pre-start up check is needed to determine if we should start the application at all.
if not cura.CuraApplication.CuraApplication.preStartUp(): if not cura.CuraApplication.CuraApplication.preStartUp(parser = parser, parsed_command_line = known_args):
sys.exit(0) sys.exit(0)
app = cura.CuraApplication.CuraApplication.getInstance() app = cura.CuraApplication.CuraApplication.getInstance(parser = parser, parsed_command_line = known_args)
app.run() app.run()

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

@ -107,24 +107,17 @@ class ThreeMFReader(MeshReader):
um_node.addDecorator(SettingOverrideDecorator()) um_node.addDecorator(SettingOverrideDecorator())
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
# Ensure the correct next container for the SettingOverride decorator is set. # Ensure the correct next container for the SettingOverride decorator is set.
if global_container_stack: if global_container_stack:
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 default_stack = ExtruderManager.getInstance().getExtruderStack(0)
# Ensure that all extruder data is reset if default_stack:
if not multi_extrusion: um_node.callDecoration("setActiveExtruder", default_stack.getId())
default_stack_id = global_container_stack.getId()
else:
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
if default_stack:
default_stack_id = default_stack.getId()
else:
default_stack_id = global_container_stack.getId()
um_node.callDecoration("setActiveExtruder", default_stack_id)
# 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()
@ -139,7 +132,7 @@ class ThreeMFReader(MeshReader):
else: else:
Logger.log("w", "Unable to find extruder in position %s", setting_value) Logger.log("w", "Unable to find extruder in position %s", setting_value)
continue continue
setting_container.setProperty(key,"value", setting_value) setting_container.setProperty(key, "value", setting_value)
if len(um_node.getChildren()) > 0: if len(um_node.getChildren()) > 0:
group_decorator = GroupDecorator() group_decorator = GroupDecorator()

View file

@ -13,6 +13,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.MimeTypeDatabase import MimeTypeDatabase from UM.MimeTypeDatabase import MimeTypeDatabase
from UM.Job import Job from UM.Job import Job
from UM.Preferences import Preferences from UM.Preferences import Preferences
from UM.Util import parseBool
from .WorkspaceDialog import WorkspaceDialog from .WorkspaceDialog import WorkspaceDialog
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -121,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")
@ -132,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}
@ -148,20 +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)
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":
@ -185,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()
@ -208,7 +205,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container = InstanceContainer(container_id) instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string # Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8")) instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"),
file_name = each_instance_container_file)
instance_container_list.append(instance_container) instance_container_list.append(instance_container)
container_type = instance_container.getMetaDataEntry("type") container_type = instance_container.getMetaDataEntry("type")
@ -378,7 +376,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return WorkspaceReader.PreReadResult.accepted return WorkspaceReader.PreReadResult.accepted
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack. ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content): def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first # Get extruder position first
extruder_config = configparser.ConfigParser() extruder_config = configparser.ConfigParser()
extruder_config.read_string(extruder_file_content) extruder_config.read_string(extruder_file_content)
@ -394,7 +392,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return None return None
# Override the given extruder stack # Override the given extruder stack
extruder_stack.deserialize(extruder_file_content) extruder_stack.deserialize(extruder_file_content, file_name = extruder_stack_file)
# return the new ExtruderStack # return the new ExtruderStack
return extruder_stack return extruder_stack
@ -446,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 = []
@ -458,7 +457,7 @@ 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
@ -467,7 +466,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
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)
@ -481,10 +480,11 @@ 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"),
file_name = definition_container_file)
self._container_registry.addContainer(definition_container) self._container_registry.addContainer(definition_container)
Job.yieldThread() Job.yieldThread()
@ -502,18 +502,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not materials: if not materials:
material_container = xml_material_profile(container_id) material_container = xml_material_profile(container_id)
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)
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)
elif self._resolve_strategies["material"] == "new": elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be # Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added. # auto created & added.
material_container = xml_material_profile(self.getNewId(container_id)) material_container = xml_material_profile(self.getNewId(container_id))
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)
containers_to_add.append(material_container) containers_to_add.append(material_container)
material_containers.append(material_container) material_containers.append(material_container)
@ -540,7 +543,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container = InstanceContainer(container_id) instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string # Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(serialized) instance_container.deserialize(serialized, file_name = instance_container_file)
container_type = instance_container.getMetaDataEntry("type") container_type = instance_container.getMetaDataEntry("type")
Job.yieldThread() Job.yieldThread()
@ -562,7 +565,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
else: else:
if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None: if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
instance_container = user_containers[0] instance_container = user_containers[0]
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"),
file_name = instance_container_file)
instance_container.setDirty(True) instance_container.setDirty(True)
elif self._resolve_strategies["machine"] == "new": elif self._resolve_strategies["machine"] == "new":
# The machine is going to get a spiffy new name, so ensure that the id's of user settings match. # The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
@ -570,7 +574,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)
@ -579,7 +583,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)
@ -595,7 +599,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# selected strategy. # selected strategy.
if self._resolve_strategies[container_type] == "override": if self._resolve_strategies[container_type] == "override":
instance_container = changes_containers[0] instance_container = changes_containers[0]
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"),
file_name = instance_container_file)
instance_container.setDirty(True) instance_container.setDirty(True)
elif self._resolve_strategies[container_type] == "new": elif self._resolve_strategies[container_type] == "new":
@ -627,8 +632,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:
@ -644,28 +655,30 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Get the stack(s) saved in the workspace. # Get the stack(s) saved in the workspace.
Logger.log("d", "Workspace loading is checking stacks containers...") Logger.log("d", "Workspace loading is checking stacks containers...")
# --
# load global stack file # load global stack file
try: try:
stack = None
if self._resolve_strategies["machine"] == "override": if self._resolve_strategies["machine"] == "override":
container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original) container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original)
stack = container_stacks[0] stack = container_stacks[0]
# 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)
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
stack = GlobalStack(global_stack_id_new) stack = GlobalStack(global_stack_id_new)
# Deserialize stack by converting read data from bytes to string # Deserialize stack by converting read data from bytes to string
stack.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)
# Ensure a unique ID and name # Ensure a unique ID and name
stack._id = global_stack_id_new stack._id = global_stack_id_new
@ -682,12 +695,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.addContainer(stack) self._container_registry.addContainer(stack)
containers_added.append(stack) containers_added.append(stack)
else: else:
Logger.log("e", "Resolve strategy of %s for machine is not supported", Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
self._resolve_strategies["machine"])
# Create a new definition_changes container if it was empty # Create a new definition_changes container if it was empty
if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer(): if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer():
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack._id + "_settings")) stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings"))
global_stack = stack global_stack = stack
Job.yieldThread() Job.yieldThread()
except: except:
@ -697,16 +709,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId()) self._container_registry.removeContainer(container.getId())
return return
#
# Use the number of extruders from the global stack instead of the number of extruder stacks this project file
# contains. The Custom FDM Printer can have multiple extruders, but the actual number of extruders in used is
# defined in the global stack.
# Because for single-extrusion machines, there won't be an extruder stack, so relying on the the extruder count
# in the global stack can avoid problems in those cases.
#
extruder_count_from_global_stack = global_stack.getProperty("machine_extruder_count", "value")
# --
# load extruder stack files # load extruder stack files
try: try:
for extruder_stack_file in extruder_stack_files: for extruder_stack_file in extruder_stack_files:
@ -714,11 +716,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8") extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8")
if self._resolve_strategies["machine"] == "override": if self._resolve_strategies["machine"] == "override":
if global_stack.getProperty("machine_extruder_count", "value") > 1: # deserialize new extruder stack over the current ones (if any)
# deserialize new extruder stack over the current ones (if any) stack = self._overrideExtruderStack(global_stack, extruder_file_content, extruder_stack_file)
stack = self._overrideExtruderStack(global_stack, extruder_file_content) if stack is None:
if stack is None: continue
continue
elif self._resolve_strategies["machine"] == "new": elif self._resolve_strategies["machine"] == "new":
new_id = extruder_stack_id_map[container_id] new_id = extruder_stack_id_map[container_id]
@ -736,7 +737,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_config.write(tmp_string_io) extruder_config.write(tmp_string_io)
extruder_file_content = tmp_string_io.getvalue() extruder_file_content = tmp_string_io.getvalue()
stack.deserialize(extruder_file_content) 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._id = new_id
@ -749,9 +750,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Create a new definition_changes container if it was empty # Create a new definition_changes container if it was empty
if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer(): if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer():
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack._id + "_settings")) stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings"))
if global_stack.getProperty("machine_extruder_count", "value") > 1:
if stack.getMetaDataEntry("type") == "extruder_train":
extruder_stacks.append(stack) extruder_stacks.append(stack)
# If not extruder stacks were saved in the project file (pre 3.1) create one manually
# We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this
if not extruder_stacks:
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder")
if stack:
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
extruder_stacks.append(existing_extruder_stack)
else:
extruder_stacks.append(stack)
except: except:
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.") Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
# Something went really wrong. Try to remove any data that we added. # Something went really wrong. Try to remove any data that we added.
@ -759,6 +777,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId()) self._container_registry.removeContainer(container.getId())
return return
# Check quality profiles to make sure that if one stack has the "not supported" quality profile, # Check quality profiles to make sure that if one stack has the "not supported" quality profile,
# all others should have the same. # all others should have the same.
# #
@ -773,18 +792,102 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# #
has_not_supported = False has_not_supported = False
for stack in [global_stack] + extruder_stacks: for stack in [global_stack] + extruder_stacks:
if stack.quality.getId() == "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,
extruder_stacks_in_use)
if not has_not_supported: if not has_not_supported:
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack, extruder_stacks)
has_not_supported = not available_quality has_not_supported = not available_quality
quality_has_been_changed = False
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]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True
else:
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
# The machine in the project has non-empty quality and there are usable qualities for this machine.
# We need to check if the current quality_type is still usable for this machine, if not, then the quality
# will be reset to the "preferred quality" if present, otherwise "normal".
available_quality_types = [q.getMetaDataEntry("quality_type") for q in available_quality]
if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types:
# We are here because the quality_type specified in the project is not supported any more,
# so we need to switch it to the "preferred quality" if present, otherwise "normal".
quality_has_been_changed = True
# find the preferred quality
preferred_quality_id = global_stack.getMetaDataEntry("preferred_quality", None)
if preferred_quality_id is not None:
definition_id = global_stack.definition.getId()
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
definition_id = "fdmprinter"
containers = self._container_registry.findInstanceContainers(id = preferred_quality_id,
type = "quality",
definition = definition_id)
containers = [c for c in containers if not c.getMetaDataEntry("material", "")]
if containers:
global_stack.quality = containers[0]
global_stack.qualityChanges = empty_quality_changes_container
# also find the quality containers for the extruders
for extruder_stack in extruder_stacks_in_use:
search_criteria = {"id": preferred_quality_id,
"type": "quality",
"definition": definition_id}
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
extruder_stack.quality = containers[0]
extruder_stack.qualityChanges = empty_quality_changes_container
else:
Logger.log("e", "Cannot find preferred quality for extruder [%s].", extruder_stack.getId())
else:
# we cannot find the preferred quality. THIS SHOULD NOT HAPPEN
Logger.log("e", "Cannot find the preferred quality for machine [%s]", global_stack.getId())
else:
# The quality_type specified in the project file is usable, but the quality container itself may not
# be correct. For example, for UM2, the quality container can be "draft" while it should be "um2_draft"
# instead.
# In this code branch, we try to fix those incorrect quality containers.
#
# ***IMPORTANT***: We only do this fix for single-extrusion machines.
# We will first find the correct quality profile for the extruder, then apply the same
# quality profile for the global stack.
#
if len(extruder_stacks) == 1:
extruder_stack = extruder_stacks[0]
search_criteria = {"type": "quality", "quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
search_criteria["definition"] = global_stack.definition.getId()
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
search_criteria["definition"] = "fdmprinter"
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
new_quality_container = containers[0]
extruder_stack.quality = new_quality_container
global_stack.quality = new_quality_container
#
# Replacing the old containers if resolve is "new". # Replacing the old containers if resolve is "new".
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those # When resolve is "new", some containers will get renamed, so all the other containers that reference to those
# MUST get updated too. # MUST get updated too.
@ -807,7 +910,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack.userChanges = container global_stack.userChanges = container
continue continue
for changes_container_type in ("quality_changes", "definition_changes"): changes_container_types = ("quality_changes", "definition_changes")
if quality_has_been_changed:
# DO NOT replace quality_changes if the current quality_type is not supported
changes_container_types = ("definition_changes",)
for changes_container_type in changes_container_types:
if self._resolve_strategies[changes_container_type] == "new": if self._resolve_strategies[changes_container_type] == "new":
# Quality changes needs to get a new ID, added to registry and to the right stacks # Quality changes needs to get a new ID, added to registry and to the right stacks
for each_changes_container in quality_and_definition_changes_instance_containers: for each_changes_container in quality_and_definition_changes_instance_containers:
@ -907,7 +1014,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"): if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")] base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
Application.getInstance().projectFileLoaded.emit(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
## HACK: Replaces the material container in the given stack with a newly created material container. ## HACK: Replaces the material container in the given stack with a newly created material container.

View file

@ -1,12 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V. # Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -256,14 +254,8 @@ class WorkspaceDialog(QObject):
return self._result return self._result
def _createViewFromQML(self): def _createViewFromQML(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)
self._component = QQmlComponent(Application.getInstance()._engine, path) self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
self._context.setContextProperty("manager", self)
self._view = self._component.create(self._context)
if self._view is None:
Logger.log("c", "QQmlComponent status %s", self._component.status())
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
def show(self): def show(self):
# Emit signal so the right thread actually shows the view. # Emit signal so the right thread actually shows the view.

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

@ -87,7 +87,7 @@ class ThreeMFWriter(MeshWriter):
if stack is not None: if stack is not None:
changed_setting_keys = set(stack.getTop().getAllKeys()) changed_setting_keys = set(stack.getTop().getAllKeys())
# Ensure that we save the extruder used for this object. # Ensure that we save the extruder used for this object in a multi-extrusion setup
if stack.getProperty("machine_extruder_count", "value") > 1: if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr") changed_setting_keys.add("extruder_nr")

View file

@ -8,8 +8,7 @@ from UM.Application import Application
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Version import Version from UM.Version import Version
from PyQt5.QtQml import QQmlComponent, QQmlContext from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
import os.path import os.path
import collections import collections
@ -106,9 +105,5 @@ class ChangeLog(Extension, QObject,):
self._changelog_window.hide() self._changelog_window.hide()
def createChangelogWindow(self): def createChangelogWindow(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")) path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})
component = QQmlComponent(Application.getInstance()._engine, path)
self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._changelog_context.setContextProperty("manager", self)
self._changelog_window = component.create(self._changelog_context)

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.
@ -571,10 +640,10 @@ The new GUI allows custom profiles to load easily and intuitively, directly from
*3MF File Loading Support *3MF File Loading Support
Were happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available. Were happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
*Intuitive Cut-Off Object Bottom *Intuitive Cut-Off Object Bottom
Weve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting. Weve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
*64-bit Windows Builds *64-bit Windows Builds
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files. An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
*Automatic calculations *Automatic calculations
@ -639,3 +708,165 @@ Prints the outer walls with a jittering motion to give your object a diffuse fin
*Wire Printing *Wire Printing
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg. The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
[15.06 Beta]
Cura 15.06 is a new release built from the ground up on a completely new
framework called Uranium. This framework has been designed to make it easier to
extend Cura with additional functionality as well as provide a cleaner UI.
[15.05.95]
* Fixed: Selection ghost remains visible after deleting an object
* Fixed: Window does not show up immediately after starting application on OSX
* Fixed: Added display of rotation angle during rotation
* Fixed: Object changes position while rotating/scaling
* Fixed: Loading improvements in the layer view
* Fixed: Added application icons
* Fixed: Improved feedback when loading models
* Fixed: Eject device on MacOSX now provides proper feedback
* Fixed: Make it possible to show retraction settings for UM2
* Fixed: Opening the machine preferences page will switch to the first available machine
* Fixed: Improved tool handle hit area size
* Fixed: Render lines with a thickness based on screen DPI
[15.05.94]
* Added Russian translations
* Fixed: Infill not displayed in layer view
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
* Fixed: Improved font rendering on Windows
* Fixed: Help > Show Documentation crashes Cura on Windows
* Fixed: "There is no disk in the drive" repeating messages on Windows
* Fixed: Retraction settings not visible for Ultimaker2
* Fixed: Display rotation angle when rotating an object
* Fixed: Time/Quality slider values are properly rounded
* Fixed: Improved clarity of buttons and text
* Fixed: No indication that anything is happening when loading a model
* Fixed: Eject device now works on Windows
[15.05.93]
* Fixed: No shortcuts for moving up/down layers in layer view.
* Fixed: Last view layers could not be scrolled through in layer view.
* Fixed: Files provided on command line would not actually show up on the build
platform.
* Fixed: Render a ghost of the selection in Layer view to make the actual object
position clear.
* Fixed: Showing a menu would clear the selection.
* Fixed: Size and scaling factor display for scale tool.
* Fixed: Missing background for additional tool controls.
* Fixed: Loading message times out when loading large files.
* Fixed: Show recent files in the file menu.
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
prevent issues with missing DLL's.
* Fixed: Collapsed/expanded state of setting categories not stored.
[15.05.91]
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
higher.
* Fixed: Need to deselect before selecting a different object.
* Fixed: Object can be moved on Z axis.
* Fixed: Error values should be considered invalid values and will not trigger a
slice.
* Fixed: Text fields used a locale-aware validator while the underlying code did
not.
* Fixed: Text fields will trigger a slice on text change, not only after focus
change/enter press.
* Fixed: Rotate Tool snaps to incorrect value.
* Fixed: Object Collision would only moved objects to the right.
* Fixed: Object Collision would move the selected object when it should not.
* Fixed: Camera panning now works correctly instead of doing nothing.
* Fixed: Camera would flip around center point at maximum rotation.
* Fixed: Build platform grid blocked view from below objects.
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
window
[15.05.90]
* Fixed: Additional UI elements for tools and views not loading.
* Fixed: Double click needed to change setting dialog page.
* Fixed: Context menu entries (reload, center object, etc.) not working.
* Fixed: "Open With" or passing files from command line not working.
* Fixed: "Reload All" would not reload files.
In addition, a lot of work has gone into getting a usable Mac OSX version.
New Features
------------
* Plugin based system
The Uranium framework provides us with a plugin-based system
that provides additional flexibility when extending Cura. Think
of new views, tools, file formats, etc. This is probably the
biggest new feature.
* Improved UI
The UI has received a complete overhaul.
* Time-Quality Slider
The 4 static quick print profiles have been replaced with
a slider that should make it easier to find the right spot
between print time and print quality.
* More Settings
The Advanced mode is now configurable and can show many
additional settings that were previously not available, while at
the same time not overwhelming new users with too many settings.
Custom set of visible settings can be created by the user.
* Support for high-DPI screens
The refreshed UI has been designed with high-DPI screens in
mind which should improve the experience of Cura on such
devices.
* Improved language support
(Not yet available for the Beta release.)
* Improved support structure generation
The new version of the CuraEngine now features improved
support generation algorithms and additional options for support
structure generation.
* Experimental Feature: Wire Printing
Wire Printing has been added as an experimental new feature. It
will print objects as a structure of lines. It can be enabled by
from Advanced Mode -> Fixes -> Wire Printing.
* Undo/Redo
It is now possible to undo and redo most scene operations, like
moving or rotating objects.
Features from earlier versions not (yet) in this release
--------------------------------------------------------
* The All-at-once/One-at-a-time toggle is not available.
We are working on an improved implementation of this mechanism
but it will not be available for this release.
* No dual extrusion features are available yet.
We are working on a completely new workflow for this but this
needs additional time.
* “Lay Flat” has been removed.
The existing implementation was unfortunately not salvageable.
We will be looking into an improved implementation for this
feature.
* "Split Object Into Parts" has been removed.
Due to the same reason as Lay Flat.
* Support for AMF and DAE file formats has been removed.
Both of these will be implemented as plugins in the future.
* Support for directly loading a GCode file is not yet available.
This will be implemented as a plugin in the future.
* Support for PNG, JPG and other image formats has been removed.
These can be supported by a plugin with an improved UI.
* Support for loading Minecraft levels has been removed.
This can be implemented as a plugin.
* Windows XP support has been dropped.
Microsoft is no longer supporting xp, so they no longer back
port certain features that we require.
* X-Ray view is missing.
Will be implemented as a (you might have guessed it) plugin.
* Fixes: Follow Mesh Surface
Has been removed from the engine, the same result can be
achieved using no infill or top/bottom layers.
Known Issues
------------
* Some OBJ files are rendered as black objects due to missing
normals.
* Disabling plugins does not work correctly yet.
* Unicorn occasionally still requires feeding. Do not feed it
after midnight.

View file

@ -61,6 +61,8 @@ message Polygon {
Type type = 1; // Type of move Type type = 1; // Type of move
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used) bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
float line_width = 3; // The width of the line being laid down float line_width = 3; // The width of the line being laid down
float line_thickness = 4; // The thickness of the line being laid down
float line_feedrate = 5; // The feedrate of the line being laid down
} }
message LayerOptimized { message LayerOptimized {
@ -82,6 +84,8 @@ message PathSegment {
bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1 bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1
bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path
bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
bytes line_thickness = 6; // The thickness of the line segments as bytes of a float array of length 1 or N
bytes line_feedrate = 7; // The feedrate of the line segments as bytes of a float array of length 1 or N
} }

View file

@ -86,6 +86,7 @@ class CuraEngineBackend(QObject, Backend):
# #
self._global_container_stack = None self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -301,6 +302,26 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
return return
elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
errors = {}
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
stack = node.callDecoration("getStack")
if not stack:
continue
for key in stack.getErrorKeys():
definition = self._global_container_stack.getBottom().findDefinitions(key = key)
if not definition:
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
continue
definition = definition[0]
errors[key] = definition.label
error_labels = ", ".join(errors.values())
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
return
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
if Application.getInstance().platformActivity: if Application.getInstance().platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."), self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
@ -588,7 +609,7 @@ class CuraEngineBackend(QObject, Backend):
def _onActiveViewChanged(self): def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView(): if Application.getInstance().getController().getActiveView():
view = Application.getInstance().getController().getActiveView() view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "LayerView": # 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.

View file

@ -61,7 +61,9 @@ class ProcessSlicedLayersJob(Job):
def run(self): def run(self):
start_time = time() start_time = time()
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": 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:
@ -95,20 +97,27 @@ class ProcessSlicedLayersJob(Job):
# Find the minimum layer number # Find the minimum layer number
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0. # instead simply offset all other layers so the lowest layer is always 0. It could happens that
# the first raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = 0 min_layer_number = 0
negative_layers = 0
for layer in self._layers: for layer in self._layers:
if layer.id < min_layer_number: if layer.id < min_layer_number:
min_layer_number = layer.id min_layer_number = layer.id
if layer.id < 0:
negative_layers += 1
current_layer = 0 current_layer = 0
for layer in self._layers: for layer in self._layers:
abs_layer_number = layer.id + abs(min_layer_number) # Negative layers are offset by the minimum layer number, but the positive layers are just
# offset by the number of negative layers so there is no layer gap between raft and model
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers
layer_data.addLayer(abs_layer_number) layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number)
layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerHeight(abs_layer_number, layer.height)
layer_data.setLayerThickness(abs_layer_number, layer.thickness)
for p in range(layer.repeatedMessageCount("path_segment")): for p in range(layer.repeatedMessageCount("path_segment")):
polygon = layer.getRepeatedMessage("path_segment", p) polygon = layer.getRepeatedMessage("path_segment", p)
@ -127,10 +136,11 @@ class ProcessSlicedLayersJob(Job):
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
# In the future, line_thicknesses should be given by CuraEngine as well. line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array
# Currently the infill layer thickness also translates to line width line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4")
line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array
line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
# Create a new 3D-array, copy the 2D points over and insert the right height. # Create a new 3D-array, copy the 2D points over and insert the right height.
# This uses manual array creation + copy rather than numpy.insert since this is # This uses manual array creation + copy rather than numpy.insert since this is
@ -145,7 +155,7 @@ class ProcessSlicedLayersJob(Job):
new_points[:, 1] = points[:, 2] new_points[:, 1] = points[:, 2]
new_points[:, 2] = -points[:, 1] new_points[:, 2] = -points[:, 1]
this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache() this_poly.buildCache()
this_layer.polygons.append(this_poly) this_layer.polygons.append(this_poly)
@ -218,10 +228,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() == "LayerView":
view.resetLayerData()
if self._progress_message: if self._progress_message:
self._progress_message.hide() self._progress_message.hide()
@ -232,7 +238,7 @@ class ProcessSlicedLayersJob(Job):
def _onActiveViewChanged(self): def _onActiveViewChanged(self):
if self.isRunning(): if self.isRunning():
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
if not self._progress_message: if not self._progress_message:
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information"))
if self._progress_message.getProgress() != 100: if self._progress_message.getProgress() != 100:

View file

@ -26,6 +26,7 @@ class StartJobResult(IntEnum):
NothingToSlice = 4 NothingToSlice = 4
MaterialIncompatible = 5 MaterialIncompatible = 5
BuildPlateError = 6 BuildPlateError = 6
ObjectSettingError = 7 #When an error occurs in per-object settings.
## Formatter class that handles token expansion in start/end gcod ## Formatter class that handles token expansion in start/end gcod
@ -44,14 +45,6 @@ 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__()
@ -105,7 +98,7 @@ class StartSliceJob(Job):
continue continue
if self._checkStackForErrors(node.callDecoration("getStack")): if self._checkStackForErrors(node.callDecoration("getStack")):
self.setResult(StartJobResult.SettingError) self.setResult(StartJobResult.ObjectSettingError)
return return
with self._scene.getSceneLock(): with self._scene.getSceneLock():
@ -138,13 +131,21 @@ 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 type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False)\ _non_printing_mesh = getattr(node, "_non_printing_mesh", False)
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): if not getattr(node, "_outside_buildarea", False) or _non_printing_mesh:
temp_list.append(node) temp_list.append(node)
if not _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)
@ -158,13 +159,9 @@ class StartSliceJob(Job):
self._buildGlobalSettingsMessage(stack) self._buildGlobalSettingsMessage(stack)
self._buildGlobalInheritsStackMessage(stack) self._buildGlobalInheritsStackMessage(stack)
# Only add extruder stacks if there are multiple extruders # Build messages for extruder stacks
# Single extruder machines only use the global stack to store setting values for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
if stack.getProperty("machine_extruder_count", "value") > 1: self._buildExtruderMessage(extruder_stack)
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
self._buildExtruderMessage(extruder_stack)
else:
self._buildExtruderMessageFromGlobalStack(stack)
for group in object_groups: for group in object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists") group_message = self._slice_message.addRepeatedMessage("object_lists")
@ -208,32 +205,53 @@ 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"))]
return result
## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in.
# \param settings A dictionary of tokens to replace and their respective
# replacement strings.
def _expandGcodeTokens(self, value: str, settings: dict):
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") 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"] settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
settings["time"] = time.strftime("%H:%M:%S") #Some extra settings. settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
settings["date"] = time.strftime("%d-%m-%Y")
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.
@ -241,26 +259,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()
## Create extruder message from global stack
def _buildExtruderMessageFromGlobalStack(self, stack):
message = self._slice_message.addRepeatedMessage("extruders")
for key in stack.getAllKeys():
# Do not send settings that are not settable_per_extruder.
if not stack.getProperty(key, "settable_per_extruder"):
continue
setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key
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.
@ -268,33 +267,28 @@ class StartSliceJob(Job):
# The settings are taken from the global stack. This does not include any # The settings are taken from the global stack. This does not include any
# per-extruder settings or per-object settings. # per-extruder settings or per-object settings.
def _buildGlobalSettingsMessage(self, stack): def _buildGlobalSettingsMessage(self, stack):
keys = stack.getAllKeys() settings = self._buildReplacementTokens(stack)
settings = {}
for key in keys:
settings[key] = stack.getProperty(key, "value")
Job.yieldThread()
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"] start_gcode = settings["machine_start_gcode"]
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings)) settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"} print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings)) settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
settings["print_bed_temperature"] = settings["material_bed_temperature"] # Find the correct temperatures from the first used extruder
settings["print_temperature"] = settings["material_print_temperature"] extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
settings["time"] = time.strftime('%H:%M:%S') # Replace the setting tokens in start and end g-code.
settings["date"] = time.strftime('%d-%m-%Y') settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings)
settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))] settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings)
for key, value in settings.items(): #Add all submessages for each individual setting. # Add all sub-messages for each individual setting.
for key, value in settings.items():
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings") setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
setting_message.name = key setting_message.name = key
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting. setting_message.value = str(value).encode("utf-8")
setting_message.value = self._expandGcodeTokens(key, value, settings)
else:
setting_message.value = str(value).encode("utf-8")
Job.yieldThread() Job.yieldThread()
## Sends for some settings which extruder they should fallback to if not ## Sends for some settings which extruder they should fallback to if not
@ -364,3 +358,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

@ -0,0 +1,462 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
# This parser is intented for interpret the common firmware codes among all the different flavors
class FlavorParser:
def __init__(self):
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._filament_diameter = 2.85
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
@staticmethod
def _getValue(line, code):
n = line.find(code)
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[5] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
extrusion_values = numpy.empty((count, 1), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
extrusion_values[i] = point[4]
if i > 0:
line_feedrates[i - 1] = point[3]
line_types[i - 1] = point[5]
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
else:
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _createEmptyLayer(self, layer_number):
self._layer_data_builder.addLayer(layer_number)
self._layer_data_builder.setLayerHeight(layer_number, 0)
self._layer_data_builder.setLayerThickness(layer_number, 0)
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
# Area of the filament
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
# Length of the extruded filament
de = current_extrusion - previous_extrusion
# Volumne of the extruded filament
dVe = de * Af
# Length of the printed line
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
# When the extruder recovers from a retraction, we get zero distance
if dX == 0:
return 0.1
# Area of the printed line. This area is a rectangle
Ae = dVe / dX
# This area is a rectangle with area equal to layer_thickness * layer_width
line_width = Ae / layer_thickness
# A threshold is set to avoid weird paths in the GCode
if line_width > 1.2:
return 0.35
return line_width
def _gCode0(self, position, params, path):
x, y, z, f, e = position
if self._is_absolute_positioning:
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else z
else:
x += params.x if params.x is not None else 0
y += params.y if params.y is not None else 0
z += params.z if params.z is not None else 0
f = params.f if params.f is not None else f
if params.e is not None:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
position.f,
position.e)
## Set the absolute positioning
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
def _gCode91(self, position, params, path):
self._is_absolute_positioning = False
self._is_absolute_extrusion = False
return position
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position, params, path):
if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
position.e[self._extruder_number] = params.e
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
params.f if params.f is not None else position.f,
position.e)
def processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, f, e = None, None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "F":
f = float(item[1:]) / 60
if item[0] == "E":
e = float(item[1:])
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
self._center_is_zero = True
params = self._position(x, y, z, f, e)
return func(position, params, path)
return position
def processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
def processMCode(self, M, line, position, path):
pass
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def processGCodeFile(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, 0, [0])
current_path = []
min_layer_number = 0
negative_layers = 0
previous_layer = 0
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
# as in ProcessSlicedLayersJob
if layer_number < min_layer_number:
min_layer_number = layer_number
if layer_number < 0:
layer_number += abs(min_layer_number)
negative_layers += 1
else:
layer_number += negative_layers
# In case there is a gap in the layer count, empty layers are created
for empty_layer in range(previous_layer + 1, layer_number):
self._createEmptyLayer(empty_layer)
self._layer_number = layer_number
previous_layer = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
# When find a movement, the new posistion is calculated and added to the current_path, but
# don't need to create a polygon until the end of the layer
current_position = self.processGCode(G, line, current_position, current_path)
continue
# When changing the extruder, the polygon with the stored paths is computed
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self.processTCode(T, line, current_position, current_path)
if line.startswith("M"):
M = self._getInt(line, "M")
self.processMCode(M, line, current_position, current_path)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -1,377 +1,44 @@
# Copyright (c) 2016 Aleph Objects, Inc. # Copyright (c) 2017 Aleph Objects, Inc.
# 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.FileHandler.FileReader import FileReader
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshReader import MeshReader
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
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 . import MarlinFlavorParser, RepRapFlavorParser
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
# Class for loading and parsing G-code files # Class for loading and parsing G-code files
class GCodeReader(MeshReader): class GCodeReader(MeshReader):
_flavor_default = "Marlin"
_flavor_keyword = ";FLAVOR:"
_flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(),
"Marlin" : MarlinFlavorParser.MarlinFlavorParser()}
def __init__(self): def __init__(self):
super(GCodeReader, self).__init__() super(GCodeReader, self).__init__()
self._supported_extensions = [".gcode", ".g"] self._supported_extensions = [".gcode", ".g"]
Application.getInstance().hideMessageSignal.connect(self._onHideMessage) self._flavor_reader = None
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True) Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self): # PreRead is used to get the correct flavor. If not, Marlin is set by default
self._extruder_number = 0 def preRead(self, file_name, *args, **kwargs):
self._layer_type = LayerPolygon.Inset0Type with open(file_name, "r") as file:
self._layer_number = 0 for line in file:
self._previous_z = 0 if line[:len(self._flavor_keyword)] == self._flavor_keyword:
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder() try:
self._center_is_zero = False self._flavor_reader = self._flavor_readers_dict[line[len(self._flavor_keyword):].rstrip()]
return FileReader.PreReadResult.accepted
except:
# If there is no entry in the dictionary for this flavor, just skip and select the by-default flavor
break
@staticmethod # If no flavor is found in the GCode, then we use the by-default
def _getValue(line, code): self._flavor_reader = self._flavor_readers_dict[self._flavor_default]
n = line.find(code) return FileReader.PreReadResult.accepted
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[3] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
# TODO: need to calculate actual line width based on E values
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
if i > 0:
line_types[i - 1] = point[3]
if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _gCode0(self, position, params, path):
x, y, z, e = position
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else position.z
if params.e is not None:
if params.e > e[self._extruder_number]:
path.append([x, y, z, self._layer_type]) # extrusion
else:
path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = params.e
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, LayerPolygon.MoveCombingType])
return self._position(x, y, z, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
0,
position.e)
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position, params, path):
if params.e is not None:
position.e[self._extruder_number] = params.e
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
position.e)
def _processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, e = None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "E":
e = float(item[1:])
if (x is not None and x < 0) or (y is not None and y < 0):
self._center_is_zero = True
params = self._position(x, y, z, e)
return func(position, params, path)
return position
def _processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def read(self, file_name): def read(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name) return self._flavor_reader.processGCodeFile(file_name)
self._cancelled = False
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
last_z = 0
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, [0])
current_path = []
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
last_z = current_position.z
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
self._layer_number = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
current_position = self._processGCode(G, line, current_position, current_path)
# < 2 is a heuristic for a movement only, that should not be counted as a layer
if current_position.z > last_z and abs(current_position.z - last_z) < 2:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
current_path.clear()
if not self._is_layers_in_file:
self._layer_number += 1
continue
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self._processTCode(T, line, current_position, current_path)
# "Flush" leftovers
if not self._is_layers_in_file and len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -0,0 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the Marlin/Sprinter Firmware flavor
class MarlinFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()

View file

@ -0,0 +1,32 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the RepRap Firmware flavor
class RepRapFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()
def processMCode(self, M, line, position, path):
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z, and E to absolute
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
# RepRapFlavor code G91 sets position of X, Y, Z to relative
# For relative E, M83 is used
def _gCode91(self, position, params, path):
self._is_absolute_positioning = False
return position

View file

@ -74,12 +74,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

@ -4,8 +4,7 @@
import os import os
import threading import threading
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QObject from PyQt5.QtCore import Qt, pyqtSignal, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Application import Application from UM.Application import Application
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
@ -81,14 +80,9 @@ class ImageReaderUI(QObject):
def _createConfigUI(self): def _createConfigUI(self):
if self._ui_view is None: if self._ui_view is None:
Logger.log("d", "Creating ImageReader config UI") Logger.log("d", "Creating ImageReader config UI")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")) path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
component = QQmlComponent(Application.getInstance()._engine, path) self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._ui_context.setContextProperty("manager", self)
self._ui_view = component.create(self._ui_context)
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint); self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
self._disable_size_callbacks = False self._disable_size_callbacks = False
@pyqtSlot() @pyqtSlot()
@ -107,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
@ -120,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
@ -132,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
@ -149,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,113 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from UM.Scene.ToolHandle import ToolHandle
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.View.RenderPass import RenderPass
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.Settings.ExtruderManager import ExtruderManager
import os.path
## RenderPass used to display g-code paths.
class LayerPass(RenderPass):
def __init__(self, width, height):
super().__init__("layerview", width, height)
self._layer_shader = None
self._tool_handle_shader = None
self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene()
self._extruder_manager = ExtruderManager.getInstance()
self._layer_view = None
self._compatibility_mode = None
def setLayerView(self, layerview):
self._layer_view = layerview
self._compatibility_mode = layerview.getCompatibilityMode()
def render(self):
if not self._layer_shader:
if self._compatibility_mode:
shader_filename = "layers.shader"
else:
shader_filename = "layers3d.shader"
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), shader_filename))
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if self._layer_view:
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getLayerViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
else:
#defaults
self._layer_shader.setUniformValue("u_layer_view_type", 1)
self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
self._layer_shader.setUniformValue("u_show_travel_moves", 0)
self._layer_shader.setUniformValue("u_show_helpers", 1)
self._layer_shader.setUniformValue("u_show_skin", 1)
self._layer_shader.setUniformValue("u_show_infill", 1)
if not self._tool_handle_shader:
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay)
for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, ToolHandle):
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Render all layers below a certain number as line mesh instead of vertices.
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
start = 0
end = 0
element_counts = layer_data.getElementCounts()
for layer in sorted(element_counts.keys()):
if layer > self._layer_view._current_layer_num:
break
if self._layer_view._minimum_layer_num > layer:
start += element_counts[layer]
end += element_counts[layer]
# This uses glDrawRangeElements internally to only draw a certain range of lines.
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
batch.addItem(node.getWorldTransformation(), layer_data)
batch.render(self._scene.getActiveCamera())
# Create a new batch that is not range-limited
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
if self._layer_view.getCurrentLayerMesh():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
if self._layer_view.getCurrentLayerJumps():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
if len(batch.items) > 0:
batch.render(self._scene.getActiveCamera())
# Render toolhandles on top of the layerview
if len(tool_handle_batch.items) > 0:
tool_handle_batch.render(self._scene.getActiveCamera())
self.release()

View file

@ -1,388 +0,0 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item
{
id: base
width: {
if (UM.LayerView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else {
return UM.Theme.getSize("layerview_menu_size").width;
}
}
height: {
if (UM.LayerView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else {
return UM.Theme.getSize("layerview_menu_size").height + UM.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
}
}
property var buttonTarget: {
if(parent != null)
{
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
}
return Qt.point(0,0)
}
visible: parent != null ? !parent.parent.monitoringPrint: true
UM.PointingRectangle {
id: layerViewMenu
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
height: parent.height
z: slider.z - 1
color: UM.Theme.getColor("tool_panel_background")
borderWidth: UM.Theme.getSize("default_lining").width
borderColor: UM.Theme.getColor("lining")
arrowSize: 0 // hide arrow until weird issue with first time rendering is fixed
ColumnLayout {
id: view_settings
property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
// if we are in compatibility mode, we only show the "line type"
property bool show_legend: UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type") == 1
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("layerview_row_spacing").height
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Label
{
id: layerViewTypesLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Color scheme")
font: UM.Theme.getFont("default");
visible: !UM.LayerView.compatibilityMode
Layout.fillWidth: true
color: UM.Theme.getColor("setting_control_text")
}
ListModel // matches LayerView.py
{
id: layerViewTypes
}
Component.onCompleted:
{
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Material Color"),
type_id: 0
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Line Type"),
type_id: 1 // these ids match the switching in the shader
})
}
ComboBox
{
id: layerTypeCombobox
anchors.left: parent.left
Layout.fillWidth: true
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
model: layerViewTypes
visible: !UM.LayerView.compatibilityMode
style: UM.Theme.styles.combobox
anchors.right: parent.right
anchors.rightMargin: 10 * screenScaleFactor
onActivated:
{
UM.Preferences.setValue("layerview/layer_view_type", index);
}
Component.onCompleted:
{
currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
updateLegends(currentIndex);
}
function updateLegends(type_id)
{
// update visibility of legends
view_settings.show_legend = UM.LayerView.compatibilityMode || (type_id == 1);
}
}
Label
{
id: compatibilityModeLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Compatibility Mode")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
visible: UM.LayerView.compatibilityMode
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
}
Label
{
id: space2Label
anchors.left: parent.left
text: " "
font.pointSize: 0.5
}
Connections {
target: UM.Preferences
onPreferenceChanged:
{
layerTypeCombobox.currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
view_settings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
view_settings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
view_settings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
view_settings.show_skin = UM.Preferences.getValue("layerview/show_skin");
view_settings.show_infill = UM.Preferences.getValue("layerview/show_infill");
view_settings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
view_settings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
}
}
Repeater {
model: Cura.ExtrudersModel{}
CheckBox {
id: extrudersModelCheckBox
checked: view_settings.extruder_opacities[index] > 0.5 || view_settings.extruder_opacities[index] == undefined || view_settings.extruder_opacities[index] == ""
onClicked: {
view_settings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", view_settings.extruder_opacities.join("|"));
}
visible: !UM.LayerView.compatibilityMode
enabled: index + 1 <= 4
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: model.name
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
Repeater {
model: ListModel {
id: typesLegenModel
Component.onCompleted:
{
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Travels"),
initialValue: view_settings.show_travel_moves,
preference: "layerview/show_travel_moves",
colorId: "layerview_move_combing"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Helpers"),
initialValue: view_settings.show_helpers,
preference: "layerview/show_helpers",
colorId: "layerview_support"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Shell"),
initialValue: view_settings.show_skin,
preference: "layerview/show_skin",
colorId: "layerview_inset_0"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Infill"),
initialValue: view_settings.show_infill,
preference: "layerview/show_infill",
colorId: "layerview_infill"
});
}
}
CheckBox {
id: legendModelCheckBox
checked: model.initialValue
onClicked: {
UM.Preferences.setValue(model.preference, checked);
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
CheckBox {
checked: view_settings.only_show_top_layers
onClicked: {
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
}
text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.LayerView.compatibilityMode
style: UM.Theme.styles.checkbox
}
CheckBox {
checked: view_settings.top_layer_count == 5
onClicked: {
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
}
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
visible: UM.LayerView.compatibilityMode
style: UM.Theme.styles.checkbox
}
Repeater {
model: ListModel {
id: typesLegenModelNoCheck
Component.onCompleted:
{
typesLegenModelNoCheck.append({
label: catalog.i18nc("@label", "Top / Bottom"),
colorId: "layerview_skin",
});
typesLegenModelNoCheck.append({
label: catalog.i18nc("@label", "Inner Wall"),
colorId: "layerview_inset_x",
});
}
}
Label {
text: label
visible: view_settings.show_legend
id: typesLegendModelLabel
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
}
}
}
LayerSlider {
id: slider
width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height
right: layerViewMenu.right
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
}
// custom properties
upperValue: UM.LayerView.currentLayer
lowerValue: UM.LayerView.minimumLayer
maximumValue: UM.LayerView.numLayers
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
upperHandleColor: UM.Theme.getColor("slider_handle")
lowerHandleColor: UM.Theme.getColor("slider_handle")
rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
layersVisible: UM.LayerView.layerActivity && CuraApplication.platformActivity ? true : false
// update values when layer data changes
Connections {
target: UM.LayerView
onMaxLayersChanged: slider.setUpperValue(UM.LayerView.currentLayer)
onMinimumLayerChanged: slider.setLowerValue(UM.LayerView.minimumLayer)
onCurrentLayerChanged: slider.setUpperValue(UM.LayerView.currentLayer)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
slider.setLowerValue(UM.LayerView.minimumLayer)
slider.setUpperValue(UM.LayerView.currentLayer)
}
}
}
FontMetrics {
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View file

@ -1,151 +0,0 @@
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
import LayerView
class LayerViewProxy(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
currentLayerChanged = pyqtSignal()
maxLayersChanged = pyqtSignal()
activityChanged = pyqtSignal()
globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getActivity()
@pyqtProperty(int, notify=maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getMaxLayers()
@pyqtProperty(int, notify=currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getCurrentLayer()
@pyqtProperty(int, notify=currentLayerChanged)
def minimumLayer(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getMinimumLayer()
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.isBusy()
return False
@pyqtProperty(bool, notify=preferencesChanged)
def compatibilityMode(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getCompatibilityMode()
return False
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setLayer(layer_num)
@pyqtSlot(int)
def setMinimumLayer(self, layer_num):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setMinimumLayer(layer_num)
@pyqtSlot(int)
def setLayerViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setLayerViewType(layer_view_type)
@pyqtSlot(result=int)
def getLayerViewType(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getLayerViewType()
return 0
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setExtruderOpacity(extruder_nr, opacity)
@pyqtSlot(int)
def setShowTravelMoves(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowTravelMoves(show)
@pyqtSlot(int)
def setShowHelpers(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowHelpers(show)
@pyqtSlot(int)
def setShowSkin(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowSkin(show)
@pyqtSlot(int)
def setShowInfill(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowInfill(show)
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getExtruderCount()
return 0
def _layerActivityChanged(self):
self.activityChanged.emit()
def _onLayerChanged(self):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onMaxLayersChanged(self):
self.maxLayersChanged.emit()
def _onBusyChanged(self):
self.busyChanged.emit()
def _onGlobalStackChanged(self):
self.globalStackChanged.emit()
def _onPreferencesChanged(self):
self.preferencesChanged.emit()
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.busyChanged.connect(self._onBusyChanged)
active_view.globalStackChanged.connect(self._onGlobalStackChanged)
active_view.preferencesChanged.connect(self._onPreferencesChanged)

View file

@ -1,25 +0,0 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import LayerView, LayerViewProxy
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layer view"),
"view_panel": "LayerView.qml",
"weight": 2
}
}
def createLayerViewProxy(engine, script_engine):
return LayerViewProxy.LayerViewProxy()
def register(app):
layer_view = LayerView.LayerView()
qmlRegisterSingletonType(LayerViewProxy.LayerViewProxy, "UM", 1, 0, "LayerView", layer_view.getProxy)
return { "view": LayerView.LayerView() }

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.
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.
@ -80,8 +81,7 @@ class LegacyProfileReader(ProfileReader):
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
@ -121,7 +121,7 @@ class LegacyProfileReader(ProfileReader):
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.getBottom()
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 +138,25 @@ 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")
# We need to downgrade the container to version 1 (in Cura 2.1) so the upgrade system can correctly upgrade
# it to the latest version.
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.setDirty(True)
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)
return profile

View file

@ -116,8 +116,7 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int) @pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count): def setMachineExtruderCount(self, extruder_count):
machine_manager = Application.getInstance().getMachineManager() extruder_manager = Application.getInstance().getExtruderManager()
extruder_manager = ExtruderManager.getInstance()
definition_changes_container = self._global_container_stack.definitionChanges definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_container: if not self._global_container_stack or definition_changes_container == self._empty_container:
@ -127,34 +126,6 @@ class MachineSettingsAction(MachineAction):
if extruder_count == previous_extruder_count: if extruder_count == previous_extruder_count:
return return
extruder_material_id = None
extruder_variant_id = None
if extruder_count == 1:
# Get the material and variant of the first extruder before setting the number extruders to 1
if machine_manager.hasMaterials:
extruder_material_id = machine_manager.allActiveMaterialIds[extruder_manager.extruderIds["0"]]
if machine_manager.hasVariants:
extruder_variant_id = machine_manager.allActiveVariantIds[extruder_manager.extruderIds["0"]]
# Copy any settable_per_extruder setting value from the extruders to the global stack
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
extruder_stacks.reverse() # make sure the first extruder is done last, so its settings override any higher extruder settings
global_user_container = self._global_container_stack.getTop()
for extruder_stack in extruder_stacks:
extruder_index = extruder_stack.getMetaDataEntry("position")
extruder_user_container = extruder_stack.getTop()
for setting_instance in extruder_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = self._global_container_stack.getProperty(setting_key, "limit_to_extruder")
if limit_to_extruder == "-1" or limit_to_extruder == extruder_index:
global_user_container.setProperty(setting_key, "value", extruder_user_container.getProperty(setting_key, "value"))
extruder_user_container.removeInstance(setting_key)
# reset all extruder number settings whose value is no longer valid # reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances(): for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
@ -177,52 +148,29 @@ class MachineSettingsAction(MachineAction):
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
if extruder_count > 1: # Make sure one of the extruder stacks is active
# Multiextrusion extruder_manager.setActiveExtruderIndex(0)
# Make sure one of the extruder stacks is active # Move settable_per_extruder values out of the global container
if extruder_manager.activeExtruderIndex == -1: # After CURA-4482 this should not be the case anymore, but we still want to support older project files.
extruder_manager.setActiveExtruderIndex(0) global_user_container = self._global_container_stack.getTop()
# Move settable_per_extruder values out of the global container if previous_extruder_count == 1:
if previous_extruder_count == 1: extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() global_user_container = self._global_container_stack.getTop()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances(): for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
else:
# Single extrusion
# Make sure the machine stack is active if settable_per_extruder:
if extruder_manager.activeExtruderIndex > -1: limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_manager.setActiveExtruderIndex(-1) extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
# Restore material and variant on global stack global_user_container.removeInstance(setting_key)
# MachineManager._onGlobalContainerChanged removes the global material and variant of multiextruder machines
if extruder_material_id or extruder_variant_id:
# Prevent the DiscardOrKeepProfileChangesDialog from popping up (twice) if there are user changes
# The dialog is not relevant here, since we're restoring the previous situation as good as possible
preferences = Preferences.getInstance()
choice_on_profile_override = preferences.getValue("cura/choice_on_profile_override")
preferences.setValue("cura/choice_on_profile_override", "always_keep")
if extruder_material_id:
machine_manager.setActiveMaterial(extruder_material_id)
if extruder_variant_id:
machine_manager.setActiveVariant(extruder_variant_id)
preferences.setValue("cura/choice_on_profile_override", choice_on_profile_override)
self.forceUpdate() self.forceUpdate()
@pyqtSlot() @pyqtSlot()
def forceUpdate(self): def forceUpdate(self):
# Force rebuilding the build volume by reloading the global container stack. # Force rebuilding the build volume by reloading the global container stack.
@ -240,29 +188,32 @@ class MachineSettingsAction(MachineAction):
# In other words: only continue for the UM2 (extended), but not for the UM2+ # In other words: only continue for the UM2 (extended), but not for the UM2+
return return
stacks = ExtruderManager.getInstance().getExtruderStacks()
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode" has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_container = self._global_container_stack.material
if has_materials: if has_materials:
if "has_materials" in self._global_container_stack.getMetaData(): if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.setMetaDataEntry("has_materials", True) self._global_container_stack.setMetaDataEntry("has_materials", True)
else: else:
self._global_container_stack.addMetaDataEntry("has_materials", True) self._global_container_stack.addMetaDataEntry("has_materials", True)
# Set the material container to a sane default # Set the material container for each extruder to a sane default
if material_container == self._empty_container: for stack in stacks:
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")} material_container = stack.material
materials = self._container_registry.findInstanceContainers(**search_criteria) if material_container == self._empty_container:
if materials: machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
self._global_container_stack.material = materials[0] search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
stack.material = materials[0]
else: else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only. # The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in self._global_container_stack.getMetaData(): if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials") self._global_container_stack.removeMetaDataEntry("has_materials")
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer() for stack in stacks:
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit() Application.getInstance().globalContainerStackChanged.emit()
@ -275,16 +226,13 @@ 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
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") material = ExtruderManager.getInstance().getActiveExtruderStack().material
if machine_extruder_count > 1:
material = ExtruderManager.getInstance().getActiveExtruderStack().material
else:
material = self._global_container_stack.material
material_diameter = material.getProperty("material_diameter", "value") material_diameter = material.getProperty("material_diameter", "value")
if not material_diameter: # in case of "empty" material if not material_diameter:
# 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 definition_changes = self._global_container_stack.definitionChanges
machine_diameter = definition_changes.getProperty("material_diameter", "value") machine_diameter = definition_changes.getProperty("material_diameter", "value")
if not machine_diameter: if not machine_diameter:
@ -294,10 +242,7 @@ class MachineSettingsAction(MachineAction):
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.")
if machine_extruder_count > 1: stacks = ExtruderManager.getInstance().getExtruderStacks()
stacks = ExtruderManager.getInstance().getExtruderStacks()
else:
stacks = [self._global_container_stack]
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()
@ -338,7 +283,7 @@ class MachineSettingsAction(MachineAction):
search_criteria["id"] = stack.getMetaDataEntry("preferred_material") search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria) materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials: if not materials:
# Preferrd material with new diameter is not found, search for any material # Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None) search_criteria.pop("id", None)
materials = self._container_registry.findInstanceContainers(**search_criteria) materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials: if not materials:

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
@ -283,6 +282,7 @@ Cura.MachineAction
Loader Loader
{ {
id: materialDiameterField id: materialDiameterField
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter" property string settingKey: "material_diameter"
property string unit: catalog.i18nc("@label", "mm") property string unit: catalog.i18nc("@label", "mm")
@ -290,15 +290,6 @@ Cura.MachineAction
property var afterOnEditingFinished: manager.updateMaterialForDiameter property var afterOnEditingFinished: manager.updateMaterialForDiameter
property string label: catalog.i18nc("@label", "Material diameter") 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 +346,7 @@ Cura.MachineAction
if(currentIndex > 0) if(currentIndex > 0)
{ {
contentItem.forceActiveFocus(); contentItem.forceActiveFocus();
ExtruderManager.setActiveExtruderIndex(currentIndex - 1); Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
} }
} }
@ -585,11 +576,11 @@ Cura.MachineAction
propertyProvider.setPropertyValue("value", text); propertyProvider.setPropertyValue("value", text);
if(_forceUpdateOnChange) if(_forceUpdateOnChange)
{ {
var extruderIndex = ExtruderManager.activeExtruderIndex; var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
manager.forceUpdate(); manager.forceUpdate();
if(ExtruderManager.activeExtruderIndex != extruderIndex) if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
{ {
ExtruderManager.setActiveExtruderIndex(extruderIndex) Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
} }
} }
if(_afterOnEditingFinished) if(_afterOnEditingFinished)

View file

@ -0,0 +1,44 @@
// 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
{
width: parent.width
height: parent.height
// 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,73 @@
# 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._setComponents)
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(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 _setIconSource(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):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
return "tab_status_connected"
if output_device.jobState == "paused":
return "tab_status_paused"
if output_device.jobState == "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

@ -26,17 +26,92 @@ 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
}
ComboBox
{
id: meshTypeSelection
style: UM.Theme.styles.combobox
onActivated: {
UM.ActiveTool.setProperty("MeshType", model.get(index).type)
}
model: ListModel
{
id: meshTypeModel
Component.onCompleted:
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypeModel.append({
type: "support_mesh",
text: catalog.i18nc("@label", "Print as support")
});
meshTypeModel.append({
type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models")
});
meshTypeModel.append({
type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
});
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");
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: 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: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
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
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
ListView ListView
@ -49,6 +124,7 @@ Item {
id: addedSettingsModel; id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ] expanded: [ "*" ]
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
visibilityHandler: Cura.PerObjectSettingVisibilityHandler visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{ {
@ -58,6 +134,7 @@ Item {
delegate: Row delegate: Row
{ {
spacing: - UM.Theme.getSize("default_margin").width
Loader Loader
{ {
id: settingLoader id: settingLoader
@ -68,6 +145,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 +190,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 +203,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 +279,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,21 +301,12 @@ 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: settingPickDialog.visible = true;
Connections
{
target: UM.Preferences;
onPreferenceChanged:
{
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
}
}
} }
} }
@ -325,7 +394,7 @@ Item {
} }
visibilityHandler: UM.SettingPreferenceVisibilityHandler {} visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ] expanded: [ "*" ]
exclude: [ "machine_settings", "command_line_settings" ] exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
} }
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
@ -78,31 +112,26 @@ class PerObjectSettingsTool(Tool):
def _onGlobalContainerChanged(self): def _onGlobalContainerChanged(self):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
# used for enabling or disabling per extruder settings per object
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
# Ensure that all extruder data is reset extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
if not self._multi_extrusion:
default_stack_id = global_container_stack.getId()
else:
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
if default_stack:
default_stack_id = default_stack.getId()
else:
default_stack_id = global_container_stack.getId()
root_node = Application.getInstance().getController().getScene().getRoot() if extruder_stack:
for node in DepthFirstIterator(root_node): root_node = Application.getInstance().getController().getScene().getRoot()
new_stack_id = default_stack_id for node in DepthFirstIterator(root_node):
# Get position of old extruder stack for this node new_stack_id = extruder_stack.getId()
old_extruder_pos = node.callDecoration("getActiveExtruderPosition") # Get position of old extruder stack for this node
if old_extruder_pos is not None: old_extruder_pos = node.callDecoration("getActiveExtruderPosition")
# Fetch current (new) extruder stack at position if old_extruder_pos is not None:
new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos) # Fetch current (new) extruder stack at position
if new_stack: new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos)
new_stack_id = new_stack.getId() if new_stack:
node.callDecoration("setActiveExtruder", new_stack_id) new_stack_id = new_stack.getId()
node.callDecoration("setActiveExtruder", new_stack_id)
self._updateEnabled() self._updateEnabled()
def _updateEnabled(self): def _updateEnabled(self):
selected_objects = Selection.getAllSelectedObjects() selected_objects = Selection.getAllSelectedObjects()

View file

@ -11,7 +11,6 @@ from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
import json import json
import os import os
@ -39,8 +38,6 @@ class PluginBrowser(QObject, Extension):
self._plugins_metadata = [] self._plugins_metadata = []
self._plugins_model = None self._plugins_model = None
self._qml_component = None
self._qml_context = None
self._dialog = None self._dialog = None
self._download_progress = 0 self._download_progress = 0
@ -111,17 +108,8 @@ class PluginBrowser(QObject, Extension):
def _createDialog(self, qml_name): def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name) Logger.log("d", "Creating dialog [%s]", qml_name)
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)) dialog = Application.getInstance().createQmlComponent(path, {"manager": self})
self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't)
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("manager", self)
dialog = self._qml_component.create(self._qml_context)
if dialog is None:
Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
return dialog return dialog
def setIsDownloading(self, is_downloading): def setIsDownloading(self, is_downloading):

View file

@ -0,0 +1,18 @@
# 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.Resources import Resources
from cura.Stages.CuraStage import CuraStage
## Stage for preparing model (slicing).
class PrepareStage(CuraStage):
def __init__(self, parent = None):
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self):
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)

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 PrepareStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Prepare"),
"weight": 0
}
}
def register(app):
return {
"stage": PrepareStage.PrepareStage()
}

View file

@ -0,0 +1,8 @@
{
"name": "Prepare Stage",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a prepare stage in Cura.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -1,312 +1,325 @@
// 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 QtQuick 2.2 import QtQuick 2.2
import QtQuick.Controls 1.2 import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1 import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM import UM 1.0 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
Item { Item {
id: sliderRoot id: sliderRoot
// handle properties // handle properties
property real handleSize: 10 property real handleSize: 10
property real handleRadius: handleSize / 2 property real handleRadius: handleSize / 2
property real minimumRangeHandleSize: handleSize / 2 property real minimumRangeHandleSize: handleSize / 2
property color upperHandleColor: "black" property color upperHandleColor: "black"
property color lowerHandleColor: "black" property color lowerHandleColor: "black"
property color rangeHandleColor: "black" property color rangeHandleColor: "black"
property real handleLabelWidth: width property color handleActiveColor: "white"
property var activeHandle: upperHandle property real handleLabelWidth: width
property var activeHandle: upperHandle
// track properties
property real trackThickness: 4 // width of the slider track // track properties
property real trackRadius: trackThickness / 2 property real trackThickness: 4 // width of the slider track
property color trackColor: "white" property real trackRadius: trackThickness / 2
property real trackBorderWidth: 1 // width of the slider track border property color trackColor: "white"
property color trackBorderColor: "black" property real trackBorderWidth: 1 // width of the slider track border
property color trackBorderColor: "black"
// value properties
property real maximumValue: 100 // value properties
property real minimumValue: 0 property real maximumValue: 100
property real minimumRange: 0 // minimum range allowed between min and max values property real minimumValue: 0
property bool roundValues: true property real minimumRange: 0 // minimum range allowed between min and max values
property real upperValue: maximumValue property bool roundValues: true
property real lowerValue: minimumValue property real upperValue: maximumValue
property real lowerValue: minimumValue
property bool layersVisible: true
property bool layersVisible: true
function getUpperValueFromSliderHandle () {
return upperHandle.getValue() function getUpperValueFromSliderHandle () {
} return upperHandle.getValue()
}
function setUpperValue (value) {
upperHandle.setValue(value) function setUpperValue (value) {
updateRangeHandle() upperHandle.setValue(value)
} updateRangeHandle()
}
function getLowerValueFromSliderHandle () {
return lowerHandle.getValue() function getLowerValueFromSliderHandle () {
} return lowerHandle.getValue()
}
function setLowerValue (value) {
lowerHandle.setValue(value) function setLowerValue (value) {
updateRangeHandle() lowerHandle.setValue(value)
} updateRangeHandle()
}
function updateRangeHandle () {
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height) function updateRangeHandle () {
} rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
}
// set the active handle to show only one label at a time
function setActiveHandle (handle) { // set the active handle to show only one label at a time
activeHandle = handle function setActiveHandle (handle) {
} activeHandle = handle
}
// slider track
Rectangle { // slider track
id: track Rectangle {
id: track
width: sliderRoot.trackThickness
height: sliderRoot.height - sliderRoot.handleSize width: sliderRoot.trackThickness
radius: sliderRoot.trackRadius height: sliderRoot.height - sliderRoot.handleSize
anchors.centerIn: sliderRoot radius: sliderRoot.trackRadius
color: sliderRoot.trackColor anchors.centerIn: sliderRoot
border.width: sliderRoot.trackBorderWidth color: sliderRoot.trackColor
border.color: sliderRoot.trackBorderColor border.width: sliderRoot.trackBorderWidth
visible: sliderRoot.layersVisible border.color: sliderRoot.trackBorderColor
} visible: sliderRoot.layersVisible
}
// Range handle
Item { // Range handle
id: rangeHandle Item {
id: rangeHandle
y: upperHandle.y + upperHandle.height
width: sliderRoot.handleSize y: upperHandle.y + upperHandle.height
height: sliderRoot.minimumRangeHandleSize width: sliderRoot.handleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter height: sliderRoot.minimumRangeHandleSize
visible: sliderRoot.layersVisible anchors.horizontalCenter: sliderRoot.horizontalCenter
visible: sliderRoot.layersVisible
// set the new value when dragging
function onHandleDragged () { // set the new value when dragging
function onHandleDragged () {
upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height
var upperValue = sliderRoot.getUpperValueFromSliderHandle()
var lowerValue = sliderRoot.getLowerValueFromSliderHandle() var upperValue = sliderRoot.getUpperValueFromSliderHandle()
var lowerValue = sliderRoot.getLowerValueFromSliderHandle()
// set both values after moving the handle position
UM.LayerView.setCurrentLayer(upperValue) // set both values after moving the handle position
UM.LayerView.setMinimumLayer(lowerValue) UM.SimulationView.setCurrentLayer(upperValue)
} UM.SimulationView.setMinimumLayer(lowerValue)
}
function setValue (value) {
var range = sliderRoot.upperValue - sliderRoot.lowerValue function setValue (value) {
value = Math.min(value, sliderRoot.maximumValue) var range = sliderRoot.upperValue - sliderRoot.lowerValue
value = Math.max(value, sliderRoot.minimumValue + range) value = Math.min(value, sliderRoot.maximumValue)
value = Math.max(value, sliderRoot.minimumValue + range)
UM.LayerView.setCurrentLayer(value)
UM.LayerView.setMinimumLayer(value - range) UM.SimulationView.setCurrentLayer(value)
} UM.SimulationView.setMinimumLayer(value - range)
}
Rectangle {
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth Rectangle {
height: parent.height + sliderRoot.handleSize width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
anchors.centerIn: parent height: parent.height + sliderRoot.handleSize
color: sliderRoot.rangeHandleColor anchors.centerIn: parent
} color: sliderRoot.rangeHandleColor
}
MouseArea {
anchors.fill: parent MouseArea {
anchors.fill: parent
drag {
target: parent drag {
axis: Drag.YAxis target: parent
minimumY: upperHandle.height axis: Drag.YAxis
maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height) minimumY: upperHandle.height
} maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height)
}
onPositionChanged: parent.onHandleDragged()
onPressed: sliderRoot.setActiveHandle(rangeHandle) onPositionChanged: parent.onHandleDragged()
} onPressed: sliderRoot.setActiveHandle(rangeHandle)
}
LayerSliderLabel {
id: rangleHandleLabel SimulationSliderLabel {
id: rangleHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
anchors.verticalCenter: parent.verticalCenter x: parent.x - width - UM.Theme.getSize("default_margin").width
target: Qt.point(sliderRoot.width, y + height / 2) anchors.verticalCenter: parent.verticalCenter
visible: sliderRoot.activeHandle == parent target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue // custom properties
value: sliderRoot.upperValue maximumValue: sliderRoot.maximumValue
busy: UM.LayerView.busy value: sliderRoot.upperValue
setValue: rangeHandle.setValue // connect callback functions busy: UM.SimulationView.busy
} setValue: rangeHandle.setValue // connect callback functions
} }
}
// Upper handle
Rectangle { // Upper handle
id: upperHandle Rectangle {
id: upperHandle
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
width: sliderRoot.handleSize y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
height: sliderRoot.handleSize width: sliderRoot.handleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter height: sliderRoot.handleSize
radius: sliderRoot.handleRadius anchors.horizontalCenter: sliderRoot.horizontalCenter
color: sliderRoot.upperHandleColor radius: sliderRoot.handleRadius
visible: sliderRoot.layersVisible color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
visible: sliderRoot.layersVisible
function onHandleDragged () {
function onHandleDragged () {
// don't allow the lower handle to be heigher than the upper handle
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) { // don't allow the lower handle to be heigher than the upper handle
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
} lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
}
// update the range handle
sliderRoot.updateRangeHandle() // update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.LayerView.setCurrentLayer(getValue()) // set the new value after moving the handle position
} UM.SimulationView.setCurrentLayer(getValue())
}
// get the upper value based on the slider position
function getValue () { // get the upper value based on the slider position
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) function getValue () {
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue)) var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
result = sliderRoot.roundValues ? Math.round(result) : result result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
return result result = sliderRoot.roundValues ? Math.round(result) : result
} return result
}
// set the slider position based on the upper value
function setValue (value) { // set the slider position based on the upper value
function setValue (value) {
UM.LayerView.setCurrentLayer(value)
UM.SimulationView.setCurrentLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))) var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
y = newUpperYPosition var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newUpperYPosition
// update the range handle
sliderRoot.updateRangeHandle() // update the range handle
} sliderRoot.updateRangeHandle()
}
// dragging
MouseArea { Keys.onUpPressed: upperHandleLabel.setValue(upperHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
anchors.fill: parent Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
drag { // dragging
target: parent MouseArea {
axis: Drag.YAxis anchors.fill: parent
minimumY: 0
maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) drag {
} target: parent
axis: Drag.YAxis
onPositionChanged: parent.onHandleDragged() minimumY: 0
onPressed: sliderRoot.setActiveHandle(upperHandle) maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)
} }
LayerSliderLabel { onPositionChanged: parent.onHandleDragged()
id: upperHandleLabel onPressed: {
sliderRoot.setActiveHandle(upperHandle)
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height upperHandleLabel.forceActiveFocus()
x: parent.x - width - UM.Theme.getSize("default_margin").width }
anchors.verticalCenter: parent.verticalCenter }
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent SimulationSliderLabel {
id: upperHandleLabel
// custom properties
maximumValue: sliderRoot.maximumValue height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
value: sliderRoot.upperValue x: parent.x - width - UM.Theme.getSize("default_margin").width
busy: UM.LayerView.busy anchors.verticalCenter: parent.verticalCenter
setValue: upperHandle.setValue // connect callback functions target: Qt.point(sliderRoot.width, y + height / 2)
} visible: sliderRoot.activeHandle == parent
}
// custom properties
// Lower handle maximumValue: sliderRoot.maximumValue
Rectangle { value: sliderRoot.upperValue
id: lowerHandle busy: UM.SimulationView.busy
setValue: upperHandle.setValue // connect callback functions
y: sliderRoot.height - sliderRoot.handleSize }
width: parent.handleSize }
height: parent.handleSize
anchors.horizontalCenter: parent.horizontalCenter // Lower handle
radius: sliderRoot.handleRadius Rectangle {
color: sliderRoot.lowerHandleColor id: lowerHandle
visible: slider.layersVisible y: sliderRoot.height - sliderRoot.handleSize
width: parent.handleSize
function onHandleDragged () { height: parent.handleSize
anchors.horizontalCenter: parent.horizontalCenter
// don't allow the upper handle to be lower than the lower handle radius: sliderRoot.handleRadius
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) { color: lowerHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.lowerHandleColor
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
} visible: sliderRoot.layersVisible
// update the range handle function onHandleDragged () {
sliderRoot.updateRangeHandle()
// don't allow the upper handle to be lower than the lower handle
// set the new value after moving the handle position if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
UM.LayerView.setMinimumLayer(getValue()) upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
} }
// get the lower value from the current slider position // update the range handle
function getValue () { sliderRoot.updateRangeHandle()
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange)) // set the new value after moving the handle position
result = sliderRoot.roundValues ? Math.round(result) : result UM.SimulationView.setMinimumLayer(getValue())
return result }
}
// get the lower value from the current slider position
// set the slider position based on the lower value function getValue () {
function setValue (value) { var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
UM.LayerView.setMinimumLayer(value) result = sliderRoot.roundValues ? Math.round(result) : result
return result
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue) }
var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newLowerYPosition // set the slider position based on the lower value
function setValue (value) {
// update the range handle
sliderRoot.updateRangeHandle() UM.SimulationView.setMinimumLayer(value)
}
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
// dragging var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
MouseArea { y = newLowerYPosition
anchors.fill: parent
// update the range handle
drag { sliderRoot.updateRangeHandle()
target: parent }
axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize Keys.onUpPressed: lowerHandleLabel.setValue(lowerHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
maximumY: sliderRoot.height - parent.height Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
}
// dragging
onPositionChanged: parent.onHandleDragged() MouseArea {
onPressed: sliderRoot.setActiveHandle(lowerHandle) anchors.fill: parent
}
drag {
LayerSliderLabel { target: parent
id: lowerHandleLabel axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height maximumY: sliderRoot.height - parent.height
x: parent.x - width - UM.Theme.getSize("default_margin").width }
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2) onPositionChanged: parent.onHandleDragged()
visible: sliderRoot.activeHandle == parent onPressed: {
sliderRoot.setActiveHandle(lowerHandle)
// custom properties lowerHandleLabel.forceActiveFocus()
maximumValue: sliderRoot.maximumValue }
value: sliderRoot.lowerValue }
busy: UM.LayerView.busy
setValue: lowerHandle.setValue // connect callback functions SimulationSliderLabel {
} id: lowerHandleLabel
}
} height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.lowerValue
busy: UM.SimulationView.busy
setValue: lowerHandle.setValue // connect callback functions
}
}
}

View file

@ -0,0 +1,49 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Math.Color import Color
from UM.Math.Vector import Vector
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode
from UM.View.GL.OpenGL import OpenGL
from UM.Resources import Resources
import os
class NozzleNode(SceneNode):
def __init__(self, parent = None):
super().__init__(parent)
self._shader = None
self.setCalculateBoundingBox(False)
self._createNozzleMesh()
def _createNozzleMesh(self):
mesh_file = "resources/nozzle.stl"
try:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), mesh_file)
except FileNotFoundError:
path = ""
reader = Application.getInstance().getMeshFileHandler().getReaderForFile(path)
node = reader.read(path)
if node.getMeshData():
self.setMeshData(node.getMeshData())
def render(self, renderer):
# Avoid to render if it is not visible
if not self.isVisible():
return False
if not self._shader:
# We now misuse the platform shader, as it actually supports textures
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
# Set the opacity to 0, so that the template is in full control.
self._shader.setUniformValue("u_opacity", 0)
if self.getMeshData():
renderer.queueNode(self, shader = self._shader, transparent = True)
return True

View file

@ -0,0 +1,161 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
id: sliderRoot
// handle properties
property real handleSize: 10
property real handleRadius: handleSize / 2
property color handleColor: "black"
property color handleActiveColor: "white"
property color rangeColor: "black"
property real handleLabelWidth: width
// track properties
property real trackThickness: 4 // width of the slider track
property real trackRadius: trackThickness / 2
property color trackColor: "white"
property real trackBorderWidth: 1 // width of the slider track border
property color trackBorderColor: "black"
// value properties
property real maximumValue: 100
property bool roundValues: true
property real handleValue: maximumValue
property bool pathsVisible: true
function getHandleValueFromSliderHandle () {
return handle.getValue()
}
function setHandleValue (value) {
handle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle () {
rangeHandle.width = handle.x - sliderRoot.handleSize
}
// slider track
Rectangle {
id: track
width: sliderRoot.width - sliderRoot.handleSize
height: sliderRoot.trackThickness
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
border.width: sliderRoot.trackBorderWidth
border.color: sliderRoot.trackBorderColor
visible: sliderRoot.pathsVisible
}
// Progress indicator
Item {
id: rangeHandle
x: handle.width
height: sliderRoot.handleSize
width: handle.x - sliderRoot.handleSize
anchors.verticalCenter: sliderRoot.verticalCenter
visible: sliderRoot.pathsVisible
Rectangle {
height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
width: parent.width + sliderRoot.handleSize
anchors.centerIn: parent
color: sliderRoot.rangeColor
}
}
// Handle
Rectangle {
id: handle
x: sliderRoot.handleSize
width: sliderRoot.handleSize
height: sliderRoot.handleSize
anchors.verticalCenter: sliderRoot.verticalCenter
radius: sliderRoot.handleRadius
color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
visible: sliderRoot.pathsVisible
function onHandleDragged () {
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.SimulationView.setCurrentPath(getValue())
}
// get the value based on the slider position
function getValue () {
var result = x / (sliderRoot.width - sliderRoot.handleSize)
result = result * sliderRoot.maximumValue
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the value
function setValue (value) {
UM.SimulationView.setCurrentPath(value)
var diff = value / sliderRoot.maximumValue
var newXPosition = Math.round(diff * (sliderRoot.width - sliderRoot.handleSize))
x = newXPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
Keys.onRightPressed: handleLabel.setValue(handleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.XAxis
minimumX: 0
maximumX: sliderRoot.width - sliderRoot.handleSize
}
onPressed: {
handleLabel.forceActiveFocus()
}
onPositionChanged: parent.onHandleDragged()
}
SimulationSliderLabel {
id: handleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
y: parent.y + sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
target: Qt.point(x + width / 2, sliderRoot.height)
visible: false
startFrom: 0
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.handleValue
busy: UM.SimulationView.busy
setValue: handle.setValue // connect callback functions
}
}
}

View file

@ -0,0 +1,190 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Math.Color import Color
from UM.Math.Vector import Vector
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from UM.Scene.ToolHandle import ToolHandle
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.View.RenderPass import RenderPass
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.Settings.ExtruderManager import ExtruderManager
import os.path
## RenderPass used to display g-code paths.
from .NozzleNode import NozzleNode
class SimulationPass(RenderPass):
def __init__(self, width, height):
super().__init__("simulationview", width, height)
self._layer_shader = None
self._layer_shadow_shader = None
self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
self._tool_handle_shader = None
self._nozzle_shader = None
self._old_current_layer = 0
self._old_current_path = 0
self._switching_layers = True # It tracks when the user is moving the layers' slider
self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene()
self._extruder_manager = ExtruderManager.getInstance()
self._layer_view = None
self._compatibility_mode = None
def setSimulationView(self, layerview):
self._layer_view = layerview
self._compatibility_mode = layerview.getCompatibilityMode()
def render(self):
if not self._layer_shader:
if self._compatibility_mode:
shader_filename = "layers.shader"
shadow_shader_filename = "layers_shadow.shader"
else:
shader_filename = "layers3d.shader"
shadow_shader_filename = "layers3d_shadow.shader"
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
self._current_shader = self._layer_shader
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if self._layer_view:
self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness())
self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
else:
#defaults
self._layer_shader.setUniformValue("u_max_feedrate", 1)
self._layer_shader.setUniformValue("u_min_feedrate", 0)
self._layer_shader.setUniformValue("u_max_thickness", 1)
self._layer_shader.setUniformValue("u_min_thickness", 0)
self._layer_shader.setUniformValue("u_layer_view_type", 1)
self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
self._layer_shader.setUniformValue("u_show_travel_moves", 0)
self._layer_shader.setUniformValue("u_show_helpers", 1)
self._layer_shader.setUniformValue("u_show_skin", 1)
self._layer_shader.setUniformValue("u_show_infill", 1)
if not self._tool_handle_shader:
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
if not self._nozzle_shader:
self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
head_position = None # Indicates the current position of the print head
nozzle_node = None
for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, ToolHandle):
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
elif isinstance(node, NozzleNode):
nozzle_node = node
nozzle_node.setVisible(False)
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Render all layers below a certain number as line mesh instead of vertices.
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
start = 0
end = 0
element_counts = layer_data.getElementCounts()
for layer in sorted(element_counts.keys()):
# In the current layer, we show just the indicated paths
if layer == self._layer_view._current_layer_num:
# We look for the position of the head, searching the point of the current path
index = self._layer_view._current_path_num
offset = 0
for polygon in layer_data.getLayer(layer).polygons:
# The size indicates all values in the two-dimension array, and the second dimension is
# always size 3 because we have 3D points.
if index >= polygon.data.size // 3 - offset:
index -= polygon.data.size // 3 - offset
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
continue
# The head position is calculated and translated
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
break
break
if self._layer_view._minimum_layer_num > layer:
start += element_counts[layer]
end += element_counts[layer]
# Calculate the range of paths in the last layer
current_layer_start = end
current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice
# This uses glDrawRangeElements internally to only draw a certain range of lines.
# All the layers but the current selected layer are rendered first
if self._old_current_path != self._layer_view._current_path_num:
self._current_shader = self._layer_shadow_shader
self._switching_layers = False
if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num:
self._current_shader = self._layer_shader
self._switching_layers = True
layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end), backface_cull = True)
layers_batch.addItem(node.getWorldTransformation(), layer_data)
layers_batch.render(self._scene.getActiveCamera())
# Current selected layer is rendered
current_layer_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (current_layer_start, current_layer_end))
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
current_layer_batch.render(self._scene.getActiveCamera())
self._old_current_layer = self._layer_view._current_layer_num
self._old_current_path = self._layer_view._current_path_num
# Create a new batch that is not range-limited
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
if self._layer_view.getCurrentLayerMesh():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
if self._layer_view.getCurrentLayerJumps():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
if len(batch.items) > 0:
batch.render(self._scene.getActiveCamera())
# The nozzle is drawn when once we know the correct position of the head,
# but the user is not using the layer slider, and the compatibility mode is not enabled
if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
if head_position is not None:
nozzle_node.setVisible(True)
nozzle_node.setPosition(head_position)
nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
nozzle_batch.render(self._scene.getActiveCamera())
# Render toolhandles on top of the layerview
if len(tool_handle_batch.items) > 0:
tool_handle_batch.render(self._scene.getActiveCamera())
self.release()

View file

@ -1,103 +1,104 @@
// 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 QtQuick 2.2 import QtQuick 2.2
import QtQuick.Controls 1.2 import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1 import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM import UM 1.0 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
UM.PointingRectangle { UM.PointingRectangle {
id: sliderLabelRoot id: sliderLabelRoot
// custom properties // custom properties
property real maximumValue: 100 property real maximumValue: 100
property real value: 0 property real value: 0
property var setValue // Function property var setValue // Function
property bool busy: false property bool busy: false
property int startFrom: 1
target: Qt.point(parent.width, y + height / 2)
arrowSize: UM.Theme.getSize("default_arrow").width target: Qt.point(parent.width, y + height / 2)
height: parent.height arrowSize: UM.Theme.getSize("default_arrow").width
width: valueLabel.width + UM.Theme.getSize("default_margin").width height: parent.height
visible: false width: valueLabel.width + UM.Theme.getSize("default_margin").width
visible: false
// make sure the text field is focussed when pressing the parent handle
// needed to connect the key bindings when switching active handle // make sure the text field is focussed when pressing the parent handle
onVisibleChanged: if (visible) valueLabel.forceActiveFocus() // needed to connect the key bindings when switching active handle
onVisibleChanged: if (visible) valueLabel.forceActiveFocus()
color: UM.Theme.getColor("tool_panel_background")
borderColor: UM.Theme.getColor("lining") color: UM.Theme.getColor("tool_panel_background")
borderWidth: UM.Theme.getSize("default_lining").width borderColor: UM.Theme.getColor("lining")
borderWidth: UM.Theme.getSize("default_lining").width
Behavior on height {
NumberAnimation { Behavior on height {
duration: 50 NumberAnimation {
} duration: 50
} }
}
// catch all mouse events so they're not handled by underlying 3D scene
MouseArea { // catch all mouse events so they're not handled by underlying 3D scene
anchors.fill: parent MouseArea {
} anchors.fill: parent
}
TextField {
id: valueLabel TextField {
id: valueLabel
anchors {
left: parent.left anchors {
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) left: parent.left
verticalCenter: parent.verticalCenter leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
} verticalCenter: parent.verticalCenter
}
width: 40 * screenScaleFactor
text: sliderLabelRoot.value + 1 // the current handle value, add 1 because layers is an array width: 40 * screenScaleFactor
horizontalAlignment: TextInput.AlignRight text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight
// key bindings, work when label is currenctly focused (active handle in LayerSlider)
Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) // key bindings, work when label is currenctly focused (active handle in LayerSlider)
Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
style: TextFieldStyle {
textColor: UM.Theme.getColor("setting_control_text") style: TextFieldStyle {
font: UM.Theme.getFont("default") textColor: UM.Theme.getColor("setting_control_text")
background: Item { } font: UM.Theme.getFont("default")
} background: Item { }
}
onEditingFinished: {
onEditingFinished: {
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle. // Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Another option would be to increase the size even further, but that gives pretty ugly results. // Seems to have to do something with different dpi densities that QML doesn't quite handle.
cursorPosition = 0 // Another option would be to increase the size even further, but that gives pretty ugly results.
cursorPosition = 0
if (valueLabel.text != "") {
// -1 because we need to convert back to an array structure if (valueLabel.text != "") {
sliderLabelRoot.setValue(parseInt(valueLabel.text) - 1) // -startFrom because we need to convert back to an array structure
} sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom)
} }
}
validator: IntValidator {
bottom: 1 validator: IntValidator {
top: sliderLabelRoot.maximumValue + 1 // +1 because actual layers is an array bottom:startFrom
} top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
} }
}
BusyIndicator {
id: busyIndicator BusyIndicator {
id: busyIndicator
anchors {
left: parent.right anchors {
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) left: parent.right
verticalCenter: parent.verticalCenter leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
} verticalCenter: parent.verticalCenter
}
width: sliderLabelRoot.height
height: width width: sliderLabelRoot.height
height: width
visible: sliderLabelRoot.busy
running: sliderLabelRoot.busy visible: sliderLabelRoot.busy
} running: sliderLabelRoot.busy
} }
}

View file

@ -1,46 +1,46 @@
# 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 sys import sys
from UM.PluginRegistry import PluginRegistry
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Event import Event, KeyEvent
from UM.Signal import Signal
from UM.Scene.Selection import Selection
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Job import Job
from UM.Preferences import Preferences
from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL
from UM.Message import Message
from UM.Application import Application
from UM.View.GL.OpenGLContext import OpenGLContext
from cura.ConvexHullNode import ConvexHullNode
from cura.Settings.ExtruderManager import ExtruderManager
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from . import LayerViewProxy from UM.Application import Application
from UM.Event import Event, KeyEvent
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Preferences import Preferences
from UM.Resources import Resources
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Signal import Signal
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.View import View
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") from cura.ConvexHullNode import ConvexHullNode
from . import LayerPass from .NozzleNode import NozzleNode
from .SimulationPass import SimulationPass
from .SimulationViewProxy import SimulationViewProxy
catalog = i18nCatalog("cura")
import numpy import numpy
import os.path import os.path
## View used to display g-code paths. ## View used to display g-code paths.
class LayerView(View): class SimulationView(View):
# Must match LayerView.qml # Must match SimulationView.qml
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
LAYER_VIEW_TYPE_LINE_TYPE = 1 LAYER_VIEW_TYPE_LINE_TYPE = 1
LAYER_VIEW_TYPE_FEEDRATE = 2
LAYER_VIEW_TYPE_THICKNESS = 3
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -54,22 +54,29 @@ class LayerView(View):
self._activity = False self._activity = False
self._old_max_layers = 0 self._old_max_layers = 0
self._max_paths = 0
self._current_path_num = 0
self._minimum_path_num = 0
self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
self._busy = False self._busy = False
self._simulation_running = False
self._ghost_shader = None self._ghost_shader = None
self._layer_pass = None self._layer_pass = None
self._composite_pass = None self._composite_pass = None
self._old_layer_bindings = None self._old_layer_bindings = None
self._layerview_composite_shader = None self._simulationview_composite_shader = None
self._old_composite_shader = None self._old_composite_shader = None
self._global_container_stack = None self._global_container_stack = None
self._proxy = LayerViewProxy.LayerViewProxy() self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._resetSettings() self._resetSettings()
self._legend_items = None self._legend_items = None
self._show_travel_moves = False self._show_travel_moves = False
self._nozzle_node = None
Preferences.getInstance().addPreference("view/top_layer_count", 5) Preferences.getInstance().addPreference("view/top_layer_count", 5)
Preferences.getInstance().addPreference("view/only_show_top_layers", False) Preferences.getInstance().addPreference("view/only_show_top_layers", False)
@ -91,7 +98,7 @@ class LayerView(View):
self._compatibility_mode = True # for safety self._compatibility_mode = True # for safety
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Layer View")) title = catalog.i18nc("@info:title", "Simulation View"))
def _resetSettings(self): def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
@ -101,17 +108,24 @@ class LayerView(View):
self._show_helpers = 1 self._show_helpers = 1
self._show_skin = 1 self._show_skin = 1
self._show_infill = 1 self._show_infill = 1
self.resetLayerData()
def getActivity(self): def getActivity(self):
return self._activity return self._activity
def getLayerPass(self): def setActivity(self, activity):
if self._activity == activity:
return
self._activity = activity
self.activityChanged.emit()
def getSimulationPass(self):
if not self._layer_pass: if not self._layer_pass:
# Currently the RenderPass constructor requires a size > 0 # Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor. # This should be fixed in RenderPass's constructor.
self._layer_pass = LayerPass.LayerPass(1, 1) self._layer_pass = SimulationPass(1, 1)
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._layer_pass.setLayerView(self) self._layer_pass.setSimulationView(self)
return self._layer_pass return self._layer_pass
def getCurrentLayer(self): def getCurrentLayer(self):
@ -120,13 +134,27 @@ class LayerView(View):
def getMinimumLayer(self): def getMinimumLayer(self):
return self._minimum_layer_num return self._minimum_layer_num
def _onSceneChanged(self, node):
self.calculateMaxLayers()
def getMaxLayers(self): def getMaxLayers(self):
return self._max_layers return self._max_layers
busyChanged = Signal() def getCurrentPath(self):
return self._current_path_num
def getMinimumPath(self):
return self._minimum_path_num
def getMaxPaths(self):
return self._max_paths
def getNozzleNode(self):
if not self._nozzle_node:
self._nozzle_node = NozzleNode()
return self._nozzle_node
def _onSceneChanged(self, node):
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self): def isBusy(self):
return self._busy return self._busy
@ -136,9 +164,19 @@ class LayerView(View):
self._busy = busy self._busy = busy
self.busyChanged.emit() self.busyChanged.emit()
def isSimulationRunning(self):
return self._simulation_running
def setSimulationRunning(self, running):
self._simulation_running = running
def resetLayerData(self): def resetLayerData(self):
self._current_layer_mesh = None self._current_layer_mesh = None
self._current_layer_jumps = None self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
def beginRendering(self): def beginRendering(self):
scene = self.getController().getScene() scene = self.getController().getScene()
@ -186,15 +224,43 @@ class LayerView(View):
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setPath(self, value):
if self._current_path_num != value:
self._current_path_num = value
if self._current_path_num < 0:
self._current_path_num = 0
if self._current_path_num > self._max_paths:
self._current_path_num = self._max_paths
if self._current_path_num < self._minimum_path_num:
self._minimum_path_num = self._current_path_num
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
def setMinimumPath(self, value):
if self._minimum_path_num != value:
self._minimum_path_num = value
if self._minimum_path_num < 0:
self._minimum_path_num = 0
if self._minimum_path_num > self._max_layers:
self._minimum_path_num = self._max_layers
if self._minimum_path_num > self._current_path_num:
self._current_path_num = self._minimum_path_num
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
## Set the layer view type ## Set the layer view type
# #
# \param layer_view_type integer as in LayerView.qml and this class # \param layer_view_type integer as in SimulationView.qml and this class
def setLayerViewType(self, layer_view_type): def setSimulationViewType(self, layer_view_type):
self._layer_view_type = layer_view_type self._layer_view_type = layer_view_type
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
## Return the layer view type, integer as in LayerView.qml and this class ## Return the layer view type, integer as in SimulationView.qml and this class
def getLayerViewType(self): def getSimulationViewType(self):
return self._layer_view_type return self._layer_view_type
## Set the extruder opacity ## Set the extruder opacity
@ -243,9 +309,20 @@ class LayerView(View):
def getExtruderCount(self): def getExtruderCount(self):
return self._extruder_count return self._extruder_count
def getMinFeedrate(self):
return self._min_feedrate
def getMaxFeedrate(self):
return self._max_feedrate
def getMinThickness(self):
return self._min_thickness
def getMaxThickness(self):
return self._max_thickness
def calculateMaxLayers(self): def calculateMaxLayers(self):
scene = self.getController().getScene() scene = self.getController().getScene()
self._activity = True
self._old_max_layers = self._max_layers self._old_max_layers = self._max_layers
## Recalculate num max layers ## Recalculate num max layers
@ -255,9 +332,16 @@ class LayerView(View):
if not layer_data: if not layer_data:
continue continue
self.setActivity(True)
min_layer_number = sys.maxsize min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers(): for layer_id in layer_data.getLayers():
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness)
if max_layer_number < layer_id: if max_layer_number < layer_id:
max_layer_number = layer_id max_layer_number = layer_id
if min_layer_number > layer_id: if min_layer_number > layer_id:
@ -281,10 +365,32 @@ class LayerView(View):
self.maxLayersChanged.emit() self.maxLayersChanged.emit()
self._startUpdateTopLayers() self._startUpdateTopLayers()
def calculateMaxPathsOnLayer(self, layer_num):
# Update the currentPath
scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()):
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
layer = layer_data.getLayer(layer_num)
if layer is None:
return
new_max_paths = layer.lineMeshElementCount()
if new_max_paths >= 0 and new_max_paths != self._max_paths:
self._max_paths = new_max_paths
self.maxPathsChanged.emit()
self.setPath(int(new_max_paths))
maxLayersChanged = Signal() maxLayersChanged = Signal()
maxPathsChanged = Signal()
currentLayerNumChanged = Signal() currentLayerNumChanged = Signal()
currentPathNumChanged = Signal()
globalStackChanged = Signal() globalStackChanged = Signal()
preferencesChanged = Signal() preferencesChanged = Signal()
busyChanged = Signal()
activityChanged = Signal()
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
# as this caused some issues. # as this caused some issues.
@ -308,26 +414,31 @@ class LayerView(View):
return True return True
if event.type == Event.ViewActivateEvent: if event.type == Event.ViewActivateEvent:
# Make sure the LayerPass is created # Make sure the SimulationPass is created
layer_pass = self.getLayerPass() layer_pass = self.getSimulationPass()
self.getRenderer().addRenderPass(layer_pass) self.getRenderer().addRenderPass(layer_pass)
# Make sure the NozzleNode is add to the root
nozzle = self.getNozzleNode()
nozzle.setParent(self.getController().getScene().getRoot())
nozzle.setVisible(False)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
if not self._layerview_composite_shader: if not self._simulationview_composite_shader:
self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader")) self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
theme = Application.getInstance().getTheme() theme = Application.getInstance().getTheme()
self._layerview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._layerview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
if not self._composite_pass: if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite") self._composite_pass = self.getRenderer().getRenderPass("composite")
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
self._composite_pass.getLayerBindings().append("layerview") self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader() self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._layerview_composite_shader) self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
elif event.type == Event.ViewDeactivateEvent: elif event.type == Event.ViewDeactivateEvent:
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
@ -335,6 +446,7 @@ class LayerView(View):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._nozzle_node.setParent(None)
self.getRenderer().removeRenderPass(self._layer_pass) self.getRenderer().removeRenderPass(self._layer_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader) self._composite_pass.setCompositeShader(self._old_composite_shader)
@ -364,6 +476,9 @@ class LayerView(View):
else: else:
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
def _onCurrentLayerNumChanged(self):
self.calculateMaxPathsOnLayer(self._current_layer_num)
def _startUpdateTopLayers(self): def _startUpdateTopLayers(self):
if not self._compatibility_mode: if not self._compatibility_mode:
return return
@ -397,7 +512,7 @@ class LayerView(View):
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool( self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self.setLayerViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type")))); self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")): for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")):
try: try:

View file

@ -0,0 +1,707 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item
{
id: base
width: {
if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else {
return UM.Theme.getSize("layerview_menu_size").width;
}
}
height: {
if (viewSettings.collapsed) {
if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height;
}
return UM.Theme.getSize("layerview_menu_size_collapsed").height;
} else if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else {
return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
}
}
Behavior on height { NumberAnimation { duration: 100 } }
property var buttonTarget: {
if(parent != null)
{
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
}
return Qt.point(0,0)
}
visible: parent != null ? !parent.parent.monitoringPrint: true
Rectangle {
id: layerViewMenu
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
height: parent.height
clip: true
z: layerSlider.z - 1
color: UM.Theme.getColor("tool_panel_background")
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
Button {
id: collapseButton
anchors.top: parent.top
anchors.topMargin: Math.floor(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
onClicked: viewSettings.collapsed = !viewSettings.collapsed
style: ButtonStyle
{
background: UM.RecolorImage
{
width: control.width
height: control.height
sourceSize.width: width
sourceSize.height: width
color: UM.Theme.getColor("setting_control_text")
source: viewSettings.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
}
label: Label{ }
}
}
ColumnLayout {
id: viewSettings
property bool collapsed: false
property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
// if we are in compatibility mode, we only show the "line type"
property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("layerview_row_spacing").height
Label
{
id: layerViewTypesLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Color scheme")
font: UM.Theme.getFont("default");
visible: !UM.SimulationView.compatibilityMode
Layout.fillWidth: true
color: UM.Theme.getColor("setting_control_text")
}
ListModel // matches SimulationView.py
{
id: layerViewTypes
}
Component.onCompleted:
{
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Material Color"),
type_id: 0
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Line Type"),
type_id: 1
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Feedrate"),
type_id: 2
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Layer thickness"),
type_id: 3 // these ids match the switching in the shader
})
}
ComboBox
{
id: layerTypeCombobox
anchors.left: parent.left
Layout.fillWidth: true
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
model: layerViewTypes
visible: !UM.SimulationView.compatibilityMode
style: UM.Theme.styles.combobox
anchors.right: parent.right
onActivated:
{
UM.Preferences.setValue("layerview/layer_view_type", index);
}
Component.onCompleted:
{
currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
updateLegends(currentIndex);
}
function updateLegends(type_id)
{
// update visibility of legends
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
Label
{
id: compatibilityModeLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Compatibility Mode")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
visible: UM.SimulationView.compatibilityMode
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
}
Item
{
height: Math.floor(UM.Theme.getSize("default_margin").width / 2)
width: width
}
Connections {
target: UM.Preferences
onPreferenceChanged:
{
layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
playButton.pauseSimulation();
viewSettings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
viewSettings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
viewSettings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
viewSettings.show_skin = UM.Preferences.getValue("layerview/show_skin");
viewSettings.show_infill = UM.Preferences.getValue("layerview/show_infill");
viewSettings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
viewSettings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
}
}
Repeater {
model: Cura.ExtrudersModel{}
CheckBox {
id: extrudersModelCheckBox
checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
onClicked: {
viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
}
visible: !UM.SimulationView.compatibilityMode
enabled: index + 1 <= 4
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !viewSettings.show_legend & !viewSettings.show_gradient
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: model.name
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
Repeater {
model: ListModel {
id: typesLegendModel
Component.onCompleted:
{
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Travels"),
initialValue: viewSettings.show_travel_moves,
preference: "layerview/show_travel_moves",
colorId: "layerview_move_combing"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Helpers"),
initialValue: viewSettings.show_helpers,
preference: "layerview/show_helpers",
colorId: "layerview_support"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Shell"),
initialValue: viewSettings.show_skin,
preference: "layerview/show_skin",
colorId: "layerview_inset_0"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Infill"),
initialValue: viewSettings.show_infill,
preference: "layerview/show_infill",
colorId: "layerview_infill"
});
}
}
CheckBox {
id: legendModelCheckBox
checked: model.initialValue
onClicked: {
UM.Preferences.setValue(model.preference, checked);
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: viewSettings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
CheckBox {
checked: viewSettings.only_show_top_layers
onClicked: {
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
}
text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox
}
CheckBox {
checked: viewSettings.top_layer_count == 5
onClicked: {
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
}
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox
}
Repeater {
model: ListModel {
id: typesLegendModelNoCheck
Component.onCompleted:
{
typesLegendModelNoCheck.append({
label: catalog.i18nc("@label", "Top / Bottom"),
colorId: "layerview_skin",
});
typesLegendModelNoCheck.append({
label: catalog.i18nc("@label", "Inner Wall"),
colorId: "layerview_inset_x",
});
}
}
Label {
text: label
visible: viewSettings.show_legend
id: typesLegendModelLabel
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: viewSettings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
}
}
// Text for the minimum, maximum and units for the feedrates and layer thickness
Item {
id: gradientLegend
visible: viewSettings.show_gradient
width: parent.width
height: UM.Theme.getSize("layerview_row").height
anchors {
topMargin: UM.Theme.getSize("slider_layerview_margin").height
horizontalCenter: parent.horizontalCenter
}
Label {
text: minText()
anchors.left: parent.left
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function minText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
}
}
return catalog.i18nc("@label","min")
}
}
Label {
text: unitsText()
anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function unitsText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return "mm/s"
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return "mm"
}
}
return ""
}
}
Label {
text: maxText()
anchors.right: parent.right
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function maxText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
}
}
return catalog.i18nc("@label","max")
}
}
}
// Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: feedrateGradient
visible: viewSettings.show_feedrate_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0.5, 0, 1)
}
GradientStop {
position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0.25, 1, 0, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
}
}
// Gradient colors for layer thickness
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: thicknessGradient
visible: viewSettings.show_thickness_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0, 0, 1)
}
GradientStop {
position: 0.25
color: Qt.rgba(0.5, 0.5, 0, 1)
}
GradientStop {
position: 0.5
color: Qt.rgba(0, 1, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0, 0.5, 0.5, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
}
}
}
}
Item {
id: slidersBox
width: parent.width
visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
anchors {
top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height
left: parent.left
}
PathSlider {
id: pathSlider
height: UM.Theme.getSize("slider_handle").width
anchors.left: playButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
visible: !UM.SimulationView.compatibilityMode
// custom properties
handleValue: UM.SimulationView.currentPath
maximumValue: UM.SimulationView.numPaths
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
handleColor: UM.Theme.getColor("slider_handle")
handleActiveColor: UM.Theme.getColor("slider_handle_active")
rangeColor: UM.Theme.getColor("slider_groove_fill")
// update values when layer data changes
Connections {
target: UM.SimulationView
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
}
LayerSlider {
id: layerSlider
width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
}
// custom properties
upperValue: UM.SimulationView.currentLayer
lowerValue: UM.SimulationView.minimumLayer
maximumValue: UM.SimulationView.numLayers
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
upperHandleColor: UM.Theme.getColor("slider_handle")
lowerHandleColor: UM.Theme.getColor("slider_handle")
rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
handleActiveColor: UM.Theme.getColor("slider_handle_active")
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
// update values when layer data changes
Connections {
target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
}
// Play simulation button
Button {
id: playButton
iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode
anchors {
verticalCenter: pathSlider.verticalCenter
}
property var status: 0 // indicates if it's stopped (0) or playing (1)
onClicked: {
switch(status) {
case 0: {
resumeSimulation()
break
}
case 1: {
pauseSimulation()
break
}
}
}
function pauseSimulation() {
UM.SimulationView.setSimulationRunning(false)
iconSource = "./resources/simulation_resume.svg"
simulationTimer.stop()
status = 0
}
function resumeSimulation() {
UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg"
simulationTimer.start()
}
}
Timer
{
id: simulationTimer
interval: 100
running: false
repeat: true
onTriggered: {
var currentPath = UM.SimulationView.currentPath
var numPaths = UM.SimulationView.numPaths
var currentLayer = UM.SimulationView.currentLayer
var numLayers = UM.SimulationView.numLayers
// When the user plays the simulation, if the path slider is at the end of this layer, we start
// the simulation at the beginning of the current layer.
if (playButton.status == 0)
{
if (currentPath >= numPaths)
{
UM.SimulationView.setCurrentPath(0)
}
else
{
UM.SimulationView.setCurrentPath(currentPath+1)
}
}
// If the simulation is already playing and we reach the end of a layer, then it automatically
// starts at the beginning of the next layer.
else
{
if (currentPath >= numPaths)
{
// At the end of the model, the simulation stops
if (currentLayer >= numLayers)
{
playButton.pauseSimulation()
}
else
{
UM.SimulationView.setCurrentLayer(currentLayer+1)
UM.SimulationView.setCurrentPath(0)
}
}
else
{
UM.SimulationView.setCurrentPath(currentPath+1)
}
}
playButton.status = 1
}
}
}
FontMetrics {
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View file

@ -0,0 +1,259 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
import SimulationView
class SimulationViewProxy(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self.is_simulationView_selected = False
currentLayerChanged = pyqtSignal()
currentPathChanged = pyqtSignal()
maxLayersChanged = pyqtSignal()
maxPathsChanged = pyqtSignal()
activityChanged = pyqtSignal()
globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getActivity()
return False
@pyqtProperty(int, notify=maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxLayers()
return 0
@pyqtProperty(int, notify=currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentLayer()
return 0
@pyqtProperty(int, notify=currentLayerChanged)
def minimumLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumLayer()
return 0
@pyqtProperty(int, notify=maxPathsChanged)
def numPaths(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxPaths()
return 0
@pyqtProperty(int, notify=currentPathChanged)
def currentPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentPath()
return 0
@pyqtProperty(int, notify=currentPathChanged)
def minimumPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumPath()
return 0
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isBusy()
return False
@pyqtProperty(bool, notify=preferencesChanged)
def compatibilityMode(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCompatibilityMode()
return False
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setLayer(layer_num)
@pyqtSlot(int)
def setMinimumLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumLayer(layer_num)
@pyqtSlot(int)
def setCurrentPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setPath(path_num)
@pyqtSlot(int)
def setMinimumPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumPath(path_num)
@pyqtSlot(int)
def setSimulationViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationViewisinstance(layer_view_type)
@pyqtSlot(result=int)
def getSimulationViewType(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getSimulationViewType()
return 0
@pyqtSlot(bool)
def setSimulationRunning(self, running):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationRunning(running)
@pyqtSlot(result=bool)
def getSimulationRunning(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isSimulationRunning()
return False
@pyqtSlot(result=float)
def getMinFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinFeedrate()
return 0
@pyqtSlot(result=float)
def getMaxFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxFeedrate()
return 0
@pyqtSlot(result=float)
def getMinThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinThickness()
return 0
@pyqtSlot(result=float)
def getMaxThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxThickness()
return 0
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setExtruderOpacity(extruder_nr, opacity)
@pyqtSlot(int)
def setShowTravelMoves(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowTravelMoves(show)
@pyqtSlot(int)
def setShowHelpers(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowHelpers(show)
@pyqtSlot(int)
def setShowSkin(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowSkin(show)
@pyqtSlot(int)
def setShowInfill(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowInfill(show)
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getExtruderCount()
return 0
def _layerActivityChanged(self):
self.activityChanged.emit()
def _onLayerChanged(self):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onPathChanged(self):
self.currentPathChanged.emit()
self._layerActivityChanged()
def _onMaxLayersChanged(self):
self.maxLayersChanged.emit()
def _onMaxPathsChanged(self):
self.maxPathsChanged.emit()
def _onBusyChanged(self):
self.busyChanged.emit()
def _onActivityChanged(self):
self.activityChanged.emit()
def _onGlobalStackChanged(self):
self.globalStackChanged.emit()
def _onPreferencesChanged(self):
self.preferencesChanged.emit()
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
# remove other connection if once the SimulationView was created.
if self.is_simulationView_selected:
active_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
active_view.currentPathNumChanged.disconnect(self._onPathChanged)
active_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
active_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)
active_view.busyChanged.disconnect(self._onBusyChanged)
active_view.activityChanged.disconnect(self._onActivityChanged)
active_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
active_view.preferencesChanged.disconnect(self._onPreferencesChanged)
self.is_simulationView_selected = True
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.currentPathNumChanged.connect(self._onPathChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.maxPathsChanged.connect(self._onMaxPathsChanged)
active_view.busyChanged.connect(self._onBusyChanged)
active_view.activityChanged.connect(self._onActivityChanged)
active_view.globalStackChanged.connect(self._onGlobalStackChanged)
active_view.preferencesChanged.connect(self._onPreferencesChanged)

View file

@ -0,0 +1,26 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
from . import SimulationViewProxy, SimulationView
catalog = i18nCatalog("cura")
def getMetaData():
return {
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layer view"),
"view_panel": "SimulationView.qml",
"weight": 2
}
}
def createSimulationViewProxy(engine, script_engine):
return SimulationViewProxy.SimulatorViewProxy()
def register(app):
simulation_view = SimulationView.SimulationView()
qmlRegisterSingletonType(SimulationViewProxy.SimulationViewProxy, "UM", 1, 0, "SimulationView", simulation_view.getProxy)
return { "view": SimulationView.SimulationView()}

View file

@ -6,6 +6,10 @@ vertex41core =
uniform highp mat4 u_modelMatrix; uniform highp mat4 u_modelMatrix;
uniform highp mat4 u_viewProjectionMatrix; uniform highp mat4 u_viewProjectionMatrix;
uniform lowp float u_active_extruder; uniform lowp float u_active_extruder;
uniform lowp float u_max_feedrate;
uniform lowp float u_min_feedrate;
uniform lowp float u_max_thickness;
uniform lowp float u_min_thickness;
uniform lowp int u_layer_view_type; uniform lowp int u_layer_view_type;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
@ -18,6 +22,8 @@ vertex41core =
in highp vec2 a_line_dim; // line width and thickness in highp vec2 a_line_dim; // line width and thickness
in highp float a_extruder; in highp float a_extruder;
in highp float a_line_type; in highp float a_line_type;
in highp float a_feedrate;
in highp float a_thickness;
out lowp vec4 v_color; out lowp vec4 v_color;
@ -32,6 +38,28 @@ vertex41core =
out highp vec3 f_vertex; out highp vec3 f_vertex;
out highp vec3 f_normal; out highp vec3 f_normal;
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = value;
float green = 1-abs(1-4*value);
if (value > 0.375)
{
green = 0.5;
}
float blue = max(1-4*value, 0);
return vec4(red, green, blue, 1.0);
}
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = max(1-2*value, 0);
return vec4(red, green, blue, 1.0);
}
void main() void main()
{ {
vec4 v1_vertex = a_vertex; vec4 v1_vertex = a_vertex;
@ -48,6 +76,12 @@ vertex41core =
case 1: // "Line type" case 1: // "Line type"
v_color = a_color; v_color = a_color;
break; break;
case 2: // "Feedrate"
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
break;
case 3: // "Layer thickness"
v_color = layerThicknessGradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
break;
} }
v_vertex = world_space_vert.xyz; v_vertex = world_space_vert.xyz;
@ -153,20 +187,27 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert)); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert)); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
EndPrimitive(); EndPrimitive();
} else { } else {
// All normal lines are rendered as 3d tubes. // All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert)); myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert)); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive(); EndPrimitive();
@ -247,6 +288,12 @@ u_show_helpers = 1
u_show_skin = 1 u_show_skin = 1
u_show_infill = 1 u_show_infill = 1
u_min_feedrate = 0
u_max_feedrate = 1
u_min_thickness = 0
u_max_thickness = 1
[bindings] [bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix u_modelViewProjectionMatrix = model_view_projection_matrix
u_modelMatrix = model_matrix u_modelMatrix = model_matrix
@ -262,3 +309,5 @@ a_line_dim = line_dim
a_extruder = extruder a_extruder = extruder
a_material_color = material_color a_material_color = material_color
a_line_type = line_type a_line_type = line_type
a_feedrate = feedrate
a_thickness = thickness

View file

@ -0,0 +1,256 @@
[shaders]
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
uniform highp mat4 u_modelMatrix;
uniform highp mat4 u_viewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
uniform highp mat4 u_normalMatrix;
in highp vec4 a_vertex;
in lowp vec4 a_color;
in lowp vec4 a_grayColor;
in lowp vec4 a_material_color;
in highp vec4 a_normal;
in highp vec2 a_line_dim; // line width and thickness
in highp float a_extruder;
in highp float a_line_type;
out lowp vec4 v_color;
out highp vec3 v_vertex;
out highp vec3 v_normal;
out lowp vec2 v_line_dim;
out highp int v_extruder;
out highp vec4 v_extruder_opacity;
out float v_line_type;
out lowp vec4 f_color;
out highp vec3 f_vertex;
out highp vec3 f_normal;
void main()
{
vec4 v1_vertex = a_vertex;
v1_vertex.y -= a_line_dim.y / 2; // half layer down
vec4 world_space_vert = u_modelMatrix * v1_vertex;
gl_Position = world_space_vert;
// shade the color depending on the extruder index stored in the alpha component of the color
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer
v_vertex = world_space_vert.xyz;
v_normal = (u_normalMatrix * normalize(a_normal)).xyz;
v_line_dim = a_line_dim;
v_extruder = int(a_extruder);
v_line_type = a_line_type;
v_extruder_opacity = u_extruder_opacity;
// for testing without geometry shader
f_color = v_color;
f_vertex = v_vertex;
f_normal = v_normal;
}
geometry41core =
#version 410
uniform highp mat4 u_viewProjectionMatrix;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
layout(lines) in;
layout(triangle_strip, max_vertices = 26) out;
in vec4 v_color[];
in vec3 v_vertex[];
in vec3 v_normal[];
in vec2 v_line_dim[];
in int v_extruder[];
in vec4 v_extruder_opacity[];
in float v_line_type[];
out vec4 f_color;
out vec3 f_normal;
out vec3 f_vertex;
// Set the set of variables and EmitVertex
void myEmitVertex(vec3 vertex, vec4 color, vec3 normal, vec4 pos) {
f_vertex = vertex;
f_color = color;
f_normal = normal;
gl_Position = pos;
EmitVertex();
}
void main()
{
vec4 g_vertex_delta;
vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers
vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position
vec3 g_vertex_normal_vert;
vec4 g_vertex_offset_vert;
vec3 g_vertex_normal_horz_head;
vec4 g_vertex_offset_horz_head;
float size_x;
float size_y;
if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
return;
}
// See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType
if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) {
return;
}
if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) {
return;
}
if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) {
return;
}
if ((u_show_infill == 0) && (v_line_type[0] == 6)) {
return;
}
if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) {
// fixed size for movements
size_x = 0.05;
} else {
size_x = v_line_dim[1].x / 2 + 0.01; // radius, and make it nicely overlapping
}
size_y = v_line_dim[1].y / 2 + 0.01;
g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position;
g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z));
g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0);
g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x));
g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz;
g_vertex_normal_vert = vec3(0.0, 1.0, 0.0);
g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0);
if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) {
// Travels: flat plane with pointy ends
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
EndPrimitive();
// left side
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
EndPrimitive();
// right side
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
EndPrimitive();
}
}
fragment41core =
#version 410
in lowp vec4 f_color;
in lowp vec3 f_normal;
in lowp vec3 f_vertex;
out vec4 frag_color;
uniform mediump vec4 u_ambientColor;
uniform highp vec3 u_lightPosition;
void main()
{
mediump vec4 finalColor = vec4(0.0);
float alpha = f_color.a;
finalColor.rgb += f_color.rgb * 0.3;
highp vec3 normal = normalize(f_normal);
highp vec3 light_dir = normalize(u_lightPosition - f_vertex);
// Diffuse Component
highp float NdotL = clamp(dot(normal, light_dir), 0.0, 1.0);
finalColor += (NdotL * f_color);
finalColor.a = alpha; // Do not change alpha in any way
frag_color = finalColor;
}
[defaults]
u_active_extruder = 0.0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0]
u_diffuseColor = [1.0, 0.79, 0.14, 1.0]
u_shininess = 20.0
u_show_travel_moves = 0
u_show_helpers = 1
u_show_skin = 1
u_show_infill = 1
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
u_modelMatrix = model_matrix
u_viewProjectionMatrix = view_projection_matrix
u_normalMatrix = normal_matrix
u_lightPosition = light_0_position
[attributes]
a_vertex = vertex
a_color = color
a_grayColor = vec4(0.87, 0.12, 0.45, 1.0)
a_normal = normal
a_line_dim = line_dim
a_extruder = extruder
a_material_color = material_color
a_line_type = line_type

View file

@ -0,0 +1,156 @@
[shaders]
vertex =
uniform highp mat4 u_modelViewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp float u_shade_factor;
uniform highp int u_layer_view_type;
attribute highp float a_extruder;
attribute highp float a_line_type;
attribute highp vec4 a_vertex;
attribute lowp vec4 a_color;
attribute lowp vec4 a_material_color;
varying lowp vec4 v_color;
varying float v_line_type;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
// shade the color depending on the extruder index
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer;
// 8 and 9 are travel moves
// if ((a_line_type != 8.0) && (a_line_type != 9.0)) {
// v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
// }
v_line_type = a_line_type;
}
fragment =
varying lowp vec4 v_color;
varying float v_line_type;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
void main()
{
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
// discard movements
discard;
}
// support: 4, 5, 7, 10
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
)) {
discard;
}
// skin: 1, 2, 3
if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5)
)) {
discard;
}
// infill:
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
// discard movements
discard;
}
gl_FragColor = v_color;
}
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp float u_shade_factor;
uniform highp int u_layer_view_type;
in highp float a_extruder;
in highp float a_line_type;
in highp vec4 a_vertex;
in lowp vec4 a_color;
in lowp vec4 a_material_color;
out lowp vec4 v_color;
out float v_line_type;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer
// if ((a_line_type != 8) && (a_line_type != 9)) {
// v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
// }
v_line_type = a_line_type;
}
fragment41core =
#version 410
in lowp vec4 v_color;
in float v_line_type;
out vec4 frag_color;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
void main()
{
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
// discard movements
discard;
}
// helpers: 4, 5, 7, 10
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
)) {
discard;
}
// skin: 1, 2, 3
if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5)
)) {
discard;
}
// infill:
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
// discard movements
discard;
}
frag_color = v_color;
}
[defaults]
u_active_extruder = 0.0
u_shade_factor = 0.60
u_layer_view_type = 0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_show_travel_moves = 0
u_show_helpers = 1
u_show_skin = 1
u_show_infill = 1
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
[attributes]
a_vertex = vertex
a_color = color
a_extruder = extruder
a_line_type = line_type
a_material_color = material_color

View file

@ -1,8 +1,8 @@
{ {
"name": "Layer View", "name": "Simulation View",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides the Layer view.", "description": "Provides the Simulation view.",
"api": 4, "api": 4,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

Binary file not shown.

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="6mm"
height="6mm"
viewBox="0 0 5.9999999 6"
version="1.1"
id="svg877"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_pause2.svg">
<defs
id="defs871" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-5.3551409"
inkscape:cy="17.386031"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata874">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.163774,-122.8006)">
<g
id="g825"
transform="matrix(0.26458333,0,0,0.26458333,10.185689,121.85192)">
<rect
y="5"
x="19"
height="20"
width="2.7552757"
id="rect5192"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="5"
x="9"
height="20"
width="2.7552757"
id="rect5192-5"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="6mm"
height="6mm"
viewBox="0 0 6 6"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_resume2.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-32.404712"
inkscape:cy="14.267522"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(81.024887,-389.647)">
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.50520164;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path847"
sodipodi:sides="3"
sodipodi:cx="-78.732257"
sodipodi:cy="392.65222"
sodipodi:r1="3.0592039"
sodipodi:r2="1.5296021"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -75.67305,392.65222 -4.588806,2.64935 v -5.2987 z"
inkscape:transform-center-x="0.75529536"
inkscape:transform-center-y="0.40090429" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -39,7 +39,7 @@ class SliceInfo(Extension):
Preferences.getInstance().addPreference("info/send_slice_info", True) Preferences.getInstance().addPreference("info/send_slice_info", True)
Preferences.getInstance().addPreference("info/asked_send_slice_info", False) Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"): if not Preferences.getInstance().getValue("info/asked_send_slice_info") and Preferences.getInstance().getValue("info/send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."), self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."),
lifetime = 0, lifetime = 0,
dismissable = False, dismissable = False,
@ -87,15 +87,10 @@ class SliceInfo(Extension):
data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")} data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")}
# add extruder specific data to slice info
data["extruders"] = [] data["extruders"] = []
extruder_count = len(global_container_stack.extruders) extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
extruders = [] extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
if extruder_count > 1:
extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
if not extruders:
extruders = [global_container_stack]
for extruder in extruders: for extruder in extruders:
extruder_dict = dict() extruder_dict = dict()
@ -104,7 +99,9 @@ class SliceInfo(Extension):
"type": extruder.material.getMetaData().get("material", ""), "type": extruder.material.getMetaData().get("material", ""),
"brand": extruder.material.getMetaData().get("brand", "") "brand": extruder.material.getMetaData().get("brand", "")
} }
extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))] extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName() extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value") extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

View file

@ -27,40 +27,39 @@ class SolidView(View):
self._enabled_shader = None self._enabled_shader = None
self._disabled_shader = None self._disabled_shader = None
self._non_printing_shader = None
self._extruders_model = ExtrudersModel() self._extruders_model = ExtrudersModel()
self._theme = None
def beginRendering(self): def beginRendering(self):
scene = self.getController().getScene() scene = self.getController().getScene()
renderer = self.getRenderer() renderer = self.getRenderer()
if not self._theme:
self._theme = Application.getInstance().getTheme()
if not self._enabled_shader: if not self._enabled_shader:
self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader")) self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
theme = Application.getInstance().getTheme() self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").getRgb()))
self._enabled_shader.setUniformValue("u_overhangColor", Color(*theme.getColor("model_overhang").getRgb()))
if not self._disabled_shader: if not self._disabled_shader:
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader")) self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
theme = Application.getInstance().getTheme() self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*self._theme.getColor("model_unslicable").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*theme.getColor("model_unslicable").getRgb())) self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_width", 50.0) self._disabled_shader.setUniformValue("u_width", 50.0)
multi_extrusion = False if not self._non_printing_shader:
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if multi_extrusion: if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
if not support_angle_stack:
support_angle_stack = global_container_stack
else:
support_angle_stack = global_container_stack
if Preferences.getInstance().getValue("view/show_overhang"):
angle = support_angle_stack.getProperty("support_angle", "value") angle = support_angle_stack.getProperty("support_angle", "value")
# Make sure the overhang angle is valid before passing it to the shader # Make sure the overhang angle is valid before passing it to the shader
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
@ -71,33 +70,34 @@ class SolidView(View):
else: else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer): if not node.render(renderer):
if node.getMeshData() and node.isVisible(): if node.getMeshData() and node.isVisible():
uniforms = {} uniforms = {}
shade_factor = 1.0 shade_factor = 1.0
if not multi_extrusion: per_mesh_stack = node.callDecoration("getStack")
if global_container_stack:
material = global_container_stack.findContainer({ "type": "material" }) # Get color to render this mesh in from ExtrudersModel
material_color = material.getMetaDataEntry("color_code", default = self._extruders_model.defaultColors[0]) if material else self._extruders_model.defaultColors[0] extruder_index = 0
else: extruder_id = node.callDecoration("getActiveExtruder")
material_color = self._extruders_model.defaultColors[0] if extruder_id:
else: extruder_index = max(0, self._extruders_model.find("id", extruder_id))
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0 # Use the support extruder instead of the active extruder if this is a support_mesh
extruder_id = node.callDecoration("getActiveExtruder") if per_mesh_stack:
if extruder_id: if per_mesh_stack.getProperty("support_mesh", "value"):
extruder_index = max(0, self._extruders_model.find("id", extruder_id)) extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
try:
material_color = self._extruders_model.getItem(extruder_index)["color"] try:
except KeyError: material_color = self._extruders_model.getItem(extruder_index)["color"]
material_color = self._extruders_model.defaultColors[0] except KeyError:
material_color = self._extruders_model.defaultColors[0]
if extruder_index != ExtruderManager.getInstance().activeExtruderIndex:
# Shade objects that are printed with the non-active extruder 25% darker
shade_factor = 0.6
if extruder_index != ExtruderManager.getInstance().activeExtruderIndex:
# Shade objects that are printed with the non-active extruder 25% darker
shade_factor = 0.6
try: try:
# Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
# an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0]) # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
@ -110,19 +110,17 @@ class SolidView(View):
except ValueError: except ValueError:
pass pass
if hasattr(node, "_outside_buildarea"): if getattr(node, "_non_printing_mesh", False):
if node._outside_buildarea: if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")):
renderer.queueNode(node, shader = self._disabled_shader) renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else: else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms) renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
else: else:
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms) renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node): if node.callDecoration("isGroup") and Selection.isSelected(node):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop) renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def endRendering(self): def endRendering(self):
pass pass
#def _onPreferenceChanged(self, preference):
#if preference == "view/show_overhang": ## Todo: This a printer only setting. Should be removed from Uranium.
#self._enabled_material = None

View file

@ -1,17 +1,15 @@
from cura.MachineAction import MachineAction import os.path
import time
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
from UM.Application import Application from UM.Application import Application
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger from UM.Logger import Logger
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
import os.path
import time
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction): class DiscoverUM3Action(MachineAction):
@ -136,17 +134,14 @@ class DiscoverUM3Action(MachineAction):
def _createAdditionalComponentsView(self): def _createAdditionalComponentsView(self):
Logger.log("d", "Creating additional ui components for UM3.") Logger.log("d", "Creating additional ui components for UM3.")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml"))
self.__additional_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't) # Create networking dialog
self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")
self.__additional_components_context.setContextProperty("manager", self) self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self.__additional_components_view = self.__additional_component.create(self.__additional_components_context)
if not self.__additional_components_view: if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.") Logger.log("w", "Could not create ui components for UM3.")
return return
# Create extra components
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -230,13 +230,13 @@ Cura.MachineAction
{ {
if(base.selectedPrinter.printerType == "ultimaker3") if(base.selectedPrinter.printerType == "ultimaker3")
{ {
return catalog.i18nc("@label", "Ultimaker 3") return catalog.i18nc("@label Printer name", "Ultimaker 3")
} else if(base.selectedPrinter.printerType == "ultimaker3_extended") } else if(base.selectedPrinter.printerType == "ultimaker3_extended")
{ {
return catalog.i18nc("@label", "Ultimaker 3 Extended") return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
} else } else
{ {
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field' return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
} }
} }
else else

View file

@ -8,12 +8,10 @@ import time
from enum import Enum from enum import Enum
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
from PyQt5.QtCore import QUrl, QByteArray, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.Application import Application from UM.Application import Application
from UM.Decorators import override
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.OutputDevice import OutputDeviceError from UM.OutputDevice import OutputDeviceError
@ -131,7 +129,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
@pyqtProperty(QObject, notify=selectedPrinterChanged) @pyqtProperty(QObject, notify=selectedPrinterChanged)
def controlItem(self): def controlItem(self):
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if not self._control_component: if not self._control_item:
self._createControlViewFromQML() self._createControlViewFromQML()
name = self._selected_printer.get("friendly_name") name = self._selected_printer.get("friendly_name")
if name == self._automatic_printer.get("friendly_name") or name == "": if name == self._automatic_printer.get("friendly_name") or name == "":
@ -235,17 +233,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
def spawnPrintView(self): def spawnPrintView(self):
if self._print_view is None: if self._print_view is None:
path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml")) path = os.path.join(self._plugin_path, "PrintWindow.qml")
component = QQmlComponent(Application.getInstance()._engine, path) self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self})
self._print_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._print_context.setContextProperty("OutputDevice", self)
self._print_view = component.create(self._print_context)
if component.isError():
Logger.log("e", " Errors creating component: \n%s", "\n".join(
[e.toString() for e in component.errors()]))
if self._print_view is not None: if self._print_view is not None:
self._print_view.show() self._print_view.show()
@ -263,6 +252,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
self._error_message.show() self._error_message.show()
return return
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1: if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it. self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1: elif len(self._printers) == 1:
@ -486,7 +477,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"]) printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
if printer_name is None: if printer_name is None:
printer_name = i18n_catalog.i18nc("@label", "Unknown") printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
message_text = (i18n_catalog.i18nc("@info:status", message_text = (i18n_catalog.i18nc("@info:status",
"Printer '{printer_name}' has finished printing '{job_name}'.") "Printer '{printer_name}' has finished printing '{job_name}'.")
@ -707,7 +698,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
if self._reply: if self._reply:
self._reply.abort() self._reply.abort()
self._stage = OutputStage.ready self._stage = OutputStage.ready
Application.getInstance().showPrintMonitor.emit(False) Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str) @pyqtSlot(int, result=str)
def formatDuration(self, seconds): def formatDuration(self, seconds):

View file

@ -672,7 +672,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state) Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state)
return return
Application.getInstance().showPrintMonitor.emit(True) Application.getInstance().getController().setActiveStage("MonitorStage")
self._print_finished = True self._print_finished = True
self.writeStarted.emit(self) self.writeStarted.emit(self)
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
@ -726,10 +726,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
remote_material_guid, remote_material_guid,
material.getMetaDataEntry("GUID")) material.getMetaDataEntry("GUID"))
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True) remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", GUID = remote_material_guid, read_only = True)
remote_material_name = "Unknown" remote_material_name = "Unknown"
if remote_materials: if remote_materials:
remote_material_name = remote_materials[0].getName() remote_material_name = remote_materials[0]["name"]
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1)) warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
try: try:
@ -767,7 +767,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
if button == QMessageBox.Yes: if button == QMessageBox.Yes:
self.startPrint() self.startPrint()
else: else:
Application.getInstance().showPrintMonitor.emit(False) Application.getInstance().getController().setActiveStage("PrepareStage")
# For some unknown reason Cura on OSX will hang if we do the call back code # For some unknown reason Cura on OSX will hang if we do the call back code
# immediately without first returning and leaving QML's event system. # immediately without first returning and leaving QML's event system.
QTimer.singleShot(100, delayedCallback) QTimer.singleShot(100, delayedCallback)
@ -850,7 +850,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
self._write_finished = True # post_reply does not always exist, so make sure we unblock writing self._write_finished = True # post_reply does not always exist, so make sure we unblock writing
if self._post_reply: if self._post_reply:
self._finalizePostReply() self._finalizePostReply()
Application.getInstance().showPrintMonitor.emit(False) Application.getInstance().getController().setActiveStage("PrepareStage")
## Attempt to start a new print. ## Attempt to start a new print.
# This function can fail to actually start a print due to not being authenticated or another print already # This function can fail to actually start a print due to not being authenticated or another print already
@ -971,7 +971,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Send all material profiles to the printer. ## Send all material profiles to the printer.
def sendMaterialProfiles(self): def sendMaterialProfiles(self):
for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material"): registry = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance()
for container in registry.findInstanceContainers(type = "material"):
try: try:
xml_data = container.serialize() xml_data = container.serialize()
if xml_data == "" or xml_data is None: if xml_data == "" or xml_data is None:
@ -980,7 +981,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
names = ContainerManager.getInstance().getLinkedMaterials(container.getId()) names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
if names: if names:
# There are other materials that share this GUID. # There are other materials that share this GUID.
if not container.isReadOnly(): if not registry.isReadOnly(container.getId()):
continue # If it's not readonly, it's created by user, so skip it. continue # If it's not readonly, it's created by user, so skip it.
material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

View file

@ -43,6 +43,7 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
# authentication requests. # authentication requests.
self._old_printers = [] self._old_printers = []
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal. # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addPrinterSignal.connect(self.addPrinter) self.addPrinterSignal.connect(self.addPrinter)
@ -300,6 +301,9 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
if type_of_device: if type_of_device:
if type_of_device == b"printer": if type_of_device == b"printer":
address = '.'.join(map(lambda n: str(n), info.address)) address = '.'.join(map(lambda n: str(n), info.address))
if address in self._excluded_addresses:
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
return False # When getting the localhost IP, then try to reconnect
self.addPrinterSignal.emit(str(name), address, info.properties) self.addPrinterSignal.emit(str(name), address, info.properties)
else: else:
Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device ) Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )

View file

@ -44,7 +44,7 @@ Rectangle
case "maintenance": // TODO: new string case "maintenance": // TODO: new string
case "unknown": case "unknown":
default: default:
return catalog.i18nc("@label", "Unknown"); return catalog.i18nc("@label Printer status", "Unknown");
} }
} }

View file

@ -13,6 +13,7 @@ from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from UM.Message import Message from UM.Message import Message
from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
@ -52,6 +53,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._heatup_wait_start_time = time.time() self._heatup_wait_start_time = time.time()
self.jobStateChanged.connect(self._onJobStateChanged)
## Queue for commands that need to be send. Used when command is sent when a print is active. ## Queue for commands that need to be send. Used when command is sent when a print is active.
self._command_queue = queue.Queue() self._command_queue = queue.Queue()
@ -60,7 +63,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Set when print is started in order to check running time. ## Set when print is started in order to check running time.
self._print_start_time = None self._print_start_time = None
self._print_start_time_100 = None self._print_estimated_time = None
## Keep track where in the provided g-code the print is ## Keep track where in the provided g-code the print is
self._gcode_position = 0 self._gcode_position = 0
@ -125,6 +128,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _homeBed(self): def _homeBed(self):
self._sendCommand("G28 Z") self._sendCommand("G28 Z")
## Updates the target bed temperature from the printer, and emit a signal if it was changed.
#
# /param temperature The new target temperature of the bed.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature == temperature:
return False
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
return True
## Updates the target hotend temperature from the printer, and emit a signal if it was changed.
#
# /param index The index of the hotend.
# /param temperature The new target temperature of the hotend.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetHotendTemperature(self, index, temperature):
if self._target_hotend_temperatures[index] == temperature:
return False
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
return True
## A name for the device. ## A name for the device.
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def name(self): def name(self):
@ -164,7 +190,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Reset line number. If this is not done, first line is sometimes ignored # Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110") self._gcode.insert(0, "M110")
self._gcode_position = 0 self._gcode_position = 0
self._print_start_time_100 = None
self._is_printing = True self._is_printing = True
self._print_start_time = time.time() self._print_start_time = time.time()
@ -394,7 +419,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._listen_thread.join() self._listen_thread.join()
except: except:
pass pass
self._serial.close() if self._serial is not None: # Avoid a race condition when a thread can change the value of self._serial to None
self._serial.close()
self._listen_thread = threading.Thread(target = self._listen) self._listen_thread = threading.Thread(target = self._listen)
self._listen_thread.daemon = True self._listen_thread.daemon = True
@ -446,7 +472,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# #
# \param nodes A collection of scene nodes to send. This is ignored. # \param nodes A collection of scene nodes to send. This is ignored.
# \param file_name \type{string} A suggestion for a file name to write. # \param file_name \type{string} A suggestion for a file name to write.
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This # \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored. # is ignored.
# \param kwargs Keyword arguments. # \param kwargs Keyword arguments.
@ -462,7 +487,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._error_message.show() self._error_message.show()
return return
Application.getInstance().showPrintMonitor.emit(True) self.setJobName(file_name)
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
Application.getInstance().getController().setActiveStage("MonitorStage")
self.startPrint() self.startPrint()
def _setEndstopState(self, endstop_key, value): def _setEndstopState(self, endstop_key, value):
@ -482,6 +510,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Listen thread function. ## Listen thread function.
def _listen(self): def _listen(self):
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port) Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
container_stack = Application.getInstance().getGlobalContainerStack()
temperature_request_timeout = time.time() temperature_request_timeout = time.time()
ok_timeout = time.time() ok_timeout = time.time()
while self._connection_state == ConnectionState.connected: while self._connection_state == ConnectionState.connected:
@ -511,16 +540,40 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._setErrorState(line[6:]) self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): # Temperature message elif b" T:" in line or line.startswith(b"T:"): # Temperature message
temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
temperature_set = False
try: try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1))) for match in temperature_matches:
if match[0]:
extruder_nr = int(match[0])
if extruder_nr >= container_stack.getProperty("machine_extruder_count", "value"):
continue
if match[1]:
self._setHotendTemperature(extruder_nr, float(match[1]))
temperature_set = True
if match[2]:
self._updateTargetHotendTemperature(extruder_nr, float(match[2]))
else:
requested_temperatures = match
if not temperature_set and requested_temperatures:
if requested_temperatures[1]:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[1]))
if requested_temperatures[2]:
self._updateTargetHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[2]))
except: except:
pass Logger.log("w", "Could not parse hotend temperatures from response: %s", line)
if b"B:" in line: # Check if it's a bed temperature # Check if there's also a bed temperature
temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line)
if container_stack.getProperty("machine_heated_bed", "value") and len(temperature_matches) > 0:
match = temperature_matches[0]
try: try:
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1))) if match[0]:
except Exception as e: self._setBedTemperature(float(match[0]))
pass if match[1]:
#TODO: temperature changed callback self._updateTargetBedTemperature(float(match[1]))
except:
Logger.log("w", "Could not parse bed temperature from response: %s", line)
elif b"_min" in line or b"_max" in line: elif b"_min" in line or b"_max" in line:
tag, value = line.split(b":", 1) tag, value = line.split(b":", 1)
self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value)) self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value))
@ -559,8 +612,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _sendNextGcodeLine(self): def _sendNextGcodeLine(self):
if self._gcode_position >= len(self._gcode): if self._gcode_position >= len(self._gcode):
return return
if self._gcode_position == 100:
self._print_start_time_100 = time.time()
line = self._gcode[self._gcode_position] line = self._gcode[self._gcode_position]
if ";" in line: if ";" in line:
@ -584,8 +635,18 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line))) checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum)) self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
progress = (self._gcode_position / len(self._gcode))
elapsed_time = int(time.time() - self._print_start_time)
self.setTimeElapsed(elapsed_time)
estimated_time = self._print_estimated_time
if progress > .1:
estimated_time = self._print_estimated_time * (1-progress) + elapsed_time
self.setTimeTotal(estimated_time)
self._gcode_position += 1 self._gcode_position += 1
self.setProgress((self._gcode_position / len(self._gcode)) * 100) self.setProgress(progress * 100)
self.progressChanged.emit() self.progressChanged.emit()
## Set the state of the print. ## Set the state of the print.
@ -600,6 +661,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elif job_state == "abort": elif job_state == "abort":
self.cancelPrint() self.cancelPrint()
def _onJobStateChanged(self):
# clear the job name & times when printing is done or aborted
if self._job_state == "ready":
self.setJobName("")
self.setTimeElapsed(0)
self.setTimeTotal(0)
## Set the progress of the print. ## Set the progress of the print.
# It will be normalized (based on max_progress) to range 0 - 100 # It will be normalized (based on max_progress) to range 0 - 100
def setProgress(self, progress, max_progress = 100): def setProgress(self, progress, max_progress = 100):
@ -630,7 +698,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._is_printing = False self._is_printing = False
self._is_paused = False self._is_paused = False
self._updateJobState("ready") self._updateJobState("ready")
Application.getInstance().showPrintMonitor.emit(False) Application.getInstance().getController().setActiveStage("PrepareStage")
## Check if the process did not encounter an error yet. ## Check if the process did not encounter an error yet.
def hasError(self): def hasError(self):

View file

@ -16,13 +16,11 @@ from cura.CuraApplication import CuraApplication
import threading import threading
import platform import platform
import glob
import time import time
import os.path import os.path
import serial.tools.list_ports import serial.tools.list_ports
from UM.Extension import Extension from UM.Extension import Extension
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -91,19 +89,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
# This will create the view if its not already created. # This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port): def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None: if self._firmware_view is None:
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")) path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
component = QQmlComponent(Application.getInstance()._engine, path) self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._firmware_context.setContextProperty("manager", self)
self._firmware_view = component.create(self._firmware_context)
self._firmware_view.show() self._firmware_view.show()
@pyqtSlot(str) @pyqtSlot(str)
def updateAllFirmware(self, file_name): def updateAllFirmware(self, file_name):
if file_name.startswith("file://"): if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices: if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show() Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
return return

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