From 6932194df75d81d76df9174e8372e26468a77918 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 20 Nov 2017 10:51:51 +0100 Subject: [PATCH 01/27] Add setting to set the maximum mesh resolution Implements issue CURA-4590. --- resources/definitions/fdmprinter.def.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index b17769eba2..5847b67816 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4827,6 +4827,18 @@ "default_value": false, "settable_per_mesh": true }, + "meshfix_maximum_resolution": + { + "label": "Maximum Resolution", + "description": "The minimum size of a line segment after slicing. If you increase this, the mesh will have a lower resolution. This may allow the printer to keep up with the speed it has to process g-code and will increase slice speed by removing details of the mesh that it can't process anyway.", + "type": "float", + "unit": "mm", + "default_value": 0.01, + "minimum_value": "0.001", + "minimum_value_warning": "0.005", + "maximum_value_warning": "0.1", + "settable_per_mesh": true + }, "multiple_mesh_overlap": { "label": "Merged Meshes Overlap", From 74eb17203d0fb528894c8ed50c098db39823f5c8 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 20 Nov 2017 13:04:46 +0100 Subject: [PATCH 02/27] Fix empty containers in stack upgrade --- .../VersionUpgrade30to31/VersionUpgrade30to31.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 4672cb1488..7064da09ad 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -45,6 +45,15 @@ _OLD_NOT_SUPPORTED_PROFILES = [ ] +# Some containers have their specific empty containers, those need to be set correctly. +_EMPTY_CONTAINER_DICT = { + "1": "empty_quality_changes", + "2": "empty_quality", + "3": "empty_material", + "4": "empty_variant", +} + + class VersionUpgrade30to31(VersionUpgrade): ## Gets the version number from a CFG file in Uranium's 3.0 format. # @@ -126,6 +135,11 @@ class VersionUpgrade30to31(VersionUpgrade): if quality_profile_id in _OLD_NOT_SUPPORTED_PROFILES: parser["containers"]["2"] = "empty_quality" + # fix empty containers + for key, specific_empty_container in _EMPTY_CONTAINER_DICT: + if parser.has_option("containers", key) and parser["containers"][key] == "empty": + parser["containers"][key] = specific_empty_container + # Update version numbers if "general" not in parser: parser["general"] = {} From 23f2d9ddfc7ac92526480f75ae3fc64e255864de Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 20 Nov 2017 13:06:02 +0100 Subject: [PATCH 03/27] Fix for loop --- .../VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 7064da09ad..7130871d8e 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -136,7 +136,7 @@ class VersionUpgrade30to31(VersionUpgrade): parser["containers"]["2"] = "empty_quality" # fix empty containers - for key, specific_empty_container in _EMPTY_CONTAINER_DICT: + for key, specific_empty_container in _EMPTY_CONTAINER_DICT.items(): if parser.has_option("containers", key) and parser["containers"][key] == "empty": parser["containers"][key] = specific_empty_container From df1a11ca07631842cda34ac4518c6fde7f0e8afa Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 3 Nov 2017 16:03:28 +0100 Subject: [PATCH 04/27] Don't crash if support density is 0 Found while working on CURA-4523. --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index b17769eba2..6628086179 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3495,7 +3495,7 @@ "minimum_value_warning": "support_line_width", "default_value": 2.66, "enabled": "support_enable", - "value": "(support_line_width * 100) / support_infill_rate * (2 if support_pattern == 'grid' else (3 if support_pattern == 'triangles' else 1))", + "value": "0 if support_infill_rate == 0 else (support_line_width * 100) / support_infill_rate * (2 if support_pattern == 'grid' else (3 if support_pattern == 'triangles' else 1))", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -5107,7 +5107,7 @@ "description": "Skip one in every N connection lines to make the support structure easier to break away.", "type": "int", "default_value": 5, - "value": "round(support_skip_zag_per_mm / support_line_distance)", + "value": "0 if support_line_distance == 0 else round(support_skip_zag_per_mm / support_line_distance)", "minimum_value": "1", "minimum_value_warning": "3", "enabled": "support_enable and (support_pattern == 'zigzag') and support_skip_some_zags", From 085e933e344e71848ca9a02c228b33064537d2f9 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 17 Nov 2017 10:59:14 +0100 Subject: [PATCH 05/27] Move ironing into shell category It is no longer considered experimental. --- resources/definitions/fdmprinter.def.json | 236 +++++++++++----------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 6628086179..bf65baf1d9 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1340,6 +1340,124 @@ "type": "int", "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true + }, + "ironing_enabled": + { + "label": "Enable Ironing", + "description": "Go over the top surface one additional time, but without extruding material. This is meant to melt the plastic on top further, creating a smoother surface.", + "type": "bool", + "default_value": false, + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "ironing_only_highest_layer": + { + "label": "Iron Only Highest Layer", + "description": "Only perform ironing on the very last layer of the mesh. This saves time if the lower layers don't need a smooth surface finish.", + "type": "bool", + "default_value": false, + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "ironing_pattern": + { + "label": "Ironing Pattern", + "description": "The pattern to use for ironing top surfaces.", + "type": "enum", + "options": + { + "concentric": "Concentric", + "zigzag": "Zig Zag" + }, + "default_value": "zigzag", + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "ironing_line_spacing": + { + "label": "Ironing Line Spacing", + "description": "The distance between the lines of ironing.", + "type": "float", + "unit": "mm", + "default_value": 0.1, + "minimum_value": "0.001", + "maximum_value_warning": "machine_nozzle_tip_outer_diameter", + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "ironing_flow": + { + "label": "Ironing Flow", + "description": "The amount of material, relative to a normal skin line, to extrude during ironing. Keeping the nozzle filled helps filling some of the crevices of the top surface, but too much results in overextrusion and blips on the side of the surface.", + "type": "float", + "unit": "%", + "default_value": 10.0, + "minimum_value": "0", + "maximum_value_warning": "50", + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "ironing_inset": + { + "label": "Ironing Inset", + "description": "A distance to keep from the edges of the model. Ironing all the way to the edge of the mesh may result in a jagged edge on your print.", + "type": "float", + "unit": "mm", + "default_value": 0.35, + "value": "wall_line_width_0 / 2", + "minimum_value_warning": "0", + "maximum_value_warning": "wall_line_width_0", + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "speed_ironing": + { + "label": "Ironing Speed", + "description": "The speed at which to pass over the top surface.", + "type": "float", + "unit": "mm/s", + "default_value": 20.0, + "value": "speed_topbottom * 20 / 30", + "minimum_value": "0.001", + "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", + "maximum_value_warning": "100", + "enabled": "ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "acceleration_ironing": + { + "label": "Ironing Acceleration", + "description": "The acceleration with which ironing is performed.", + "unit": "mm/s²", + "type": "float", + "minimum_value": "0.1", + "minimum_value_warning": "100", + "maximum_value_warning": "10000", + "default_value": 3000, + "value": "acceleration_topbottom", + "enabled": "resolveOrValue('acceleration_enabled') and ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true + }, + "jerk_ironing": + { + "label": "Ironing Jerk", + "description": "The maximum instantaneous velocity change while performing ironing.", + "unit": "mm/s", + "type": "float", + "minimum_value": "0", + "maximum_value_warning": "50", + "default_value": 20, + "value": "jerk_topbottom", + "enabled": "resolveOrValue('jerk_enabled') and ironing_enabled", + "limit_to_extruder": "top_bottom_extruder_nr", + "settable_per_mesh": true } } }, @@ -5813,124 +5931,6 @@ "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false - }, - "ironing_enabled": - { - "label": "Enable Ironing", - "description": "Go over the top surface one additional time, but without extruding material. This is meant to melt the plastic on top further, creating a smoother surface.", - "type": "bool", - "default_value": false, - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "ironing_only_highest_layer": - { - "label": "Iron Only Highest Layer", - "description": "Only perform ironing on the very last layer of the mesh. This saves time if the lower layers don't need a smooth surface finish.", - "type": "bool", - "default_value": false, - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "ironing_pattern": - { - "label": "Ironing Pattern", - "description": "The pattern to use for ironing top surfaces.", - "type": "enum", - "options": - { - "concentric": "Concentric", - "zigzag": "Zig Zag" - }, - "default_value": "zigzag", - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "ironing_line_spacing": - { - "label": "Ironing Line Spacing", - "description": "The distance between the lines of ironing.", - "type": "float", - "unit": "mm", - "default_value": 0.1, - "minimum_value": "0.001", - "maximum_value_warning": "machine_nozzle_tip_outer_diameter", - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "ironing_flow": - { - "label": "Ironing Flow", - "description": "The amount of material, relative to a normal skin line, to extrude during ironing. Keeping the nozzle filled helps filling some of the crevices of the top surface, but too much results in overextrusion and blips on the side of the surface.", - "type": "float", - "unit": "%", - "default_value": 10.0, - "minimum_value": "0", - "maximum_value_warning": "50", - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "ironing_inset": - { - "label": "Ironing Inset", - "description": "A distance to keep from the edges of the model. Ironing all the way to the edge of the mesh may result in a jagged edge on your print.", - "type": "float", - "unit": "mm", - "default_value": 0.35, - "value": "wall_line_width_0 / 2", - "minimum_value_warning": "0", - "maximum_value_warning": "wall_line_width_0", - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "speed_ironing": - { - "label": "Ironing Speed", - "description": "The speed at which to pass over the top surface.", - "type": "float", - "unit": "mm/s", - "default_value": 20.0, - "value": "speed_topbottom * 20 / 30", - "minimum_value": "0.001", - "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", - "maximum_value_warning": "100", - "enabled": "ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "acceleration_ironing": - { - "label": "Ironing Acceleration", - "description": "The acceleration with which ironing is performed.", - "unit": "mm/s²", - "type": "float", - "minimum_value": "0.1", - "minimum_value_warning": "100", - "maximum_value_warning": "10000", - "default_value": 3000, - "value": "acceleration_topbottom", - "enabled": "resolveOrValue('acceleration_enabled') and ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true - }, - "jerk_ironing": - { - "label": "Ironing Jerk", - "description": "The maximum instantaneous velocity change while performing ironing.", - "unit": "mm/s", - "type": "float", - "minimum_value": "0", - "maximum_value_warning": "50", - "default_value": 20, - "value": "jerk_topbottom", - "enabled": "resolveOrValue('jerk_enabled') and ironing_enabled", - "limit_to_extruder": "top_bottom_extruder_nr", - "settable_per_mesh": true } } }, From da14ce5bef7b2ad56dc9942a26e1cffcd854f38e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 17 Nov 2017 11:46:55 +0100 Subject: [PATCH 06/27] Add to all installation to lib/ folders Some systems have a suffix there to indicate the address size, such as lib64. --- CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 44b4b57f54..9296c4ce4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ find_package(PythonInterp 3.5.0 REQUIRED) install(DIRECTORY resources DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) install(DIRECTORY plugins - DESTINATION lib/cura) + DESTINATION lib${LIB_SUFFIX}/cura) if(NOT APPLE AND NOT WIN32) install(FILES cura_app.py DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -47,16 +47,16 @@ if(NOT APPLE AND NOT WIN32) RENAME cura) if(EXISTS /etc/debian_version) 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) 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() 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) 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() install(FILES ${CMAKE_BINARY_DIR}/cura.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) @@ -72,8 +72,8 @@ else() DESTINATION ${CMAKE_INSTALL_BINDIR} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) 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) 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() From 657a91c525ee7743bce7b51fd1dfbbf3ac54a67e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 17 Nov 2017 13:09:54 +0100 Subject: [PATCH 07/27] Merge pull request #2685 from Ultimaker/container_stack_improvements Container stack improvements --- .gitignore | 1 + cura/BuildVolume.py | 94 ++---- cura/ConvexHullDecorator.py | 21 +- cura/CuraApplication.py | 29 +- cura/Settings/CuraContainerRegistry.py | 74 ++++- cura/Settings/CuraContainerStack.py | 4 +- cura/Settings/CuraStackBuilder.py | 45 ++- cura/Settings/ExtruderManager.py | 298 +++++------------- cura/Settings/ExtruderStack.py | 5 + cura/Settings/ExtrudersModel.py | 70 ++-- cura/Settings/GlobalStack.py | 11 +- cura/Settings/MachineManager.py | 206 ++++++------ cura/Settings/ProfilesModel.py | 69 ++-- cura/Settings/QualityAndUserProfilesModel.py | 42 +-- cura/Settings/QualitySettingsModel.py | 1 - cura/Settings/SettingInheritanceManager.py | 25 +- cura/Settings/SettingOverrideDecorator.py | 6 +- cura/Settings/UserProfilesModel.py | 42 +-- plugins/3MFReader/ThreeMFReader.py | 17 +- plugins/3MFReader/ThreeMFWorkspaceReader.py | 31 +- plugins/3MFWriter/ThreeMFWriter.py | 2 +- plugins/CuraEngineBackend/StartSliceJob.py | 23 +- .../MachineSettingsAction.py | 104 ++---- .../PerObjectSettingsTool.py | 37 +-- plugins/SliceInfoPlugin/SliceInfo.py | 11 +- plugins/SolidView/SolidView.py | 46 +-- .../UM2UpgradeSelection.py | 4 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 2 +- resources/definitions/101Hero.def.json | 4 - resources/definitions/3dator.def.json | 7 +- resources/definitions/fdmextruder.def.json | 3 +- resources/qml/Cura.qml | 2 +- resources/qml/ExtruderButton.qml | 2 +- resources/qml/Menus/ContextMenu.qml | 2 +- resources/qml/Menus/MaterialMenu.qml | 16 +- resources/qml/Menus/NozzleMenu.qml | 14 +- resources/qml/Menus/ProfileMenu.qml | 2 +- resources/qml/Preferences/ProfilesPage.qml | 2 +- resources/qml/PrintMonitor.qml | 2 +- resources/qml/Settings/SettingItem.qml | 2 +- resources/qml/Settings/SettingView.qml | 8 +- resources/qml/SidebarHeader.qml | 4 +- resources/qml/SidebarSimple.qml | 2 +- 43 files changed, 556 insertions(+), 836 deletions(-) diff --git a/.gitignore b/.gitignore index 570c932d28..ac1e8eba92 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ plugins/ProfileFlattener plugins/cura-god-mode-plugin plugins/cura-big-flame-graph plugins/cura-siemensnx-plugin +plugins/CuraVariSlicePlugin #Build stuff CMakeCache.txt diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index e87bfebd94..50f63e49d5 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -876,15 +876,6 @@ class BuildVolume(SceneNode): 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. # # For single extrusion machines, this gets the setting from the global @@ -899,44 +890,6 @@ class BuildVolume(SceneNode): all_values[i] = 0 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. # # This disallowed radius is to allow for space around the models that is @@ -945,6 +898,7 @@ class BuildVolume(SceneNode): def _getEdgeDisallowedSize(self): if not self._global_container_stack: return 0 + container_stack = self._global_container_stack used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() @@ -953,32 +907,44 @@ class BuildVolume(SceneNode): 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") + 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": - skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap") - skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count") - 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: - 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 - #We don't create an additional line for the extruder we're printing the skirt with. - bed_adhesion_size -= self._getSettingFromAdhesionExtruder("skirt_brim_line_width", "value") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor", "value") / 100.0 + skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value") + skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value") + + 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 + + # 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": - bed_adhesion_size = self._getSettingFromAdhesionExtruder("skirt_brim_line_width") * self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor") / 100.0 - if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: - 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 - #We don't create an additional line for the extruder we're printing the brim with. - bed_adhesion_size -= self._getSettingFromAdhesionExtruder("skirt_brim_line_width", "value") * self._getSettingFromAdhesionExtruder("initial_layer_line_width_factor", "value") / 100.0 + brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value") + 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 + + # 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": - bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin") + bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") + elif adhesion_type == "none": bed_adhesion_size = 0 + else: raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?") support_expansion = 0 - if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"): - support_expansion += self._getSettingFromSupportInfillExtruder("support_offset") + support_enabled = self._global_container_stack.getProperty("support_enable", "value") + support_offset = self._global_container_stack.getProperty("support_offset", "value") + if support_enabled and support_offset: + support_expansion += support_offset farthest_shield_distance = 0 if container_stack.getProperty("draft_shield_enabled", "value"): diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index bfeb690192..50fa8ce7f6 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -302,24 +302,23 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() ## 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") if per_mesh_stack: - return per_mesh_stack.getProperty(setting_key, property) - - multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1 - if not multi_extrusion: - return self._global_stack.getProperty(setting_key, property) + return per_mesh_stack.getProperty(setting_key, prop) 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") - 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 = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] - return extruder_stack.getProperty(setting_key, property) - else: #Limit_to_extruder is set. The global stack handles this then. - return self._global_stack.getProperty(setting_key, property) + return extruder_stack.getProperty(setting_key, prop) + else: + # 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. def __isDescendant(self, root, node): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 96ca15741e..8c1ee8fc36 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -200,6 +200,7 @@ class CuraApplication(QtApplication): self._machine_action_manager = MachineActionManager.MachineActionManager() self._machine_manager = None # This is initialized on demand. + self._extruder_manager = None self._material_manager = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None @@ -260,14 +261,17 @@ class CuraApplication(QtApplication): # Since they are empty, they should never be serialized and instead just programmatically created. # We need them to simplify the switching between materials. empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer() + empty_variant_container = copy.deepcopy(empty_container) empty_variant_container._id = "empty_variant" empty_variant_container.addMetaDataEntry("type", "variant") ContainerRegistry.getInstance().addContainer(empty_variant_container) + empty_material_container = copy.deepcopy(empty_container) empty_material_container._id = "empty_material" empty_material_container.addMetaDataEntry("type", "material") ContainerRegistry.getInstance().addContainer(empty_material_container) + empty_quality_container = copy.deepcopy(empty_container) empty_quality_container._id = "empty_quality" empty_quality_container.setName("Not Supported") @@ -275,6 +279,7 @@ class CuraApplication(QtApplication): empty_quality_container.addMetaDataEntry("type", "quality") empty_quality_container.addMetaDataEntry("supported", False) ContainerRegistry.getInstance().addContainer(empty_quality_container) + empty_quality_changes_container = copy.deepcopy(empty_container) empty_quality_changes_container._id = "empty_quality_changes" empty_quality_changes_container.addMetaDataEntry("type", "quality_changes") @@ -425,7 +430,7 @@ class CuraApplication(QtApplication): def discardOrKeepProfileChangesClosed(self, option): if option == "discard": 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() global_stack.getTop().clear() @@ -433,7 +438,7 @@ class CuraApplication(QtApplication): # before slicing. To ensure that slicer uses right settings values elif option == "keep": 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() if user_extruder_container: user_extruder_container.update() @@ -699,16 +704,13 @@ class CuraApplication(QtApplication): 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. - ExtruderManager.getInstance() + qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager) qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager) qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager) - qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", - self.getSettingInheritanceManager) - qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", - self.getSimpleModeSettingsManager) - + qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager) + qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) + self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles)) @@ -733,6 +735,11 @@ class CuraApplication(QtApplication): self._machine_manager = MachineManager.createMachineManager() 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): if self._material_manager is None: self._material_manager = MaterialManager.createMaterialManager() @@ -783,7 +790,6 @@ class CuraApplication(QtApplication): qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") - qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel") qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel) qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel") @@ -793,15 +799,12 @@ class CuraApplication(QtApplication): qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel") - 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. actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions") - engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.getInstance()) - for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles): type_name = os.path.splitext(os.path.basename(path))[0] if type_name in ("Cura", "Actions"): diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index e623bd860b..5cfe867bc2 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -17,7 +17,7 @@ from UM.Application import Application from UM.Logger import Logger from UM.Message import Message 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 . import ExtruderStack @@ -42,12 +42,13 @@ class CuraContainerRegistry(ContainerRegistry): # Global stack based on metadata information. @override(ContainerRegistry) def addContainer(self, container): + # Note: Intentional check with type() because we want to ignore subclasses if type(container) == ContainerStack: container = self._convertContainerStack(container) 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 actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) if required_setting_version != actual_setting_version: @@ -256,7 +257,8 @@ class CuraContainerRegistry(ContainerRegistry): @override(ContainerRegistry) def load(self): super().load() - self._fixupExtruders() + self._registerSingleExtrusionMachinesExtruderStacks() + self._connectUpgradedExtruderStacksToMachines() ## Update an imported profile to match the current machine configuration. # @@ -299,10 +301,13 @@ class CuraContainerRegistry(ContainerRegistry): machine_definition = Application.getInstance().getGlobalContainerStack().getBottom() del quality_type_criteria["definition"] - materials = None + + # materials = None + 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"] + # Do not filter quality containers here with materials because we are trying to import a profile, so it should # NOT be restricted by the active materials on the current machine. materials = None @@ -360,8 +365,8 @@ class CuraContainerRegistry(ContainerRegistry): return global_container_stack.material.getId() return "" - ## Returns true if the current machien requires its own quality profiles - # \return 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 machine requires its own quality profiles def _machineHasOwnQualities(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: @@ -394,12 +399,65 @@ class CuraContainerRegistry(ContainerRegistry): return new_stack + def _registerSingleExtrusionMachinesExtruderStacks(self): + machines = ContainerRegistry.getInstance().findContainerStacks(machine_extruder_trains = {"0": "fdmextruder"}) + for machine in machines: + self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") + + def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id): + new_extruder_id = extruder_id + extruder_stack = None + + # if extruders are defined in the machine definition use those instead + if machine.extruders and len(machine.extruders) > 0: + new_extruder_id = machine.extruders["0"].getId() + extruder_stack = machine.extruders["0"] + + # if the extruder stack doesn't exist yet we create and add it + if not extruder_stack: + 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")) + extruder_stack.setNextStack(machine) + + if machine.userChanges: + # set existing user changes if found + extruder_stack.setUserChanges(machine.userChanges) + else: + # 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(extruder_definition) + extruder_stack.setUserChanges(user_container) + self.addContainer(user_container) + + variant_id = "default" + if machine.variant.getId() != "empty_variant": + variant_id = machine.variant.getId() + extruder_stack.setVariantById(variant_id) + extruder_stack.setMaterialById("default") + extruder_stack.setQualityById("default") + + self.addContainer(extruder_stack) + # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. # 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 # set after upgrading, because the proper global stack was not yet loaded. This method # makes sure those extruders also get the right stack set. - def _fixupExtruders(self): + def _connectUpgradedExtruderStacksToMachines(self): extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack) for extruder_stack in extruder_stacks: if extruder_stack.getNextStack(): diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 2d3bf683f6..2a804def4d 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -396,7 +396,9 @@ class CuraContainerStack(ContainerStack): # \note This method assumes the stack has a valid machine definition. def findDefaultVariant(self) -> Optional[ContainerInterface]: 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. return None diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 09815da319..a661237722 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -47,21 +47,40 @@ class CuraStackBuilder: new_global_stack.setName(generated_name) - 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) + extruder_definition = registry.findDefinitionContainers(machine = machine_definition.getId()) - 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_id, - definition = extruder_definition, - machine_definition = machine_definition, - quality = "default", - material = "default", - variant = "default", - next_stack = new_global_stack + definition=extruder_definition, + machine_definition=machine_definition, + quality="default", + material="default", + variant="default", + 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 = machine_definition, + quality = "default", + material = "default", + variant = "default", + next_stack = new_global_stack + ) + new_global_stack.addExtruder(new_extruder) return new_global_stack @@ -79,7 +98,9 @@ class CuraStackBuilder: stack.setName(definition.getName()) stack.setDefinition(definition) stack.addMetaDataEntry("position", definition.getMetaDataEntry("position")) - if "next_stack" in kwargs: #Add stacks before containers are added, since they may trigger a setting update. + + if "next_stack" in kwargs: + # Add stacks before containers are added, since they may trigger a setting update. stack.setNextStack(kwargs["next_stack"]) user_container = InstanceContainer(new_stack_id + "_user") diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index c8daca7f92..34b283107d 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -1,21 +1,18 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant #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.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.Decorators import deprecated from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator -from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers by ID. -from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.SettingFunction import SettingFunction from UM.Settings.ContainerStack import ContainerStack -from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext 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. 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. extrudersChanged = pyqtSignal(QVariant) @@ -38,18 +49,6 @@ class ExtruderManager(QObject): ## Notify when the user switches the currently active extruder. 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. # # The currently active extruder stack is the stack that is currently being @@ -59,10 +58,10 @@ class ExtruderManager(QObject): @pyqtProperty(str, notify = activeExtruderChanged) def activeExtruderStackId(self) -> Optional[str]: if not Application.getInstance().getGlobalContainerStack(): - return None # No active machine, so no active extruder. + return None # No active machine, so no active extruder. try: 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 extruder count according to extruder trains. @@ -76,23 +75,23 @@ class ExtruderManager(QObject): return 0 ## 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) def extruderIds(self): extruder_stack_ids = {} + global_stack_id = Application.getInstance().getGlobalContainerStack().getId() - extruder_stack_ids["-1"] = global_stack_id + if global_stack_id in self._extruder_trains: for position in self._extruder_trains[global_stack_id]: extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId() + return extruder_stack_ids @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()]: extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position] - if extruder.getId() == id: + if extruder.getId() == extruder_stack_id: return extruder.qualityChanges.getId() ## 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. __instance = None + @staticmethod + def createExtruderManager(): + return ExtruderManager().getInstance() + ## Gets an instance of the extruder manager, or creates one if no instance # exists yet. # @@ -185,6 +188,7 @@ class ExtruderManager(QObject): if global_container_stack.getId() in self._extruder_trains: 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 None ## Get an extruder stack by index @@ -203,40 +207,6 @@ class ExtruderManager(QObject): result.append(self.getExtruderStack(i)) 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): changed = False @@ -256,138 +226,6 @@ class ExtruderManager(QObject): if changed: 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): 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 property \type{str} The property to get. # \return \type{List} the list of results - def getAllExtruderSettings(self, setting_key, property): - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack.getProperty("machine_extruder_count", "value") <= 1: - return [global_container_stack.getProperty(setting_key, property)] - + def getAllExtruderSettings(self, setting_key: str, prop: str): result = [] for index in self.extruderIds: extruder_stack_id = self.extruderIds[str(index)] - stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] - result.append(stack.getProperty(setting_key, property)) + extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] + result.append(extruder_stack.getProperty(setting_key, prop)) return result ## Gets the extruder stacks that are actually being used at the moment. @@ -422,20 +256,25 @@ class ExtruderManager(QObject): global_stack = Application.getInstance().getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() - if global_stack.getProperty("machine_extruder_count", "value") <= 1: #For single extrusion. - return [global_stack] - 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_bottom_enabled = False support_roof_enabled = False + 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: 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"] used_extruder_stack_ids.add(extruder_stack_id) @@ -471,9 +310,10 @@ class ExtruderManager(QObject): if support_roof_enabled: 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": used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))]) + try: 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. @@ -520,10 +360,6 @@ class ExtruderManager(QObject): result = [] machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value") - # 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()]): result.append(self._extruder_trains[global_stack.getId()][extruder]) @@ -536,24 +372,39 @@ class ExtruderManager(QObject): self._global_container_stack_definition_id = global_container_stack.getBottom().getId() self.globalContainerStackDefinitionChanged.emit() - # If the global container changed, the number of extruders could be changed and so the active_extruder_index is updated - extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") - 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() + # If the global container changed, the machine changed and might have extruders that were not registered yet + self._addCurrentMachineExtruders() self.resetSelectedObjectExtruders() ## Adds the extruders of the currently active machine. def _addCurrentMachineExtruders(self) -> None: global_stack = Application.getInstance().getGlobalContainerStack() - if global_stack and global_stack.getBottom(): - self.addMachineExtruders(global_stack.getBottom(), global_stack.getId()) + extruders_changed = False + + 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. # @@ -632,7 +483,7 @@ class ExtruderManager(QObject): # # 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 @pyqtSlot(str, result="QVariant") @@ -656,7 +507,8 @@ class ExtruderManager(QObject): value = extruder.getRawProperty(key, "value") if isinstance(value, SettingFunction): value = value(extruder) - else: #Just a value from global. + else: + # Just a value from global. value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value") return value diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index d8ff6645a8..fe7068b7ea 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -115,6 +115,11 @@ class ExtruderStack(CuraContainerStack): if has_global_dependencies: 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( name = "application/x-cura-extruderstack", diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py index b13e51723b..40d13461cc 100644 --- a/cura/Settings/ExtrudersModel.py +++ b/cura/Settings/ExtrudersModel.py @@ -71,13 +71,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): self._add_global = False self._simple_names = False - self._active_machine_extruders = [] # type: Iterable[ExtruderStack] + self._active_machine_extruders = [] # type: Iterable[ExtruderStack] self._add_optional_extruder = False - #Listen to changes. - 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. - self._extrudersChanged() #Also calls _updateExtruders. + # Listen to changes + 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 + self._extrudersChanged() # Also calls _updateExtruders def setAddGlobal(self, add): if add != self._add_global: @@ -128,21 +128,24 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): def _extrudersChanged(self, machine_id = None): if machine_id is not 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(): - return #Not the current machine. - #Unlink from old extruders. + # Not the current machine + return + + # Unlink from old extruders for extruder in self._active_machine_extruders: extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged) - #Link to new extruders. + # Link to new extruders self._active_machine_extruders = [] extruder_manager = ExtruderManager.getInstance() for extruder in extruder_manager.getExtruderStacks(): extruder.containersChanged.connect(self._onExtruderStackContainersChanged) 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): # Update when there is an empty container or material change @@ -150,7 +153,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 self._updateExtruders() - modelChanged = pyqtSignal() def _updateExtruders(self): @@ -161,14 +163,17 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): # This should be called whenever the list of extruders changes. @UM.FlameProfiler.profile def __updateExtruders(self): - changed = False + extruders_changed = False if self.rowCount() != 0: - changed = True + extruders_changed = True items = [] + global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: + + # TODO: remove this - CURA-4482 if self._add_global: material = global_container_stack.material color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0] @@ -180,40 +185,44 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): "definition": "" } items.append(item) - changed = True + extruders_changed = True + # get machine extruder count for verification 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 ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()): position = extruder.getMetaDataEntry("position", default = "0") # Get the position try: position = int(position) - except ValueError: #Not a proper int. + except ValueError: + # Not a proper int. position = -1 if position >= machine_extruder_count: 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] - color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color - item = { #Construct an item with only the relevant information. + default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0] + color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color + + # construct an item with only the relevant information + item = { "id": extruder.getId(), - "name": extruder_name, + "name": extruder.getName(), "color": color, "index": position, "definition": extruder.getBottom().getId(), - "material": material.getName() if material else "", - "variant": variant.getName() if variant else "", + "material": extruder.material.getName() if extruder.material 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"]) + # 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: item = { "id": "", @@ -223,5 +232,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): "definition": "" } items.append(item) + self.setItems(items) self.modelChanged.emit() diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index 88218c2f1e..2f27c4ab72 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -23,9 +23,9 @@ class GlobalStack(CuraContainerStack): def __init__(self, container_id: str, *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 # and if so, to bypass the resolve to prevent an infinite recursion that would occur @@ -61,13 +61,6 @@ class GlobalStack(CuraContainerStack): # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we # already have the maximum number of extruders. 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") 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) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index f85acc164d..0daf54c018 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -107,9 +107,8 @@ class MachineManager(QObject): if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacks(id = active_machine_id): # An active machine was saved, so restore it. 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 - ExtruderManager.getInstance().setActiveExtruderIndex(0) + # Make sure _active_container_stack is properly initiated + ExtruderManager.getInstance().setActiveExtruderIndex(0) self._auto_materials_changed = {} self._auto_hotends_changed = {} @@ -162,7 +161,7 @@ class MachineManager(QObject): @pyqtProperty(int, constant=True) 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: if not self._global_container_stack: @@ -258,13 +257,13 @@ class MachineManager(QObject): if old_index is not None: 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): if self._global_container_stack: try: 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 try: self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged) @@ -274,52 +273,38 @@ class MachineManager(QObject): self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) except TypeError: pass - material = self._global_container_stack.material - material.nameChanged.disconnect(self._onMaterialNameChanged) - quality = self._global_container_stack.quality - quality.nameChanged.disconnect(self._onQualityNameChanged) - - 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) + 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.globalContainerChanged.emit() + # after switching the global stack we reconnect all the signals and set the variant and material references if self._global_container_stack: Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId()) + self._global_container_stack.nameChanged.connect(self._onMachineNameChanged) self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged) self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) - if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1: - # For multi-extrusion machines, we do not want variant or material profiles in the stack, - # because these are extruder specific and may cause wrong values to be used for extruders - # that did not specify a value in the extruder. - global_variant = self._global_container_stack.variant - if global_variant != self._empty_variant_container: - self._global_container_stack.setVariant(self._empty_variant_container) + # set the global variant to empty as we now use the extruder stack at all times - CURA-4482 + 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 - if global_material != self._empty_material_container: - self._global_container_stack.setMaterial(self._empty_material_container) + # set the global material to empty as we now use the extruder stack at all times - CURA-4482 + global_material = self._global_container_stack.material + 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. - 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() + # Listen for changes on all extruder stacks + for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks(): + extruder_stack.propertyChanged.connect(self._onPropertyChanged) + extruder_stack.containersChanged.connect(self._onInstanceContainersChanged) self._error_check_timer.start() @@ -336,8 +321,6 @@ class MachineManager(QObject): old_active_container_stack = self._active_container_stack 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() @@ -384,15 +367,6 @@ class MachineManager(QObject): else: 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: if self._global_container_stack is None: #No active machine. return False @@ -732,15 +706,13 @@ class MachineManager(QObject): ## Copy the value of the setting of the current extruder to all other extruders as well as the global container. @pyqtSlot(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") - stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] - stacks.append(self._global_container_stack) - for extruder_stack in stacks: + extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] + + # 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: - 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 # Depending on from/to material+current variant, a quality profile is chosen and set. @@ -947,35 +919,46 @@ class MachineManager(QObject): global_container_stack = self._global_container_stack if not global_container_stack: return [] + global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom()) - extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - if extruder_stacks: - stacks = extruder_stacks - else: - stacks = [global_container_stack] - for stack in stacks: - material = stack.material + # find qualities for extruders + for extruder_stack in extruder_stacks: + material = extruder_stack.material # 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 quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material]) + if not quality: # No quality profile is found for this quality type. quality = self._empty_quality_container - result.append({"stack": stack, "quality": quality, "quality_changes": empty_quality_changes}) - if extruder_stacks: - # Add an extra entry for the global stack. - global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = "True") + result.append({ + "stack": extruder_stack, + "quality": quality, + "quality_changes": empty_quality_changes + }) - if not global_quality: - global_quality = self._empty_quality_container + # also find a global quality for the machine + 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 @@ -988,10 +971,8 @@ class MachineManager(QObject): quality_manager = QualityManager.getInstance() global_container_stack = self._global_container_stack - global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom()) - - quality_changes_profiles = quality_manager.findQualityChangesByName(quality_changes_name, - global_machine_definition) + global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.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] if global_quality_changes: @@ -1002,27 +983,22 @@ class MachineManager(QObject): material = global_container_stack.material + # find a quality type that matches both machine and materials if self._new_material_container and self._active_container_stack.getId() == global_container_stack.getId(): material = self._new_material_container # For the global stack, find a quality which matches the quality_type in # the quality changes profile and also satisfies any material constraints. 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() - for stack in extruder_stacks: - extruder_definition = quality_manager.getParentMachineDefinition(stack.getBottom()) + # append the extruder quality changes + 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: quality_changes = quality_changes_list[0] else: @@ -1030,24 +1006,39 @@ class MachineManager(QObject): if not quality_changes: quality_changes = self._empty_quality_changes_container - material = stack.material + material = extruder_stack.material - 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 quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material]) - 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 - result.append({"stack": stack, "quality": quality, "quality_changes": quality_changes}) + result.append({ + "stack": extruder_stack, + "quality": quality, + "quality_changes": quality_changes + }) - if extruder_stacks: - global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material], global_quality = "True") - if not global_quality: - global_quality = self._empty_quality_container - result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes}) - else: - result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes}) + # append the global quality changes + global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material], global_quality = "True") + + # if there is not global quality but we're using a single extrusion machine, copy the quality of the first extruder - CURA-4482 + if not global_quality and len(extruder_stacks) == 1: + global_quality = result[0]["quality"] + + # 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 @@ -1156,10 +1147,11 @@ class MachineManager(QObject): @pyqtSlot(str, str) def renameMachine(self, machine_id: str, new_name: str): - containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id) - if containers: - new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName()) - containers[0].setName(new_name) + container_registry = ContainerRegistry.getInstance() + machine_stack = container_registry.findContainerStacks(id = machine_id) + if machine_stack: + new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].getBottom().getName()) + machine_stack[0].setName(new_name) self.globalContainerChanged.emit() @pyqtSlot(str) @@ -1183,15 +1175,14 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = globalContainerChanged) def hasMaterials(self) -> bool: 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 @pyqtProperty(bool, notify = globalContainerChanged) def hasVariants(self) -> bool: 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 ## Property to indicate if a machine has "specialized" material profiles. @@ -1199,8 +1190,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = globalContainerChanged) def filterMaterialsByMachine(self) -> bool: 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 ## Property to indicate if a machine has "specialized" quality profiles. @@ -1208,7 +1198,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = globalContainerChanged) def filterQualityByMachine(self) -> bool: 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 ## Get the Definition ID of a machine (specified by ID) @@ -1221,7 +1211,7 @@ class MachineManager(QObject): return containers[0].getBottom().getId() @staticmethod - def createMachineManager(engine=None, script_engine=None): + def createMachineManager(): return MachineManager() @deprecated("Use ExtruderStack.material = ... and it won't be necessary", "2.7") diff --git a/cura/Settings/ProfilesModel.py b/cura/Settings/ProfilesModel.py index 6353d3ce84..2b75cf1bd2 100644 --- a/cura/Settings/ProfilesModel.py +++ b/cura/Settings/ProfilesModel.py @@ -12,6 +12,11 @@ from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel from cura.QualityManager import QualityManager from cura.Settings.ExtruderManager import ExtruderManager +from typing import List, TYPE_CHECKING + +if TYPE_CHECKING: + from cura.Settings.ExtruderStack import ExtruderStack + ## QML Model for listing the current list of valid quality profiles. # @@ -27,7 +32,6 @@ class ProfilesModel(InstanceContainersModel): self.addRoleName(self.AvailableRole, "available") Application.getInstance().globalContainerStackChanged.connect(self._update) - Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update) Application.getInstance().getMachineManager().activeStackChanged.connect(self._update) Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update) @@ -54,18 +58,12 @@ class ProfilesModel(InstanceContainersModel): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: 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. - extruder_manager = ExtruderManager.getInstance() - active_extruder = extruder_manager.getActiveExtruderStack() - 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] + extruder_stacks = self._getOrderedExtruderStacksList() + materials = [extruder.material for extruder in extruder_stacks] # Fetch the list of usable qualities across all extruders. # The actual list of quality profiles come from the first extruder in the extruder list. @@ -100,32 +98,12 @@ class ProfilesModel(InstanceContainersModel): if global_container_stack is None: return - # 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() - 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 + extruder_stacks = self._getOrderedExtruderStacksList() + container_registry = ContainerRegistry.getInstance() # Get a list of usable/available qualities for this machine and material qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks) - container_registry = ContainerRegistry.getInstance() - machine_manager = Application.getInstance().getMachineManager() - unit = global_container_stack.getBottom().getProperty("layer_height", "unit") if not unit: unit = "" @@ -190,6 +168,8 @@ class ProfilesModel(InstanceContainersModel): yield item continue + machine_manager = Application.getInstance().getMachineManager() + # Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile. quality_type = profile.getMetaDataEntry("quality_type", None) if quality_type: @@ -201,7 +181,8 @@ class ProfilesModel(InstanceContainersModel): else: # No global container stack in the 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: quality = None if quality and quality.hasProperty("layer_height", "value"): @@ -211,13 +192,27 @@ class ProfilesModel(InstanceContainersModel): # Quality has no value for layer height either. Get the layer height from somewhere lower in the stack. skip_until_container = global_container_stack.material - if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): #No material in stack. + if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No material in stack. skip_until_container = global_container_stack.variant - if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): #No variant in stack. + if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No variant in stack. skip_until_container = global_container_stack.getBottom() self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material. yield item - 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_without_unit"] = str(value) diff --git a/cura/Settings/QualityAndUserProfilesModel.py b/cura/Settings/QualityAndUserProfilesModel.py index 9d7d913d5e..2e181c6031 100644 --- a/cura/Settings/QualityAndUserProfilesModel.py +++ b/cura/Settings/QualityAndUserProfilesModel.py @@ -22,47 +22,23 @@ class QualityAndUserProfilesModel(ProfilesModel): # Fetch the list of quality changes. 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) - # 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() 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] - 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 + extruder_stacks = self._getOrderedExtruderStacksList() - # 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. - quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, - extruder_stacks) + quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks) # Filter the quality_change by the list of available quality_types quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list]) - - if multiple_extrusion: - # If the printer has multiple extruders then quality changes related to the current extruder are kept - 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") == 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] + 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") == active_extruder.definition.getMetaDataEntry("quality_definition") or + qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())] return quality_list + filtered_quality_changes diff --git a/cura/Settings/QualitySettingsModel.py b/cura/Settings/QualitySettingsModel.py index 2ab4e2a9b5..d0379dc510 100644 --- a/cura/Settings/QualitySettingsModel.py +++ b/cura/Settings/QualitySettingsModel.py @@ -224,7 +224,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel): if self._extruder_id == "" and settable_per_extruder: continue - label = definition.label if self._i18n_catalog: label = self._i18n_catalog.i18nc(definition.key + " label", label) diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 8c9f5b20d6..0d4cd02cdb 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -47,21 +47,20 @@ class SettingInheritanceManager(QObject): @pyqtSlot(str, str, result = "QStringList") def getOverridesForExtruder(self, key, extruder_index): - multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1 - 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 [] + result = [] - 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: Logger.log("w", "Could not find definition for key [%s] (2)", key) - return [] - result = [] + return result + for key in definitions[0].getAllKeys(): - if self._settingIsOverwritingInheritance(key, extruder): + if self._settingIsOverwritingInheritance(key, extruder_stack): result.append(key) return result @@ -78,8 +77,8 @@ class SettingInheritanceManager(QObject): def _onActiveExtruderChanged(self): new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack() - if not new_active_stack: - new_active_stack = self._global_container_stack + # if not new_active_stack: + # new_active_stack = self._global_container_stack if new_active_stack != self._active_container_stack: # Check if changed if self._active_container_stack: # Disconnect signal from old container (if any) diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 4e0893a35f..5026f9760d 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -27,11 +27,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): self._stack = PerObjectContainerStack(stack_id = id(self)) self._stack.setDirty(False) # This stack does not need to be saved. self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer")) - - if ExtruderManager.getInstance().extruderCount > 1: - self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() - else: - self._extruder_stack = None + self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._stack.propertyChanged.connect(self._onSettingChanged) diff --git a/cura/Settings/UserProfilesModel.py b/cura/Settings/UserProfilesModel.py index aa815ef4aa..5ae9055759 100644 --- a/cura/Settings/UserProfilesModel.py +++ b/cura/Settings/UserProfilesModel.py @@ -22,47 +22,23 @@ class UserProfilesModel(ProfilesModel): # Fetch the list of quality changes. 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) - # 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() 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] - 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 + extruder_stacks = self._getOrderedExtruderStacksList() - # 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. - quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, - extruder_stacks) + quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks) # Filter the quality_change by the list of available quality_types quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list]) - - if multiple_extrusion: - # If the printer has multiple extruders then quality changes related to the current extruder are kept - 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") == 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] + 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") == active_extruder.definition.getMetaDataEntry("quality_definition") or + qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())] return filtered_quality_changes diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index a34bf771d7..786226ae61 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -107,20 +107,13 @@ class ThreeMFReader(MeshReader): um_node.addDecorator(SettingOverrideDecorator()) global_container_stack = Application.getInstance().getGlobalContainerStack() + # Ensure the correct next container for the SettingOverride decorator is set. 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 not 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() - um_node.callDecoration("setActiveExtruder", default_stack_id) + if default_stack: + um_node.callDecoration("setActiveExtruder", default_stack.getId()) # Get the definition & set it definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) @@ -139,7 +132,7 @@ class ThreeMFReader(MeshReader): else: Logger.log("w", "Unable to find extruder in position %s", setting_value) continue - setting_container.setProperty(key,"value", setting_value) + setting_container.setProperty(key, "value", setting_value) if len(um_node.getChildren()) > 0: group_decorator = GroupDecorator() diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 28d12bf2d5..a3aadc79b8 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -644,9 +644,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Get the stack(s) saved in the workspace. Logger.log("d", "Workspace loading is checking stacks containers...") - # -- # load global stack file try: + stack = None + if self._resolve_strategies["machine"] == "override": container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original) stack = container_stacks[0] @@ -682,12 +683,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.addContainer(stack) containers_added.append(stack) else: - Logger.log("e", "Resolve strategy of %s for machine is not supported", - self._resolve_strategies["machine"]) + Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) # Create a new definition_changes container if it was empty 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 Job.yieldThread() except: @@ -697,16 +697,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.removeContainer(container.getId()) 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 try: for extruder_stack_file in extruder_stack_files: @@ -749,9 +739,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Create a new definition_changes container if it was empty if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer(): - stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack._id + "_settings")) - if global_stack.getProperty("machine_extruder_count", "value") > 1: - extruder_stacks.append(stack) + stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings")) + + 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: + self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder") + except: 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. @@ -784,7 +780,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in [global_stack] + extruder_stacks: stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container) - # # 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 # MUST get updated too. diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index a764d30fac..6254bef03a 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -87,7 +87,7 @@ class ThreeMFWriter(MeshWriter): if stack is not None: 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: changed_setting_keys.add("extruder_nr") diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index eb0337c4f2..b922baea52 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -159,13 +159,9 @@ class StartSliceJob(Job): self._buildGlobalSettingsMessage(stack) self._buildGlobalInheritsStackMessage(stack) - # Only add extruder stacks if there are multiple extruders - # Single extruder machines only use the global stack to store setting values - if stack.getProperty("machine_extruder_count", "value") > 1: - for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): - self._buildExtruderMessage(extruder_stack) - else: - self._buildExtruderMessageFromGlobalStack(stack) + # Build messages for extruder stacks + for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): + self._buildExtruderMessage(extruder_stack) for group in object_groups: group_message = self._slice_message.addRepeatedMessage("object_lists") @@ -251,19 +247,6 @@ class StartSliceJob(Job): 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() - ## Sends all global settings to the engine. # # The settings are taken from the global stack. This does not include any diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 360dae7a2c..2de5b6e9b8 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -116,8 +116,7 @@ class MachineSettingsAction(MachineAction): @pyqtSlot(int) def setMachineExtruderCount(self, extruder_count): - machine_manager = Application.getInstance().getMachineManager() - extruder_manager = ExtruderManager.getInstance() + extruder_manager = Application.getInstance().getExtruderManager() definition_changes_container = self._global_container_stack.definitionChanges 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: 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 for setting_instance in self._global_container_stack.userChanges.findInstances(): setting_key = setting_instance.definition.key @@ -177,52 +148,29 @@ class MachineSettingsAction(MachineAction): definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) - if extruder_count > 1: - # Multiextrusion + # Make sure one of the extruder stacks is active + extruder_manager.setActiveExtruderIndex(0) - # Make sure one of the extruder stacks is active - if extruder_manager.activeExtruderIndex == -1: - extruder_manager.setActiveExtruderIndex(0) + # Move settable_per_extruder values out of the global container + # After CURA-4482 this should not be the case anymore, but we still want to support older project files. + global_user_container = self._global_container_stack.getTop() - # Move settable_per_extruder values out of the global container - if previous_extruder_count == 1: - extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - global_user_container = self._global_container_stack.getTop() + if previous_extruder_count == 1: + extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() + global_user_container = self._global_container_stack.getTop() - for setting_instance in global_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 = 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 + for setting_instance in global_user_container.findInstances(): + setting_key = setting_instance.definition.key + settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") - # Make sure the machine stack is active - if extruder_manager.activeExtruderIndex > -1: - extruder_manager.setActiveExtruderIndex(-1) - - # Restore material and variant on global stack - # 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) + 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) self.forceUpdate() - @pyqtSlot() def forceUpdate(self): # Force rebuilding the build volume by reloading the global container stack. @@ -275,16 +223,13 @@ class MachineSettingsAction(MachineAction): if not self._global_container_stack.getMetaDataEntry("has_materials", False): return - machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") - if machine_extruder_count > 1: - material = ExtruderManager.getInstance().getActiveExtruderStack().material - else: - material = self._global_container_stack.material + material = ExtruderManager.getInstance().getActiveExtruderStack().material 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_approximate_diameter = str(round(material_diameter)) + material_approximate_diameter = str(round(material_diameter)) definition_changes = self._global_container_stack.definitionChanges machine_diameter = definition_changes.getProperty("material_diameter", "value") if not machine_diameter: @@ -294,10 +239,7 @@ class MachineSettingsAction(MachineAction): if material_approximate_diameter != machine_approximate_diameter: Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.") - if machine_extruder_count > 1: - stacks = ExtruderManager.getInstance().getExtruderStacks() - else: - stacks = [self._global_container_stack] + stacks = ExtruderManager.getInstance().getExtruderStacks() if self._global_container_stack.getMetaDataEntry("has_machine_materials", False): materials_definition = self._global_container_stack.definition.getId() @@ -338,7 +280,7 @@ class MachineSettingsAction(MachineAction): search_criteria["id"] = stack.getMetaDataEntry("preferred_material") materials = self._container_registry.findInstanceContainers(**search_criteria) 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) materials = self._container_registry.findInstanceContainers(**search_criteria) if not materials: diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index d0cb53c4f8..dc6efafa13 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -78,31 +78,26 @@ class PerObjectSettingsTool(Tool): def _onGlobalContainerChanged(self): global_container_stack = Application.getInstance().getGlobalContainerStack() 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 - # Ensure that all extruder data is reset - 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() + extruder_stack = ExtruderManager.getInstance().getExtruderStack(0) - root_node = Application.getInstance().getController().getScene().getRoot() - for node in DepthFirstIterator(root_node): - new_stack_id = default_stack_id - # Get position of old extruder stack for this node - old_extruder_pos = node.callDecoration("getActiveExtruderPosition") - if old_extruder_pos is not None: - # Fetch current (new) extruder stack at position - new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos) - if new_stack: - new_stack_id = new_stack.getId() - node.callDecoration("setActiveExtruder", new_stack_id) + if extruder_stack: + root_node = Application.getInstance().getController().getScene().getRoot() + for node in DepthFirstIterator(root_node): + new_stack_id = extruder_stack.getId() + # Get position of old extruder stack for this node + old_extruder_pos = node.callDecoration("getActiveExtruderPosition") + if old_extruder_pos is not None: + # Fetch current (new) extruder stack at position + new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos) + if new_stack: + new_stack_id = new_stack.getId() + node.callDecoration("setActiveExtruder", new_stack_id) - self._updateEnabled() + self._updateEnabled() def _updateEnabled(self): selected_objects = Selection.getAllSelectedObjects() diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index 0514c4dacf..79963a4740 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -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","")} + # add extruder specific data to slice info data["extruders"] = [] - extruder_count = len(global_container_stack.extruders) - extruders = [] - 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] + extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId())) + extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position")) for extruder in extruders: extruder_dict = dict() diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 8f0c9a4dc1..bc1b08cc5e 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -46,19 +46,10 @@ class SolidView(View): self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*theme.getColor("model_unslicable_alt").getRgb())) self._disabled_shader.setUniformValue("u_width", 50.0) - multi_extrusion = False - global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 - - if multi_extrusion: - 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 + support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value") + support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr) if Preferences.getInstance().getValue("view/show_overhang"): angle = support_angle_stack.getProperty("support_angle", "value") @@ -71,33 +62,26 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) - for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): if node.getMeshData() and node.isVisible(): uniforms = {} shade_factor = 1.0 - if not multi_extrusion: - if global_container_stack: - material = global_container_stack.findContainer({ "type": "material" }) - material_color = material.getMetaDataEntry("color_code", default = self._extruders_model.defaultColors[0]) if material else self._extruders_model.defaultColors[0] - else: - material_color = self._extruders_model.defaultColors[0] - else: - # Get color to render this mesh in from ExtrudersModel - extruder_index = 0 - extruder_id = node.callDecoration("getActiveExtruder") - if extruder_id: - extruder_index = max(0, self._extruders_model.find("id", extruder_id)) - try: - material_color = self._extruders_model.getItem(extruder_index)["color"] - except KeyError: - material_color = self._extruders_model.defaultColors[0] + # Get color to render this mesh in from ExtrudersModel + extruder_index = 0 + extruder_id = node.callDecoration("getActiveExtruder") + if extruder_id: + extruder_index = max(0, self._extruders_model.find("id", extruder_id)) + try: + material_color = self._extruders_model.getItem(extruder_index)["color"] + 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: # 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]) diff --git a/plugins/UltimakerMachineActions/UM2UpgradeSelection.py b/plugins/UltimakerMachineActions/UM2UpgradeSelection.py index c84033a98e..db277bc485 100644 --- a/plugins/UltimakerMachineActions/UM2UpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UM2UpgradeSelection.py @@ -37,7 +37,7 @@ class UM2UpgradeSelection(MachineAction): def setHasVariants(self, has_variants = True): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - variant_container = global_container_stack.variant + variant_container = global_container_stack.extruders["0"].variant variant_index = global_container_stack.getContainerIndex(variant_container) if has_variants: @@ -52,7 +52,7 @@ class UM2UpgradeSelection(MachineAction): search_criteria = { "type": "variant", "definition": "ultimaker2", "id": "*0.4*" } containers = self._container_registry.findInstanceContainers(**search_criteria) if containers: - global_container_stack.variant = containers[0] + global_container_stack.extruders["0"].variant = containers[0] else: # 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. diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 86f90e8970..fa40819eeb 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -549,7 +549,7 @@ class XmlMaterialProfile(InstanceContainer): definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id) if not definitions: - Logger.log("w", "No definition found for machine ID %s", machine_id) + # Logger.log("w", "No definition found for machine ID %s", machine_id) continue definition = definitions[0] diff --git a/resources/definitions/101Hero.def.json b/resources/definitions/101Hero.def.json index 3d19aef626..aaea743b47 100644 --- a/resources/definitions/101Hero.def.json +++ b/resources/definitions/101Hero.def.json @@ -7,10 +7,6 @@ "visible": true, "author": "rikky", "manufacturer": "101Hero", - "machine_extruder_trains": - { - "0": "fdmextruder" - }, "file_formats": "text/x-gcode", "platform": "101hero-platform.stl", "supports_usb_connection": true diff --git a/resources/definitions/3dator.def.json b/resources/definitions/3dator.def.json index b72a49a35b..513ee8f0e1 100644 --- a/resources/definitions/3dator.def.json +++ b/resources/definitions/3dator.def.json @@ -10,11 +10,7 @@ "file_formats": "text/x-gcode", "icon": "icon_ultimaker2", "supports_usb_connection": true, - "platform": "3dator_platform.stl", - "machine_extruder_trains": - { - "0": "fdmextruder" - } + "platform": "3dator_platform.stl" }, "overrides": { @@ -29,7 +25,6 @@ "layer_height": { "default_value": 0.2 }, "speed_print": { "default_value": 50 }, "speed_infill": { "default_value": 60 }, - "machine_extruder_count": { "default_value": 1 }, "machine_heated_bed": { "default_value": true }, "machine_center_is_zero": { "default_value": false }, "machine_height": { "default_value": 260 }, diff --git a/resources/definitions/fdmextruder.def.json b/resources/definitions/fdmextruder.def.json index 8ed194fc2d..ada35fafe0 100644 --- a/resources/definitions/fdmextruder.def.json +++ b/resources/definitions/fdmextruder.def.json @@ -8,7 +8,8 @@ "author": "Ultimaker", "manufacturer": "Unknown", "setting_version": 1, - "visible": false + "visible": false, + "position": "0" }, "settings": { diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 2fd19a8a03..fb2bd3bff2 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -177,7 +177,7 @@ UM.MainWindow MenuSeparator { } - MenuItem { text: catalog.i18nc("@action:inmenu", "Set as Active Extruder"); onTriggered: ExtruderManager.setActiveExtruderIndex(model.index) } + MenuItem { text: catalog.i18nc("@action:inmenu", "Set as Active Extruder"); onTriggered: Cura.ExtruderManager.setActiveExtruderIndex(model.index) } } onObjectAdded: settingsMenu.insertItem(index, object) onObjectRemoved: settingsMenu.removeItem(object) diff --git a/resources/qml/ExtruderButton.qml b/resources/qml/ExtruderButton.qml index 99196b0c9f..9212c705f7 100644 --- a/resources/qml/ExtruderButton.qml +++ b/resources/qml/ExtruderButton.qml @@ -18,7 +18,7 @@ Button style: UM.Theme.styles.tool_button; iconSource: UM.Theme.getIcon("extruder_button") - checked: ExtruderManager.selectedObjectExtruders.indexOf(extruder.id) != -1 + checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(extruder.id) != -1 enabled: UM.Selection.hasSelection property color customColor: base.hovered ? UM.Theme.getColor("button_hover") : UM.Theme.getColor("button"); diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 39d497722f..b2c95ebcd8 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -31,7 +31,7 @@ Menu visible: base.shouldShowExtruders enabled: UM.Selection.hasSelection checkable: true - checked: ExtruderManager.selectedObjectExtruders.indexOf(model.id) != -1 + checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(model.id) != -1 onTriggered: CuraActions.setExtruderForSelection(model.id) shortcut: "Ctrl+" + (model.index + 1) } diff --git a/resources/qml/Menus/MaterialMenu.qml b/resources/qml/Menus/MaterialMenu.qml index d3ecfb69fe..a6666b67f4 100644 --- a/resources/qml/Menus/MaterialMenu.qml +++ b/resources/qml/Menus/MaterialMenu.qml @@ -78,16 +78,16 @@ Menu { text: model.name checkable: true - checked: model.id == Cura.MachineManager.allActiveMaterialIds[ExtruderManager.extruderIds[extruderIndex]] + checked: model.id == Cura.MachineManager.allActiveMaterialIds[Cura.ExtruderManager.extruderIds[extruderIndex]] exclusiveGroup: group onTriggered: { // This workaround is done because of the application menus for materials and variants for multiextrusion printers. // The extruder menu would always act on the correspoding extruder only, instead of acting on the extruder selected in the UI. - var activeExtruderIndex = ExtruderManager.activeExtruderIndex; - ExtruderManager.setActiveExtruderIndex(extruderIndex); + var activeExtruderIndex = Cura.ExtruderManager.activeExtruderIndex; + Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex); Cura.MachineManager.setActiveMaterial(model.id); - ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); + Cura.ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); } } onObjectAdded: menu.insertItem(index, object) @@ -121,16 +121,16 @@ Menu { text: model.name checkable: true - checked: model.id == Cura.MachineManager.allActiveMaterialIds[ExtruderManager.extruderIds[extruderIndex]] + checked: model.id == Cura.MachineManager.allActiveMaterialIds[Cura.ExtruderManager.extruderIds[extruderIndex]] exclusiveGroup: group onTriggered: { // This workaround is done because of the application menus for materials and variants for multiextrusion printers. // The extruder menu would always act on the correspoding extruder only, instead of acting on the extruder selected in the UI. - var activeExtruderIndex = ExtruderManager.activeExtruderIndex; - ExtruderManager.setActiveExtruderIndex(extruderIndex); + var activeExtruderIndex = Cura.ExtruderManager.activeExtruderIndex; + Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex); Cura.MachineManager.setActiveMaterial(model.id); - ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); + Cura.ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); } } onObjectAdded: brandMaterialsMenu.insertItem(index, object) diff --git a/resources/qml/Menus/NozzleMenu.qml b/resources/qml/Menus/NozzleMenu.qml index e9f2df1f38..f70e639872 100644 --- a/resources/qml/Menus/NozzleMenu.qml +++ b/resources/qml/Menus/NozzleMenu.qml @@ -44,15 +44,15 @@ Menu visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].hotendIds.length > extruderIndex && !isClusterPrinter onTriggered: { - var activeExtruderIndex = ExtruderManager.activeExtruderIndex; - ExtruderManager.setActiveExtruderIndex(extruderIndex); + var activeExtruderIndex = Cura.ExtruderManager.activeExtruderIndex; + Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex); var hotendId = Cura.MachineManager.printerOutputDevices[0].hotendIds[extruderIndex]; var itemIndex = nozzleInstantiator.model.find("name", hotendId); if(itemIndex > -1) { Cura.MachineManager.setActiveVariant(nozzleInstantiator.model.getItem(itemIndex).id); } - ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); + Cura.ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); } } @@ -75,14 +75,14 @@ Menu MenuItem { text: model.name checkable: true - checked: model.id == Cura.MachineManager.allActiveVariantIds[ExtruderManager.extruderIds[extruderIndex]] + checked: model.id == Cura.MachineManager.allActiveVariantIds[Cura.ExtruderManager.extruderIds[extruderIndex]] exclusiveGroup: group onTriggered: { - var activeExtruderIndex = ExtruderManager.activeExtruderIndex; - ExtruderManager.setActiveExtruderIndex(extruderIndex); + var activeExtruderIndex = Cura.ExtruderManager.activeExtruderIndex; + Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex); Cura.MachineManager.setActiveVariant(model.id); - ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); + Cura.ExtruderManager.setActiveExtruderIndex(activeExtruderIndex); } } onObjectAdded: menu.insertItem(index, object) diff --git a/resources/qml/Menus/ProfileMenu.qml b/resources/qml/Menus/ProfileMenu.qml index fecea5ef99..edce2641af 100644 --- a/resources/qml/Menus/ProfileMenu.qml +++ b/resources/qml/Menus/ProfileMenu.qml @@ -41,7 +41,7 @@ Menu MenuItem { - text: model.name + " - " + model.layer_height + text: model.name checkable: true checked: Cura.MachineManager.activeQualityChangesId == model.id exclusiveGroup: group diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index c7b6b3e933..e3ba9b23a4 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -208,7 +208,7 @@ UM.ManagementPage anchors.right: parent.right anchors.bottom: parent.bottom - currentIndex: ExtruderManager.extruderCount > 0 ? ExtruderManager.activeExtruderIndex + 1 : 0 + currentIndex: Cura.ExtruderManager.extruderCount > 0 ? Cura.ExtruderManager.activeExtruderIndex + 1 : 0 ProfileTab { diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index ce169ba714..e69f7cf4fd 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -87,7 +87,7 @@ Column Label //Extruder name. { - text: ExtruderManager.getExtruderName(index) != "" ? ExtruderManager.getExtruderName(index) : catalog.i18nc("@label", "Extruder") + text: Cura.ExtruderManager.getExtruderName(index) != "" ? Cura.ExtruderManager.getExtruderName(index) : catalog.i18nc("@label", "Extruder") color: UM.Theme.getColor("text") font: UM.Theme.getFont("default") anchors.left: parent.left diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml index 2bf2c17273..6234e5f1f7 100644 --- a/resources/qml/Settings/SettingItem.qml +++ b/resources/qml/Settings/SettingItem.qml @@ -157,7 +157,7 @@ Item { var tooltipText = catalog.i18nc("@label", "This setting is always shared between all extruders. Changing it here will change the value for all extruders") + "."; if ((resolve != "None") && (stackLevel != 0)) { // We come here if a setting has a resolve and the setting is not manually edited. - tooltipText += " " + catalog.i18nc("@label", "The value is resolved from per-extruder values ") + "[" + ExtruderManager.getInstanceExtruderValues(definition.key) + "]."; + tooltipText += " " + catalog.i18nc("@label", "The value is resolved from per-extruder values ") + "[" + Cura.ExtruderManager.getInstanceExtruderValues(definition.key) + "]."; } base.showTooltip(tooltipText); } diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index c116fa933a..d1c8ae2c51 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -275,7 +275,7 @@ Item Behavior on opacity { NumberAnimation { duration: 100 } } enabled: { - if(!ExtruderManager.activeExtruderStackId && machineExtruderCount.properties.value > 1) + if (!Cura.ExtruderManager.activeExtruderStackId && machineExtruderCount.properties.value > 1) { // disable all controls on the global tab, except categories return model.type == "category" @@ -345,12 +345,12 @@ Item if(inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0) { //We have limit_to_extruder, so pick that stack. - return ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)]; + return Cura.ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)]; } - if(ExtruderManager.activeExtruderStackId) + if(Cura.ExtruderManager.activeExtruderStackId) { //We're on an extruder tab. Pick the current extruder. - return ExtruderManager.activeExtruderStackId; + return Cura.ExtruderManager.activeExtruderStackId; } //No extruder tab is selected. Pick the global stack. Shouldn't happen any more since we removed the global tab. return activeMachineId; diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 78e21f3a68..8bba4cf6fd 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -14,7 +14,7 @@ Column { id: base; - property int currentExtruderIndex: ExtruderManager.activeExtruderIndex; + property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex; property bool currentExtruderVisible: extrudersList.visible; spacing: Math.floor(UM.Theme.getSize("sidebar_margin").width * 0.9) @@ -93,7 +93,7 @@ Column onClicked: { forceActiveFocus() // Changing focus applies the currently-being-typed values so it can change the displayed setting values. - ExtruderManager.setActiveExtruderIndex(index); + Cura.ExtruderManager.setActiveExtruderIndex(index); } style: ButtonStyle diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 549d203c4d..020b75e3ce 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -19,7 +19,7 @@ Item property Action configureSettings; property variant minimumPrintTime: PrintInformation.minimumPrintTime; property variant maximumPrintTime: PrintInformation.maximumPrintTime; - property bool settingsEnabled: ExtruderManager.activeExtruderStackId || machineExtruderCount.properties.value == 1 + property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || machineExtruderCount.properties.value == 1 Component.onCompleted: PrintInformation.enabled = true Component.onDestruction: PrintInformation.enabled = false From dd8e5e2c6e83581137e5dc13910b299b96ac669e Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 17 Nov 2017 15:05:08 +0100 Subject: [PATCH 08/27] CURA-4577 Take into account relative movements in GCodes --- plugins/GCodeReader/GCodeReader.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 9107ec3258..b8e6a8f009 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -135,16 +135,22 @@ class GCodeReader(MeshReader): 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 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 position.z + else: + x = x + params.x if params.x is not None else x + y = y + params.y if params.y is not None else y + z = 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]: + new_extrusion_value = params.e if self._is_absolute_positioning else e[self._extruder_number] + params.e + if new_extrusion_value > 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 + 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. From 10b5a19043d730431cb68d346f02df56452d67b8 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Fri, 17 Nov 2017 15:22:29 +0100 Subject: [PATCH 09/27] Fix merge conflict --- cura/PrintInformation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 3e1ed2f8fa..3353cc176e 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -308,6 +308,9 @@ class PrintInformation(QObject): # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again is_empty = name == "" if 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._updateJobName( empty_name = is_empty) From 85eab3606b459489db6a546598afe5f65419156f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 17 Nov 2017 16:31:16 +0100 Subject: [PATCH 10/27] Simplify relative positioning xyz value updates - CURA-4577 --- plugins/GCodeReader/GCodeReader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index b8e6a8f009..3243a1623e 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -140,9 +140,9 @@ class GCodeReader(MeshReader): y = params.y if params.y is not None else y z = params.z if params.z is not None else position.z else: - x = x + params.x if params.x is not None else x - y = y + params.y if params.y is not None else y - z = z + params.z if params.z is not None else position.z + 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: new_extrusion_value = params.e if self._is_absolute_positioning else e[self._extruder_number] + params.e From a4ed3bb2c6ac08470c3d5d5541268e2328af0e90 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 20 Nov 2017 09:04:07 +0100 Subject: [PATCH 11/27] CURA-4577 Fix the previous change --- plugins/GCodeReader/GCodeReader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 3243a1623e..e8c2a4e0fd 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -138,11 +138,11 @@ class GCodeReader(MeshReader): 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 position.z + z = params.z if params.z is not None else z else: - 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 + 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 if params.e is not None: new_extrusion_value = params.e if self._is_absolute_positioning else e[self._extruder_number] + params.e From b875f06bafe01413261c8ee6e5b69e087efbac83 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 20 Nov 2017 10:51:51 +0100 Subject: [PATCH 12/27] Add setting to set the maximum mesh resolution Implements issue CURA-4590. --- resources/definitions/fdmprinter.def.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index bf65baf1d9..0fedc4accc 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4945,6 +4945,18 @@ "default_value": false, "settable_per_mesh": true }, + "meshfix_maximum_resolution": + { + "label": "Maximum Resolution", + "description": "The minimum size of a line segment after slicing. If you increase this, the mesh will have a lower resolution. This may allow the printer to keep up with the speed it has to process g-code and will increase slice speed by removing details of the mesh that it can't process anyway.", + "type": "float", + "unit": "mm", + "default_value": 0.01, + "minimum_value": "0.001", + "minimum_value_warning": "0.005", + "maximum_value_warning": "0.1", + "settable_per_mesh": true + }, "multiple_mesh_overlap": { "label": "Merged Meshes Overlap", From 8cad79831acbc63d11956396a9050ebab7b84e80 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 20 Nov 2017 13:04:46 +0100 Subject: [PATCH 13/27] Fix empty containers in stack upgrade --- .../VersionUpgrade30to31/VersionUpgrade30to31.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 4672cb1488..7064da09ad 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -45,6 +45,15 @@ _OLD_NOT_SUPPORTED_PROFILES = [ ] +# Some containers have their specific empty containers, those need to be set correctly. +_EMPTY_CONTAINER_DICT = { + "1": "empty_quality_changes", + "2": "empty_quality", + "3": "empty_material", + "4": "empty_variant", +} + + class VersionUpgrade30to31(VersionUpgrade): ## Gets the version number from a CFG file in Uranium's 3.0 format. # @@ -126,6 +135,11 @@ class VersionUpgrade30to31(VersionUpgrade): if quality_profile_id in _OLD_NOT_SUPPORTED_PROFILES: parser["containers"]["2"] = "empty_quality" + # fix empty containers + for key, specific_empty_container in _EMPTY_CONTAINER_DICT: + if parser.has_option("containers", key) and parser["containers"][key] == "empty": + parser["containers"][key] = specific_empty_container + # Update version numbers if "general" not in parser: parser["general"] = {} From d00f3ba79b0fbcb470f9574813aad8067bad6677 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 20 Nov 2017 13:06:02 +0100 Subject: [PATCH 14/27] Fix for loop --- .../VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 7064da09ad..7130871d8e 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -136,7 +136,7 @@ class VersionUpgrade30to31(VersionUpgrade): parser["containers"]["2"] = "empty_quality" # fix empty containers - for key, specific_empty_container in _EMPTY_CONTAINER_DICT: + for key, specific_empty_container in _EMPTY_CONTAINER_DICT.items(): if parser.has_option("containers", key) and parser["containers"][key] == "empty": parser["containers"][key] = specific_empty_container From f197508694e965603e7326c274c610341e98c88c Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 20 Nov 2017 13:35:15 +0100 Subject: [PATCH 15/27] Merge pull request #2814 from Ultimaker/feature_maximum_resolution Maximum mesh resolution --- cura/PrintInformation.py | 2 +- plugins/GCodeReader/GCodeReader.py | 13 ++++++++++++- resources/definitions/cartesio.def.json | 9 ++++++--- resources/extruders/cartesio_extruder_0.def.json | 2 +- resources/extruders/cartesio_extruder_1.def.json | 2 +- resources/extruders/cartesio_extruder_2.def.json | 2 +- resources/extruders/cartesio_extruder_3.def.json | 2 +- .../cartesio/pc/cartesio_0.25_pc_high.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.25_pc_normal.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.4_pc_high.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.4_pc_normal.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.8_pc_coarse.inst.cfg | 2 +- .../pc/cartesio_0.8_pc_extra_coarse.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.8_pc_high.inst.cfg | 2 +- .../cartesio/pc/cartesio_0.8_pc_normal.inst.cfg | 2 +- 15 files changed, 31 insertions(+), 17 deletions(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 3353cc176e..46d9a61254 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -312,7 +312,7 @@ class PrintInformation(QObject): if name.endswith(".curaproject"): name = name[:name.rfind(".curaproject")] self._base_name = name - self._updateJobName( empty_name = is_empty) + self._updateJobName(empty_name = is_empty) ## Created an acronymn-like abbreviated machine name from the currently active machine name # Called each time the global stack is switched diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index e8c2a4e0fd..1b2795800e 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -54,6 +54,7 @@ class GCodeReader(MeshReader): 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) @staticmethod def _getValue(line, code): @@ -172,6 +173,16 @@ class GCodeReader(MeshReader): 0, position.e) + ## Set the absolute positioning + def _gCode90(self, position, params, path): + self._is_absolute_positioning = True + return position + + ## Set the relative positioning + def _gCode91(self, position, params, path): + self._is_absolute_positioning = 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): @@ -202,7 +213,7 @@ class GCodeReader(MeshReader): 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): + 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, e) return func(position, params, path) diff --git a/resources/definitions/cartesio.def.json b/resources/definitions/cartesio.def.json index 45b0111209..5d6a0fca16 100644 --- a/resources/definitions/cartesio.def.json +++ b/resources/definitions/cartesio.def.json @@ -47,21 +47,24 @@ "material_bed_temp_wait": { "default_value": false }, "prime_tower_enable": { "default_value": true }, "prime_tower_wall_thickness": { "resolve": 0.7 }, - "prime_tower_position_x": { "value": "50" }, - "prime_tower_position_y": { "value": "150" }, + "prime_tower_size": { "value": 24.0 }, + "prime_tower_position_x": { "value": 125 }, + "prime_tower_position_y": { "value": 70 }, "prime_blob_enable": { "default_value": false }, "machine_max_feedrate_z": { "default_value": 20 }, "machine_disallowed_areas": { "default_value": [ [[215, 135], [-215, 135], [-215, 75], [215, 75]] ]}, "machine_start_gcode": { - "default_value": "\nM92 E159 ;2288 for V5 extruder\n\nM104 S120 T1\nM104 S120 T2\nM104 S120 T3\n\nG21\nG90\nM42 S255 P13 ;chamber lights\nM42 S255 P12 ;fume extraction\nM204 S300 ;default acceleration\nM205 X10 ;default jerk\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\nG1 Z10 F600\nG1 X70 Y20 F9000;go to wipe point\n\nM190 S{material_bed_temperature_layer_0}\n\nM117 Heating for 50 sec.\nG4 S20\nM117 Heating for 30 sec.\nG4 S20\nM117 Heating for 10 sec.\nM300 S1200 P1000\nG4 S9\n\nM117 purging nozzle....\nT0\nG92 E0;set E\nG1 E10 F100\nG92 E0\nG1 E-1 F600\n\nM117 wiping nozzle....\nG1 X1 Y24 F3000\nG1 X70 F9000\nG1 Z10 F900\n\nM104 S21 T1\nM104 S21 T2\nM104 S21 T3\n\nM117 Printing .....\n" + "default_value": "\nM92 E159 ;2288 for V5 extruder\n\nM140 S{material_bed_temperature_layer_0}\nM104 S120 T1\nM104 S120 T2\nM104 S120 T3\n\nG21\nG90\nM42 S255 P13 ;chamber lights\nM42 S255 P12 ;fume extraction\nM204 S300 ;default acceleration\nM205 X10 ;default jerk\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\nG1 Z10 F600\nG1 X70 Y20 F9000;go to wipe point\n\nM190 S{material_bed_temperature_layer_0}\n\nM117 Heating for 50 sec.\nG4 S20\nM117 Heating for 30 sec.\nG4 S20\nM117 Heating for 10 sec.\nM300 S1200 P1000\nG4 S9\n\nM117 purging nozzle....\nT0\nG92 E0;set E\nG1 E10 F100\nG92 E0\nG1 E-1 F600\n\nM117 wiping nozzle....\nG1 X1 Y24 F3000\nG1 X70 F9000\nG1 Z10 F900\n\nM104 S21 T1\nM104 S21 T2\nM104 S21 T3\n\nM117 Printing .....\n" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM117 cooling down....\nM106 S255\nM140 S5\nM104 S5 T0\nM104 S5 T1\nM104 S5 T2\nM104 S5 T3\n\nG91\nG1 Z1 F900\nG90\n\nG1 X20.0 Y260.0 F6000\nG4 S7\nM84\nG4 S90\nM107\nM42 P12 S0\nM42 P13 S0\nM84\nT0\nM117 Finished.\n; -- end of GCODE --" }, "layer_height": { "maximum_value": "(0.8 * min(extruderValues('machine_nozzle_size')))" }, "layer_height_0": { "maximum_value": "(0.8 * min(extruderValues('machine_nozzle_size')))" }, + "retraction_extra_prime_amount": { "minimum_value_warning": "-2.0" }, + "optimize_wall_printing_order": { "default_value": true }, "machine_nozzle_heat_up_speed": {"default_value": 20}, "machine_nozzle_cool_down_speed": {"default_value": 20}, "machine_min_cool_heat_time_window": {"default_value": 5} diff --git a/resources/extruders/cartesio_extruder_0.def.json b/resources/extruders/cartesio_extruder_0.def.json index f1423f3530..5558d9325e 100644 --- a/resources/extruders/cartesio_extruder_0.def.json +++ b/resources/extruders/cartesio_extruder_0.def.json @@ -1,7 +1,7 @@ { "id": "cartesio_extruder_0", "version": 2, - "name": "Extruder 0", + "name": "Extruder 1", "inherits": "fdmextruder", "metadata": { "machine": "cartesio", diff --git a/resources/extruders/cartesio_extruder_1.def.json b/resources/extruders/cartesio_extruder_1.def.json index 402553ff96..f8350f8091 100644 --- a/resources/extruders/cartesio_extruder_1.def.json +++ b/resources/extruders/cartesio_extruder_1.def.json @@ -1,7 +1,7 @@ { "id": "cartesio_extruder_1", "version": 2, - "name": "Extruder 1", + "name": "Extruder 2", "inherits": "fdmextruder", "metadata": { "machine": "cartesio", diff --git a/resources/extruders/cartesio_extruder_2.def.json b/resources/extruders/cartesio_extruder_2.def.json index e8f47772cb..bfc10e75c3 100644 --- a/resources/extruders/cartesio_extruder_2.def.json +++ b/resources/extruders/cartesio_extruder_2.def.json @@ -1,7 +1,7 @@ { "id": "cartesio_extruder_2", "version": 2, - "name": "Extruder 2", + "name": "Extruder 3", "inherits": "fdmextruder", "metadata": { "machine": "cartesio", diff --git a/resources/extruders/cartesio_extruder_3.def.json b/resources/extruders/cartesio_extruder_3.def.json index a3e435470a..f0be53e564 100644 --- a/resources/extruders/cartesio_extruder_3.def.json +++ b/resources/extruders/cartesio_extruder_3.def.json @@ -1,7 +1,7 @@ { "id": "cartesio_extruder_3", "version": 2, - "name": "Extruder 3", + "name": "Extruder 4", "inherits": "fdmextruder", "metadata": { "machine": "cartesio", diff --git a/resources/quality/cartesio/pc/cartesio_0.25_pc_high.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.25_pc_high.inst.cfg index 2f401556d4..ff6547dd91 100644 --- a/resources/quality/cartesio/pc/cartesio_0.25_pc_high.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.25_pc_high.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.25_pc_normal.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.25_pc_normal.inst.cfg index 225b0ec5cf..f3faa9c129 100644 --- a/resources/quality/cartesio/pc/cartesio_0.25_pc_normal.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.25_pc_normal.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.4_pc_high.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.4_pc_high.inst.cfg index 53e61a81a2..3aa808fab5 100644 --- a/resources/quality/cartesio/pc/cartesio_0.4_pc_high.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.4_pc_high.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.4_pc_normal.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.4_pc_normal.inst.cfg index e7b179dfe1..f9be12da3a 100644 --- a/resources/quality/cartesio/pc/cartesio_0.4_pc_normal.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.4_pc_normal.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.8_pc_coarse.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.8_pc_coarse.inst.cfg index baa3a1fdce..597187437b 100644 --- a/resources/quality/cartesio/pc/cartesio_0.8_pc_coarse.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.8_pc_coarse.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 30 +speed_print = 15 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.8_pc_extra_coarse.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.8_pc_extra_coarse.inst.cfg index 81f939f800..6d1fa196ec 100644 --- a/resources/quality/cartesio/pc/cartesio_0.8_pc_extra_coarse.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.8_pc_extra_coarse.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 25 +speed_print = 15 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.8_pc_high.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.8_pc_high.inst.cfg index 41b2cc70f0..c570b65350 100644 --- a/resources/quality/cartesio/pc/cartesio_0.8_pc_high.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.8_pc_high.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) diff --git a/resources/quality/cartesio/pc/cartesio_0.8_pc_normal.inst.cfg b/resources/quality/cartesio/pc/cartesio_0.8_pc_normal.inst.cfg index e5ae6aff1d..0d73b24197 100644 --- a/resources/quality/cartesio/pc/cartesio_0.8_pc_normal.inst.cfg +++ b/resources/quality/cartesio/pc/cartesio_0.8_pc_normal.inst.cfg @@ -31,7 +31,7 @@ switch_extruder_retraction_amount = 2 switch_extruder_retraction_speeds = =retraction_speed switch_extruder_prime_speed = =retraction_prime_speed -speed_print = 50 +speed_print = 20 speed_infill = =speed_print speed_layer_0 = =round(speed_print / 5 * 4) speed_wall = =round(speed_print / 2) From 7219d84b913d094d3d91aff9862891eece053e24 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 20 Nov 2017 13:01:09 +0100 Subject: [PATCH 16/27] CURA-4104 added settings for flow rate compensated extrusion --- resources/definitions/fdmprinter.def.json | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 0fedc4accc..b7f5bc8352 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -5589,6 +5589,36 @@ } } }, + "flow_rate_max_extrusion_offset": + { + "label": "Flow rate compensation max extrusion offset", + "description": "The maximum distance in mm to compensate for pressure in the bowden tube.", + "unit": "mm", + "type": "float", + "minimum_value": "0", + "maximum_value_warning": "10", + "default_value": 0, + "value": "0", + "enabled": true, + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, + "flow_rate_extrusion_offset_factor": + { + "label": "Flow rate compensation factor", + "description": "The multiplication factor for the flow rate -> distance translation.", + "unit": "%", + "type": "float", + "minimum_value": "0", + "maximum_value_warning": "100", + "default_value": 100, + "value": "100", + "enabled": true, + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, "wireframe_enabled": { "label": "Wire Printing", From 43f339f927c120fedcff6bfd893692c4d06ff371 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 20 Nov 2017 14:19:58 +0100 Subject: [PATCH 17/27] Update fdmprinter.def.json --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index b7f5bc8352..be2c5d9757 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -5592,7 +5592,7 @@ "flow_rate_max_extrusion_offset": { "label": "Flow rate compensation max extrusion offset", - "description": "The maximum distance in mm to compensate for pressure in the bowden tube.", + "description": "The maximum distance in mm to compensate.", "unit": "mm", "type": "float", "minimum_value": "0", From b6e997c88d5d8ac846f550b2e2ef1a6efeb7a288 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 10:47:29 +0100 Subject: [PATCH 18/27] CURA-4526 Delete LayerView plugin because it will be replaced with the SimulationView. This commit also adapts the code in order to accept the messages coming from the engine, with information about feedrates and line thicknesses. Add also some changes in the GCodeReader that reads feedrates and line thickness from the gcode file. --- cura/CuraApplication.py | 4 +- cura/Layer.py | 4 +- cura/LayerDataBuilder.py | 12 +- cura/LayerPolygon.py | 20 +- plugins/CuraEngineBackend/Cura.proto | 4 + .../CuraEngineBackend/CuraEngineBackend.py | 2 +- .../ProcessSlicedLayersJob.py | 18 +- plugins/GCodeReader/GCodeReader.py | 91 +++- plugins/LayerView/LayerPass.py | 113 ---- plugins/LayerView/LayerSlider.qml | 312 ----------- plugins/LayerView/LayerSliderLabel.qml | 103 ---- plugins/LayerView/LayerView.py | 495 ------------------ plugins/LayerView/LayerView.qml | 388 -------------- plugins/LayerView/LayerViewProxy.py | 151 ------ plugins/LayerView/__init__.py | 25 - plugins/LayerView/layers.shader | 156 ------ plugins/LayerView/layers3d.shader | 264 ---------- plugins/LayerView/layerview_composite.shader | 148 ------ plugins/LayerView/plugin.json | 8 - resources/themes/cura-dark/theme.json | 2 + resources/themes/cura-light/styles.qml | 1 + resources/themes/cura-light/theme.json | 4 +- 22 files changed, 116 insertions(+), 2209 deletions(-) delete mode 100755 plugins/LayerView/LayerPass.py delete mode 100644 plugins/LayerView/LayerSlider.qml delete mode 100644 plugins/LayerView/LayerSliderLabel.qml delete mode 100755 plugins/LayerView/LayerView.py delete mode 100755 plugins/LayerView/LayerView.qml delete mode 100644 plugins/LayerView/LayerViewProxy.py delete mode 100644 plugins/LayerView/__init__.py delete mode 100755 plugins/LayerView/layers.shader delete mode 100755 plugins/LayerView/layers3d.shader delete mode 100644 plugins/LayerView/layerview_composite.shader delete mode 100644 plugins/LayerView/plugin.json diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8c1ee8fc36..caa39cc703 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -218,7 +218,7 @@ class CuraApplication(QtApplication): "CuraEngineBackend", "UserAgreement", "SolidView", - "LayerView", + "SimulationView", "STLReader", "SelectionTool", "CameraTool", @@ -1386,7 +1386,7 @@ class CuraApplication(QtApplication): extension = os.path.splitext(filename)[1] if extension.lower() in self._non_sliceable_extensions: - self.getController().setActiveView("LayerView") + self.getController().setActiveView("SimulationView") view = self.getController().getActiveView() view.resetLayerData() view.setLayer(9999999) diff --git a/cura/Layer.py b/cura/Layer.py index d5ef5c9bb4..9cd45380fc 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -47,12 +47,12 @@ class Layer: 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_index_offset = index_offset self._element_count = 0 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_index_offset += polygon.lineMeshElementCount() self._element_count += polygon.elementCount diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index 6e50611e64..d6cc81a4e9 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -20,11 +20,11 @@ class LayerDataBuilder(MeshBuilder): if layer not in self._layers: 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: 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) def getLayer(self, layer): @@ -64,13 +64,14 @@ class LayerDataBuilder(MeshBuilder): line_dimensions = numpy.empty((vertex_count, 2), numpy.float32) colors = numpy.empty((vertex_count, 4), numpy.float32) indices = numpy.empty((index_count, 2), numpy.int32) + feedrates = numpy.empty((vertex_count), numpy.float32) extruders = numpy.empty((vertex_count), numpy.float32) line_types = numpy.empty((vertex_count), numpy.float32) vertex_offset = 0 index_offset = 0 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.addVertices(vertices) @@ -107,6 +108,11 @@ class LayerDataBuilder(MeshBuilder): "value": line_types, "opengl_name": "a_line_type", "opengl_type": "float" + }, + "feedrates": { + "value": feedrates, + "opengl_name": "a_feedrate", + "opengl_type": "float" } } diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 7f41351b7f..9766e0c82a 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -28,7 +28,8 @@ class LayerPolygon: # \param data new_points # \param line_widths array with line widths # \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._types = line_types for i in range(len(self._types)): @@ -37,6 +38,7 @@ class LayerPolygon: self._data = data self._line_widths = line_widths self._line_thicknesses = line_thicknesses + self._line_feedrates = line_feedrates self._vertex_begin = 0 self._vertex_end = 0 @@ -84,10 +86,11 @@ class LayerPolygon: # \param vertices : vertex numpy array to be filled # \param colors : 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 line_types : vertex 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: 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. 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, 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 # Convert type per vertex to type per line @@ -166,6 +172,14 @@ class LayerPolygon: @property def lineWidths(self): return self._line_widths + + @property + def lineThicknesses(self): + return self._line_thicknesses + + @property + def lineFeedrates(self): + return self._line_feedrates @property def jumpMask(self): diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index c2e4e5bb5f..69612210ec 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -61,6 +61,8 @@ message Polygon { 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) 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 { @@ -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 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_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 } diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 14c1c10b90..d35df967b2 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -608,7 +608,7 @@ class CuraEngineBackend(QObject, Backend): def _onActiveViewChanged(self): if 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 # 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. diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index a352564bc2..14646cbac1 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -61,7 +61,7 @@ class ProcessSlicedLayersJob(Job): def run(self): start_time = time() - if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": self._progress_message.show() Job.yieldThread() if self._abort_requested: @@ -109,6 +109,7 @@ class ProcessSlicedLayersJob(Job): layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) + layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) @@ -127,10 +128,11 @@ class ProcessSlicedLayersJob(Job): 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. - # In the future, line_thicknesses should be given by CuraEngine as well. - # Currently the infill layer thickness also translates to line width - line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") - line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter + line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array + 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_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. # This uses manual array creation + copy rather than numpy.insert since this is @@ -145,7 +147,7 @@ class ProcessSlicedLayersJob(Job): new_points[:, 1] = points[:, 2] 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_layer.polygons.append(this_poly) @@ -219,7 +221,7 @@ class ProcessSlicedLayersJob(Job): self._progress_message.setProgress(100) view = Application.getInstance().getController().getActiveView() - if view.getPluginId() == "LayerView": + if view.getPluginId() == "SimulationView": view.resetLayerData() if self._progress_message: @@ -232,7 +234,7 @@ class ProcessSlicedLayersJob(Job): def _onActiveViewChanged(self): if self.isRunning(): - if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 1b2795800e..2a7e29e370 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Aleph Objects, Inc. +# Copyright (c) 2017 Aleph Objects, Inc. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application @@ -40,7 +40,8 @@ class GCodeReader(MeshReader): self._extruder_number = 0 self._clearValues() self._scene_node = None - self._position = namedtuple('Position', ['x', 'y', 'z', 'e']) + # 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 @@ -48,7 +49,9 @@ class GCodeReader(MeshReader): 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 @@ -97,7 +100,7 @@ class GCodeReader(MeshReader): def _createPolygon(self, layer_thickness, path, extruder_offsets): countvalid = 0 for point in path: - if point[3] > 0: + if point[5] > 0: countvalid += 1 if countvalid >= 2: # we know what to do now, no need to count further @@ -115,27 +118,56 @@ class GCodeReader(MeshReader): 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_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_types[i - 1] = point[3] - if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: + 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) + 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 _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, e = position + 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 @@ -145,22 +177,24 @@ class GCodeReader(MeshReader): 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_positioning else e[self._extruder_number] + params.e if new_extrusion_value > e[self._extruder_number]: - path.append([x, y, z, self._layer_type]) # extrusion + path.append([x, y, z, f, params.e + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion else: - path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction + path.append([x, y, z, f, params.e + 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 + 0.05 # allow a tiny overlap + self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._previous_z = z else: - path.append([x, y, z, LayerPolygon.MoveCombingType]) - return self._position(x, y, z, e) + 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 @@ -171,6 +205,7 @@ class GCodeReader(MeshReader): params.x if params.x is not None else position.x, params.y if params.y is not None else position.y, 0, + position.f, position.e) ## Set the absolute positioning @@ -187,11 +222,14 @@ class GCodeReader(MeshReader): # 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): @@ -199,7 +237,7 @@ class GCodeReader(MeshReader): 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 + x, y, z, f, e = None, None, None, None, None for item in s[1:]: if len(item) <= 1: continue @@ -211,17 +249,20 @@ class GCodeReader(MeshReader): 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, e) + 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 @@ -240,6 +281,8 @@ class GCodeReader(MeshReader): def read(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 @@ -277,7 +320,7 @@ class GCodeReader(MeshReader): Logger.log("d", "Parsing %s..." % file_name) - current_position = self._position(0, 0, 0, [0]) + current_position = self._position(0, 0, 0, 0, [0]) current_path = [] for line in file: @@ -310,6 +353,7 @@ class GCodeReader(MeshReader): 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):]) @@ -325,17 +369,12 @@ class GCodeReader(MeshReader): 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) - - # < 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 + # 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: @@ -344,8 +383,8 @@ class GCodeReader(MeshReader): current_position = self._processTCode(T, line, current_position, current_path) - # "Flush" leftovers - if not self._is_layers_in_file and len(current_path) > 1: + # "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() diff --git a/plugins/LayerView/LayerPass.py b/plugins/LayerView/LayerPass.py deleted file mode 100755 index 963c8c75c8..0000000000 --- a/plugins/LayerView/LayerPass.py +++ /dev/null @@ -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() diff --git a/plugins/LayerView/LayerSlider.qml b/plugins/LayerView/LayerSlider.qml deleted file mode 100644 index 9abeb01148..0000000000 --- a/plugins/LayerView/LayerSlider.qml +++ /dev/null @@ -1,312 +0,0 @@ -// 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 real minimumRangeHandleSize: handleSize / 2 - property color upperHandleColor: "black" - property color lowerHandleColor: "black" - property color rangeHandleColor: "black" - property real handleLabelWidth: width - property var activeHandle: upperHandle - - // 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 real minimumValue: 0 - property real minimumRange: 0 // minimum range allowed between min and max values - property bool roundValues: true - property real upperValue: maximumValue - property real lowerValue: minimumValue - - property bool layersVisible: true - - function getUpperValueFromSliderHandle () { - return upperHandle.getValue() - } - - function setUpperValue (value) { - upperHandle.setValue(value) - updateRangeHandle() - } - - function getLowerValueFromSliderHandle () { - return lowerHandle.getValue() - } - - function setLowerValue (value) { - lowerHandle.setValue(value) - updateRangeHandle() - } - - 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) { - activeHandle = handle - } - - // slider track - Rectangle { - id: track - - width: sliderRoot.trackThickness - height: sliderRoot.height - sliderRoot.handleSize - radius: sliderRoot.trackRadius - anchors.centerIn: sliderRoot - color: sliderRoot.trackColor - border.width: sliderRoot.trackBorderWidth - border.color: sliderRoot.trackBorderColor - visible: sliderRoot.layersVisible - } - - // Range handle - Item { - id: rangeHandle - - y: upperHandle.y + upperHandle.height - width: sliderRoot.handleSize - height: sliderRoot.minimumRangeHandleSize - anchors.horizontalCenter: sliderRoot.horizontalCenter - visible: sliderRoot.layersVisible - - // set the new value when dragging - function onHandleDragged () { - - upperHandle.y = y - upperHandle.height - lowerHandle.y = y + height - - var upperValue = sliderRoot.getUpperValueFromSliderHandle() - var lowerValue = sliderRoot.getLowerValueFromSliderHandle() - - // set both values after moving the handle position - UM.LayerView.setCurrentLayer(upperValue) - UM.LayerView.setMinimumLayer(lowerValue) - } - - function setValue (value) { - var range = sliderRoot.upperValue - sliderRoot.lowerValue - value = Math.min(value, sliderRoot.maximumValue) - value = Math.max(value, sliderRoot.minimumValue + range) - - UM.LayerView.setCurrentLayer(value) - UM.LayerView.setMinimumLayer(value - range) - } - - Rectangle { - width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth - height: parent.height + sliderRoot.handleSize - anchors.centerIn: parent - color: sliderRoot.rangeHandleColor - } - - MouseArea { - anchors.fill: parent - - drag { - target: parent - axis: Drag.YAxis - minimumY: upperHandle.height - maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height) - } - - onPositionChanged: parent.onHandleDragged() - onPressed: sliderRoot.setActiveHandle(rangeHandle) - } - - LayerSliderLabel { - id: rangleHandleLabel - - 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.upperValue - busy: UM.LayerView.busy - setValue: rangeHandle.setValue // connect callback functions - } - } - - // Upper handle - Rectangle { - id: upperHandle - - y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize) - width: sliderRoot.handleSize - height: sliderRoot.handleSize - anchors.horizontalCenter: sliderRoot.horizontalCenter - radius: sliderRoot.handleRadius - color: sliderRoot.upperHandleColor - visible: sliderRoot.layersVisible - - function onHandleDragged () { - - // don't allow the lower handle to be heigher than the upper handle - if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) { - lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize - } - - // update the range handle - sliderRoot.updateRangeHandle() - - // set the new value after moving the handle position - UM.LayerView.setCurrentLayer(getValue()) - } - - // get the upper value based on the slider position - function getValue () { - var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) - result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue)) - result = sliderRoot.roundValues ? Math.round(result) : result - return result - } - - // set the slider position based on the upper value - function setValue (value) { - - UM.LayerView.setCurrentLayer(value) - - var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue) - var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))) - y = newUpperYPosition - - // update the range handle - sliderRoot.updateRangeHandle() - } - - // dragging - MouseArea { - anchors.fill: parent - - drag { - target: parent - axis: Drag.YAxis - minimumY: 0 - maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) - } - - onPositionChanged: parent.onHandleDragged() - onPressed: sliderRoot.setActiveHandle(upperHandle) - } - - LayerSliderLabel { - id: upperHandleLabel - - 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.upperValue - busy: UM.LayerView.busy - setValue: upperHandle.setValue // connect callback functions - } - } - - // Lower handle - Rectangle { - id: lowerHandle - - y: sliderRoot.height - sliderRoot.handleSize - width: parent.handleSize - height: parent.handleSize - anchors.horizontalCenter: parent.horizontalCenter - radius: sliderRoot.handleRadius - color: sliderRoot.lowerHandleColor - - visible: slider.layersVisible - - function onHandleDragged () { - - // don't allow the upper handle to be lower than the lower handle - if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) { - upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize) - } - - // update the range handle - sliderRoot.updateRangeHandle() - - // set the new value after moving the handle position - UM.LayerView.setMinimumLayer(getValue()) - } - - // get the lower value from the current slider position - function getValue () { - 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)) - result = sliderRoot.roundValues ? Math.round(result) : result - return result - } - - // set the slider position based on the lower value - function setValue (value) { - - UM.LayerView.setMinimumLayer(value) - - 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 - - // update the range handle - sliderRoot.updateRangeHandle() - } - - // dragging - MouseArea { - anchors.fill: parent - - drag { - target: parent - axis: Drag.YAxis - minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize - maximumY: sliderRoot.height - parent.height - } - - onPositionChanged: parent.onHandleDragged() - onPressed: sliderRoot.setActiveHandle(lowerHandle) - } - - LayerSliderLabel { - 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.LayerView.busy - setValue: lowerHandle.setValue // connect callback functions - } - } -} diff --git a/plugins/LayerView/LayerSliderLabel.qml b/plugins/LayerView/LayerSliderLabel.qml deleted file mode 100644 index c989679285..0000000000 --- a/plugins/LayerView/LayerSliderLabel.qml +++ /dev/null @@ -1,103 +0,0 @@ -// 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 - -UM.PointingRectangle { - id: sliderLabelRoot - - // custom properties - property real maximumValue: 100 - property real value: 0 - property var setValue // Function - property bool busy: false - - target: Qt.point(parent.width, y + height / 2) - arrowSize: UM.Theme.getSize("default_arrow").width - height: parent.height - 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 - onVisibleChanged: if (visible) valueLabel.forceActiveFocus() - - color: UM.Theme.getColor("tool_panel_background") - borderColor: UM.Theme.getColor("lining") - borderWidth: UM.Theme.getSize("default_lining").width - - Behavior on height { - NumberAnimation { - duration: 50 - } - } - - // catch all mouse events so they're not handled by underlying 3D scene - MouseArea { - anchors.fill: parent - } - - TextField { - id: valueLabel - - anchors { - left: parent.left - 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 - 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)) - Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) - - style: TextFieldStyle { - textColor: UM.Theme.getColor("setting_control_text") - font: UM.Theme.getFont("default") - background: Item { } - } - - 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. - // 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 - sliderLabelRoot.setValue(parseInt(valueLabel.text) - 1) - } - } - - validator: IntValidator { - bottom: 1 - top: sliderLabelRoot.maximumValue + 1 // +1 because actual layers is an array - } - } - - BusyIndicator { - id: busyIndicator - - anchors { - left: parent.right - leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) - verticalCenter: parent.verticalCenter - } - - width: sliderLabelRoot.height - height: width - - visible: sliderLabelRoot.busy - running: sliderLabelRoot.busy - } -} diff --git a/plugins/LayerView/LayerView.py b/plugins/LayerView/LayerView.py deleted file mode 100755 index 04be97b747..0000000000 --- a/plugins/LayerView/LayerView.py +++ /dev/null @@ -1,495 +0,0 @@ -# Copyright (c) 2015 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -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.QtWidgets import QApplication - -from . import LayerViewProxy - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - -from . import LayerPass - -import numpy -import os.path - -## View used to display g-code paths. -class LayerView(View): - # Must match LayerView.qml - LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 - LAYER_VIEW_TYPE_LINE_TYPE = 1 - - def __init__(self): - super().__init__() - - self._max_layers = 0 - self._current_layer_num = 0 - self._minimum_layer_num = 0 - self._current_layer_mesh = None - self._current_layer_jumps = None - self._top_layers_job = None - self._activity = False - self._old_max_layers = 0 - - self._busy = False - - self._ghost_shader = None - self._layer_pass = None - self._composite_pass = None - self._old_layer_bindings = None - self._layerview_composite_shader = None - self._old_composite_shader = None - - self._global_container_stack = None - self._proxy = LayerViewProxy.LayerViewProxy() - self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) - - self._resetSettings() - self._legend_items = None - self._show_travel_moves = False - - Preferences.getInstance().addPreference("view/top_layer_count", 5) - Preferences.getInstance().addPreference("view/only_show_top_layers", False) - Preferences.getInstance().addPreference("view/force_layer_view_compatibility_mode", False) - - Preferences.getInstance().addPreference("layerview/layer_view_type", 0) - Preferences.getInstance().addPreference("layerview/extruder_opacities", "") - - Preferences.getInstance().addPreference("layerview/show_travel_moves", False) - Preferences.getInstance().addPreference("layerview/show_helpers", True) - Preferences.getInstance().addPreference("layerview/show_skin", True) - Preferences.getInstance().addPreference("layerview/show_infill", True) - - Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) - self._updateWithPreferences() - - self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) - self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) - 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"), - title = catalog.i18nc("@info:title", "Layer View")) - - def _resetSettings(self): - self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed - self._extruder_count = 0 - self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] - self._show_travel_moves = 0 - self._show_helpers = 1 - self._show_skin = 1 - self._show_infill = 1 - - def getActivity(self): - return self._activity - - def getLayerPass(self): - if not self._layer_pass: - # Currently the RenderPass constructor requires a size > 0 - # This should be fixed in RenderPass's constructor. - self._layer_pass = LayerPass.LayerPass(1, 1) - self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) - self._layer_pass.setLayerView(self) - return self._layer_pass - - def getCurrentLayer(self): - return self._current_layer_num - - def getMinimumLayer(self): - return self._minimum_layer_num - - def _onSceneChanged(self, node): - self.calculateMaxLayers() - - def getMaxLayers(self): - return self._max_layers - - busyChanged = Signal() - - def isBusy(self): - return self._busy - - def setBusy(self, busy): - if busy != self._busy: - self._busy = busy - self.busyChanged.emit() - - def resetLayerData(self): - self._current_layer_mesh = None - self._current_layer_jumps = None - - def beginRendering(self): - scene = self.getController().getScene() - renderer = self.getRenderer() - - if not self._ghost_shader: - self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader")) - self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb())) - - for node in DepthFirstIterator(scene.getRoot()): - # We do not want to render ConvexHullNode as it conflicts with the bottom layers. - # However, it is somewhat relevant when the node is selected, so do render it then. - if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): - continue - - if not node.render(renderer): - if (node.getMeshData()) and node.isVisible(): - renderer.queueNode(node, transparent = True, shader = self._ghost_shader) - - def setLayer(self, value): - if self._current_layer_num != value: - self._current_layer_num = value - if self._current_layer_num < 0: - self._current_layer_num = 0 - if self._current_layer_num > self._max_layers: - self._current_layer_num = self._max_layers - if self._current_layer_num < self._minimum_layer_num: - self._minimum_layer_num = self._current_layer_num - - self._startUpdateTopLayers() - - self.currentLayerNumChanged.emit() - - def setMinimumLayer(self, value): - if self._minimum_layer_num != value: - self._minimum_layer_num = value - if self._minimum_layer_num < 0: - self._minimum_layer_num = 0 - if self._minimum_layer_num > self._max_layers: - self._minimum_layer_num = self._max_layers - if self._minimum_layer_num > self._current_layer_num: - self._current_layer_num = self._minimum_layer_num - - self._startUpdateTopLayers() - - self.currentLayerNumChanged.emit() - - ## Set the layer view type - # - # \param layer_view_type integer as in LayerView.qml and this class - def setLayerViewType(self, layer_view_type): - self._layer_view_type = layer_view_type - self.currentLayerNumChanged.emit() - - ## Return the layer view type, integer as in LayerView.qml and this class - def getLayerViewType(self): - return self._layer_view_type - - ## Set the extruder opacity - # - # \param extruder_nr 0..3 - # \param opacity 0.0 .. 1.0 - def setExtruderOpacity(self, extruder_nr, opacity): - if 0 <= extruder_nr <= 3: - self._extruder_opacity[extruder_nr] = opacity - self.currentLayerNumChanged.emit() - - def getExtruderOpacities(self): - return self._extruder_opacity - - def setShowTravelMoves(self, show): - self._show_travel_moves = show - self.currentLayerNumChanged.emit() - - def getShowTravelMoves(self): - return self._show_travel_moves - - def setShowHelpers(self, show): - self._show_helpers = show - self.currentLayerNumChanged.emit() - - def getShowHelpers(self): - return self._show_helpers - - def setShowSkin(self, show): - self._show_skin = show - self.currentLayerNumChanged.emit() - - def getShowSkin(self): - return self._show_skin - - def setShowInfill(self, show): - self._show_infill = show - self.currentLayerNumChanged.emit() - - def getShowInfill(self): - return self._show_infill - - def getCompatibilityMode(self): - return self._compatibility_mode - - def getExtruderCount(self): - return self._extruder_count - - def calculateMaxLayers(self): - scene = self.getController().getScene() - self._activity = True - - self._old_max_layers = self._max_layers - ## Recalculate num max layers - new_max_layers = 0 - for node in DepthFirstIterator(scene.getRoot()): - layer_data = node.callDecoration("getLayerData") - if not layer_data: - continue - - min_layer_number = sys.maxsize - max_layer_number = -sys.maxsize - for layer_id in layer_data.getLayers(): - if max_layer_number < layer_id: - max_layer_number = layer_id - if min_layer_number > layer_id: - min_layer_number = layer_id - layer_count = max_layer_number - min_layer_number - - if new_max_layers < layer_count: - new_max_layers = layer_count - - if new_max_layers > 0 and new_max_layers != self._old_max_layers: - self._max_layers = new_max_layers - - # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first - # if it's the largest value. If we don't do this, we can have a slider block outside of the - # slider. - if new_max_layers > self._current_layer_num: - self.maxLayersChanged.emit() - self.setLayer(int(self._max_layers)) - else: - self.setLayer(int(self._max_layers)) - self.maxLayersChanged.emit() - self._startUpdateTopLayers() - - maxLayersChanged = Signal() - currentLayerNumChanged = Signal() - globalStackChanged = Signal() - preferencesChanged = Signal() - - ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created - # as this caused some issues. - def getProxy(self, engine, script_engine): - return self._proxy - - def endRendering(self): - pass - - def event(self, event): - modifiers = QApplication.keyboardModifiers() - ctrl_is_active = modifiers & Qt.ControlModifier - shift_is_active = modifiers & Qt.ShiftModifier - if event.type == Event.KeyPressEvent and ctrl_is_active: - amount = 10 if shift_is_active else 1 - if event.key == KeyEvent.UpKey: - self.setLayer(self._current_layer_num + amount) - return True - if event.key == KeyEvent.DownKey: - self.setLayer(self._current_layer_num - amount) - return True - - if event.type == Event.ViewActivateEvent: - # Make sure the LayerPass is created - layer_pass = self.getLayerPass() - self.getRenderer().addRenderPass(layer_pass) - - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - self._onGlobalStackChanged() - - if not self._layerview_composite_shader: - self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader")) - theme = Application.getInstance().getTheme() - self._layerview_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())) - - if not self._composite_pass: - 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._composite_pass.getLayerBindings().append("layerview") - self._old_composite_shader = self._composite_pass.getCompositeShader() - self._composite_pass.setCompositeShader(self._layerview_composite_shader) - - elif event.type == Event.ViewDeactivateEvent: - self._wireprint_warning_message.hide() - Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) - if self._global_container_stack: - self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) - - self.getRenderer().removeRenderPass(self._layer_pass) - self._composite_pass.setLayerBindings(self._old_layer_bindings) - self._composite_pass.setCompositeShader(self._old_composite_shader) - - def getCurrentLayerMesh(self): - return self._current_layer_mesh - - def getCurrentLayerJumps(self): - return self._current_layer_jumps - - def _onGlobalStackChanged(self): - if self._global_container_stack: - self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) - self._global_container_stack = Application.getInstance().getGlobalContainerStack() - if self._global_container_stack: - self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) - self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") - self._onPropertyChanged("wireframe_enabled", "value") - self.globalStackChanged.emit() - else: - self._wireprint_warning_message.hide() - - def _onPropertyChanged(self, key, property_name): - if key == "wireframe_enabled" and property_name == "value": - if self._global_container_stack.getProperty("wireframe_enabled", "value"): - self._wireprint_warning_message.show() - else: - self._wireprint_warning_message.hide() - - def _startUpdateTopLayers(self): - if not self._compatibility_mode: - return - - if self._top_layers_job: - self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh) - self._top_layers_job.cancel() - - self.setBusy(True) - - self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) - self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) - self._top_layers_job.start() - - def _updateCurrentLayerMesh(self, job): - self.setBusy(False) - - if not job.getResult(): - return - self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data. - self._current_layer_mesh = job.getResult().get("layers") - if self._show_travel_moves: - self._current_layer_jumps = job.getResult().get("jumps") - self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot()) - - self._top_layers_job = None - - def _updateWithPreferences(self): - self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) - self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) - self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool( - Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) - - self.setLayerViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type")))); - - for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")): - try: - opacity = float(extruder_opacity) - except ValueError: - opacity = 1.0 - self.setExtruderOpacity(extruder_nr, opacity) - - self.setShowTravelMoves(bool(Preferences.getInstance().getValue("layerview/show_travel_moves"))) - self.setShowHelpers(bool(Preferences.getInstance().getValue("layerview/show_helpers"))) - self.setShowSkin(bool(Preferences.getInstance().getValue("layerview/show_skin"))) - self.setShowInfill(bool(Preferences.getInstance().getValue("layerview/show_infill"))) - - self._startUpdateTopLayers() - self.preferencesChanged.emit() - - def _onPreferencesChanged(self, preference): - if preference not in { - "view/top_layer_count", - "view/only_show_top_layers", - "view/force_layer_view_compatibility_mode", - "layerview/layer_view_type", - "layerview/extruder_opacities", - "layerview/show_travel_moves", - "layerview/show_helpers", - "layerview/show_skin", - "layerview/show_infill", - }: - return - - self._updateWithPreferences() - - -class _CreateTopLayersJob(Job): - def __init__(self, scene, layer_number, solid_layers): - super().__init__() - - self._scene = scene - self._layer_number = layer_number - self._solid_layers = solid_layers - self._cancel = False - - def run(self): - layer_data = None - for node in DepthFirstIterator(self._scene.getRoot()): - layer_data = node.callDecoration("getLayerData") - if layer_data: - break - - if self._cancel or not layer_data: - return - - layer_mesh = MeshBuilder() - for i in range(self._solid_layers): - layer_number = self._layer_number - i - if layer_number < 0: - continue - - try: - layer = layer_data.getLayer(layer_number).createMesh() - except Exception: - Logger.logException("w", "An exception occurred while creating layer mesh.") - return - - if not layer or layer.getVertices() is None: - continue - - layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices()) - layer_mesh.addVertices(layer.getVertices()) - - # Scale layer color by a brightness factor based on the current layer number - # This will result in a range of 0.5 - 1.0 to multiply colors by. - brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0 - brightness[0, 3] = 1.0 - layer_mesh.addColors(layer.getColors() * brightness) - - if self._cancel: - return - - Job.yieldThread() - - if self._cancel: - return - - Job.yieldThread() - jump_mesh = layer_data.getLayer(self._layer_number).createJumps() - if not jump_mesh or jump_mesh.getVertices() is None: - jump_mesh = None - - self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh}) - - def cancel(self): - self._cancel = True - super().cancel() - diff --git a/plugins/LayerView/LayerView.qml b/plugins/LayerView/LayerView.qml deleted file mode 100755 index 7261926bc5..0000000000 --- a/plugins/LayerView/LayerView.qml +++ /dev/null @@ -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") - } -} diff --git a/plugins/LayerView/LayerViewProxy.py b/plugins/LayerView/LayerViewProxy.py deleted file mode 100644 index 4cf84117da..0000000000 --- a/plugins/LayerView/LayerViewProxy.py +++ /dev/null @@ -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) diff --git a/plugins/LayerView/__init__.py b/plugins/LayerView/__init__.py deleted file mode 100644 index da1a5aed19..0000000000 --- a/plugins/LayerView/__init__.py +++ /dev/null @@ -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() } diff --git a/plugins/LayerView/layers.shader b/plugins/LayerView/layers.shader deleted file mode 100755 index d340773403..0000000000 --- a/plugins/LayerView/layers.shader +++ /dev/null @@ -1,156 +0,0 @@ -[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 = a_color; - // 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 = a_color; - 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 diff --git a/plugins/LayerView/layers3d.shader b/plugins/LayerView/layers3d.shader deleted file mode 100755 index e8fe425c70..0000000000 --- a/plugins/LayerView/layers3d.shader +++ /dev/null @@ -1,264 +0,0 @@ -[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 int u_layer_view_type; - 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_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 - - switch (u_layer_view_type) { - case 0: // "Material color" - v_color = a_material_color; - break; - case 1: // "Line type" - v_color = a_color; - break; - } - - 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_layer_view_type = 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_normal = normal -a_line_dim = line_dim -a_extruder = extruder -a_material_color = material_color -a_line_type = line_type diff --git a/plugins/LayerView/layerview_composite.shader b/plugins/LayerView/layerview_composite.shader deleted file mode 100644 index dcc02acc84..0000000000 --- a/plugins/LayerView/layerview_composite.shader +++ /dev/null @@ -1,148 +0,0 @@ -[shaders] -vertex = - uniform highp mat4 u_modelViewProjectionMatrix; - attribute highp vec4 a_vertex; - attribute highp vec2 a_uvs; - - varying highp vec2 v_uvs; - - void main() - { - gl_Position = u_modelViewProjectionMatrix * a_vertex; - v_uvs = a_uvs; - } - -fragment = - uniform sampler2D u_layer0; - uniform sampler2D u_layer1; - uniform sampler2D u_layer2; - - uniform vec2 u_offset[9]; - - uniform vec4 u_background_color; - uniform float u_outline_strength; - uniform vec4 u_outline_color; - - varying vec2 v_uvs; - - float kernel[9]; - - const vec3 x_axis = vec3(1.0, 0.0, 0.0); - const vec3 y_axis = vec3(0.0, 1.0, 0.0); - const vec3 z_axis = vec3(0.0, 0.0, 1.0); - - void main() - { - // blur kernel - kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; - kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; - kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; - - vec4 result = u_background_color; - - vec4 main_layer = texture2D(u_layer0, v_uvs); - vec4 selection_layer = texture2D(u_layer1, v_uvs); - vec4 layerview_layer = texture2D(u_layer2, v_uvs); - - result = main_layer * main_layer.a + result * (1.0 - main_layer.a); - result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); - - vec4 sum = vec4(0.0); - for (int i = 0; i < 9; i++) - { - vec4 color = vec4(texture2D(u_layer1, v_uvs.xy + u_offset[i]).a); - sum += color * (kernel[i] / u_outline_strength); - } - - if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) - { - gl_FragColor = result; - } - else - { - gl_FragColor = mix(result, u_outline_color, abs(sum.a)); - } - } - -vertex41core = - #version 410 - uniform highp mat4 u_modelViewProjectionMatrix; - in highp vec4 a_vertex; - in highp vec2 a_uvs; - - out highp vec2 v_uvs; - - void main() - { - gl_Position = u_modelViewProjectionMatrix * a_vertex; - v_uvs = a_uvs; - } - -fragment41core = - #version 410 - uniform sampler2D u_layer0; - uniform sampler2D u_layer1; - uniform sampler2D u_layer2; - - uniform vec2 u_offset[9]; - - uniform vec4 u_background_color; - uniform float u_outline_strength; - uniform vec4 u_outline_color; - - in vec2 v_uvs; - - float kernel[9]; - - const vec3 x_axis = vec3(1.0, 0.0, 0.0); - const vec3 y_axis = vec3(0.0, 1.0, 0.0); - const vec3 z_axis = vec3(0.0, 0.0, 1.0); - - out vec4 frag_color; - - void main() - { - // blur kernel - kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; - kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; - kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; - - vec4 result = u_background_color; - - vec4 main_layer = texture(u_layer0, v_uvs); - vec4 selection_layer = texture(u_layer1, v_uvs); - vec4 layerview_layer = texture(u_layer2, v_uvs); - - result = main_layer * main_layer.a + result * (1.0 - main_layer.a); - result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); - - vec4 sum = vec4(0.0); - for (int i = 0; i < 9; i++) - { - vec4 color = vec4(texture(u_layer1, v_uvs.xy + u_offset[i]).a); - sum += color * (kernel[i] / u_outline_strength); - } - - if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) - { - frag_color = result; - } - else - { - frag_color = mix(result, u_outline_color, abs(sum.a)); - } - } - -[defaults] -u_layer0 = 0 -u_layer1 = 1 -u_layer2 = 2 -u_background_color = [0.965, 0.965, 0.965, 1.0] -u_outline_strength = 1.0 -u_outline_color = [0.05, 0.66, 0.89, 1.0] - -[bindings] - -[attributes] -a_vertex = vertex -a_uvs = uv diff --git a/plugins/LayerView/plugin.json b/plugins/LayerView/plugin.json deleted file mode 100644 index 232fe9c361..0000000000 --- a/plugins/LayerView/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Layer View", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Provides the Layer view.", - "api": 4, - "i18n-catalog": "cura" -} diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index e61c48bffd..5cfed426e5 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -133,6 +133,7 @@ "slider_groove_fill": [245, 245, 245, 255], "slider_handle": [255, 255, 255, 255], "slider_handle_hover": [77, 182, 226, 255], + "slider_handle_active": [68, 192, 255, 255], "slider_handle_border": [39, 44, 48, 255], "slider_text_background": [255, 255, 255, 255], @@ -194,6 +195,7 @@ "layerview_move_combing": [0, 0, 255, 255], "layerview_move_retraction": [128, 128, 255, 255], "layerview_support_interface": [64, 192, 255, 255], + "layerview_nozzle": [181, 166, 66, 120], "material_compatibility_warning": [255, 255, 255, 255], diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index 6d991c5541..ea9d184926 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -269,6 +269,7 @@ QtObject { arrowSize: Theme.getSize("button_tooltip_arrow").width color: Theme.getColor("button_tooltip") opacity: control.hovered ? 1.0 : 0.0; + visible: control.text != "" width: control.hovered ? button_tip.width + Theme.getSize("button_tooltip").width : 0 height: Theme.getSize("button_tooltip").height diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index f084e87da2..4197285dd8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -183,6 +183,7 @@ "slider_groove_fill": [127, 127, 127, 255], "slider_handle": [0, 0, 0, 255], "slider_handle_hover": [77, 182, 226, 255], + "slider_handle_active": [68, 192, 255, 255], "slider_handle_border": [39, 44, 48, 255], "slider_text_background": [255, 255, 255, 255], @@ -271,7 +272,8 @@ "layerview_support_infill": [0, 255, 255, 255], "layerview_move_combing": [0, 0, 255, 255], "layerview_move_retraction": [128, 128, 255, 255], - "layerview_support_interface": [64, 192, 255, 255] + "layerview_support_interface": [64, 192, 255, 255], + "layerview_nozzle": [181, 166, 66, 50] }, "sizes": { From 2df06bbbb9d517d66b7fb5b722e4eddc3b0afc47 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 10:51:57 +0100 Subject: [PATCH 19/27] CURA-4526 Add Simulation View plugin --- plugins/SimulationView/LayerSlider.qml | 325 +++++++++ plugins/SimulationView/NozzleNode.py | 49 ++ plugins/SimulationView/PathSlider.qml | 161 +++++ plugins/SimulationView/SimulationPass.py | 186 +++++ .../SimulationView/SimulationSliderLabel.qml | 104 +++ plugins/SimulationView/SimulationView.py | 609 +++++++++++++++++ plugins/SimulationView/SimulationView.qml | 645 ++++++++++++++++++ plugins/SimulationView/SimulationViewProxy.py | 259 +++++++ plugins/SimulationView/__init__.py | 26 + plugins/SimulationView/layers.shader | 156 +++++ plugins/SimulationView/layers3d.shader | 293 ++++++++ plugins/SimulationView/layers3d_shadow.shader | 256 +++++++ plugins/SimulationView/layers_shadow.shader | 156 +++++ plugins/SimulationView/plugin.json | 8 + plugins/SimulationView/resources/nozzle.stl | Bin 0 -> 210284 bytes .../resources/simulation_pause.svg | 79 +++ .../resources/simulation_resume.svg | 82 +++ .../simulationview_composite.shader | 148 ++++ 18 files changed, 3542 insertions(+) create mode 100644 plugins/SimulationView/LayerSlider.qml create mode 100644 plugins/SimulationView/NozzleNode.py create mode 100644 plugins/SimulationView/PathSlider.qml create mode 100644 plugins/SimulationView/SimulationPass.py create mode 100644 plugins/SimulationView/SimulationSliderLabel.qml create mode 100644 plugins/SimulationView/SimulationView.py create mode 100644 plugins/SimulationView/SimulationView.qml create mode 100644 plugins/SimulationView/SimulationViewProxy.py create mode 100644 plugins/SimulationView/__init__.py create mode 100644 plugins/SimulationView/layers.shader create mode 100644 plugins/SimulationView/layers3d.shader create mode 100644 plugins/SimulationView/layers3d_shadow.shader create mode 100644 plugins/SimulationView/layers_shadow.shader create mode 100644 plugins/SimulationView/plugin.json create mode 100644 plugins/SimulationView/resources/nozzle.stl create mode 100644 plugins/SimulationView/resources/simulation_pause.svg create mode 100644 plugins/SimulationView/resources/simulation_resume.svg create mode 100644 plugins/SimulationView/simulationview_composite.shader diff --git a/plugins/SimulationView/LayerSlider.qml b/plugins/SimulationView/LayerSlider.qml new file mode 100644 index 0000000000..22f9d91340 --- /dev/null +++ b/plugins/SimulationView/LayerSlider.qml @@ -0,0 +1,325 @@ +// 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 real minimumRangeHandleSize: handleSize / 2 + property color upperHandleColor: "black" + property color lowerHandleColor: "black" + property color rangeHandleColor: "black" + property color handleActiveColor: "white" + property real handleLabelWidth: width + property var activeHandle: upperHandle + + // 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 real minimumValue: 0 + property real minimumRange: 0 // minimum range allowed between min and max values + property bool roundValues: true + property real upperValue: maximumValue + property real lowerValue: minimumValue + + property bool layersVisible: true + + function getUpperValueFromSliderHandle () { + return upperHandle.getValue() + } + + function setUpperValue (value) { + upperHandle.setValue(value) + updateRangeHandle() + } + + function getLowerValueFromSliderHandle () { + return lowerHandle.getValue() + } + + function setLowerValue (value) { + lowerHandle.setValue(value) + updateRangeHandle() + } + + 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) { + activeHandle = handle + } + + // slider track + Rectangle { + id: track + + width: sliderRoot.trackThickness + height: sliderRoot.height - sliderRoot.handleSize + radius: sliderRoot.trackRadius + anchors.centerIn: sliderRoot + color: sliderRoot.trackColor + border.width: sliderRoot.trackBorderWidth + border.color: sliderRoot.trackBorderColor + visible: sliderRoot.layersVisible + } + + // Range handle + Item { + id: rangeHandle + + y: upperHandle.y + upperHandle.height + width: sliderRoot.handleSize + height: sliderRoot.minimumRangeHandleSize + anchors.horizontalCenter: sliderRoot.horizontalCenter + visible: sliderRoot.layersVisible + + // set the new value when dragging + function onHandleDragged () { + + upperHandle.y = y - upperHandle.height + lowerHandle.y = y + height + + var upperValue = sliderRoot.getUpperValueFromSliderHandle() + var lowerValue = sliderRoot.getLowerValueFromSliderHandle() + + // set both values after moving the handle position + UM.SimulationView.setCurrentLayer(upperValue) + UM.SimulationView.setMinimumLayer(lowerValue) + } + + function setValue (value) { + var range = sliderRoot.upperValue - sliderRoot.lowerValue + value = Math.min(value, sliderRoot.maximumValue) + value = Math.max(value, sliderRoot.minimumValue + range) + + UM.SimulationView.setCurrentLayer(value) + UM.SimulationView.setMinimumLayer(value - range) + } + + Rectangle { + width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth + height: parent.height + sliderRoot.handleSize + anchors.centerIn: parent + color: sliderRoot.rangeHandleColor + } + + MouseArea { + anchors.fill: parent + + drag { + target: parent + axis: Drag.YAxis + minimumY: upperHandle.height + maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height) + } + + onPositionChanged: parent.onHandleDragged() + onPressed: sliderRoot.setActiveHandle(rangeHandle) + } + + SimulationSliderLabel { + id: rangleHandleLabel + + 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.upperValue + busy: UM.SimulationView.busy + setValue: rangeHandle.setValue // connect callback functions + } + } + + // Upper handle + Rectangle { + id: upperHandle + + y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize) + width: sliderRoot.handleSize + height: sliderRoot.handleSize + anchors.horizontalCenter: sliderRoot.horizontalCenter + radius: sliderRoot.handleRadius + color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor + visible: sliderRoot.layersVisible + + function onHandleDragged () { + + // don't allow the lower handle to be heigher than the upper handle + if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) { + lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize + } + + // update the range handle + sliderRoot.updateRangeHandle() + + // set the new value after moving the handle position + UM.SimulationView.setCurrentLayer(getValue()) + } + + // get the upper value based on the slider position + function getValue () { + var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) + result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue)) + result = sliderRoot.roundValues ? Math.round(result) : result + return result + } + + // set the slider position based on the upper value + function setValue (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))) + y = newUpperYPosition + + // update the range handle + sliderRoot.updateRangeHandle() + } + + Keys.onUpPressed: upperHandleLabel.setValue(upperHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) + Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) + + // dragging + MouseArea { + anchors.fill: parent + + drag { + target: parent + axis: Drag.YAxis + minimumY: 0 + maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + } + + onPositionChanged: parent.onHandleDragged() + onPressed: { + sliderRoot.setActiveHandle(upperHandle) + upperHandleLabel.forceActiveFocus() + } + } + + SimulationSliderLabel { + id: upperHandleLabel + + 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.upperValue + busy: UM.SimulationView.busy + setValue: upperHandle.setValue // connect callback functions + } + } + + // Lower handle + Rectangle { + id: lowerHandle + + y: sliderRoot.height - sliderRoot.handleSize + width: parent.handleSize + height: parent.handleSize + anchors.horizontalCenter: parent.horizontalCenter + radius: sliderRoot.handleRadius + color: lowerHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.lowerHandleColor + + visible: sliderRoot.layersVisible + + function onHandleDragged () { + + // don't allow the upper handle to be lower than the lower handle + if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) { + upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize) + } + + // update the range handle + sliderRoot.updateRangeHandle() + + // set the new value after moving the handle position + UM.SimulationView.setMinimumLayer(getValue()) + } + + // get the lower value from the current slider position + function getValue () { + 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)) + result = sliderRoot.roundValues ? Math.round(result) : result + return result + } + + // set the slider position based on the lower value + function setValue (value) { + + UM.SimulationView.setMinimumLayer(value) + + 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 + + // update the range handle + sliderRoot.updateRangeHandle() + } + + Keys.onUpPressed: lowerHandleLabel.setValue(lowerHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) + Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) + + // dragging + MouseArea { + anchors.fill: parent + + drag { + target: parent + axis: Drag.YAxis + minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize + maximumY: sliderRoot.height - parent.height + } + + onPositionChanged: parent.onHandleDragged() + onPressed: { + sliderRoot.setActiveHandle(lowerHandle) + lowerHandleLabel.forceActiveFocus() + } + } + + 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 + } + } +} diff --git a/plugins/SimulationView/NozzleNode.py b/plugins/SimulationView/NozzleNode.py new file mode 100644 index 0000000000..8a29871775 --- /dev/null +++ b/plugins/SimulationView/NozzleNode.py @@ -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 diff --git a/plugins/SimulationView/PathSlider.qml b/plugins/SimulationView/PathSlider.qml new file mode 100644 index 0000000000..0a4af904aa --- /dev/null +++ b/plugins/SimulationView/PathSlider.qml @@ -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 + } + } +} diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py new file mode 100644 index 0000000000..24a13eaf7a --- /dev/null +++ b/plugins/SimulationView/SimulationPass.py @@ -0,0 +1,186 @@ +# 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._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.Solid) + 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 + if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num: + self._current_shader = self._layer_shader + + layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end)) + 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 once we know the correct position + if 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.Solid) + 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() diff --git a/plugins/SimulationView/SimulationSliderLabel.qml b/plugins/SimulationView/SimulationSliderLabel.qml new file mode 100644 index 0000000000..1c8daf867f --- /dev/null +++ b/plugins/SimulationView/SimulationSliderLabel.qml @@ -0,0 +1,104 @@ +// 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 + +UM.PointingRectangle { + id: sliderLabelRoot + + // custom properties + property real maximumValue: 100 + property real value: 0 + property var setValue // Function + property bool busy: false + property int startFrom: 1 + + target: Qt.point(parent.width, y + height / 2) + arrowSize: UM.Theme.getSize("default_arrow").width + height: parent.height + 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 + onVisibleChanged: if (visible) valueLabel.forceActiveFocus() + + color: UM.Theme.getColor("tool_panel_background") + borderColor: UM.Theme.getColor("lining") + borderWidth: UM.Theme.getSize("default_lining").width + + Behavior on height { + NumberAnimation { + duration: 50 + } + } + + // catch all mouse events so they're not handled by underlying 3D scene + MouseArea { + anchors.fill: parent + } + + TextField { + id: valueLabel + + anchors { + left: parent.left + leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) + verticalCenter: parent.verticalCenter + } + + width: 40 * screenScaleFactor + 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)) + Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) + + style: TextFieldStyle { + textColor: UM.Theme.getColor("setting_control_text") + font: UM.Theme.getFont("default") + background: Item { } + } + + 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. + // Another option would be to increase the size even further, but that gives pretty ugly results. + cursorPosition = 0 + + if (valueLabel.text != "") { + // -startFrom because we need to convert back to an array structure + sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom) + } + } + + validator: IntValidator { + bottom:startFrom + top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0 + } + } + + BusyIndicator { + id: busyIndicator + + anchors { + left: parent.right + leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) + verticalCenter: parent.verticalCenter + } + + width: sliderLabelRoot.height + height: width + + visible: sliderLabelRoot.busy + running: sliderLabelRoot.busy + } +} diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py new file mode 100644 index 0000000000..2751ea4f60 --- /dev/null +++ b/plugins/SimulationView/SimulationView.py @@ -0,0 +1,609 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import sys + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication + +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 cura.ConvexHullNode import ConvexHullNode + +from .NozzleNode import NozzleNode +from .SimulationPass import SimulationPass +from .SimulationViewProxy import SimulationViewProxy + +catalog = i18nCatalog("cura") + +import numpy +import os.path + +## View used to display g-code paths. +class SimulationView(View): + # Must match SimulationView.qml + LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 + LAYER_VIEW_TYPE_LINE_TYPE = 1 + LAYER_VIEW_TYPE_FEEDRATE = 2 + LAYER_VIEW_TYPE_THICKNESS = 3 + + def __init__(self): + super().__init__() + + self._max_layers = 0 + self._current_layer_num = 0 + self._minimum_layer_num = 0 + self._current_layer_mesh = None + self._current_layer_jumps = None + self._top_layers_job = None + self._activity = False + 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._simulation_running = False + + self._ghost_shader = None + self._layer_pass = None + self._composite_pass = None + self._old_layer_bindings = None + self._simulationview_composite_shader = None + self._old_composite_shader = None + + self._global_container_stack = None + self._proxy = SimulationViewProxy() + self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) + + self._resetSettings() + self._legend_items = None + self._show_travel_moves = False + self._nozzle_node = None + + Preferences.getInstance().addPreference("view/top_layer_count", 5) + Preferences.getInstance().addPreference("view/only_show_top_layers", False) + Preferences.getInstance().addPreference("view/force_layer_view_compatibility_mode", False) + + Preferences.getInstance().addPreference("layerview/layer_view_type", 0) + Preferences.getInstance().addPreference("layerview/extruder_opacities", "") + + Preferences.getInstance().addPreference("layerview/show_travel_moves", False) + Preferences.getInstance().addPreference("layerview/show_helpers", True) + Preferences.getInstance().addPreference("layerview/show_skin", True) + Preferences.getInstance().addPreference("layerview/show_infill", True) + + Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + self._updateWithPreferences() + + self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) + self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) + 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"), + title = catalog.i18nc("@info:title", "Simulation View")) + + def _resetSettings(self): + self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed + self._extruder_count = 0 + self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] + self._show_travel_moves = 0 + self._show_helpers = 1 + self._show_skin = 1 + self._show_infill = 1 + self.resetLayerData() + + def getActivity(self): + return self._activity + + def setActivity(self, activity): + if self._activity == activity: + return + self._activity = activity + self.activityChanged.emit() + + def getSimulationPass(self): + if not self._layer_pass: + # Currently the RenderPass constructor requires a size > 0 + # This should be fixed in RenderPass's constructor. + self._layer_pass = SimulationPass(1, 1) + self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) + self._layer_pass.setSimulationView(self) + return self._layer_pass + + def getCurrentLayer(self): + return self._current_layer_num + + def getMinimumLayer(self): + return self._minimum_layer_num + + def getMaxLayers(self): + return self._max_layers + + 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() + + def isBusy(self): + return self._busy + + def setBusy(self, busy): + if busy != self._busy: + self._busy = busy + self.busyChanged.emit() + + def isSimulationRunning(self): + return self._simulation_running + + def setSimulationRunning(self, running): + self._simulation_running = running + + def resetLayerData(self): + self._current_layer_mesh = 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): + scene = self.getController().getScene() + renderer = self.getRenderer() + + if not self._ghost_shader: + self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader")) + self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb())) + + for node in DepthFirstIterator(scene.getRoot()): + # We do not want to render ConvexHullNode as it conflicts with the bottom layers. + # However, it is somewhat relevant when the node is selected, so do render it then. + if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): + continue + + if not node.render(renderer): + if (node.getMeshData()) and node.isVisible(): + renderer.queueNode(node, transparent = True, shader = self._ghost_shader) + + def setLayer(self, value): + if self._current_layer_num != value: + self._current_layer_num = value + if self._current_layer_num < 0: + self._current_layer_num = 0 + if self._current_layer_num > self._max_layers: + self._current_layer_num = self._max_layers + if self._current_layer_num < self._minimum_layer_num: + self._minimum_layer_num = self._current_layer_num + + self._startUpdateTopLayers() + + self.currentLayerNumChanged.emit() + + def setMinimumLayer(self, value): + if self._minimum_layer_num != value: + self._minimum_layer_num = value + if self._minimum_layer_num < 0: + self._minimum_layer_num = 0 + if self._minimum_layer_num > self._max_layers: + self._minimum_layer_num = self._max_layers + if self._minimum_layer_num > self._current_layer_num: + self._current_layer_num = self._minimum_layer_num + + self._startUpdateTopLayers() + + 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 + # + # \param layer_view_type integer as in SimulationView.qml and this class + def setSimulationViewType(self, layer_view_type): + self._layer_view_type = layer_view_type + self.currentLayerNumChanged.emit() + + ## Return the layer view type, integer as in SimulationView.qml and this class + def getSimulationViewType(self): + return self._layer_view_type + + ## Set the extruder opacity + # + # \param extruder_nr 0..3 + # \param opacity 0.0 .. 1.0 + def setExtruderOpacity(self, extruder_nr, opacity): + if 0 <= extruder_nr <= 3: + self._extruder_opacity[extruder_nr] = opacity + self.currentLayerNumChanged.emit() + + def getExtruderOpacities(self): + return self._extruder_opacity + + def setShowTravelMoves(self, show): + self._show_travel_moves = show + self.currentLayerNumChanged.emit() + + def getShowTravelMoves(self): + return self._show_travel_moves + + def setShowHelpers(self, show): + self._show_helpers = show + self.currentLayerNumChanged.emit() + + def getShowHelpers(self): + return self._show_helpers + + def setShowSkin(self, show): + self._show_skin = show + self.currentLayerNumChanged.emit() + + def getShowSkin(self): + return self._show_skin + + def setShowInfill(self, show): + self._show_infill = show + self.currentLayerNumChanged.emit() + + def getShowInfill(self): + return self._show_infill + + def getCompatibilityMode(self): + return self._compatibility_mode + + def getExtruderCount(self): + 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): + scene = self.getController().getScene() + + self._old_max_layers = self._max_layers + ## Recalculate num max layers + new_max_layers = 0 + for node in DepthFirstIterator(scene.getRoot()): + layer_data = node.callDecoration("getLayerData") + if not layer_data: + continue + + self.setActivity(True) + min_layer_number = sys.maxsize + max_layer_number = -sys.maxsize + 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: + max_layer_number = layer_id + if min_layer_number > layer_id: + min_layer_number = layer_id + layer_count = max_layer_number - min_layer_number + + if new_max_layers < layer_count: + new_max_layers = layer_count + + if new_max_layers > 0 and new_max_layers != self._old_max_layers: + self._max_layers = new_max_layers + + # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first + # if it's the largest value. If we don't do this, we can have a slider block outside of the + # slider. + if new_max_layers > self._current_layer_num: + self.maxLayersChanged.emit() + self.setLayer(int(self._max_layers)) + else: + self.setLayer(int(self._max_layers)) + self.maxLayersChanged.emit() + 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() + maxPathsChanged = Signal() + currentLayerNumChanged = Signal() + currentPathNumChanged = Signal() + globalStackChanged = 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 + # as this caused some issues. + def getProxy(self, engine, script_engine): + return self._proxy + + def endRendering(self): + pass + + def event(self, event): + modifiers = QApplication.keyboardModifiers() + ctrl_is_active = modifiers & Qt.ControlModifier + shift_is_active = modifiers & Qt.ShiftModifier + if event.type == Event.KeyPressEvent and ctrl_is_active: + amount = 10 if shift_is_active else 1 + if event.key == KeyEvent.UpKey: + self.setLayer(self._current_layer_num + amount) + return True + if event.key == KeyEvent.DownKey: + self.setLayer(self._current_layer_num - amount) + return True + + if event.type == Event.ViewActivateEvent: + # Make sure the SimulationPass is created + layer_pass = self.getSimulationPass() + 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) + self._onGlobalStackChanged() + + if not self._simulationview_composite_shader: + self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader")) + theme = Application.getInstance().getTheme() + self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) + self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) + + if not self._composite_pass: + 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._composite_pass.getLayerBindings().append("simulationview") + self._old_composite_shader = self._composite_pass.getCompositeShader() + self._composite_pass.setCompositeShader(self._simulationview_composite_shader) + + elif event.type == Event.ViewDeactivateEvent: + self._wireprint_warning_message.hide() + Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) + if self._global_container_stack: + self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) + + self._nozzle_node.setParent(None) + self.getRenderer().removeRenderPass(self._layer_pass) + self._composite_pass.setLayerBindings(self._old_layer_bindings) + self._composite_pass.setCompositeShader(self._old_composite_shader) + + def getCurrentLayerMesh(self): + return self._current_layer_mesh + + def getCurrentLayerJumps(self): + return self._current_layer_jumps + + def _onGlobalStackChanged(self): + if self._global_container_stack: + self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) + self._global_container_stack = Application.getInstance().getGlobalContainerStack() + if self._global_container_stack: + self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) + self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") + self._onPropertyChanged("wireframe_enabled", "value") + self.globalStackChanged.emit() + else: + self._wireprint_warning_message.hide() + + def _onPropertyChanged(self, key, property_name): + if key == "wireframe_enabled" and property_name == "value": + if self._global_container_stack.getProperty("wireframe_enabled", "value"): + self._wireprint_warning_message.show() + else: + self._wireprint_warning_message.hide() + + def _onCurrentLayerNumChanged(self): + self.calculateMaxPathsOnLayer(self._current_layer_num) + + def _startUpdateTopLayers(self): + if not self._compatibility_mode: + return + + if self._top_layers_job: + self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh) + self._top_layers_job.cancel() + + self.setBusy(True) + + self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) + self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) + self._top_layers_job.start() + + def _updateCurrentLayerMesh(self, job): + self.setBusy(False) + + if not job.getResult(): + return + self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data. + self._current_layer_mesh = job.getResult().get("layers") + if self._show_travel_moves: + self._current_layer_jumps = job.getResult().get("jumps") + self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot()) + + self._top_layers_job = None + + def _updateWithPreferences(self): + self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) + self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) + self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool( + Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) + + 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("|")): + try: + opacity = float(extruder_opacity) + except ValueError: + opacity = 1.0 + self.setExtruderOpacity(extruder_nr, opacity) + + self.setShowTravelMoves(bool(Preferences.getInstance().getValue("layerview/show_travel_moves"))) + self.setShowHelpers(bool(Preferences.getInstance().getValue("layerview/show_helpers"))) + self.setShowSkin(bool(Preferences.getInstance().getValue("layerview/show_skin"))) + self.setShowInfill(bool(Preferences.getInstance().getValue("layerview/show_infill"))) + + self._startUpdateTopLayers() + self.preferencesChanged.emit() + + def _onPreferencesChanged(self, preference): + if preference not in { + "view/top_layer_count", + "view/only_show_top_layers", + "view/force_layer_view_compatibility_mode", + "layerview/layer_view_type", + "layerview/extruder_opacities", + "layerview/show_travel_moves", + "layerview/show_helpers", + "layerview/show_skin", + "layerview/show_infill", + }: + return + + self._updateWithPreferences() + + +class _CreateTopLayersJob(Job): + def __init__(self, scene, layer_number, solid_layers): + super().__init__() + + self._scene = scene + self._layer_number = layer_number + self._solid_layers = solid_layers + self._cancel = False + + def run(self): + layer_data = None + for node in DepthFirstIterator(self._scene.getRoot()): + layer_data = node.callDecoration("getLayerData") + if layer_data: + break + + if self._cancel or not layer_data: + return + + layer_mesh = MeshBuilder() + for i in range(self._solid_layers): + layer_number = self._layer_number - i + if layer_number < 0: + continue + + try: + layer = layer_data.getLayer(layer_number).createMesh() + except Exception: + Logger.logException("w", "An exception occurred while creating layer mesh.") + return + + if not layer or layer.getVertices() is None: + continue + + layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices()) + layer_mesh.addVertices(layer.getVertices()) + + # Scale layer color by a brightness factor based on the current layer number + # This will result in a range of 0.5 - 1.0 to multiply colors by. + brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0 + brightness[0, 3] = 1.0 + layer_mesh.addColors(layer.getColors() * brightness) + + if self._cancel: + return + + Job.yieldThread() + + if self._cancel: + return + + Job.yieldThread() + jump_mesh = layer_data.getLayer(self._layer_number).createJumps() + if not jump_mesh or jump_mesh.getVertices() is None: + jump_mesh = None + + self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh}) + + def cancel(self): + self._cancel = True + super().cancel() + diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml new file mode 100644 index 0000000000..4c7d99deec --- /dev/null +++ b/plugins/SimulationView/SimulationView.qml @@ -0,0 +1,645 @@ +// 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 (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) + } + } + + 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: layerSlider.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.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 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.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 + anchors.rightMargin: 10 * screenScaleFactor + + 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 + view_settings.show_legend = UM.SimulationView.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.SimulationView.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.SimulationView.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.SimulationView.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 & !view_settings.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: view_settings.show_travel_moves, + preference: "layerview/show_travel_moves", + colorId: "layerview_move_combing" + }); + typesLegendModel.append({ + label: catalog.i18nc("@label", "Show Helpers"), + initialValue: view_settings.show_helpers, + preference: "layerview/show_helpers", + colorId: "layerview_support" + }); + typesLegendModel.append({ + label: catalog.i18nc("@label", "Show Shell"), + initialValue: view_settings.show_skin, + preference: "layerview/show_skin", + colorId: "layerview_inset_0" + }); + typesLegendModel.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.SimulationView.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.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: 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") + } + } + + // Text for the minimum, maximum and units for the feedrates and layer thickness + Rectangle { + id: gradientLegend + visible: view_settings.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 and thickness + Rectangle { // In QML 5.9 can be changed by LinearGradient + // Invert values because then the bar is rotated 90 degrees + id: gradient + visible: view_settings.show_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.75, 0.5, 0.25, 1) + } + GradientStop { + position: 0.5 + color: Qt.rgba(0.5, 1, 0.5, 1) + } + GradientStop { + position: 0.75 + color: Qt.rgba(0.25, 0.5, 0.75, 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 + + width: parent.width + height: UM.Theme.getSize("slider_handle").width + anchors.left: parent.left + 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: pathSlider.bottom + topMargin: UM.Theme.getSize("slider_layerview_margin").height + 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 + implicitWidth: UM.Theme.getSize("button").width * 0.75; + implicitHeight: UM.Theme.getSize("button").height * 0.75; + iconSource: "./resources/simulation_resume.svg" + style: UM.Theme.styles.tool_button + visible: !UM.SimulationView.compatibilityMode + anchors { + horizontalCenter: layerSlider.horizontalCenter + top: layerSlider.bottom + topMargin: UM.Theme.getSize("slider_layerview_margin").width + } + + 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: 250 + 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") + } +} diff --git a/plugins/SimulationView/SimulationViewProxy.py b/plugins/SimulationView/SimulationViewProxy.py new file mode 100644 index 0000000000..e144b841e6 --- /dev/null +++ b/plugins/SimulationView/SimulationViewProxy.py @@ -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) diff --git a/plugins/SimulationView/__init__.py b/plugins/SimulationView/__init__.py new file mode 100644 index 0000000000..f7ccf41acc --- /dev/null +++ b/plugins/SimulationView/__init__.py @@ -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", "Simulation 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()} diff --git a/plugins/SimulationView/layers.shader b/plugins/SimulationView/layers.shader new file mode 100644 index 0000000000..d340773403 --- /dev/null +++ b/plugins/SimulationView/layers.shader @@ -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 = a_color; + // 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 = a_color; + 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 diff --git a/plugins/SimulationView/layers3d.shader b/plugins/SimulationView/layers3d.shader new file mode 100644 index 0000000000..f377fca055 --- /dev/null +++ b/plugins/SimulationView/layers3d.shader @@ -0,0 +1,293 @@ +[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 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 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_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; + in highp float a_feedrate; + in highp float a_thickness; + + 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; + + vec4 gradientColor(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-2*value); + float blue = 1-value; + return vec4(red, green, blue, 1.0); + } + + 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 + + switch (u_layer_view_type) { + case 0: // "Material color" + v_color = a_material_color; + break; + case 1: // "Line type" + v_color = a_color; + break; + case 2: // "Feedrate" + v_color = gradientColor(a_feedrate, u_min_feedrate, u_max_feedrate); + break; + case 3: // "Layer thickness" + v_color = gradientColor(a_line_dim.y, u_min_thickness, u_max_thickness); + break; + } + + 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_layer_view_type = 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 + +u_min_feedrate = 0 +u_max_feedrate = 1 + +u_min_thickness = 0 +u_max_thickness = 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_normal = normal +a_line_dim = line_dim +a_extruder = extruder +a_material_color = material_color +a_line_type = line_type +a_feedrate = feedrate +a_thickness = thickness diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader new file mode 100644 index 0000000000..ad75fcf9d0 --- /dev/null +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -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 diff --git a/plugins/SimulationView/layers_shadow.shader b/plugins/SimulationView/layers_shadow.shader new file mode 100644 index 0000000000..972f18c921 --- /dev/null +++ b/plugins/SimulationView/layers_shadow.shader @@ -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 diff --git a/plugins/SimulationView/plugin.json b/plugins/SimulationView/plugin.json new file mode 100644 index 0000000000..0e7bec0626 --- /dev/null +++ b/plugins/SimulationView/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Simulation View", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Provides the Simulation view.", + "api": 4, + "i18n-catalog": "cura" +} diff --git a/plugins/SimulationView/resources/nozzle.stl b/plugins/SimulationView/resources/nozzle.stl new file mode 100644 index 0000000000000000000000000000000000000000..7f4b22804aab6768ed2b9c0ccdc2f89847e38125 GIT binary patch literal 210284 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%0AbuZy#5DmdXLV_bm} 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 MV-kv + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/plugins/SimulationView/resources/simulation_resume.svg b/plugins/SimulationView/resources/simulation_resume.svg new file mode 100644 index 0000000000..a8ed8e79a3 --- /dev/null +++ b/plugins/SimulationView/resources/simulation_resume.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/plugins/SimulationView/simulationview_composite.shader b/plugins/SimulationView/simulationview_composite.shader new file mode 100644 index 0000000000..dcc02acc84 --- /dev/null +++ b/plugins/SimulationView/simulationview_composite.shader @@ -0,0 +1,148 @@ +[shaders] +vertex = + uniform highp mat4 u_modelViewProjectionMatrix; + attribute highp vec4 a_vertex; + attribute highp vec2 a_uvs; + + varying highp vec2 v_uvs; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + v_uvs = a_uvs; + } + +fragment = + uniform sampler2D u_layer0; + uniform sampler2D u_layer1; + uniform sampler2D u_layer2; + + uniform vec2 u_offset[9]; + + uniform vec4 u_background_color; + uniform float u_outline_strength; + uniform vec4 u_outline_color; + + varying vec2 v_uvs; + + float kernel[9]; + + const vec3 x_axis = vec3(1.0, 0.0, 0.0); + const vec3 y_axis = vec3(0.0, 1.0, 0.0); + const vec3 z_axis = vec3(0.0, 0.0, 1.0); + + void main() + { + // blur kernel + kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; + kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; + kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; + + vec4 result = u_background_color; + + vec4 main_layer = texture2D(u_layer0, v_uvs); + vec4 selection_layer = texture2D(u_layer1, v_uvs); + vec4 layerview_layer = texture2D(u_layer2, v_uvs); + + result = main_layer * main_layer.a + result * (1.0 - main_layer.a); + result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); + + vec4 sum = vec4(0.0); + for (int i = 0; i < 9; i++) + { + vec4 color = vec4(texture2D(u_layer1, v_uvs.xy + u_offset[i]).a); + sum += color * (kernel[i] / u_outline_strength); + } + + if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) + { + gl_FragColor = result; + } + else + { + gl_FragColor = mix(result, u_outline_color, abs(sum.a)); + } + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelViewProjectionMatrix; + in highp vec4 a_vertex; + in highp vec2 a_uvs; + + out highp vec2 v_uvs; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + v_uvs = a_uvs; + } + +fragment41core = + #version 410 + uniform sampler2D u_layer0; + uniform sampler2D u_layer1; + uniform sampler2D u_layer2; + + uniform vec2 u_offset[9]; + + uniform vec4 u_background_color; + uniform float u_outline_strength; + uniform vec4 u_outline_color; + + in vec2 v_uvs; + + float kernel[9]; + + const vec3 x_axis = vec3(1.0, 0.0, 0.0); + const vec3 y_axis = vec3(0.0, 1.0, 0.0); + const vec3 z_axis = vec3(0.0, 0.0, 1.0); + + out vec4 frag_color; + + void main() + { + // blur kernel + kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; + kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; + kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; + + vec4 result = u_background_color; + + vec4 main_layer = texture(u_layer0, v_uvs); + vec4 selection_layer = texture(u_layer1, v_uvs); + vec4 layerview_layer = texture(u_layer2, v_uvs); + + result = main_layer * main_layer.a + result * (1.0 - main_layer.a); + result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); + + vec4 sum = vec4(0.0); + for (int i = 0; i < 9; i++) + { + vec4 color = vec4(texture(u_layer1, v_uvs.xy + u_offset[i]).a); + sum += color * (kernel[i] / u_outline_strength); + } + + if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) + { + frag_color = result; + } + else + { + frag_color = mix(result, u_outline_color, abs(sum.a)); + } + } + +[defaults] +u_layer0 = 0 +u_layer1 = 1 +u_layer2 = 2 +u_background_color = [0.965, 0.965, 0.965, 1.0] +u_outline_strength = 1.0 +u_outline_color = [0.05, 0.66, 0.89, 1.0] + +[bindings] + +[attributes] +a_vertex = vertex +a_uvs = uv From feb0a08fd90b8e52fe8415544048192577b769c1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 12:34:08 +0100 Subject: [PATCH 20/27] CURA-4581 The project name is cached instead of set --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index a3aadc79b8..a237460bab 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -902,7 +902,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): base_file_name = os.path.basename(file_name) if base_file_name.endswith(".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 ## HACK: Replaces the material container in the given stack with a newly created material container. From abe9ba379575ba5dbe4556b4afa4d62ce6847309 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 13:23:47 +0100 Subject: [PATCH 21/27] CURA-4526 Stop simulation when changing preferences. Hide nozzle node when in compatibility mode. --- plugins/SimulationView/SimulationPass.py | 2 +- plugins/SimulationView/SimulationView.qml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index 24a13eaf7a..e3b840fc87 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -171,7 +171,7 @@ class SimulationPass(RenderPass): batch.render(self._scene.getActiveCamera()) # The nozzle is drawn once we know the correct position - if self._layer_view.getActivity() and nozzle_node is not None: + if 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) diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml index 4c7d99deec..67ca39d992 100644 --- a/plugins/SimulationView/SimulationView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -168,6 +168,7 @@ Item { layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type"); layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex); + playButton.pauseSimulation(); 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"); From b09ffaf9e2ce0973f2ea36131c44d64a16719d71 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 21 Nov 2017 13:35:59 +0100 Subject: [PATCH 22/27] Remove "Fine" 0.1mm quality for BAM CURA-4595 --- .../um3_aa0.4_BAM_Normal_Quality.inst.cfg | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 resources/quality/ultimaker3/um3_aa0.4_BAM_Normal_Quality.inst.cfg diff --git a/resources/quality/ultimaker3/um3_aa0.4_BAM_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_BAM_Normal_Quality.inst.cfg deleted file mode 100644 index b9f35fcfa0..0000000000 --- a/resources/quality/ultimaker3/um3_aa0.4_BAM_Normal_Quality.inst.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[general] -version = 2 -name = Fine -definition = ultimaker3 - -[metadata] -type = quality -quality_type = normal -material = generic_bam_ultimaker3_AA_0.4 -weight = 0 -setting_version = 4 - -[values] -default_material_print_temperature = 225 -cool_fan_full_at_height = =layer_height_0 + 2 * layer_height -cool_fan_speed_max = =cool_fan_speed -cool_min_speed = 7 -machine_nozzle_cool_down_speed = 0.75 -machine_nozzle_heat_up_speed = 1.6 -material_standby_temperature = 100 -# prime_tower_enable: see CURA-4248 -prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 -skin_overlap = 10 -speed_layer_0 = 20 -top_bottom_thickness = 1 -wall_thickness = 1 -support_interface_enable = True -support_interface_density = =min(extruderValues('material_surface_energy')) -support_interface_pattern = ='lines' if support_interface_density < 100 else 'concentric' -support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 1) * layer_height -support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height -support_angle = 45 -support_join_distance = 5 -support_offset = 2 -support_pattern = triangles -support_infill_rate = 10 From d415c5fcbb1ee90cc7c9d32dd2eef7c128c47e88 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 13:42:46 +0100 Subject: [PATCH 23/27] CURA-4526 Add transparency to the nozzle so the user can see the lines and layers through the nozzle. --- plugins/SimulationView/SimulationPass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index e3b840fc87..2b9063e27a 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -175,7 +175,7 @@ class SimulationPass(RenderPass): if head_position is not None: nozzle_node.setVisible(True) nozzle_node.setPosition(head_position) - nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Solid) + 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()) From f18cb951fcc68624a564c17fbcae2da14dcce62b Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 21 Nov 2017 13:48:03 +0100 Subject: [PATCH 24/27] Use workspaceLoaded signal instead of projectFileLoaded CURA-4581 --- cura/CuraApplication.py | 2 -- cura/PrintInformation.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index caa39cc703..b3e6caed4e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -126,8 +126,6 @@ class CuraApplication(QtApplication): # Cura will always show the Add Machine Dialog upon start. stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished - projectFileLoaded = pyqtSignal(str) # Emitted whenever a project file is loaded - def __init__(self): # 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"]: diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 46d9a61254..0e10eda02a 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -71,7 +71,7 @@ class PrintInformation(QObject): Application.getInstance().globalContainerStackChanged.connect(self._updateJobName) Application.getInstance().fileLoaded.connect(self.setBaseName) - Application.getInstance().projectFileLoaded.connect(self.setProjectName) + Application.getInstance().workspaceLoaded.connect(self.setProjectName) Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) self._active_material_container = None From 602e8c0e66be33224b96823298b2c16b0f157a57 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 21 Nov 2017 15:12:17 +0100 Subject: [PATCH 25/27] Changed 'Check material compatibility' to 'Check compatibility' because it's not only materials possibly being incompatible --- resources/qml/SidebarHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 8bba4cf6fd..fab8dc6130 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -346,7 +346,7 @@ Column Label { id: materialInfoLabel wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "Check material compatibility") + text: catalog.i18nc("@label", "Check compatibility") font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") From 8782e2e529c3abde36e5d473e18d7293cb59090c Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 21 Nov 2017 15:12:17 +0100 Subject: [PATCH 26/27] Rename parameters CURA-4581 --- cura/PrintInformation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 0e10eda02a..0a5db439c3 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -264,13 +264,14 @@ class PrintInformation(QObject): def jobName(self): return self._job_name - def _updateJobName(self, empty_name = False): + def _updateJobName(self, is_project_name_empty = False): # 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 != "": - if empty_name: + if is_project_name_empty: self._project_name = "" - return + else: + return if self._base_name == "": self._job_name = "" @@ -312,7 +313,7 @@ class PrintInformation(QObject): if name.endswith(".curaproject"): name = name[:name.rfind(".curaproject")] self._base_name = name - self._updateJobName(empty_name = is_empty) + self._updateJobName(is_project_name_empty = is_empty) ## Created an acronymn-like abbreviated machine name from the currently active machine name # Called each time the global stack is switched From 62d70b46d6fb829c239b81b30a5aade7bac5bd2e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 21 Nov 2017 15:31:59 +0100 Subject: [PATCH 27/27] Fix filtering custom profiles per extruder - CURA-4482 --- cura/QualityManager.py | 15 +++------------ cura/Settings/UserProfilesModel.py | 9 +-------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/cura/QualityManager.py b/cura/QualityManager.py index abd14fa841..d2162c2449 100644 --- a/cura/QualityManager.py +++ b/cura/QualityManager.py @@ -183,20 +183,11 @@ class QualityManager: materials = [] - # TODO: fix this - if extruder_stacks: - # 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: + for stack in extruder_stacks: + if stack.getId() == active_stack_id and machine_manager.newMaterial: materials.append(machine_manager.newMaterial) else: - materials.append(global_container_stack.material) + materials.append(stack.material) quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials) diff --git a/cura/Settings/UserProfilesModel.py b/cura/Settings/UserProfilesModel.py index 5ae9055759..aedbba34d5 100644 --- a/cura/Settings/UserProfilesModel.py +++ b/cura/Settings/UserProfilesModel.py @@ -24,9 +24,6 @@ class UserProfilesModel(ProfilesModel): quality_manager = QualityManager.getInstance() machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition) quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition) - - extruder_manager = ExtruderManager.getInstance() - active_extruder = extruder_manager.getActiveExtruderStack() extruder_stacks = self._getOrderedExtruderStacksList() # Fetch the list of usable qualities across all extruders. @@ -35,10 +32,6 @@ class UserProfilesModel(ProfilesModel): # Filter the quality_change by the list of available quality_types quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list]) - filtered_quality_changes = [qc 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())] + filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set] return filtered_quality_changes