Merge branch 'main' into fix_qml_py_re

This commit is contained in:
Erwan MATHIEU 2025-02-11 13:19:50 +01:00 committed by GitHub
commit 01fd82e8e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8063 changed files with 78301 additions and 15031 deletions

View file

@ -132,6 +132,7 @@ class ThreeMFReader(MeshReader):
vertices = numpy.resize(data, (int(data.size / 3), 3))
mesh_builder.setVertices(vertices)
mesh_builder.calculateNormals(fast=True)
mesh_builder.setMeshId(node_id)
if file_name:
# The filename is used to give the user the option to reload the file if it is changed on disk
# It is only set for the root node of the 3mf file

View file

@ -1354,7 +1354,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
else:
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
quality_group_dict = container_tree.getCurrentQualityGroups()
if self._quality_type_to_apply in quality_group_dict:
quality_group = quality_group_dict[self._quality_type_to_apply]

View file

@ -8,9 +8,12 @@ from io import StringIO
from threading import Lock
import zipfile
from typing import Dict, Any
from pathlib import Path
from zipfile import ZipFile
from UM.Application import Application
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
@ -33,7 +36,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
if self._ucp_model != model:
self._ucp_model = model
def _write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
def _write(self, stream, nodes, mode, include_log):
application = Application.getInstance()
machine_manager = application.getMachineManager()
@ -79,6 +82,11 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
if self._ucp_model is not None:
user_settings_data = self._getUserSettings(self._ucp_model)
ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
# Write log file
if include_log:
ThreeMFWorkspaceWriter._writeLogFile(archive)
except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.")
@ -125,8 +133,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
return True
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
success = self._write(stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode)
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode, **kwargs):
success = self._write(stream, nodes, WorkspaceWriter.OutputMode.BinaryMode, kwargs.get("include_log", False))
self._ucp_model = None
return success
@ -191,6 +199,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
return
@staticmethod
def _writeLogFile(archive: ZipFile) -> None:
"""Helper function that writes the Cura log file to the archive.
:param archive: The archive to write to.
"""
file_logger = PluginRegistry.getInstance().getPluginObject("FileLogger")
file_logger.flush()
for file_path in file_logger.getFilesPaths():
archive.write(file_path, arcname=f"log/{Path(file_path).name}")
@staticmethod
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
user_settings = {}

View file

@ -114,22 +114,24 @@ class ThreeMFWriter(MeshWriter):
mesh_data = um_node.getMeshData()
node_matrix = um_node.getLocalTransformation()
node_matrix.preMultiply(transformation)
if center_mesh:
node_matrix = Matrix()
center_matrix = Matrix()
# compensate for original center position, if object(s) is/are not around its zero position
if mesh_data is not None:
extents = mesh_data.getExtents()
if extents is not None:
# We use a different coordinate space while writing, so flip Z and Y
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
node_matrix.setByTranslation(center_vector)
node_matrix.multiply(um_node.getLocalTransformation())
else:
node_matrix = um_node.getLocalTransformation()
center_vector = Vector(-extents.center.x, -extents.center.y, -extents.center.z)
center_matrix.setByTranslation(center_vector)
node_matrix.preMultiply(center_matrix)
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix)
savitar_node.setTransformation(matrix_string)
if mesh_data is not None:
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
indices_array = mesh_data.getIndicesAsByteArray()

View file

@ -68,6 +68,9 @@ class CuraEngineBackend(QObject, Backend):
"""
super().__init__()
self._init_done = False
self._immediate_slice_after_init = False
# Find out where the engine is located, and how it is called.
# This depends on how Cura is packaged and which OS we are running on.
executable_name = "CuraEngine"
@ -197,7 +200,8 @@ class CuraEngineBackend(QObject, Backend):
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._resetLastSliceTimeStats()
self._snapshot: Optional[QImage] = None
self._snapshot: Optional[QImage] = None
self._last_socket_error: Optional[Arcus.Error] = None
application.initializationFinished.connect(self.initialize)
@ -267,6 +271,10 @@ class CuraEngineBackend(QObject, Backend):
self._machine_error_checker = application.getMachineErrorChecker()
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
self._init_done = True
if self._immediate_slice_after_init:
self.slice()
def close(self) -> None:
"""Terminate the engine process.
@ -341,6 +349,11 @@ class CuraEngineBackend(QObject, Backend):
def slice(self) -> None:
"""Perform a slice of the scene."""
if not self._init_done:
self._immediate_slice_after_init = True
return
self._immediate_slice_after_init = False
self._createSnapshot()
self.startPlugins()
@ -569,7 +582,20 @@ class CuraEngineBackend(QObject, Backend):
return
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
immediate_success = self._socket.sendMessage(job.getSliceMessage())
if (not CuraApplication.getInstance().getUseExternalBackend()) and (not immediate_success):
if self._last_socket_error is not None and self._last_socket_error.getErrorCode() == Arcus.ErrorCode.MessageTooBigError:
error_txt = catalog.i18nc("@info:status", "Unable to send the model data to the engine. Please try to use a less detailed model, or reduce the number of instances.")
else:
error_txt = catalog.i18nc("@info:status", "Unable to send the model data to the engine. Please try again, or contact support.")
self._error_message = Message(error_txt,
title=catalog.i18nc("@info:title", "Unable to slice"),
message_type=Message.MessageType.WARNING)
self._error_message.show()
self.setState(BackendState.Error)
self.backendError.emit(job)
return
# Notify the user that it's now up to the backend to do its job
self.setState(BackendState.Processing)
@ -691,6 +717,7 @@ class CuraEngineBackend(QObject, Backend):
if error.getErrorCode() == Arcus.ErrorCode.Debug:
return
self._last_socket_error = error
self._terminate()
self._createSocket()

View file

@ -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,50 @@ 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) -> 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 = CuraApplication.getInstance()
self._extruder_manager = 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 +128,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):
@ -366,7 +454,12 @@ class StartSliceJob(Job):
for extruder_stack in global_stack.extruderList:
self._buildExtruderMessage(extruder_stack)
for plugin in CuraApplication.getInstance().getBackendPlugins():
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
# Sort backend plugins by name. Not a very good strategy, but at least it is repeatable. This will be improved later.
backend_plugins = sorted(backend_plugins, key=lambda backend_plugin: backend_plugin.getId())
for plugin in backend_plugins:
if not plugin.usePlugin():
continue
for slot in plugin.getSupportedSlots():
@ -465,6 +558,9 @@ class StartSliceJob(Job):
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
# If adding or changing a setting here, please update the associated wiki page
# https://github.com/Ultimaker/Cura/wiki/Start-End-G%E2%80%90Code
return result
def _cacheAllExtruderSettings(self):
@ -518,6 +614,7 @@ class StartSliceJob(Job):
# 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)

View file

@ -208,7 +208,14 @@ Item
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
enabled: UM.Backend.state == UM.Backend.Done
currentIndex: UM.Backend.state == UM.Backend.Done ? (Cura.MachineManager.activeMachine.getOutputFileFormats.includes("application/x-makerbot") ? 1 : 0) : 2
// Pre-select the correct index, depending on the situation (see the model-property below):
// - Don't select any post-slice-file-format when the engine isn't done.
// - Choose either the S-series or the Makerbot-series of printers' format otherwise, depending on the active printer.
// This way, the user can just click 'save' without having to worry about wether or not the format is right.
property int isMakerbotFormat: Cura.MachineManager.activeMachine.getOutputFileFormats.includes("application/x-makerbot") || Cura.MachineManager.activeMachine.getOutputFileFormats.includes("application/x-makerbot-sketch")
property int isBackendDone: UM.Backend.state == UM.Backend.Done
currentIndex: isBackendDone ? (isMakerbotFormat ? 1 : 0) : 2
textRole: "text"
valueRole: "value"

View file

