diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 7c2183c5a09..973b7a00390 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -2,10 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import uuid -import os - import numpy -from string import Formatter from enum import IntEnum import time from typing import Any, cast, Dict, List, Optional, Set, Tuple @@ -26,7 +23,6 @@ from UM.Scene.Scene import Scene #For typing. from UM.Settings.Validator import ValidatorState from UM.Settings.SettingRelation import RelationType -from UM.Settings.SettingFunction import SettingFunction from cura.CuraApplication import CuraApplication from cura.Scene.CuraSceneNode import CuraSceneNode @@ -49,181 +45,6 @@ class StartJobResult(IntEnum): ObjectsWithDisabledExtruder = 8 -class GcodeConditionState(IntEnum): - OutsideCondition = 1 - ConditionFalse = 2 - ConditionTrue = 3 - ConditionDone = 4 - - -class GcodeInstruction(IntEnum): - Skip = 1 - Evaluate = 2 - EvaluateAndWrite = 3 - - -class GcodeStartEndFormatter: - # Formatter class that handles token expansion in start/end gcode - # Example of a start/end gcode string: - # ``` - # M104 S{material_print_temperature_layer_0, 0} ;pre-heat - # M140 S{material_bed_temperature_layer_0} ;heat bed - # M204 P{acceleration_print, 0} T{acceleration_travel, 0} - # M205 X{jerk_print, 0} - # ``` - # Any expression between curly braces will be evaluated and replaced with the result, using the - # context of the provided default extruder. If no default extruder is provided, the global stack - # will be used. Alternatively, if the expression is formatted as "{[expression], [extruder_nr]}", - # then the expression will be evaluated with the extruder stack of the specified extruder_nr. - - _instruction_regex = re.compile(r"{(?Pif|else|elif|endif)?\s*(?P[^{}]*?)\s*(?:,\s*(?P[^{}]*))?\s*}(?P\n?)") - - def __init__(self, all_extruder_settings: Dict[str, Dict[str, Any]], default_extruder_nr: int = -1) -> None: - super().__init__() - self._all_extruder_settings: Dict[str, Dict[str, Any]] = all_extruder_settings - self._default_extruder_nr: int = default_extruder_nr - self._cura_application = CuraApplication.getInstance() - self._extruder_manager = ExtruderManager.getInstance() - - def format(self, text: str) -> str: - remaining_text: str = text - result: str = "" - - self._condition_state: GcodeConditionState = GcodeConditionState.OutsideCondition - - while len(remaining_text) > 0: - next_code_match = self._instruction_regex.search(remaining_text) - if next_code_match is not None: - expression_start, expression_end = next_code_match.span() - - if expression_start > 0: - result += self._process_statement(remaining_text[:expression_start]) - - result += self._process_code(next_code_match) - - remaining_text = remaining_text[expression_end:] - - else: - result += self._process_statement(remaining_text) - remaining_text = "" - - return result - - def _process_statement(self, statement: str) -> str: - if self._condition_state in [GcodeConditionState.OutsideCondition, GcodeConditionState.ConditionTrue]: - return statement - else: - return "" - - def _process_code(self, code: re.Match) -> str: - condition: Optional[str] = code.group("condition") - expression: Optional[str] = code.group("expression") - extruder_nr_expr: Optional[str] = code.group("extruder_nr_expr") - end_of_line: Optional[str] = code.group("end_of_line") - - # The following variables are not settings, but only become available after slicing. - # when these variables are encountered, we return them as-is. They are replaced later - # when the actual values are known. - post_slice_data_variables = ["filament_cost", "print_time", "filament_amount", "filament_weight", "jobname"] - if expression in post_slice_data_variables: - return f"{{{expression}}}" - - extruder_nr: str = str(self._default_extruder_nr) - instruction: GcodeInstruction = GcodeInstruction.Skip - - # The settings may specify a specific extruder to use. This is done by - # formatting the expression as "{expression}, {extruder_nr_expr}". If the - # expression is formatted like this, we extract the extruder_nr and use - # it to get the value from the correct extruder stack. - if condition is None: - # This is a classic statement - if self._condition_state in [GcodeConditionState.OutsideCondition, GcodeConditionState.ConditionTrue]: - # Skip and move to next - instruction = GcodeInstruction.EvaluateAndWrite - else: - # This is a condition statement, first check validity - if condition == "if": - if self._condition_state != GcodeConditionState.OutsideCondition: - raise SyntaxError("Nested conditions are not supported") - else: - if self._condition_state == GcodeConditionState.OutsideCondition: - raise SyntaxError("Condition should start with an 'if' statement") - - if condition == "if": - # First instruction, just evaluate it - instruction = GcodeInstruction.Evaluate - - else: - if self._condition_state == GcodeConditionState.ConditionTrue: - # We have reached the next condition after a valid one has been found, skip the rest - self._condition_state = GcodeConditionState.ConditionDone - - if condition == "elif": - if self._condition_state == GcodeConditionState.ConditionFalse: - # New instruction, and valid condition has not been reached so far => evaluate it - instruction = GcodeInstruction.Evaluate - else: - # New instruction, but valid condition has already been reached => skip it - instruction = GcodeInstruction.Skip - - elif condition == "else": - instruction = GcodeInstruction.Skip # Never evaluate, expression should be empty - if self._condition_state == GcodeConditionState.ConditionFalse: - # Fallback instruction, and valid condition has not been reached so far => active next - self._condition_state = GcodeConditionState.ConditionTrue - - elif condition == "endif": - instruction = GcodeInstruction.Skip # Never evaluate, expression should be empty - self._condition_state = GcodeConditionState.OutsideCondition - - if instruction >= GcodeInstruction.Evaluate and extruder_nr_expr is not None: - extruder_nr_function = SettingFunction(extruder_nr_expr) - container_stack = self._cura_application.getGlobalContainerStack() - - # We add the variables contained in `_all_extruder_settings["-1"]`, which is a dict-representation of the - # global container stack, with additional properties such as `initial_extruder_nr`. As users may enter such - # expressions we can't use the global container stack. The variables contained in the global container stack - # will then be inserted twice, which is not optimal but works well. - extruder_nr = str(extruder_nr_function(container_stack, additional_variables=self._all_extruder_settings["-1"])) - - if instruction >= GcodeInstruction.Evaluate: - if extruder_nr in self._all_extruder_settings: - additional_variables = self._all_extruder_settings[extruder_nr].copy() - else: - Logger.warning(f"Extruder {extruder_nr} does not exist, using global settings") - additional_variables = self._all_extruder_settings["-1"].copy() - - if extruder_nr == "-1": - container_stack = self._cura_application.getGlobalContainerStack() - else: - container_stack = self._extruder_manager.getExtruderStack(extruder_nr) - if not container_stack: - Logger.warning(f"Extruder {extruder_nr} does not exist, using global settings") - container_stack = self._cura_application.getGlobalContainerStack() - - setting_function = SettingFunction(expression) - value = setting_function(container_stack, additional_variables=additional_variables) - - if instruction == GcodeInstruction.Evaluate: - if value: - self._condition_state = GcodeConditionState.ConditionTrue - else: - self._condition_state = GcodeConditionState.ConditionFalse - - return "" - else: - value_str = str(value) - - if end_of_line is not None: - # If we are evaluating an expression that is not a condition, restore the end of line - value_str += end_of_line - - return value_str - - else: - return "" - - class StartSliceJob(Job): """Job class that builds up the message of scene data to send to CuraEngine.""" @@ -413,6 +234,9 @@ def run(self) -> None: is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh") if not is_non_printing_mesh: for used_extruder in StartSliceJob._getMainExtruders(node): + if used_extruder >= len(extruders_enabled): + continue + if not extruders_enabled[used_extruder]: skip_group = True has_model_with_disabled_extruders = True @@ -580,25 +404,6 @@ def _cacheAllExtruderSettings(self): self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str: - """Replace setting tokens in a piece of g-code. - - :param value: A piece of g-code to replace tokens in. - :param default_extruder_nr: Stack nr to use when no stack nr is specified, defaults to the global stack - """ - if not self._all_extruders_settings: - self._cacheAllExtruderSettings() - - try: - # Get "replacement-keys" for the extruders. In the formatter the settings stack is used to get the - # replacement values for the setting-keys. However, the values for `material_id`, `material_type`, - # etc are not in the settings stack. - fmt = GcodeStartEndFormatter(self._all_extruders_settings, default_extruder_nr=default_extruder_nr) - return str(fmt.format(value)) - except: - Logger.logException("w", "Unable to do token replacement on start/end g-code") - return str(value) - def _buildExtruderMessage(self, stack: ContainerStack) -> None: """Create extruder message from stack""" @@ -616,12 +421,6 @@ def _buildExtruderMessage(self, stack: ContainerStack) -> None: # Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it. settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "") - # Replace the setting tokens in start and end g-code. - extruder_nr = stack.getProperty("extruder_nr", "value") - settings["machine_extruder_prestart_code"] = self._expandGcodeTokens(settings["machine_extruder_prestart_code"], extruder_nr) - settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr) - settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr) - global_definition = cast(ContainerInterface, cast(ContainerStack, stack.getNextStack()).getBottom()) own_definition = cast(ContainerInterface, stack.getBottom()) @@ -666,12 +465,6 @@ def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr} settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) is None - # Replace the setting tokens in start and end g-code. - # Use values from the first used extruder by default so we get the expected temperatures - initial_extruder_nr = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr() - settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr) - settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr) - # Manually add 'nozzle offsetting', since that is a metadata-entry instead for some reason. # NOTE: This probably needs to be an actual setting at some point. settings["nozzle_offsetting_for_disallowed_areas"] = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("nozzle_offsetting_for_disallowed_areas", True) diff --git a/tests/Machines/TestStartEndGCode.py b/tests/Machines/TestStartEndGCode.py deleted file mode 100644 index 611567bacde..00000000000 --- a/tests/Machines/TestStartEndGCode.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright (c) 2024 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import pytest -from unittest.mock import MagicMock - -from plugins.CuraEngineBackend.StartSliceJob import GcodeStartEndFormatter - - -class MockValueProvider: - ## Creates a mock value provider. - # - # This initialises a dictionary with key-value pairs. - def __init__(self, values): - self._values = values - - ## Provides a value. - # - # \param name The key of the value to provide. - def getProperty(self, key, property_name, context = None): - if not (key in self._values): - return None - return self._values[key] - -extruder_0_values = { - "material_temperature": 190.0 -} - -extruder_1_values = { - "material_temperature": 210.0 -} - -global_values = { - "bed_temperature": 50.0, - "initial_extruder": 0 -} - -extruder_0_provider = MockValueProvider(extruder_0_values) -extruder_1_provider = MockValueProvider(extruder_1_values) - -all_extruder_settings = {"-1": global_values, "0": extruder_0_values, "1": extruder_1_values} - -test_cases = [ - ('Static code', None, 'G0', 'G0'), - - ('Basic replacement', None, 'M128 {bed_temperature}', 'M128 50.0'), - - ( - 'Conditional expression with global setting', - None, -'''{if bed_temperature > 30} -G123 -{else} -G456 -{endif}''', -'''G123 -''' - ), - - ( - 'Conditional expression with extruder setting directly specified by index 0', - None, -'''{if material_temperature > 200, 0} -G10 -{else} -G20 -{endif}''', -'''G20 -''' - ), - ( - 'Conditional expression with extruder setting directly specified by index 1', - None, -'''{if material_temperature > 200, 1} -G100 -{else} -G200 -{endif}''', -'''G100 -''' - ), - - ( - 'Conditional expression with extruder index specified by setting', - None, -'''{if material_temperature > 200, initial_extruder} -G1000 -{else} -G2000 -{endif}''', -'''G2000 -''' - ), - - ( - 'Conditional expression with extruder index specified by formula', - None, -'''{if material_temperature > 200, (initial_extruder + 1) % 2} -X1000 -{else} -X2000 -{endif}''', -'''X1000 -''' - ), - - ( - 'Conditional expression with elsif', - None, -'''{if bed_temperature < 30} -T30 -{elif bed_temperature >= 30 and bed_temperature < 40} -T40 -{elif bed_temperature >= 40 and bed_temperature < 50} -T50 -{elif bed_temperature >= 50 and bed_temperature < 60} -T60 -{elif bed_temperature >= 60 and bed_temperature < 70} -T70 -{else} -T-800 -{endif}''', -'''T60 -''' - ), - - ( - 'Formula inside a conditional expression', - None, -'''{if bed_temperature < 30} -Z000 -{else} -Z{bed_temperature + 10} -{endif}''', -'''Z60.0 -''' - ), - - ( - 'Other commands around conditional expression', - None, -''' -R000 -# My super initial command -R111 X123 Y456 Z789 -{if bed_temperature > 30} -R987 -R654 X321 -{else} -R963 X852 Y741 -R321 X654 Y987 -{endif} -# And finally, the end of the start at the beginning of the header -R369 -R357 X951 Y843''', -''' -R000 -# My super initial command -R111 X123 Y456 Z789 -R987 -R654 X321 -# And finally, the end of the start at the beginning of the header -R369 -R357 X951 Y843''' - ), - - ( - 'Multiple conditional expressions', - None, -''' -A999 -{if bed_temperature > 30} -A000 -{else} -A100 -{endif} -A888 -{if material_temperature > 200, 0} -A200 -{else} -A300 -{endif} -A777 -''', -''' -A999 -A000 -A888 -A300 -A777 -''' - ), - - ( - 'Nested condition expression', - SyntaxError, -'''{if bed_temperature < 30} -{if material_temperature < 30, 0} -M000 -{else} -M888 -{endif} -{else} -M{bed_temperature + 10} -{endif}''', - '' - ), - - ( - 'Wrong condition expression', - SyntaxError, -'''{of material_temperature > 200, 1} -G100 -{else} -G200 -{endif}''', - '' - ), - - ( - 'Condition expression without start', - SyntaxError, -''' -W100 -{else} -W200 -{endif}''', - '' - ), - - ( - 'Formula with non-existing variable', - None, - '{material_storage_temperature}', - '0' - ), - - ( - 'Missing formula end character', - None, - '{material_temperature, 0', - '{material_temperature, 0' - ), - - ( - 'Conditional expression with missing end character', - SyntaxError, -'''{if material_temperature > 200, 0 -Q1000 -{else} -Q2000 -{endif}''', - '' - ), - - ( - 'Unexpected end character', - None, -'''{if material_temperature > 200, 0}} -S1000 -{else} -S2000 -{endif}''', -'''S2000 -''' - ), - - ( - 'Multiple replaces on single line', - None, -'''BT={bed_temperature} IE={initial_extruder}''', -'''BT=50.0 IE=0''' - ), - - ( - 'Multiple extruder replaces on single line', - None, -'''MT0={material_temperature, 0} MT1={material_temperature, 1}''', -'''MT0=190.0 MT1=210.0''' - ), -] - -def pytest_generate_tests(metafunc): - if "original_gcode" in metafunc.fixturenames: - tests_ids = [test[0] for test in test_cases] - tests_data = [test[1:] for test in test_cases] - metafunc.parametrize("exception_type, original_gcode, expected_gcode", tests_data, ids = tests_ids) - -@pytest.fixture -def cura_application(): - result = MagicMock() - result.getGlobalContainerStack = MagicMock(return_value = MockValueProvider(global_values)) - return result - -@pytest.fixture -def extruder_manager(): - def get_extruder(extruder_nr: str): - if extruder_nr == "0": - return extruder_0_provider - elif extruder_nr == "1": - return extruder_1_provider - else: - return None - - result = MagicMock() - result.getExtruderStack = MagicMock(side_effect = get_extruder) - return result - -def test_startEndGCode_replace(cura_application, extruder_manager, exception_type, original_gcode, expected_gcode): - formatter = GcodeStartEndFormatter(all_extruder_settings, -1) - formatter._cura_application = cura_application - formatter._extruder_manager = extruder_manager - - if exception_type is not None: - with pytest.raises(exception_type): - formatter.format(original_gcode) - else: - assert formatter.format(original_gcode) == expected_gcode