diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index b21efc93f3..3358fe8b85 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -727,48 +727,17 @@ class BuildVolume(SceneNode): self._error_areas = [] - extruder_manager = ExtruderManager.getInstance() - used_extruders = extruder_manager.getUsedExtruderStacks() + used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() disallowed_border_size = self.getEdgeDisallowedSize() - if not used_extruders: - # If no extruder is used, assume that the active extruder is used (else nothing is drawn) - if extruder_manager.getActiveExtruderStack(): - used_extruders = [extruder_manager.getActiveExtruderStack()] - else: - used_extruders = [self._global_container_stack] - - result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added. + result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) # Normal machine disallowed areas can always be added. prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders) - result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. - prime_disallowed_areas = copy.deepcopy(result_areas_no_brim) + result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) # Where the priming is not allowed to happen. This is not added to the result, just for collision checking. - #Check if prime positions intersect with disallowed areas. + # Check if prime positions intersect with disallowed areas. for extruder in used_extruders: extruder_id = extruder.getId() - collision = False - for prime_polygon in prime_areas[extruder_id]: - for disallowed_polygon in prime_disallowed_areas[extruder_id]: - if prime_polygon.intersectsPolygon(disallowed_polygon) is not None: - collision = True - break - if collision: - break - - #Also check other prime positions (without additional offset). - for other_extruder_id in prime_areas: - if extruder_id == other_extruder_id: #It is allowed to collide with itself. - continue - for other_prime_polygon in prime_areas[other_extruder_id]: - if prime_polygon.intersectsPolygon(other_prime_polygon): - collision = True - break - if collision: - break - if collision: - break - result_areas[extruder_id].extend(prime_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id]) @@ -776,37 +745,32 @@ class BuildVolume(SceneNode): for area in nozzle_disallowed_areas: polygon = Polygon(numpy.array(area, numpy.float32)) polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) - result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these. - #polygon_minimal_border = polygon.getMinkowskiHull(5) - result_areas_no_brim[extruder_id].append(polygon) # no brim + result_areas[extruder_id].append(polygon_disallowed_border) # Don't perform the offset on these. + result_areas_no_brim[extruder_id].append(polygon) # No brim # Add prime tower location as disallowed area. - if len(used_extruders) > 1: #No prime tower in single-extrusion. - - if len([x for x in used_extruders if x.isEnabled]) > 1: #No prime tower if only one extruder is enabled - prime_tower_collision = False - prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) - for extruder_id in prime_tower_areas: - for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]): - for area in result_areas[extruder_id]: - if prime_tower_area.intersectsPolygon(area) is not None: - prime_tower_collision = True - break - if prime_tower_collision: #Already found a collision. + if len([x for x in used_extruders if x.isEnabled]) > 1: # No prime tower if only one extruder is enabled + prime_tower_collision = False + prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) + for extruder_id in prime_tower_areas: + for area_index, prime_tower_area in enumerate(prime_tower_areas[extruder_id]): + for area in result_areas[extruder_id]: + if prime_tower_area.intersectsPolygon(area) is not None: + prime_tower_collision = True break - if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and - ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"): - prime_tower_areas[extruder_id][i_area] = prime_tower_area.getMinkowskiHull( - Polygon.approximatedCircle(disallowed_border_size)) - if not prime_tower_collision: - result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) - result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) - else: - self._error_areas.extend(prime_tower_areas[extruder_id]) + if prime_tower_collision: # Already found a collision. + break + if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft": + prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) + if not prime_tower_collision: + result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) + result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) + else: + self._error_areas.extend(prime_tower_areas[extruder_id]) self._has_errors = len(self._error_areas) > 0 - self._disallowed_areas = [] + self._disallowed_areas = [] # type: List[Polygon] for extruder_id in result_areas: self._disallowed_areas.extend(result_areas[extruder_id]) self._disallowed_areas_no_brim = [] @@ -826,8 +790,8 @@ class BuildVolume(SceneNode): for extruder in used_extruders: result[extruder.getId()] = [] - #Currently, the only normally printed object is the prime tower. - if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable"): + # Currently, the only normally printed object is the prime tower. + if self._global_container_stack.getProperty("prime_tower_enable"): prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value") @@ -837,8 +801,7 @@ class BuildVolume(SceneNode): prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_y = prime_tower_y + machine_depth / 2 - if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and - ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"): + if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft": brim_size = ( extruder.getProperty("brim_line_count", "value") * extruder.getProperty("skirt_brim_line_width", "value") / 100.0 * @@ -908,9 +871,12 @@ class BuildVolume(SceneNode): # for. # \return A dictionary with for each used extruder ID the disallowed areas # where that extruder may not print. - def _computeDisallowedAreasStatic(self, border_size, used_extruders): - #Convert disallowed areas to polygons and dilate them. + def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: + # Convert disallowed areas to polygons and dilate them. machine_disallowed_polygons = [] + if self._global_container_stack is None: + return {} + for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"): polygon = Polygon(numpy.array(area, numpy.float32)) polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size)) @@ -921,7 +887,7 @@ class BuildVolume(SceneNode): nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry( "nozzle_offsetting_for_disallowed_areas", True) - result = {} + result = {} # type: Dict[str, List[Polygon]] for extruder in used_extruders: extruder_id = extruder.getId() offset_x = extruder.getProperty("machine_nozzle_offset_x", "value") @@ -930,13 +896,13 @@ class BuildVolume(SceneNode): offset_y = extruder.getProperty("machine_nozzle_offset_y", "value") if offset_y is None: offset_y = 0 - offset_y = -offset_y #Y direction of g-code is the inverse of Y direction of Cura's scene space. + offset_y = -offset_y # Y direction of g-code is the inverse of Y direction of Cura's scene space. result[extruder_id] = [] for polygon in machine_disallowed_polygons: - result[extruder_id].append(polygon.translate(offset_x, offset_y)) #Compensate for the nozzle offset of this extruder. + result[extruder_id].append(polygon.translate(offset_x, offset_y)) # Compensate for the nozzle offset of this extruder. - #Add the border around the edge of the build volume. + # Add the border around the edge of the build volume. left_unreachable_border = 0 right_unreachable_border = 0 top_unreachable_border = 0 @@ -944,7 +910,8 @@ class BuildVolume(SceneNode): # Only do nozzle offsetting if needed if nozzle_offsetting_for_disallowed_areas: - #The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders. + # The build volume is defined as the union of the area that all extruders can reach, so we need to know + # the relative offset to all extruders. for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value") if other_offset_x is None: @@ -1028,8 +995,8 @@ class BuildVolume(SceneNode): [ half_machine_width - border_size, 0] ], numpy.float32))) result[extruder_id].append(Polygon(numpy.array([ - [ half_machine_width,-half_machine_depth], - [-half_machine_width,-half_machine_depth], + [ half_machine_width, -half_machine_depth], + [-half_machine_width, -half_machine_depth], [ 0, -half_machine_depth + border_size] ], numpy.float32))) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 53c0a54f85..7674aa05b9 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -205,7 +205,7 @@ class ExtruderManager(QObject): # list. # # \return A list of extruder stacks. - def getUsedExtruderStacks(self) -> List["ContainerStack"]: + def getUsedExtruderStacks(self) -> List["ExtruderStack"]: global_stack = self._application.getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 0760581c70..241d1a954f 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -1196,6 +1196,13 @@ class XmlMaterialProfile(InstanceContainer): "surface energy": "material_surface_energy", "shrinkage percentage": "material_shrinkage_percentage", "build volume temperature": "build_volume_temperature", + "anti ooze retracted position": "material_anti_ooze_retracted_position", + "anti ooze retract speed": "material_anti_ooze_retraction_speed", + "break preparation retracted position": "material_break_preparation_retracted_position", + "break preparation speed": "material_break_preparation_speed", + "break retracted position": "material_break_retracted_position", + "break speed": "material_break_speed", + "break temperature": "material_break_temperature" } __unmapped_settings = [ "hardware compatible", diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index d37be875b8..bbd3f5b870 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2234,6 +2234,107 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "material_crystallinity": + { + "label": "Crystalline Material", + "description": "Is this material the type that breaks off cleanly when heated (crystalline), or is it the type that produces long intertwined polymer chains (non-crystalline)?", + "type": "bool", + "default_value": false, + "enabled": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_anti_ooze_retracted_position": + { + "label": "Anti-ooze Retracted Position", + "description": "How far the material needs to be retracted before it stops oozing.", + "type": "float", + "unit": "mm", + "default_value": 4, + "enabled": false, + "minimum_value_warning": "0", + "maximum_value_warning": "retraction_amount", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_anti_ooze_retraction_speed": + { + "label": "Anti-ooze Retraction Speed", + "description": "How fast the material needs to be retracted during a filament switch to prevent oozing.", + "type": "float", + "unit": "mm/s", + "default_value": 5, + "enabled": false, + "minimum_value": "0", + "maximum_value": "machine_max_feedrate_e", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_break_preparation_retracted_position": + { + "label": "Break Preparation Retracted Position", + "description": "How far the filament can be stretched before it breaks, while heated.", + "type": "float", + "unit": "mm", + "default_value": 16, + "enabled": false, + "minimum_value_warning": "0", + "maximum_value_warning": "retraction_amount * 4", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_break_preparation_speed": + { + "label": "Break Preparation Retraction Speed", + "description": "How fast the filament needs to be retracted just before breaking it off in a retraction.", + "type": "float", + "unit": "mm/s", + "default_value": 2, + "enabled": false, + "minimum_value": "0", + "maximum_value": "machine_max_feedrate_e", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_break_retracted_position": + { + "label": "Break Retracted Position", + "description": "How far to retract the filament in order to break it cleanly.", + "type": "float", + "unit": "mm", + "default_value": 50, + "enabled": false, + "minimum_value_warning": "0", + "maximum_value_warning": "100", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_break_speed": + { + "label": "Break Retraction Speed", + "description": "The speed at which to retract the filament in order to break it cleanly.", + "type": "float", + "unit": "mm/s", + "default_value": 25, + "enabled": false, + "minimum_value": "0", + "maximum_value": "machine_max_feedrate_e", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "material_break_temperature": + { + "label": "Break Temperature", + "description": "The temperature at which the filament is broken for a clean break.", + "type": "float", + "unit": "°C", + "default_value": 50, + "enabled": false, + "minimum_value": "-273.15", + "maximum_value_warning": "300", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "material_flow": { "label": "Flow", diff --git a/tests/TestBuildVolume.py b/tests/TestBuildVolume.py index 51a5f7e9e2..902d8a839c 100644 --- a/tests/TestBuildVolume.py +++ b/tests/TestBuildVolume.py @@ -43,6 +43,109 @@ def test_buildGridMesh(build_volume): assert numpy.array_equal(result_vertices, mesh.getVertices()) +def test_clamp(build_volume): + assert build_volume._clamp(0, 0, 200) == 0 + assert build_volume._clamp(0, -200, 200) == 0 + assert build_volume._clamp(300, -200, 200) == 200 + + +class TestCalculateBedAdhesionSize: + setting_property_dict = {"adhesion_type": {"value": "brim"}, + "skirt_brim_line_width": {"value": 0}, + "initial_layer_line_width_factor": {"value": 0}, + "brim_line_count": {"value": 0}, + "machine_width": {"value": 200}, + "machine_depth": {"value": 200}, + "skirt_line_count": {"value": 0}, + "skirt_gap": {"value": 0}, + "raft_margin": {"value": 0} + } + + def getPropertySideEffect(*args, **kwargs): + properties = TestCalculateBedAdhesionSize.setting_property_dict.get(args[1]) + if properties: + return properties.get(args[2]) + + def createAndSetGlobalStack(self, build_volume): + mocked_stack = MagicMock() + mocked_stack.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + + build_volume._global_container_stack = mocked_stack + + def test_noGlobalStack(self, build_volume: BuildVolume): + assert build_volume._calculateBedAdhesionSize([]) is None + + @pytest.mark.parametrize("setting_dict, result", [ + ({}, 0), + ({"adhesion_type": {"value": "skirt"}}, 0), + ({"adhesion_type": {"value": "raft"}}, 0), + ({"adhesion_type": {"value": "none"}}, 0), + ({"adhesion_type": {"value": "skirt"}, "skirt_line_count": {"value": 2}, "initial_layer_line_width_factor": {"value": 1}, "skirt_brim_line_width": {"value": 2}}, 0.02), + # Even though it's marked as skirt, it should behave as a brim as the prime tower has a brim (skirt line count is still at 0!) + ({"adhesion_type": {"value": "skirt"}, "prime_tower_brim_enable": {"value": True}, "skirt_brim_line_width": {"value": 2}, "initial_layer_line_width_factor": {"value": 3}}, -0.06), + ({"brim_line_count": {"value": 1}, "skirt_brim_line_width": {"value": 2}, "initial_layer_line_width_factor": {"value": 3}}, 0), + ({"brim_line_count": {"value": 2}, "skirt_brim_line_width": {"value": 2}, "initial_layer_line_width_factor": {"value": 3}}, 0.06), + ({"brim_line_count": {"value": 9000000}, "skirt_brim_line_width": {"value": 90000}, "initial_layer_line_width_factor": {"value": 9000}}, 100), # Clamped at half the max size of buildplate + ]) + def test_singleExtruder(self, build_volume: BuildVolume, setting_dict, result): + self.createAndSetGlobalStack(build_volume) + patched_dictionary = self.setting_property_dict.copy() + patched_dictionary.update(setting_dict) + with patch.dict(self.setting_property_dict, patched_dictionary): + assert build_volume._calculateBedAdhesionSize([]) == result + + def test_unknownBedAdhesion(self, build_volume: BuildVolume): + self.createAndSetGlobalStack(build_volume) + patched_dictionary = self.setting_property_dict.copy() + patched_dictionary.update({"adhesion_type": {"value": "OMGZOMGBBQ"}}) + with patch.dict(self.setting_property_dict, patched_dictionary): + with pytest.raises(Exception): + build_volume._calculateBedAdhesionSize([]) + +class TestComputeDisallowedAreasStatic: + setting_property_dict = {"machine_disallowed_areas": {"value": [[[-200, 112.5], [ -82, 112.5], [ -84, 102.5], [-115, 102.5]]]}, + "machine_width": {"value": 200}, + "machine_depth": {"value": 200}, + } + + def getPropertySideEffect(*args, **kwargs): + properties = TestComputeDisallowedAreasStatic.setting_property_dict.get(args[1]) + if properties: + return properties.get(args[2]) + + def test_computeDisallowedAreasStaticNoExtruder(self, build_volume: BuildVolume): + mocked_stack = MagicMock() + mocked_stack.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + + build_volume._global_container_stack = mocked_stack + assert build_volume._computeDisallowedAreasStatic(0, []) == {} + + def test_computeDisalowedAreasStaticSingleExtruder(self, build_volume: BuildVolume): + mocked_stack = MagicMock() + mocked_stack.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + + mocked_extruder = MagicMock() + mocked_extruder.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + mocked_extruder.getId = MagicMock(return_value = "zomg") + + build_volume._global_container_stack = mocked_stack + with patch("cura.Settings.ExtruderManager.ExtruderManager.getInstance"): + result = build_volume._computeDisallowedAreasStatic(0, [mocked_extruder]) + assert result == {"zomg": [Polygon([[-84.0, 102.5], [-115.0, 102.5], [-200.0, 112.5], [-82.0, 112.5]])]} + + def test_computeDisalowedAreasMutliExtruder(self, build_volume): + mocked_stack = MagicMock() + mocked_stack.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + + mocked_extruder = MagicMock() + mocked_extruder.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + mocked_extruder.getId = MagicMock(return_value="zomg") + extruder_manager = MagicMock() + extruder_manager.getActiveExtruderStacks = MagicMock(return_value = [mocked_stack]) + build_volume._global_container_stack = mocked_stack + with patch("cura.Settings.ExtruderManager.ExtruderManager.getInstance", MagicMock(return_value = extruder_manager)): + result = build_volume._computeDisallowedAreasStatic(0, [mocked_extruder]) + assert result == {"zomg": [Polygon([[-84.0, 102.5], [-115.0, 102.5], [-200.0, 112.5], [-82.0, 112.5]])]} class TestUpdateRaftThickness: setting_property_dict = {"raft_base_thickness": {"value": 1},