@ -196,7 +196,7 @@ class DigitalFactoryApiClient:
url = "{}/projects/{}/files".format(self.CURA_API_ROOT, library_project_id)
self._http.get(url,
scope = self._scope,
callback = self._parseCallback(on_finished, DigitalFactoryFileResponse, failed),
callback = self._parseCallback(on_finished, DigitalFactoryFileResponse, failed, default_values = {'username': ''}),
error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
@ -205,7 +205,8 @@ class DigitalFactoryApiClient:
Callable[[List[CloudApiClientModel]], Any]],
model: Type[CloudApiClientModel],
on_error: Optional[Callable] = None,
pagination_manager: Optional[PaginationManager] = None) -> Callable[[QNetworkReply], None]:
pagination_manager: Optional[PaginationManager] = None,
default_values: Dict[str, str] = None) -> Callable[[QNetworkReply], None]:
"""
Creates a callback function so that it includes the parsing of the response into the correct model.
@ -234,7 +235,7 @@ class DigitalFactoryApiClient:
if status_code >= 300 and on_error is not None:
on_error()
else:
self._parseModels(response, on_finished, model, pagination_manager = pagination_manager)
self._parseModels(response, on_finished, model, pagination_manager = pagination_manager, default_values = default_values)
self._anti_gc_callbacks.append(parse)
return parse
@ -262,7 +263,8 @@ class DigitalFactoryApiClient:
on_finished: Union[Callable[[CloudApiClientModel], Any],
Callable[[List[CloudApiClientModel]], Any]],
model_class: Type[CloudApiClientModel],
pagination_manager: Optional[PaginationManager] = None) -> None:
pagination_manager: Optional[PaginationManager] = None,
default_values: Dict[str, str] = None) -> None:
"""Parses the given models and calls the correct callback depending on the result.
:param response: The response from the server, after being converted to a dict.
@ -279,7 +281,10 @@ class DigitalFactoryApiClient:
if "links" in response and pagination_manager:
pagination_manager.setLinks(response["links"])
if isinstance(data, list):
results = [model_class(**c) for c in data] # type: List[CloudApiClientModel]
results = [] # type: List[CloudApiClientModel]
for model_data in data:
complete_model_data = (default_values | model_data) if default_values is not None else model_data
results.append(model_class(**complete_model_data))
on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
on_finished_list(results)
else:

View file

@ -298,8 +298,14 @@ class FlavorParser:
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
pass
def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> None:
# Set extrusion mode
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"

View file

@ -11,14 +11,6 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()
def processMCode(self, M, line, position, path):
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
def _gCode90(self, position, params, path):
"""Set the absolute positioning

View file

@ -54,7 +54,7 @@ Item
{
anchors.top: parent.top
anchors.left: parent.left
width: parent.width * 2 / 3
width: parent.width / 2
spacing: base.columnSpacing
@ -139,6 +139,39 @@ Item
decimals: 0
forceUpdateOnChangeFunction: forceUpdateFunction
}
}
// =======================================
// Right-side column "Nozzle Settings"
// =======================================
Column
{
anchors.top: parent.top
anchors.right: parent.right
width: parent.width / 2
spacing: base.columnSpacing
UM.Label // Title Label
{
text: catalog.i18nc("@title:label", " ")
font: UM.Theme.getFont("medium_bold")
}
Cura.NumericTextFieldWithUnit
{
id: extruderChangeDurationFieldId
containerStackId: base.extruderStackId
settingKey: "machine_extruder_change_duration"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Extruder Change duration")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "s")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit
{
@ -179,24 +212,48 @@ Item
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
Cura.GcodeTextArea // "Extruder Start G-code"
Column
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
width: base.columnWidth - UM.Theme.getSize("default_margin").width
anchors.bottom: buttonLearnMore.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
width: parent.width / 2
labelText: catalog.i18nc("@title:label", "Extruder Start G-code")
containerStackId: base.extruderStackId
settingKey: "machine_extruder_start_code"
settingStoreIndex: propertyStoreIndex
spacing: base.columnSpacing
Cura.GcodeTextArea // "Extruder Prestart G-code"
{
anchors.top: parent.top
anchors.left: parent.left
height: (parent.height / 2) - UM.Theme.getSize("default_margin").height
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "Extruder Prestart G-code")
containerStackId: base.extruderStackId
settingKey: "machine_extruder_prestart_code"
settingStoreIndex: propertyStoreIndex
}
Cura.GcodeTextArea // "Extruder Start G-code"
{
anchors.bottom: parent.bottom
anchors.left: parent.left
height: (parent.height / 2) - UM.Theme.getSize("default_margin").height
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "Extruder Start G-code")
containerStackId: base.extruderStackId
settingKey: "machine_extruder_start_code"
settingStoreIndex: propertyStoreIndex
}
}
Cura.GcodeTextArea // "Extruder End G-code"
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottom: buttonLearnMore.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.right: parent.right
width: base.columnWidth - UM.Theme.getSize("default_margin").width
@ -206,5 +263,17 @@ Item
settingKey: "machine_extruder_end_code"
settingStoreIndex: propertyStoreIndex
}
Cura.TertiaryButton
{
id: buttonLearnMore
text: catalog.i18nc("@button", "Learn more")
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally("https://github.com/Ultimaker/Cura/wiki/Start-End-G%E2%80%90Code")
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}
}

View file

@ -214,7 +214,7 @@ Item
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Y min")
labelText: catalog.i18nc("@label", "Y min ( '-' towards back)")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
@ -254,7 +254,7 @@ Item
settingKey: "machine_head_with_fans_polygon"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Y max")
labelText: catalog.i18nc("@label", "Y max ( '+' towards front)")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
@ -344,6 +344,21 @@ Item
labelWidth: base.labelWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
/*
- Allows user to toggle if Start Gcode is the absolute first gcode.
*/
Cura.SimpleCheckBox // "Make sure Start Code is before all gcodes"
{
id: applyStartGcodeFirstCheckbox
containerStackId: machineStackId
settingKey: "machine_start_gcode_first"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Start GCode must be first")
labelFont: base.labelFont
labelWidth: base.labelWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
/* The "Shared Heater" feature is temporarily disabled because its
@ -376,7 +391,7 @@ Item
anchors
{
top: upperBlock.bottom
bottom: parent.bottom
bottom: buttonLearnMore.top
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
@ -403,5 +418,19 @@ Item
settingKey: "machine_end_gcode"
settingStoreIndex: propertyStoreIndex
}
}
Cura.TertiaryButton
{
id: buttonLearnMore
text: catalog.i18nc("@button", "Learn more")
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally("https://github.com/Ultimaker/Cura/wiki/Start-End-G%E2%80%90Code")
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
}
}

View file

@ -1,4 +1,4 @@
# Copyright (c) 2023 UltiMaker
# Copyright (c) 2024 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from io import StringIO, BufferedIOBase
import json
@ -18,6 +18,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.FormatMaps import FormatMaps
from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
from cura.CuraVersion import ConanInstalls
@ -45,6 +46,13 @@ class MakerbotWriter(MeshWriter):
suffixes=["makerbot"]
)
)
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-makerbot-replicator_plus",
comment="Makerbot Toolpath Package",
suffixes=["makerbot"]
)
)
_PNG_FORMAT = [
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
@ -111,15 +119,15 @@ class MakerbotWriter(MeshWriter):
match file_format:
case "application/x-makerbot-sketch":
filename, filedata = "print.gcode", gcode_text_io.getvalue()
self._PNG_FORMATS = self._PNG_FORMAT
case "application/x-makerbot":
filename, filedata = "print.jsontoolpath", du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
self._PNG_FORMATS = self._PNG_FORMAT + self._PNG_FORMAT_METHOD
case "application/x-makerbot-replicator_plus":
filename, filedata = "print.jsontoolpath", du.gcode_2_miracle_jtp(gcode_text_io.getvalue(), nb_extruders=1)
case _:
raise Exception("Unsupported Mime type")
png_files = []
for png_format in self._PNG_FORMATS:
for png_format in (self._PNG_FORMAT + self._PNG_FORMAT_METHOD):
width, height, prefix = png_format["width"], png_format["height"], png_format["prefix"]
thumbnail_buffer = self._createThumbnail(width, height)
if thumbnail_buffer is None:
@ -137,6 +145,30 @@ class MakerbotWriter(MeshWriter):
for png_file in png_files:
file, data = png_file["file"], png_file["data"]
zip_stream.writestr(file, data)
api = CuraApplication.getInstance().getCuraAPI()
metadata_json = api.interface.settings.getSliceMetadata()
# All the mapping stuff we have to do:
product_to_id_map = FormatMaps.getProductIdMap()
printer_name_map = FormatMaps.getInversePrinterNameMap()
extruder_type_map = FormatMaps.getInverseExtruderTypeMap()
material_map = FormatMaps.getInverseMaterialMap()
for key, value in metadata_json.items():
if "all_settings" in value:
if "machine_name" in value["all_settings"]:
machine_name = value["all_settings"]["machine_name"]
if machine_name in product_to_id_map:
machine_name = product_to_id_map[machine_name][0]
value["all_settings"]["machine_name"] = printer_name_map.get(machine_name, machine_name)
if "machine_nozzle_id" in value["all_settings"]:
extruder_type = value["all_settings"]["machine_nozzle_id"]
value["all_settings"]["machine_nozzle_id"] = extruder_type_map.get(extruder_type, extruder_type)
if "material_type" in value["all_settings"]:
material_type = value["all_settings"]["material_type"]
value["all_settings"]["material_type"] = material_map.get(material_type, material_type)
slice_metadata = json.dumps(metadata_json, separators=(", ", ": "), indent=4)
zip_stream.writestr("slicemetadata.json", slice_metadata)
except (IOError, OSError, BadZipFile) as ex:
Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
@ -226,11 +258,87 @@ class MakerbotWriter(MeshWriter):
meta["preferences"] = dict()
bounds = application.getBuildVolume().getBoundingBox()
intent = CuraApplication.getInstance().getIntentManager().currentIntentCategory
meta["preferences"]["instance0"] = {
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
"machineBounds": [bounds.right, bounds.front, bounds.left, bounds.back] if bounds is not None else None,
"printMode": intent
}
if file_format == "application/x-makerbot":
accel_overrides = meta["accel_overrides"] = {}
if intent in ['highspeed', 'highspeedsolid']:
accel_overrides['do_input_shaping'] = True
accel_overrides['do_corner_rounding'] = True
bead_mode_overrides = accel_overrides["bead_mode"] = {}
accel_enabled = global_stack.getProperty('acceleration_enabled', 'value')
if accel_enabled:
global_accel_setting = global_stack.getProperty('acceleration_print', 'value')
accel_overrides["rate_mm_per_s_sq"] = {
"x": global_accel_setting,
"y": global_accel_setting
}
if global_stack.getProperty('acceleration_travel_enabled', 'value'):
travel_accel_setting = global_stack.getProperty('acceleration_travel', 'value')
bead_mode_overrides['Travel Move'] = {
"rate_mm_per_s_sq": {
"x": travel_accel_setting,
"y": travel_accel_setting
}
}
jerk_enabled = global_stack.getProperty('jerk_enabled', 'value')
if jerk_enabled:
global_jerk_setting = global_stack.getProperty('jerk_print', 'value')
accel_overrides["max_speed_change_mm_per_s"] = {
"x": global_jerk_setting,
"y": global_jerk_setting
}
if global_stack.getProperty('jerk_travel_enabled', 'value'):
travel_jerk_setting = global_stack.getProperty('jerk_travel', 'value')
if 'Travel Move' not in bead_mode_overrides:
bead_mode_overrides['Travel Move' ] = {}
bead_mode_overrides['Travel Move'].update({
"max_speed_change_mm_per_s": {
"x": travel_jerk_setting,
"y": travel_jerk_setting
}
})
# Get bead mode settings per extruder
available_bead_modes = {
"infill": "FILL",
"prime_tower": "PRIME_TOWER",
"roofing": "TOP_SURFACE",
"support_infill": "SUPPORT",
"support_interface": "SUPPORT_INTERFACE",
"wall_0": "WALL_OUTER",
"wall_x": "WALL_INNER",
"skirt_brim": "SKIRT"
}
for idx, extruder in enumerate(extruders):
for bead_mode_setting, bead_mode_tag in available_bead_modes.items():
ext_specific_tag = "%s_%s" % (bead_mode_tag, idx)
if accel_enabled or jerk_enabled:
bead_mode_overrides[ext_specific_tag] = {}
if accel_enabled:
accel_val = extruder.getProperty('acceleration_%s' % bead_mode_setting, 'value')
bead_mode_overrides[ext_specific_tag]["rate_mm_per_s_sq"] = {
"x": accel_val,
"y": accel_val
}
if jerk_enabled:
jerk_val = extruder.getProperty('jerk_%s' % bead_mode_setting, 'value')
bead_mode_overrides[ext_specific_tag][ "max_speed_change_mm_per_s"] = {
"x": jerk_val,
"y": jerk_val
}
meta["miracle_config"] = {"gaggles": {"instance0": {}}}
version_info = dict()

View file

@ -25,6 +25,12 @@ def getMetaData():
"description": catalog.i18nc("@item:inlistbox", "Makerbot Sketch Printfile"),
"mime_type": "application/x-makerbot-sketch",
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
},
{
"extension": file_extension,
"description": catalog.i18nc("@item:inlistbox", "Makerbot Replicator+ Printfile"),
"mime_type": "application/x-makerbot-replicator_plus",
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
}
]
},

View file

@ -1,65 +1,217 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Created by Wayne Porter
# Re-write in April of 2024 by GregValiant (Greg Foresi)
# Changes:
# Added an 'Enable' setting
# Added support for multi-line insertions (comma delimited)
# Added insertions in a range of layers or a single insertion at a layer. Numbers are consistent with the Cura Preview (base1)
# Added frequency of Insertion (once only, every layer, every 2nd, 3rd, 5th, 10th, 25th, 50th, 100th)
# Added support for 'One at a Time' print sequence
# Rafts are allowed and accounted for but no insertions are made in raft layers
from ..Script import Script
import re
from UM.Application import Application
class InsertAtLayerChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Insert at layer change",
"name": "Insert at Layer Change",
"key": "InsertAtLayerChange",
"metadata": {},
"version": 2,
"settings":
{
"insert_location":
"enabled":
{
"label": "When to insert",
"description": "Whether to insert code before or after layer change.",
"label": "Enable this script",
"description": "You must enable the script for it to run.",
"type": "bool",
"default_value": true,
"enabled": true
},
"insert_frequency":
{
"label": "How often to insert",
"description": "Every so many layers starting with the Start Layer OR as single insertion at a specific layer. If the print sequence is 'one_at_a_time' then the insertions will be made for every model. Insertions are made at the beginning of a layer.",
"type": "enum",
"options": {"before": "Before", "after": "After"},
"default_value": "before"
"options": {
"once_only": "One insertion only",
"every_layer": "Every Layer",
"every_2nd": "Every 2nd",
"every_3rd": "Every 3rd",
"every_5th": "Every 5th",
"every_10th": "Every 10th",
"every_25th": "Every 25th",
"every_50th": "Every 50th",
"every_100th": "Every 100th"},
"default_value": "every_layer",
"enabled": "enabled"
},
"start_layer":
{
"label": "Starting Layer",
"description": "The layer before which the first insertion will take place. If the Print_Sequence is 'All at Once' then use the layer numbers from the Cura Preview. Enter '1' to start at gcode LAYER:0. In 'One at a Time' mode use the layer numbers from the first model that prints AND all models will receive the same insertions. NOTE: There is never an insertion for raft layers.",
"type": "int",
"default_value": 1,
"minimum_value": 1,
"enabled": "insert_frequency != 'once_only' and enabled"
},
"end_layer":
{
"label": "Ending Layer",
"description": "The layer before which the last insertion will take place. Enter '-1' to indicate the entire file. Use layer numbers from the Cura Preview.",
"type": "int",
"default_value": -1,
"minimum_value": -1,
"enabled": "insert_frequency != 'once_only' and enabled"
},
"single_end_layer":
{
"label": "Layer # for Single Insertion.",
"description": "The layer before which the Gcode insertion will take place. Use the layer numbers from the Cura Preview.",
"type": "str",
"default_value": "",
"enabled": "insert_frequency == 'once_only' and enabled"
},
"gcode_to_add":
{
"label": "G-code to insert",
"description": "G-code to add before or after layer change.",
"label": "G-code to insert.",
"description": "G-code to add at start of the layer. Use a comma to delimit multi-line commands. EX: G28 X Y,M220 S100,M117 HELL0. NOTE: All inserted text will be converted to upper-case as some firmwares don't understand lower-case.",
"type": "str",
"default_value": ""
},
"skip_layers":
{
"label": "Skip layers",
"description": "Number of layers to skip between insertions (0 for every layer).",
"type": "int",
"default_value": 0,
"minimum_value": 0
"default_value": "",
"enabled": "enabled"
}
}
}"""
def execute(self, data):
gcode_to_add = self.getSettingValueByKey("gcode_to_add") + "\n"
skip_layers = self.getSettingValueByKey("skip_layers")
count = 0
for layer in data:
# Check that a layer is being printed
lines = layer.split("\n")
for line in lines:
if ";LAYER:" in line:
index = data.index(layer)
if count == 0:
if self.getSettingValueByKey("insert_location") == "before":
layer = gcode_to_add + layer
else:
layer = layer + gcode_to_add
data[index] = layer
count = (count + 1) % (skip_layers + 1)
break
return data
# Exit if the script is not enabled
if not bool(self.getSettingValueByKey("enabled")):
return data
#Initialize variables
mycode = self.getSettingValueByKey("gcode_to_add").upper()
start_layer = int(self.getSettingValueByKey("start_layer"))
end_layer = int(self.getSettingValueByKey("end_layer"))
when_to_insert = self.getSettingValueByKey("insert_frequency")
end_list = [0]
print_sequence = Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value")
# Get the topmost layer number and adjust the end_list
if end_layer == -1:
if print_sequence == "all_at_once":
for lnum in range(0, len(data) - 1):
if ";LAYER:" in data[lnum]:
the_top = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
end_list[0] = the_top
# Get the topmost layer number for each model and append it to the end_list
if print_sequence == "one_at_a_time":
for lnum in range(0, 10):
if ";LAYER:0" in data[lnum]:
start_at = lnum + 1
break
for lnum in range(start_at, len(data)-1, 1):
if ";LAYER:" in data[lnum] and not ";LAYER:0" in data[lnum] and not ";LAYER:-" in data[lnum]:
end_list[len(end_list) - 1] = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
continue
if ";LAYER:0" in data[lnum]:
end_list.append(0)
elif end_layer != -1:
if print_sequence == "all_at_once":
# Catch an error if the entered End_Layer > the top layer in the gcode
for e_num, layer in enumerate(data):
if ";LAYER:" in layer:
top_layer = int(data[e_num].split(";LAYER:")[1].split("\n")[0])
end_list[0] = end_layer - 1
if top_layer < end_layer - 1:
end_list[0] = top_layer
elif print_sequence == "one_at_a_time":
# Find the index of the first Layer:0
for lnum in range(0, 10):
if ";LAYER:0" in data[lnum]:
start_at = lnum + 1
break
# Get the top layer number for each model
for lnum in range(start_at, len(data)-1):
if ";LAYER:" in data[lnum] and not ";LAYER:0" in data[lnum] and not ";LAYER:-" in data[lnum]:
end_list[len(end_list) - 1] = int(data[lnum].split(";LAYER:")[1].split("\n")[0])
if ";LAYER:0" in data[lnum]:
end_list.append(0)
# Adjust the end list if an end layer was named
for index, num in enumerate(end_list):
if num > end_layer - 1:
end_list[index] = end_layer - 1
#If the gcode_to_enter is multi-line then replace the commas with newline characters
if mycode != "":
if "," in mycode:
mycode = re.sub(",", "\n",mycode)
gcode_to_add = mycode
#Get the insertion frequency
match when_to_insert:
case "every_layer":
freq = 1
case "every_2nd":
freq = 2
case "every_3rd":
freq = 3
case "every_5th":
freq = 5
case "every_10th":
freq = 10
case "every_25th":
freq = 25
case "every_50th":
freq = 50
case "every_100th":
freq = 100
case "once_only":
the_insert_layer = int(self.getSettingValueByKey("single_end_layer"))-1
case _:
raise ValueError(f"Unexpected insertion frequency {when_to_insert}")
#Single insertion
if when_to_insert == "once_only":
# For print sequence 'All at once'
if print_sequence == "all_at_once":
for index, layer in enumerate(data):
if ";LAYER:" + str(the_insert_layer) + "\n" in layer:
lines = layer.split("\n")
lines.insert(1,gcode_to_add)
data[index] = "\n".join(lines)
return data
# For print sequence 'One at a time'
else:
for index, layer in enumerate(data):
if ";LAYER:" + str(the_insert_layer) + "\n" in layer:
lines = layer.split("\n")
lines.insert(1,gcode_to_add)
data[index] = "\n".join(lines)
return data
# For multiple insertions
if when_to_insert != "once_only":
# Search from the line after the first Layer:0 so we know when a model ends if in One at a Time mode.
first_0 = True
next_layer = start_layer - 1
end_layer = end_list.pop(0)
for index, layer in enumerate(data):
lines = layer.split("\n")
for l_index, line in enumerate(lines):
if ";LAYER:" in line:
layer_number = int(line.split(":")[1])
if layer_number == next_layer and layer_number <= end_layer:
lines.insert(l_index + 1,gcode_to_add)
data[index] = "\n".join(lines)
next_layer += freq
# Reset the next_layer for one-at-a-time
if next_layer > int(end_layer):
next_layer = start_layer - 1
# Index to the next end_layer when a Layer:0 is encountered
try:
if not first_0 and layer_number == 0:
end_layer = end_list.pop(0)
except:
pass
# Beyond the initial Layer:0 futher Layer:0's indicate the top layer of a model.
if layer_number == 0:
first_0 = False
break
return data

View file

@ -1,9 +1,15 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Created by Wayne Porter
# Modified 5/15/2023 - Greg Valiant (Greg Foresi)
# Created by Wayne Porter
# Added insertion frequency
# Adjusted for use with Relative Extrusion
# Changed Retract to a boolean and when true use the regular Cura retract settings.
# Use the regular Cura settings for Travel Speed and Speed_Z instead of asking.
# Added code to check the E location to prevent retracts if the filament was already retracted.
# Added 'Pause before image' per LemanRus
from ..Script import Script
from UM.Application import Application
from UM.Logger import Logger
class TimeLapse(Script):
def __init__(self):
@ -11,7 +17,7 @@ class TimeLapse(Script):
def getSettingDataString(self):
return """{
"name": "Time Lapse",
"name": "Time Lapse Camera",
"key": "TimeLapse",
"metadata": {},
"version": 2,
@ -19,24 +25,49 @@ class TimeLapse(Script):
{
"trigger_command":
{
"label": "Trigger camera command",
"description": "G-code command used to trigger camera.",
"label": "Camera Trigger Command",
"description": "G-code command used to trigger the camera. The setting box will take any command and parameters.",
"type": "str",
"default_value": "M240"
},
"insert_frequency":
{
"label": "How often (layers)",
"description": "Every so many layers (always starts at the first layer whether it's the model or a raft).",
"type": "enum",
"options": {
"every_layer": "Every Layer",
"every_2nd": "Every 2nd",
"every_3rd": "Every 3rd",
"every_5th": "Every 5th",
"every_10th": "Every 10th",
"every_25th": "Every 25th",
"every_50th": "Every 50th",
"every_100th": "Every 100th"},
"default_value": "every_layer"
},
"anti_shake_length":
{
"label": "Pause before image",
"description": "How long to wait (in ms) before capturing the image. This is to allow the printer to 'settle down' after movement. To disable set this to '0'.",
"type": "int",
"default_value": 0,
"minimum_value": 0,
"unit": "ms"
},
"pause_length":
{
"label": "Pause length",
"label": "Pause after image",
"description": "How long to wait (in ms) after camera was triggered.",
"type": "int",
"default_value": 700,
"default_value": 500,
"minimum_value": 0,
"unit": "ms"
},
"park_print_head":
{
"label": "Park Print Head",
"description": "Park the print head out of the way. Assumes absolute positioning.",
"description": "Park the print head out of the way.",
"type": "bool",
"default_value": true
},
@ -55,90 +86,166 @@ class TimeLapse(Script):
"description": "What Y location does the head move to for photo.",
"unit": "mm",
"type": "float",
"default_value": 190,
"enabled": "park_print_head"
},
"park_feed_rate":
{
"label": "Park Feed Rate",
"description": "How fast does the head move to the park coordinates.",
"unit": "mm/s",
"type": "float",
"default_value": 9000,
"default_value": 0,
"enabled": "park_print_head"
},
"retract":
{
"label": "Retraction Distance",
"description": "Filament retraction distance for camera trigger.",
"unit": "mm",
"type": "int",
"default_value": 0
"label": "Retract when required",
"description": "Retract if there isn't already a retraction. If unchecked then there will be no retraction even if there is none in the gcode. If retractions are not enabled in Cura there won't be a retraction. regardless of this setting.",
"type": "bool",
"default_value": true
},
"zhop":
{
"label": "Z-Hop Height When Parking",
"description": "Z-hop length before parking",
"description": "The height to lift the nozzle off the print before parking.",
"unit": "mm",
"type": "float",
"default_value": 0
"default_value": 2.0,
"minimum_value": 0.0
},
"ensure_final_image":
{
"label": "Ensure Final Image",
"description": "Depending on how the layer numbers work out with the 'How Often' frequency there might not be an image taken at the end of the last layer. This will ensure that one is taken. There is no parking as the Ending Gcode comes right up.",
"type": "bool",
"default_value": false
}
}
}"""
def execute(self, data):
feed_rate = self.getSettingValueByKey("park_feed_rate")
mycura = Application.getInstance().getGlobalContainerStack()
relative_extrusion = bool(mycura.getProperty("relative_extrusion", "value"))
extruder = mycura.extruderList
retract_speed = int(extruder[0].getProperty("retraction_speed", "value"))*60
retract_dist = round(float(extruder[0].getProperty("retraction_amount", "value")), 2)
retract_enabled = bool(extruder[0].getProperty("retraction_enable", "value"))
firmware_retract = bool(mycura.getProperty("machine_firmware_retract", "value"))
speed_z = int(extruder[0].getProperty("speed_z_hop", "value"))*60
if relative_extrusion:
rel_cmd = 83
else:
rel_cmd = 82
travel_speed = int(extruder[0].getProperty("speed_travel", "value"))*60
park_print_head = self.getSettingValueByKey("park_print_head")
x_park = self.getSettingValueByKey("head_park_x")
y_park = self.getSettingValueByKey("head_park_y")
trigger_command = self.getSettingValueByKey("trigger_command")
pause_length = self.getSettingValueByKey("pause_length")
retract = int(self.getSettingValueByKey("retract"))
retract = bool(self.getSettingValueByKey("retract"))
zhop = self.getSettingValueByKey("zhop")
gcode_to_append = ";TimeLapse Begin\n"
ensure_final_image = bool(self.getSettingValueByKey("ensure_final_image"))
when_to_insert = self.getSettingValueByKey("insert_frequency")
last_x = 0
last_y = 0
last_z = 0
last_e = 0
prev_e = 0
is_retracted = False
gcode_to_append = ""
if park_print_head:
gcode_to_append += self.putValue(G=1, F=feed_rate,
X=x_park, Y=y_park) + " ;Park print head\n"
gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
gcode_to_append += trigger_command + " ;Snap Photo\n"
gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
for idx, layer in enumerate(data):
for line in layer.split("\n"):
if self.getValue(line, "G") in {0, 1}: # Track X,Y,Z location.
last_x = self.getValue(line, "X", last_x)
last_y = self.getValue(line, "Y", last_y)
last_z = self.getValue(line, "Z", last_z)
# Check that a layer is being printed
lines = layer.split("\n")
for line in lines:
if ";LAYER:" in line:
if retract != 0: # Retract the filament so no stringing happens
layer += self.putValue(M=83) + " ;Extrude Relative\n"
layer += self.putValue(G=1, E=-retract, F=3000) + " ;Retract filament\n"
layer += self.putValue(M=82) + " ;Extrude Absolute\n"
layer += self.putValue(M=400) + " ;Wait for moves to finish\n" # Wait to fully retract before hopping
if zhop != 0:
layer += self.putValue(G=1, Z=last_z+zhop, F=3000) + " ;Z-Hop\n"
layer += gcode_to_append
if zhop != 0:
layer += self.putValue(G=0, X=last_x, Y=last_y, Z=last_z) + "; Restore position \n"
else:
layer += self.putValue(G=0, X=last_x, Y=last_y) + "; Restore position \n"
if retract != 0:
layer += self.putValue(M=400) + " ;Wait for moves to finish\n"
layer += self.putValue(M=83) + " ;Extrude Relative\n"
layer += self.putValue(G=1, E=retract, F=3000) + " ;Retract filament\n"
layer += self.putValue(M=82) + " ;Extrude Absolute\n"
data[idx] = layer
break
gcode_to_append += f"G0 F{travel_speed} X{x_park} Y{y_park} ;Park print head\n"
gcode_to_append += "M400 ;Wait for moves to finish\n"
anti_shake_length = self.getSettingValueByKey("anti_shake_length")
if anti_shake_length > 0:
gcode_to_append += f"G4 P{anti_shake_length} ;Wait for printer to settle down\n"
gcode_to_append += trigger_command + " ;Snap the Image\n"
gcode_to_append += f"G4 P{pause_length} ;Wait for camera to finish\n"
match when_to_insert:
case "every_layer":
step_freq = 1
case "every_2nd":
step_freq = 2
case "every_3rd":
step_freq = 3
case "every_5th":
step_freq = 5
case "every_10th":
step_freq = 10
case "every_25th":
step_freq = 25
case "every_50th":
step_freq = 50
case "every_100th":
step_freq = 100
case _:
step_freq = 1
# Use the step_freq to index through the layers----------------------------------------
for num in range(2,len(data)-1,step_freq):
layer = data[num]
try:
# Track X,Y,Z location.--------------------------------------------------------
for line in layer.split("\n"):
if self.getValue(line, "G") in {0, 1}:
last_x = self.getValue(line, "X", last_x)
last_y = self.getValue(line, "Y", last_y)
last_z = self.getValue(line, "Z", last_z)
#Track the E location so that if there is already a retraction we don't double dip.
if rel_cmd == 82:
if " E" in line:
last_e = line.split("E")[1]
if float(last_e) < float(prev_e):
is_retracted = True
else:
is_retracted = False
prev_e = last_e
elif rel_cmd == 83:
if " E" in line:
last_e = line.split("E")[1]
if float(last_e) < 0:
is_retracted = True
else:
is_retracted = False
prev_e = last_e
if firmware_retract and self.getValue(line, "G") in {10, 11}:
if self.getValue(line, "G") == 10:
is_retracted = True
last_e = float(prev_e) - float(retract_dist)
if self.getValue(line, "G") == 11:
is_retracted = False
last_e = float(prev_e) + float(retract_dist)
prev_e = last_e
lines = layer.split("\n")
# Insert the code----------------------------------------------------
camera_code = ""
for line in lines:
if ";LAYER:" in line:
if retract and not is_retracted and retract_enabled: # Retract unless already retracted
camera_code += ";TYPE:CUSTOM-----------------TimeLapse Begin\n"
camera_code += "M83 ;Extrude Relative\n"
if not firmware_retract:
camera_code += f"G1 F{retract_speed} E-{retract_dist} ;Retract filament\n"
else:
camera_code += "G10 ;Retract filament\n"
else:
camera_code += ";TYPE:CUSTOM-----------------TimeLapse Begin\n"
if zhop != 0:
camera_code += f"G1 F{speed_z} Z{round(last_z + zhop,2)} ;Z-Hop\n"
camera_code += gcode_to_append
camera_code += f"G0 F{travel_speed} X{last_x} Y{last_y} ;Restore XY position\n"
if zhop != 0:
camera_code += f"G0 F{speed_z} Z{last_z} ;Restore Z position\n"
if retract and not is_retracted and retract_enabled:
if not firmware_retract:
camera_code += f"G1 F{retract_speed} E{retract_dist} ;Un-Retract filament\n"
else:
camera_code += "G11 ;Un-Retract filament\n"
camera_code += f"M{rel_cmd} ;Extrude Mode\n"
camera_code += f";{'-' * 28}TimeLapse End"
# Format the camera code to be inserted
temp_lines = camera_code.split("\n")
for temp_index, temp_line in enumerate(temp_lines):
if ";" in temp_line and not temp_line.startswith(";"):
temp_lines[temp_index] = temp_line.replace(temp_line.split(";")[0], temp_line.split(";")[0] + str(" " * (29 - len(temp_line.split(";")[0]))),1)
temp_lines = "\n".join(temp_lines)
lines.insert(len(lines) - 2, temp_lines)
data[num] = "\n".join(lines)
break
except Exception as e:
Logger.log("w", "TimeLapse Error: " + repr(e))
# Take a final image if there was no camera shot at the end of the last layer.
if "TimeLapse Begin" not in data[len(data) - (3 if retract_enabled else 2)] and ensure_final_image:
data[len(data)-1] = "M400 ; Wait for all moves to finish\n" + trigger_command + " ;Snap the final Image\n" + f"G4 P{pause_length} ;Wait for camera\n" + data[len(data)-1]
return data

View file

@ -222,12 +222,11 @@ class SimulationView(CuraView):
self.setPath(i + fractional_value)
def advanceTime(self, time_increase: float) -> bool:
def advanceTime(self, time_increase: float) -> None:
"""
Advance the time by the given amount.
:param time_increase: The amount of time to advance (in seconds).
:return: True if the time was advanced, False if the end of the simulation was reached.
"""
total_duration = 0.0
if len(self.cumulativeLineDuration()) > 0:
@ -237,15 +236,13 @@ class SimulationView(CuraView):
# If we have reached the end of the simulation, go to the next layer.
if self.getCurrentLayer() == self.getMaxLayers():
# If we are already at the last layer, go to the first layer.
self.setTime(total_duration)
return False
# advance to the next layer, and reset the time
self.setLayer(self.getCurrentLayer() + 1)
self.setLayer(0)
else:
# advance to the next layer, and reset the time
self.setLayer(self.getCurrentLayer() + 1)
self.setTime(0.0)
else:
self.setTime(self._current_time + time_increase)
return True
def cumulativeLineDuration(self) -> List[float]:
# Make sure _cumulative_line_duration is initialized properly
@ -256,9 +253,19 @@ class SimulationView(CuraView):
polylines = self.getLayerData()
if polylines is not None:
for polyline in polylines.polygons:
for line_duration in list((polyline.lineLengths / polyline.lineFeedrates)[0]):
for line_index in range(len(polyline.lineLengths)):
line_length = polyline.lineLengths[line_index]
line_feedrate = polyline.lineFeedrates[line_index][0]
if line_feedrate > 0.0:
line_duration = line_length / line_feedrate
else:
# Something is wrong with this line, set an arbitrary non-null duration
line_duration = 0.1
total_duration += line_duration / SimulationView.SIMULATION_FACTOR
self._cumulative_line_duration.append(total_duration)
# for tool change we add an extra tool path
self._cumulative_line_duration.append(total_duration)
# set current cached layer
@ -583,7 +590,7 @@ class SimulationView(CuraView):
self._max_thickness = sys.float_info.min
self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min
self._cumulative_line_duration = {}
self._cumulative_line_duration = []
# The colour scheme is only influenced by the visible lines, so filter the lines by if they should be visible.
visible_line_types = []

View file

@ -144,9 +144,7 @@ Item
{
// divide by 1000 to account for ms to s conversion
const advance_time = simulationTimer.interval / 1000.0;
if (!UM.SimulationView.advanceTime(advance_time)) {
playButton.pauseSimulation();
}
UM.SimulationView.advanceTime(advance_time);
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
// correctly, because part of the logic is in this trigger function.
isSimulationPlaying = true;

View file

@ -54,9 +54,9 @@ class SimulationViewProxy(QObject):
def currentPath(self):
return self._simulation_view.getCurrentPath()
@pyqtSlot(float, result=bool)
def advanceTime(self, duration: float) -> bool:
return self._simulation_view.advanceTime(duration)
@pyqtSlot(float)
def advanceTime(self, duration: float) -> None:
self._simulation_view.advanceTime(duration)
@pyqtProperty(int, notify=currentPathChanged)
def minimumPath(self):

View file

@ -360,8 +360,8 @@ geometry41core =
((v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) ||
((v_prev_line_type[0] != 4) && (v_line_type[0] == 4))
)) {
float w = size_x;
float h = size_y;
float w = max(0.05, size_x);
float h = max(0.05, size_y);
myEmitVertex(v_vertex[0] + vec3( w, h, w), u_starts_color, normalize(vec3( 1.0, 1.0, 1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4( w, h, w, 0.0))); // Front-top-left
myEmitVertex(v_vertex[0] + vec3(-w, h, w), u_starts_color, normalize(vec3(-1.0, 1.0, 1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, h, w, 0.0))); // Front-top-right

View file

@ -1,11 +1,12 @@
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import datetime
import json
import os
import platform
import time
from typing import Optional, Set, TYPE_CHECKING
from typing import Any, Optional, Set, TYPE_CHECKING
from PyQt6.QtCore import pyqtSlot, QObject
from PyQt6.QtNetwork import QNetworkRequest
@ -33,7 +34,18 @@ class SliceInfo(QObject, Extension):
no model files are being sent (Just a SHA256 hash of the model).
"""
info_url = "https://stats.ultimaker.com/api/cura"
info_url = "https://statistics.ultimaker.com/api/v2/cura/slice"
_adjust_flattened_names = {
"extruders_extruder": "extruders",
"extruders_settings": "extruders",
"models_model": "models",
"models_transformation_data": "models_transformation",
"print_settings_": "",
"print_times": "print_time",
"active_machine_": "",
"slice_uuid": "slice_id",
}
def __init__(self, parent = None):
QObject.__init__(self, parent)
@ -112,6 +124,26 @@ class SliceInfo(QObject, Extension):
return list(sorted(user_modified_setting_keys))
def _flattenData(self, data: Any, result: dict, current_flat_key: Optional[str] = None, lift_list: bool = False) -> None:
if isinstance(data, dict):
for key, value in data.items():
total_flat_key = key if current_flat_key is None else f"{current_flat_key}_{key}"
self._flattenData(value, result, total_flat_key, lift_list)
elif isinstance(data, list):
for item in data:
self._flattenData(item, result, current_flat_key, True)
else:
actual_flat_key = current_flat_key.lower()
for key, value in self._adjust_flattened_names.items():
if actual_flat_key.startswith(key):
actual_flat_key = actual_flat_key.replace(key, value)
if lift_list:
if actual_flat_key not in result:
result[actual_flat_key] = []
result[actual_flat_key].append(data)
else:
result[actual_flat_key] = data
def _onWriteStarted(self, output_device):
try:
if not self._application.getPreferences().getValue("info/send_slice_info"):
@ -125,8 +157,7 @@ class SliceInfo(QObject, Extension):
global_stack = machine_manager.activeMachine
data = dict() # The data that we're going to submit.
data["time_stamp"] = time.time()
data["schema_version"] = 0
data["schema_version"] = 1000
data["cura_version"] = self._application.getVersion()
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
org_id = user_profile.get("organization_id", None) if user_profile else None
@ -298,6 +329,11 @@ class SliceInfo(QObject, Extension):
"time_backend": int(round(time_backend)),
}
# Massage data into format used in the DB:
flat_data = dict()
self._flattenData(data, flat_data)
data = flat_data
# Convert data to bytes
binary_data = json.dumps(data).encode("utf-8")

View file

@ -24,6 +24,7 @@ from UM.Settings.InstanceContainer import InstanceContainer
from cura.CuraApplication import CuraApplication
from cura.Settings.GlobalStack import GlobalStack
from cura.Utils.Threading import call_on_qt_thread
from cura.API import CuraAPI
from UM.i18n import i18nCatalog
@ -85,7 +86,8 @@ class UFPWriter(MeshWriter):
try:
archive.addContentType(extension="json", mime_type="application/json")
setting_textio = StringIO()
json.dump(self._getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
api = CuraApplication.getInstance().getCuraAPI()
json.dump(api.interface.settings.getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
steam = archive.getStream(SLICE_METADATA_PATH)
steam.write(setting_textio.getvalue().encode("UTF-8"))
except EnvironmentError as e:
@ -210,57 +212,3 @@ class UFPWriter(MeshWriter):
return [{"name": item.getName()}
for item in DepthFirstIterator(node)
if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")]
def _getSliceMetadata(self) -> Dict[str, Dict[str, Dict[str, str]]]:
"""Get all changed settings and all settings. For each extruder and the global stack"""
print_information = CuraApplication.getInstance().getPrintInformation()
machine_manager = CuraApplication.getInstance().getMachineManager()
settings = {
"material": {
"length": print_information.materialLengths,
"weight": print_information.materialWeights,
"cost": print_information.materialCosts,
},
"global": {
"changes": {},
"all_settings": {},
},
"quality": asdict(machine_manager.activeQualityDisplayNameMap()),
}
def _retrieveValue(container: InstanceContainer, setting_: str):
value_ = container.getProperty(setting_, "value")
for _ in range(0, 1024): # Prevent possibly endless loop by not using a limit.
if not isinstance(value_, SettingFunction):
return value_ # Success!
value_ = value_(container)
return 0 # Fallback value after breaking possibly endless loop.
global_stack = cast(GlobalStack, Application.getInstance().getGlobalContainerStack())
# Add global user or quality changes
global_flattened_changes = InstanceContainer.createMergedInstanceContainer(global_stack.userChanges, global_stack.qualityChanges)
for setting in global_flattened_changes.getAllKeys():
settings["global"]["changes"][setting] = _retrieveValue(global_flattened_changes, setting)
# Get global all settings values without user or quality changes
for setting in global_stack.getAllKeys():
settings["global"]["all_settings"][setting] = _retrieveValue(global_stack, setting)
for i, extruder in enumerate(global_stack.extruderList):
# Add extruder fields to settings dictionary
settings[f"extruder_{i}"] = {
"changes": {},
"all_settings": {},
}
# Add extruder user or quality changes
extruder_flattened_changes = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder.qualityChanges)
for setting in extruder_flattened_changes.getAllKeys():
settings[f"extruder_{i}"]["changes"][setting] = _retrieveValue(extruder_flattened_changes, setting)
# Get extruder all settings values without user or quality changes
for setting in extruder.getAllKeys():
settings[f"extruder_{i}"]["all_settings"][setting] = _retrieveValue(extruder, setting)
return settings

View file

@ -42,7 +42,7 @@ class CloudApiClient:
CLUSTER_API_ROOT = f"{ROOT_PATH}/connect/v1"
CURA_API_ROOT = f"{ROOT_PATH}/cura/v1"
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
DEFAULT_REQUEST_TIMEOUT = 30 # seconds
# In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]

View file

@ -331,7 +331,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return False
[printer, *_] = self._printers
return printer.type in ("MakerBot Method X", "MakerBot Method XL", "MakerBot Sketch")
return printer.type in ("MakerBot Method", "MakerBot Method X", "MakerBot Method XL", "MakerBot Sketch", "MakerBot Sketch Large", "MakerBot Sketch Sprint")
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
def supportsPrintJobActions(self) -> bool:

View file

@ -3,5 +3,7 @@
"ultimaker_methodx": "MakerBot Method X",
"ultimaker_methodxl": "MakerBot Method XL",
"ultimaker_factor4": "Ultimaker Factor 4",
"ultimaker_sketch": "MakerBot Sketch"
"ultimaker_sketch": "MakerBot Sketch",
"ultimaker_sketch_large": "MakerBot Sketch Large",
"ultimaker_sketch_sprint": "MakerBot Sketch Sprint"
}

View file

@ -97,6 +97,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
CuraApplication.getInstance().getPreferences().addPreference("usb_printing/enabled", False)
# This is a callback function that checks if there is any printing in progress via USB when the application tries
# to exit. If so, it will show a confirmation before
def _checkActivePrintingUponAppExit(self) -> None:
@ -144,6 +146,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
CuraApplication.getInstance().getPreferences().setValue("usb_printing/enabled", True)
#Find the g-code to print.
gcode_textio = StringIO()
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))

View file

@ -0,0 +1,103 @@
# Copyright (c) 2024 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
from typing import Dict, List, Tuple
import io
from UM.VersionUpgrade import VersionUpgrade
# Just to be sure, since in my testing there were both 0.1.0 and 0.2.0 settings about.
_PLUGIN_NAME = "_plugin__curaenginegradualflow"
_FROM_PLUGINS_SETTINGS = {
"gradual_flow_enabled",
"max_flow_acceleration",
"layer_0_max_flow_acceleration",
"gradual_flow_discretisation_step_size",
"reset_flow_duration",
} # type: Set[str]
_NEW_SETTING_VERSION = "24"
class VersionUpgrade58to59(VersionUpgrade):
def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades preferences to remove from the visibility list the settings that were removed in this version.
It also changes the preferences to have the new version number.
This removes any settings that were removed in the new Cura version.
:param serialized: The original contents of the preferences file.
:param filename: The file name of the preferences file.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
# Fix renamed settings for visibility
if "visible_settings" in parser["general"]:
all_setting_keys = parser["general"]["visible_settings"].strip().split(";")
if all_setting_keys:
for idx, key in enumerate(all_setting_keys):
if key.startswith(_PLUGIN_NAME):
all_setting_keys[idx] = key.split("__")[-1]
parser["general"]["visible_settings"] = ";".join(all_setting_keys)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades instance containers to remove the settings that were removed in this version.
It also changes the instance containers to have the new version number.
This removes any settings that were removed in the new Cura version and updates settings that need to be updated
with a new value.
:param serialized: The original contents of the instance container.
:param filename: The original file name of the instance container.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
# Rename settings.
if "values" in parser:
for key, value in parser["values"].items():
if key.startswith(_PLUGIN_NAME):
parser["values"][key.split("__")[-1]] = parser["values"][key]
del parser["values"][key]
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades stacks to have the new version number.
:param serialized: The original contents of the stack.
:param filename: The original file name of the stack.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,61 @@
# Copyright (c) 2024 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade58to59
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade58to59.VersionUpgrade58to59()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 7000023): ("preferences", 7000024, upgrade.upgradePreferences),
("machine_stack", 6000023): ("machine_stack", 6000024, upgrade.upgradeStack),
("extruder_train", 6000023): ("extruder_train", 6000024, upgrade.upgradeStack),
("definition_changes", 4000023): ("definition_changes", 4000024, upgrade.upgradeInstanceContainer),
("quality_changes", 4000023): ("quality_changes", 4000024, upgrade.upgradeInstanceContainer),
("quality", 4000023): ("quality", 4000024, upgrade.upgradeInstanceContainer),
("user", 4000023): ("user", 4000024, upgrade.upgradeInstanceContainer),
("intent", 4000023): ("intent", 4000024, upgrade.upgradeInstanceContainer),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 5.8 to 5.9",
"author": "UltiMaker",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 5.8 to Cura 5.9.",
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,68 @@
import configparser
import io
from typing import Dict, Tuple, List
from UM.VersionUpgrade import VersionUpgrade
_RENAMED_SETTINGS = {
"wall_overhang_speed_factor": "wall_overhang_speed_factors"
} # type: Dict[str, str]
_NEW_SETTING_VERSION = "25"
class VersionUpgrade59to510(VersionUpgrade):
def upgradePreferences(self, serialized: str, filename: str):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Fix 'renamed'(ish) settings for visibility
if "visible_settings" in parser["general"]:
all_setting_keys = parser["general"]["visible_settings"].strip().split(";")
if all_setting_keys:
for idx, key in enumerate(all_setting_keys):
if key in _RENAMED_SETTINGS:
all_setting_keys[idx] = _RENAMED_SETTINGS[key]
parser["general"]["visible_settings"] = ";".join(all_setting_keys)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
if "values" in parser:
for old_name, new_name in _RENAMED_SETTINGS.items():
if old_name in parser["values"]:
parser["values"][new_name] = parser["values"][old_name]
del parser["values"][old_name]
if "wall_overhang_speed_factors" in parser["values"]:
old_value = float(parser["values"]["wall_overhang_speed_factors"])
new_value = [max(1, int(round(old_value)))]
parser["values"]["wall_overhang_speed_factor"] = str(new_value)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = _NEW_SETTING_VERSION
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,60 @@
# Copyright (c) 2024 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade59to510
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade59to510.VersionUpgrade59to510()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 7000024): ("preferences", 7000025, upgrade.upgradePreferences),
("machine_stack", 6000024): ("machine_stack", 6000025, upgrade.upgradeStack),
("extruder_train", 6000024): ("extruder_train", 6000025, upgrade.upgradeStack),
("definition_changes", 4000024): ("definition_changes", 4000025, upgrade.upgradeInstanceContainer),
("quality_changes", 4000024): ("quality_changes", 4000025, upgrade.upgradeInstanceContainer),
("quality", 4000024): ("quality", 4000025, upgrade.upgradeInstanceContainer),
("user", 4000024): ("user", 4000025, upgrade.upgradeInstanceContainer),
("intent", 4000024): ("intent", 4000025, upgrade.upgradeInstanceContainer),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 5.9 to 5.10",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 5.9 to Cura 5.10",
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -11,12 +11,14 @@ import xml.etree.ElementTree as ET
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Logger import Logger
from UM.Decorators import CachedMemberFunctions
import UM.Dictionary
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.FormatMaps import FormatMaps
from cura.Machines.VariantType import VariantType
try:
@ -70,6 +72,8 @@ class XmlMaterialProfile(InstanceContainer):
Logger.log("w", "Can't change metadata {key} of material {material_id} because it's read-only.".format(key = key, material_id = self.getId()))
return
CachedMemberFunctions.clearInstanceCache(self)
# Some metadata such as diameter should also be instantiated to be a setting. Go though all values for the
# "properties" field and apply the new values to SettingInstances as well.
new_setting_values_dict = {}
@ -249,7 +253,7 @@ class XmlMaterialProfile(InstanceContainer):
machine_variant_map[definition_id][variant_name] = variant_dict
# Map machine human-readable names to IDs
product_id_map = self.getProductIdMap()
product_id_map = FormatMaps.getProductIdMap()
for definition_id, container in machine_container_map.items():
definition_id = container.getMetaDataEntry("definition")
@ -479,6 +483,7 @@ class XmlMaterialProfile(InstanceContainer):
first.append(element)
def clearData(self):
CachedMemberFunctions.clearInstanceCache(self)
self._metadata = {
"id": self.getId(),
"name": ""
@ -518,6 +523,8 @@ class XmlMaterialProfile(InstanceContainer):
def deserialize(self, serialized, file_name = None):
"""Overridden from InstanceContainer"""
CachedMemberFunctions.clearInstanceCache(self)
containers_to_add = []
# update the serialized data first
from UM.Settings.Interfaces import ContainerInterface
@ -647,7 +654,7 @@ class XmlMaterialProfile(InstanceContainer):
self._dirty = False
# Map machine human-readable names to IDs
product_id_map = self.getProductIdMap()
product_id_map = FormatMaps.getProductIdMap()
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
for machine in machines:
@ -911,9 +918,6 @@ class XmlMaterialProfile(InstanceContainer):
base_metadata["properties"] = property_values
base_metadata["definition"] = "fdmprinter"
# Certain materials are loaded but should not be visible / selectable to the user.
base_metadata["visible"] = not base_metadata.get("abstract_color", False)
compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces)
try:
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text) # type: ignore
@ -923,7 +927,7 @@ class XmlMaterialProfile(InstanceContainer):
result_metadata.append(base_metadata)
# Map machine human-readable names to IDs
product_id_map = cls.getProductIdMap()
product_id_map = FormatMaps.getProductIdMap()
for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces):
machine_compatibility = common_compatibility
@ -1127,29 +1131,6 @@ class XmlMaterialProfile(InstanceContainer):
id_list = list(id_list)
return id_list
__product_to_id_map: Optional[Dict[str, List[str]]] = None
@classmethod
def getProductIdMap(cls) -> Dict[str, List[str]]:
"""Gets a mapping from product names in the XML files to their definition IDs.
This loads the mapping from a file.
"""
if cls.__product_to_id_map is not None:
return cls.__product_to_id_map
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("XmlMaterialProfile"))
product_to_id_file = os.path.join(plugin_path, "product_to_id.json")
with open(product_to_id_file, encoding = "utf-8") as f:
contents = ""
for line in f:
contents += line if "#" not in line else "".join([line.replace("#", str(n)) for n in range(1, 12)])
cls.__product_to_id_map = json.loads(contents)
cls.__product_to_id_map = {key: [value] for key, value in cls.__product_to_id_map.items()}
#This also loads "Ultimaker S5" -> "ultimaker_s5" even though that is not strictly necessary with the default to change spaces into underscores.
#However it is not always loaded with that default; this mapping is also used in serialize() without that default.
return cls.__product_to_id_map
@staticmethod
def _parseCompatibleValue(value: str):
"""Parse the value of the "material compatible" property."""

View file

@ -1,23 +0,0 @@
{
"Ultimaker #": "ultimaker#",
"Ultimaker # Extended": "ultimaker#_extended",
"Ultimaker # Extended+": "ultimaker#_extended_plus",
"Ultimaker # Go": "ultimaker#_go",
"Ultimaker #+": "ultimaker#_plus",
"Ultimaker #+ Connect": "ultimaker#_plus_connect",
"Ultimaker S#": "ultimaker_s#",
"Ultimaker Factor #": "ultimaker_factor#",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus",
"Ultimaker Original Dual Extrusion": "ultimaker_original_dual",
"IMADE3D JellyBOX": "imade3d_jellybox",
"DUAL600": "strateo3d",
"IDEX420": "strateo3d_IDEX420",
"IDEX420 Duplicate": "strateo3d_IDEX420_duplicate",
"IDEX420 Mirror": "strateo3d_IDEX420_mirror",
"UltiMaker Method": "ultimaker_method",
"UltiMaker Method X": "ultimaker_methodx",
"UltiMaker Method XL": "ultimaker_methodxl",
"UltiMaker Sketch": "ultimaker_sketch",
"UltiMaker Sketch Large": "ultimaker_sketch_large"
}