mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-23 14:44:13 -06:00
Merge branch 'main' into DisplayInfoOnLCD
This commit is contained in:
commit
ad9b11a256
2267 changed files with 57635 additions and 22915 deletions
|
@ -56,7 +56,8 @@ class ThreeMFReader(MeshReader):
|
|||
def emptyFileHintSet(self) -> bool:
|
||||
return self._empty_project
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
||||
@staticmethod
|
||||
def _createMatrixFromTransformationString(transformation: str) -> Matrix:
|
||||
if transformation == "":
|
||||
return Matrix()
|
||||
|
||||
|
@ -90,7 +91,8 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
return temp_mat
|
||||
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
@staticmethod
|
||||
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
|
||||
:returns: Scene node.
|
||||
|
@ -119,7 +121,7 @@ class ThreeMFReader(MeshReader):
|
|||
pass
|
||||
um_node.setName(node_name)
|
||||
um_node.setId(node_id)
|
||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||
transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||
um_node.setTransformation(transformation)
|
||||
mesh_builder = MeshBuilder()
|
||||
|
||||
|
@ -138,7 +140,7 @@ class ThreeMFReader(MeshReader):
|
|||
um_node.setMeshData(mesh_data)
|
||||
|
||||
for child in savitar_node.getChildren():
|
||||
child_node = self._convertSavitarNodeToUMNode(child)
|
||||
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
|
||||
if child_node:
|
||||
um_node.addChild(child_node)
|
||||
|
||||
|
@ -184,6 +186,13 @@ class ThreeMFReader(MeshReader):
|
|||
if len(um_node.getAllChildren()) == 1:
|
||||
# We don't want groups of one, so move the node up one "level"
|
||||
child_node = um_node.getChildren()[0]
|
||||
# Move all the meshes of children so that toolhandles are shown in the correct place.
|
||||
if child_node.getMeshData():
|
||||
extents = child_node.getMeshData().getExtents()
|
||||
move_matrix = Matrix()
|
||||
move_matrix.translate(-extents.center)
|
||||
child_node.setMeshData(child_node.getMeshData().getTransformed(move_matrix))
|
||||
child_node.translate(extents.center)
|
||||
parent_transformation = um_node.getLocalTransformation()
|
||||
child_transformation = child_node.getLocalTransformation()
|
||||
child_node.setTransformation(parent_transformation.multiply(child_transformation))
|
||||
|
@ -214,7 +223,7 @@ class ThreeMFReader(MeshReader):
|
|||
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
||||
|
||||
for node in scene_3mf.getSceneNodes():
|
||||
um_node = self._convertSavitarNodeToUMNode(node, file_name)
|
||||
um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name)
|
||||
if um_node is None:
|
||||
continue
|
||||
|
||||
|
@ -300,8 +309,23 @@ class ThreeMFReader(MeshReader):
|
|||
if unit is None:
|
||||
unit = "millimeter"
|
||||
elif unit not in conversion_to_mm:
|
||||
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
|
||||
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit=unit))
|
||||
unit = "millimeter"
|
||||
|
||||
scale = conversion_to_mm[unit]
|
||||
return Vector(scale, scale, scale)
|
||||
|
||||
@staticmethod
|
||||
def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
|
||||
parser = Savitar.ThreeMFParser()
|
||||
scene = parser.parse(scene_string)
|
||||
|
||||
# Convert the scene to scene nodes
|
||||
nodes = []
|
||||
for savitar_node in scene.getSceneNodes():
|
||||
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
|
||||
if scene_node is None:
|
||||
continue
|
||||
nodes.append(scene_node)
|
||||
|
||||
return nodes
|
||||
|
|
|
@ -606,7 +606,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._dialog.setNumVisibleSettings(num_visible_settings)
|
||||
self._dialog.setQualityName(quality_name)
|
||||
self._dialog.setQualityType(quality_type)
|
||||
self._dialog.setIntentName(intent_name)
|
||||
self._dialog.setIntentName(intent_category)
|
||||
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
|
||||
self._dialog.setNumUserSettings(num_user_settings)
|
||||
self._dialog.setActiveMode(active_mode)
|
||||
|
@ -1095,7 +1095,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if global_stack.getProperty(key, "settable_per_extruder"):
|
||||
values_to_set_for_extruders[key] = value
|
||||
else:
|
||||
global_stack.definitionChanges.setProperty(key, "value", value)
|
||||
if not self._settingIsFromMissingPackage(key, value):
|
||||
global_stack.definitionChanges.setProperty(key, "value", value)
|
||||
|
||||
for position, extruder_stack in extruder_stack_dict.items():
|
||||
if position not in self._machine_info.extruder_info_dict:
|
||||
|
@ -1109,7 +1110,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
extruder_stack.definitionChanges.setProperty(key, "value", value)
|
||||
if parser is not None:
|
||||
for key, value in parser["values"].items():
|
||||
extruder_stack.definitionChanges.setProperty(key, "value", value)
|
||||
if not self._settingIsFromMissingPackage(key, value):
|
||||
extruder_stack.definitionChanges.setProperty(key, "value", value)
|
||||
|
||||
def _applyUserChanges(self, global_stack, extruder_stack_dict):
|
||||
values_to_set_for_extruder_0 = {}
|
||||
|
@ -1119,7 +1121,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if global_stack.getProperty(key, "settable_per_extruder"):
|
||||
values_to_set_for_extruder_0[key] = value
|
||||
else:
|
||||
global_stack.userChanges.setProperty(key, "value", value)
|
||||
if not self._settingIsFromMissingPackage(key, value):
|
||||
global_stack.userChanges.setProperty(key, "value", value)
|
||||
|
||||
for position, extruder_stack in extruder_stack_dict.items():
|
||||
if position not in self._machine_info.extruder_info_dict:
|
||||
|
@ -1133,7 +1136,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
extruder_stack.userChanges.setProperty(key, "value", value)
|
||||
if parser is not None:
|
||||
for key, value in parser["values"].items():
|
||||
extruder_stack.userChanges.setProperty(key, "value", value)
|
||||
if not self._settingIsFromMissingPackage(key, value):
|
||||
extruder_stack.userChanges.setProperty(key, "value", value)
|
||||
|
||||
def _applyVariants(self, global_stack, extruder_stack_dict):
|
||||
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
||||
|
@ -1208,6 +1212,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if key not in _ignored_machine_network_metadata:
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
|
||||
def _settingIsFromMissingPackage(self, key, value):
|
||||
# Check if the key and value pair is from the missing package
|
||||
for package in self._dialog.missingPackages:
|
||||
if value.startswith("PLUGIN::"):
|
||||
if (package['id'] + "@" + package['package_version']) in value:
|
||||
Logger.log("w", f"Ignoring {key} value {value} from missing package")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _updateActiveMachine(self, global_stack):
|
||||
# Actually change the active machine.
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
|
@ -1246,7 +1259,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
|
||||
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
|
||||
machine_manager.setIntentByCategory(self._intent_category_to_apply)
|
||||
|
||||
else:
|
||||
# if no intent is provided, reset to the default (balanced) intent
|
||||
machine_manager.resetIntents()
|
||||
# Notify everything/one that is to notify about changes.
|
||||
global_stack.containersChanged.emit(global_stack.getTop())
|
||||
|
||||
|
@ -1327,3 +1342,4 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
missing_packages.append(package)
|
||||
|
||||
return missing_packages
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from PyQt6.QtGui import QDesktopServices
|
|||
from typing import List, Optional, Dict, cast
|
||||
|
||||
from cura.Machines.Models.MachineListModel import MachineListModel
|
||||
from cura.Machines.Models.IntentTranslations import intent_translations
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from UM.Application import Application
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
@ -35,10 +36,12 @@ class WorkspaceDialog(QObject):
|
|||
self._qml_url = "WorkspaceDialog.qml"
|
||||
self._lock = threading.Lock()
|
||||
self._default_strategy = None
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._result = {
|
||||
"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy,
|
||||
}
|
||||
self._override_machine = None
|
||||
self._visible = False
|
||||
self.showDialogSignal.connect(self.__show)
|
||||
|
@ -221,7 +224,14 @@ class WorkspaceDialog(QObject):
|
|||
|
||||
def setIntentName(self, intent_name: str) -> None:
|
||||
if self._intent_name != intent_name:
|
||||
self._intent_name = intent_name
|
||||
try:
|
||||
self._intent_name = intent_translations[intent_name]["name"]
|
||||
except:
|
||||
self._intent_name = intent_name.title()
|
||||
self.intentNameChanged.emit()
|
||||
|
||||
if not self._intent_name:
|
||||
self._intent_name = intent_translations["default"]["name"]
|
||||
self.intentNameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=activeModeChanged)
|
||||
|
@ -347,10 +357,12 @@ class WorkspaceDialog(QObject):
|
|||
if threading.current_thread() != threading.main_thread():
|
||||
self._lock.acquire()
|
||||
# Reset the result
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._result = {
|
||||
"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy,
|
||||
}
|
||||
self._visible = True
|
||||
self.showDialogSignal.emit()
|
||||
|
||||
|
@ -408,26 +420,27 @@ class WorkspaceDialog(QObject):
|
|||
@pyqtSlot()
|
||||
def showMissingMaterialsWarning(self) -> None:
|
||||
result_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
|
||||
i18n_catalog.i18nc("@info:status",
|
||||
"Some of the packages used in the project file are currently not installed in Cura, this might produce undesirable print results. We highly recommend installing the all required packages from the Marketplace."),
|
||||
lifetime=0,
|
||||
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
|
||||
title=i18n_catalog.i18nc("@info:title", "Some required packages are not installed"),
|
||||
message_type=Message.MessageType.WARNING
|
||||
)
|
||||
result_message.addAction(
|
||||
"learn_more",
|
||||
name=i18n_catalog.i18nc("@action:button", "Learn more"),
|
||||
icon="",
|
||||
description="Learn more about project materials.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style=Message.ActionButtonStyle.LINK
|
||||
"learn_more",
|
||||
name=i18n_catalog.i18nc("@action:button", "Learn more"),
|
||||
icon="",
|
||||
description=i18n_catalog.i18nc("@label", "Learn more about project packages."),
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style=Message.ActionButtonStyle.LINK
|
||||
)
|
||||
result_message.addAction(
|
||||
"install_materials",
|
||||
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
|
||||
icon="",
|
||||
description="Install missing materials from project file.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
|
||||
button_style=Message.ActionButtonStyle.DEFAULT
|
||||
"install_packages",
|
||||
name=i18n_catalog.i18nc("@action:button", "Install Packages"),
|
||||
icon="",
|
||||
description=i18n_catalog.i18nc("@label", "Install missing packages from project file."),
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
|
||||
button_style=Message.ActionButtonStyle.DEFAULT
|
||||
)
|
||||
result_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||
result_message.show()
|
||||
|
|
|
@ -364,7 +364,7 @@ UM.Dialog
|
|||
UM.Label
|
||||
{
|
||||
id: warningText
|
||||
text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project.")
|
||||
text: catalog.i18nc("@label", "This project contains materials or plugins that are currently not installed in Cura.<br/>Install the missing packages and reopen the project.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,7 +404,7 @@ UM.Dialog
|
|||
Cura.PrimaryButton
|
||||
{
|
||||
visible: warning
|
||||
text: catalog.i18nc("@action:button", "Install missing material")
|
||||
text: catalog.i18nc("@action:button", "Install missing packages")
|
||||
onClicked: manager.installMissingPackages()
|
||||
}
|
||||
]
|
||||
|
|
|
@ -40,7 +40,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
|
||||
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
||||
mesh_writer.setStoreArchive(True)
|
||||
mesh_writer.write(stream, nodes, mode)
|
||||
if not mesh_writer.write(stream, nodes, mode):
|
||||
self.setInformation(mesh_writer.getInformation())
|
||||
return False
|
||||
|
||||
archive = mesh_writer.getArchive()
|
||||
if archive is None: # This happens if there was no mesh data to write.
|
||||
|
@ -98,7 +100,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
Logger.error("No permission to write workspace to this stream.")
|
||||
return False
|
||||
except EnvironmentError as e:
|
||||
self.setInformation(catalog.i18nc("@error:zip", "The operating system does not allow saving a project file to this location or with this file name."))
|
||||
self.setInformation(catalog.i18nc("@error:zip", str(e)))
|
||||
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
|
||||
return False
|
||||
mesh_writer.setStoreArchive(False)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import re
|
||||
|
||||
from typing import Optional, cast, List, Dict
|
||||
from typing import Optional, cast, List, Dict, Pattern, Set
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -17,6 +18,7 @@ from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
|||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
from cura.Settings import CuraContainerStack
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from cura.Snapshot import Snapshot
|
||||
|
||||
|
@ -55,11 +57,12 @@ class ThreeMFWriter(MeshWriter):
|
|||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
||||
}
|
||||
|
||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||
self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
|
||||
self._archive: Optional[zipfile.ZipFile] = None
|
||||
self._store_archive = False
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
@staticmethod
|
||||
def _convertMatrixToString(matrix):
|
||||
result = ""
|
||||
result += str(matrix._data[0, 0]) + " "
|
||||
result += str(matrix._data[1, 0]) + " "
|
||||
|
@ -83,7 +86,8 @@ class ThreeMFWriter(MeshWriter):
|
|||
"""
|
||||
self._store_archive = store_archive
|
||||
|
||||
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||
@staticmethod
|
||||
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
|
||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
|
||||
:returns: Uranium Scene node.
|
||||
|
@ -98,12 +102,20 @@ class ThreeMFWriter(MeshWriter):
|
|||
savitar_node = Savitar.SceneNode()
|
||||
savitar_node.setName(um_node.getName())
|
||||
|
||||
node_matrix = um_node.getLocalTransformation()
|
||||
node_matrix = Matrix()
|
||||
mesh_data = um_node.getMeshData()
|
||||
# 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.z, extents.center.y)
|
||||
node_matrix.setByTranslation(center_vector)
|
||||
node_matrix.multiply(um_node.getLocalTransformation())
|
||||
|
||||
matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
|
||||
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
|
||||
|
||||
savitar_node.setTransformation(matrix_string)
|
||||
mesh_data = um_node.getMeshData()
|
||||
if mesh_data is not None:
|
||||
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
|
||||
indices_array = mesh_data.getIndicesAsByteArray()
|
||||
|
@ -133,7 +145,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
# only save the nodes on the active build plate
|
||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||
continue
|
||||
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
|
||||
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
|
||||
if savitar_child_node is not None:
|
||||
savitar_node.addChild(savitar_child_node)
|
||||
|
||||
|
@ -175,13 +187,15 @@ class ThreeMFWriter(MeshWriter):
|
|||
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
||||
|
||||
# Add PNG to content types file
|
||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
|
||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
||||
# Add thumbnail relation to _rels/.rels file
|
||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship",
|
||||
Target="/" + THUMBNAIL_PATH, Id="rel1",
|
||||
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||
|
||||
# Write material metadata
|
||||
material_metadata = self._getMaterialPackageMetadata()
|
||||
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
|
||||
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
||||
self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
|
||||
|
||||
savitar_scene = Savitar.Scene()
|
||||
|
||||
|
@ -221,7 +235,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
for node in nodes:
|
||||
if node == root_node:
|
||||
for root_child in node.getChildren():
|
||||
savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
|
||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
|
||||
if savitar_node:
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
else:
|
||||
|
@ -235,9 +249,9 @@ class ThreeMFWriter(MeshWriter):
|
|||
archive.writestr(model_file, scene_string)
|
||||
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
|
||||
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
||||
except Exception as e:
|
||||
except Exception as error:
|
||||
Logger.logException("e", "Error writing zip file")
|
||||
self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
|
||||
self.setInformation(str(error))
|
||||
return False
|
||||
finally:
|
||||
if not self._store_archive:
|
||||
|
@ -253,7 +267,64 @@ class ThreeMFWriter(MeshWriter):
|
|||
metadata_file = zipfile.ZipInfo(path)
|
||||
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||
metadata_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
|
||||
archive.writestr(metadata_file,
|
||||
json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
|
||||
|
||||
@staticmethod
|
||||
def _getPluginPackageMetadata() -> List[Dict[str, str]]:
|
||||
"""Get metadata for all backend plugins that are used in the project.
|
||||
|
||||
:return: List of material metadata dictionaries.
|
||||
"""
|
||||
|
||||
backend_plugin_enum_value_regex = re.compile(
|
||||
r"PLUGIN::(?P<plugin_id>\w+)@(?P<version>\d+.\d+.\d+)::(?P<value>\w+)")
|
||||
# This regex parses enum values to find if they contain custom
|
||||
# backend engine values. These custom enum values are in the format
|
||||
# PLUGIN::<plugin_id>@<version>::<value>
|
||||
# where
|
||||
# - plugin_id is the id of the plugin
|
||||
# - version is in the semver format
|
||||
# - value is the value of the enum
|
||||
|
||||
plugin_ids = set()
|
||||
|
||||
def addPluginIdsInStack(stack: CuraContainerStack) -> None:
|
||||
for key in stack.getAllKeys():
|
||||
value = str(stack.getProperty(key, "value"))
|
||||
for plugin_id, _version, _value in backend_plugin_enum_value_regex.findall(value):
|
||||
plugin_ids.add(plugin_id)
|
||||
|
||||
# Go through all stacks and find all the plugin id contained in the project
|
||||
global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
|
||||
addPluginIdsInStack(global_stack)
|
||||
|
||||
for container in global_stack.getContainers():
|
||||
addPluginIdsInStack(container)
|
||||
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
addPluginIdsInStack(extruder_stack)
|
||||
|
||||
for container in extruder_stack.getContainers():
|
||||
addPluginIdsInStack(container)
|
||||
|
||||
metadata = {}
|
||||
|
||||
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
for plugin_id in plugin_ids:
|
||||
package_data = package_manager.getInstalledPackageInfo(plugin_id)
|
||||
|
||||
metadata[plugin_id] = {
|
||||
"id": plugin_id,
|
||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
|
||||
"sdk_version_semver") else "",
|
||||
"type": "plugin",
|
||||
}
|
||||
|
||||
# Storing in a dict and fetching values to avoid duplicates
|
||||
return list(metadata.values())
|
||||
|
||||
@staticmethod
|
||||
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
|
||||
|
@ -278,7 +349,8 @@ class ThreeMFWriter(MeshWriter):
|
|||
# Don't export bundled materials
|
||||
continue
|
||||
|
||||
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
|
||||
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(),
|
||||
extruder.material.getMetaDataEntry("GUID"))
|
||||
package_data = package_manager.getInstalledPackageInfo(package_id)
|
||||
|
||||
# We failed to find the package for this material
|
||||
|
@ -286,10 +358,14 @@ class ThreeMFWriter(MeshWriter):
|
|||
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
|
||||
continue
|
||||
|
||||
material_metadata = {"id": package_id,
|
||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
|
||||
material_metadata = {
|
||||
"id": package_id,
|
||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
|
||||
"sdk_version_semver") else "",
|
||||
"type": "material",
|
||||
}
|
||||
|
||||
metadata[package_id] = material_metadata
|
||||
|
||||
|
@ -303,9 +379,19 @@ class ThreeMFWriter(MeshWriter):
|
|||
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
||||
return None
|
||||
try:
|
||||
snapshot = Snapshot.snapshot(width = 300, height = 300)
|
||||
snapshot = Snapshot.snapshot(width=300, height=300)
|
||||
except:
|
||||
Logger.logException("w", "Failed to create snapshot image")
|
||||
return None
|
||||
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
|
||||
savitar_scene = Savitar.Scene()
|
||||
for scene_node in scene_nodes:
|
||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node)
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
parser = Savitar.ThreeMFParser()
|
||||
scene_string = parser.sceneToString(savitar_scene)
|
||||
return scene_string
|
||||
|
|
60
plugins/3MFWriter/tests/TestMFWriter.py
Normal file
60
plugins/3MFWriter/tests/TestMFWriter.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import sys
|
||||
import os.path
|
||||
from typing import Dict, Optional
|
||||
import pytest
|
||||
|
||||
from unittest.mock import patch, MagicMock, PropertyMock
|
||||
|
||||
from UM.PackageManager import PackageManager
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
import ThreeMFWriter
|
||||
|
||||
PLUGIN_ID = "my_plugin"
|
||||
DISPLAY_NAME = "MyPlugin"
|
||||
PACKAGE_VERSION = "0.0.1"
|
||||
SDK_VERSION = "8.0.0"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_manager() -> MagicMock:
|
||||
pm = MagicMock(spec=PackageManager)
|
||||
pm.getInstalledPackageInfo.return_value = {
|
||||
"display_name": DISPLAY_NAME,
|
||||
"package_version": PACKAGE_VERSION,
|
||||
"sdk_version_semver": SDK_VERSION
|
||||
}
|
||||
return pm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def machine_manager() -> MagicMock:
|
||||
mm = MagicMock(spec=PackageManager)
|
||||
active_machine = MagicMock()
|
||||
active_machine.getAllKeys.return_value = ["infill_pattern", "layer_height", "material_bed_temperature"]
|
||||
active_machine.getProperty.return_value = f"PLUGIN::{PLUGIN_ID}@{PACKAGE_VERSION}::custom_value"
|
||||
active_machine.getContainers.return_value = []
|
||||
active_machine.extruderList = []
|
||||
mm.activeMachine = active_machine
|
||||
return mm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(package_manager, machine_manager):
|
||||
app = MagicMock()
|
||||
app.getPackageManager.return_value = package_manager
|
||||
app.getMachineManager.return_value = machine_manager
|
||||
return app
|
||||
|
||||
|
||||
def test_enumParsing(application):
|
||||
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
|
||||
packages_metadata = ThreeMFWriter.ThreeMFWriter._getPluginPackageMetadata()[0]
|
||||
|
||||
assert packages_metadata.get("id") == PLUGIN_ID
|
||||
assert packages_metadata.get("display_name") == DISPLAY_NAME
|
||||
assert packages_metadata.get("package_version") == PACKAGE_VERSION
|
||||
assert packages_metadata.get("sdk_version_semver") == SDK_VERSION
|
||||
assert packages_metadata.get("type") == "plugin"
|
|
@ -8,12 +8,31 @@ message ObjectList
|
|||
repeated Setting settings = 2; // meshgroup settings (for one-at-a-time printing)
|
||||
}
|
||||
|
||||
enum SlotID {
|
||||
SETTINGS_BROADCAST = 0;
|
||||
SIMPLIFY_MODIFY = 100;
|
||||
POSTPROCESS_MODIFY = 101;
|
||||
INFILL_MODIFY = 102;
|
||||
GCODE_PATHS_MODIFY = 103;
|
||||
INFILL_GENERATE = 200;
|
||||
}
|
||||
|
||||
message EnginePlugin
|
||||
{
|
||||
SlotID id = 1;
|
||||
string address = 2;
|
||||
uint32 port = 3;
|
||||
string plugin_name = 4;
|
||||
string plugin_version = 5;
|
||||
}
|
||||
|
||||
message Slice
|
||||
{
|
||||
repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another
|
||||
SettingList global_settings = 2; // The global settings used for the whole print job
|
||||
repeated Extruder extruders = 3; // The settings sent to each extruder object
|
||||
repeated SettingExtruder limit_to_extruder = 4; // From which stack the setting would inherit if not defined per object
|
||||
repeated EnginePlugin engine_plugins = 5;
|
||||
}
|
||||
|
||||
message Extruder
|
||||
|
|
|
@ -46,6 +46,19 @@ catalog = i18nCatalog("cura")
|
|||
class CuraEngineBackend(QObject, Backend):
|
||||
backendError = Signal()
|
||||
|
||||
printDurationMessage = Signal()
|
||||
"""Emitted when we get a message containing print duration and material amount.
|
||||
|
||||
This also implies the slicing has finished.
|
||||
:param time: The amount of time the print will take.
|
||||
:param material_amount: The amount of material the print will use.
|
||||
"""
|
||||
slicingStarted = Signal()
|
||||
"""Emitted when the slicing process starts."""
|
||||
|
||||
slicingCancelled = Signal()
|
||||
"""Emitted when the slicing process is aborted forcefully."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Starts the back-end plug-in.
|
||||
|
||||
|
@ -70,7 +83,6 @@ class CuraEngineBackend(QObject, Backend):
|
|||
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
|
||||
os.path.dirname(os.path.abspath(sys.executable)),
|
||||
]
|
||||
|
||||
for path in search_path:
|
||||
engine_path = os.path.join(path, executable_name)
|
||||
if os.path.isfile(engine_path):
|
||||
|
@ -86,9 +98,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._default_engine_location = execpath
|
||||
break
|
||||
|
||||
application = CuraApplication.getInstance() #type: CuraApplication
|
||||
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
||||
application: CuraApplication = CuraApplication.getInstance()
|
||||
self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None
|
||||
self._machine_error_checker: Optional[MachineErrorChecker] = None
|
||||
|
||||
if not self._default_engine_location:
|
||||
raise EnvironmentError("Could not find CuraEngine")
|
||||
|
@ -99,13 +111,15 @@ class CuraEngineBackend(QObject, Backend):
|
|||
application.getPreferences().addPreference("backend/location", self._default_engine_location)
|
||||
|
||||
# Workaround to disable layer view processing if layer view is not active.
|
||||
self._layer_view_active = False #type: bool
|
||||
self._layer_view_active: bool = False
|
||||
self._onActiveViewChanged()
|
||||
|
||||
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
|
||||
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||
self._stored_layer_data: List[Arcus.PythonMessage] = []
|
||||
|
||||
self._scene = application.getController().getScene() #type: Scene
|
||||
# key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||
self._stored_optimized_layer_data: Dict[int, List[Arcus.PythonMessage]] = {}
|
||||
|
||||
self._scene: Scene = application.getController().getScene()
|
||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||
|
||||
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
||||
|
@ -116,7 +130,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
|
||||
# to start the auto-slicing timer again.
|
||||
#
|
||||
self._global_container_stack = None #type: Optional[ContainerStack]
|
||||
self._global_container_stack: Optional[ContainerStack] = None
|
||||
|
||||
# Listeners for receiving messages from the back-end.
|
||||
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
||||
|
@ -128,31 +142,34 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
||||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||
|
||||
self._start_slice_job = None #type: Optional[StartSliceJob]
|
||||
self._start_slice_job_build_plate = None #type: Optional[int]
|
||||
self._slicing = False #type: bool # Are we currently slicing?
|
||||
self._restart = False #type: bool # Back-end is currently restarting?
|
||||
self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything
|
||||
self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
||||
self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers.
|
||||
self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing?
|
||||
self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not?
|
||||
self._start_slice_job: Optional[StartSliceJob] = None
|
||||
self._start_slice_job_build_plate: Optional[int] = None
|
||||
self._slicing: bool = False # Are we currently slicing?
|
||||
self._restart: bool = False # Back-end is currently restarting?
|
||||
self._tool_active: bool = False # If a tool is active, some tasks do not have to do anything
|
||||
self._always_restart: bool = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
||||
self._process_layers_job: Optional[ProcessSlicedLayersJob] = None # The currently active job to process layers, or None if it is not processing layers.
|
||||
self._build_plates_to_be_sliced: List[int] = [] # what needs slicing?
|
||||
self._engine_is_fresh: bool = True # Is the newly started engine used before or not?
|
||||
|
||||
self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer
|
||||
self._error_message = None #type: Optional[Message] # Pop-up message that shows errors.
|
||||
self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed
|
||||
self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool)
|
||||
self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer
|
||||
self._error_message: Optional[Message] = None # Pop-up message that shows errors.
|
||||
|
||||
self._time_start_process = None #type: Optional[float]
|
||||
self._is_disabled = False #type: bool
|
||||
# Count number of objects to see if there is something changed
|
||||
self._last_num_objects: Dict[int, int] = defaultdict(int)
|
||||
self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool)
|
||||
|
||||
self._time_start_process: Optional[float] = None
|
||||
self._is_disabled: bool = False
|
||||
|
||||
application.getPreferences().addPreference("general/auto_slice", False)
|
||||
|
||||
self._use_timer = False #type: bool
|
||||
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
||||
# This timer will group them up, and only slice for the last setting changed signal.
|
||||
self._use_timer: bool = False
|
||||
|
||||
# When you update a setting and other settings get changed through inheritance, many propertyChanged
|
||||
# signals are fired. This timer will group them up, and only slice for the last setting changed signal.
|
||||
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
|
||||
self._change_timer = QTimer() #type: QTimer
|
||||
self._change_timer: QTimer = QTimer()
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.setInterval(500)
|
||||
self.determineAutoSlicing()
|
||||
|
@ -172,10 +189,33 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
|
||||
|
||||
self._resetLastSliceTimeStats()
|
||||
self._snapshot = None #type: Optional[QImage]
|
||||
self._snapshot: Optional[QImage] = None
|
||||
|
||||
application.initializationFinished.connect(self.initialize)
|
||||
|
||||
def startPlugins(self) -> None:
|
||||
"""
|
||||
Ensure that all backend plugins are started
|
||||
It assigns unique ports to each plugin to avoid conflicts.
|
||||
:return:
|
||||
"""
|
||||
self.stopPlugins()
|
||||
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
|
||||
for backend_plugin in backend_plugins:
|
||||
# Set the port to prevent plugins from using the same one.
|
||||
if backend_plugin.getPort() < 1:
|
||||
backend_plugin.setAvailablePort()
|
||||
backend_plugin.start()
|
||||
|
||||
def stopPlugins(self) -> None:
|
||||
"""
|
||||
Ensure that all backend plugins will be terminated.
|
||||
"""
|
||||
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
|
||||
for backend_plugin in backend_plugins:
|
||||
if backend_plugin.isRunning():
|
||||
backend_plugin.stop()
|
||||
|
||||
def _resetLastSliceTimeStats(self) -> None:
|
||||
self._time_start_process = None
|
||||
self._time_send_message = None
|
||||
|
@ -202,7 +242,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
|
||||
# Extruder enable / disable. Actually wanted to use machine manager here,
|
||||
# but the initialization order causes it to crash
|
||||
ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
|
||||
|
||||
self.backendQuit.connect(self._onBackendQuit)
|
||||
|
@ -239,26 +280,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||
|
||||
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||
parser.add_argument("--debug", action = "store_true", default = False,
|
||||
help = "Turn on the debug mode by setting this option.")
|
||||
known_args = vars(parser.parse_known_args()[0])
|
||||
if known_args["debug"]:
|
||||
command.append("-vvv")
|
||||
|
||||
return command
|
||||
|
||||
printDurationMessage = Signal()
|
||||
"""Emitted when we get a message containing print duration and material amount.
|
||||
|
||||
This also implies the slicing has finished.
|
||||
:param time: The amount of time the print will take.
|
||||
:param material_amount: The amount of material the print will use.
|
||||
"""
|
||||
slicingStarted = Signal()
|
||||
"""Emitted when the slicing process starts."""
|
||||
|
||||
slicingCancelled = Signal()
|
||||
"""Emitted when the slicing process is aborted forcefully."""
|
||||
|
||||
@pyqtSlot()
|
||||
def stopSlicing(self) -> None:
|
||||
self.setState(BackendState.NotStarted)
|
||||
|
@ -266,7 +295,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._terminate()
|
||||
self._createSocket()
|
||||
|
||||
if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon.
|
||||
if self._process_layers_job is not None:
|
||||
# We were processing layers. Stop that, the layers are going to change soon.
|
||||
Logger.log("i", "Aborting process layers job...")
|
||||
self._process_layers_job.abort()
|
||||
self._process_layers_job = None
|
||||
|
@ -281,7 +311,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.markSliceAll()
|
||||
self.slice()
|
||||
|
||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||
@call_on_qt_thread # Must be called from the main thread because of OpenGL
|
||||
def _createSnapshot(self) -> None:
|
||||
self._snapshot = None
|
||||
if not CuraApplication.getInstance().isVisible:
|
||||
|
@ -290,7 +320,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Logger.log("i", "Creating thumbnail image (just before slice)...")
|
||||
try:
|
||||
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
|
||||
except:
|
||||
except Exception:
|
||||
Logger.logException("w", "Failed to create snapshot image")
|
||||
self._snapshot = None # Failing to create thumbnail should not fail creation of UFP
|
||||
|
||||
|
@ -302,6 +332,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
self._createSnapshot()
|
||||
|
||||
self.startPlugins()
|
||||
|
||||
Logger.log("i", "Starting to slice...")
|
||||
self._time_start_process = time()
|
||||
if not self._build_plates_to_be_sliced:
|
||||
|
@ -315,7 +347,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
return
|
||||
|
||||
if not hasattr(self._scene, "gcode_dict"):
|
||||
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
|
||||
self._scene.gcode_dict = {} # type: ignore
|
||||
# We need to ignore type because we are creating the missing attribute here.
|
||||
|
||||
# see if we really have to slice
|
||||
application = CuraApplication.getInstance()
|
||||
|
@ -326,9 +359,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
self._stored_layer_data = []
|
||||
|
||||
|
||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore
|
||||
# We need to ignore the type because we created this attribute above.
|
||||
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||
if self._build_plates_to_be_sliced:
|
||||
self.slice()
|
||||
|
@ -337,7 +370,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||
application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||
|
||||
if self._process is None: # type: ignore
|
||||
if self._process is None: # type: ignore
|
||||
self._createSocket()
|
||||
self.stopSlicing()
|
||||
self._engine_is_fresh = False # Yes we're going to use the engine
|
||||
|
@ -345,7 +378,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore #[] indexed by build plate number
|
||||
self._slicing = True
|
||||
self.slicingStarted.emit()
|
||||
|
||||
|
@ -370,6 +403,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if self._start_slice_job is not None:
|
||||
self._start_slice_job.cancel()
|
||||
|
||||
self.stopPlugins()
|
||||
|
||||
self.slicingCancelled.emit()
|
||||
self.processingProgress.emit(0)
|
||||
Logger.log("d", "Attempting to kill the engine process")
|
||||
|
@ -377,14 +412,15 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if CuraApplication.getInstance().getUseExternalBackend():
|
||||
return
|
||||
|
||||
if self._process is not None: # type: ignore
|
||||
if self._process is not None: # type: ignore
|
||||
Logger.log("d", "Killing engine process")
|
||||
try:
|
||||
self._process.terminate() # type: ignore
|
||||
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
|
||||
self._process = None # type: ignore
|
||||
self._process.terminate() # type: ignore
|
||||
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
|
||||
self._process = None # type: ignore
|
||||
|
||||
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
|
||||
except Exception as e:
|
||||
# Terminating a process that is already terminating causes an exception, silently ignore this.
|
||||
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
|
||||
|
||||
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
|
||||
|
@ -429,14 +465,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
|
||||
return
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
error_keys = [] #type: List[str]
|
||||
error_keys: List[str] = []
|
||||
for extruder in extruders:
|
||||
error_keys.extend(extruder.getErrorKeys())
|
||||
if not extruders:
|
||||
error_keys = self._global_container_stack.getErrorKeys()
|
||||
error_labels = set()
|
||||
for key in error_keys:
|
||||
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
||||
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
||||
definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key)
|
||||
if definitions:
|
||||
break #Found it! No need to continue search.
|
||||
|
@ -524,7 +560,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# Preparation completed, send it to the backend.
|
||||
self._socket.sendMessage(job.getSliceMessage())
|
||||
|
||||
# Notify the user that it's now up to the backend to do it's job
|
||||
# Notify the user that it's now up to the backend to do its job
|
||||
self.setState(BackendState.Processing)
|
||||
|
||||
# Handle time reporting.
|
||||
|
@ -551,7 +587,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._is_disabled = True
|
||||
gcode_list = node.callDecoration("getGCodeList")
|
||||
if gcode_list is not None:
|
||||
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically.
|
||||
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list # type: ignore
|
||||
# We need to ignore type because we generate this attribute dynamically.
|
||||
|
||||
if self._use_timer == enable_timer:
|
||||
return self._use_timer
|
||||
|
@ -566,7 +603,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
|
||||
"""Return a dict with number of objects per build plate"""
|
||||
|
||||
num_objects = defaultdict(int) #type: Dict[int, int]
|
||||
num_objects: Dict[int, int] = defaultdict(int)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# Only count sliceable objects
|
||||
if node.callDecoration("isSliceable"):
|
||||
|
@ -646,11 +683,13 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._terminate()
|
||||
self._createSocket()
|
||||
|
||||
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
|
||||
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError,
|
||||
Arcus.ErrorCode.ConnectionResetError,
|
||||
Arcus.ErrorCode.Debug]:
|
||||
Logger.log("w", "A socket error caused the connection to be reset")
|
||||
|
||||
# _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
|
||||
# needs to be updated. Otherwise backendState is "Unable To Slice"
|
||||
# needs to be updated. Otherwise, backendState is "Unable To Slice"
|
||||
if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
|
||||
self._start_slice_job.setIsCancelled(False)
|
||||
|
||||
|
@ -672,7 +711,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
||||
# We can assume that all nodes have a parent as we're looping through the scene (and filter out root)
|
||||
# We can assume that all nodes have a parent as we're looping through the scene and filter out root
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
|
||||
def markSliceAll(self) -> None:
|
||||
|
@ -701,7 +740,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
:param instance: The setting instance that has changed.
|
||||
:param property: The property of the setting instance that has changed.
|
||||
"""
|
||||
if property == "value": # Only reslice if the value has changed.
|
||||
if property == "value": # Only re-slice if the value has changed.
|
||||
self.needsSlicing()
|
||||
self._onChanged()
|
||||
|
||||
|
@ -765,13 +804,17 @@ class CuraEngineBackend(QObject, Backend):
|
|||
:param message: The protobuf message signalling that slicing is finished.
|
||||
"""
|
||||
|
||||
self.stopPlugins()
|
||||
|
||||
self.setState(BackendState.Done)
|
||||
self.processingProgress.emit(1.0)
|
||||
self._time_end_slice = time()
|
||||
|
||||
try:
|
||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
||||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore
|
||||
# We need to ignore the type because it was generated dynamically.
|
||||
except KeyError:
|
||||
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
gcode_list = []
|
||||
application = CuraApplication.getInstance()
|
||||
for index, line in enumerate(gcode_list):
|
||||
|
@ -816,7 +859,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
try:
|
||||
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
|
||||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
except KeyError:
|
||||
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
pass # Throw the message away.
|
||||
|
||||
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
|
@ -828,7 +872,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
try:
|
||||
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
|
||||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
except KeyError:
|
||||
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
pass # Throw the message away.
|
||||
|
||||
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
|
@ -955,7 +1000,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
view = CuraApplication.getInstance().getController().getActiveView()
|
||||
if view:
|
||||
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
||||
if view.getPluginId() == "SimulationView":
|
||||
# If switching to layer view, we should process the layers if that hasn't been done yet.
|
||||
self._layer_view_active = True
|
||||
# There is data and we're not slicing at the moment
|
||||
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
||||
|
@ -974,7 +1020,6 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
We should reset our state and start listening for new connections.
|
||||
"""
|
||||
|
||||
if not self._restart:
|
||||
if self._process: # type: ignore
|
||||
return_code = self._process.wait()
|
||||
|
@ -985,6 +1030,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.stopSlicing()
|
||||
else:
|
||||
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
|
||||
self.stopPlugins()
|
||||
self._process = None # type: ignore
|
||||
|
||||
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
|
||||
|
@ -1007,7 +1053,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
||||
# Note: Only starts slicing when the value changed.
|
||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged)
|
||||
self._global_container_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
for extruder in self._global_container_stack.extruderList:
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os
|
||||
|
||||
import numpy
|
||||
from string import Formatter
|
||||
from enum import IntEnum
|
||||
import time
|
||||
from typing import Any, cast, Dict, List, Optional, Set
|
||||
from typing import Any, cast, Dict, List, Optional, Set, Tuple
|
||||
import re
|
||||
import pyArcus as Arcus # For typing.
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
|
@ -23,6 +24,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Scene.Scene import Scene #For typing.
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Settings.SettingRelation import RelationType
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
@ -45,44 +47,77 @@ class StartJobResult(IntEnum):
|
|||
|
||||
|
||||
class GcodeStartEndFormatter(Formatter):
|
||||
"""Formatter class that handles token expansion in start/end gcode"""
|
||||
# Formatter class that handles token expansion in start/end gcode
|
||||
# Example of a start/end gcode string:
|
||||
# ```
|
||||
# M104 S{material_print_temperature_layer_0, 0} ;pre-heat
|
||||
# M140 S{material_bed_temperature_layer_0} ;heat bed
|
||||
# M204 P{acceleration_print, 0} T{acceleration_travel, 0}
|
||||
# M205 X{jerk_print, 0}
|
||||
# ```
|
||||
# Any expression between curly braces will be evaluated and replaced with the result, using the
|
||||
# context of the provided default extruder. If no default extruder is provided, the global stack
|
||||
# will be used. Alternatively, if the expression is formatted as "{[expression], [extruder_nr]}",
|
||||
# then the expression will be evaluated with the extruder stack of the specified extruder_nr.
|
||||
|
||||
def __init__(self, default_extruder_nr: int = -1) -> None:
|
||||
_extruder_regex = re.compile(r"^\s*(?P<expression>.*)\s*,\s*(?P<extruder_nr>\d+)\s*$")
|
||||
|
||||
def __init__(self, default_extruder_nr: int = -1, *,
|
||||
additional_per_extruder_settings: Optional[Dict[str, Dict[str, any]]] = None) -> None:
|
||||
super().__init__()
|
||||
self._default_extruder_nr = default_extruder_nr
|
||||
self._default_extruder_nr: int = default_extruder_nr
|
||||
self._additional_per_extruder_settings: Optional[Dict[str, Dict[str, any]]] = additional_per_extruder_settings
|
||||
|
||||
def get_value(self, key: str, args: str, kwargs: dict) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
|
||||
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
|
||||
# and a default_extruder_nr to use when no extruder_nr is specified
|
||||
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 get_value(self, expression: str, args: [str], kwargs: dict) -> str:
|
||||
|
||||
# The following variables are not settings, but only become available after slicing.
|
||||
# when these variables are encountered, we return them as-is. They are replaced later
|
||||
# when the actual values are known.
|
||||
post_slice_data_variables = ["filament_cost", "print_time", "filament_amount", "filament_weight", "jobname"]
|
||||
if expression in post_slice_data_variables:
|
||||
return f"{{{expression}}}"
|
||||
|
||||
extruder_nr = self._default_extruder_nr
|
||||
|
||||
key_fragments = [fragment.strip() for fragment in key.split(",")]
|
||||
if len(key_fragments) == 2:
|
||||
try:
|
||||
extruder_nr = int(key_fragments[1])
|
||||
except ValueError:
|
||||
try:
|
||||
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack #TODO: How can you ever provide the '-1' kwarg?
|
||||
except (KeyError, ValueError):
|
||||
# either the key does not exist, or the value is not an int
|
||||
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
|
||||
elif len(key_fragments) != 1:
|
||||
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
|
||||
return "{" + key + "}"
|
||||
# The settings may specify a specific extruder to use. This is done by
|
||||
# formatting the expression as "{expression}, {extruder_nr}". 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 = int(match.group("extruder_nr"))
|
||||
|
||||
key = key_fragments[0]
|
||||
if self._additional_per_extruder_settings is not None and str(
|
||||
extruder_nr) in self._additional_per_extruder_settings:
|
||||
additional_variables = self._additional_per_extruder_settings[str(extruder_nr)]
|
||||
else:
|
||||
additional_variables = dict()
|
||||
|
||||
default_value_str = "{" + key + "}"
|
||||
value = default_value_str
|
||||
# "-1" is global stack, and if the setting value exists in the global stack, use it as the fallback value.
|
||||
if key in kwargs["-1"]:
|
||||
value = kwargs["-1"][key]
|
||||
if str(extruder_nr) in kwargs and key in kwargs[str(extruder_nr)]:
|
||||
value = kwargs[str(extruder_nr)][key]
|
||||
# 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
|
||||
|
||||
if extruder_nr == -1:
|
||||
container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
else:
|
||||
container_stack = ExtruderManager.getInstance().getExtruderStack(extruder_nr)
|
||||
|
||||
setting_function = SettingFunction(expression)
|
||||
value = setting_function(container_stack, additional_variables=additional_variables)
|
||||
|
||||
if value == default_value_str:
|
||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
||||
|
||||
return value
|
||||
|
||||
|
@ -301,6 +336,23 @@ class StartSliceJob(Job):
|
|||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for plugin in CuraApplication.getInstance().getBackendPlugins():
|
||||
if not plugin.usePlugin():
|
||||
continue
|
||||
for slot in plugin.getSupportedSlots():
|
||||
# Right now we just send the message for every slot that we support. A single plugin can support
|
||||
# multiple slots
|
||||
# In the future the frontend will need to decide what slots that a plugin actually supports should
|
||||
# also be used. For instance, if you have two plugins and each of them support a_generate and b_generate
|
||||
# only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does
|
||||
# b_generate, etc).
|
||||
plugin_message = self._slice_message.addRepeatedMessage("engine_plugins")
|
||||
plugin_message.id = slot
|
||||
plugin_message.address = plugin.getAddress()
|
||||
plugin_message.port = plugin.getPort()
|
||||
plugin_message.plugin_name = plugin.getPluginId()
|
||||
plugin_message.plugin_version = plugin.getVersion()
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
|
@ -408,13 +460,14 @@ class StartSliceJob(Job):
|
|||
self._cacheAllExtruderSettings()
|
||||
|
||||
try:
|
||||
# any setting can be used as a token
|
||||
fmt = GcodeStartEndFormatter(default_extruder_nr = default_extruder_nr)
|
||||
if self._all_extruders_settings is None:
|
||||
return ""
|
||||
settings = self._all_extruders_settings.copy()
|
||||
settings["default_extruder_nr"] = default_extruder_nr
|
||||
return str(fmt.format(value, **settings))
|
||||
# Get "replacement-keys" for the extruders. In the formatter the settings stack is used to get the
|
||||
# replacement values for the setting-keys. However, the values for `material_id`, `material_type`,
|
||||
# etc are not in the settings stack.
|
||||
additional_per_extruder_settings = self._all_extruders_settings.copy()
|
||||
additional_per_extruder_settings["default_extruder_nr"] = default_extruder_nr
|
||||
fmt = GcodeStartEndFormatter(default_extruder_nr=default_extruder_nr,
|
||||
additional_per_extruder_settings=additional_per_extruder_settings)
|
||||
return str(fmt.format(value))
|
||||
except:
|
||||
Logger.logException("w", "Unable to do token replacement on start/end g-code")
|
||||
return str(value)
|
||||
|
|
|
@ -208,12 +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 ? 0 : 1
|
||||
currentIndex: UM.Backend.state == UM.Backend.Done ? dfFilenameTextfield.text.startsWith("MM")? 1 : 0 : 2
|
||||
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
|
||||
model: [
|
||||
{ text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
|
||||
{ text: catalog.i18nc("@option", "Save Cura project and .ufp print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
|
||||
{ text: catalog.i18nc("@option", "Save Cura project and .makerbot print file"), key: "3mf_makerbot", value: ["3mf", "makerbot"] },
|
||||
{ text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ from .ExportFileJob import ExportFileJob
|
|||
class DFFileExportAndUploadManager:
|
||||
"""
|
||||
Class responsible for exporting the scene and uploading the exported data to the Digital Factory Library. Since 3mf
|
||||
and UFP files may need to be uploaded at the same time, this class keeps a single progress and success message for
|
||||
and (UFP or makerbot) files may need to be uploaded at the same time, this class keeps a single progress and success message for
|
||||
both files and updates those messages according to the progress of both the file job uploads.
|
||||
"""
|
||||
def __init__(self, file_handlers: Dict[str, FileHandler],
|
||||
|
@ -118,7 +118,7 @@ class DFFileExportAndUploadManager:
|
|||
library_project_id = self._library_project_id,
|
||||
source_file_id = self._source_file_id
|
||||
)
|
||||
self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed)
|
||||
self._api.requestUploadMeshFile(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed)
|
||||
|
||||
def _uploadFileData(self, file_upload_response: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse]) -> None:
|
||||
"""Uploads the exported file data after the file or print job upload has been registered at the Digital Factory
|
||||
|
@ -279,22 +279,25 @@ class DFFileExportAndUploadManager:
|
|||
This means that something went wrong with the initial request to create a "file" entry in the digital library.
|
||||
"""
|
||||
reply_string = bytes(reply.readAll()).decode()
|
||||
filename_ufp = self._file_name + ".ufp"
|
||||
Logger.log("d", "An error occurred while uploading the print job file '{}' to the Digital Library project '{}': {}".format(filename_ufp, self._library_project_id, reply_string))
|
||||
if "ufp" in self._formats:
|
||||
filename_meshfile = self._file_name + ".ufp"
|
||||
elif "makerbot" in self._formats:
|
||||
filename_meshfile = self._file_name + ".makerbot"
|
||||
Logger.log("d", "An error occurred while uploading the print job file '{}' to the Digital Library project '{}': {}".format(filename_meshfile, self._library_project_id, reply_string))
|
||||
with self._message_lock:
|
||||
# Set the progress to 100% when the upload job fails, to avoid having the progress message stuck
|
||||
self._file_upload_job_metadata[filename_ufp]["upload_status"] = "failed"
|
||||
self._file_upload_job_metadata[filename_ufp]["upload_progress"] = 100
|
||||
self._file_upload_job_metadata[filename_meshfile]["upload_status"] = "failed"
|
||||
self._file_upload_job_metadata[filename_meshfile]["upload_progress"] = 100
|
||||
|
||||
human_readable_error = self.extractErrorTitle(reply_string)
|
||||
self._file_upload_job_metadata[filename_ufp]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
self._file_upload_job_metadata[filename_meshfile]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
title = "File upload error",
|
||||
text = "Failed to upload the file '{}' to '{}'. {}".format(filename_ufp, self._library_project_name, human_readable_error),
|
||||
text = "Failed to upload the file '{}' to '{}'. {}".format(filename_meshfile, self._library_project_name, human_readable_error),
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
self._on_upload_error()
|
||||
self._onFileUploadFinished(filename_ufp)
|
||||
self._onFileUploadFinished(filename_meshfile)
|
||||
|
||||
@staticmethod
|
||||
def extractErrorTitle(reply_body: Optional[str]) -> str:
|
||||
|
@ -407,4 +410,28 @@ class DFFileExportAndUploadManager:
|
|||
job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp")
|
||||
job_ufp.finished.connect(self._onPrintFileExported)
|
||||
self._upload_jobs.append(job_ufp)
|
||||
|
||||
if "makerbot" in self._formats and "makerbot" in self._file_handlers and self._file_handlers["makerbot"]:
|
||||
filename_makerbot = self._file_name + ".makerbot"
|
||||
metadata[filename_makerbot] = {
|
||||
"export_job_output" : None,
|
||||
"upload_progress" : -1,
|
||||
"upload_status" : "",
|
||||
"file_upload_response": None,
|
||||
"file_upload_success_message": getBackwardsCompatibleMessage(
|
||||
text = "'{}' was uploaded to '{}'.".format(filename_makerbot, self._library_project_name),
|
||||
title = "Upload successful",
|
||||
message_type_str = "POSITIVE",
|
||||
lifetime = 30,
|
||||
),
|
||||
"file_upload_failed_message": getBackwardsCompatibleMessage(
|
||||
text = "Failed to upload the file '{}' to '{}'.".format(filename_makerbot, self._library_project_name),
|
||||
title = "File upload error",
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
}
|
||||
job_makerbot = ExportFileJob(self._file_handlers["makerbot"], self._nodes, self._file_name, "makerbot")
|
||||
job_makerbot.finished.connect(self._onPrintFileExported)
|
||||
self._upload_jobs.append(job_makerbot)
|
||||
return metadata
|
||||
|
|
|
@ -313,7 +313,7 @@ class DigitalFactoryApiClient:
|
|||
error_callback = on_error,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def requestUploadUFP(self, request: DFPrintJobUploadRequest,
|
||||
def requestUploadMeshFile(self, request: DFPrintJobUploadRequest,
|
||||
on_finished: Callable[[DFPrintJobUploadResponse], Any],
|
||||
on_error: Optional[Callable[["QNetworkReply", "QNetworkReply.NetworkError"], None]] = None) -> None:
|
||||
"""Requests the Digital Factory to register the upload of a file in a library project.
|
||||
|
|
|
@ -92,7 +92,8 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
|
|||
if not self._controller.file_handlers:
|
||||
self._controller.file_handlers = {
|
||||
"3mf": CuraApplication.getInstance().getWorkspaceFileHandler(),
|
||||
"ufp": CuraApplication.getInstance().getMeshFileHandler()
|
||||
"ufp": CuraApplication.getInstance().getMeshFileHandler(),
|
||||
"makerbot": CuraApplication.getInstance().getMeshFileHandler()
|
||||
}
|
||||
|
||||
self._dialog = CuraApplication.getInstance().createQmlComponent(self._dialog_path, {"manager": self._controller})
|
||||
|
|
|
@ -139,6 +139,34 @@ Item
|
|||
decimals: 0
|
||||
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||
}
|
||||
|
||||
Cura.NumericTextFieldWithUnit
|
||||
{
|
||||
id: extruderStartCodeDurationFieldId
|
||||
containerStackId: base.extruderStackId
|
||||
settingKey: "machine_extruder_start_code_duration"
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
labelText: catalog.i18nc("@label", "Extruder Start G-code duration")
|
||||
labelFont: base.labelFont
|
||||
labelWidth: base.labelWidth
|
||||
controlWidth: base.controlWidth
|
||||
unitText: catalog.i18nc("@label", "s")
|
||||
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||
}
|
||||
|
||||
Cura.NumericTextFieldWithUnit
|
||||
{
|
||||
id: extruderEndCodeDurationFieldId
|
||||
containerStackId: base.extruderStackId
|
||||
settingKey: "machine_extruder_end_code_duration"
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
labelText: catalog.i18nc("@label", "Extruder End G-code duration")
|
||||
labelFont: base.labelFont
|
||||
labelWidth: base.labelWidth
|
||||
controlWidth: base.controlWidth
|
||||
unitText: catalog.i18nc("@label", "s")
|
||||
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
312
plugins/MakerbotWriter/MakerbotWriter.py
Normal file
312
plugins/MakerbotWriter/MakerbotWriter.py
Normal file
|
@ -0,0 +1,312 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from io import StringIO, BufferedIOBase
|
||||
import json
|
||||
from typing import cast, List, Optional, Dict
|
||||
from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED
|
||||
import pyDulcificum as du
|
||||
|
||||
from PyQt6.QtCore import QBuffer
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Snapshot import Snapshot
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from cura.CuraVersion import ConanInstalls
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class MakerbotWriter(MeshWriter):
|
||||
"""A file writer that writes '.makerbot' files."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(add_to_recent_files=False)
|
||||
Logger.info(f"Using PyDulcificum: {du.__version__}")
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name="application/x-makerbot",
|
||||
comment="Makerbot Toolpath Package",
|
||||
suffixes=["makerbot"]
|
||||
)
|
||||
)
|
||||
|
||||
_PNG_FORMATS = [
|
||||
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
|
||||
{"prefix": "isometric_thumbnail", "width": 320, "height": 320},
|
||||
{"prefix": "isometric_thumbnail", "width": 640, "height": 640},
|
||||
{"prefix": "thumbnail", "width": 140, "height": 106},
|
||||
{"prefix": "thumbnail", "width": 212, "height": 300},
|
||||
{"prefix": "thumbnail", "width": 960, "height": 1460},
|
||||
{"prefix": "thumbnail", "width": 90, "height": 90},
|
||||
]
|
||||
_META_VERSION = "3.0.0"
|
||||
_PRINT_NAME_MAP = {
|
||||
"UltiMaker Method": "fire_e",
|
||||
"UltiMaker Method X": "lava_f",
|
||||
"UltiMaker Method XL": "magma_10",
|
||||
}
|
||||
_EXTRUDER_NAME_MAP = {
|
||||
"1XA": "mk14_hot",
|
||||
"2XA": "mk14_hot_s",
|
||||
"1C": "mk14_c",
|
||||
"1A": "mk14",
|
||||
"2A": "mk14_s",
|
||||
}
|
||||
_MATERIAL_MAP = {"2780b345-577b-4a24-a2c5-12e6aad3e690": "abs",
|
||||
"88c8919c-6a09-471a-b7b6-e801263d862d": "abs-wss1",
|
||||
"416eead4-0d8e-4f0b-8bfc-a91a519befa5": "asa",
|
||||
"85bbae0e-938d-46fb-989f-c9b3689dc4f0": "nylon-cf",
|
||||
"283d439a-3490-4481-920c-c51d8cdecf9c": "nylon",
|
||||
"62414577-94d1-490d-b1e4-7ef3ec40db02": "pc",
|
||||
"69386c85-5b6c-421a-bec5-aeb1fb33f060": "petg",
|
||||
"0ff92885-617b-4144-a03c-9989872454bc": "pla",
|
||||
"a4255da2-cb2a-4042-be49-4a83957a2f9a": "pva",
|
||||
"a140ef8f-4f26-4e73-abe0-cfc29d6d1024": "wss1",
|
||||
"77873465-83a9-4283-bc44-4e542b8eb3eb": "sr30",
|
||||
"96fca5d9-0371-4516-9e96-8e8182677f3c": "im-pla",
|
||||
"9f52c514-bb53-46a6-8c0c-d507cd6ee742": "abs",
|
||||
"0f9a2a91-f9d6-4b6b-bd9b-a120a29391be": "abs",
|
||||
"d3e972f2-68c0-4d2f-8cfd-91028dfc3381": "abs",
|
||||
"495a0ce5-9daf-4a16-b7b2-06856d82394d": "abs-cf10",
|
||||
"cb76bd6e-91fd-480c-a191-12301712ec77": "abs-wss1",
|
||||
"a017777e-3f37-4d89-a96c-dc71219aac77": "abs-wss1",
|
||||
"4d96000d-66de-4d54-a580-91827dcfd28f": "abs-wss1",
|
||||
"0ecb0e1a-6a66-49fb-b9ea-61a8924e0cf5": "asa",
|
||||
"efebc2ea-2381-4937-926f-e824524524a5": "asa",
|
||||
"b0199512-5714-4951-af85-be19693430f8": "asa",
|
||||
"b9f55a0a-a2b6-4b8d-8d48-07802c575bd1": "pla",
|
||||
"c439d884-9cdc-4296-a12c-1bacae01003f": "pla",
|
||||
"16a723e3-44df-49f4-82ec-2a1173c1e7d9": "pla",
|
||||
"74d0f5c2-fdfd-4c56-baf1-ff5fa92d177e": "pla",
|
||||
"64dcb783-470d-4400-91b1-7001652f20da": "pla",
|
||||
"3a1b479b-899c-46eb-a2ea-67050d1a4937": "pla",
|
||||
"4708ac49-5dde-4cc2-8c0a-87425a92c2b3": "pla",
|
||||
"4b560eda-1719-407f-b085-1c2c1fc8ffc1": "pla",
|
||||
"e10a287d-0067-4a58-9083-b7054f479991": "im-pla",
|
||||
"01a6b5b0-fab1-420c-a5d9-31713cbeb404": "im-pla",
|
||||
"f65df4ad-a027-4a48-a51d-975cc8b87041": "im-pla",
|
||||
"f48739f8-6d96-4a3d-9a2e-8505a47e2e35": "im-pla",
|
||||
"5c7d7672-e885-4452-9a78-8ba90ec79937": "petg",
|
||||
"91e05a6e-2f5b-4964-b973-d83b5afe6db4": "petg",
|
||||
"bdc7dd03-bf38-48ee-aeca-c3e11cee799e": "petg",
|
||||
"54f66c89-998d-4070-aa60-1cb0fd887518": "nylon",
|
||||
"002c84b3-84ac-4b5a-b57d-fe1f555a6351": "pva",
|
||||
"e4da5fcb-f62d-48a2-aaef-0b645aa6973b": "wss1",
|
||||
"77f06146-6569-437d-8380-9edb0d635a32": "sr30"}
|
||||
|
||||
# must be called from the main thread because of OpenGL
|
||||
@staticmethod
|
||||
@call_on_qt_thread
|
||||
def _createThumbnail(width: int, height: int) -> Optional[QBuffer]:
|
||||
if not CuraApplication.getInstance().isVisible:
|
||||
Logger.warning("Can't create snapshot when renderer not initialized.")
|
||||
return
|
||||
try:
|
||||
snapshot = Snapshot.isometricSnapshot(width, height)
|
||||
|
||||
thumbnail_buffer = QBuffer()
|
||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.WriteOnly)
|
||||
|
||||
snapshot.save(thumbnail_buffer, "PNG")
|
||||
|
||||
return thumbnail_buffer
|
||||
|
||||
except:
|
||||
Logger.logException("w", "Failed to create snapshot image")
|
||||
|
||||
return None
|
||||
|
||||
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||
if mode != MeshWriter.OutputMode.BinaryMode:
|
||||
Logger.log("e", "MakerbotWriter does not support text mode.")
|
||||
self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
|
||||
return False
|
||||
|
||||
# The GCodeWriter plugin is always available since it is in the "required" list of plugins.
|
||||
gcode_writer = PluginRegistry.getInstance().getPluginObject("GCodeWriter")
|
||||
|
||||
if gcode_writer is None:
|
||||
Logger.log("e", "Could not find the GCodeWriter plugin, is it disabled?.")
|
||||
self.setInformation(
|
||||
catalog.i18nc("@error:load", "Could not load GCodeWriter plugin. Try to re-enable the plugin."))
|
||||
return False
|
||||
|
||||
gcode_writer = cast(MeshWriter, gcode_writer)
|
||||
|
||||
gcode_text_io = StringIO()
|
||||
success = gcode_writer.write(gcode_text_io, None)
|
||||
|
||||
# Writing the g-code failed. Then I can also not write the gzipped g-code.
|
||||
if not success:
|
||||
self.setInformation(gcode_writer.getInformation())
|
||||
return False
|
||||
|
||||
json_toolpaths = du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
|
||||
metadata = self._getMeta(nodes)
|
||||
|
||||
png_files = []
|
||||
for png_format in self._PNG_FORMATS:
|
||||
width, height, prefix = png_format["width"], png_format["height"], png_format["prefix"]
|
||||
thumbnail_buffer = self._createThumbnail(width, height)
|
||||
if thumbnail_buffer is None:
|
||||
Logger.warning(f"Could not create thumbnail of size {width}x{height}.")
|
||||
continue
|
||||
png_files.append({
|
||||
"file": f"{prefix}_{width}x{height}.png",
|
||||
"data": thumbnail_buffer.data(),
|
||||
})
|
||||
|
||||
try:
|
||||
with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
|
||||
zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
|
||||
zip_stream.writestr("print.jsontoolpath", json_toolpaths)
|
||||
for png_file in png_files:
|
||||
file, data = png_file["file"], png_file["data"]
|
||||
zip_stream.writestr(file, data)
|
||||
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."))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _getMeta(self, root_nodes: List[SceneNode]) -> Dict[str, any]:
|
||||
application = CuraApplication.getInstance()
|
||||
machine_manager = application.getMachineManager()
|
||||
global_stack = machine_manager.activeMachine
|
||||
extruders = global_stack.extruderList
|
||||
|
||||
nodes = []
|
||||
for root_node in root_nodes:
|
||||
for node in DepthFirstIterator(root_node):
|
||||
if not getattr(node, "_outside_buildarea", False):
|
||||
if node.callDecoration(
|
||||
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
|
||||
"isNonThumbnailVisibleMesh"):
|
||||
nodes.append(node)
|
||||
|
||||
meta = dict()
|
||||
|
||||
meta["bot_type"] = MakerbotWriter._PRINT_NAME_MAP.get((name := global_stack.definition.name), name)
|
||||
|
||||
bounds: Optional[AxisAlignedBox] = None
|
||||
for node in nodes:
|
||||
node_bounds = node.getBoundingBox()
|
||||
if node_bounds is None:
|
||||
continue
|
||||
if bounds is None:
|
||||
bounds = node_bounds
|
||||
else:
|
||||
bounds = bounds + node_bounds
|
||||
|
||||
if bounds is not None:
|
||||
meta["bounding_box"] = {
|
||||
"x_min": bounds.left,
|
||||
"x_max": bounds.right,
|
||||
"y_min": bounds.back,
|
||||
"y_max": bounds.front,
|
||||
"z_min": bounds.bottom,
|
||||
"z_max": bounds.top,
|
||||
}
|
||||
|
||||
material_bed_temperature = global_stack.getProperty("material_bed_temperature", "value")
|
||||
meta["platform_temperature"] = material_bed_temperature
|
||||
|
||||
build_volume_temperature = global_stack.getProperty("build_volume_temperature", "value")
|
||||
meta["build_plane_temperature"] = build_volume_temperature
|
||||
|
||||
print_information = application.getPrintInformation()
|
||||
|
||||
meta["commanded_duration_s"] = int(print_information.currentPrintTime)
|
||||
meta["duration_s"] = int(print_information.currentPrintTime)
|
||||
|
||||
material_lengths = list(map(meterToMillimeter, print_information.materialLengths))
|
||||
meta["extrusion_distance_mm"] = material_lengths[0]
|
||||
meta["extrusion_distances_mm"] = material_lengths
|
||||
|
||||
meta["extrusion_mass_g"] = print_information.materialWeights[0]
|
||||
meta["extrusion_masses_g"] = print_information.materialWeights
|
||||
|
||||
meta["uuid"] = print_information.slice_uuid
|
||||
|
||||
materials = []
|
||||
for extruder in extruders:
|
||||
guid = extruder.material.getMetaData().get("GUID")
|
||||
material_name = extruder.material.getMetaData().get("material")
|
||||
material = self._MATERIAL_MAP.get(guid, material_name)
|
||||
materials.append(material)
|
||||
|
||||
meta["material"] = materials[0]
|
||||
meta["materials"] = materials
|
||||
|
||||
materials_temps = [extruder.getProperty("default_material_print_temperature", "value") for extruder in
|
||||
extruders]
|
||||
meta["extruder_temperature"] = materials_temps[0]
|
||||
meta["extruder_temperatures"] = materials_temps
|
||||
|
||||
meta["model_counts"] = [{"count": 1, "name": node.getName()} for node in nodes]
|
||||
|
||||
tool_types = [MakerbotWriter._EXTRUDER_NAME_MAP.get((name := extruder.variant.getName()), name) for extruder in
|
||||
extruders]
|
||||
meta["tool_type"] = tool_types[0]
|
||||
meta["tool_types"] = tool_types
|
||||
|
||||
meta["version"] = MakerbotWriter._META_VERSION
|
||||
|
||||
meta["preferences"] = dict()
|
||||
for node in nodes:
|
||||
bounds = node.getBoundingBox()
|
||||
meta["preferences"][str(node.getName())] = {
|
||||
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
|
||||
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
|
||||
}
|
||||
|
||||
meta["miracle_config"] = {"gaggles": {str(node.getName()): {} for node in nodes}}
|
||||
|
||||
version_info = dict()
|
||||
cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
|
||||
version_info["curaengine_version"] = cura_engine_info["version"]
|
||||
version_info["curaengine_commit_hash"] = cura_engine_info["revision"]
|
||||
|
||||
dulcificum_info = ConanInstalls.get("dulcificum", {"version": "unknown", "revision": "unknown"})
|
||||
version_info["dulcificum_version"] = dulcificum_info["version"]
|
||||
version_info["dulcificum_commit_hash"] = dulcificum_info["revision"]
|
||||
|
||||
version_info["makerbot_writer_version"] = self.getVersion()
|
||||
version_info["pyDulcificum_version"] = du.__version__
|
||||
|
||||
# Add engine plugin information to the metadata
|
||||
for name, package_info in ConanInstalls.items():
|
||||
if not name.startswith("curaengine_"):
|
||||
continue
|
||||
version_info[f"{name}_version"] = package_info["version"]
|
||||
version_info[f"{name}_commit_hash"] = package_info["revision"]
|
||||
|
||||
# Add version info to the main metadata, but also to "miracle_config"
|
||||
# so that it shows up in analytics
|
||||
meta["miracle_config"].update(version_info)
|
||||
meta.update(version_info)
|
||||
|
||||
# TODO add the following instructions
|
||||
# num_tool_changes
|
||||
# num_z_layers
|
||||
# num_z_transitions
|
||||
# platform_temperature
|
||||
# total_commands
|
||||
|
||||
return meta
|
||||
|
||||
|
||||
def meterToMillimeter(value: float) -> float:
|
||||
"""Converts a value in meters to millimeters."""
|
||||
return value * 1000.0
|
28
plugins/MakerbotWriter/__init__.py
Normal file
28
plugins/MakerbotWriter/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from . import MakerbotWriter
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
file_extension = "makerbot"
|
||||
return {
|
||||
"mesh_writer": {
|
||||
"output": [{
|
||||
"extension": file_extension,
|
||||
"description": catalog.i18nc("@item:inlistbox", "Makerbot Printfile"),
|
||||
"mime_type": "application/x-makerbot",
|
||||
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {
|
||||
"mesh_writer": MakerbotWriter.MakerbotWriter(),
|
||||
}
|
13
plugins/MakerbotWriter/plugin.json
Normal file
13
plugins/MakerbotWriter/plugin.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Makerbot Printfile Writer",
|
||||
"author": "UltiMaker",
|
||||
"version": "0.1.0",
|
||||
"description": "Provides support for writing MakerBot Format Packages.",
|
||||
"api": 8,
|
||||
"supported_sdk_versions": [
|
||||
"8.0.0",
|
||||
"8.1.0",
|
||||
"8.2.0"
|
||||
],
|
||||
"i18n-catalog": "cura"
|
||||
}
|
|
@ -20,7 +20,6 @@ class MissingPackageList(RemotePackageList):
|
|||
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._packages_metadata: List[Dict[str, str]] = packages_metadata
|
||||
self._package_type_filter = "material"
|
||||
self._search_type = "package_ids"
|
||||
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata))
|
||||
|
||||
|
@ -38,7 +37,14 @@ class MissingPackageList(RemotePackageList):
|
|||
|
||||
for package_metadata in self._packages_metadata:
|
||||
if package_metadata["id"] not in returned_packages_ids:
|
||||
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"], package_metadata["package_version"], self._package_type_filter)
|
||||
package_type = package_metadata["type"] if "type" in package_metadata else "material"
|
||||
# When this feature was originally introduced only missing materials were detected. With the inclusion
|
||||
# of backend plugins this system was extended to also detect missing plugins. With that change the type
|
||||
# of the package was added to the metadata. Project files before this change do not have this type. So
|
||||
# if the type is not present we assume it is a material.
|
||||
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"],
|
||||
package_metadata["package_version"],
|
||||
package_type)
|
||||
self.appendItem({"package": package})
|
||||
|
||||
self.itemsChanged.emit()
|
||||
|
|
|
@ -87,12 +87,22 @@ class PackageModel(QObject):
|
|||
self._is_missing_package_information = False
|
||||
|
||||
@classmethod
|
||||
def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
|
||||
def fromIncompletePackageInformation(cls, display_name: str, package_version: str,
|
||||
package_type: str) -> "PackageModel":
|
||||
description = ""
|
||||
match package_type:
|
||||
case "material":
|
||||
description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
|
||||
"The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
|
||||
case "plugin":
|
||||
description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
|
||||
"The plugin associated with the Cura project could not be found on the Ultimaker Marketplace. As the plugin may be required to slice the project it might not be possible to correctly slice the file.")
|
||||
|
||||
package_data = {
|
||||
"display_name": display_name,
|
||||
"package_version": package_version,
|
||||
"package_type": package_type,
|
||||
"description": catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate", "The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
|
||||
"description": description,
|
||||
}
|
||||
package_model = cls(package_data)
|
||||
package_model.setIsMissingPackageInformation(True)
|
||||
|
|
|
@ -21,6 +21,7 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
class RemotePackageList(PackageList):
|
||||
ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
|
||||
SORT_TYPE = "last_updated" # Default value to send for sort_by filter.
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
@ -28,6 +29,7 @@ class RemotePackageList(PackageList):
|
|||
self._package_type_filter = ""
|
||||
self._requested_search_string = ""
|
||||
self._current_search_string = ""
|
||||
self._search_sort = "sort_by"
|
||||
self._search_type = "search"
|
||||
self._request_url = self._initialRequestUrl()
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
|
@ -102,6 +104,8 @@ class RemotePackageList(PackageList):
|
|||
request_url += f"&package_type={self._package_type_filter}"
|
||||
if self._current_search_string != "":
|
||||
request_url += f"&{self._search_type}={self._current_search_string}"
|
||||
if self.SORT_TYPE:
|
||||
request_url += f"&{self._search_sort}={self.SORT_TYPE}"
|
||||
return request_url
|
||||
|
||||
def _parseResponse(self, reply: "QNetworkReply") -> None:
|
||||
|
|
|
@ -12,7 +12,7 @@ import Cura 1.6 as Cura
|
|||
Marketplace
|
||||
{
|
||||
modality: Qt.ApplicationModal
|
||||
title: catalog.i18nc("@title", "Install missing Materials")
|
||||
title: catalog.i18nc("@title", "Install missing packages")
|
||||
pageContentsSource: "MissingPackages.qml"
|
||||
showSearchHeader: false
|
||||
showOnboadBanner: false
|
||||
|
|
|
@ -280,7 +280,7 @@ Window
|
|||
onClicked:
|
||||
{
|
||||
marketplaceDialog.hide();
|
||||
CuraApplication.closeApplication();
|
||||
CuraApplication.checkAndExitApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import UM 1.4 as UM
|
|||
|
||||
Packages
|
||||
{
|
||||
pageTitle: catalog.i18nc("@header", "Install Materials")
|
||||
pageTitle: catalog.i18nc("@header", "Install Packages")
|
||||
|
||||
bannerVisible: false
|
||||
showUpdateButton: false
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import UM 1.5 as UM
|
||||
|
@ -234,10 +234,11 @@ Item
|
|||
setDestroyed(true)
|
||||
}
|
||||
}
|
||||
|
||||
property int indexWithFocus: -1
|
||||
delegate: Row
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
property var settingLoaderItem: settingLoader.item
|
||||
Loader
|
||||
{
|
||||
id: settingLoader
|
||||
|
@ -340,6 +341,44 @@ Item
|
|||
function onPropertiesChanged() { provider.forcePropertiesChanged() }
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: settingLoader.item
|
||||
function onFocusReceived()
|
||||
{
|
||||
|
||||
contents.indexWithFocus = index
|
||||
contents.positionViewAtIndex(index, ListView.Contain)
|
||||
}
|
||||
function onSetActiveFocusToNextSetting(forward)
|
||||
{
|
||||
if (forward == undefined || forward)
|
||||
{
|
||||
contents.currentIndex = contents.indexWithFocus + 1
|
||||
while(contents.currentItem && contents.currentItem.height <= 0)
|
||||
{
|
||||
contents.currentIndex++
|
||||
}
|
||||
if (contents.currentItem)
|
||||
{
|
||||
contents.currentItem.settingLoaderItem.focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
contents.currentIndex = contents.indexWithFocus - 1
|
||||
while(contents.currentItem && contents.currentItem.height <= 0)
|
||||
{
|
||||
contents.currentIndex--
|
||||
}
|
||||
if (contents.currentItem)
|
||||
{
|
||||
contents.currentItem.settingLoaderItem.focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: UM.ActiveTool
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # The script lists are stored in metadata as serialised config files.
|
||||
import importlib.util
|
||||
|
@ -93,6 +93,11 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
Logger.logException("e", "Exception in post-processing script.")
|
||||
if len(self._script_list): # Add comment to g-code if any changes were made.
|
||||
gcode_list[0] += ";POSTPROCESSED\n"
|
||||
# Add all the active post processor names to data[0]
|
||||
pp_name_list = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("post_processing_scripts")
|
||||
for pp_name in pp_name_list.split("\n"):
|
||||
pp_name = pp_name.split("]")
|
||||
gcode_list[0] += "; " + str(pp_name[0]) + "]\n"
|
||||
gcode_dict[active_build_plate_id] = gcode_list
|
||||
setattr(scene, "gcode_dict", gcode_dict)
|
||||
else:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2022 Jaime van Kessel, Ultimaker B.V.
|
||||
// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
// The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 2.15
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2015 Jaime van Kessel
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Any, Dict, TYPE_CHECKING, List
|
||||
|
||||
from UM.Signal import Signal, signalemitter
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# ChangeAtZ script - Change printing parameters at a given height
|
||||
# This script is the successor of the TweakAtZ plugin for legacy Cura.
|
||||
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
|
||||
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
|
||||
# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
|
||||
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
|
||||
|
||||
# Authors of the ChangeAtZ plugin / script:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# ColorMix script - 2-1 extruder color mix and blending
|
||||
# This script is specific for the Geeetech A10M dual extruder but should work with other Marlin printers.
|
||||
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
|
||||
# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
|
||||
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
|
||||
|
||||
#Authors of the 2-1 ColorMix plug-in / script:
|
||||
|
|
|
@ -37,7 +37,7 @@ class CreateThumbnail(Script):
|
|||
|
||||
encoded_snapshot_length = len(encoded_snapshot)
|
||||
gcode.append(";")
|
||||
gcode.append("; thumbnail begin {} {} {}".format(
|
||||
gcode.append("; thumbnail begin {}x{} {}".format(
|
||||
width, height, encoded_snapshot_length))
|
||||
|
||||
chunks = ["; {}".format(encoded_snapshot[i:i+chunk_size])
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2023 Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# Modification 06.09.2020
|
||||
# add checkbox, now you can choose and use configuration from the firmware itself.
|
||||
|
|
349
plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py
Normal file
349
plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py
Normal file
|
@ -0,0 +1,349 @@
|
|||
# Limit XY Accel: Authored by: Greg Foresi (GregValiant)
|
||||
# July 2023
|
||||
# Sometimes bed-slinger printers need different Accel and Jerk values for the Y but Cura always makes them the same.
|
||||
# This script changes the Accel and/or Jerk from the beginning of the 'Start Layer' to the end of the 'End Layer'.
|
||||
# The existing M201 Max Accel will be changed to limit the Y (and/or X) accel at the printer. If you have Accel enabled in Cura and the XY Accel is set to 3000 then setting the Y limit to 1000 will result in the printer limiting the Y to 1000. This can keep tall skinny prints from breaking loose of the bed and failing. The script was not tested with Junction Deviation.
|
||||
# If enabled - the Jerk setting is changed line-by-line within the gcode as there is no "limit" on Jerk.
|
||||
# if 'Gradual ACCEL change' is enabled then the Accel is changed gradually from the Start to the End layer and that will be the final Accel setting in the file. If 'Gradual' is enabled then the Jerk settings will continue to be changed to the end of the file (rather than ending at the End layer).
|
||||
# This post is intended for printers with moving beds (bed slingers) so UltiMaker printers are excluded.
|
||||
# When setting an accel limit on multi-extruder printers ALL extruders are effected.
|
||||
# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300.
|
||||
# 9/15/2023 added support for RepRap M566 command for Jerk in mm/min
|
||||
|
||||
from ..Script import Script
|
||||
from cura.CuraApplication import CuraApplication
|
||||
import re
|
||||
from UM.Message import Message
|
||||
|
||||
class LimitXYAccelJerk(Script):
|
||||
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
# Get the Accel and Jerk and set the values in the setting boxes--
|
||||
mycura = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
extruder = mycura.extruderList
|
||||
accel_print = extruder[0].getProperty("acceleration_print", "value")
|
||||
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
|
||||
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
|
||||
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
|
||||
self._instance.setProperty("x_accel_limit", "value", round(accel_print))
|
||||
self._instance.setProperty("y_accel_limit", "value", round(accel_print))
|
||||
self._instance.setProperty("x_jerk", "value", jerk_print_old)
|
||||
self._instance.setProperty("y_jerk", "value", jerk_print_old)
|
||||
ext_count = int(mycura.getProperty("machine_extruder_count", "value"))
|
||||
machine_name = str(mycura.getProperty("machine_name", "value"))
|
||||
if str(mycura.getProperty("machine_gcode_flavor", "value")) == "RepRap (RepRap)":
|
||||
self._instance.setProperty("jerk_cmd", "value", "reprap_flavor")
|
||||
else:
|
||||
self._instance.setProperty("jerk_cmd", "value", "marlin_flavor")
|
||||
firmware_flavor = str(mycura.getProperty("machine_gcode_flavor", "value"))
|
||||
|
||||
# Warn the user if the printer is an Ultimaker-------------------------
|
||||
if "Ultimaker" in machine_name or "UltiGCode" in firmware_flavor or "Griffin" in firmware_flavor:
|
||||
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN because Ultimaker printers don't have sliding beds.").show()
|
||||
|
||||
# Warn the user if the printer is multi-extruder------------------
|
||||
if ext_count > 1:
|
||||
Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show()
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Limit the X-Y Accel/Jerk (all extruders equal)",
|
||||
"key": "LimitXYAccelJerk",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"type_of_change":
|
||||
{
|
||||
"label": "Immediate or Gradual change",
|
||||
"description": "An 'Immediate' change will insert the new numbers immediately at the Start Layer. A 'Gradual' change will transition from the starting Accel to the new Accel limit across a range of layers.",
|
||||
"type": "enum",
|
||||
"options": {
|
||||
"immediate_change": "Immediate",
|
||||
"gradual_change": "Gradual"},
|
||||
"default_value": "immediate_change"
|
||||
},
|
||||
"x_accel_limit":
|
||||
{
|
||||
"label": "X MAX Acceleration",
|
||||
"description": "If this number is lower than the 'X Print Accel' in Cura then this will limit the Accel on the X axis. Enter the Maximum Acceleration value for the X axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).",
|
||||
"type": "int",
|
||||
"enabled": true,
|
||||
"minimum_value": 50,
|
||||
"unit": "mm/sec² ",
|
||||
"default_value": 500
|
||||
},
|
||||
"y_accel_limit":
|
||||
{
|
||||
"label": "Y MAX Acceleration",
|
||||
"description": "If this number is lower than the Y accel in Cura then this will limit the Accel on the Y axis. Enter the Maximum Acceleration value for the Y axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).",
|
||||
"type": "int",
|
||||
"enabled": true,
|
||||
"minimum_value": 50,
|
||||
"unit": "mm/sec² ",
|
||||
"default_value": 500
|
||||
},
|
||||
"jerk_enable":
|
||||
{
|
||||
"label": "Change the Jerk",
|
||||
"description": "Whether to change the Jerk values.",
|
||||
"type": "bool",
|
||||
"enabled": true,
|
||||
"default_value": false
|
||||
},
|
||||
"jerk_cmd":
|
||||
{
|
||||
"label": "G-Code Jerk Command",
|
||||
"description": "Marlin uses M205. RepRap might use M566.",
|
||||
"type": "enum",
|
||||
"options": {
|
||||
"marlin_flavor": "M205",
|
||||
"reprap_flavor": "M566"},
|
||||
"default_value": "marlin_flavor",
|
||||
"enabled": "jerk_enable"
|
||||
},
|
||||
"x_jerk":
|
||||
{
|
||||
"label": " X jerk",
|
||||
"description": "Enter the Jerk value for the X axis. Enter '0' to use the existing X Jerk. This setting will affect both the Print and Travel jerk.",
|
||||
"type": "int",
|
||||
"enabled": "jerk_enable",
|
||||
"unit": "mm/sec ",
|
||||
"default_value": 8
|
||||
},
|
||||
"y_jerk":
|
||||
{
|
||||
"label": " Y jerk",
|
||||
"description": "Enter the Jerk value for the Y axis. Enter '0' to use the existing Y Jerk. This setting will affect both the Print and Travel jerk.",
|
||||
"type": "int",
|
||||
"enabled": "jerk_enable",
|
||||
"unit": "mm/sec ",
|
||||
"default_value": 8
|
||||
},
|
||||
"start_layer":
|
||||
{
|
||||
"label": "From Start of Layer:",
|
||||
"description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.",
|
||||
"type": "int",
|
||||
"default_value": 1,
|
||||
"minimum_value": 1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "type_of_change == 'immediate_change'"
|
||||
},
|
||||
"end_layer":
|
||||
{
|
||||
"label": "To End of Layer",
|
||||
"description": "Use the Cura Preview numbers. Enter '-1' for the entire file or enter a layer number. The changes will end at your 'End Layer' and revert back to the original numbers.",
|
||||
"type": "int",
|
||||
"default_value": -1,
|
||||
"minimum_value": -1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "type_of_change == 'immediate_change'"
|
||||
},
|
||||
"gradient_start_layer":
|
||||
{
|
||||
"label": " Gradual From Layer:",
|
||||
"description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.",
|
||||
"type": "int",
|
||||
"default_value": 1,
|
||||
"minimum_value": 1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "type_of_change == 'gradual_change'"
|
||||
},
|
||||
"gradient_end_layer":
|
||||
{
|
||||
"label": " Gradual To Layer",
|
||||
"description": "Use the Cura Preview numbers. Enter '-1' for the top layer or enter a layer number. The last 'Gradual' change will continue to the end of the file.",
|
||||
"type": "int",
|
||||
"default_value": -1,
|
||||
"minimum_value": -1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "type_of_change == 'gradual_change'"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
mycura = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
extruder = mycura.extruderList
|
||||
machine_name = str(mycura.getProperty("machine_name", "value"))
|
||||
print_sequence = str(mycura.getProperty("print_sequence", "value"))
|
||||
|
||||
# Exit if 'one_at_a_time' is enabled-------------------------
|
||||
if print_sequence == "one_at_a_time":
|
||||
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is not compatible with 'One-at-a-Time' mode.").show()
|
||||
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because Cura is set to 'One-at-a-Time' mode.\n"
|
||||
return data
|
||||
|
||||
# Exit if the printer is an Ultimaker-------------------------
|
||||
if "Ultimaker" in machine_name:
|
||||
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is for bed slinger printers only.").show()
|
||||
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because the printer doesn't have a sliding bed.\n"
|
||||
return data
|
||||
|
||||
type_of_change = str(self.getSettingValueByKey("type_of_change"))
|
||||
accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
|
||||
accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value"))
|
||||
accel_print = extruder[0].getProperty("acceleration_print", "value")
|
||||
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
|
||||
jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value"))
|
||||
jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value"))
|
||||
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
|
||||
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
|
||||
if int(accel_print) >= int(accel_travel):
|
||||
accel_old = accel_print
|
||||
else:
|
||||
accel_old = accel_travel
|
||||
jerk_travel = str(extruder[0].getProperty("jerk_travel", "value"))
|
||||
if int(jerk_print_old) >= int(jerk_travel_old):
|
||||
jerk_old = jerk_print_old
|
||||
else:
|
||||
jerk_old = jerk_travel_old
|
||||
|
||||
#Set the new Accel values----------------------------------------------------------
|
||||
x_accel = str(self.getSettingValueByKey("x_accel_limit"))
|
||||
y_accel = str(self.getSettingValueByKey("y_accel_limit"))
|
||||
x_jerk = int(self.getSettingValueByKey("x_jerk"))
|
||||
y_jerk = int(self.getSettingValueByKey("y_jerk"))
|
||||
if str(self.getSettingValueByKey("jerk_cmd")) == "reprap_flavor":
|
||||
jerk_cmd = "M566"
|
||||
x_jerk *= 60
|
||||
y_jerk *= 60
|
||||
jerk_old *= 60
|
||||
else:
|
||||
jerk_cmd = "M205"
|
||||
|
||||
# Put the strings together-------------------------------------------
|
||||
m201_limit_new = f"M201 X{x_accel} Y{y_accel}"
|
||||
m201_limit_old = f"M201 X{round(accel_old)} Y{round(accel_old)}"
|
||||
if x_jerk == 0:
|
||||
m205_jerk_pattern = r"Y(\d*)"
|
||||
m205_jerk_new = f"Y{y_jerk}"
|
||||
if y_jerk == 0:
|
||||
m205_jerk_pattern = r"X(\d*)"
|
||||
m205_jerk_new = f"X{x_jerk}"
|
||||
if x_jerk != 0 and y_jerk != 0:
|
||||
m205_jerk_pattern = jerk_cmd + r" X(\d*) Y(\d*)"
|
||||
m205_jerk_new = jerk_cmd + f" X{x_jerk} Y{y_jerk}"
|
||||
m205_jerk_old = jerk_cmd + f" X{jerk_old} Y{jerk_old}"
|
||||
type_of_change = self.getSettingValueByKey("type_of_change")
|
||||
|
||||
#Get the indexes of the start and end layers----------------------------------------
|
||||
if type_of_change == 'immediate_change':
|
||||
start_layer = int(self.getSettingValueByKey("start_layer"))-1
|
||||
end_layer = int(self.getSettingValueByKey("end_layer"))
|
||||
else:
|
||||
start_layer = int(self.getSettingValueByKey("gradient_start_layer"))-1
|
||||
end_layer = int(self.getSettingValueByKey("gradient_end_layer"))
|
||||
start_index = 2
|
||||
end_index = len(data)-2
|
||||
for num in range(2,len(data)-1):
|
||||
if ";LAYER:" + str(start_layer) + "\n" in data[num]:
|
||||
start_index = num
|
||||
break
|
||||
if int(end_layer) > 0:
|
||||
for num in range(3,len(data)-1):
|
||||
try:
|
||||
if ";LAYER:" + str(end_layer) + "\n" in data[num]:
|
||||
end_index = num
|
||||
break
|
||||
except:
|
||||
end_index = len(data)-2
|
||||
|
||||
#Add Accel limit and new Jerk at start layer-----------------------------------------------------
|
||||
if type_of_change == "immediate_change":
|
||||
layer = data[start_index]
|
||||
lines = layer.split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if lines[index].startswith(";LAYER:"):
|
||||
lines.insert(index+1,m201_limit_new)
|
||||
if self.getSettingValueByKey("jerk_enable"):
|
||||
lines.insert(index+2,m205_jerk_new)
|
||||
data[start_index] = "\n".join(lines)
|
||||
break
|
||||
|
||||
#Alter any existing jerk lines. Accel lines can be ignored-----------------------------------
|
||||
for num in range(start_index,end_index,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("M205") or line.startswith("M566"):
|
||||
lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line)
|
||||
data[num] = "\n".join(lines)
|
||||
if end_layer != -1:
|
||||
try:
|
||||
layer = data[end_index-1]
|
||||
lines = layer.split("\n")
|
||||
lines.insert(len(lines)-2,m201_limit_old)
|
||||
lines.insert(len(lines)-2,m205_jerk_old)
|
||||
data[end_index-1] = "\n".join(lines)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1]
|
||||
return data
|
||||
|
||||
elif type_of_change == "gradual_change":
|
||||
layer_spread = end_index - start_index
|
||||
if accel_old >= int(x_accel):
|
||||
x_accel_hyst = round((accel_old - int(x_accel)) / layer_spread)
|
||||
else:
|
||||
x_accel_hyst = round((int(x_accel) - accel_old) / layer_spread)
|
||||
if accel_old >= int(y_accel):
|
||||
y_accel_hyst = round((accel_old - int(y_accel)) / layer_spread)
|
||||
else:
|
||||
y_accel_hyst = round((int(y_accel) - accel_old) / layer_spread)
|
||||
|
||||
if accel_old >= int(x_accel):
|
||||
x_accel_start = round(round((accel_old - x_accel_hyst)/25)*25)
|
||||
else:
|
||||
x_accel_start = round(round((x_accel_hyst + accel_old)/25)*25)
|
||||
if accel_old >= int(y_accel):
|
||||
y_accel_start = round(round((accel_old - y_accel_hyst)/25)*25)
|
||||
else:
|
||||
y_accel_start = round(round((y_accel_hyst + accel_old)/25)*25)
|
||||
m201_limit_new = "M201 X" + str(x_accel_start) + " Y" + str(y_accel_start)
|
||||
#Add Accel limit and new Jerk at start layer-------------------------------------------------------------
|
||||
layer = data[start_index]
|
||||
lines = layer.split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if lines[index].startswith(";LAYER:"):
|
||||
lines.insert(index+1,m201_limit_new)
|
||||
if self.getSettingValueByKey("jerk_enable"):
|
||||
lines.insert(index+2,m205_jerk_new)
|
||||
data[start_index] = "\n".join(lines)
|
||||
break
|
||||
for num in range(start_index + 1, end_index,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
if accel_old >= int(x_accel):
|
||||
x_accel_start -= x_accel_hyst
|
||||
if x_accel_start < int(x_accel): x_accel_start = int(x_accel)
|
||||
else:
|
||||
x_accel_start += x_accel_hyst
|
||||
if x_accel_start > int(x_accel): x_accel_start = int(x_accel)
|
||||
if accel_old >= int(y_accel):
|
||||
y_accel_start -= y_accel_hyst
|
||||
if y_accel_start < int(y_accel): y_accel_start = int(y_accel)
|
||||
else:
|
||||
y_accel_start += y_accel_hyst
|
||||
if y_accel_start > int(y_accel): y_accel_start = int(y_accel)
|
||||
m201_limit_new = "M201 X" + str(round(round(x_accel_start/25)*25)) + " Y" + str(round(round(y_accel_start/25)*25))
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith(";LAYER:"):
|
||||
lines.insert(index+1, m201_limit_new)
|
||||
continue
|
||||
data[num] = "\n".join(lines)
|
||||
|
||||
#Alter any existing jerk lines. Accel lines can be ignored---------------
|
||||
if self.getSettingValueByKey("jerk_enable"):
|
||||
for num in range(start_index,len(data)-1,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("M205") or line.startswith("M566"):
|
||||
lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line)
|
||||
data[num] = "\n".join(lines)
|
||||
data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1]
|
||||
return data
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2023 UltiMaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from ..Script import Script
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2017 Ghostkeeper
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import re #To perform the search and replace.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
|
||||
# This PostProcessingPlugin script is released under the terms of the LGPLv3 or higher.
|
||||
"""
|
||||
Copyright (c) 2017 Christophe Baribaud 2017
|
||||
Python implementation of https://github.com/electrocbd/post_stretch
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from UM.Application import Application
|
||||
|
@ -143,38 +144,44 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
|
||||
def _onFinished(self, job):
|
||||
if self._stream:
|
||||
# Explicitly closing the stream flushes the write-buffer
|
||||
error = job.getError()
|
||||
try:
|
||||
# Explicitly closing the stream flushes the write-buffer
|
||||
self._stream.close()
|
||||
self._stream = None
|
||||
except:
|
||||
Logger.logException("w", "An exception occurred while trying to write to removable drive.")
|
||||
message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(),str(job.getError())),
|
||||
title = catalog.i18nc("@info:title", "Error"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
except Exception as e:
|
||||
if not error:
|
||||
# Only log new error if there was no previous one
|
||||
error = e
|
||||
|
||||
self._stream = None
|
||||
self._writing = False
|
||||
self.writeFinished.emit(self)
|
||||
|
||||
if not error:
|
||||
message = Message(
|
||||
catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(),
|
||||
os.path.basename(
|
||||
job.getFileName())),
|
||||
title=catalog.i18nc("@info:title", "File Saved"),
|
||||
message_type=Message.MessageType.POSITIVE)
|
||||
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject",
|
||||
catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
|
||||
message.actionTriggered.connect(self._onActionTriggered)
|
||||
message.show()
|
||||
self.writeSuccess.emit(self)
|
||||
else:
|
||||
try:
|
||||
os.remove(job.getFileName())
|
||||
except Exception as e:
|
||||
Logger.logException("e", "Exception when trying to remove incomplete exported file %s",
|
||||
str(job.getFileName()))
|
||||
message = Message(catalog.i18nc("@info:status",
|
||||
"Could not save to removable drive {0}: {1}").format(self.getName(),
|
||||
str(job.getError())),
|
||||
title=catalog.i18nc("@info:title", "Error"),
|
||||
message_type=Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.writeError.emit(self)
|
||||
return
|
||||
|
||||
self._writing = False
|
||||
self.writeFinished.emit(self)
|
||||
if job.getResult():
|
||||
message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())),
|
||||
title = catalog.i18nc("@info:title", "File Saved"),
|
||||
message_type = Message.MessageType.POSITIVE)
|
||||
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
|
||||
message.actionTriggered.connect(self._onActionTriggered)
|
||||
message.show()
|
||||
self.writeSuccess.emit(self)
|
||||
else:
|
||||
message = Message(catalog.i18nc("@info:status",
|
||||
"Could not save to removable drive {0}: {1}").format(self.getName(),
|
||||
str(job.getError())),
|
||||
title = catalog.i18nc("@info:title", "Error"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.writeError.emit(self)
|
||||
job.getStream().close()
|
||||
|
||||
def _onActionTriggered(self, message, action):
|
||||
if action == "eject":
|
||||
|
|
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method X.png
Normal file
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method X.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 202 KiB |
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method XL.png
Normal file
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method XL.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 620 KiB |
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method.png
Normal file
BIN
plugins/UM3NetworkPrinting/resources/png/MakerBot Method.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
|
@ -115,7 +115,7 @@ UM.Dialog
|
|||
// Utils
|
||||
function formatPrintJobName(name)
|
||||
{
|
||||
var extensions = [ ".gcode.gz", ".gz", ".gcode", ".ufp" ]
|
||||
var extensions = [ ".gcode.gz", ".gz", ".gcode", ".ufp", ".makerbot" ]
|
||||
for (var i = 0; i < extensions.length; i++)
|
||||
{
|
||||
var extension = extensions[i]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2019 Ultimaker B.V.
|
||||
// Copyright (c) 2023 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
|
@ -6,7 +6,7 @@ import UM 1.3 as UM
|
|||
import Cura 1.0 as Cura
|
||||
|
||||
Item {
|
||||
property var cameraUrl: "";
|
||||
property string cameraUrl: "";
|
||||
|
||||
Rectangle {
|
||||
anchors.fill:parent;
|
||||
|
@ -34,22 +34,29 @@ Item {
|
|||
|
||||
Cura.NetworkMJPGImage {
|
||||
id: cameraImage
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
anchors.verticalCenter: parent.verticalCenter;
|
||||
height: Math.round((imageHeight === 0 ? 600 * screenScaleFactor : imageHeight) * width / imageWidth);
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
readonly property real img_scale_factor: {
|
||||
if (imageWidth > maximumWidth || imageHeight > maximumHeight) {
|
||||
return Math.min(maximumWidth / imageWidth, maximumHeight / imageHeight);
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
width: imageWidth === 0 ? 800 * screenScaleFactor : imageWidth * img_scale_factor
|
||||
height: imageHeight === 0 ? 600 * screenScaleFactor : imageHeight * img_scale_factor
|
||||
|
||||
onVisibleChanged: {
|
||||
if (cameraUrl === "") return;
|
||||
|
||||
if (visible) {
|
||||
if (cameraUrl != "") {
|
||||
start();
|
||||
}
|
||||
start();
|
||||
} else {
|
||||
if (cameraUrl != "") {
|
||||
stop();
|
||||
}
|
||||
stop();
|
||||
}
|
||||
}
|
||||
source: cameraUrl
|
||||
width: Math.min(imageWidth === 0 ? 800 * screenScaleFactor : imageWidth, maximumWidth);
|
||||
z: 1
|
||||
}
|
||||
|
||||
|
|
|
@ -82,13 +82,22 @@ class CloudApiClient:
|
|||
# HACK: There is something weird going on with the API, as it reports printer types in formats like
|
||||
# "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some
|
||||
# conversion!
|
||||
# API points to "MakerBot Method" for a makerbot printertypes which we already changed to allign with other printer_type
|
||||
|
||||
machine_type = machine_type.replace("_plus", "+")
|
||||
machine_type = machine_type.replace("_", " ")
|
||||
machine_type = machine_type.replace("ultimaker", "ultimaker ")
|
||||
machine_type = machine_type.replace(" ", " ")
|
||||
machine_type = machine_type.title()
|
||||
machine_type = urllib.parse.quote_plus(machine_type)
|
||||
method_x = {
|
||||
"ultimaker_method":"MakerBot Method",
|
||||
"ultimaker_methodx":"MakerBot Method X",
|
||||
"ultimaker_methodxl":"MakerBot Method XL"
|
||||
}
|
||||
if machine_type in method_x:
|
||||
machine_type = method_x[machine_type]
|
||||
else:
|
||||
machine_type = machine_type.replace("_plus", "+")
|
||||
machine_type = machine_type.replace("_", " ")
|
||||
machine_type = machine_type.replace("ultimaker", "ultimaker ")
|
||||
machine_type = machine_type.replace(" ", " ")
|
||||
machine_type = machine_type.title()
|
||||
machine_type = urllib.parse.quote_plus(machine_type)
|
||||
url = f"{self.CLUSTER_API_ROOT}/clusters?machine_variant={machine_type}"
|
||||
self._http.get(url,
|
||||
scope=self._scope,
|
||||
|
|
|
@ -58,6 +58,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
|
||||
# The minimum version of firmware that support print job actions over cloud.
|
||||
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12")
|
||||
PRINT_JOB_ACTIONS_MIN_VERSION_METHOD = Version("2.700")
|
||||
|
||||
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
|
||||
# Therefore, we create a private signal used to trigger the printersChanged signal.
|
||||
|
@ -325,8 +326,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
if not self._printers:
|
||||
return False
|
||||
version_number = self.printers[0].firmwareVersion.split(".")
|
||||
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
||||
if len(version_number)> 2:
|
||||
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
||||
else:
|
||||
firmware_version = Version([version_number[0], version_number[1]])
|
||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION_METHOD
|
||||
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def supportsPrintJobQueue(self) -> bool:
|
||||
|
|
|
@ -9,6 +9,7 @@ from PyQt6.QtWidgets import QMessageBox
|
|||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger # To log errors talking to the API.
|
||||
from UM.Message import Message
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Signal import Signal
|
||||
from UM.Util import parseBool
|
||||
|
@ -25,7 +26,7 @@ from .CloudOutputDevice import CloudOutputDevice
|
|||
from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage
|
||||
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||
from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class CloudOutputDeviceManager:
|
||||
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||
|
@ -179,6 +180,13 @@ class CloudOutputDeviceManager:
|
|||
return
|
||||
Logger.log("e", f"Failed writing to specific cloud printer: {unique_id} not in remote clusters.")
|
||||
|
||||
# This message is added so that user knows when the print job was not sent to cloud printer
|
||||
message = Message(catalog.i18nc("@info:status",
|
||||
"Failed writing to specific cloud printer: {0} not in remote clusters.").format(unique_id),
|
||||
title=catalog.i18nc("@info:title", "Error"),
|
||||
message_type=Message.MessageType.ERROR)
|
||||
message.show()
|
||||
|
||||
def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None:
|
||||
"""**Synchronously** create machines for discovered devices
|
||||
|
||||
|
|
|
@ -106,6 +106,10 @@ class MeshFormatHandler:
|
|||
if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"):
|
||||
machine_file_formats = ["application/x-ufp"] + machine_file_formats
|
||||
|
||||
# Exception for makerbot firmware version >=2.700: makerbot is supported
|
||||
elif "application/x-makerbot" not in machine_file_formats and Version(firmware_version >= Version("2.700")):
|
||||
machine_file_formats = ["application/x-makerbot"] + machine_file_formats
|
||||
|
||||
# Take the intersection between file_formats and machine_file_formats.
|
||||
format_by_mimetype = {f["mime_type"]: f for f in file_formats}
|
||||
|
||||
|
|
|
@ -37,24 +37,13 @@ class NewPrinterDetectedMessage(Message):
|
|||
|
||||
def finalize(self, new_devices_added, new_output_devices):
|
||||
self.setProgress(None)
|
||||
num_devices_added = len(new_devices_added)
|
||||
max_disp_devices = 3
|
||||
|
||||
if num_devices_added > max_disp_devices:
|
||||
num_hidden = num_devices_added - max_disp_devices
|
||||
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in
|
||||
new_output_devices[0: max_disp_devices]]
|
||||
device_name_list.append(
|
||||
"<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other",
|
||||
"... and {0} others", num_hidden) + "</li>")
|
||||
device_names = "".join(device_name_list)
|
||||
else:
|
||||
device_names = "".join(
|
||||
["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices_added])
|
||||
|
||||
if new_devices_added:
|
||||
message_text = self.i18n_catalog.i18nc("info:status",
|
||||
"Printers added from Digital Factory:") + f"<ul>{device_names}</ul>"
|
||||
device_names = ""
|
||||
for device in new_devices_added:
|
||||
device_names = device_names + "<li>{} ({})</li>".format(device.name, device.printerTypeName)
|
||||
message_title = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:")
|
||||
message_text = f"{message_title}<ul>{device_names}</ul>"
|
||||
self.setText(message_text)
|
||||
else:
|
||||
self.hide()
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, List
|
||||
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
||||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
|
@ -34,7 +35,7 @@ class CloudClusterResponse(BaseModel):
|
|||
self.host_version = host_version
|
||||
self.host_internal_ip = host_internal_ip
|
||||
self.friendly_name = friendly_name
|
||||
self.printer_type = printer_type
|
||||
self.printer_type = NetworkedPrinterOutputDevice.applyPrinterTypeMapping(printer_type)
|
||||
self.printer_count = printer_count
|
||||
self.capabilities = capabilities if capabilities is not None else []
|
||||
super().__init__(**kwargs)
|
||||
|
@ -51,3 +52,4 @@ class CloudClusterResponse(BaseModel):
|
|||
:return: A human-readable representation of the data in this object.
|
||||
"""
|
||||
return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}})
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ class SendMaterialJob(Job):
|
|||
|
||||
result = {} # type: Dict[str, LocalMaterial]
|
||||
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material")
|
||||
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent.
|
||||
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file") and material.get("visible", True)] # Don't send materials without base_file: The empty material doesn't need to be sent.
|
||||
|
||||
# Find the latest version of all material containers in the registry.
|
||||
for material_metadata in all_base_files:
|
||||
|
|
|
@ -21,20 +21,13 @@ class AutoDetectBaudJob(Job):
|
|||
self._all_baud_rates = [115200, 250000, 500000, 230400, 76800, 57600, 38400, 19200, 9600]
|
||||
|
||||
def run(self) -> None:
|
||||
Logger.log("d", "Auto detect baud rate started.")
|
||||
Logger.debug(f"Auto detect baud rate started for {self._serial_port}")
|
||||
wait_response_timeouts = [3, 15, 30]
|
||||
wait_bootloader_times = [1.5, 5, 15]
|
||||
write_timeout = 3
|
||||
read_timeout = 3
|
||||
tries = 2
|
||||
|
||||
programmer = Stk500v2()
|
||||
serial = None
|
||||
try:
|
||||
programmer.connect(self._serial_port)
|
||||
serial = programmer.leaveISP()
|
||||
except ispBase.IspError:
|
||||
programmer.close()
|
||||
|
||||
for retry in range(tries):
|
||||
for baud_rate in self._all_baud_rates:
|
||||
|
@ -46,8 +39,7 @@ class AutoDetectBaudJob(Job):
|
|||
wait_bootloader = wait_bootloader_times[retry]
|
||||
else:
|
||||
wait_bootloader = wait_bootloader_times[-1]
|
||||
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works. Retry nr: {retry}. Wait timeout: {timeout}".format(
|
||||
serial = self._serial_port, baud_rate = baud_rate, retry = retry, timeout = wait_response_timeout))
|
||||
Logger.debug(f"Checking {self._serial_port} if baud rate {baud_rate} works. Retry nr: {retry}. Wait timeout: {wait_response_timeout}")
|
||||
|
||||
if serial is None:
|
||||
try:
|
||||
|
@ -61,7 +53,9 @@ class AutoDetectBaudJob(Job):
|
|||
serial.baudrate = baud_rate
|
||||
except ValueError:
|
||||
continue
|
||||
sleep(wait_bootloader) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
|
||||
|
||||
# Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
|
||||
sleep(wait_bootloader)
|
||||
|
||||
serial.write(b"\n") # Ensure we clear out previous responses
|
||||
serial.write(b"M105\n")
|
||||
|
@ -83,4 +77,5 @@ class AutoDetectBaudJob(Job):
|
|||
|
||||
serial.write(b"M105\n")
|
||||
sleep(15) # Give the printer some time to init and try again.
|
||||
Logger.debug(f"Unable to find a working baudrate for {serial}")
|
||||
self.setResult(None) # Unable to detect the correct baudrate.
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser
|
||||
from typing import Tuple, List
|
||||
import io
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
import re
|
||||
|
||||
|
||||
|
||||
class VersionUpgrade54to55(VersionUpgrade):
|
||||
profile_regex = re.compile(
|
||||
r"um\_(?P<machine>s(3|5|7))_(?P<core_type>aa|cc|bb)(?P<nozzle_size>0\.(6|4|8))_(?P<material>pla|petg|abs|tough_pla)_(?P<layer_height>0\.\d{1,2}mm)")
|
||||
|
||||
@staticmethod
|
||||
def _isUpgradedUltimakerDefinitionId(definition_id: str) -> bool:
|
||||
if definition_id.startswith("ultimaker_s5"):
|
||||
return True
|
||||
if definition_id.startswith("ultimaker_s3"):
|
||||
return True
|
||||
if definition_id.startswith("ultimaker_s7"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _isBrandedMaterialID(material_id: str) -> bool:
|
||||
return material_id.startswith("ultimaker_")
|
||||
|
||||
@staticmethod
|
||||
def upgradeStack(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 "general" not in parser:
|
||||
parser["general"] = {}
|
||||
|
||||
extruder_definition_id = parser["containers"]["7"]
|
||||
if parser["metadata"]["type"] == "extruder_train" and VersionUpgrade54to55._isUpgradedUltimakerDefinitionId(extruder_definition_id):
|
||||
# We only need to update certain Ultimaker extruder ID's
|
||||
material_id = parser["containers"]["4"]
|
||||
quality_id = parser["containers"]["3"]
|
||||
intent_id = parser["containers"]["2"]
|
||||
if VersionUpgrade54to55._isBrandedMaterialID(material_id):
|
||||
# We have an Ultimaker branded material ID, so we should change the intent & quality!
|
||||
|
||||
quality_id = VersionUpgrade54to55.profile_regex.sub(
|
||||
r"um_\g<machine>_\g<core_type>\g<nozzle_size>_um-\g<material>_\g<layer_height>", quality_id)
|
||||
|
||||
|
||||
intent_id = VersionUpgrade54to55.profile_regex.sub(
|
||||
r"um_\g<machine>_\g<core_type>\g<nozzle_size>_um-\g<material>_\g<layer_height>", intent_id)
|
||||
|
||||
parser["containers"]["3"] = quality_id
|
||||
parser["containers"]["2"] = intent_id
|
||||
|
||||
# We're not changing any settings, but we are changing how certain stacks are handled.
|
||||
parser["general"]["version"] = "6"
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
35
plugins/VersionUpgrade/VersionUpgrade54to55/__init__.py
Normal file
35
plugins/VersionUpgrade/VersionUpgrade54to55/__init__.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, Dict, TYPE_CHECKING
|
||||
|
||||
from . import VersionUpgrade54to55
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Application import Application
|
||||
|
||||
upgrade = VersionUpgrade54to55.VersionUpgrade54to55()
|
||||
|
||||
|
||||
def getMetaData() -> Dict[str, Any]:
|
||||
return {
|
||||
"version_upgrade": {
|
||||
# From To Upgrade function
|
||||
("machine_stack", 5000022): ("machine_stack", 6000022, upgrade.upgradeStack),
|
||||
("extruder_train", 5000022): ("extruder_train", 6000022, upgrade.upgradeStack),
|
||||
},
|
||||
"sources": {
|
||||
"machine_stack": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./machine_instances"}
|
||||
},
|
||||
"extruder_train": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./extruders"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def register(app: "Application") -> Dict[str, Any]:
|
||||
return {"version_upgrade": upgrade}
|
8
plugins/VersionUpgrade/VersionUpgrade54to55/plugin.json
Normal file
8
plugins/VersionUpgrade/VersionUpgrade54to55/plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Version Upgrade 5.4 to 5.5",
|
||||
"author": "UltiMaker",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 5.4 to Cura 5.5.",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
|
@ -910,6 +910,9 @@ 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
|
||||
|
|
|
@ -9,5 +9,14 @@
|
|||
"Ultimaker Original": "ultimaker_original",
|
||||
"Ultimaker Original+": "ultimaker_original_plus",
|
||||
"Ultimaker Original Dual Extrusion": "ultimaker_original_dual",
|
||||
"IMADE3D JellyBOX": "imade3d_jellybox"
|
||||
}
|
||||
"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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue