mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-04-19 20:29:40 +08:00
Add ability to write condition Start/End gcode parts
CURA-12093
This commit is contained in:
parent
11f1608b6c
commit
ccffbea8c9
@ -49,7 +49,20 @@ class StartJobResult(IntEnum):
|
||||
ObjectsWithDisabledExtruder = 8
|
||||
|
||||
|
||||
class GcodeStartEndFormatter(Formatter):
|
||||
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:
|
||||
# ```
|
||||
@ -63,22 +76,55 @@ class GcodeStartEndFormatter(Formatter):
|
||||
# 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.
|
||||
|
||||
_extruder_regex = re.compile(r"^\s*(?P<expression>.*)\s*,\s*(?P<extruder_nr_expr>.*)\s*$")
|
||||
_instruction_regex = re.compile(r"{(?P<condition>if|else|elif|endif)?\s*(?P<expression>.*?)\s*(?:,\s*(?P<extruder_nr_expr>.*))?\s*}(?P<end_of_line>\n?)")
|
||||
|
||||
def __init__(self, all_extruder_settings: Dict[str, Any], default_extruder_nr: int = -1) -> None:
|
||||
def __init__(self,
|
||||
all_extruder_settings: Dict[str, Dict[str, Any]],
|
||||
default_extruder_nr: int = -1,
|
||||
cura_application: CuraApplication = None,
|
||||
extruder_manager: ExtruderManager = None
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._all_extruder_settings: Dict[str, Any] = all_extruder_settings
|
||||
self._all_extruder_settings: Dict[str, Dict[str, Any]] = all_extruder_settings
|
||||
self._default_extruder_nr: int = default_extruder_nr
|
||||
self._cura_application = cura_application if cura_application is not None else CuraApplication.getInstance()
|
||||
self._extruder_manager = extruder_manager if extruder_manager is not None else ExtruderManager.getInstance()
|
||||
|
||||
def get_field(self, field_name, args: [str], kwargs: dict) -> Tuple[str, str]:
|
||||
# get_field method parses all fields in the format-string and parses them individually to the get_value method.
|
||||
# e.g. for a string "Hello {foo.bar}" would the complete field "foo.bar" would be passed to get_field, and then
|
||||
# the individual parts "foo" and "bar" would be passed to get_value. This poses a problem for us, because want
|
||||
# to parse the entire field as a single expression. To solve this, we override the get_field method and return
|
||||
# the entire field as the expression.
|
||||
return self.get_value(field_name, args, kwargs), field_name
|
||||
def format(self, text: str) -> str:
|
||||
remaining_text: str = text
|
||||
result: str = ""
|
||||
|
||||
def get_value(self, expression: str, args: [str], kwargs: dict) -> 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
|
||||
@ -87,53 +133,100 @@ class GcodeStartEndFormatter(Formatter):
|
||||
if expression in post_slice_data_variables:
|
||||
return f"{{{expression}}}"
|
||||
|
||||
extruder_nr = str(self._default_extruder_nr)
|
||||
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.
|
||||
match = self._extruder_regex.match(expression)
|
||||
if match:
|
||||
expression = match.group("expression")
|
||||
extruder_nr_expr = match.group("extruder_nr_expr")
|
||||
|
||||
if extruder_nr_expr.isdigit():
|
||||
extruder_nr = extruder_nr_expr
|
||||
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:
|
||||
# We get the value of the extruder_nr_expr from `_all_extruder_settings` dictionary
|
||||
# rather than the global container stack. The `_all_extruder_settings["-1"]` 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.
|
||||
extruder_nr = str(self._all_extruder_settings["-1"].get(extruder_nr_expr, "-1"))
|
||||
if self._condition_state == GcodeConditionState.OutsideCondition:
|
||||
raise SyntaxError("Condition should start with an 'if' statement")
|
||||
|
||||
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 condition == "if":
|
||||
# First instruction, just evaluate it
|
||||
instruction = GcodeInstruction.Evaluate
|
||||
|
||||
# Add the arguments and keyword arguments to the additional settings. These
|
||||
# are currently _not_ used, but they are added for consistency with the
|
||||
# base Formatter class.
|
||||
for key, value in enumerate(args):
|
||||
additional_variables[key] = value
|
||||
for key, value in kwargs.items():
|
||||
additional_variables[key] = value
|
||||
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 extruder_nr == "-1":
|
||||
container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
else:
|
||||
container_stack = ExtruderManager.getInstance().getExtruderStack(extruder_nr)
|
||||
if not container_stack:
|
||||
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")
|
||||
container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
additional_variables = self._all_extruder_settings["-1"].copy()
|
||||
|
||||
setting_function = SettingFunction(expression)
|
||||
value = setting_function(container_stack, additional_variables=additional_variables)
|
||||
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()
|
||||
|
||||
return value
|
||||
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):
|
||||
|
220
tests/Machines/TestStartEndGCode.py
Normal file
220
tests/Machines/TestStartEndGCode.py
Normal file
@ -0,0 +1,220 @@
|
||||
# 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
|
||||
|
||||
|
||||
# def createMockedInstanceContainer(container_id):
|
||||
# result = MagicMock()
|
||||
# result.getId = MagicMock(return_value=container_id)
|
||||
# result.getMetaDataEntry = MagicMock(side_effect=getMetadataEntrySideEffect)
|
||||
# return result
|
||||
|
||||
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', 'G0', 'G0'),
|
||||
|
||||
('Basic replacement', 'M128 {bed_temperature}', 'M128 50.0'),
|
||||
|
||||
(
|
||||
'Conditional expression with global setting',
|
||||
'''{if bed_temperature > 30}
|
||||
G123
|
||||
{else}
|
||||
G456
|
||||
{endif}''',
|
||||
'''G123
|
||||
'''
|
||||
),
|
||||
|
||||
(
|
||||
'Conditional expression with extruder setting directly specified by index 0',
|
||||
'''{if material_temperature > 200, 0}
|
||||
G10
|
||||
{else}
|
||||
G20
|
||||
{endif}''',
|
||||
'''G20
|
||||
'''
|
||||
),
|
||||
(
|
||||
'Conditional expression with extruder setting directly specified by index 1',
|
||||
'''{if material_temperature > 200, 1}
|
||||
G100
|
||||
{else}
|
||||
G200
|
||||
{endif}''',
|
||||
'''G100
|
||||
'''
|
||||
),
|
||||
|
||||
(
|
||||
'Conditional expression with extruder index specified by setting',
|
||||
'''{if material_temperature > 200, initial_extruder}
|
||||
G1000
|
||||
{else}
|
||||
G2000
|
||||
{endif}''',
|
||||
'''G2000
|
||||
'''
|
||||
),
|
||||
|
||||
(
|
||||
'Conditional expression with extruder index specified by formula',
|
||||
'''{if material_temperature > 200, (initial_extruder + 1) % 2}
|
||||
X1000
|
||||
{else}
|
||||
X2000
|
||||
{endif}''',
|
||||
'''X1000
|
||||
'''
|
||||
),
|
||||
|
||||
(
|
||||
'Conditional expression with elsif',
|
||||
'''{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',
|
||||
'''{if bed_temperature < 30}
|
||||
Z000
|
||||
{else}
|
||||
Z{bed_temperature + 10}
|
||||
{endif}''',
|
||||
'''Z60.0
|
||||
'''
|
||||
),
|
||||
|
||||
(
|
||||
'Other commands around conditional expression',
|
||||
'''
|
||||
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',
|
||||
'''
|
||||
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
|
||||
'''
|
||||
),
|
||||
]
|
||||
|
||||
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("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, original_gcode, expected_gcode):
|
||||
formatter = GcodeStartEndFormatter(all_extruder_settings, -1, cura_application, extruder_manager)
|
||||
assert formatter.format(original_gcode) == expected_gcode
|
Loading…
x
Reference in New Issue
Block a user