mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 14:37:29 -06:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
4bd1c4c4e0
7956 changed files with 237740 additions and 241926 deletions
62
plugins/3MFReader/SpecificSettingsModel.py
Normal file
62
plugins/3MFReader/SpecificSettingsModel.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Copyright (c) 2024 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.SettingDefinition import SettingDefinition
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
class SpecificSettingsModel(ListModel):
|
||||
CategoryRole = Qt.ItemDataRole.UserRole + 1
|
||||
LabelRole = Qt.ItemDataRole.UserRole + 2
|
||||
ValueRole = Qt.ItemDataRole.UserRole + 3
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent = parent)
|
||||
self.addRoleName(self.CategoryRole, "category")
|
||||
self.addRoleName(self.LabelRole, "label")
|
||||
self.addRoleName(self.ValueRole, "value")
|
||||
|
||||
self._settings_catalog = i18nCatalog("fdmprinter.def.json")
|
||||
self._update()
|
||||
|
||||
modelChanged = pyqtSignal()
|
||||
|
||||
|
||||
def addSettingsFromStack(self, stack, category, settings):
|
||||
for setting, value in settings.items():
|
||||
unit = stack.getProperty(setting, "unit")
|
||||
|
||||
setting_type = stack.getProperty(setting, "type")
|
||||
if setting_type is not None:
|
||||
# This is not very good looking, but will do for now
|
||||
value = str(SettingDefinition.settingValueToString(setting_type, value))
|
||||
if unit:
|
||||
value += " " + str(unit)
|
||||
if setting_type == "enum":
|
||||
options = stack.getProperty(setting, "options")
|
||||
value_msgctxt = f"{str(setting)} option {str(value)}"
|
||||
value_msgid = options[stack.getProperty(setting, "value")]
|
||||
value = self._settings_catalog.i18nc(value_msgctxt, value_msgid)
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
label_msgctxt = f"{str(setting)} label"
|
||||
label_msgid = stack.getProperty(setting, "label")
|
||||
label = self._settings_catalog.i18nc(label_msgctxt, label_msgid)
|
||||
|
||||
self.appendItem({
|
||||
"category": category,
|
||||
"label": label,
|
||||
"value": value
|
||||
})
|
||||
self.modelChanged.emit()
|
||||
|
||||
def _update(self):
|
||||
Logger.debug(f"Updating {self.__class__.__name__}")
|
||||
self.setItems([])
|
||||
self.modelChanged.emit()
|
||||
return
|
|
@ -16,6 +16,7 @@ from UM.Mesh.MeshReader import MeshReader
|
|||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
from UM.Scene.SceneNode import SceneNode # For typing.
|
||||
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
|
@ -41,7 +42,7 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
comment="3MF",
|
||||
suffixes=["3mf"]
|
||||
)
|
||||
|
@ -56,7 +57,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 +92,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 +122,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 +141,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)
|
||||
|
||||
|
@ -175,6 +178,12 @@ class ThreeMFReader(MeshReader):
|
|||
else:
|
||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||
continue
|
||||
if key == "print_order":
|
||||
um_node.printOrder = int(setting_value)
|
||||
continue
|
||||
if key =="drop_to_buildplate":
|
||||
um_node.setSetting(SceneNodeSettings.AutoDropDown, eval(setting_value))
|
||||
continue
|
||||
if key in known_setting_keys:
|
||||
setting_container.setProperty(key, "value", setting_value)
|
||||
else:
|
||||
|
@ -184,6 +193,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 +230,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 +316,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
|
||||
|
|
|
@ -5,10 +5,13 @@ from configparser import ConfigParser
|
|||
import zipfile
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from typing import cast, Dict, List, Optional, Tuple, Any, Set
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Util import parseBool
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Application import Application
|
||||
|
@ -43,7 +46,7 @@ from .WorkspaceDialog import WorkspaceDialog
|
|||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
_ignored_machine_network_metadata = {
|
||||
_ignored_machine_network_metadata: Set[str] = {
|
||||
"um_cloud_cluster_id",
|
||||
"um_network_key",
|
||||
"um_linked_to_account",
|
||||
|
@ -55,8 +58,9 @@ _ignored_machine_network_metadata = {
|
|||
"capabilities",
|
||||
"octoprint_api_key",
|
||||
"is_abstract_machine"
|
||||
} # type: Set[str]
|
||||
}
|
||||
|
||||
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
||||
|
||||
class ContainerInfo:
|
||||
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
|
||||
|
@ -69,41 +73,41 @@ class ContainerInfo:
|
|||
|
||||
class QualityChangesInfo:
|
||||
def __init__(self) -> None:
|
||||
self.name = None
|
||||
self.name: Optional[str] = None
|
||||
self.global_info = None
|
||||
self.extruder_info_dict = {} # type: Dict[str, ContainerInfo]
|
||||
self.extruder_info_dict: Dict[str, ContainerInfo] = {}
|
||||
|
||||
|
||||
class MachineInfo:
|
||||
def __init__(self) -> None:
|
||||
self.container_id = None
|
||||
self.name = None
|
||||
self.definition_id = None
|
||||
self.container_id: Optional[str] = None
|
||||
self.name: Optional[str] = None
|
||||
self.definition_id: Optional[str] = None
|
||||
|
||||
self.metadata_dict = {} # type: Dict[str, str]
|
||||
self.metadata_dict: Dict[str, str] = {}
|
||||
|
||||
self.quality_type = None
|
||||
self.intent_category = None
|
||||
self.custom_quality_name = None
|
||||
self.quality_changes_info = None
|
||||
self.variant_info = None
|
||||
self.quality_type: Optional[str] = None
|
||||
self.intent_category: Optional[str] = None
|
||||
self.custom_quality_name: Optional[str] = None
|
||||
self.quality_changes_info: Optional[QualityChangesInfo] = None
|
||||
self.variant_info: Optional[ContainerInfo] = None
|
||||
|
||||
self.definition_changes_info = None
|
||||
self.user_changes_info = None
|
||||
self.definition_changes_info: Optional[ContainerInfo] = None
|
||||
self.user_changes_info: Optional[ContainerInfo] = None
|
||||
|
||||
self.extruder_info_dict = {} # type: Dict[str, ExtruderInfo]
|
||||
self.extruder_info_dict: Dict[str, str] = {}
|
||||
|
||||
|
||||
class ExtruderInfo:
|
||||
def __init__(self) -> None:
|
||||
self.position = None
|
||||
self.enabled = True
|
||||
self.variant_info = None
|
||||
self.root_material_id = None
|
||||
self.variant_info: Optional[ContainerInfo] = None
|
||||
self.root_material_id: Optional[str] = None
|
||||
|
||||
self.definition_changes_info = None
|
||||
self.user_changes_info = None
|
||||
self.intent_info = None
|
||||
self.definition_changes_info: Optional[ContainerInfo] = None
|
||||
self.user_changes_info: Optional[ContainerInfo] = None
|
||||
self.intent_info: Optional[ContainerInfo] = None
|
||||
|
||||
|
||||
class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
|
@ -115,6 +119,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._supported_extensions = [".3mf"]
|
||||
self._dialog = WorkspaceDialog()
|
||||
self._3mf_mesh_reader = None
|
||||
self._is_ucp = None
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
# suffixes registered with the MimeTypes don't start with a dot '.'
|
||||
|
@ -131,20 +136,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
# - variant
|
||||
self._ignored_instance_container_types = {"quality", "variant"}
|
||||
|
||||
self._resolve_strategies = {} # type: Dict[str, str]
|
||||
self._resolve_strategies: Dict[str, str] = {}
|
||||
|
||||
self._id_mapping = {} # type: Dict[str, str]
|
||||
self._id_mapping: Dict[str, str] = {}
|
||||
|
||||
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
|
||||
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
|
||||
|
||||
self._old_new_materials = {} # type: Dict[str, str]
|
||||
self._old_new_materials: Dict[str, str] = {}
|
||||
self._machine_info = None
|
||||
|
||||
self._user_settings: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def _clearState(self):
|
||||
self._id_mapping = {}
|
||||
self._old_new_materials = {}
|
||||
self._machine_info = None
|
||||
self._user_settings = {}
|
||||
|
||||
def clearOpenAsUcp(self):
|
||||
self._is_ucp = None
|
||||
|
||||
def getNewId(self, old_id: str):
|
||||
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
|
@ -200,6 +211,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
return global_stack_file_list[0], extruder_stack_file_list
|
||||
|
||||
def _isProjectUcp(self, file_name) -> bool:
|
||||
if self._is_ucp == None:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
self._is_ucp =True if USER_SETTINGS_PATH in cura_file_names else False
|
||||
|
||||
def getIsProjectUcp(self) -> bool:
|
||||
return self._is_ucp
|
||||
|
||||
|
||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||
"""Read some info so we can make decisions
|
||||
|
||||
|
@ -208,7 +229,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
we don't want to show a dialog.
|
||||
"""
|
||||
self._clearState()
|
||||
|
||||
self._isProjectUcp(file_name)
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
||||
pass
|
||||
|
@ -228,11 +249,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
|
||||
containers_found_dict = {k: False for k in resolve_strategy_keys}
|
||||
|
||||
# Check whether the file is a UCP, which changes some import options
|
||||
is_ucp = USER_SETTINGS_PATH in cura_file_names
|
||||
|
||||
#
|
||||
# Read definition containers
|
||||
#
|
||||
machine_definition_id = None
|
||||
updatable_machines = []
|
||||
updatable_machines = None if self._is_ucp else []
|
||||
machine_definition_container_count = 0
|
||||
extruder_definition_container_count = 0
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
|
@ -250,7 +274,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if definition_container_type == "machine":
|
||||
machine_definition_id = container_id
|
||||
machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
|
||||
if machine_definition_containers:
|
||||
if machine_definition_containers and updatable_machines is not None:
|
||||
updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
|
||||
machine_type = definition_container["name"]
|
||||
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
||||
|
@ -461,11 +485,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
materials_in_extruders_dict = {} # Which material is in which extruder
|
||||
|
||||
# if the global stack is found, we check if there are conflicts in the extruder stacks
|
||||
# If the global stack is found, we check if there are conflicts in the extruder stacks
|
||||
for extruder_stack_file in extruder_stack_files:
|
||||
serialized = archive.open(extruder_stack_file).read().decode("utf-8")
|
||||
|
||||
not_upgraded_parser = ConfigParser(interpolation=None)
|
||||
not_upgraded_parser.read_string(serialized)
|
||||
|
||||
serialized = ExtruderStack._updateSerialized(serialized, extruder_stack_file)
|
||||
parser = ConfigParser(interpolation = None)
|
||||
parser = ConfigParser(interpolation=None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
# The check should be done for the extruder stack that's associated with the existing global stack,
|
||||
|
@ -497,19 +525,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
|
||||
self._machine_info.extruder_info_dict[position] = extruder_info
|
||||
|
||||
intent_container_id = parser["containers"][str(_ContainerIndexes.Intent)]
|
||||
|
||||
intent_id = parser["containers"][str(_ContainerIndexes.Intent)]
|
||||
if intent_id not in ("empty", "empty_intent"):
|
||||
extruder_info.intent_info = instance_container_info_dict[intent_id]
|
||||
if intent_container_id in instance_container_info_dict:
|
||||
extruder_info.intent_info = instance_container_info_dict[intent_id]
|
||||
else:
|
||||
# It can happen that an intent has been renamed. In that case, we should still use the old
|
||||
# name, since we used that to generate the instance_container_info_dict keys.
|
||||
extruder_info.intent_info = instance_container_info_dict[not_upgraded_parser["containers"][str(_ContainerIndexes.Intent)]]
|
||||
|
||||
if not machine_conflict and containers_found_dict["machine"] and global_stack:
|
||||
if int(position) >= len(global_stack.extruderList):
|
||||
continue
|
||||
|
||||
existing_extruder_stack = global_stack.extruderList[int(position)]
|
||||
# check if there are any changes at all in any of the container stacks.
|
||||
# Check if there are any changes at all in any of the container stacks.
|
||||
id_list = self._getContainerIdListFromSerialized(serialized)
|
||||
for index, container_id in enumerate(id_list):
|
||||
# take into account the old empty container IDs
|
||||
# Take into account the old empty container IDs
|
||||
container_id = self._old_empty_profile_id_dict.get(container_id, container_id)
|
||||
if existing_extruder_stack.getContainer(index).getId() != container_id:
|
||||
machine_conflict = True
|
||||
|
@ -586,6 +621,39 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
package_metadata = self._parse_packages_metadata(archive)
|
||||
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
|
||||
|
||||
# Load the user specifically exported settings
|
||||
self._dialog.exportedSettingModel.clear()
|
||||
self._dialog.setCurrentMachineName("")
|
||||
if self._is_ucp:
|
||||
try:
|
||||
self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8"))
|
||||
any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
||||
actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
self._dialog.setCurrentMachineName(actual_global_stack.id)
|
||||
|
||||
for stack_name, settings in self._user_settings.items():
|
||||
if stack_name == 'global':
|
||||
self._dialog.exportedSettingModel.addSettingsFromStack(actual_global_stack, i18n_catalog.i18nc("@label", "Global"), settings)
|
||||
else:
|
||||
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
|
||||
if extruder_match is not None:
|
||||
extruder_nr = int(extruder_match.group(1))
|
||||
self._dialog.exportedSettingModel.addSettingsFromStack(any_extruder_stack,
|
||||
i18n_catalog.i18nc("@label",
|
||||
"Extruder {0}", extruder_nr + 1),
|
||||
settings)
|
||||
except KeyError as e:
|
||||
# If there is no user settings file, it's not a UCP, so notify user of failure.
|
||||
Logger.log("w", "File %s is not a valid UCP.", file_name)
|
||||
message = Message(
|
||||
i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
|
||||
file_name, str(e)),
|
||||
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type=Message.MessageType.ERROR)
|
||||
message.show()
|
||||
return WorkspaceReader.PreReadResult.failed
|
||||
|
||||
# Show the dialog, informing the user what is about to happen.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setIsPrinterGroup(is_printer_group)
|
||||
|
@ -595,7 +663,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)
|
||||
|
@ -606,12 +674,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._dialog.setVariantType(variant_type_name)
|
||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
||||
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
||||
self._dialog.setAllowCreatemachine(not self._is_ucp)
|
||||
self._dialog.setIsUcp(self._is_ucp)
|
||||
self._dialog.show()
|
||||
|
||||
|
||||
# Choosing the initially selected printer in MachineSelector
|
||||
is_networked_machine = False
|
||||
is_abstract_machine = False
|
||||
if global_stack and isinstance(global_stack, GlobalStack):
|
||||
if global_stack and isinstance(global_stack, GlobalStack) and not self._is_ucp:
|
||||
# The machine included in the project file exists locally already, no need to change selected printers.
|
||||
is_networked_machine = global_stack.hasNetworkedConnection()
|
||||
is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False))
|
||||
|
@ -620,7 +691,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
elif self._dialog.updatableMachinesModel.count > 0:
|
||||
# The machine included in the project file does not exist. There is another machine of the same type.
|
||||
# This will always default to an abstract machine first.
|
||||
machine = self._dialog.updatableMachinesModel.getItem(0)
|
||||
machine = self._dialog.updatableMachinesModel.getItem(self._dialog.currentMachinePositionIndex)
|
||||
machine_name = machine["name"]
|
||||
is_networked_machine = machine["isNetworked"]
|
||||
is_abstract_machine = machine["isAbstractMachine"]
|
||||
|
@ -637,6 +708,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._dialog.setIsNetworkedMachine(is_networked_machine)
|
||||
self._dialog.setIsAbstractMachine(is_abstract_machine)
|
||||
self._dialog.setMachineName(machine_name)
|
||||
self._dialog.updateCompatibleMachine()
|
||||
|
||||
# Block until the dialog is closed.
|
||||
self._dialog.waitForClose()
|
||||
|
@ -658,7 +730,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
if key not in containers_found_dict or strategy is not None:
|
||||
continue
|
||||
self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new"
|
||||
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
@call_on_qt_thread
|
||||
|
@ -679,16 +750,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
except EnvironmentError as e:
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||
"Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
|
||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
except zipfile.BadZipFile as e:
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
|
||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
|
@ -740,7 +811,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
# quality_changes file. If that's the case, take the extruder count into account when creating the machine
|
||||
# or else the extruderList will return only the first extruder, leading to missing non-global settings in
|
||||
# the other extruders.
|
||||
machine_extruder_count = self._getMachineExtruderCount() # type: Optional[int]
|
||||
machine_extruder_count: Optional[int] = self._getMachineExtruderCount()
|
||||
global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id, machine_extruder_count)
|
||||
if global_stack: # Only switch if creating the machine was successful.
|
||||
extruder_stack_dict = {str(position): extruder for position, extruder in enumerate(global_stack.extruderList)}
|
||||
|
@ -750,10 +821,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
# Find the machine which will be overridden
|
||||
global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
|
||||
if not global_stacks:
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
|
||||
"Project file <filename>{0}</filename> is made using profiles that"
|
||||
" are unknown to this version of Ultimaker Cura.", file_name),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
|
||||
"Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
|
@ -767,84 +837,86 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
for stack in extruder_stacks:
|
||||
stack.setNextStack(global_stack, connect_signals = False)
|
||||
|
||||
Logger.log("d", "Workspace loading is checking definitions...")
|
||||
# Get all the definition files & check if they exist. If not, add them.
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
for definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(definition_container_file)
|
||||
if not self._is_ucp:
|
||||
Logger.log("d", "Workspace loading is checking definitions...")
|
||||
# Get all the definition files & check if they exist. If not, add them.
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
for definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(definition_container_file)
|
||||
|
||||
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
||||
if not definitions:
|
||||
definition_container = DefinitionContainer(container_id)
|
||||
try:
|
||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
||||
file_name = definition_container_file)
|
||||
except ContainerFormatError:
|
||||
# We cannot just skip the definition file because everything else later will just break if the
|
||||
# machine definition cannot be found.
|
||||
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
|
||||
definition_container_file, file_name)
|
||||
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
|
||||
self._container_registry.addContainer(definition_container)
|
||||
Job.yieldThread()
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
Logger.log("d", "Workspace loading is checking materials...")
|
||||
# Get all the material files and check if they exist. If not, add them.
|
||||
xml_material_profile = self._getXmlProfileClass()
|
||||
if self._material_container_suffix is None:
|
||||
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
|
||||
if xml_material_profile:
|
||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||
for material_container_file in material_container_files:
|
||||
to_deserialize_material = False
|
||||
container_id = self._stripFileToId(material_container_file)
|
||||
need_new_name = False
|
||||
materials = self._container_registry.findInstanceContainers(id = container_id)
|
||||
|
||||
if not materials:
|
||||
# No material found, deserialize this material later and add it
|
||||
to_deserialize_material = True
|
||||
else:
|
||||
material_container = materials[0]
|
||||
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
||||
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
||||
to_deserialize_material = True
|
||||
|
||||
if self._resolve_strategies["material"] == "override":
|
||||
# Remove the old materials and then deserialize the one from the project
|
||||
root_material_id = material_container.getMetaDataEntry("base_file")
|
||||
application.getContainerRegistry().removeContainer(root_material_id)
|
||||
elif self._resolve_strategies["material"] == "new":
|
||||
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
||||
# auto created & added.
|
||||
container_id = self.getNewId(container_id)
|
||||
self._old_new_materials[old_material_root_id] = container_id
|
||||
need_new_name = True
|
||||
|
||||
if to_deserialize_material:
|
||||
material_container = xml_material_profile(container_id)
|
||||
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
|
||||
if not definitions:
|
||||
definition_container = DefinitionContainer(container_id)
|
||||
try:
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
||||
file_name = container_id + "." + self._material_container_suffix)
|
||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
|
||||
file_name = definition_container_file)
|
||||
except ContainerFormatError:
|
||||
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
|
||||
material_container_file, file_name)
|
||||
continue
|
||||
if need_new_name:
|
||||
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
|
||||
material_container.setName(new_name)
|
||||
material_container.setDirty(True)
|
||||
self._container_registry.addContainer(material_container)
|
||||
# We cannot just skip the definition file because everything else later will just break if the
|
||||
# machine definition cannot be found.
|
||||
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
|
||||
definition_container_file, file_name)
|
||||
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
|
||||
self._container_registry.addContainer(definition_container)
|
||||
Job.yieldThread()
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
if global_stack:
|
||||
# Handle quality changes if any
|
||||
self._processQualityChanges(global_stack)
|
||||
Logger.log("d", "Workspace loading is checking materials...")
|
||||
# Get all the material files and check if they exist. If not, add them.
|
||||
xml_material_profile = self._getXmlProfileClass()
|
||||
if self._material_container_suffix is None:
|
||||
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
|
||||
if xml_material_profile:
|
||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||
for material_container_file in material_container_files:
|
||||
to_deserialize_material = False
|
||||
container_id = self._stripFileToId(material_container_file)
|
||||
need_new_name = False
|
||||
materials = self._container_registry.findInstanceContainers(id = container_id)
|
||||
|
||||
# Prepare the machine
|
||||
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
||||
if not materials:
|
||||
# No material found, deserialize this material later and add it
|
||||
to_deserialize_material = True
|
||||
else:
|
||||
material_container = materials[0]
|
||||
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
||||
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
||||
to_deserialize_material = True
|
||||
|
||||
if self._resolve_strategies["material"] == "override":
|
||||
# Remove the old materials and then deserialize the one from the project
|
||||
root_material_id = material_container.getMetaDataEntry("base_file")
|
||||
application.getContainerRegistry().removeContainer(root_material_id)
|
||||
elif self._resolve_strategies["material"] == "new":
|
||||
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
||||
# auto created & added.
|
||||
container_id = self.getNewId(container_id)
|
||||
self._old_new_materials[old_material_root_id] = container_id
|
||||
need_new_name = True
|
||||
|
||||
if to_deserialize_material:
|
||||
material_container = xml_material_profile(container_id)
|
||||
try:
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
|
||||
file_name = container_id + "." + self._material_container_suffix)
|
||||
except ContainerFormatError:
|
||||
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
|
||||
material_container_file, file_name)
|
||||
continue
|
||||
if need_new_name:
|
||||
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
|
||||
material_container.setName(new_name)
|
||||
material_container.setDirty(True)
|
||||
self._container_registry.addContainer(material_container)
|
||||
Job.yieldThread()
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
if global_stack:
|
||||
if not self._is_ucp:
|
||||
# Handle quality changes if any
|
||||
self._processQualityChanges(global_stack)
|
||||
|
||||
# Prepare the machine
|
||||
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
||||
|
||||
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
||||
# Actually change the active machine.
|
||||
|
@ -854,21 +926,45 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
|
||||
# they won't take effect until this function is done.
|
||||
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
|
||||
|
||||
self._updateActiveMachine(global_stack)
|
||||
|
||||
if self._is_ucp:
|
||||
# Now we have switched, apply the user settings
|
||||
self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings)
|
||||
|
||||
# Load all the nodes / mesh data of the workspace
|
||||
nodes = self._3mf_mesh_reader.read(file_name)
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
|
||||
if self._is_ucp:
|
||||
# We might be on a different printer than the one this project was made on.
|
||||
# The offset to the printers' center isn't saved; instead, try to just fit everything on the buildplate.
|
||||
full_extents = None
|
||||
for node in nodes:
|
||||
extents = node.getMeshData().getExtents() if node.getMeshData() else None
|
||||
if extents is not None:
|
||||
pos = node.getPosition()
|
||||
node_box = AxisAlignedBox(extents.minimum + pos, extents.maximum + pos)
|
||||
if full_extents is None:
|
||||
full_extents = node_box
|
||||
else:
|
||||
full_extents = full_extents + node_box
|
||||
if full_extents and full_extents.isValid():
|
||||
for node in nodes:
|
||||
pos = node.getPosition()
|
||||
node.setPosition(Vector(pos.x - full_extents.center.x, pos.y, pos.z - full_extents.center.z))
|
||||
|
||||
base_file_name = os.path.basename(file_name)
|
||||
self.setWorkspaceName(base_file_name)
|
||||
|
||||
self._is_ucp = None
|
||||
return nodes, self._loadMetadata(file_name)
|
||||
|
||||
@staticmethod
|
||||
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
|
||||
result = dict() # type: Dict[str, Dict[str, Any]]
|
||||
result: Dict[str, Dict[str, Any]] = dict()
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
except zipfile.BadZipFile:
|
||||
|
@ -880,7 +976,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
|
||||
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
plugin_id = metadata_file.split("/")[0]
|
||||
|
@ -921,7 +1016,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
quality_changes_name = self._container_registry.uniqueName(quality_changes_name)
|
||||
for position, container_info in container_info_dict.items():
|
||||
extruder_stack = None
|
||||
intent_category = None # type: Optional[str]
|
||||
intent_category: Optional[str] = None
|
||||
if position is not None:
|
||||
try:
|
||||
extruder_stack = global_stack.extruderList[int(position)]
|
||||
|
@ -1086,7 +1181,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:
|
||||
|
@ -1100,7 +1196,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 = {}
|
||||
|
@ -1110,7 +1207,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:
|
||||
|
@ -1124,7 +1222,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()]
|
||||
|
@ -1146,7 +1245,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
node = machine_node.variants.get(machine_node.preferred_variant_name, next(iter(machine_node.variants.values())))
|
||||
else:
|
||||
variant_name = extruder_info.variant_info.parser["general"]["name"]
|
||||
node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[variant_name]
|
||||
node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants.get(variant_name, next(iter(machine_node.variants.values())))
|
||||
extruder_stack.variant = node.container
|
||||
|
||||
def _applyMaterials(self, global_stack, extruder_stack_dict):
|
||||
|
@ -1161,24 +1260,50 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
root_material_id = extruder_info.root_material_id
|
||||
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
|
||||
|
||||
material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
|
||||
extruder_stack.material = material_node.container # type: InstanceContainer
|
||||
available_materials = machine_node.variants[extruder_stack.variant.getName()].materials
|
||||
if root_material_id not in available_materials:
|
||||
continue
|
||||
material_node = available_materials[root_material_id]
|
||||
extruder_stack.material = material_node.container
|
||||
|
||||
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
|
||||
# Clear all first
|
||||
def _clearMachineSettings(self, global_stack, extruder_stack_dict):
|
||||
self._clearStack(global_stack)
|
||||
for extruder_stack in extruder_stack_dict.values():
|
||||
self._clearStack(extruder_stack)
|
||||
|
||||
self._quality_changes_to_apply = None
|
||||
self._quality_type_to_apply = None
|
||||
self._intent_category_to_apply = None
|
||||
self._user_settings_to_apply = None
|
||||
|
||||
def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
|
||||
for stack_name, settings in user_settings.items():
|
||||
if stack_name == 'global':
|
||||
ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
|
||||
else:
|
||||
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
|
||||
if extruder_match is not None:
|
||||
extruder_nr = extruder_match.group(1)
|
||||
if extruder_nr in extruder_stack_dict:
|
||||
ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
|
||||
|
||||
@staticmethod
|
||||
def _applyUserSettingsOnStack(stack, user_settings):
|
||||
user_settings_container = stack.userChanges
|
||||
|
||||
for setting_to_import, setting_value in user_settings.items():
|
||||
user_settings_container.setProperty(setting_to_import, 'value', setting_value)
|
||||
|
||||
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
|
||||
# Clear all first
|
||||
self._clearMachineSettings(global_stack, extruder_stack_dict)
|
||||
|
||||
self._applyDefinitionChanges(global_stack, extruder_stack_dict)
|
||||
self._applyUserChanges(global_stack, extruder_stack_dict)
|
||||
self._applyVariants(global_stack, extruder_stack_dict)
|
||||
self._applyMaterials(global_stack, extruder_stack_dict)
|
||||
|
||||
# prepare the quality to select
|
||||
self._quality_changes_to_apply = None
|
||||
self._quality_type_to_apply = None
|
||||
self._intent_category_to_apply = None
|
||||
if self._machine_info.quality_changes_info is not None:
|
||||
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
|
||||
else:
|
||||
|
@ -1199,6 +1324,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()
|
||||
|
@ -1207,37 +1341,40 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
machine_manager.setActiveMachine(global_stack.getId())
|
||||
|
||||
# Set metadata fields that are missing from the global stack
|
||||
for key, value in self._machine_info.metadata_dict.items():
|
||||
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
if not self._is_ucp:
|
||||
for key, value in self._machine_info.metadata_dict.items():
|
||||
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
|
||||
if self._quality_changes_to_apply:
|
||||
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
|
||||
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
|
||||
if not quality_changes_group:
|
||||
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
|
||||
return
|
||||
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
|
||||
else:
|
||||
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
|
||||
quality_group_dict = container_tree.getCurrentQualityGroups()
|
||||
if self._quality_type_to_apply in quality_group_dict:
|
||||
quality_group = quality_group_dict[self._quality_type_to_apply]
|
||||
if self._quality_changes_to_apply !=None:
|
||||
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
|
||||
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
|
||||
if not quality_changes_group:
|
||||
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
|
||||
return
|
||||
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
|
||||
else:
|
||||
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
|
||||
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
|
||||
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||
if quality_group is None:
|
||||
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
|
||||
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
|
||||
quality_group_dict = container_tree.getCurrentQualityGroups()
|
||||
if self._quality_type_to_apply in quality_group_dict:
|
||||
quality_group = quality_group_dict[self._quality_type_to_apply]
|
||||
else:
|
||||
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
|
||||
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
|
||||
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||
if quality_group is None:
|
||||
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
|
||||
|
||||
if quality_group is not None:
|
||||
machine_manager.setQualityGroup(quality_group, no_dialog = True)
|
||||
|
||||
# Also apply intent if available
|
||||
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)
|
||||
if quality_group is not None:
|
||||
machine_manager.setQualityGroup(quality_group, no_dialog = True)
|
||||
|
||||
# Also apply intent if available
|
||||
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())
|
||||
|
||||
|
@ -1318,3 +1455,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
|
||||
|
@ -21,6 +22,8 @@ import time
|
|||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .SpecificSettingsModel import SpecificSettingsModel
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
|
@ -35,10 +38,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)
|
||||
|
@ -58,16 +63,23 @@ class WorkspaceDialog(QObject):
|
|||
self._machine_name = ""
|
||||
self._machine_type = ""
|
||||
self._variant_type = ""
|
||||
self._current_machine_name = ""
|
||||
self._material_labels = []
|
||||
self._extruders = []
|
||||
self._objects_on_plate = False
|
||||
self._is_printer_group = False
|
||||
self._updatable_machines_model = MachineListModel(self, listenToChanges=False)
|
||||
self._updatable_machines_model = MachineListModel(self, listenToChanges = False, showCloudPrinters = True)
|
||||
self._missing_package_metadata: List[Dict[str, str]] = []
|
||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||
self._install_missing_package_dialog: Optional[QObject] = None
|
||||
self._is_abstract_machine = False
|
||||
self._is_networked_machine = False
|
||||
self._is_compatible_machine = False
|
||||
self._allow_create_machine = True
|
||||
self._exported_settings_model = SpecificSettingsModel()
|
||||
self._exported_settings_model.modelChanged.connect(self.exportedSettingModelChanged.emit)
|
||||
self._current_machine_pos_index = 0
|
||||
self._is_ucp = False
|
||||
|
||||
machineConflictChanged = pyqtSignal()
|
||||
qualityChangesConflictChanged = pyqtSignal()
|
||||
|
@ -91,6 +103,9 @@ class WorkspaceDialog(QObject):
|
|||
extrudersChanged = pyqtSignal()
|
||||
isPrinterGroupChanged = pyqtSignal()
|
||||
missingPackagesChanged = pyqtSignal()
|
||||
isCompatibleMachineChanged = pyqtSignal()
|
||||
isUcpChanged = pyqtSignal()
|
||||
exportedSettingModelChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||
def isPrinterGroup(self) -> bool:
|
||||
|
@ -163,8 +178,30 @@ class WorkspaceDialog(QObject):
|
|||
self._machine_name = machine_name
|
||||
self.machineNameChanged.emit()
|
||||
|
||||
def setCurrentMachineName(self, machine: str) -> None:
|
||||
self._current_machine_name = machine
|
||||
|
||||
@pyqtProperty(str, notify = machineNameChanged)
|
||||
def currentMachineName(self) -> str:
|
||||
return self._current_machine_name
|
||||
|
||||
@staticmethod
|
||||
def getIndexOfCurrentMachine(list_of_dicts, key, value, defaultIndex):
|
||||
for i, d in enumerate(list_of_dicts):
|
||||
if d.get(key) == value: # found the dictionary
|
||||
return i
|
||||
return defaultIndex
|
||||
|
||||
@pyqtProperty(int, notify = machineNameChanged)
|
||||
def currentMachinePositionIndex(self):
|
||||
return self._current_machine_pos_index
|
||||
|
||||
@pyqtProperty(QObject, notify = updatableMachinesChanged)
|
||||
def updatableMachinesModel(self) -> MachineListModel:
|
||||
if self._current_machine_name != "":
|
||||
self._current_machine_pos_index = self.getIndexOfCurrentMachine(self._updatable_machines_model.getItems(), "id", self._current_machine_name, defaultIndex = 0)
|
||||
else:
|
||||
self._current_machine_pos_index = 0
|
||||
return cast(MachineListModel, self._updatable_machines_model)
|
||||
|
||||
def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None:
|
||||
|
@ -221,7 +258,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)
|
||||
|
@ -282,7 +326,49 @@ class WorkspaceDialog(QObject):
|
|||
@pyqtSlot(str)
|
||||
def setMachineToOverride(self, machine_name: str) -> None:
|
||||
self._override_machine = machine_name
|
||||
self.updateCompatibleMachine()
|
||||
|
||||
def updateCompatibleMachine(self):
|
||||
registry = ContainerRegistry.getInstance()
|
||||
containers_expected = registry.findDefinitionContainers(name=self._machine_type)
|
||||
containers_selected = registry.findContainerStacks(id=self._override_machine)
|
||||
if len(containers_expected) == 1 and len(containers_selected) == 1:
|
||||
new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
|
||||
if new_compatible_machine != self._is_compatible_machine:
|
||||
self._is_compatible_machine = new_compatible_machine
|
||||
self.isCompatibleMachineChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = isCompatibleMachineChanged)
|
||||
def isCompatibleMachine(self) -> bool:
|
||||
return self._is_compatible_machine
|
||||
|
||||
def setIsUcp(self, isUcp: bool) -> None:
|
||||
if isUcp != self._is_ucp:
|
||||
self._is_ucp = isUcp
|
||||
self.isUcpChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isUcpChanged)
|
||||
def isUcp(self):
|
||||
return self._is_ucp
|
||||
|
||||
def setAllowCreatemachine(self, allow_create_machine):
|
||||
self._allow_create_machine = allow_create_machine
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def allowCreateMachine(self):
|
||||
return self._allow_create_machine
|
||||
|
||||
@pyqtProperty(QObject, notify=exportedSettingModelChanged)
|
||||
def exportedSettingModel(self):
|
||||
return self._exported_settings_model
|
||||
|
||||
@pyqtProperty("QVariantList", notify=exportedSettingModelChanged)
|
||||
def exportedSettingModelItems(self):
|
||||
return self._exported_settings_model.items
|
||||
|
||||
@pyqtProperty(int, notify=exportedSettingModelChanged)
|
||||
def exportedSettingModelRowCount(self):
|
||||
return self._exported_settings_model.rowCount()
|
||||
@pyqtSlot()
|
||||
def closeBackend(self) -> None:
|
||||
"""Close the backend: otherwise one could end up with "Slicing..."""
|
||||
|
@ -347,10 +433,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 +496,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()
|
||||
|
|
|
@ -6,13 +6,13 @@ import QtQuick.Controls 2.3
|
|||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.5 as UM
|
||||
import UM 1.6 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
id: workspaceDialog
|
||||
title: catalog.i18nc("@title:window", "Open Project")
|
||||
title: manager.isUcp? catalog.i18nc("@title:window Don't translate 'Universal Cura Project'", "Open Universal Cura Project (UCP)"): catalog.i18nc("@title:window", "Open Project")
|
||||
|
||||
margin: UM.Theme.getSize("default_margin").width
|
||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||
|
@ -24,16 +24,34 @@ UM.Dialog
|
|||
{
|
||||
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
UM.Label
|
||||
ColumnLayout
|
||||
{
|
||||
id: titleLabel
|
||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||
font: UM.Theme.getFont("large")
|
||||
id: headerColumn
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.rightMargin: anchors.leftMargin
|
||||
RowLayout
|
||||
{
|
||||
UM.Label
|
||||
{
|
||||
id: titleLabel
|
||||
text: manager.isUcp? catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||
font: UM.Theme.getFont("large")
|
||||
}
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: learnMoreButton
|
||||
visible: manager.isUcp
|
||||
text: catalog.i18nc("@button", "Learn more")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +114,7 @@ UM.Dialog
|
|||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||
rightLabelText: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName
|
||||
rightLabelText: manager.isUcp? manager.machineType: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,13 +138,17 @@ UM.Dialog
|
|||
|
||||
minDropDownWidth: machineSelector.width
|
||||
|
||||
buttons: [
|
||||
Component
|
||||
{
|
||||
id: componentNewPrinter
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: createNewPrinter
|
||||
text: catalog.i18nc("@button", "Create new")
|
||||
fixedWidthMode: true
|
||||
width: parent.width - leftPadding * 1.5
|
||||
visible: manager.allowCreateMachine
|
||||
onClicked:
|
||||
{
|
||||
toggleContent()
|
||||
|
@ -136,7 +158,9 @@ UM.Dialog
|
|||
manager.setIsNetworkedMachine(false)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : []
|
||||
|
||||
onSelectPrinter: function(machine)
|
||||
{
|
||||
|
@ -152,39 +176,56 @@ UM.Dialog
|
|||
|
||||
WorkspaceSection
|
||||
{
|
||||
id: profileSection
|
||||
title: catalog.i18nc("@action:label", "Profile settings")
|
||||
iconSource: UM.Theme.getIcon("Sliders")
|
||||
id: ucpProfileSection
|
||||
visible: manager.isUcp
|
||||
title: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
|
||||
iconSource: UM.Theme.getIcon("Settings")
|
||||
|
||||
content: Column
|
||||
{
|
||||
id: profileSettingsValuesTable
|
||||
id: ucpProfileSettingsValuesTable
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Name")
|
||||
rightLabelText: manager.qualityName
|
||||
id: numberOfOverrides
|
||||
leftLabelText: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
|
||||
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.exportedSettingModelRowCount).arg(manager.exportedSettingModelRowCount)
|
||||
buttonText: tableViewSpecificSettings.shouldBeVisible ? catalog.i18nc("@action:button", "Hide settings") : catalog.i18nc("@action:button", "Show settings")
|
||||
onButtonClicked: tableViewSpecificSettings.shouldBeVisible = !tableViewSpecificSettings.shouldBeVisible
|
||||
}
|
||||
|
||||
WorkspaceRow
|
||||
Cura.TableView
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Intent")
|
||||
rightLabelText: manager.intentName
|
||||
}
|
||||
id: tableViewSpecificSettings
|
||||
width: parent.width - parent.leftPadding - UM.Theme.getSize("default_margin").width
|
||||
height: UM.Theme.getSize("card").height
|
||||
visible: shouldBeVisible && manager.isUcp
|
||||
property bool shouldBeVisible: true
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Not in profile")
|
||||
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||
visible: manager.numUserSettings != 0
|
||||
}
|
||||
columnHeaders:
|
||||
[
|
||||
catalog.i18nc("@title:column", "Applies on"),
|
||||
catalog.i18nc("@title:column", "Setting"),
|
||||
catalog.i18nc("@title:column", "Value")
|
||||
]
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Derivative from")
|
||||
rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
model: UM.TableModel
|
||||
{
|
||||
id: tableModel
|
||||
headers: ["category", "label", "value"]
|
||||
rows: manager.exportedSettingModelItems
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager
|
||||
function onExportedSettingModelChanged()
|
||||
{
|
||||
tableModel.clear()
|
||||
tableModel.rows = manager.exportedSettingModelItems
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +235,7 @@ UM.Dialog
|
|||
id: qualityChangesResolveComboBox
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
visible: manager.qualityChangesConflict
|
||||
visible: manager.qualityChangesConflict && !manager.isUcp
|
||||
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
||||
textFont: UM.Theme.getFont("medium")
|
||||
|
||||
|
@ -220,10 +261,51 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
WorkspaceSection
|
||||
{
|
||||
id: profileSection
|
||||
title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Profile settings"):catalog.i18nc("@action:label", "Profile settings")
|
||||
iconSource: UM.Theme.getIcon("Sliders")
|
||||
content: Column
|
||||
{
|
||||
id: profileSettingsValuesTable
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Name")
|
||||
rightLabelText: manager.qualityName
|
||||
visible: manager.isCompatibleMachine
|
||||
}
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Intent")
|
||||
rightLabelText: manager.intentName
|
||||
visible: manager.isCompatibleMachine
|
||||
}
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Not in profile")
|
||||
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||
visible: manager.numUserSettings != 0 && !manager.isUcp
|
||||
}
|
||||
|
||||
WorkspaceRow
|
||||
{
|
||||
leftLabelText: catalog.i18nc("@action:label", "Derivative from")
|
||||
rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.isCompatibleMachine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceSection
|
||||
{
|
||||
id: materialSection
|
||||
title: catalog.i18nc("@action:label", "Material settings")
|
||||
title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Material settings"): catalog.i18nc("@action:label", "Material settings")
|
||||
iconSource: UM.Theme.getIcon("Spool")
|
||||
content: Column
|
||||
{
|
||||
|
@ -248,7 +330,7 @@ UM.Dialog
|
|||
id: materialResolveComboBox
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
visible: manager.materialConflict
|
||||
visible: manager.materialConflict && !manager.isUcp
|
||||
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
|
||||
textFont: UM.Theme.getFont("medium")
|
||||
|
||||
|
@ -279,6 +361,7 @@ UM.Dialog
|
|||
id: visibilitySection
|
||||
title: catalog.i18nc("@action:label", "Setting visibility")
|
||||
iconSource: UM.Theme.getIcon("Eye")
|
||||
visible : !manager.isUcp
|
||||
content: Column
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
@ -364,7 +447,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 +487,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()
|
||||
}
|
||||
]
|
||||
|
@ -416,12 +499,13 @@ UM.Dialog
|
|||
{
|
||||
if (visible)
|
||||
{
|
||||
// Force relead the comboboxes
|
||||
// Force reload the comboboxes
|
||||
// Since this dialog is only created once the first time you open it, these comboxes need to be reloaded
|
||||
// each time it is shown after the first time so that the indexes will update correctly.
|
||||
materialSection.reloadValues()
|
||||
profileSection.reloadValues()
|
||||
printerSection.reloadValues()
|
||||
ucpProfileSection.reloadValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,26 +9,38 @@ import QtQuick.Window 2.2
|
|||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
Row
|
||||
RowLayout
|
||||
{
|
||||
id: root
|
||||
|
||||
property alias leftLabelText: leftLabel.text
|
||||
property alias rightLabelText: rightLabel.text
|
||||
property alias buttonText: button.text
|
||||
signal buttonClicked
|
||||
|
||||
width: parent.width
|
||||
height: visible ? childrenRect.height : 0
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: leftLabel
|
||||
text: catalog.i18nc("@action:label", "Type")
|
||||
width: Math.round(parent.width / 4)
|
||||
Layout.preferredWidth: Math.round(parent.width / 4)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: rightLabel
|
||||
text: manager.machineType
|
||||
width: Math.round(parent.width / 3)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: button
|
||||
visible: !text.isEmpty
|
||||
Layout.maximumHeight: leftLabel.implicitHeight
|
||||
Layout.fillWidth: true
|
||||
onClicked: root.buttonClicked()
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import QtQuick 2.10
|
|||
import QtQuick.Controls 2.3
|
||||
|
||||
|
||||
import UM 1.5 as UM
|
||||
import UM 1.8 as UM
|
||||
|
||||
|
||||
Item
|
||||
|
@ -80,42 +80,22 @@ Item
|
|||
sourceComponent: combobox
|
||||
}
|
||||
|
||||
MouseArea
|
||||
UM.HelpIcon
|
||||
{
|
||||
id: helpIconMouseArea
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: comboboxLabel.verticalCenter
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
hoverEnabled: true
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: width
|
||||
|
||||
visible: comboboxTooltipText != ""
|
||||
source: UM.Theme.getIcon("Help")
|
||||
color: UM.Theme.getColor("text")
|
||||
|
||||
UM.ToolTip
|
||||
{
|
||||
text: comboboxTooltipText
|
||||
visible: helpIconMouseArea.containsMouse
|
||||
targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y)
|
||||
x: 0
|
||||
y: parent.y + parent.height + UM.Theme.getSize("default_margin").height
|
||||
width: UM.Theme.getSize("tooltip").width
|
||||
}
|
||||
}
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
icon: UM.Theme.getIcon("Information")
|
||||
text: comboboxTooltipText
|
||||
visible: comboboxTooltipText != ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loader
|
||||
{
|
||||
width: parent.width
|
||||
height: content.height
|
||||
z: -1
|
||||
anchors.top: sectionTitleRow.bottom
|
||||
sourceComponent: content
|
||||
}
|
||||
|
|
48
plugins/3MFWriter/SettingExport.py
Normal file
48
plugins/3MFWriter/SettingExport.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Copyright (c) 2024 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
|
||||
class SettingExport(QObject):
|
||||
|
||||
def __init__(self, id, name, value, value_name, selectable, show):
|
||||
super().__init__()
|
||||
self.id = id
|
||||
self._name = name
|
||||
self._value = value
|
||||
self._value_name = value_name
|
||||
self._selected = selectable
|
||||
self._selectable = selectable
|
||||
self._show_in_menu = show
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def valuename(self):
|
||||
return str(self._value_name)
|
||||
|
||||
selectedChanged = pyqtSignal(bool)
|
||||
|
||||
def setSelected(self, selected):
|
||||
if selected != self._selected:
|
||||
self._selected = selected
|
||||
self.selectedChanged.emit(self._selected)
|
||||
|
||||
@pyqtProperty(bool, fset = setSelected, notify = selectedChanged)
|
||||
def selected(self):
|
||||
return self._selected
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def selectable(self):
|
||||
return self._selectable
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def isVisible(self):
|
||||
return self._show_in_menu
|
39
plugins/3MFWriter/SettingSelection.qml
Normal file
39
plugins/3MFWriter/SettingSelection.qml
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2024 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.8 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: settingSelection
|
||||
|
||||
UM.CheckBox
|
||||
{
|
||||
text: modelData.name
|
||||
Layout.preferredWidth: UM.Theme.getSize("setting").width
|
||||
checked: modelData.selected
|
||||
onClicked: modelData.selected = checked
|
||||
tooltip: modelData.selectable ? "" :catalog.i18nc("@tooltip Don't translate 'Universal Cura Project'", "This setting may not perform well while exporting to Universal Cura Project. Users are asked to add it at their own risk.")
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: modelData.valuename
|
||||
}
|
||||
|
||||
UM.HelpIcon
|
||||
{
|
||||
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||
text: catalog.i18nc("@tooltip Don't translate 'Universal Cura Project'",
|
||||
"This setting may not perform well while exporting to Universal Cura Project, Users are asked to add it at their own risk.")
|
||||
visible: !modelData.selectable
|
||||
}
|
||||
|
||||
|
||||
}
|
56
plugins/3MFWriter/SettingsExportGroup.py
Normal file
56
plugins/3MFWriter/SettingsExportGroup.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Copyright (c) 2024 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtProperty, pyqtEnum
|
||||
|
||||
|
||||
class SettingsExportGroup(QObject):
|
||||
|
||||
@pyqtEnum
|
||||
class Category(IntEnum):
|
||||
Global = 0
|
||||
Extruder = 1
|
||||
Model = 2
|
||||
|
||||
def __init__(self, stack, name, category, settings, category_details = '', extruder_index = 0, extruder_color = ''):
|
||||
super().__init__()
|
||||
self.stack = stack
|
||||
self._name = name
|
||||
self._settings = settings
|
||||
self._category = category
|
||||
self._category_details = category_details
|
||||
self._extruder_index = extruder_index
|
||||
self._extruder_color = extruder_color
|
||||
self._visible_settings = []
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@pyqtProperty(list, constant=True)
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
@pyqtProperty(list, constant=True)
|
||||
def visibleSettings(self):
|
||||
if self._visible_settings == []:
|
||||
self._visible_settings = list(filter(lambda item : item.isVisible, self._settings))
|
||||
return self._visible_settings
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def category(self):
|
||||
return self._category
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def category_details(self):
|
||||
return self._category_details
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def extruder_index(self):
|
||||
return self._extruder_index
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def extruder_color(self):
|
||||
return self._extruder_color
|
150
plugins/3MFWriter/SettingsExportModel.py
Normal file
150
plugins/3MFWriter/SettingsExportModel.py
Normal file
|
@ -0,0 +1,150 @@
|
|||
# Copyright (c) 2024 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Optional, cast, List, Dict, Pattern, Set
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Settings.SettingDefinition import SettingDefinition
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from .SettingsExportGroup import SettingsExportGroup
|
||||
from .SettingExport import SettingExport
|
||||
|
||||
|
||||
class SettingsExportModel(QObject):
|
||||
|
||||
EXPORTABLE_SETTINGS = {'infill_sparse_density',
|
||||
'adhesion_type',
|
||||
'support_enable',
|
||||
'infill_pattern',
|
||||
'support_type',
|
||||
'support_structure',
|
||||
'support_angle',
|
||||
'support_infill_rate',
|
||||
'ironing_enabled',
|
||||
'fill_outline_gaps',
|
||||
'coasting_enable',
|
||||
'skin_monotonic',
|
||||
'z_seam_position',
|
||||
'infill_before_walls',
|
||||
'ironing_only_highest_layer',
|
||||
'xy_offset',
|
||||
'adaptive_layer_height_enabled',
|
||||
'brim_gap',
|
||||
'support_offset',
|
||||
'brim_location',
|
||||
'magic_spiralize',
|
||||
'slicing_tolerance',
|
||||
'outer_inset_first',
|
||||
'magic_fuzzy_skin_outside_only',
|
||||
'conical_overhang_enabled',
|
||||
'min_infill_area',
|
||||
'small_hole_max_size',
|
||||
'magic_mesh_surface_mode',
|
||||
'carve_multiple_volumes',
|
||||
'meshfix_union_all_remove_holes',
|
||||
'support_tree_rest_preference',
|
||||
'small_feature_max_length',
|
||||
'draft_shield_enabled',
|
||||
'brim_smart_ordering',
|
||||
'ooze_shield_enabled',
|
||||
'bottom_skin_preshrink',
|
||||
'skin_edge_support_thickness',
|
||||
'alternate_carve_order',
|
||||
'top_skin_preshrink',
|
||||
'interlocking_enable'}
|
||||
|
||||
PER_MODEL_EXPORTABLE_SETTINGS_KEYS = {"anti_overhang_mesh",
|
||||
"infill_mesh",
|
||||
"cutting_mesh",
|
||||
"support_mesh"}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._settings_groups = []
|
||||
|
||||
application = CuraApplication.getInstance()
|
||||
|
||||
self._appendGlobalSettings(application)
|
||||
self._appendExtruderSettings(application)
|
||||
self._appendModelSettings(application)
|
||||
|
||||
def _appendGlobalSettings(self, application):
|
||||
global_stack = application.getGlobalContainerStack()
|
||||
self._settings_groups.append(SettingsExportGroup(
|
||||
global_stack, "Global settings", SettingsExportGroup.Category.Global, self._exportSettings(global_stack)))
|
||||
|
||||
def _appendExtruderSettings(self, application):
|
||||
extruders_stacks = ExtruderManager.getInstance().getUsedExtruderStacks()
|
||||
for extruder_stack in extruders_stacks:
|
||||
color = extruder_stack.material.getMetaDataEntry("color_code") if extruder_stack.material else ""
|
||||
self._settings_groups.append(SettingsExportGroup(
|
||||
extruder_stack, "Extruder settings", SettingsExportGroup.Category.Extruder,
|
||||
self._exportSettings(extruder_stack), extruder_index=extruder_stack.position, extruder_color=color))
|
||||
|
||||
def _appendModelSettings(self, application):
|
||||
scene = application.getController().getScene()
|
||||
for scene_node in scene.getRoot().getChildren():
|
||||
self._appendNodeSettings(scene_node, "Model settings", SettingsExportGroup.Category.Model)
|
||||
|
||||
def _appendNodeSettings(self, node, title_prefix, category):
|
||||
stack = node.callDecoration("getStack")
|
||||
if stack:
|
||||
self._settings_groups.append(SettingsExportGroup(
|
||||
stack, f"{title_prefix}", category, self._exportSettings(stack), node.getName()))
|
||||
for child in node.getChildren():
|
||||
self._appendNodeSettings(child, f"Children of {node.getName()}", SettingsExportGroup.Category.Model)
|
||||
|
||||
|
||||
@pyqtProperty(list, constant=True)
|
||||
def settingsGroups(self) -> List[SettingsExportGroup]:
|
||||
return self._settings_groups
|
||||
|
||||
@staticmethod
|
||||
def _exportSettings(settings_stack):
|
||||
settings_catalog = i18nCatalog("fdmprinter.def.json")
|
||||
user_settings_container = settings_stack.userChanges
|
||||
user_keys = user_settings_container.getAllKeys()
|
||||
exportable_settings = SettingsExportModel.EXPORTABLE_SETTINGS
|
||||
settings_export = []
|
||||
# Check whether any of the user keys exist in PER_MODEL_EXPORTABLE_SETTINGS_KEYS
|
||||
is_exportable = any(key in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS for key in user_keys)
|
||||
|
||||
for setting_to_export in user_keys:
|
||||
show_in_menu = setting_to_export not in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS
|
||||
label_msgtxt = f"{str(setting_to_export)} label"
|
||||
label_msgid = settings_stack.getProperty(setting_to_export, "label")
|
||||
label = settings_catalog.i18nc(label_msgtxt, label_msgid)
|
||||
value = settings_stack.getProperty(setting_to_export, "value")
|
||||
unit = settings_stack.getProperty(setting_to_export, "unit")
|
||||
setting_type = settings_stack.getProperty(setting_to_export, "type")
|
||||
value_name = str(SettingDefinition.settingValueToString(setting_type, value))
|
||||
if unit:
|
||||
value_name += " " + str(unit)
|
||||
if setting_type == "enum":
|
||||
options = settings_stack.getProperty(setting_to_export, "options")
|
||||
value_msgctxt = f"{str(setting_to_export)} option {str(value)}"
|
||||
value_msgid = options.get(value, "")
|
||||
value_name = settings_catalog.i18nc(value_msgctxt, value_msgid)
|
||||
|
||||
if setting_type is not None:
|
||||
value = f"{str(SettingDefinition.settingValueToString(setting_type, value))} {unit}"
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
settings_export.append(SettingExport(setting_to_export,
|
||||
label,
|
||||
value,
|
||||
value_name,
|
||||
is_exportable or setting_to_export in exportable_settings,
|
||||
show_in_menu))
|
||||
|
||||
return settings_export
|
86
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal file
86
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2024 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
import ThreeMFWriter 1.0 as ThreeMFWriter
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
id: settingsGroup
|
||||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: settingsGroupTitleRow
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Item
|
||||
{
|
||||
id: icon
|
||||
height: UM.Theme.getSize("medium_button_icon").height
|
||||
width: height
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
id: settingsMainImage
|
||||
anchors.fill: parent
|
||||
source:
|
||||
{
|
||||
switch(modelData.category)
|
||||
{
|
||||
case ThreeMFWriter.SettingsExportGroup.Global:
|
||||
return UM.Theme.getIcon("Sliders")
|
||||
case ThreeMFWriter.SettingsExportGroup.Model:
|
||||
return UM.Theme.getIcon("View3D")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Cura.ExtruderIcon
|
||||
{
|
||||
id: settingsExtruderIcon
|
||||
anchors.fill: parent
|
||||
visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder
|
||||
text: (modelData.extruder_index + 1).toString()
|
||||
font: UM.Theme.getFont("tiny_emphasis")
|
||||
materialColor: modelData.extruder_color
|
||||
}
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: settingsTitle
|
||||
text: modelData.name + (modelData.category_details ? ' (%1)'.arg(modelData.category_details) : '')
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
}
|
||||
|
||||
ListView
|
||||
{
|
||||
id: settingsExportList
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
spacing: 0
|
||||
model: modelData.visibleSettings
|
||||
visible: modelData.visibleSettings.length > 0
|
||||
|
||||
delegate: SettingSelection { }
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||
text: catalog.i18nc("@label", "No specific value has been set")
|
||||
visible: modelData.visibleSettings.length === 0
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import configparser
|
||||
from io import StringIO
|
||||
from threading import Lock
|
||||
import zipfile
|
||||
from typing import Dict, Any
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
|
@ -13,15 +17,23 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from .ThreeMFWriter import ThreeMFWriter
|
||||
from .SettingsExportModel import SettingsExportModel
|
||||
from .SettingsExportGroup import SettingsExportGroup
|
||||
|
||||
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
||||
|
||||
|
||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ucp_model: Optional[SettingsExportModel] = None
|
||||
|
||||
@call_on_qt_thread
|
||||
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||
def setExportModel(self, model: SettingsExportModel) -> None:
|
||||
if self._ucp_model != model:
|
||||
self._ucp_model = model
|
||||
|
||||
def _write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||
application = Application.getInstance()
|
||||
machine_manager = application.getMachineManager()
|
||||
|
||||
|
@ -34,18 +46,20 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if global_stack is None:
|
||||
self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
||||
self.setInformation(
|
||||
catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
||||
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
|
||||
return False
|
||||
|
||||
# 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._ucp_model):
|
||||
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.
|
||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||
|
||||
archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED)
|
||||
|
||||
try:
|
||||
# Add global container stack data to the archive.
|
||||
|
@ -60,15 +74,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
self._writeContainerToArchive(extruder_stack, archive)
|
||||
for container in extruder_stack.getContainers():
|
||||
self._writeContainerToArchive(container, archive)
|
||||
|
||||
# Write user settings data
|
||||
if self._ucp_model is not None:
|
||||
user_settings_data = self._getUserSettings(self._ucp_model)
|
||||
ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
|
||||
except PermissionError:
|
||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||
Logger.error("No permission to write workspace to this stream.")
|
||||
return False
|
||||
|
||||
# Write preferences to archive
|
||||
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
||||
original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
|
||||
temp_preferences = Preferences()
|
||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}:
|
||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
|
||||
"metadata/setting_version"}:
|
||||
temp_preferences.addPreference(preference, None)
|
||||
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
||||
preferences_string = StringIO()
|
||||
|
@ -79,7 +99,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
|
||||
# Save Cura version
|
||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||
version_config_parser = configparser.ConfigParser(interpolation = None)
|
||||
version_config_parser = configparser.ConfigParser(interpolation=None)
|
||||
version_config_parser.add_section("versions")
|
||||
version_config_parser.set("versions", "cura_version", application.getVersion())
|
||||
version_config_parser.set("versions", "build_type", application.getBuildType())
|
||||
|
@ -98,12 +118,18 @@ 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."))
|
||||
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||
success = self._write(stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode)
|
||||
self._ucp_model = None
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
||||
file_name_template = "%s/plugin_metadata.json"
|
||||
|
@ -163,4 +189,27 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
archive.writestr(file_in_archive, serialized_data)
|
||||
except (FileNotFoundError, EnvironmentError):
|
||||
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
|
||||
return
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
|
||||
user_settings = {}
|
||||
|
||||
for group in model.settingsGroups:
|
||||
category = ''
|
||||
if group.category == SettingsExportGroup.Category.Global:
|
||||
category = 'global'
|
||||
elif group.category == SettingsExportGroup.Category.Extruder:
|
||||
category = f"extruder_{group.extruder_index}"
|
||||
|
||||
if len(category) > 0:
|
||||
settings_values = {}
|
||||
stack = group.stack
|
||||
|
||||
for setting in group.settings:
|
||||
if setting.selected:
|
||||
settings_values[setting.id] = stack.getProperty(setting.id, "value")
|
||||
|
||||
user_settings[category] = settings_values
|
||||
|
||||
return user_settings
|
|
@ -1,29 +1,34 @@
|
|||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
|
||||
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
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Application import Application
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
from UM.Message import Message
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
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.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Snapshot import Snapshot
|
||||
|
||||
from PyQt6.QtCore import QBuffer
|
||||
from PyQt6.QtCore import Qt, QBuffer
|
||||
from PyQt6.QtGui import QImage, QPainter
|
||||
|
||||
import pySavitar as Savitar
|
||||
|
||||
from .UCPDialog import UCPDialog
|
||||
import numpy
|
||||
import datetime
|
||||
|
||||
|
@ -38,6 +43,9 @@ except ImportError:
|
|||
import zipfile
|
||||
import UM.Application
|
||||
|
||||
from .SettingsExportModel import SettingsExportModel
|
||||
from .SettingsExportGroup import SettingsExportGroup
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -55,11 +63,13 @@ 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
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
@staticmethod
|
||||
def _convertMatrixToString(matrix):
|
||||
result = ""
|
||||
result += str(matrix._data[0, 0]) + " "
|
||||
result += str(matrix._data[1, 0]) + " "
|
||||
|
@ -83,7 +93,10 @@ class ThreeMFWriter(MeshWriter):
|
|||
"""
|
||||
self._store_archive = store_archive
|
||||
|
||||
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||
@staticmethod
|
||||
def _convertUMNodeToSavitarNode(um_node,
|
||||
transformation = Matrix(),
|
||||
exported_settings: Optional[Dict[str, Set[str]]] = None):
|
||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
|
||||
:returns: Uranium Scene node.
|
||||
|
@ -98,12 +111,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()
|
||||
|
@ -117,13 +138,26 @@ class ThreeMFWriter(MeshWriter):
|
|||
if stack is not None:
|
||||
changed_setting_keys = stack.getTop().getAllKeys()
|
||||
|
||||
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
changed_setting_keys.add("extruder_nr")
|
||||
if exported_settings is None:
|
||||
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
changed_setting_keys.add("extruder_nr")
|
||||
|
||||
# Get values for all changed settings & save them.
|
||||
for key in changed_setting_keys:
|
||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||
# Get values for all changed settings & save them.
|
||||
for key in changed_setting_keys:
|
||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||
else:
|
||||
# We want to export only the specified settings
|
||||
if um_node.getName() in exported_settings:
|
||||
model_exported_settings = exported_settings[um_node.getName()]
|
||||
|
||||
# Get values for all exported settings & save them.
|
||||
for key in model_exported_settings:
|
||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||
|
||||
if isinstance(um_node, CuraSceneNode):
|
||||
savitar_node.setSetting("cura:print_order", str(um_node.printOrder))
|
||||
savitar_node.setSetting("cura:drop_to_buildplate", str(um_node.isDropDownEnabled))
|
||||
|
||||
# Store the metadata.
|
||||
for key, value in um_node.metadata.items():
|
||||
|
@ -133,7 +167,8 @@ 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,
|
||||
exported_settings = exported_settings)
|
||||
if savitar_child_node is not None:
|
||||
savitar_node.addChild(savitar_child_node)
|
||||
|
||||
|
@ -142,7 +177,24 @@ class ThreeMFWriter(MeshWriter):
|
|||
def getArchive(self):
|
||||
return self._archive
|
||||
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||
def _addLogoToThumbnail(self, primary_image, logo_name):
|
||||
# Load the icon png image
|
||||
icon_image = QImage(Resources.getPath(Resources.Images, logo_name))
|
||||
|
||||
# Resize icon_image to be 1/4 of primary_image size
|
||||
new_width = int(primary_image.width() / 4)
|
||||
new_height = int(primary_image.height() / 4)
|
||||
icon_image = icon_image.scaled(new_width, new_height, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
# Create a QPainter to draw on the image
|
||||
painter = QPainter(primary_image)
|
||||
|
||||
# Draw the icon in the top-left corner (adjust coordinates as needed)
|
||||
icon_position = (10, 10)
|
||||
painter.drawImage(icon_position[0], icon_position[1], icon_image)
|
||||
|
||||
painter.end()
|
||||
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool:
|
||||
self._archive = None # Reset archive
|
||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||
try:
|
||||
|
@ -166,6 +218,10 @@ class ThreeMFWriter(MeshWriter):
|
|||
# Attempt to add a thumbnail
|
||||
snapshot = self._createSnapshot()
|
||||
if snapshot:
|
||||
if export_settings_model != None:
|
||||
self._addLogoToThumbnail(snapshot, "cura-share.png")
|
||||
elif export_settings_model == None and self._store_archive:
|
||||
self._addLogoToThumbnail(snapshot, "cura-icon.png")
|
||||
thumbnail_buffer = QBuffer()
|
||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
snapshot.save(thumbnail_buffer, "PNG")
|
||||
|
@ -175,13 +231,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()
|
||||
|
||||
|
@ -218,14 +276,20 @@ class ThreeMFWriter(MeshWriter):
|
|||
transformation_matrix.preMultiply(translation_matrix)
|
||||
|
||||
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
||||
exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model) if export_settings_model != None else None
|
||||
|
||||
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,
|
||||
exported_model_settings)
|
||||
if savitar_node:
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
else:
|
||||
savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
|
||||
savitar_node = self._convertUMNodeToSavitarNode(node,
|
||||
transformation_matrix,
|
||||
exported_model_settings)
|
||||
if savitar_node:
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
|
||||
|
@ -235,9 +299,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 +317,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 +399,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 +408,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
|
||||
|
||||
|
@ -299,13 +425,46 @@ class ThreeMFWriter(MeshWriter):
|
|||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||
def _createSnapshot(self):
|
||||
Logger.log("d", "Creating thumbnail image...")
|
||||
self._lock.acquire()
|
||||
if not CuraApplication.getInstance().isVisible:
|
||||
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
|
||||
finally: self._lock.release()
|
||||
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _extractModelExportedSettings(model: Optional[SettingsExportModel]) -> Dict[str, Set[str]]:
|
||||
extra_settings = {}
|
||||
|
||||
if model is not None:
|
||||
for group in model.settingsGroups:
|
||||
if group.category == SettingsExportGroup.Category.Model:
|
||||
exported_model_settings = set()
|
||||
|
||||
for exported_setting in group.settings:
|
||||
if exported_setting.selected:
|
||||
exported_model_settings.add(exported_setting.id)
|
||||
|
||||
extra_settings[group.category_details] = exported_model_settings
|
||||
|
||||
return extra_settings
|
||||
|
||||
def exportUcp(self):
|
||||
self._config_dialog = UCPDialog()
|
||||
self._config_dialog.show()
|
||||
|
|
114
plugins/3MFWriter/UCPDialog.py
Normal file
114
plugins/3MFWriter/UCPDialog.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Copyright (c) 2024 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, QObject
|
||||
|
||||
import UM
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .SettingsExportModel import SettingsExportModel
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UCPDialog(QObject):
|
||||
finished = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
plugin_path = os.path.dirname(__file__)
|
||||
dialog_path = os.path.join(plugin_path, 'UCPDialog.qml')
|
||||
self._model = SettingsExportModel()
|
||||
self._view = CuraApplication.getInstance().createQmlComponent(
|
||||
dialog_path,
|
||||
{
|
||||
"manager": self,
|
||||
"settingsExportModel": self._model
|
||||
}
|
||||
)
|
||||
self._view.accepted.connect(self._onAccepted)
|
||||
self._view.rejected.connect(self._onRejected)
|
||||
self._finished = False
|
||||
self._accepted = False
|
||||
|
||||
def show(self) -> None:
|
||||
self._finished = False
|
||||
self._accepted = False
|
||||
self._view.show()
|
||||
|
||||
def getModel(self) -> SettingsExportModel:
|
||||
return self._model
|
||||
|
||||
@pyqtSlot()
|
||||
def notifyClosed(self):
|
||||
self._onFinished()
|
||||
|
||||
def save3mf(self):
|
||||
application = CuraApplication.getInstance()
|
||||
workspace_handler = application.getInstance().getWorkspaceFileHandler()
|
||||
|
||||
# Set the model to the workspace writer
|
||||
mesh_writer = workspace_handler.getWriter("3MFWriter")
|
||||
mesh_writer.setExportModel(self._model)
|
||||
|
||||
# Open file dialog and write the file
|
||||
device = application.getOutputDeviceManager().getOutputDevice("local_file")
|
||||
nodes = [application.getController().getScene().getRoot()]
|
||||
|
||||
device.writeError.connect(lambda: self._onRejected())
|
||||
device.writeSuccess.connect(lambda: self._onSuccess())
|
||||
device.writeFinished.connect(lambda: self._onFinished())
|
||||
|
||||
file_name = f"UCP_{CuraApplication.getInstance().getPrintInformation().baseName}"
|
||||
|
||||
try:
|
||||
device.requestWrite(
|
||||
nodes,
|
||||
file_name,
|
||||
["application/x-ucp"],
|
||||
workspace_handler,
|
||||
preferred_mimetype_list="application/x-ucp"
|
||||
)
|
||||
except OutputDeviceError.UserCanceledError:
|
||||
self._onRejected()
|
||||
except Exception as e:
|
||||
message = Message(
|
||||
i18n_catalog.i18nc("@info:error", "Unable to write to file: {0}", file_name),
|
||||
title=i18n_catalog.i18nc("@info:title", "Error"),
|
||||
message_type=Message.MessageType.ERROR
|
||||
)
|
||||
message.show()
|
||||
Logger.logException("e", "Unable to write to file %s: %s", file_name, e)
|
||||
self._onRejected()
|
||||
|
||||
def _onAccepted(self):
|
||||
self.save3mf()
|
||||
|
||||
def _onRejected(self):
|
||||
self._onFinished()
|
||||
|
||||
def _onSuccess(self):
|
||||
self._accepted = True
|
||||
self._onFinished()
|
||||
|
||||
def _onFinished(self):
|
||||
# Make sure we don't send the finished signal twice, whatever happens
|
||||
if self._finished:
|
||||
return
|
||||
self._finished = True
|
||||
|
||||
# Reset the model to the workspace writer
|
||||
mesh_writer = CuraApplication.getInstance().getInstance().getWorkspaceFileHandler().getWriter("3MFWriter")
|
||||
mesh_writer.setExportModel(None)
|
||||
|
||||
self.finished.emit(self._accepted)
|
109
plugins/3MFWriter/UCPDialog.qml
Normal file
109
plugins/3MFWriter/UCPDialog.qml
Normal file
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) 2024 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
id: exportDialog
|
||||
title: catalog.i18nc("@title:window Don't translate 'Universal Cura Project'", "Export Universal Cura Project")
|
||||
|
||||
margin: UM.Theme.getSize("default_margin").width
|
||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
|
||||
|
||||
backgroundColor: UM.Theme.getColor("detail_background")
|
||||
|
||||
headerComponent: Rectangle
|
||||
{
|
||||
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
id: headerColumn
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.rightMargin: anchors.leftMargin
|
||||
|
||||
RowLayout
|
||||
{
|
||||
UM.Label
|
||||
{
|
||||
id: titleLabel
|
||||
text: catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Universal Cura Project")
|
||||
font: UM.Theme.getFont("large")
|
||||
}
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: learnMoreButton
|
||||
text: catalog.i18nc("@button", "Learn more")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
|
||||
}
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
id: descriptionLabel
|
||||
text: catalog.i18nc("@action:description Don't translate 'Universal Cura Project'", "Universal Cura Project files can be printed on different 3D printers while retaining positional data and selected settings. When exported, all models present on the build plate will be included along with their current position, orientation, and scale. You can also select which per-extruder or per-model settings should be included to ensure proper printing.")
|
||||
font: UM.Theme.getFont("default")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.maximumWidth: headerColumn.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||
|
||||
ListView
|
||||
{
|
||||
id: settingsExportList
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
spacing: UM.Theme.getSize("thick_margin").height
|
||||
model: settingsExportModel.settingsGroups
|
||||
clip: true
|
||||
|
||||
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
|
||||
|
||||
delegate: SettingsSelectionGroup { Layout.margins: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons:
|
||||
[
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Cancel")
|
||||
onClicked: reject()
|
||||
},
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Save project")
|
||||
onClicked: accept()
|
||||
}
|
||||
]
|
||||
|
||||
buttonSpacing: UM.Theme.getSize("wide_margin").width
|
||||
|
||||
onClosing:
|
||||
{
|
||||
manager.notifyClosed()
|
||||
}
|
||||
}
|
|
@ -2,9 +2,12 @@
|
|||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
import sys
|
||||
|
||||
from PyQt6.QtQml import qmlRegisterType
|
||||
|
||||
from UM.Logger import Logger
|
||||
try:
|
||||
from . import ThreeMFWriter
|
||||
from .SettingsExportGroup import SettingsExportGroup
|
||||
threemf_writer_was_imported = True
|
||||
except ImportError:
|
||||
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
||||
|
@ -23,20 +26,30 @@ def getMetaData():
|
|||
|
||||
if threemf_writer_was_imported:
|
||||
metaData["mesh_writer"] = {
|
||||
"output": [{
|
||||
"extension": "3mf",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||
}]
|
||||
"output": [
|
||||
{
|
||||
"extension": "3mf",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||
},
|
||||
]
|
||||
}
|
||||
metaData["workspace_writer"] = {
|
||||
"output": [{
|
||||
"extension": workspace_extension,
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||
}]
|
||||
"output": [
|
||||
{
|
||||
"extension": workspace_extension,
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||
},
|
||||
{
|
||||
"extension": "3mf",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"),
|
||||
"mime_type": "application/x-ucp",
|
||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return metaData
|
||||
|
@ -44,6 +57,8 @@ def getMetaData():
|
|||
|
||||
def register(app):
|
||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||
qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup")
|
||||
|
||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
||||
else:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "3MF Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"description": "Provides support for writing 3MF and UCP files.",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
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,35 @@ 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;
|
||||
string sentry_id = 6; // The anonymized Sentry user id that requested the slice
|
||||
string cura_version = 7; // The version of Cura that requested the slice
|
||||
optional string project_name = 8; // The name of the project that requested the slice
|
||||
optional string user_name = 9; // The Digital Factory account name of the user that requested the slice
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
|
@ -63,6 +76,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._default_engine_location = executable_name
|
||||
|
||||
search_path = [
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "Resources")),
|
||||
os.path.abspath(os.path.dirname(sys.executable)),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
|
||||
|
@ -70,7 +84,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 +99,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 +112,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 +131,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,38 +143,48 @@ 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)
|
||||
application.getPreferences().addPreference("info/send_engine_crash", True)
|
||||
application.getPreferences().addPreference("info/anonymous_engine_crash_report", True)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._slicing_error_message = Message(
|
||||
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
|
||||
text = catalog.i18nc("@message", "Oops! We encountered an unexpected error during your slicing process. "
|
||||
"Rest assured, we've automatically received the crash logs for analysis, "
|
||||
"if you have not disabled data sharing in your preferences. To assist us "
|
||||
"further, consider sharing your project details on our issue tracker."),
|
||||
title = catalog.i18nc("@message:title", "Slicing failed"),
|
||||
message_type = Message.MessageType.ERROR
|
||||
)
|
||||
|
@ -172,10 +197,36 @@ 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)
|
||||
|
||||
# Ensure that the initial value for send_engine_crash is handled correctly.
|
||||
application.callLater(self._onPreferencesChanged, "info/send_engine_crash")
|
||||
|
||||
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 +253,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 +291,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 +306,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 +322,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 +331,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 +343,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 +358,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 +370,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 +381,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 +389,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 +414,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 +423,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 +476,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 +571,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 +598,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 +614,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 +694,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 +722,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 +751,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 +815,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 +870,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 +883,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 +1011,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 +1031,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 +1041,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 +1064,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:
|
||||
|
@ -1041,11 +1099,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._change_timer.timeout.disconnect(self.slice)
|
||||
|
||||
def _onPreferencesChanged(self, preference: str) -> None:
|
||||
if preference != "general/auto_slice":
|
||||
if preference != "general/auto_slice" and preference != "info/send_engine_crash" and preference != "info/anonymous_engine_crash_report":
|
||||
return
|
||||
auto_slice = self.determineAutoSlicing()
|
||||
if auto_slice:
|
||||
self._change_timer.start()
|
||||
if preference == "general/auto_slice":
|
||||
auto_slice = self.determineAutoSlicing()
|
||||
if auto_slice:
|
||||
self._change_timer.start()
|
||||
elif preference == "info/send_engine_crash":
|
||||
os.environ["USE_SENTRY"] = "1" if CuraApplication.getInstance().getPreferences().getValue("info/send_engine_crash") else "0"
|
||||
|
||||
def tickle(self) -> None:
|
||||
"""Tickle the backend so in case of auto slicing, it starts the timer."""
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Copyright (c) 2024 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import uuid
|
||||
|
||||
import os
|
||||
|
||||
import numpy
|
||||
from string import Formatter
|
||||
from enum import IntEnum
|
||||
import time
|
||||
from typing import Any, cast, Dict, List, Optional, Set
|
||||
from typing import Any, cast, Dict, List, Optional, Set, Tuple
|
||||
import re
|
||||
import pyArcus as Arcus # For typing.
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
|
@ -23,11 +26,13 @@ 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
|
||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.CuraVersion import CuraVersion
|
||||
|
||||
|
||||
NON_PRINTING_MESH_SETTINGS = ["anti_overhang_mesh", "infill_mesh", "cutting_mesh"]
|
||||
|
@ -45,44 +50,88 @@ 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_expr>.*)\s*$")
|
||||
|
||||
def __init__(self, all_extruder_settings: Dict[str, Any], default_extruder_nr: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._default_extruder_nr = default_extruder_nr
|
||||
self._all_extruder_settings: Dict[str, Any] = all_extruder_settings
|
||||
self._default_extruder_nr: int = default_extruder_nr
|
||||
|
||||
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
|
||||
|
||||
extruder_nr = self._default_extruder_nr
|
||||
def get_value(self, expression: str, args: [str], kwargs: dict) -> str:
|
||||
|
||||
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 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}}}"
|
||||
|
||||
key = key_fragments[0]
|
||||
extruder_nr = str(self._default_extruder_nr)
|
||||
|
||||
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]
|
||||
# The settings may specify a specific extruder to use. This is done by
|
||||
# formatting the expression as "{expression}, {extruder_nr_expr}". If the
|
||||
# expression is formatted like this, we extract the extruder_nr and use
|
||||
# it to get the value from the correct extruder stack.
|
||||
match = self._extruder_regex.match(expression)
|
||||
if match:
|
||||
expression = match.group("expression")
|
||||
extruder_nr_expr = match.group("extruder_nr_expr")
|
||||
|
||||
if value == default_value_str:
|
||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
||||
if extruder_nr_expr.isdigit():
|
||||
extruder_nr = extruder_nr_expr
|
||||
else:
|
||||
# We get the value of the extruder_nr_expr from `_all_extruder_settings` dictionary
|
||||
# rather than the global container stack. The `_all_extruder_settings["-1"]` is a
|
||||
# dict-representation of the global container stack, with additional properties such
|
||||
# as `initial_extruder_nr`. As users may enter such expressions we can't use the
|
||||
# global container stack.
|
||||
extruder_nr = str(self._all_extruder_settings["-1"].get(extruder_nr_expr, "-1"))
|
||||
|
||||
if extruder_nr in self._all_extruder_settings:
|
||||
additional_variables = self._all_extruder_settings[extruder_nr].copy()
|
||||
else:
|
||||
Logger.warning(f"Extruder {extruder_nr} does not exist, using global settings")
|
||||
additional_variables = self._all_extruder_settings["-1"].copy()
|
||||
|
||||
# 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)
|
||||
if not container_stack:
|
||||
Logger.warning(f"Extruder {extruder_nr} does not exist, using global settings")
|
||||
container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
|
||||
setting_function = SettingFunction(expression)
|
||||
value = setting_function(container_stack, additional_variables=additional_variables)
|
||||
|
||||
return value
|
||||
|
||||
|
@ -93,12 +142,13 @@ class StartSliceJob(Job):
|
|||
def __init__(self, slice_message: Arcus.PythonMessage) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene
|
||||
self._scene: Scene = CuraApplication.getInstance().getController().getScene()
|
||||
self._slice_message: Arcus.PythonMessage = slice_message
|
||||
self._is_cancelled = False #type: bool
|
||||
self._build_plate_number = None #type: Optional[int]
|
||||
self._is_cancelled: bool = False
|
||||
self._build_plate_number: Optional[int] = None
|
||||
|
||||
self._all_extruders_settings = None #type: Optional[Dict[str, Any]] # cache for all setting values from all stacks (global & extruder) for the current machine
|
||||
# cache for all setting values from all stacks (global & extruder) for the current machine
|
||||
self._all_extruders_settings: Optional[Dict[str, Any]] = None
|
||||
|
||||
def getSliceMessage(self) -> Arcus.PythonMessage:
|
||||
return self._slice_message
|
||||
|
@ -297,10 +347,38 @@ class StartSliceJob(Job):
|
|||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
user_id = uuid.getnode() # On all of Cura's supported platforms, this returns the MAC address which is pseudonymical information (!= anonymous).
|
||||
user_id %= 2 ** 16 # So to make it anonymous, apply a bitmask selecting only the last 16 bits. This prevents it from being traceable to a specific user but still gives somewhat of an idea of whether it's just the same user hitting the same crash over and over again, or if it's widespread.
|
||||
self._slice_message.sentry_id = f"{user_id}"
|
||||
self._slice_message.cura_version = CuraVersion
|
||||
|
||||
# Add the project name to the message if the user allows for non-anonymous crash data collection.
|
||||
account = CuraApplication.getInstance().getCuraAPI().account
|
||||
if account and account.isLoggedIn and not CuraApplication.getInstance().getPreferences().getValue("info/anonymous_engine_crash_report"):
|
||||
self._slice_message.project_name = CuraApplication.getInstance().getPrintInformation().baseName
|
||||
self._slice_message.user_name = account.userName
|
||||
|
||||
# Build messages for extruder stacks
|
||||
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 +486,11 @@ 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.
|
||||
fmt = GcodeStartEndFormatter(self._all_extruders_settings, default_extruder_nr=default_extruder_nr)
|
||||
return str(fmt.format(value))
|
||||
except:
|
||||
Logger.logException("w", "Unable to do token replacement on start/end g-code")
|
||||
return str(value)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
from .src import DigitalFactoryFileProvider, DigitalFactoryOutputDevicePlugin, DigitalFactoryController
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -135,9 +135,21 @@ class DFFileExportAndUploadManager:
|
|||
file_name = file_upload_response.job_name if file_upload_response.job_name is not None else ""
|
||||
else:
|
||||
Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library")
|
||||
getBackwardsCompatibleMessage(
|
||||
text = "Upload error",
|
||||
title = f"Failed to upload {file_name}. Received unexpected response from server.",
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 0
|
||||
).show()
|
||||
return
|
||||
if file_name not in self._file_upload_job_metadata:
|
||||
Logger.error(f"API response for uploading doesn't match the file name we just uploaded: {file_name} was never uploaded.")
|
||||
getBackwardsCompatibleMessage(
|
||||
text = "Upload error",
|
||||
title = f"Failed to upload {file_name}. Name doesn't match the one sent back in confirmation.",
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 0
|
||||
).show()
|
||||
return
|
||||
with self._message_lock:
|
||||
self.progress_message.show()
|
||||
|
@ -267,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:
|
||||
|
@ -395,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
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
import re
|
||||
from time import time
|
||||
from typing import List, Any, Optional, Union, Type, Tuple, Dict, cast, TypeVar, Callable
|
||||
|
||||
|
@ -313,7 +312,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.
|
||||
|
|
|
@ -501,6 +501,12 @@ class DigitalFactoryController(QObject):
|
|||
"""
|
||||
if not download_url:
|
||||
Logger.log("e", "No download url for file '{}'".format(file_name))
|
||||
getBackwardsCompatibleMessage(
|
||||
text = "Download error",
|
||||
title = f"No download url could be found for '{file_name}'.",
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 0
|
||||
).show()
|
||||
return
|
||||
|
||||
progress_message = Message(text = "{0}/{1}".format(project_name, file_name), dismissable = False, lifetime = 0,
|
||||
|
@ -584,6 +590,12 @@ class DigitalFactoryController(QObject):
|
|||
"""
|
||||
if self._selected_project_idx == -1:
|
||||
Logger.log("e", "No DF Library project is selected.")
|
||||
getBackwardsCompatibleMessage(
|
||||
text = "No Digital Library project was selected",
|
||||
title = "No project selected",
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 0
|
||||
).show()
|
||||
return
|
||||
|
||||
if filename == "":
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -4,7 +4,6 @@ from typing import List, Optional
|
|||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
|
||||
from . import GCodeGzWriter
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
|||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -303,18 +303,17 @@ Item
|
|||
|
||||
Component.onCompleted:
|
||||
{
|
||||
update()
|
||||
updateModel();
|
||||
}
|
||||
|
||||
function update()
|
||||
function updateModel()
|
||||
{
|
||||
clear()
|
||||
for (var i = 1; i <= Cura.MachineManager.activeMachine.maxExtruderCount; i++)
|
||||
{
|
||||
clear();
|
||||
for (var i = 1; i <= Cura.MachineManager.activeMachine.maxExtruderCount; i ++) {
|
||||
// Use String as value. JavaScript only has Number. PropertyProvider.setPropertyValue()
|
||||
// takes a QVariant as value, and Number gets translated into a float. This will cause problem
|
||||
// for integer settings such as "Number of Extruders".
|
||||
append({ text: String(i), value: String(i) })
|
||||
append({ text: String(i), value: String(i) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +321,9 @@ Item
|
|||
Connections
|
||||
{
|
||||
target: Cura.MachineManager
|
||||
function onGlobalContainerChanged() { extruderCountModel.update() }
|
||||
function onGlobalContainerChanged() {
|
||||
extruderCountModel.updateModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
253
plugins/MakerbotWriter/MakerbotWriter.py
Normal file
253
plugins/MakerbotWriter/MakerbotWriter.py
Normal file
|
@ -0,0 +1,253 @@
|
|||
# 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"
|
||||
|
||||
# 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"] = global_stack.definition.getMetaDataEntry("reference_machine_id")
|
||||
|
||||
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 = [extruder.material.getMetaData().get("reference_material_id") for extruder in extruders]
|
||||
|
||||
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 = [extruder.variant.getMetaDataEntry("reference_extruder_id") 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"
|
||||
}
|
|
@ -3,12 +3,10 @@
|
|||
|
||||
from typing import Optional, TYPE_CHECKING, Dict, List
|
||||
|
||||
from .Constants import PACKAGES_URL
|
||||
from .PackageModel import PackageModel
|
||||
from .RemotePackageList import RemotePackageList
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -20,7 +18,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 +35,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()
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, cast, Dict, List, Optional
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
||||
|
@ -12,7 +11,6 @@ from cura.CuraApplication import CuraApplication
|
|||
from cura.CuraPackageManager import CuraPackageManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
|
||||
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
@ -87,12 +85,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:
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"api": 8,
|
||||
"description": "Manages extensions to the application and allows browsing extensions from the Ultimaker website.",
|
||||
"description": "Manages extensions to the application and allows browsing extensions from the UltiMaker website.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -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,9 +280,38 @@ Window
|
|||
onClicked:
|
||||
{
|
||||
marketplaceDialog.hide();
|
||||
CuraApplication.closeApplication();
|
||||
CuraApplication.checkAndExitApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("main_background")
|
||||
anchors.fill: parent
|
||||
visible: !Cura.API.account.isLoggedIn && CuraApplication.isEnterprise
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: signInLabel
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(UM.Theme.getSize("modal_window_minimum").width / 2.5)
|
||||
text: catalog.i18nc("@description","Please sign in to get verified plugins and materials for UltiMaker Cura Enterprise")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: loginButton
|
||||
width: UM.Theme.getSize("account_button").width
|
||||
height: UM.Theme.getSize("account_button").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: signInLabel.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height * 2
|
||||
text: catalog.i18nc("@button", "Sign in")
|
||||
fixedWidthMode: true
|
||||
onClicked: Cura.API.account.login()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,7 @@ UM.TooltipArea
|
|||
onClicked:
|
||||
{
|
||||
addedSettingsModel.setVisible(model.key, checked);
|
||||
UM.ActiveTool.forceUpdate();
|
||||
UM.Controller.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from UM.Settings.SettingInstance import SettingInstance
|
|||
from UM.Logger import Logger
|
||||
import UM.Settings.Models.SettingVisibilityHandler
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherits-stack setting values from different extruders.
|
||||
from cura.Settings.ExtruderManager import ExtruderManager # To get global-inherits-stack setting values from different extruders.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -23,7 +23,7 @@ Item
|
|||
readonly property string infillMeshType: "infill_mesh"
|
||||
readonly property string antiOverhangMeshType: "anti_overhang_mesh"
|
||||
|
||||
property var currentMeshType: UM.ActiveTool.properties.getValue("MeshType")
|
||||
property var currentMeshType: UM.Controller.properties.getValue("MeshType")
|
||||
|
||||
// Update the view every time the currentMeshType changes
|
||||
onCurrentMeshTypeChanged:
|
||||
|
@ -56,7 +56,7 @@ Item
|
|||
|
||||
function setMeshType(type)
|
||||
{
|
||||
UM.ActiveTool.setProperty("MeshType", type)
|
||||
UM.Controller.setProperty("MeshType", type)
|
||||
updateMeshTypeCheckedState(type)
|
||||
}
|
||||
|
||||
|
@ -167,11 +167,7 @@ Item
|
|||
|
||||
onActivated:
|
||||
{
|
||||
if (index == 0){
|
||||
setMeshType(infillMeshType)
|
||||
} else {
|
||||
setMeshType(cuttingMeshType)
|
||||
}
|
||||
setMeshType(index === 0 ? infillMeshType : cuttingMeshType);
|
||||
}
|
||||
|
||||
Binding
|
||||
|
@ -204,21 +200,21 @@ Item
|
|||
model: UM.SettingDefinitionsModel
|
||||
{
|
||||
id: addedSettingsModel
|
||||
containerId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.definition.id: ""
|
||||
expanded: [ "*" ]
|
||||
containerId: Cura.MachineManager.activeMachine !== null ? Cura.MachineManager.activeMachine.definition.id: ""
|
||||
expanded: ["*"]
|
||||
filter:
|
||||
{
|
||||
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
|
||||
if (printSequencePropertyProvider.properties.value === "one_at_a_time")
|
||||
{
|
||||
return {"settable_per_meshgroup": true}
|
||||
return { settable_per_meshgroup: true }
|
||||
}
|
||||
return {"settable_per_mesh": true}
|
||||
return { settable_per_meshgroup: true }
|
||||
}
|
||||
exclude:
|
||||
{
|
||||
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
|
||||
const excluded_settings = ["support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh"]
|
||||
|
||||
if (currentMeshType == "support_mesh")
|
||||
if (currentMeshType === "support_mesh")
|
||||
{
|
||||
excluded_settings = excluded_settings.concat(base.allCategoriesExceptSupport)
|
||||
}
|
||||
|
@ -228,7 +224,7 @@ Item
|
|||
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
|
||||
{
|
||||
id: visibility_handler
|
||||
selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId")
|
||||
selectedObjectId: UM.Controller.properties.getValue("SelectedObjectId")
|
||||
}
|
||||
|
||||
// For some reason the model object is updated after removing him from the memory and
|
||||
|
@ -238,15 +234,15 @@ Item
|
|||
setDestroyed(true)
|
||||
}
|
||||
}
|
||||
|
||||
property int indexWithFocus: -1
|
||||
delegate: Row
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
property var settingLoaderItem: settingLoader.item
|
||||
Loader
|
||||
{
|
||||
id: settingLoader
|
||||
width: UM.Theme.getSize("setting").width - removeButton.width - scrollBar.width
|
||||
height: UM.Theme.getSize("section").height + UM.Theme.getSize("narrow_margin").height
|
||||
enabled: provider.properties.enabled === "True"
|
||||
property var definition: model
|
||||
property var settingDefinitionsModel: addedSettingsModel
|
||||
|
@ -257,7 +253,7 @@ Item
|
|||
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
|
||||
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
|
||||
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
|
||||
asynchronous: model.type != "enum" && model.type != "extruder"
|
||||
asynchronous: model.type !== "enum" && model.type !== "extruder"
|
||||
|
||||
onLoaded:
|
||||
{
|
||||
|
@ -266,6 +262,7 @@ Item
|
|||
settingLoader.item.showLinkedSettingIcon = false
|
||||
settingLoader.item.doDepthIndentation = false
|
||||
settingLoader.item.doQualityUserSettingEmphasis = false
|
||||
settingLoader.item.height = UM.Theme.getSize("setting").height + UM.Theme.getSize("narrow_margin").height
|
||||
}
|
||||
|
||||
sourceComponent:
|
||||
|
@ -323,7 +320,7 @@ Item
|
|||
{
|
||||
id: provider
|
||||
|
||||
containerStackId: UM.ActiveTool.properties.getValue("ContainerID")
|
||||
containerStackId: UM.Controller.properties.getValue("ContainerID")
|
||||
key: model.key
|
||||
watchedProperties: [ "value", "enabled", "validationState" ]
|
||||
storeIndex: 0
|
||||
|
@ -333,7 +330,7 @@ Item
|
|||
UM.SettingPropertyProvider
|
||||
{
|
||||
id: inheritStackProvider
|
||||
containerStackId: UM.ActiveTool.properties.getValue("ContainerID")
|
||||
containerStackId: UM.Controller.properties.getValue("ContainerID")
|
||||
key: model.key
|
||||
watchedProperties: [ "limit_to_extruder" ]
|
||||
}
|
||||
|
@ -346,27 +343,65 @@ Item
|
|||
|
||||
Connections
|
||||
{
|
||||
target: UM.ActiveTool
|
||||
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.Controller
|
||||
function onPropertiesChanged()
|
||||
{
|
||||
// the values cannot be bound with UM.ActiveTool.properties.getValue() calls,
|
||||
// the values cannot be bound with UM.Controller.properties.getValue() calls,
|
||||
// so here we connect to the signal and update the those values.
|
||||
if (typeof UM.ActiveTool.properties.getValue("SelectedObjectId") !== "undefined")
|
||||
if (typeof UM.Controller.properties.getValue("SelectedObjectId") !== "undefined")
|
||||
{
|
||||
const selectedObjectId = UM.ActiveTool.properties.getValue("SelectedObjectId")
|
||||
const selectedObjectId = UM.Controller.properties.getValue("SelectedObjectId")
|
||||
if (addedSettingsModel.visibilityHandler.selectedObjectId != selectedObjectId)
|
||||
{
|
||||
addedSettingsModel.visibilityHandler.selectedObjectId = selectedObjectId
|
||||
}
|
||||
}
|
||||
if (typeof UM.ActiveTool.properties.getValue("ContainerID") !== "undefined")
|
||||
if (typeof UM.Controller.properties.getValue("ContainerID") !== "undefined")
|
||||
{
|
||||
const containerId = UM.ActiveTool.properties.getValue("ContainerID")
|
||||
if (provider.containerStackId != containerId)
|
||||
const containerId = UM.Controller.properties.getValue("ContainerID")
|
||||
if (provider.containerStackId !== containerId)
|
||||
{
|
||||
provider.containerStackId = containerId
|
||||
}
|
||||
if (inheritStackProvider.containerStackId != containerId)
|
||||
if (inheritStackProvider.containerStackId !== containerId)
|
||||
{
|
||||
inheritStackProvider.containerStackId = containerId
|
||||
}
|
||||
|
@ -388,13 +423,13 @@ Item
|
|||
onClicked:
|
||||
{
|
||||
settingPickDialog.visible = true;
|
||||
if (currentMeshType == "support_mesh")
|
||||
if (currentMeshType === "support_mesh")
|
||||
{
|
||||
settingPickDialog.additional_excluded_settings = base.allCategoriesExceptSupport;
|
||||
}
|
||||
else
|
||||
{
|
||||
settingPickDialog.additional_excluded_settings = []
|
||||
settingPickDialog.additional_excluded_settings = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -412,7 +447,7 @@ Item
|
|||
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
key: "machine_extruder_count"
|
||||
watchedProperties: [ "value" ]
|
||||
watchedProperties: ["value"]
|
||||
storeIndex: 0
|
||||
}
|
||||
|
||||
|
@ -422,56 +457,15 @@ Item
|
|||
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
key: "print_sequence"
|
||||
watchedProperties: [ "value" ]
|
||||
watchedProperties: ["value"]
|
||||
storeIndex: 0
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingTextField
|
||||
|
||||
Cura.SettingTextField { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingComboBox
|
||||
|
||||
Cura.SettingComboBox { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingExtruder
|
||||
|
||||
Cura.SettingExtruder { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingOptionalExtruder
|
||||
|
||||
Cura.SettingOptionalExtruder { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingCheckBox
|
||||
|
||||
Cura.SettingCheckBox { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingCategory
|
||||
|
||||
Cura.SettingCategory { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingUnknown
|
||||
|
||||
Cura.SettingUnknown { }
|
||||
}
|
||||
Component { id: settingTextField; Cura.SettingTextField { } }
|
||||
Component { id: settingComboBox; Cura.SettingComboBox { } }
|
||||
Component { id: settingExtruder; Cura.SettingExtruder { } }
|
||||
Component { id: settingOptionalExtruder; Cura.SettingOptionalExtruder { } }
|
||||
Component { id: settingCheckBox; Cura.SettingCheckBox { } }
|
||||
Component { id: settingCategory; Cura.SettingCategory { } }
|
||||
Component { id: settingUnknown; Cura.SettingUnknown { } }
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
@ -139,22 +144,28 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
if self._loaded_scripts: # Already loaded.
|
||||
return
|
||||
|
||||
# The PostProcessingPlugin path is for built-in scripts.
|
||||
# The Resources path is where the user should store custom scripts.
|
||||
# The Preferences path is legacy, where the user may previously have stored scripts.
|
||||
resource_folders = [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]
|
||||
resource_folders.extend(Resources.getAllPathsForType(Resources.Resources))
|
||||
for root in resource_folders:
|
||||
if root is None:
|
||||
continue
|
||||
path = os.path.join(root, "scripts")
|
||||
# Make sure a "scripts" folder exists in the main configuration folder and the preferences folder.
|
||||
# On some platforms the resources and preferences folders resolve to the same folder,
|
||||
# but on Linux they can be different.
|
||||
for path in set([os.path.join(Resources.getStoragePath(r), "scripts") for r in [Resources.Resources, Resources.Preferences]]):
|
||||
if not os.path.isdir(path):
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError:
|
||||
Logger.log("w", "Unable to create a folder for scripts: " + path)
|
||||
continue
|
||||
|
||||
# The PostProcessingPlugin path is for built-in scripts.
|
||||
# The Resources path is where the user should store custom scripts.
|
||||
# The Preferences path is legacy, where the user may previously have stored scripts.
|
||||
resource_folders = [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]
|
||||
resource_folders.extend(Resources.getAllPathsForType(Resources.Resources))
|
||||
|
||||
for root in resource_folders:
|
||||
if root is None:
|
||||
continue
|
||||
path = os.path.join(root, "scripts")
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
self.loadScripts(path)
|
||||
|
||||
def loadScripts(self, path: str) -> None:
|
||||
|
|
|
@ -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
|
||||
|
@ -120,6 +120,8 @@ UM.Dialog
|
|||
UM.Label
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: height
|
||||
elide: Text.ElideRight
|
||||
text: manager.getScriptLabelByKey(modelData.toString())
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
837
plugins/PostProcessingPlugin/scripts/AddCoolingProfile.py
Normal file
837
plugins/PostProcessingPlugin/scripts/AddCoolingProfile.py
Normal file
|
@ -0,0 +1,837 @@
|
|||
# Designed in January 2023 by GregValiant (Greg Foresi)
|
||||
## My design intent was to make this as full featured and "industrial strength" as I could. People printing exotic materials on large custom printers may want to turn the fans off for certain layers, and then back on again later in the print. This script allows that.
|
||||
# Functions:
|
||||
## Remove all fan speed lines from the file (optional). This should be enabled for the first instance of the script. It is disabled by default in any following instances.
|
||||
## "By Layer" allows the user to adjust the fan speed up, or down, or off, within the print. "By Feature" allows different fan speeds for different features (;TYPE:WALL-OUTER, etc.).
|
||||
## If 'By Feature' then a Start Layer and/or an End Layer can be defined.
|
||||
## Fan speeds are scaled PWM (0 - 255) or RepRap (0.0 - 1.0) depending on {machine_scale_fan_speed_zero_to_one}.
|
||||
## A minimum fan speed of 12% is enforced. It is the slowest speed that my cooling fan will turn on so that's what I used. 'M106 S14' (as Cura might insert) was pretty useless.
|
||||
## If multiple extruders have separate fan circuits the speeds are set at tool changes and conform to the layer or feature setting. There is support for up to 4 layer cooling fan circuits.
|
||||
## My thanks to @5axes(@CUQ), @fieldOfView(@AHoeben), @Ghostkeeper, and @Torgeir. A special thanks to @RBurema for his patience in reviewing my 'non-pythonic' script.
|
||||
## 9/14/23 (Greg Foresi) Added support for One-at-a-Time print sequence.
|
||||
## 12/15/23 (Greg Foresi) Split off 'Single Fan By Layer', 'Multi-fan By Layer', 'Single Fan By Feature', and 'Multi-fan By Feature' from the main 'execute' script.
|
||||
## 1/5/24 (Greg Foresi) Revised the regex replacements.
|
||||
|
||||
from ..Script import Script
|
||||
from UM.Application import Application
|
||||
import re
|
||||
|
||||
class AddCoolingProfile(Script):
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Advanced Cooling Fan Control",
|
||||
"key": "AddCoolingProfile",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"fan_layer_or_feature":
|
||||
{
|
||||
"label": "Cooling Control by:",
|
||||
"description": "A fan percentage of ''0'' turns the fan off. Minimum Fan is 12% (when on). All layer entries are the Cura Preview number. ''By Layer'': Enter as ''Layer#/Fan%'' (foreslash is the delimiter). Your final layer speed will continue to the end of the Gcode. ''By Feature'': If you enable an 'End Layer' then the ''Final %'' is available and is the speed that will finish the file. 'By Feature' is better for large slow prints than it is for short fast prints.",
|
||||
"type": "enum",
|
||||
"options": {
|
||||
"by_layer": "Layer Numbers",
|
||||
"by_feature": "Feature Types"},
|
||||
"default_value": "by_layer"
|
||||
},
|
||||
"delete_existing_m106":
|
||||
{
|
||||
"label": "Remove M106 lines prior to inserting new.",
|
||||
"description": "If you have 2 or more instances of 'Advanced Cooling Fan Control' running (to cool a portion of a print differently), then you must uncheck this box or the followup instances will remove all the lines inserted by the first instance. Pay attention to the Start and Stop layers. Regardless of this setting: The script always removes M106 lines starting with the lowest layer number (when 'By Layer') or the starting layer number (when 'By Feature'). If you want to keep the M106 lines that Cura inserted up to the point where this post-processor will start making insertions, then un-check the box.",
|
||||
"type": "bool",
|
||||
"enabled": true,
|
||||
"value": true,
|
||||
"default_value": true
|
||||
},
|
||||
"feature_fan_start_layer":
|
||||
{
|
||||
"label": "Starting Layer",
|
||||
"description": "Layer to start the insertion at. Use the Cura preview numbers. Changes will begin at the start of that layer.",
|
||||
"type": "int",
|
||||
"default_value": 5,
|
||||
"minimum_value": 1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_end_layer":
|
||||
{
|
||||
"label": "Ending Layer",
|
||||
"description": "Layer to complete the insertion at. Enter '-1' for the entire file or enter a layer number. Insertions will stop at the END of this layer. If you set an End Layer then you should set the Final % that will finish the file",
|
||||
"type": "int",
|
||||
"default_value": -1,
|
||||
"minimum_value": -1,
|
||||
"unit": "Lay# ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"layer_fan_1":
|
||||
{
|
||||
"label": "Layer/Percent #1",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage. There are up to 8 changes. If you need more then add a second instance of this script and remember to turn off 'Remove M106 lines' in the second instance. The layer numbers in the second instance must start with a layer number higher than the last layer number in a previous script. You can't end the first script with a setting for layer 80 and then start the second script with a setting for layer 40 because 'Remove M106 lines' always starts with the lowest layer number when 'By Layer' is selected.",
|
||||
"type": "str",
|
||||
"default_value": "5/30",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_2":
|
||||
{
|
||||
"label": "Layer/Percent #2",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_3":
|
||||
{
|
||||
"label": "Layer/Percent #3",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_4":
|
||||
{
|
||||
"label": "Layer/Percent #4",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_5":
|
||||
{
|
||||
"label": "Layer/Percent #5",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_6":
|
||||
{
|
||||
"label": "Layer/Percent #6",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_7":
|
||||
{
|
||||
"label": "Layer/Percent #7",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"layer_fan_8":
|
||||
{
|
||||
"label": "Layer/Percent #8",
|
||||
"description": "Enter as: 'LAYER / Percent' Ex: 55/100 with the layer first, then a '/' to delimit, and then the fan percentage.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "L#/% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_layer'"
|
||||
},
|
||||
"feature_fan_skirt":
|
||||
{
|
||||
"label": "Skirt/Brim/Ooze Shield %",
|
||||
"description": "Enter the fan percentage for skirt/brim. If you are starting at a layer above 1 then this setting only affects Ooze Shields and from the Start Layer up.",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_wall_inner":
|
||||
{
|
||||
"label": "Inner Walls %",
|
||||
"description": "Enter the fan percentage for the Wall-Inner.",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_wall_outer":
|
||||
{
|
||||
"label": "Outer Walls %",
|
||||
"description": "Enter the fan percentage for the Wall-Outer.",
|
||||
"type": "int",
|
||||
"default_value": 75,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_fill":
|
||||
{
|
||||
"label": "Infill %",
|
||||
"description": "Enter the fan percentage for the Infill.",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_skin":
|
||||
{
|
||||
"label": "Top/Bottom (Skin) %",
|
||||
"description": "Enter the fan percentage for the Skins.",
|
||||
"type": "int",
|
||||
"default_value": 100,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_support":
|
||||
{
|
||||
"label": "Support %",
|
||||
"description": "Enter the fan percentage for the Supports.",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_support_interface":
|
||||
{
|
||||
"label": "Support Interface %",
|
||||
"description": "Enter the fan percentage for the Support Interface.",
|
||||
"type": "int",
|
||||
"default_value": 100,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_prime_tower":
|
||||
{
|
||||
"label": "Prime Tower %",
|
||||
"description": "Enter the fan percentage for the Prime Tower (whether it's used or not).",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_bridge":
|
||||
{
|
||||
"label": "Bridge %",
|
||||
"description": "Enter the fan percentage for any Bridging (whether it's used on not).",
|
||||
"type": "int",
|
||||
"default_value": 100,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'"
|
||||
},
|
||||
"feature_fan_combing":
|
||||
{
|
||||
"label": "Fan 'OFF' during Combing:",
|
||||
"description": "When checked will set the fan to 0% for combing moves over 5 lines long in the gcode. When un-checked the fan speed during combing is whatever the previous speed is set to.",
|
||||
"type": "bool",
|
||||
"enabled": "fan_layer_or_feature == 'by_feature'",
|
||||
"default_value": true
|
||||
},
|
||||
"feature_fan_feature_final":
|
||||
{
|
||||
"label": "Final %",
|
||||
"description": "If you choose an 'End Layer' then this is the fan speed that will carry through to the end of the gcode file. It will go into effect at the 'END' of your End layer.",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "(int(feature_fan_end_layer) != -1) and (fan_layer_or_feature == 'by_feature')"
|
||||
},
|
||||
"fan_enable_raft":
|
||||
{
|
||||
"label": "Enable Raft Cooling",
|
||||
"description": "Enable the fan for the raft layers. When enabled the Raft Fan Speed will continue until another Layer or Feature setting over-rides it.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": true
|
||||
},
|
||||
"fan_raft_percent":
|
||||
{
|
||||
"label": "Raft Fan %:",
|
||||
"description": "Enter the percentage for the Raft.",
|
||||
"type": "int",
|
||||
"default_value": 35,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "fan_enable_raft"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
scripts = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("post_processing_scripts")
|
||||
if scripts != None:
|
||||
script_count = scripts.count("AddCoolingProfile")
|
||||
if script_count > 0:
|
||||
## Set 'Remove M106 lines' to "false" if there is already an instance of this script running.
|
||||
self._instance.setProperty("delete_existing_m106", "value", False)
|
||||
|
||||
def execute(self, data):
|
||||
#Initialize variables that are buried in if statements.
|
||||
mycura = Application.getInstance().getGlobalContainerStack()
|
||||
t0_fan = " P0"; t1_fan = " P0"; t2_fan = " P0"; t3_fan = " P0"; is_multi_extr_print = True
|
||||
|
||||
#Get some information from Cura-----------------------------------
|
||||
extruder = mycura.extruderList
|
||||
|
||||
#This will be true when fan scale is 0-255pwm and false when it's RepRap 0-1 (Cura 5.x)
|
||||
fan_mode = True
|
||||
##For 4.x versions that don't have the 0-1 option
|
||||
try:
|
||||
fan_mode = not bool(extruder[0].getProperty("machine_scale_fan_speed_zero_to_one", "value"))
|
||||
except:
|
||||
pass
|
||||
bed_adhesion = (extruder[0].getProperty("adhesion_type", "value"))
|
||||
extruder_count = mycura.getProperty("machine_extruder_count", "value")
|
||||
print_sequence = str(mycura.getProperty("print_sequence", "value"))
|
||||
|
||||
#Assign the fan numbers to the tools------------------------------
|
||||
if extruder_count == 1:
|
||||
is_multi_fan = False
|
||||
is_multi_extr_print = False
|
||||
if int((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value"))) > 0:
|
||||
t0_fan = " P" + str((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value")))
|
||||
else:
|
||||
#No P parameter if there is a single fan circuit------------------
|
||||
t0_fan = ""
|
||||
|
||||
#Get the cooling fan numbers for each extruder if the printer has multiple extruders
|
||||
elif extruder_count > 1:
|
||||
is_multi_fan = True
|
||||
t0_fan = " P" + str((extruder[0].getProperty("machine_extruder_cooling_fan_number", "value")))
|
||||
if is_multi_fan:
|
||||
if extruder_count > 1: t1_fan = " P" + str((extruder[1].getProperty("machine_extruder_cooling_fan_number", "value")))
|
||||
if extruder_count > 2: t2_fan = " P" + str((extruder[2].getProperty("machine_extruder_cooling_fan_number", "value")))
|
||||
if extruder_count > 3: t3_fan = " P" + str((extruder[3].getProperty("machine_extruder_cooling_fan_number", "value")))
|
||||
|
||||
#Initialize the fan_list with defaults----------------------------
|
||||
fan_list = ["z"] * 16
|
||||
for num in range(0,15,2):
|
||||
fan_list[num] = len(data)
|
||||
fan_list[num + 1] = "M106 S0"
|
||||
|
||||
#Assign the variable values if "By Layer"-------------------------
|
||||
by_layer_or_feature = self.getSettingValueByKey("fan_layer_or_feature")
|
||||
if by_layer_or_feature == "by_layer":
|
||||
## By layer doesn't do any feature search so there is no need to look for combing moves
|
||||
feature_fan_combing = False
|
||||
fan_list[0] = self.getSettingValueByKey("layer_fan_1")
|
||||
fan_list[2] = self.getSettingValueByKey("layer_fan_2")
|
||||
fan_list[4] = self.getSettingValueByKey("layer_fan_3")
|
||||
fan_list[6] = self.getSettingValueByKey("layer_fan_4")
|
||||
fan_list[8] = self.getSettingValueByKey("layer_fan_5")
|
||||
fan_list[10] = self.getSettingValueByKey("layer_fan_6")
|
||||
fan_list[12] = self.getSettingValueByKey("layer_fan_7")
|
||||
fan_list[14] = self.getSettingValueByKey("layer_fan_8")
|
||||
## If there is no '/' delimiter then ignore the line else put the settings in a list
|
||||
for num in range(0,15,2):
|
||||
if "/" in fan_list[num]:
|
||||
fan_list[num + 1] = self._layer_checker(fan_list[num], "p", fan_mode)
|
||||
fan_list[num] = self._layer_checker(fan_list[num], "l", fan_mode)
|
||||
|
||||
## Assign the variable values if "By Feature"
|
||||
elif by_layer_or_feature == "by_feature":
|
||||
the_start_layer = self.getSettingValueByKey("feature_fan_start_layer") - 1
|
||||
the_end_layer = self.getSettingValueByKey("feature_fan_end_layer")
|
||||
try:
|
||||
if int(the_end_layer) != -1:
|
||||
## Catch a possible input error.
|
||||
if the_end_layer < the_start_layer:
|
||||
the_end_layer = the_start_layer
|
||||
except:
|
||||
the_end_layer = -1 ## If there is an input error default to the entire gcode file.
|
||||
|
||||
## Get the speed for each feature
|
||||
feature_name_list = []
|
||||
feature_speed_list = []
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_skirt"), fan_mode)); feature_name_list.append(";TYPE:SKIRT")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_wall_inner"), fan_mode)); feature_name_list.append(";TYPE:WALL-INNER")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_wall_outer"), fan_mode)); feature_name_list.append(";TYPE:WALL-OUTER")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_fill"), fan_mode)); feature_name_list.append(";TYPE:FILL")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_skin"), fan_mode)); feature_name_list.append(";TYPE:SKIN")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_support"), fan_mode)); feature_name_list.append(";TYPE:SUPPORT")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_support_interface"), fan_mode)); feature_name_list.append(";TYPE:SUPPORT-INTERFACE")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_prime_tower"), fan_mode)); feature_name_list.append(";TYPE:PRIME-TOWER")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_bridge"), fan_mode)); feature_name_list.append(";BRIDGE")
|
||||
feature_speed_list.append(self._feature_checker(self.getSettingValueByKey("feature_fan_feature_final"), fan_mode)); feature_name_list.append("FINAL_FAN")
|
||||
feature_fan_combing = self.getSettingValueByKey("feature_fan_combing")
|
||||
if the_end_layer > -1 and by_layer_or_feature == "by_feature":
|
||||
## Required so the final speed input can be determined
|
||||
the_end_is_enabled = True
|
||||
else:
|
||||
## There is no ending layer so do the whole file
|
||||
the_end_is_enabled = False
|
||||
if the_end_layer == -1 or the_end_is_enabled == False:
|
||||
the_end_layer = len(data) + 2
|
||||
|
||||
## Find the Layer0Index and the RaftIndex
|
||||
raft_start_index = 0
|
||||
number_of_raft_layers = 0
|
||||
layer_0_index = 0
|
||||
## Catch the number of raft layers.
|
||||
for l_num in range(1,10,1):
|
||||
layer = data[l_num]
|
||||
if ";LAYER:-" in layer:
|
||||
number_of_raft_layers += 1
|
||||
if raft_start_index == 0:
|
||||
raft_start_index = l_num
|
||||
if ";LAYER:0" in layer:
|
||||
layer_0_index = l_num
|
||||
break
|
||||
|
||||
## Is this a single extruder print on a multi-extruder printer? - get the correct fan number for the extruder being used.
|
||||
if is_multi_fan:
|
||||
T0_used = False
|
||||
T1_used = False
|
||||
T2_used = False
|
||||
T3_used = False
|
||||
## Bypass the file header and ending gcode.
|
||||
for num in range(1,len(data)-1,1):
|
||||
lines = data[num]
|
||||
if "T0" in lines:
|
||||
T0_used = True
|
||||
if "T1" in lines:
|
||||
T1_used = True
|
||||
if "T2" in lines:
|
||||
T2_used = True
|
||||
if "T3" in lines:
|
||||
T3_used = True
|
||||
is_multi_extr_print = True if sum([T0_used, T1_used, T2_used, T3_used]) > 1 else False
|
||||
|
||||
## On a multi-extruder printer and single extruder print find out which extruder starts the file.
|
||||
init_fan = t0_fan
|
||||
if not is_multi_extr_print:
|
||||
startup = data[1]
|
||||
lines = startup.split("\n")
|
||||
for line in lines:
|
||||
if line == "T1":
|
||||
t0_fan = t1_fan
|
||||
elif line == "T2":
|
||||
t0_fan = t2_fan
|
||||
elif line == "T3":
|
||||
t0_fan = t3_fan
|
||||
elif is_multi_extr_print:
|
||||
## On a multi-extruder printer and multi extruder print find out which extruder starts the file.
|
||||
startup = data[1]
|
||||
lines = startup.split("\n")
|
||||
for line in lines:
|
||||
if line == "T0":
|
||||
init_fan = t0_fan
|
||||
elif line == "T1":
|
||||
init_fan = t1_fan
|
||||
elif line == "T2":
|
||||
init_fan = t2_fan
|
||||
elif line == "T3":
|
||||
init_fan = t3_fan
|
||||
else:
|
||||
init_fan = ""
|
||||
## Assign the variable values if "Raft Enabled"
|
||||
raft_enabled = self.getSettingValueByKey("fan_enable_raft")
|
||||
if raft_enabled and bed_adhesion == "raft":
|
||||
fan_sp_raft = self._feature_checker(self.getSettingValueByKey("fan_raft_percent"), fan_mode)
|
||||
else:
|
||||
fan_sp_raft = "M106 S0"
|
||||
|
||||
# Start to alter the data-----------------------------------------
|
||||
## Strip the existing M106 lines from the file up to the end of the last layer. If a user wants to use more than one instance of this plugin then they won't want to erase the M106 lines that the preceding plugins inserted so 'delete_existing_m106' is an option.
|
||||
delete_existing_m106 = self.getSettingValueByKey("delete_existing_m106")
|
||||
if delete_existing_m106:
|
||||
## Start deleting from the beginning
|
||||
start_from = int(raft_start_index)
|
||||
else:
|
||||
if by_layer_or_feature == "by_layer":
|
||||
altered_start_layer = str(len(data))
|
||||
## The fan list layers don't need to be in ascending order. Get the lowest.
|
||||
for num in range(0,15,2):
|
||||
try:
|
||||
if int(fan_list[num]) < int(altered_start_layer):
|
||||
altered_start_layer = int(fan_list[num])
|
||||
except:
|
||||
pass
|
||||
elif by_layer_or_feature == "by_feature":
|
||||
altered_start_layer = int(the_start_layer) - 1
|
||||
start_from = int(layer_0_index) + int(altered_start_layer)
|
||||
## Strip the M106 and M107 lines from the file
|
||||
for l_index in range(int(start_from), len(data) - 1, 1):
|
||||
data[l_index] = re.sub(re.compile("M106(.*)\n"), "", data[l_index])
|
||||
data[l_index] = re.sub(re.compile("M107(.*)\n"), "", data[l_index])
|
||||
|
||||
## Deal with a raft and with One-At-A-Time print sequence
|
||||
if raft_enabled and bed_adhesion == "raft":
|
||||
if print_sequence == "one_at_a_time":
|
||||
for r_index in range(2,len(data)-2,1):
|
||||
lines = data[r_index].split("\n")
|
||||
if not raft_enabled or bed_adhesion != "raft":
|
||||
if ";LAYER:0" in data[r_index] or ";LAYER:-" in data[r_index]:
|
||||
lines.insert(1, "M106 S0" + str(t0_fan))
|
||||
if raft_enabled and bed_adhesion == "raft":
|
||||
if ";LAYER:-" in data[r_index]:
|
||||
## Turn the raft fan on
|
||||
lines.insert(1, fan_sp_raft + str(t0_fan))
|
||||
## Shut the raft fan off at layer 0
|
||||
if ";LAYER:0" in data[r_index]:
|
||||
lines.insert(1,"M106 S0" + str(t0_fan))
|
||||
data[r_index] = "\n".join(lines)
|
||||
elif print_sequence == "all_at_once":
|
||||
layer = data[raft_start_index]
|
||||
lines = layer.split("\n")
|
||||
if ";LAYER:-" in layer:
|
||||
## Turn the raft fan on
|
||||
lines.insert(1, fan_sp_raft + str(init_fan))
|
||||
layer = "\n".join(lines)
|
||||
data[raft_start_index] = layer
|
||||
layer = data[layer_0_index]
|
||||
lines = layer.split("\n")
|
||||
## Shut the raft fan off
|
||||
lines.insert(1, "M106 S0" + str(init_fan))
|
||||
data[layer_0_index] = "\n".join(lines)
|
||||
else:
|
||||
for r_index in range(2,len(data)-2,1):
|
||||
lines = data[r_index].split("\n")
|
||||
if ";LAYER:0" in data[r_index] or ";LAYER:-" in data[r_index]:
|
||||
if not "0" in fan_list:
|
||||
lines.insert(1, "M106 S0" + str(t0_fan))
|
||||
data[r_index] = "\n".join(lines)
|
||||
|
||||
## Turn off all fans at the end of data[1]. If more than one instance of this script is running then this will result in multiple M106 lines.
|
||||
temp_startup = data[1].split("\n")
|
||||
temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t0_fan))
|
||||
## If there are multiple cooling fans shut them all off
|
||||
if is_multi_fan:
|
||||
if extruder_count > 1 and t1_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t1_fan))
|
||||
if extruder_count > 2 and t2_fan != t1_fan and t2_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t2_fan))
|
||||
if extruder_count > 3 and t3_fan != t2_fan and t3_fan != t1_fan and t3_fan != t0_fan: temp_startup.insert(len(temp_startup)-2,"M106 S0" + str(t3_fan))
|
||||
data[1] = "\n".join(temp_startup)
|
||||
|
||||
## If 'feature_fan_combing' is True then add additional 'MESH:NONMESH' lines for travel moves over 5 lines long
|
||||
## For compatibility with 5.3.0 change any MESH:NOMESH to MESH:NONMESH.
|
||||
if feature_fan_combing:
|
||||
for layer_num in range(2,len(data)):
|
||||
layer = data[layer_num]
|
||||
data[layer_num] = re.sub(";MESH:NOMESH", ";MESH:NONMESH", layer)
|
||||
data = self._add_travel_comment(data, layer_0_index)
|
||||
|
||||
# Single Fan "By Layer"--------------------------------------------
|
||||
if by_layer_or_feature == "by_layer" and not is_multi_fan:
|
||||
return self._single_fan_by_layer(data, layer_0_index, fan_list, t0_fan)
|
||||
|
||||
# Multi-Fan "By Layer"---------------------------------------------
|
||||
if by_layer_or_feature == "by_layer" and is_multi_fan:
|
||||
return self._multi_fan_by_layer(data, layer_0_index, fan_list, t0_fan, t1_fan, t2_fan, t3_fan)
|
||||
|
||||
#Single Fan "By Feature"------------------------------------------
|
||||
if by_layer_or_feature == "by_feature" and (not is_multi_fan or not is_multi_extr_print):
|
||||
return self._single_fan_by_feature(data, layer_0_index, the_start_layer, the_end_layer, the_end_is_enabled, fan_list, t0_fan, feature_speed_list, feature_name_list, feature_fan_combing)
|
||||
|
||||
#Multi Fan "By Feature"-------------------------------------------
|
||||
if by_layer_or_feature == "by_feature" and is_multi_fan:
|
||||
return self._multi_fan_by_feature(data, layer_0_index, the_start_layer, the_end_layer, the_end_is_enabled, fan_list, t0_fan, t1_fan, t2_fan, t3_fan, feature_speed_list, feature_name_list, feature_fan_combing)
|
||||
|
||||
# The Single Fan "By Layer"----------------------------------------
|
||||
def _single_fan_by_layer(self, data: str, layer_0_index: int, fan_list: str, t0_fan: str)->str:
|
||||
layer_number = "0"
|
||||
single_fan_data = data
|
||||
for l_index in range(layer_0_index,len(single_fan_data)-1,1):
|
||||
layer = single_fan_data[l_index]
|
||||
fan_lines = layer.split("\n")
|
||||
for fan_line in fan_lines:
|
||||
if ";LAYER:" in fan_line:
|
||||
layer_number = str(fan_line.split(":")[1])
|
||||
## If there is a match for the current layer number make the insertion
|
||||
for num in range(0,15,2):
|
||||
if layer_number == str(fan_list[num]):
|
||||
layer = layer.replace(fan_lines[0],fan_lines[0] + "\n" + fan_list[num + 1] + str(t0_fan))
|
||||
single_fan_data[l_index] = layer
|
||||
return single_fan_data
|
||||
|
||||
# Multi-Fan "By Layer"-----------------------------------------
|
||||
def _multi_fan_by_layer(self, data: str, layer_0_index: int, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str)->str:
|
||||
multi_fan_data = data
|
||||
layer_number = "0"
|
||||
current_fan_speed = "0"
|
||||
prev_fan = str(t0_fan)
|
||||
this_fan = str(t0_fan)
|
||||
start_index = str(len(multi_fan_data))
|
||||
for num in range(0,15,2):
|
||||
## The fan_list may not be in ascending order. Get the lowest layer number
|
||||
try:
|
||||
if int(fan_list[num]) < int(start_index):
|
||||
start_index = str(fan_list[num])
|
||||
except:
|
||||
pass
|
||||
## Move the start point if delete_existing_m106 is false
|
||||
start_index = int(start_index) + int(layer_0_index)
|
||||
## Track the tool number
|
||||
for num in range(1,int(start_index),1):
|
||||
layer = multi_fan_data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line == "T0":
|
||||
prev_fan = this_fan
|
||||
this_fan = t0_fan
|
||||
elif line == "T1":
|
||||
prev_fan = this_fan
|
||||
this_fan = t1_fan
|
||||
elif line == "T2":
|
||||
prev_fan = this_fan
|
||||
this_fan = t2_fan
|
||||
elif line == "T3":
|
||||
prev_fan = this_fan
|
||||
this_fan = t3_fan
|
||||
for l_index in range(int(start_index),len(multi_fan_data)-1,1):
|
||||
modified_data = ""
|
||||
layer = multi_fan_data[l_index]
|
||||
fan_lines = layer.split("\n")
|
||||
for fan_line in fan_lines:
|
||||
## Prepare to shut down the previous fan and start the next one.
|
||||
if fan_line.startswith("T"):
|
||||
if fan_line == "T0": this_fan = str(t0_fan)
|
||||
if fan_line == "T1": this_fan = str(t1_fan)
|
||||
if fan_line == "T2": this_fan = str(t2_fan)
|
||||
if fan_line == "T3": this_fan = str(t3_fan)
|
||||
modified_data += "M106 S0" + prev_fan + "\n"
|
||||
modified_data += fan_line + "\n"
|
||||
modified_data += "M106 S" + str(current_fan_speed) + this_fan + "\n"
|
||||
prev_fan = this_fan
|
||||
elif ";LAYER:" in fan_line:
|
||||
modified_data += fan_line + "\n"
|
||||
layer_number = str(fan_line.split(":")[1])
|
||||
for num in range(0,15,2):
|
||||
if layer_number == str(fan_list[num]):
|
||||
modified_data += fan_list[num + 1] + this_fan + "\n"
|
||||
current_fan_speed = str(fan_list[num + 1].split("S")[1])
|
||||
current_fan_speed = str(current_fan_speed.split(" ")[0]) ## Just in case
|
||||
else:
|
||||
modified_data += fan_line + "\n"
|
||||
if modified_data.endswith("\n"): modified_data = modified_data[0:-1]
|
||||
multi_fan_data[l_index] = modified_data
|
||||
return multi_fan_data
|
||||
|
||||
# Single fan by feature-----------------------------------------------
|
||||
def _single_fan_by_feature(self, data: str, layer_0_index: int, the_start_layer: str, the_end_layer: str, the_end_is_enabled: str, fan_list: str, t0_fan: str, feature_speed_list: str, feature_name_list: str, feature_fan_combing: bool)->str:
|
||||
single_fan_data = data
|
||||
layer_number = "0"
|
||||
index = 1
|
||||
## Start with layer:0
|
||||
for l_index in range(layer_0_index,len(single_fan_data)-1,1):
|
||||
modified_data = ""
|
||||
layer = single_fan_data[l_index]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if ";LAYER:" in line:
|
||||
layer_number = str(line.split(":")[1])
|
||||
if int(layer_number) >= int(the_start_layer) and int(layer_number) < int(the_end_layer)-1:
|
||||
temp = line.split(" ")[0]
|
||||
try:
|
||||
name_index = feature_name_list.index(temp)
|
||||
except:
|
||||
name_index = -1
|
||||
if name_index != -1:
|
||||
modified_data += feature_speed_list[name_index] + t0_fan + "\n"
|
||||
elif ";MESH:NONMESH" in line:
|
||||
if feature_fan_combing == True:
|
||||
modified_data += "M106 S0" + t0_fan + "\n"
|
||||
modified_data += line + "\n"
|
||||
## If an End Layer is defined and is less than the last layer then insert the Final Speed
|
||||
if line == ";LAYER:" + str(the_end_layer) and the_end_is_enabled == True:
|
||||
modified_data += feature_speed_list[len(feature_speed_list) - 1] + t0_fan + "\n"
|
||||
if modified_data.endswith("\n"): modified_data = modified_data[0: - 1]
|
||||
single_fan_data[l_index] = modified_data
|
||||
return single_fan_data
|
||||
|
||||
# Multi-fan by feature------------------------------------------------
|
||||
def _multi_fan_by_feature(self, data: str, layer_0_index: int, the_start_layer: str, the_end_layer: str, the_end_is_enabled: str, fan_list: str, t0_fan: str, t1_fan: str, t2_fan: str, t3_fan: str, feature_speed_list: str, feature_name_list: str, feature_fan_combing: bool)->str:
|
||||
multi_fan_data = data
|
||||
layer_number = "0"
|
||||
start_index = 1
|
||||
prev_fan = t0_fan
|
||||
this_fan = t0_fan
|
||||
modified_data = ""
|
||||
current_fan_speed = "0"
|
||||
for my_index in range(1, len(multi_fan_data) - 1, 1):
|
||||
layer = multi_fan_data[my_index]
|
||||
if ";LAYER:" + str(the_start_layer) + "\n" in layer:
|
||||
start_index = int(my_index) - 1
|
||||
break
|
||||
## Track the previous tool changes
|
||||
for num in range(1,start_index,1):
|
||||
layer = multi_fan_data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line == "T0":
|
||||
prev_fan = this_fan
|
||||
this_fan = t0_fan
|
||||
elif line == "T1":
|
||||
prev_fan = this_fan
|
||||
this_fan = t1_fan
|
||||
elif line == "T2":
|
||||
prev_fan = this_fan
|
||||
this_fan = t2_fan
|
||||
elif line == "T3":
|
||||
prev_fan = this_fan
|
||||
this_fan = t3_fan
|
||||
## Get the current tool.
|
||||
for l_index in range(start_index,start_index + 1,1):
|
||||
layer = multi_fan_data[l_index]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("T"):
|
||||
if line == "T0": this_fan = t0_fan
|
||||
if line == "T1": this_fan = t1_fan
|
||||
if line == "T2": this_fan = t2_fan
|
||||
if line == "T3": this_fan = t3_fan
|
||||
prev_fan = this_fan
|
||||
|
||||
## Start to make insertions-------------------------------------
|
||||
for l_index in range(start_index+1,len(multi_fan_data)-1,1):
|
||||
layer = multi_fan_data[l_index]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("T"):
|
||||
if line == "T0": this_fan = t0_fan
|
||||
if line == "T1": this_fan = t1_fan
|
||||
if line == "T2": this_fan = t2_fan
|
||||
if line == "T3": this_fan = t3_fan
|
||||
## Turn off the prev fan
|
||||
modified_data += "M106 S0" + prev_fan + "\n"
|
||||
modified_data += line + "\n"
|
||||
## Turn on the current fan
|
||||
modified_data += "M106 S" + str(current_fan_speed) + this_fan + "\n"
|
||||
prev_fan = this_fan
|
||||
if ";LAYER:" in line:
|
||||
layer_number = str(line.split(":")[1])
|
||||
modified_data += line + "\n"
|
||||
if int(layer_number) >= int(the_start_layer):
|
||||
temp = line.split(" ")[0]
|
||||
try:
|
||||
name_index = feature_name_list.index(temp)
|
||||
except:
|
||||
name_index = -1
|
||||
if name_index != -1:
|
||||
modified_data += line + "\n" + feature_speed_list[name_index] + this_fan + "\n"
|
||||
#modified_data += feature_speed_list[name_index] + this_fan + "\n"
|
||||
current_fan_speed = str(feature_speed_list[name_index].split("S")[1])
|
||||
elif ";MESH:NONMESH" in line:
|
||||
if feature_fan_combing == True:
|
||||
modified_data += line + "\n"
|
||||
modified_data += "M106 S0" + this_fan + "\n"
|
||||
current_fan_speed = "0"
|
||||
else:
|
||||
modified_data += line + "\n"
|
||||
## If an end layer is defined - Insert the final speed and set the other variables to Final Speed to finish the file
|
||||
## There cannot be a break here because if there are multiple fan numbers they still need to be shut off and turned on.
|
||||
elif line == ";LAYER:" + str(the_end_layer):
|
||||
modified_data += feature_speed_list[len(feature_speed_list) - 1] + this_fan + "\n"
|
||||
for set_speed in range(0, len(feature_speed_list) - 2):
|
||||
feature_speed_list[set_speed] = feature_speed_list[len(feature_speed_list) - 1]
|
||||
else:
|
||||
## Layer and Tool get inserted into modified_data above. All other lines go into modified_data here
|
||||
if not line.startswith("T") and not line.startswith(";LAYER:"): modified_data += line + "\n"
|
||||
if modified_data.endswith("\n"): modified_data = modified_data[0: - 1]
|
||||
multi_fan_data[l_index] = modified_data
|
||||
modified_data = ""
|
||||
return multi_fan_data
|
||||
|
||||
#Try to catch layer input errors, set the minimum speed to 12%, and put the strings together
|
||||
def _layer_checker(self, fan_string: str, ty_pe: str, fan_mode: bool) -> str:
|
||||
fan_string_l = str(fan_string.split("/")[0])
|
||||
try:
|
||||
if int(fan_string_l) <= 1: fan_string_l = "1"
|
||||
if fan_string_l == "": fan_string_l = str(len(data))
|
||||
except ValueError:
|
||||
fan_string_l = str(len(data))
|
||||
fan_string_l = str(int(fan_string_l) - 1)
|
||||
fan_string_p = str(fan_string.split("/")[1])
|
||||
if fan_string_p == "": fan_string_p = "0"
|
||||
try:
|
||||
if int(fan_string_p) < 0: fan_string_p = "0"
|
||||
if int(fan_string_p) > 100: fan_string_p = "100"
|
||||
except ValueError:
|
||||
fan_string_p = "0"
|
||||
## Set the minimum fan speed to 12%
|
||||
if int(fan_string_p) < 12 and int(fan_string_p) != 0:
|
||||
fan_string_p = "12"
|
||||
fan_layer_line = str(fan_string_l)
|
||||
if fan_mode:
|
||||
fan_percent_line = "M106 S" + str(round(int(fan_string_p) * 2.55))
|
||||
else:
|
||||
fan_percent_line = "M106 S" + str(round(int(fan_string_p) / 100, 1))
|
||||
if ty_pe == "l":
|
||||
return str(fan_layer_line)
|
||||
elif ty_pe == "p":
|
||||
return fan_percent_line
|
||||
|
||||
#Try to catch feature input errors, set the minimum speed to 12%, and put the strings together when 'By Feature'
|
||||
def _feature_checker(self, fan_feat_string: int, fan_mode: bool) -> str:
|
||||
if fan_feat_string < 0: fan_feat_string = 0
|
||||
## Set the minimum fan speed to 12%
|
||||
if fan_feat_string > 0 and fan_feat_string < 12: fan_feat_string = 12
|
||||
if fan_feat_string > 100: fan_feat_string = 100
|
||||
if fan_mode:
|
||||
fan_sp_feat = "M106 S" + str(round(fan_feat_string * 2.55))
|
||||
else:
|
||||
fan_sp_feat = "M106 S" + str(round(fan_feat_string / 100, 1))
|
||||
return fan_sp_feat
|
||||
|
||||
# Add additional travel comments to turn the fan off during combing.
|
||||
def _add_travel_comment(self, comment_data: str, lay_0_index: str) -> str:
|
||||
for lay_num in range(int(lay_0_index), len(comment_data)-1,1):
|
||||
layer = comment_data[lay_num]
|
||||
lines = layer.split("\n")
|
||||
## Copy the data to new_data and make the insertions there
|
||||
new_data = lines
|
||||
g0_count = 0
|
||||
g0_index = -1
|
||||
feature_type = ";TYPE:SUPPORT"
|
||||
is_travel = False
|
||||
for index, line in enumerate(lines):
|
||||
insert_index = 0
|
||||
if ";TYPE:" in line:
|
||||
feature_type = line
|
||||
is_travel = False
|
||||
g0_count = 0
|
||||
if ";MESH:NONMESH" in line:
|
||||
is_travel = True
|
||||
g0_count = 0
|
||||
if line.startswith("G0 ") and not is_travel:
|
||||
g0_count += 1
|
||||
if g0_index == -1:
|
||||
g0_index = lines.index(line)
|
||||
elif not line.startswith("G0 ") and not is_travel:
|
||||
## Add additional 'NONMESH' lines to shut the fan off during long combing moves--------
|
||||
if g0_count > 5:
|
||||
if not is_travel:
|
||||
new_data.insert(g0_index + insert_index, ";MESH:NONMESH")
|
||||
insert_index += 1
|
||||
## Add the feature_type at the end of the combing move to turn the fan back on
|
||||
new_data.insert(g0_index + g0_count + 1, feature_type)
|
||||
insert_index += 1
|
||||
g0_count = 0
|
||||
g0_index = -1
|
||||
is_travel = False
|
||||
elif g0_count <= 5:
|
||||
g0_count = 0
|
||||
g0_index = -1
|
||||
is_travel = False
|
||||
comment_data[lay_num] = "\n".join(new_data)
|
||||
return comment_data
|
|
@ -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:
|
||||
|
@ -21,7 +21,7 @@
|
|||
# M163 - Set Mix Factor
|
||||
# M164 - Save Mix - saves to T2 as a unique mix
|
||||
|
||||
import re #To perform the search and replace.
|
||||
import re # To perform the search and replace.
|
||||
from ..Script import Script
|
||||
|
||||
class ColorMix(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])
|
||||
|
|
|
@ -3,21 +3,15 @@
|
|||
# Date: August 28, 2018
|
||||
# Modified: November 16, 2018 by Joshua Pope-Lewis
|
||||
|
||||
# Description: This plugin shows custom messages about your print on the Status bar...
|
||||
# Please look at the 5 options
|
||||
# - Scrolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you aren't printing a small item select this option.
|
||||
# - Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - Type a custom name in here
|
||||
# - Start Num: Choose which number you prefer for the initial layer, 0 or 1
|
||||
# - Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||
# - Add prefix 'Printing': Enabling this will add the prefix 'Printing'
|
||||
# Description: This plugin is now an option in 'Display Info on LCD'
|
||||
|
||||
from ..Script import Script
|
||||
from UM.Application import Application
|
||||
from UM.Message import Message
|
||||
|
||||
class DisplayFilenameAndLayerOnLCD(Script):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def initialize(self) -> None:
|
||||
Message(title = "[Display Filename and Layer on LCD]", text = "This script is now an option in 'Display Info on LCD'. This post processor no longer works.").show()
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Display Filename And Layer On LCD",
|
||||
|
@ -26,40 +20,10 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
|||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"scroll":
|
||||
"enable_script":
|
||||
{
|
||||
"label": "Scroll enabled/Small layers?",
|
||||
"description": "If SCROLL_LONG_FILENAMES is enabled select this setting however, if the model is small disable this setting!",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
},
|
||||
"name":
|
||||
{
|
||||
"label": "Text to display:",
|
||||
"description": "By default the current filename will be displayed on the LCD. Enter text here to override the filename and display something else.",
|
||||
"type": "str",
|
||||
"default_value": ""
|
||||
},
|
||||
"startNum":
|
||||
{
|
||||
"label": "Initial layer number:",
|
||||
"description": "Choose which number you prefer for the initial layer, 0 or 1",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 1
|
||||
},
|
||||
"maxlayer":
|
||||
{
|
||||
"label": "Display max layer?:",
|
||||
"description": "Display how many layers are in the entire print on status bar?",
|
||||
"type": "bool",
|
||||
"default_value": true
|
||||
},
|
||||
"addPrefixPrinting":
|
||||
{
|
||||
"label": "Add prefix 'Printing'?",
|
||||
"description": "This will add the prefix 'Printing'",
|
||||
"label": "Deprecated/Obsolete",
|
||||
"description": "This script is now included in 'Display Info on LCD'.",
|
||||
"type": "bool",
|
||||
"default_value": true
|
||||
}
|
||||
|
@ -67,43 +31,6 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
|||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
max_layer = 0
|
||||
lcd_text = "M117 "
|
||||
if self.getSettingValueByKey("name") != "":
|
||||
name = self.getSettingValueByKey("name")
|
||||
else:
|
||||
name = Application.getInstance().getPrintInformation().jobName
|
||||
if self.getSettingValueByKey("addPrefixPrinting"):
|
||||
lcd_text += "Printing "
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
lcd_text += "Layer "
|
||||
else:
|
||||
lcd_text += name + " - Layer "
|
||||
i = self.getSettingValueByKey("startNum")
|
||||
for layer in data:
|
||||
display_text = lcd_text + str(i)
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
max_layer = line
|
||||
max_layer = max_layer.split(":")[1]
|
||||
if self.getSettingValueByKey("startNum") == 0:
|
||||
max_layer = str(int(max_layer) - 1)
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.getSettingValueByKey("maxlayer"):
|
||||
display_text = display_text + " of " + max_layer
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + name
|
||||
else:
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + name + "!"
|
||||
else:
|
||||
display_text = display_text + "!"
|
||||
line_index = lines.index(line)
|
||||
lines.insert(line_index + 1, display_text)
|
||||
i += 1
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
|
||||
Message(title = "[Display Filename and Layer on LCD]", text = "This post is now included in 'Display Info on LCD'. This script will exit.").show()
|
||||
data[0] += "; [Display Filename and Layer on LCD] Did not run. It is now included in 'Display Info on LCD'.\n"
|
||||
return data
|
||||
|
|
483
plugins/PostProcessingPlugin/scripts/DisplayInfoOnLCD.py
Normal file
483
plugins/PostProcessingPlugin/scripts/DisplayInfoOnLCD.py
Normal file
|
@ -0,0 +1,483 @@
|
|||
# Display Filename and Layer on the LCD by Amanda de Castilho on August 28, 2018
|
||||
# Modified: Joshua Pope-Lewis on November 16, 2018
|
||||
# Display Progress on LCD by Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez on July 31, 2019
|
||||
# Show Progress was adapted from Display Progress by Louis Wooters on January 6, 2020. His changes are included here.
|
||||
#---------------------------------------------------------------
|
||||
# DisplayNameOrProgressOnLCD.py
|
||||
# Cura Post-Process plugin
|
||||
# Combines 'Display Filename and Layer on the LCD' with 'Display Progress'
|
||||
# Combined and with additions by: GregValiant (Greg Foresi)
|
||||
# Date: September 8, 2023
|
||||
# NOTE: This combined post processor will make 'Display Filename and Layer on the LCD' and 'Display Progress' obsolete
|
||||
# Description: Display Filename and Layer options:
|
||||
# Status messages sent to the printer...
|
||||
# - Scrolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you aren't printing a small item select this option.
|
||||
# - Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - You may enter a custom name here
|
||||
# - Start Num: Choose which number you prefer for the initial layer, 0 or 1
|
||||
# - Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||
# - Add prefix 'Printing': Enabling this will add the prefix 'Printing'
|
||||
# - Example Line on LCD: Printing Layer 0 of 395 3DBenchy
|
||||
# Display Progress options:
|
||||
# - Display Total Layer Count
|
||||
# - Disply Time Remaining for the print
|
||||
# - Time Fudge Factor % - Divide the Actual Print Time by the Cura Estimate. Enter as a percentage and the displayed time will be adjusted. This allows you to bring the displayed time closer to reality (Ex: Entering 87.5 would indicate an adjustment to 87.5% of the Cura estimate).
|
||||
# - Example line on LCD: 1/479 | ET 2h13m
|
||||
# - Time to Pauses changes the M117/M118 lines to countdown to the next pause as 1/479 | TP 2h36m
|
||||
# - 'Add M118 Line' is available with either option. M118 will bounce the message back to a remote print server through the USB connection.
|
||||
# - 'Add M73 Line' is used by 'Display Progress' only. There are options to incluse M73 P(percent) and M73 R(time remaining)
|
||||
# - Enable 'Finish-Time' Message - when enabled, takes the Print Time and calculates when the print will end. It takes into account the Time Fudge Factor. The user may enter a print start time. This is also available for Display Filename.
|
||||
|
||||
from ..Script import Script
|
||||
from UM.Application import Application
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
import time
|
||||
import datetime
|
||||
import math
|
||||
from UM.Message import Message
|
||||
|
||||
class DisplayInfoOnLCD(Script):
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Display Info on LCD",
|
||||
"key": "DisplayInfoOnLCD",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"display_option":
|
||||
{
|
||||
"label": "LCD display option...",
|
||||
"description": "Display Filename and Layer was formerly 'Display Filename and Layer on LCD' post-processor. The message format on the LCD is 'Printing Layer 0 of 15 3D Benchy'. Display Progress is similar to the former 'Display Progress on LCD' post-processor. The display format is '1/16 | ET 2hr28m'. Display Progress includes a fudge factor for the print time estimate.",
|
||||
"type": "enum",
|
||||
"options": {
|
||||
"display_progress": "Display Progress",
|
||||
"filename_layer": "Filename and Layer"
|
||||
},
|
||||
"default_value": "display_progress"
|
||||
},
|
||||
"format_option":
|
||||
{
|
||||
"label": "Scroll enabled/Small layers?",
|
||||
"description": "If SCROLL_LONG_FILENAMES is enabled in your firmware select this setting.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
},
|
||||
"file_name":
|
||||
{
|
||||
"label": "Text to display:",
|
||||
"description": "By default the current filename will be displayed on the LCD. Enter text here to override the filename and display something else.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
},
|
||||
"startNum":
|
||||
{
|
||||
"label": "Initial layer number:",
|
||||
"description": "Choose which number you prefer for the initial layer, 0 or 1",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 1,
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
},
|
||||
"maxlayer":
|
||||
{
|
||||
"label": "Display max layer?:",
|
||||
"description": "Display how many layers are in the entire print on status bar?",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
},
|
||||
"addPrefixPrinting":
|
||||
{
|
||||
"label": "Add prefix 'Printing'?",
|
||||
"description": "This will add the prefix 'Printing'",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
},
|
||||
"display_total_layers":
|
||||
{
|
||||
"label": "Display total layers",
|
||||
"description": "This setting adds the 'Total Layers' to the LCD message as '17/234'.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
},
|
||||
"display_remaining_time":
|
||||
{
|
||||
"label": "Display remaining time",
|
||||
"description": "This will add the remaining printing time to the LCD message.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
},
|
||||
"add_m118_line":
|
||||
{
|
||||
"label": "Add M118 Line",
|
||||
"description": "Adds M118 in addition to the M117. It will bounce the message back through the USB port to a computer print server (if a printer server like Octoprint or Pronterface is in use).",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
},
|
||||
"add_m73_line":
|
||||
{
|
||||
"label": "Add M73 Line(s)",
|
||||
"description": "Adds M73 in addition to the M117. For some firmware this will set the printers time and or percentage.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
},
|
||||
"add_m73_percent":
|
||||
{
|
||||
"label": " Add M73 Percentage",
|
||||
"description": "Adds M73 with the P parameter. For some firmware this will set the printers 'percentage' of layers completed and it will count upward.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m73_line and display_option == 'display_progress'"
|
||||
},
|
||||
"add_m73_time":
|
||||
{
|
||||
"label": " Add M73 Time",
|
||||
"description": "Adds M73 with the R parameter. For some firmware this will set the printers 'print time' and it will count downward.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m73_line and display_option == 'display_progress'"
|
||||
},
|
||||
"speed_factor":
|
||||
{
|
||||
"label": "Time Fudge Factor %",
|
||||
"description": "When using 'Display Progress' tweak this value to get better estimates. ([Actual Print Time]/[Cura Estimate]) x 100 = Time Fudge Factor. If Cura estimated 9hr and the print actually took 10hr30min then enter 117 here to adjust any estimate closer to reality. This Fudge Factor is also used to calculate the print finish time.",
|
||||
"type": "float",
|
||||
"unit": "%",
|
||||
"default_value": 100,
|
||||
"enabled": "enable_end_message or display_option == 'display_progress'"
|
||||
},
|
||||
"countdown_to_pause":
|
||||
{
|
||||
"label": "Countdown to Pauses",
|
||||
"description": "Instead of the remaining print time the LCD will show the estimated time to pause (TP).",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
},
|
||||
"enable_end_message":
|
||||
{
|
||||
"label": "Enable 'Finish-Time' Message",
|
||||
"description": "Get a message when you save a fresh slice. It will show the estimated date and time that the print would finish.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": true
|
||||
},
|
||||
"print_start_time":
|
||||
{
|
||||
"label": "Print Start Time (Ex 16:45)",
|
||||
"description": "Use 'Military' time. 16:45 would be 4:45PM. 09:30 would be 9:30AM. If you leave this blank it will be assumed that the print will start Now. If you enter a guesstimate of your printer start time and that time is before 'Now' the guesstimate will consider that the print will start tomorrow at the entered time. ",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "hrs ",
|
||||
"enabled": "enable_end_message"
|
||||
}
|
||||
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
display_option = self.getSettingValueByKey("display_option")
|
||||
add_m118_line = self.getSettingValueByKey("add_m118_line")
|
||||
add_m73_line = self.getSettingValueByKey("add_m73_line")
|
||||
add_m73_time = self.getSettingValueByKey("add_m73_time")
|
||||
add_m73_percent = self.getSettingValueByKey("add_m73_percent")
|
||||
|
||||
# This is Display Filename and Layer on LCD---------------------------------------------------------
|
||||
if display_option == "filename_layer":
|
||||
max_layer = 0
|
||||
lcd_text = "M117 "
|
||||
if self.getSettingValueByKey("file_name") != "":
|
||||
file_name = self.getSettingValueByKey("file_name")
|
||||
else:
|
||||
file_name = Application.getInstance().getPrintInformation().jobName
|
||||
if self.getSettingValueByKey("addPrefixPrinting"):
|
||||
lcd_text += "Printing "
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
lcd_text += "Layer "
|
||||
else:
|
||||
lcd_text += file_name + " - Layer "
|
||||
i = self.getSettingValueByKey("startNum")
|
||||
for layer in data:
|
||||
display_text = lcd_text + str(i)
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
max_layer = line
|
||||
max_layer = max_layer.split(":")[1]
|
||||
if self.getSettingValueByKey("startNum") == 0:
|
||||
max_layer = str(int(max_layer) - 1)
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.getSettingValueByKey("maxlayer"):
|
||||
display_text = display_text + " of " + max_layer
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + file_name
|
||||
else:
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + file_name + "!"
|
||||
else:
|
||||
display_text = display_text + "!"
|
||||
line_index = lines.index(line)
|
||||
lines.insert(line_index + 1, display_text)
|
||||
if add_m118_line:
|
||||
lines.insert(line_index + 2, str(display_text.replace("M117", "M118", 1)))
|
||||
i += 1
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self.message_to_user(self.getSettingValueByKey("speed_factor") / 100)
|
||||
Message(title = "Display Info on LCD - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
return data
|
||||
|
||||
# Display Progress (from 'Show Progress' and 'Display Progress on LCD')---------------------------------------
|
||||
elif display_option == "display_progress":
|
||||
# get settings
|
||||
display_total_layers = self.getSettingValueByKey("display_total_layers")
|
||||
display_remaining_time = self.getSettingValueByKey("display_remaining_time")
|
||||
speed_factor = self.getSettingValueByKey("speed_factor") / 100
|
||||
m73_time = False
|
||||
m73_percent = False
|
||||
if add_m73_line and add_m73_time:
|
||||
m73_time = True
|
||||
if add_m73_line and add_m73_percent:
|
||||
m73_percent = True
|
||||
# initialize global variables
|
||||
first_layer_index = 0
|
||||
time_total = 0
|
||||
number_of_layers = 0
|
||||
time_elapsed = 0
|
||||
# if at least one of the settings is disabled, there is enough room on the display to display "layer"
|
||||
first_section = data[0]
|
||||
lines = first_section.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME:"):
|
||||
tindex = lines.index(line)
|
||||
cura_time = int(line.split(":")[1])
|
||||
print_time = cura_time * speed_factor
|
||||
hhh = print_time/3600
|
||||
hr = round(hhh // 1)
|
||||
mmm = round((hhh % 1) * 60)
|
||||
orig_hhh = cura_time/3600
|
||||
orig_hr = round(orig_hhh // 1)
|
||||
orig_mmm = math.floor((orig_hhh % 1) * 60)
|
||||
orig_sec = round((((orig_hhh % 1) * 60) % 1) * 60)
|
||||
if add_m118_line: lines.insert(tindex + 3,"M118 Adjusted Print Time " + str(hr) + "hr " + str(mmm) + "min")
|
||||
lines.insert(tindex + 3,"M117 ET " + str(hr) + "hr " + str(mmm) + "min")
|
||||
# add M73 line at beginning
|
||||
mins = int(60 * hr + mmm)
|
||||
if m73_time:
|
||||
lines.insert(tindex + 3, "M73 R{}".format(mins))
|
||||
if m73_percent:
|
||||
lines.insert(tindex + 3, "M73 P0")
|
||||
# If Countdonw to pause is enabled then count the pauses
|
||||
pause_str = ""
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
pause_count = 0
|
||||
for num in range(2,len(data) - 1, 1):
|
||||
if "PauseAtHeight.py" in data[num]:
|
||||
pause_count += 1
|
||||
pause_str = f" with {pause_count} pause(s)"
|
||||
# This line goes in to convert seconds to hours and minutes
|
||||
lines.insert(tindex + 3, f";Cura Time Estimate: {cura_time}sec = {orig_hr}hr {orig_mmm}min {orig_sec}sec {pause_str}")
|
||||
data[0] = "\n".join(lines)
|
||||
data[len(data)-1] += "M117 Orig Cura Est " + str(orig_hr) + "hr " + str(orig_mmm) + "min\n"
|
||||
if add_m118_line: data[len(data)-1] += "M118 Est w/FudgeFactor " + str(speed_factor * 100) + "% was " + str(hr) + "hr " + str(mmm) + "min\n"
|
||||
if not display_total_layers or not display_remaining_time:
|
||||
base_display_text = "layer "
|
||||
else:
|
||||
base_display_text = ""
|
||||
layer = data[len(data)-1]
|
||||
data[len(data)-1] = layer.replace(";End of Gcode" + "\n", "")
|
||||
data[len(data)-1] += ";End of Gcode" + "\n"
|
||||
# Search for the number of layers and the total time from the start code
|
||||
for index in range(len(data)):
|
||||
data_section = data[index]
|
||||
# We have everything we need, save the index of the first layer and exit the loop
|
||||
if ";LAYER:" in data_section:
|
||||
first_layer_index = index
|
||||
break
|
||||
else:
|
||||
for line in data_section.split("\n"):
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
number_of_layers = int(line.split(":")[1])
|
||||
elif line.startswith(";TIME:"):
|
||||
time_total = int(line.split(":")[1])
|
||||
# for all layers...
|
||||
current_layer = 0
|
||||
for layer_counter in range(len(data)-2):
|
||||
current_layer += 1
|
||||
layer_index = first_layer_index + layer_counter
|
||||
display_text = base_display_text
|
||||
display_text += str(current_layer)
|
||||
# create a list where each element is a single line of code within the layer
|
||||
lines = data[layer_index].split("\n")
|
||||
if not ";LAYER:" in data[layer_index]:
|
||||
current_layer -= 1
|
||||
continue
|
||||
# add the total number of layers if this option is checked
|
||||
if display_total_layers:
|
||||
display_text += "/" + str(number_of_layers)
|
||||
# if display_remaining_time is checked, it is calculated in this loop
|
||||
if display_remaining_time:
|
||||
time_remaining_display = " | ET " # initialize the time display
|
||||
m = (time_total - time_elapsed) // 60 # estimated time in minutes
|
||||
m *= speed_factor # correct for printing time
|
||||
m = int(m)
|
||||
h, m = divmod(m, 60) # convert to hours and minutes
|
||||
# add the time remaining to the display_text
|
||||
if h > 0: # if it's more than 1 hour left, display format = xhxxm
|
||||
time_remaining_display += str(h) + "h"
|
||||
if m < 10: # add trailing zero if necessary
|
||||
time_remaining_display += "0"
|
||||
time_remaining_display += str(m) + "m"
|
||||
else:
|
||||
time_remaining_display += str(m) + "m"
|
||||
display_text += time_remaining_display
|
||||
# find time_elapsed at the end of the layer (used to calculate the remaining time of the next layer)
|
||||
if not current_layer == number_of_layers:
|
||||
for line_index in range(len(lines) - 1, -1, -1):
|
||||
line = lines[line_index]
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
# update time_elapsed for the NEXT layer and exit the loop
|
||||
time_elapsed = int(float(line.split(":")[1]))
|
||||
break
|
||||
# insert the text AFTER the first line of the layer (in case other scripts use ";LAYER:")
|
||||
for l_index, line in enumerate(lines):
|
||||
if line.startswith(";LAYER:"):
|
||||
lines[l_index] += "\nM117 " + display_text
|
||||
# add M73 line
|
||||
mins = int(60 * h + m)
|
||||
if m73_time:
|
||||
lines[l_index] += "\nM73 R{}".format(mins)
|
||||
if m73_percent:
|
||||
lines[l_index] += "\nM73 P" + str(round(int(current_layer) / int(number_of_layers) * 100))
|
||||
if add_m118_line:
|
||||
lines[l_index] += "\nM118 " + display_text
|
||||
break
|
||||
# overwrite the layer with the modified layer
|
||||
data[layer_index] = "\n".join(lines)
|
||||
|
||||
# If enabled then change the ET to TP for 'Time To Pause'
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
time_list = []
|
||||
time_list.append("0")
|
||||
time_list.append("0")
|
||||
this_time = 0
|
||||
pause_index = 1
|
||||
|
||||
# Get the layer times
|
||||
for num in range(2,len(data) - 1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
this_time = (float(line.split(":")[1]))*speed_factor
|
||||
time_list.append(str(this_time))
|
||||
if "PauseAtHeight.py" in layer:
|
||||
for qnum in range(num - 1, pause_index, -1):
|
||||
time_list[qnum] = str(float(this_time) - float(time_list[qnum])) + "P"
|
||||
pause_index = num-1
|
||||
|
||||
# Make the adjustments to the M117 (and M118) lines that are prior to a pause
|
||||
for num in range (2, len(data) - 1,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("M117") and "|" in line and "P" in time_list[num]:
|
||||
M117_line = line.split("|")[0] + "| TP "
|
||||
alt_time = time_list[num][:-1]
|
||||
hhh = int(float(alt_time) / 3600)
|
||||
if hhh > 0:
|
||||
hhr = str(hhh) + "h"
|
||||
else:
|
||||
hhr = ""
|
||||
mmm = ((float(alt_time) / 3600) - (int(float(alt_time) / 3600))) * 60
|
||||
sss = int((mmm - int(mmm)) * 60)
|
||||
mmm = str(round(mmm)) + "m"
|
||||
time_to_go = str(hhr) + str(mmm)
|
||||
if hhr == "": time_to_go = time_to_go + str(sss) + "s"
|
||||
M117_line = M117_line + time_to_go
|
||||
layer = layer.replace(line, M117_line)
|
||||
if line.startswith("M118") and "|" in line and "P" in time_list[num]:
|
||||
M118_line = line.split("|")[0] + "| TP " + time_to_go
|
||||
layer = layer.replace(line, M118_line)
|
||||
data[num] = layer
|
||||
setting_data = ""
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self.message_to_user(speed_factor)
|
||||
Message(title = "[Display Info on LCD] - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
return data
|
||||
|
||||
def message_to_user(self, speed_factor: float):
|
||||
# Message the user of the projected finish time of the print
|
||||
print_time = Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)
|
||||
print_start_time = self.getSettingValueByKey("print_start_time")
|
||||
# If the user entered a print start time make sure it is in the correct format or ignore it.
|
||||
if print_start_time == "" or print_start_time == "0" or len(print_start_time) != 5 or not ":" in print_start_time:
|
||||
print_start_time = ""
|
||||
# Change the print start time to proper time format, or, use the current time
|
||||
if print_start_time != "":
|
||||
hr = int(print_start_time.split(":")[0])
|
||||
min = int(print_start_time.split(":")[1])
|
||||
sec = 0
|
||||
else:
|
||||
hr = int(time.strftime("%H"))
|
||||
min = int(time.strftime("%M"))
|
||||
sec = int(time.strftime("%S"))
|
||||
|
||||
#Get the current data/time info
|
||||
yr = int(time.strftime("%Y"))
|
||||
day = int(time.strftime("%d"))
|
||||
mo = int(time.strftime("%m"))
|
||||
|
||||
date_and_time = datetime.datetime(yr, mo, day, hr, min, sec)
|
||||
#Split the Cura print time
|
||||
pr_hr = int(print_time.split(":")[0])
|
||||
pr_min = int(print_time.split(":")[1])
|
||||
pr_sec = int(print_time.split(":")[2])
|
||||
#Adjust the print time if none was entered
|
||||
print_seconds = pr_hr*3600 + pr_min*60 + pr_sec
|
||||
#Adjust the total seconds by the Fudge Factor
|
||||
adjusted_print_time = print_seconds * speed_factor
|
||||
#Break down the adjusted seconds back into hh:mm:ss
|
||||
adj_hr = int(adjusted_print_time/3600)
|
||||
print_seconds = adjusted_print_time - (adj_hr * 3600)
|
||||
adj_min = int(print_seconds) / 60
|
||||
adj_sec = int(print_seconds - (adj_min * 60))
|
||||
#Get the print time to add to the start time
|
||||
time_change = datetime.timedelta(hours=adj_hr, minutes=adj_min, seconds=adj_sec)
|
||||
new_time = date_and_time + time_change
|
||||
#Get the day of the week that the print will end on
|
||||
week_day = str(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][int(new_time.strftime("%w"))])
|
||||
#Get the month that the print will end in
|
||||
mo_str = str(["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][int(new_time.strftime("%m"))-1])
|
||||
#Make adjustments from 24hr time to 12hr time
|
||||
if int(new_time.strftime("%H")) > 12:
|
||||
show_hr = str(int(new_time.strftime("%H")) - 12) + ":"
|
||||
show_ampm = " PM"
|
||||
elif int(new_time.strftime("%H")) == 0:
|
||||
show_hr = "12:"
|
||||
show_ampm = " AM"
|
||||
else:
|
||||
show_hr = str(new_time.strftime("%H")) + ":"
|
||||
show_ampm = " AM"
|
||||
if print_start_time == "":
|
||||
start_str = "Now"
|
||||
else:
|
||||
start_str = "and your entered 'print start time' of " + print_start_time + "hrs."
|
||||
if print_start_time != "":
|
||||
print_start_str = "Print Start Time................." + str(print_start_time) + "hrs"
|
||||
else:
|
||||
print_start_str = "Print Start Time.................Now."
|
||||
estimate_str = "Cura Time Estimate.........." + str(print_time)
|
||||
adjusted_str = "Adjusted Time Estimate..." + str(time_change)
|
||||
finish_str = week_day + " " + str(mo_str) + " " + str(new_time.strftime("%d")) + ", " + str(new_time.strftime("%Y")) + " at " + str(show_hr) + str(new_time.strftime("%M")) + str(show_ampm)
|
||||
return finish_str, estimate_str, adjusted_str, print_start_str
|
|
@ -7,14 +7,13 @@
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
import re
|
||||
import datetime
|
||||
from UM.Message import Message
|
||||
|
||||
class DisplayProgressOnLCD(Script):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def initialize(self) -> None:
|
||||
Message(title = "[Display Progress on LCD]", text = "This script is now an option in 'Display Info on LCD'. This post processor no longer works.").show()
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Display Progress On LCD",
|
||||
|
@ -23,176 +22,17 @@ class DisplayProgressOnLCD(Script):
|
|||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"time_remaining":
|
||||
"enable_script":
|
||||
{
|
||||
"label": "Time Remaining",
|
||||
"description": "Select to write remaining time to the display.Select to write remaining time on the display using M117 status line message (almost all printers) or using M73 command (Prusa and Marlin 2 if enabled).",
|
||||
"label": "Deprecated/Obsolete",
|
||||
"description": "This script is now included in 'Display Info on LCD'.",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
},
|
||||
"time_remaining_method":
|
||||
{
|
||||
"label": "Time Reporting Method",
|
||||
"description": "How should remaining time be shown on the display? It could use a generic message command (M117, almost all printers), or a specialised time remaining command (M73, Prusa and Marlin 2).",
|
||||
"type": "enum",
|
||||
"options": {
|
||||
"m117":"M117 - All printers",
|
||||
"m73":"M73 - Prusa, Marlin 2",
|
||||
"m118":"M118 - Octoprint"
|
||||
},
|
||||
"enabled": "time_remaining",
|
||||
"default_value": "m117"
|
||||
},
|
||||
"update_frequency":
|
||||
{
|
||||
"label": "Update frequency",
|
||||
"description": "Update remaining time for every layer or periodically every minute or faster.",
|
||||
"type": "enum",
|
||||
"options": {"0":"Every layer","15":"Every 15 seconds","30":"Every 30 seconds","60":"Every minute"},
|
||||
"default_value": "0",
|
||||
"enabled": "time_remaining"
|
||||
},
|
||||
"percentage":
|
||||
{
|
||||
"label": "Percentage",
|
||||
"description": "When enabled, set the completion bar percentage on the LCD using Marlin's M73 command.",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
"default_value": true
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
# Get the time value from a line as a float.
|
||||
# Example line ;TIME_ELAPSED:1234.6789 or ;TIME:1337
|
||||
def getTimeValue(self, line):
|
||||
list_split = re.split(":", line) # Split at ":" so we can get the numerical value
|
||||
return float(list_split[1]) # Convert the numerical portion to a float
|
||||
|
||||
def outputTime(self, lines, line_index, time_left, mode):
|
||||
# Do some math to get the time left in seconds into the right format. (HH,MM,SS)
|
||||
time_left = max(time_left, 0)
|
||||
m, s = divmod(time_left, 60)
|
||||
h, m = divmod(m, 60)
|
||||
# Create the string
|
||||
if mode == "m117":
|
||||
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||
# And now insert that into the GCODE
|
||||
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
|
||||
elif mode == "m118":
|
||||
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||
# And now insert that into the GCODE
|
||||
lines.insert(line_index, "M118 A1 P0 action:notification Time Left {}".format(current_time_string))
|
||||
else:
|
||||
mins = int(60 * h + m + s / 30)
|
||||
lines.insert(line_index, "M73 R{}".format(mins))
|
||||
|
||||
def execute(self, data):
|
||||
output_time = self.getSettingValueByKey("time_remaining")
|
||||
output_time_method = self.getSettingValueByKey("time_remaining_method")
|
||||
output_frequency = int(self.getSettingValueByKey("update_frequency"))
|
||||
output_percentage = self.getSettingValueByKey("percentage")
|
||||
line_set = {}
|
||||
if output_percentage or output_time:
|
||||
total_time = -1
|
||||
previous_layer_end_percentage = 0
|
||||
previous_layer_end_time = 0
|
||||
for layer in data:
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
|
||||
for line in lines:
|
||||
if (line.startswith(";TIME:") or line.startswith(";PRINT.TIME:")) and total_time == -1:
|
||||
# This line represents the total time required to print the gcode
|
||||
total_time = self.getTimeValue(line)
|
||||
line_index = lines.index(line)
|
||||
|
||||
# In the beginning we may have 2 M73 lines, but it makes logic less complicated
|
||||
if output_time:
|
||||
self.outputTime(lines, line_index, total_time, output_time_method)
|
||||
|
||||
if output_percentage:
|
||||
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
|
||||
if output_time_method == "m118":
|
||||
lines.insert(line_index, "M118 A1 P0 action:notification Data Left 0/100")
|
||||
else:
|
||||
lines.insert(line_index, "M73 P0")
|
||||
|
||||
elif line.startswith(";TIME_ELAPSED:"):
|
||||
# We've found one of the time elapsed values which are added at the end of layers
|
||||
|
||||
# If we have seen this line before then skip processing it. We can see lines multiple times because we are adding
|
||||
# intermediate percentages before the line being processed. This can cause the current line to shift back and be
|
||||
# encountered more than once
|
||||
if line in line_set:
|
||||
continue
|
||||
line_set[line] = True
|
||||
|
||||
# If total_time was not already found then noop
|
||||
if total_time == -1:
|
||||
continue
|
||||
|
||||
current_time = self.getTimeValue(line)
|
||||
line_index = lines.index(line)
|
||||
|
||||
if output_time:
|
||||
if output_frequency == 0:
|
||||
# Here we calculate remaining time
|
||||
self.outputTime(lines, line_index, total_time - current_time, output_time_method)
|
||||
else:
|
||||
# Here we calculate remaining time and how many outputs are expected for the layer
|
||||
layer_time_delta = int(current_time - previous_layer_end_time)
|
||||
layer_step_delta = int((current_time - previous_layer_end_time) / output_frequency)
|
||||
# If this layer represents less than 1 step then we don't need to emit anything, continue to the next layer
|
||||
if layer_step_delta != 0:
|
||||
# Grab the index of the current line and figure out how many lines represent one second
|
||||
step = line_index / layer_time_delta
|
||||
# Move new lines further as we add new lines above it
|
||||
lines_added = 1
|
||||
# Run through layer in seconds
|
||||
for seconds in range(1, layer_time_delta + 1):
|
||||
# Time from start to decide when to update
|
||||
line_time = int(previous_layer_end_time + seconds)
|
||||
# Output every X seconds and after last layer
|
||||
if line_time % output_frequency == 0 or line_time == total_time:
|
||||
# Line to add the output
|
||||
time_line_index = int((seconds * step) + lines_added)
|
||||
|
||||
# Insert remaining time into the GCODE
|
||||
self.outputTime(lines, time_line_index, total_time - line_time, output_time_method)
|
||||
# Next line will be again lower
|
||||
lines_added = lines_added + 1
|
||||
|
||||
previous_layer_end_time = int(current_time)
|
||||
|
||||
if output_percentage:
|
||||
# Calculate percentage value this layer ends at
|
||||
layer_end_percentage = int((current_time / total_time) * 100)
|
||||
|
||||
# Figure out how many percent of the total time is spent in this layer
|
||||
layer_percentage_delta = layer_end_percentage - previous_layer_end_percentage
|
||||
|
||||
# If this layer represents less than 1 percent then we don't need to emit anything, continue to the next layer
|
||||
if layer_percentage_delta != 0:
|
||||
# Grab the index of the current line and figure out how many lines represent one percent
|
||||
step = line_index / layer_percentage_delta
|
||||
|
||||
for percentage in range(1, layer_percentage_delta + 1):
|
||||
# We add the percentage value here as while processing prior lines we will have inserted
|
||||
# percentage lines before the current one. Failing to do this will upset the spacing
|
||||
percentage_line_index = int((percentage * step) + percentage)
|
||||
|
||||
# Due to integer truncation of the total time value in the gcode the percentage we
|
||||
# calculate may slightly exceed 100, as that is not valid we cap the value here
|
||||
output = min(percentage + previous_layer_end_percentage, 100)
|
||||
|
||||
# Now insert the sanitized percentage into the GCODE
|
||||
if output_time_method == "m118":
|
||||
lines.insert(percentage_line_index, "M118 A1 P0 action:notification Data Left {}/100".format(output))
|
||||
else:
|
||||
lines.insert(percentage_line_index, "M73 P{}".format(output))
|
||||
|
||||
previous_layer_end_percentage = layer_end_percentage
|
||||
|
||||
# Join up the lines for this layer again and store them in the data array
|
||||
data[layer_index] = "\n".join(lines)
|
||||
Message(title = "[Display Progress on LCD]", text = "This post is now included in 'Display Info on LCD'. This script will exit.").show()
|
||||
data[0] += "; [Display Progress on LCD] Did not run. It is now included in 'Display Info on LCD'.\n"
|
||||
return data
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
# Copyright (c) 2023 Ultimaker B.V.
|
||||
# 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.
|
||||
|
@ -7,7 +7,7 @@
|
|||
from typing import List
|
||||
from ..Script import Script
|
||||
|
||||
from UM.Application import Application #To get the current printer's settings.
|
||||
from UM.Application import Application # To get the current printer's settings.
|
||||
|
||||
class FilamentChange(Script):
|
||||
|
||||
|
@ -199,7 +199,7 @@ class FilamentChange(Script):
|
|||
if enable_before_macro:
|
||||
color_change = color_change + before_macro + "\n"
|
||||
|
||||
color_change = color_change + "M600\n"
|
||||
color_change = color_change + "M600"
|
||||
|
||||
if not firmware_config:
|
||||
if initial_retract is not None and initial_retract > 0.:
|
||||
|
@ -213,13 +213,15 @@ class FilamentChange(Script):
|
|||
|
||||
if x_pos is not None:
|
||||
color_change = color_change + (" X%.2f" % x_pos)
|
||||
|
||||
|
||||
if y_pos is not None:
|
||||
color_change = color_change + (" Y%.2f" % y_pos)
|
||||
|
||||
|
||||
if z_pos is not None and z_pos > 0.:
|
||||
color_change = color_change + (" Z%.2f" % z_pos)
|
||||
|
||||
color_change = color_change + "\n"
|
||||
|
||||
if enable_after_macro:
|
||||
color_change = color_change + after_macro + "\n"
|
||||
|
||||
|
|
357
plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py
Normal file
357
plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py
Normal file
|
@ -0,0 +1,357 @@
|
|||
# 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
|
||||
# 2/4/2024 Added a block so the script doesn't run unless Accel Control is enabled in Cura. This should keep users from increasing the Accel Limits.
|
||||
|
||||
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()
|
||||
|
||||
# Warn the user if Accel Control is not enabled in Cura. This keeps the script from being able to increase the Accel limits-----------
|
||||
if not bool(extruder[0].getProperty("acceleration_enabled", "value")):
|
||||
Message(title = "[Limit the X-Y Accel/Jerk]", text = "You must have 'Enable Acceleration Control' checked in Cura or the script will exit.").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"))
|
||||
acceleration_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
|
||||
|
||||
# Exit if acceleration control is not enabled----------------
|
||||
if not acceleration_enabled:
|
||||
Message(title = "[Limit the X-Y Accel/Jerk]", text = "DID NOT RUN. You must have 'Enable Acceleration Control' checked in Cura.").show()
|
||||
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because 'Enable Acceleration Control' is not checked in Cura.\n"
|
||||
return data
|
||||
|
||||
# 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 = 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")
|
||||
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
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
from ..Script import Script
|
||||
import re
|
||||
from UM.Application import Application #To get the current printer's settings.
|
||||
from UM.Application import Application # To get the current printer's settings.
|
||||
from UM.Logger import Logger
|
||||
|
||||
from typing import List, Tuple
|
||||
|
@ -46,7 +46,7 @@ class PauseAtHeight(Script):
|
|||
"pause_layer":
|
||||
{
|
||||
"label": "Pause Layer",
|
||||
"description": "Enter the Number of the LAST layer you want to finish prior to the pause (from the Cura preview).",
|
||||
"description": "Enter the Number of the LAST layer you want to finish prior to the pause. Note that 0 is the first layer printed.",
|
||||
"type": "int",
|
||||
"value": "math.floor((pause_height - 0.27) / 0.1) + 1",
|
||||
"minimum_value": "0",
|
||||
|
@ -67,7 +67,7 @@ class PauseAtHeight(Script):
|
|||
"label": "Keep motors engaged",
|
||||
"description": "Keep the steppers engaged to allow change of filament without moving the head. Applying too much force will move the head/bed anyway",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"default_value": false,
|
||||
"enabled": "pause_method != \\\"griffin\\\""
|
||||
},
|
||||
"disarm_timeout":
|
||||
|
@ -218,7 +218,7 @@ class PauseAtHeight(Script):
|
|||
"label": "Beep at pause",
|
||||
"description": "Make a beep when pausing",
|
||||
"type": "bool",
|
||||
"default_value": true
|
||||
"default_value": false
|
||||
},
|
||||
"beep_length":
|
||||
{
|
||||
|
@ -338,7 +338,7 @@ class PauseAtHeight(Script):
|
|||
nbr_negative_layers += 1
|
||||
|
||||
#Track the latest printing temperature in order to resume at the correct temperature.
|
||||
if line.startswith("T"):
|
||||
if re.match("T(\d*)", line):
|
||||
current_t = self.getValue(line, "T")
|
||||
m = self.getValue(line, "M")
|
||||
if m is not None and (m == 104 or m == 109) and self.getValue(line, "S") is not None:
|
||||
|
@ -478,10 +478,11 @@ class PauseAtHeight(Script):
|
|||
prepend_gcode += "M117 " + display_text + "\n"
|
||||
|
||||
# Set the disarm timeout
|
||||
if hold_steppers_on:
|
||||
prepend_gcode += self.putValue(M = 84, S = 3600) + " ; Keep steppers engaged for 1h\n"
|
||||
elif disarm_timeout > 0:
|
||||
prepend_gcode += self.putValue(M = 84, S = disarm_timeout) + " ; Set the disarm timeout\n"
|
||||
if pause_method != "griffin":
|
||||
if hold_steppers_on:
|
||||
prepend_gcode += self.putValue(M = 84, S = 3600) + " ; Keep steppers engaged for 1h\n"
|
||||
elif disarm_timeout > 0:
|
||||
prepend_gcode += self.putValue(M = 84, S = disarm_timeout) + " ; Set the disarm timeout\n"
|
||||
|
||||
# Beep at pause
|
||||
if beep_at_pause:
|
||||
|
|
|
@ -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,7 +1,7 @@
|
|||
# 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.
|
||||
import re # To perform the search and replace.
|
||||
|
||||
from ..Script import Script
|
||||
|
||||
|
|
|
@ -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,13 +1,14 @@
|
|||
# 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
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.FileHandler.WriteFileJob import WriteFileJob
|
||||
from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary).
|
||||
from UM.FileHandler.FileWriter import FileWriter # To check against the write modes (text vs. binary).
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
|
@ -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":
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import math
|
||||
|
||||
from UM.Math.Color import Color
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -35,7 +36,7 @@ class SimulationPass(RenderPass):
|
|||
self._nozzle_shader = None
|
||||
self._disabled_shader = None
|
||||
self._old_current_layer = 0
|
||||
self._old_current_path = 0
|
||||
self._old_current_path: float = 0.0
|
||||
self._switching_layers = True # Tracking whether the user is moving across layers (True) or across paths (False). If false, lower layers render as shadowy.
|
||||
self._gl = OpenGL.getInstance().getBindingsObject()
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
|
@ -120,6 +121,7 @@ class SimulationPass(RenderPass):
|
|||
disabled_batch = RenderBatch(self._disabled_shader)
|
||||
head_position = None # Indicates the current position of the print head
|
||||
nozzle_node = None
|
||||
not_a_vector = Vector(math.nan, math.nan, math.nan)
|
||||
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
|
||||
|
@ -139,44 +141,67 @@ class SimulationPass(RenderPass):
|
|||
continue
|
||||
|
||||
# Render all layers below a certain number as line mesh instead of vertices.
|
||||
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
||||
if self._layer_view.getCurrentLayer() > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
||||
start = 0
|
||||
end = 0
|
||||
vertex_before_head = not_a_vector
|
||||
vertex_after_head = not_a_vector
|
||||
vertex_distance_ratio = 0.0
|
||||
towards_next_vertex = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer in sorted(element_counts.keys()):
|
||||
# In the current layer, we show just the indicated paths
|
||||
if layer == self._layer_view._current_layer_num:
|
||||
# We look for the position of the head, searching the point of the current path
|
||||
index = self._layer_view._current_path_num
|
||||
offset = 0
|
||||
index = int(self._layer_view.getCurrentPath()) if not math.isnan(
|
||||
self._layer_view.getCurrentPath()) else 0
|
||||
for polygon in layer_data.getLayer(layer).polygons:
|
||||
# The size indicates all values in the two-dimension array, and the second dimension is
|
||||
# always size 3 because we have 3D points.
|
||||
if index >= polygon.data.size // 3 - offset:
|
||||
index -= polygon.data.size // 3 - offset
|
||||
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
|
||||
if index >= polygon.data.size // 3 :
|
||||
index -= polygon.data.size // 3
|
||||
continue
|
||||
# The head position is calculated and translated
|
||||
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
|
||||
ratio = self._layer_view.getCurrentPath() - math.floor(self._layer_view.getCurrentPath())
|
||||
pos_a = Vector(polygon.data[index][0], polygon.data[index][1],
|
||||
polygon.data[index][2])
|
||||
vertex_before_head = pos_a
|
||||
vertex_distance_ratio = ratio
|
||||
if ratio <= 0.0001 or index + 1 == len(polygon.data):
|
||||
# in case there multiple polygons and polygon changes, the first point has the same value as the last point in the previous polygon
|
||||
head_position = pos_a + node.getWorldPosition()
|
||||
else:
|
||||
pos_b = Vector(polygon.data[index + 1][0],
|
||||
polygon.data[index + 1][1],
|
||||
polygon.data[index + 1][2])
|
||||
vec = pos_a * (1.0 - ratio) + pos_b * ratio
|
||||
head_position = vec + node.getWorldPosition()
|
||||
vertex_after_head = pos_b
|
||||
towards_next_vertex = 2 # Add two to the index to print the current and next vertices as an 'unfinished' line (to the nozzle).
|
||||
break
|
||||
break
|
||||
if self._layer_view._minimum_layer_num > layer:
|
||||
if self._layer_view.getMinimumLayer() > layer:
|
||||
start += element_counts[layer]
|
||||
end += element_counts[layer]
|
||||
|
||||
# Calculate the range of paths in the last layer
|
||||
current_layer_start = end
|
||||
current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice
|
||||
current_layer_end = end + int( self._layer_view.getCurrentPath()) * 2 # Because each point is used twice
|
||||
|
||||
# This uses glDrawRangeElements internally to only draw a certain range of lines.
|
||||
# All the layers but the current selected layer are rendered first
|
||||
if self._old_current_path != self._layer_view._current_path_num:
|
||||
if self._old_current_path != self._layer_view.getCurrentPath():
|
||||
self._current_shader = self._layer_shadow_shader
|
||||
self._switching_layers = False
|
||||
if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num:
|
||||
if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view.getCurrentLayer():
|
||||
self._current_shader = self._layer_shader
|
||||
self._switching_layers = True
|
||||
|
||||
# reset 'last vertex'
|
||||
self._layer_shader.setUniformValue("u_last_vertex", not_a_vector)
|
||||
self._layer_shader.setUniformValue("u_next_vertex", not_a_vector)
|
||||
self._layer_shader.setUniformValue("u_last_line_ratio", 1.0)
|
||||
|
||||
# The first line does not have a previous line: add a MoveCombingType in front for start detection
|
||||
# this way the first start of the layer can also be drawn
|
||||
prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
|
||||
|
@ -193,8 +218,19 @@ class SimulationPass(RenderPass):
|
|||
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
|
||||
current_layer_batch.render(self._scene.getActiveCamera())
|
||||
|
||||
self._old_current_layer = self._layer_view._current_layer_num
|
||||
self._old_current_path = self._layer_view._current_path_num
|
||||
# Last line may be partial
|
||||
if vertex_after_head != not_a_vector and vertex_after_head != not_a_vector:
|
||||
self._layer_shader.setUniformValue("u_last_vertex", vertex_before_head)
|
||||
self._layer_shader.setUniformValue("u_next_vertex", vertex_after_head)
|
||||
self._layer_shader.setUniformValue("u_last_line_ratio", vertex_distance_ratio)
|
||||
last_line_start = current_layer_end
|
||||
last_line_end = current_layer_end + towards_next_vertex
|
||||
last_line_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode=RenderBatch.RenderMode.Lines, range = (last_line_start, last_line_end))
|
||||
last_line_batch.addItem(node.getWorldTransformation(), layer_data)
|
||||
last_line_batch.render(self._scene.getActiveCamera())
|
||||
|
||||
self._old_current_layer = self._layer_view.getCurrentLayer()
|
||||
self._old_current_path = self._layer_view.getCurrentPath()
|
||||
|
||||
# Create a new batch that is not range-limited
|
||||
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
|
||||
|
@ -230,4 +266,4 @@ class SimulationPass(RenderPass):
|
|||
if changed_object.callDecoration("getLayerData"): # Any layer data has changed.
|
||||
self._switching_layers = True
|
||||
self._old_current_layer = 0
|
||||
self._old_current_path = 0
|
||||
self._old_current_path = 0.0
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import math
|
||||
import sys
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
|
@ -58,6 +58,7 @@ class SimulationView(CuraView):
|
|||
LAYER_VIEW_TYPE_LINE_TYPE = 1
|
||||
LAYER_VIEW_TYPE_FEEDRATE = 2
|
||||
LAYER_VIEW_TYPE_THICKNESS = 3
|
||||
SIMULATION_FACTOR = 2
|
||||
|
||||
_no_layers_warning_preference = "view/no_layers_warning"
|
||||
|
||||
|
@ -74,19 +75,20 @@ class SimulationView(CuraView):
|
|||
self._old_max_layers = 0
|
||||
|
||||
self._max_paths = 0
|
||||
self._current_path_num = 0
|
||||
self._current_path_num: float = 0.0
|
||||
self._current_time = 0.0
|
||||
self._minimum_path_num = 0
|
||||
self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
|
||||
|
||||
self._busy = False
|
||||
self._simulation_running = False
|
||||
|
||||
self._ghost_shader = None # type: Optional["ShaderProgram"]
|
||||
self._layer_pass = None # type: Optional[SimulationPass]
|
||||
self._composite_pass = None # type: Optional[CompositePass]
|
||||
self._old_layer_bindings = None # type: Optional[List[str]]
|
||||
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._old_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._ghost_shader: Optional["ShaderProgram"] = None
|
||||
self._layer_pass: Optional[SimulationPass] = None
|
||||
self._composite_pass: Optional[CompositePass] = None
|
||||
self._old_layer_bindings: Optional[List[str]] = None
|
||||
self._simulationview_composite_shader: Optional["ShaderProgram"] = None
|
||||
self._old_composite_shader: Optional["ShaderProgram"] = None
|
||||
|
||||
self._max_feedrate = sys.float_info.min
|
||||
self._min_feedrate = sys.float_info.max
|
||||
|
@ -96,14 +98,16 @@ class SimulationView(CuraView):
|
|||
self._min_line_width = sys.float_info.max
|
||||
self._min_flow_rate = sys.float_info.max
|
||||
self._max_flow_rate = sys.float_info.min
|
||||
self._cumulative_line_duration_layer: Optional[int] = None
|
||||
self._cumulative_line_duration: List[float] = []
|
||||
|
||||
self._global_container_stack = None # type: Optional[ContainerStack]
|
||||
self._global_container_stack: Optional[ContainerStack] = None
|
||||
self._proxy = None
|
||||
|
||||
self._resetSettings()
|
||||
self._legend_items = None
|
||||
self._show_travel_moves = False
|
||||
self._nozzle_node = None # type: Optional[NozzleNode]
|
||||
self._nozzle_node: Optional[NozzleNode] = None
|
||||
|
||||
Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
|
||||
Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
|
||||
|
@ -125,17 +129,12 @@ class SimulationView(CuraView):
|
|||
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
|
||||
self._compatibility_mode = self._evaluateCompatibilityMode()
|
||||
|
||||
self._wireprint_warning_message = Message(catalog.i18nc("@info:status",
|
||||
"Cura does not accurately display layers when Wire Printing is enabled."),
|
||||
title = catalog.i18nc("@info:title", "Simulation View"),
|
||||
message_type = Message.MessageType.WARNING)
|
||||
self._slice_first_warning_message = Message(catalog.i18nc("@info:status",
|
||||
"Nothing is shown because you need to slice first."),
|
||||
title = catalog.i18nc("@info:title", "No layers to show"),
|
||||
option_text = catalog.i18nc("@info:option_text",
|
||||
"Do not show this message again"),
|
||||
option_state = False,
|
||||
message_type = Message.MessageType.WARNING)
|
||||
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."),
|
||||
title=catalog.i18nc("@info:title", "No layers to show"),
|
||||
option_text=catalog.i18nc("@info:option_text",
|
||||
"Do not show this message again"),
|
||||
option_state=False,
|
||||
message_type=Message.MessageType.WARNING)
|
||||
self._slice_first_warning_message.optionToggled.connect(self._onDontAskMeAgain)
|
||||
CuraApplication.getInstance().getPreferences().addPreference(self._no_layers_warning_preference, True)
|
||||
|
||||
|
@ -191,9 +190,91 @@ class SimulationView(CuraView):
|
|||
def getMaxLayers(self) -> int:
|
||||
return self._max_layers
|
||||
|
||||
def getCurrentPath(self) -> int:
|
||||
def getCurrentPath(self) -> float:
|
||||
return self._current_path_num
|
||||
|
||||
def setTime(self, time: float) -> None:
|
||||
cumulative_line_duration = self.cumulativeLineDuration()
|
||||
if len(cumulative_line_duration) > 0:
|
||||
self._current_time = time
|
||||
left_i = 0
|
||||
right_i = len(cumulative_line_duration) - 1
|
||||
total_duration = cumulative_line_duration[-1]
|
||||
# make an educated guess about where to start
|
||||
i = int(right_i * max(0.0, min(1.0, self._current_time / total_duration)))
|
||||
# binary search for the correct path
|
||||
while left_i < right_i:
|
||||
if cumulative_line_duration[i] <= self._current_time:
|
||||
left_i = i + 1
|
||||
else:
|
||||
right_i = i
|
||||
i = int((left_i + right_i) / 2)
|
||||
|
||||
left_value = cumulative_line_duration[i - 1] if i > 0 else 0.0
|
||||
right_value = cumulative_line_duration[i]
|
||||
|
||||
if not (left_value <= self._current_time <= right_value):
|
||||
Logger.warn(
|
||||
f"Binary search error (out of bounds): index {i}: left value {left_value} right value {right_value} and current time is {self._current_time}")
|
||||
|
||||
segment_duration = right_value - left_value
|
||||
fractional_value = 0.0 if segment_duration == 0.0 else (self._current_time - left_value) / segment_duration
|
||||
|
||||
self.setPath(i + fractional_value)
|
||||
|
||||
def advanceTime(self, time_increase: float) -> bool:
|
||||
"""
|
||||
Advance the time by the given amount.
|
||||
|
||||
:param time_increase: The amount of time to advance (in seconds).
|
||||
:return: True if the time was advanced, False if the end of the simulation was reached.
|
||||
"""
|
||||
total_duration = 0.0
|
||||
if len(self.cumulativeLineDuration()) > 0:
|
||||
total_duration = self.cumulativeLineDuration()[-1]
|
||||
|
||||
if self._current_time + time_increase > total_duration:
|
||||
# If we have reached the end of the simulation, go to the next layer.
|
||||
if self.getCurrentLayer() == self.getMaxLayers():
|
||||
# If we are already at the last layer, go to the first layer.
|
||||
self.setTime(total_duration)
|
||||
return False
|
||||
|
||||
# advance to the next layer, and reset the time
|
||||
self.setLayer(self.getCurrentLayer() + 1)
|
||||
self.setTime(0.0)
|
||||
else:
|
||||
self.setTime(self._current_time + time_increase)
|
||||
return True
|
||||
|
||||
def cumulativeLineDuration(self) -> List[float]:
|
||||
# Make sure _cumulative_line_duration is initialized properly
|
||||
if self.getCurrentLayer() != self._cumulative_line_duration_layer:
|
||||
#clear cache
|
||||
self._cumulative_line_duration = []
|
||||
total_duration = 0.0
|
||||
polylines = self.getLayerData()
|
||||
if polylines is not None:
|
||||
for polyline in polylines.polygons:
|
||||
for line_duration in list((polyline.lineLengths / polyline.lineFeedrates)[0]):
|
||||
total_duration += line_duration / SimulationView.SIMULATION_FACTOR
|
||||
self._cumulative_line_duration.append(total_duration)
|
||||
# for tool change we add an extra tool path
|
||||
self._cumulative_line_duration.append(total_duration)
|
||||
# set current cached layer
|
||||
self._cumulative_line_duration_layer = self.getCurrentLayer()
|
||||
|
||||
return self._cumulative_line_duration
|
||||
|
||||
def getLayerData(self) -> Optional["LayerData"]:
|
||||
scene = self.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
return layer_data.getLayer(self.getCurrentLayer())
|
||||
return None
|
||||
|
||||
def getMinimumPath(self) -> int:
|
||||
return self._minimum_path_num
|
||||
|
||||
|
@ -281,7 +362,7 @@ class SimulationView(CuraView):
|
|||
self._startUpdateTopLayers()
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def setPath(self, value: int) -> None:
|
||||
def setPath(self, value: float) -> None:
|
||||
"""
|
||||
Set the upper end of the range of visible paths on the current layer.
|
||||
|
||||
|
@ -291,6 +372,12 @@ class SimulationView(CuraView):
|
|||
if self._current_path_num != value:
|
||||
self._current_path_num = min(max(value, 0), self._max_paths)
|
||||
self._minimum_path_num = min(self._minimum_path_num, self._current_path_num)
|
||||
# update _current time when the path is changed by user
|
||||
if self._current_path_num < self._max_paths and round(self._current_path_num)== self._current_path_num:
|
||||
actual_path_num = int(self._current_path_num)
|
||||
cumulative_line_duration = self.cumulativeLineDuration()
|
||||
if actual_path_num < len(cumulative_line_duration):
|
||||
self._current_time = cumulative_line_duration[actual_path_num]
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
self.currentPathNumChanged.emit()
|
||||
|
@ -496,6 +583,7 @@ class SimulationView(CuraView):
|
|||
self._max_thickness = sys.float_info.min
|
||||
self._min_flow_rate = sys.float_info.max
|
||||
self._max_flow_rate = sys.float_info.min
|
||||
self._cumulative_line_duration = {}
|
||||
|
||||
# The colour scheme is only influenced by the visible lines, so filter the lines by if they should be visible.
|
||||
visible_line_types = []
|
||||
|
@ -671,11 +759,8 @@ class SimulationView(CuraView):
|
|||
elif event.type == Event.ViewDeactivateEvent:
|
||||
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
|
||||
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
|
||||
self._wireprint_warning_message.hide()
|
||||
self._slice_first_warning_message.hide()
|
||||
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
if self._nozzle_node:
|
||||
self._nozzle_node.setParent(None)
|
||||
|
||||
|
@ -698,23 +783,10 @@ class SimulationView(CuraView):
|
|||
return self._current_layer_jumps
|
||||
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
self._onPropertyChanged("wireframe_enabled", "value")
|
||||
self.globalStackChanged.emit()
|
||||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _onPropertyChanged(self, key: str, property_name: str) -> None:
|
||||
if key == "wireframe_enabled" and property_name == "value":
|
||||
if self._global_container_stack and self._global_container_stack.getProperty("wireframe_enabled", "value"):
|
||||
self._wireprint_warning_message.show()
|
||||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _onCurrentLayerNumChanged(self) -> None:
|
||||
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
||||
|
|
|
@ -127,6 +127,7 @@ Item
|
|||
function resumeSimulation()
|
||||
{
|
||||
UM.SimulationView.setSimulationRunning(true)
|
||||
UM.SimulationView.setCurrentPath(UM.SimulationView.currentPath)
|
||||
simulationTimer.start()
|
||||
layerSlider.manuallyChanged = false
|
||||
pathSlider.manuallyChanged = false
|
||||
|
@ -136,54 +137,19 @@ Item
|
|||
Timer
|
||||
{
|
||||
id: simulationTimer
|
||||
interval: 100
|
||||
interval: 1000 / 15
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered:
|
||||
{
|
||||
var currentPath = UM.SimulationView.currentPath
|
||||
var numPaths = UM.SimulationView.numPaths
|
||||
var currentLayer = UM.SimulationView.currentLayer
|
||||
var numLayers = UM.SimulationView.numLayers
|
||||
|
||||
// When the user plays the simulation, if the path slider is at the end of this layer, we start
|
||||
// the simulation at the beginning of the current layer.
|
||||
if (!isSimulationPlaying)
|
||||
{
|
||||
if (currentPath >= numPaths)
|
||||
{
|
||||
UM.SimulationView.setCurrentPath(0)
|
||||
}
|
||||
else
|
||||
{
|
||||
UM.SimulationView.setCurrentPath(currentPath + 1)
|
||||
}
|
||||
}
|
||||
// If the simulation is already playing and we reach the end of a layer, then it automatically
|
||||
// starts at the beginning of the next layer.
|
||||
else
|
||||
{
|
||||
if (currentPath >= numPaths)
|
||||
{
|
||||
// At the end of the model, the simulation stops
|
||||
if (currentLayer >= numLayers)
|
||||
{
|
||||
playButton.pauseSimulation()
|
||||
}
|
||||
else
|
||||
{
|
||||
UM.SimulationView.setCurrentLayer(currentLayer + 1)
|
||||
UM.SimulationView.setCurrentPath(0)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UM.SimulationView.setCurrentPath(currentPath + 1)
|
||||
}
|
||||
// divide by 1000 to account for ms to s conversion
|
||||
const advance_time = simulationTimer.interval / 1000.0;
|
||||
if (!UM.SimulationView.advanceTime(advance_time)) {
|
||||
playButton.pauseSimulation();
|
||||
}
|
||||
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
|
||||
// correctly, because part of the logic is in this trigger function.
|
||||
isSimulationPlaying = true
|
||||
isSimulationPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,10 +50,14 @@ class SimulationViewProxy(QObject):
|
|||
def numPaths(self):
|
||||
return self._simulation_view.getMaxPaths()
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
@pyqtProperty(float, notify=currentPathChanged)
|
||||
def currentPath(self):
|
||||
return self._simulation_view.getCurrentPath()
|
||||
|
||||
@pyqtSlot(float, result=bool)
|
||||
def advanceTime(self, duration: float) -> bool:
|
||||
return self._simulation_view.advanceTime(duration)
|
||||
|
||||
@pyqtProperty(int, notify=currentPathChanged)
|
||||
def minimumPath(self):
|
||||
return self._simulation_view.getMinimumPath()
|
||||
|
@ -78,8 +82,8 @@ class SimulationViewProxy(QObject):
|
|||
def setMinimumLayer(self, layer_num):
|
||||
self._simulation_view.setMinimumLayer(layer_num)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def setCurrentPath(self, path_num):
|
||||
@pyqtSlot(float)
|
||||
def setCurrentPath(self, path_num: float):
|
||||
self._simulation_view.setPath(path_num)
|
||||
|
||||
@pyqtSlot(int)
|
||||
|
@ -215,4 +219,3 @@ class SimulationViewProxy(QObject):
|
|||
self._simulation_view.activityChanged.disconnect(self._onActivityChanged)
|
||||
self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged)
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ vertex41core =
|
|||
|
||||
uniform highp mat4 u_normalMatrix;
|
||||
|
||||
uniform vec3 u_last_vertex;
|
||||
uniform vec3 u_next_vertex;
|
||||
uniform float u_last_line_ratio;
|
||||
|
||||
in highp vec4 a_vertex;
|
||||
in lowp vec4 a_color;
|
||||
in lowp vec4 a_material_color;
|
||||
|
@ -134,6 +138,10 @@ vertex41core =
|
|||
void main()
|
||||
{
|
||||
vec4 v1_vertex = a_vertex;
|
||||
if (v1_vertex.xyz == u_next_vertex)
|
||||
{
|
||||
v1_vertex.xyz = mix(u_last_vertex, u_next_vertex, u_last_line_ratio);
|
||||
}
|
||||
v1_vertex.y -= a_line_dim.y / 2; // half layer down
|
||||
|
||||
vec4 world_space_vert = u_modelMatrix * v1_vertex;
|
||||
|
@ -427,6 +435,10 @@ u_max_feedrate = 1
|
|||
u_min_thickness = 0
|
||||
u_max_thickness = 1
|
||||
|
||||
u_last_vertex = [0.0, 0.0, 0.0]
|
||||
u_next_vertex = [0.0, 0.0, 0.0]
|
||||
u_last_line_ratio = 1.0
|
||||
|
||||
[bindings]
|
||||
u_modelMatrix = model_matrix
|
||||
u_viewMatrix = view_matrix
|
||||
|
|
|
@ -5,7 +5,7 @@ import json
|
|||
import os
|
||||
import platform
|
||||
import time
|
||||
from typing import cast, Optional, Set, TYPE_CHECKING
|
||||
from typing import Optional, Set, TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import pyqtSlot, QObject
|
||||
from PyQt6.QtNetwork import QNetworkRequest
|
||||
|
@ -264,6 +264,7 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
# Prime tower settings
|
||||
print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")
|
||||
print_settings["prime_tower_mode"] = global_stack.getProperty("prime_tower_mode", "value")
|
||||
|
||||
# Infill settings
|
||||
print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")
|
||||
|
|
|
@ -16,8 +16,6 @@ from UM.Application import Application
|
|||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Math.Color import Color
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Platform import Platform
|
||||
from UM.Event import Event
|
||||
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
|
|
|
@ -22,7 +22,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ try:
|
|||
except ImportError:
|
||||
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
|
||||
|
||||
from UM.i18n import i18nCatalog #To translate the file format description.
|
||||
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
|
||||
from UM.i18n import i18nCatalog # To translate the file format description.
|
||||
from UM.Mesh.MeshWriter import MeshWriter # For the binary mode flag.
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Ultimaker Network Connection",
|
||||
"name": "UltiMaker Network Connection",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Manages network connections to Ultimaker networked printers.",
|
||||
"description": "Manages network connections to UltiMaker networked printers.",
|
||||
"version": "2.0.0",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
|
|
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
|
||||
}
|
||||
|
||||
|
|
|
@ -96,6 +96,9 @@ class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
|
||||
@pyqtSlot(str)
|
||||
def printerSelected(self, unique_id: str):
|
||||
# The device that it defers the actual write to isn't hooked up correctly. So we should emit the write signal
|
||||
# here.
|
||||
self.writeStarted.emit(self)
|
||||
self._request_write_callback(unique_id, self._nodes)
|
||||
if self._on_print_dialog:
|
||||
self._on_print_dialog.close()
|
||||
|
|
|
@ -5,6 +5,7 @@ import urllib.parse
|
|||
from json import JSONDecodeError
|
||||
from time import time
|
||||
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
@ -38,14 +39,17 @@ class CloudApiClient:
|
|||
|
||||
# The cloud URL to use for this remote cluster.
|
||||
ROOT_PATH = UltimakerCloudConstants.CuraCloudAPIRoot
|
||||
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
||||
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
||||
CLUSTER_API_ROOT = f"{ROOT_PATH}/connect/v1"
|
||||
CURA_API_ROOT = f"{ROOT_PATH}/cura/v1"
|
||||
|
||||
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
|
||||
|
||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]
|
||||
|
||||
# Custom machine definition ID to cloud cluster name mapping
|
||||
_machine_id_to_name: Dict[str, str] = None
|
||||
|
||||
def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None:
|
||||
"""Initializes a new cloud API client.
|
||||
|
||||
|
@ -73,22 +77,27 @@ class CloudApiClient:
|
|||
|
||||
url = f"{self.CLUSTER_API_ROOT}/clusters?status=active"
|
||||
self._http.get(url,
|
||||
scope = self._scope,
|
||||
callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
|
||||
error_callback = failed,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
scope=self._scope,
|
||||
callback=self._parseCallback(on_finished, CloudClusterResponse, failed),
|
||||
error_callback=failed,
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def getClustersByMachineType(self, machine_type, on_finished: Callable[[List[CloudClusterWithConfigResponse]], Any], failed: Callable) -> None:
|
||||
# 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)
|
||||
machine_id_to_name = self.getMachineIDMap()
|
||||
if machine_type in machine_id_to_name:
|
||||
machine_type = machine_id_to_name[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,
|
||||
|
@ -105,9 +114,9 @@ class CloudApiClient:
|
|||
|
||||
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/status"
|
||||
self._http.get(url,
|
||||
scope = self._scope,
|
||||
callback = self._parseCallback(on_finished, CloudClusterStatus),
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
scope=self._scope,
|
||||
callback=self._parseCallback(on_finished, CloudClusterStatus),
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
||||
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
||||
|
@ -122,10 +131,10 @@ class CloudApiClient:
|
|||
data = json.dumps({"data": request.toDict()}).encode()
|
||||
|
||||
self._http.put(url,
|
||||
scope = self._scope,
|
||||
data = data,
|
||||
callback = self._parseCallback(on_finished, CloudPrintJobResponse),
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
scope=self._scope,
|
||||
data=data,
|
||||
callback=self._parseCallback(on_finished, CloudPrintJobResponse),
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
||||
|
@ -151,11 +160,11 @@ class CloudApiClient:
|
|||
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
|
||||
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print/{job_id}"
|
||||
self._http.post(url,
|
||||
scope = self._scope,
|
||||
data = b"",
|
||||
callback = self._parseCallback(on_finished, CloudPrintResponse),
|
||||
error_callback = on_error,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
scope=self._scope,
|
||||
data=b"",
|
||||
callback=self._parseCallback(on_finished, CloudPrintResponse),
|
||||
error_callback=on_error,
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
|
||||
data: Optional[Dict[str, Any]] = None) -> None:
|
||||
|
@ -165,14 +174,15 @@ class CloudApiClient:
|
|||
:param cluster_id: The ID of the cluster.
|
||||
:param cluster_job_id: The ID of the print job within the cluster.
|
||||
:param action: The name of the action to execute.
|
||||
:param data: Optional data to send with the POST request
|
||||
"""
|
||||
|
||||
body = json.dumps({"data": data}).encode() if data else b""
|
||||
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print_jobs/{cluster_job_id}/action/{action}"
|
||||
self._http.post(url,
|
||||
scope = self._scope,
|
||||
data = body,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
scope=self._scope,
|
||||
data=body,
|
||||
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
||||
"""We override _createEmptyRequest in order to add the user credentials.
|
||||
|
@ -207,8 +217,11 @@ class CloudApiClient:
|
|||
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
|
||||
return status_code, {"errors": [error.toDict()]}
|
||||
|
||||
def _parseResponse(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
|
||||
def _parseResponse(self,
|
||||
response: Dict[str, Any],
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model_class: Type[CloudApiClientModel]) -> None:
|
||||
"""Parses the given response and calls the correct callback depending on the result.
|
||||
|
||||
:param response: The response from the server, after being converted to a dict.
|
||||
|
@ -267,3 +280,14 @@ class CloudApiClient:
|
|||
|
||||
self._anti_gc_callbacks.append(parse)
|
||||
return parse
|
||||
|
||||
@classmethod
|
||||
def getMachineIDMap(cls) -> Dict[str, str]:
|
||||
if cls._machine_id_to_name is None:
|
||||
try:
|
||||
with open(Path(__file__).parent / "machine_id_to_name.json", "rt") as f:
|
||||
cls._machine_id_to_name = json.load(f)
|
||||
except Exception as e:
|
||||
Logger.logException("e", f"Could not load machine_id_to_name.json: '{e}'")
|
||||
cls._machine_id_to_name = {}
|
||||
return cls._machine_id_to_name
|
||||
|
|
|
@ -213,7 +213,12 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
return
|
||||
|
||||
# Export the scene to the correct file type.
|
||||
job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion)
|
||||
job = ExportFileJob(
|
||||
file_handler=file_handler,
|
||||
nodes=nodes,
|
||||
firmware_version=self.firmwareVersion,
|
||||
print_type=self.printerType,
|
||||
)
|
||||
job.finished.connect(self._onPrintJobCreated)
|
||||
job.start()
|
||||
|
||||
|
@ -318,16 +323,31 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def isMethod(self) -> bool:
|
||||
"""Whether the printer that this output device represents is a Method series printer."""
|
||||
|
||||
if not self._printers:
|
||||
return False
|
||||
|
||||
[printer, *_] = self._printers
|
||||
return printer.type in ("MakerBot Method X", "MakerBot Method XL")
|
||||
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
"""Whether the printer that this output device represents supports print job actions via the cloud."""
|
||||
|
||||
if not self._printers:
|
||||
return False
|
||||
|
||||
if self.isMethod:
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def supportsPrintJobQueue(self) -> bool:
|
||||
"""Gets whether the printer supports a queue"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"ultimaker_method": "MakerBot Method",
|
||||
"ultimaker_methodx": "MakerBot Method X",
|
||||
"ultimaker_methodxl": "MakerBot Method XL"
|
||||
}
|
|
@ -16,9 +16,9 @@ from .MeshFormatHandler import MeshFormatHandler
|
|||
class ExportFileJob(WriteFileJob):
|
||||
"""Job that exports the build plate to the correct file format for the target cluster."""
|
||||
|
||||
def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None:
|
||||
|
||||
self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version)
|
||||
def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str,
|
||||
print_type: str) -> None:
|
||||
self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version, print_type)
|
||||
if not self._mesh_format_handler.is_valid:
|
||||
Logger.log("e", "Missing file or mesh writer!")
|
||||
return
|
||||
|
|
|
@ -19,10 +19,9 @@ I18N_CATALOG = i18nCatalog("cura")
|
|||
class MeshFormatHandler:
|
||||
"""This class is responsible for choosing the formats used by the connected clusters."""
|
||||
|
||||
|
||||
def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None:
|
||||
def __init__(self, file_handler: Optional[FileHandler], firmware_version: str, printer_type: str) -> None:
|
||||
self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler()
|
||||
self._preferred_format = self._getPreferredFormat(firmware_version)
|
||||
self._preferred_format = self._getPreferredFormat(firmware_version, printer_type)
|
||||
self._writer = self._getWriter(self.mime_type) if self._preferred_format else None
|
||||
|
||||
@property
|
||||
|
@ -82,7 +81,7 @@ class MeshFormatHandler:
|
|||
value = value.encode()
|
||||
return value
|
||||
|
||||
def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]:
|
||||
def _getPreferredFormat(self, firmware_version: str, printer_type: str) -> Dict[str, Union[str, int, bool]]:
|
||||
"""Chooses the preferred file format for the given file handler.
|
||||
|
||||
:param firmware_version: The version of the firmware.
|
||||
|
@ -103,7 +102,9 @@ class MeshFormatHandler:
|
|||
machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
|
||||
|
||||
# Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
|
||||
if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"):
|
||||
if printer_type in (
|
||||
"ultimaker3", "ultimaker3_extended") and "application/x-ufp" not in machine_file_formats and Version(
|
||||
firmware_version) >= Version("4.4"):
|
||||
machine_file_formats = ["application/x-ufp"] + machine_file_formats
|
||||
|
||||
# Take the intersection between file_formats and machine_file_formats.
|
||||
|
|
|
@ -16,7 +16,7 @@ class LegacyDeviceNoLongerSupportedMessage(Message):
|
|||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not "
|
||||
"running Ultimaker Connect. Please update the printer to the "
|
||||
"running UltiMaker Connect. Please update the printer to the "
|
||||
"latest firmware."),
|
||||
title = I18N_CATALOG.i18nc("@info:title", "Update your printer"),
|
||||
lifetime = 10,
|
||||
|
|
|
@ -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"}})
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, List
|
||||
|
||||
import uuid
|
||||
|
||||
from .CloudClusterResponse import CloudClusterResponse
|
||||
from .ClusterPrinterStatus import ClusterPrinterStatus
|
||||
|
||||
|
@ -11,4 +13,20 @@ class CloudClusterWithConfigResponse(CloudClusterResponse):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.configuration = self.parseModel(ClusterPrinterStatus, kwargs.get("host_printer"))
|
||||
|
||||
# Some printers will return a null UUID in the host_printer.uuid field. For those we can fall back using
|
||||
# the host_guid field of the cluster data
|
||||
valid_uuid = False
|
||||
try:
|
||||
parsed_uuid = uuid.UUID(self.configuration.uuid)
|
||||
valid_uuid = parsed_uuid.int != 0
|
||||
except:
|
||||
pass
|
||||
|
||||
if not valid_uuid:
|
||||
try:
|
||||
self.configuration.uuid = kwargs.get("host_guid")
|
||||
except:
|
||||
pass
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -10,6 +10,7 @@ from PyQt6.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
|||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Version import Version
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
@ -86,7 +87,10 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
|
||||
@pyqtSlot(name="openPrinterControlPanel")
|
||||
def openPrinterControlPanel(self) -> None:
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
||||
if Version(self.firmwareVersion) >= Version("7.0.2"):
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
||||
else:
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
||||
|
||||
@pyqtSlot(str, name="sendJobToTop")
|
||||
def sendJobToTop(self, print_job_uuid: str) -> None:
|
||||
|
@ -142,7 +146,12 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self.writeStarted.emit(self)
|
||||
|
||||
# Export the scene to the correct file type.
|
||||
job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion)
|
||||
job = ExportFileJob(
|
||||
file_handler=file_handler,
|
||||
nodes=nodes,
|
||||
firmware_version=self.firmwareVersion,
|
||||
print_type=self.printerType,
|
||||
)
|
||||
job.finished.connect(self._onPrintJobCreated)
|
||||
job.start()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
||||
from .avr_isp import ispBase
|
||||
from .avr_isp.stk500v2 import Stk500v2
|
||||
|
||||
from time import time, sleep
|
||||
from serial import Serial, SerialException
|
||||
|
||||
|
@ -21,20 +18,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,14 +36,13 @@ 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:
|
||||
serial = Serial(str(self._serial_port), baud_rate, timeout = read_timeout, writeTimeout = write_timeout)
|
||||
except SerialException:
|
||||
Logger.logException("w", "Unable to create serial")
|
||||
Logger.warning(f"Unable to create serial connection to {serial} with baud rate {baud_rate}")
|
||||
continue
|
||||
else:
|
||||
# We already have a serial connection, just change the baud rate.
|
||||
|
@ -61,7 +50,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 +74,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.
|
||||
|
|
|
@ -5,9 +5,9 @@ import os
|
|||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Mesh.MeshWriter import MeshWriter #To get the g-code output.
|
||||
from UM.Message import Message #Show an error when already printing.
|
||||
from UM.PluginRegistry import PluginRegistry #To get the g-code output.
|
||||
from UM.Mesh.MeshWriter import MeshWriter # To get the g-code output.
|
||||
from UM.Message import Message # Show an error when already printing.
|
||||
from UM.PluginRegistry import PluginRegistry # To get the g-code output.
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -19,7 +19,7 @@ from cura.PrinterOutput.GenericOutputController import GenericOutputController
|
|||
from .AutoDetectBaudJob import AutoDetectBaudJob
|
||||
from .AvrFirmwareUpdater import AvrFirmwareUpdater
|
||||
|
||||
from io import StringIO #To write the g-code output.
|
||||
from io import StringIO # To write the g-code output.
|
||||
from queue import Queue
|
||||
from serial import Serial, SerialException, SerialTimeoutException
|
||||
from threading import Thread, Event
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Ultimaker machine actions",
|
||||
"name": "UltiMaker machine actions",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser #To read config files.
|
||||
import io #To write config files to strings as if they were files.
|
||||
import os.path #To get the path to write new user profiles to.
|
||||
import configparser # To read config files.
|
||||
import io # To write config files to strings as if they were files.
|
||||
import os.path # To get the path to write new user profiles to.
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
import urllib #To serialise the user container file name properly.
|
||||
import urllib # To serialise the user container file name properly.
|
||||
import urllib.parse
|
||||
|
||||
import UM.VersionUpgrade #To indicate that a file is of incorrect format.
|
||||
import UM.VersionUpgradeManager #To schedule more files to be upgraded.
|
||||
from UM.Resources import Resources #To get the config storage path.
|
||||
import UM.VersionUpgrade # To indicate that a file is of incorrect format.
|
||||
import UM.VersionUpgradeManager # To schedule more files to be upgraded.
|
||||
from UM.Resources import Resources # To get the config storage path.
|
||||
|
||||
## Creates a new machine instance instance by parsing a serialised machine
|
||||
# instance in version 1 of the file format.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser #To read config files.
|
||||
import io #To output config files to string.
|
||||
import configparser # To read config files.
|
||||
import io # To output config files to string.
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import UM.VersionUpgrade #To indicate that a file is of the wrong format.
|
||||
import UM.VersionUpgrade # To indicate that a file is of the wrong format.
|
||||
|
||||
## Creates a new preferences instance by parsing a serialised preferences file
|
||||
# in version 1 of the file format.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser #To read config files.
|
||||
import io #To write config files to strings as if they were files.
|
||||
import configparser # To read config files.
|
||||
import io # To write config files to strings as if they were files.
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import UM.VersionUpgrade
|
||||
|
|
|
@ -10,6 +10,7 @@ if TYPE_CHECKING:
|
|||
|
||||
upgrade = VersionUpgrade52to53.VersionUpgrade52to53()
|
||||
|
||||
|
||||
def getMetaData() -> Dict[str, Any]:
|
||||
return {
|
||||
"version_upgrade": {
|
||||
|
@ -21,6 +22,7 @@ def getMetaData() -> Dict[str, Any]:
|
|||
("quality_changes", 4000020): ("quality_changes", 4000021, upgrade.upgradeInstanceContainer),
|
||||
("quality", 4000020): ("quality", 4000021, upgrade.upgradeInstanceContainer),
|
||||
("user", 4000020): ("user", 4000021, upgrade.upgradeInstanceContainer),
|
||||
("intent", 4000020): ("intent", 4000021, upgrade.upgradeInstanceContainer),
|
||||
},
|
||||
"sources": {
|
||||
"preferences": {
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
# 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
|
||||
|
||||
_REMOVED_SETTINGS = {
|
||||
"wireframe_enabled",
|
||||
"wireframe_height",
|
||||
"wireframe_roof_inset",
|
||||
"wireframe_printspeed",
|
||||
"wireframe_printspeed_bottom",
|
||||
"wireframe_printspeed_up",
|
||||
"wireframe_printspeed_down",
|
||||
"wireframe_printspeed_flat",
|
||||
"wireframe_flow",
|
||||
"wireframe_flow_connection",
|
||||
"wireframe_flow_flat",
|
||||
"wireframe_top_delay",
|
||||
"wireframe_bottom_delay",
|
||||
"wireframe_flat_delay",
|
||||
"wireframe_up_half_speed",
|
||||
"wireframe_top_jump",
|
||||
"wireframe_fall_down",
|
||||
"wireframe_drag_along",
|
||||
"wireframe_strategy",
|
||||
"wireframe_straight_before_down",
|
||||
"wireframe_roof_fall_down",
|
||||
"wireframe_roof_drag_along",
|
||||
"wireframe_roof_outer_delay",
|
||||
"wireframe_nozzle_clearance",
|
||||
"support_tree_branch_distance",
|
||||
"support_tree_collision_resolution",
|
||||
}
|
||||
|
||||
_RENAMED_PROFILES = {
|
||||
"abs_040012": "elegoo_abs_nozzle_0.40_layer_0.10",
|
||||
"abs_040016": "elegoo_abs_nozzle_0.40_layer_0.15",
|
||||
"abs_040020": "elegoo_abs_nozzle_0.40_layer_0.20",
|
||||
"abs_040024": "elegoo_abs_nozzle_0.40_layer_0.20",
|
||||
"abs_040028": "elegoo_abs_nozzle_0.40_layer_0.30",
|
||||
|
||||
"asa_040012": "elegoo_asa_nozzle_0.40_layer_0.10",
|
||||
"asa_040016": "elegoo_asa_nozzle_0.40_layer_0.15",
|
||||
"asa_040020": "elegoo_asa_nozzle_0.40_layer_0.20",
|
||||
"asa_040024": "elegoo_asa_nozzle_0.40_layer_0.20",
|
||||
"asa_040028": "elegoo_asa_nozzle_0.40_layer_0.30",
|
||||
|
||||
"petg_040012": "elegoo_petg_nozzle_0.40_layer_0.10",
|
||||
"petg_040016": "elegoo_petg_nozzle_0.40_layer_0.15",
|
||||
"petg_040020": "elegoo_petg_nozzle_0.40_layer_0.20",
|
||||
"petg_040024": "elegoo_petg_nozzle_0.40_layer_0.20",
|
||||
"petg_040028": "elegoo_petg_nozzle_0.40_layer_0.30",
|
||||
|
||||
"pla_040012": "elegoo_pla_nozzle_0.40_layer_0.10",
|
||||
"pla_040016": "elegoo_pla_nozzle_0.40_layer_0.15",
|
||||
"pla_040020": "elegoo_pla_nozzle_0.40_layer_0.20",
|
||||
"pla_040024": "elegoo_pla_nozzle_0.40_layer_0.20",
|
||||
"pla_040028": "elegoo_pla_nozzle_0.40_layer_0.30",
|
||||
|
||||
"tpu_040012": "elegoo_tpu_nozzle_0.40_layer_0.10",
|
||||
"tpu_040016": "elegoo_tpu_nozzle_0.40_layer_0.15",
|
||||
"tpu_040020": "elegoo_tpu_nozzle_0.40_layer_0.20",
|
||||
"tpu_040024": "elegoo_tpu_nozzle_0.40_layer_0.20",
|
||||
"tpu_040028": "elegoo_tpu_nozzle_0.40_layer_0.30",
|
||||
|
||||
"elegoo_global_012_high": "elegoo_layer_0.10",
|
||||
"elegoo_global_016_normal": "elegoo_layer_0.15",
|
||||
"elegoo_global_020_fine": "elegoo_layer_0.20",
|
||||
"elegoo_global_024_medium": "elegoo_layer_0.20",
|
||||
"elegoo_global_028_draft": "elegoo_layer_0.30",
|
||||
}
|
||||
|
||||
|
||||
class VersionUpgrade53to54(VersionUpgrade):
|
||||
def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Upgrades preferences to remove from the visibility list the settings that were removed in this version.
|
||||
It also changes the preferences to have the new version number.
|
||||
|
||||
This removes any settings that were removed in the new Cura version.
|
||||
:param serialized: The original contents of the preferences file.
|
||||
:param filename: The file name of the preferences file.
|
||||
:return: A list of new file names, and a list of the new contents for
|
||||
those files.
|
||||
"""
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Update version number.
|
||||
parser["metadata"]["setting_version"] = "22"
|
||||
|
||||
# Remove deleted settings from the visible settings list.
|
||||
if "general" in parser and "visible_settings" in parser["general"]:
|
||||
visible_settings = set(parser["general"]["visible_settings"].split(";"))
|
||||
for removed in _REMOVED_SETTINGS:
|
||||
if removed in visible_settings:
|
||||
visible_settings.remove(removed)
|
||||
|
||||
parser["general"]["visible_settings"] = ";".join(visible_settings)
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
||||
|
||||
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Upgrades instance containers to remove the settings that were removed in this version.
|
||||
It also changes the instance containers to have the new version number.
|
||||
|
||||
This removes any settings that were removed in the new Cura version and updates settings that need to be updated
|
||||
with a new value.
|
||||
|
||||
:param serialized: The original contents of the instance container.
|
||||
:param filename: The original file name of the instance container.
|
||||
:return: A list of new file names, and a list of the new contents for
|
||||
those files.
|
||||
"""
|
||||
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Update version number.
|
||||
parser["metadata"]["setting_version"] = "22"
|
||||
|
||||
if "values" in parser:
|
||||
# Remove deleted settings from the instance containers.
|
||||
for removed in _REMOVED_SETTINGS:
|
||||
if removed in parser["values"]:
|
||||
del parser["values"][removed]
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
||||
|
||||
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Upgrades stacks to have the new version number.
|
||||
|
||||
:param serialized: The original contents of the stack.
|
||||
:param filename: The original file name of the stack.
|
||||
:return: A list of new file names, and a list of the new contents for
|
||||
those files.
|
||||
"""
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
# Update version number.
|
||||
if "metadata" not in parser:
|
||||
parser["metadata"] = {}
|
||||
|
||||
parser["metadata"]["setting_version"] = "22"
|
||||
|
||||
for container in parser['containers']:
|
||||
parser['containers'][container] = _RENAMED_PROFILES.get(parser['containers'][container], parser['containers'][container])
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
61
plugins/VersionUpgrade/VersionUpgrade53to54/__init__.py
Normal file
61
plugins/VersionUpgrade/VersionUpgrade53to54/__init__.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
# Copyright (c) 2023 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, Dict, TYPE_CHECKING
|
||||
|
||||
from . import VersionUpgrade53to54
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Application import Application
|
||||
|
||||
upgrade = VersionUpgrade53to54.VersionUpgrade53to54()
|
||||
|
||||
|
||||
def getMetaData() -> Dict[str, Any]:
|
||||
return {
|
||||
"version_upgrade": {
|
||||
# From To Upgrade function
|
||||
("preferences", 7000021): ("preferences", 7000022, upgrade.upgradePreferences),
|
||||
("machine_stack", 5000021): ("machine_stack", 5000022, upgrade.upgradeStack),
|
||||
("extruder_train", 5000021): ("extruder_train", 5000022, upgrade.upgradeStack),
|
||||
("definition_changes", 4000021): ("definition_changes", 4000022, upgrade.upgradeInstanceContainer),
|
||||
("quality_changes", 4000021): ("quality_changes", 4000022, upgrade.upgradeInstanceContainer),
|
||||
("quality", 4000021): ("quality", 4000022, upgrade.upgradeInstanceContainer),
|
||||
("user", 4000021): ("user", 4000022, upgrade.upgradeInstanceContainer),
|
||||
("intent", 4000021): ("intent", 4000022, upgrade.upgradeInstanceContainer),
|
||||
},
|
||||
"sources": {
|
||||
"preferences": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"."}
|
||||
},
|
||||
"machine_stack": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./machine_instances"}
|
||||
},
|
||||
"extruder_train": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./extruders"}
|
||||
},
|
||||
"definition_changes": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./definition_changes"}
|
||||
},
|
||||
"quality_changes": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./quality_changes"}
|
||||
},
|
||||
"quality": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./quality"}
|
||||
},
|
||||
"user": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./user"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def register(app: "Application") -> Dict[str, Any]:
|
||||
return {"version_upgrade": upgrade}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue