Convert remaining doxygen to rst

This commit is contained in:
Nino van Hooff 2020-05-28 17:13:44 +02:00
parent fe779d9501
commit c2c96faf5f
49 changed files with 2163 additions and 1657 deletions

View File

@ -11,11 +11,13 @@ import os
import sys import sys
## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
#
# \param work_dir The directory to look for JSON files recursively.
# \return A list of JSON files in absolute paths that are found in the given directory.
def find_json_files(work_dir: str) -> list: def find_json_files(work_dir: str) -> list:
"""Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
:param work_dir: The directory to look for JSON files recursively.
:return: A list of JSON files in absolute paths that are found in the given directory.
"""
json_file_list = [] json_file_list = []
for root, dir_names, file_names in os.walk(work_dir): for root, dir_names, file_names in os.walk(work_dir):
for file_name in file_names: for file_name in file_names:
@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list:
return json_file_list return json_file_list
## Removes the given entries from the given JSON file. The file will modified in-place.
#
# \param file_path The JSON file to modify.
# \param entries A list of strings as entries to remove.
# \return None
def remove_entries_from_json_file(file_path: str, entries: list) -> None: def remove_entries_from_json_file(file_path: str, entries: list) -> None:
"""Removes the given entries from the given JSON file. The file will modified in-place.
:param file_path: The JSON file to modify.
:param entries: A list of strings as entries to remove.
:return: None
"""
try: try:
with open(file_path, "r", encoding = "utf-8") as f: with open(file_path, "r", encoding = "utf-8") as f:
package_dict = json.load(f, object_hook = collections.OrderedDict) package_dict = json.load(f, object_hook = collections.OrderedDict)

View File

@ -6,8 +6,9 @@ from UM.Operations.GroupedOperation import GroupedOperation
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
## A specialised operation designed specifically to modify the previous operation.
class PlatformPhysicsOperation(Operation): class PlatformPhysicsOperation(Operation):
"""A specialised operation designed specifically to modify the previous operation."""
def __init__(self, node: SceneNode, translation: Vector) -> None: def __init__(self, node: SceneNode, translation: Vector) -> None:
super().__init__() super().__init__()
self._node = node self._node = node

View File

@ -7,8 +7,9 @@ from UM.Operations.Operation import Operation
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the buildplate number of a scenenode.
class SetBuildPlateNumberOperation(Operation): class SetBuildPlateNumberOperation(Operation):
"""Simple operation to set the buildplate number of a scenenode."""
def __init__(self, node: SceneNode, build_plate_nr: int) -> None: def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
super().__init__() super().__init__()
self._node = node self._node = node

View File

@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode
from UM.Operations import Operation from UM.Operations import Operation
## An operation that parents a scene node to another scene node.
class SetParentOperation(Operation.Operation): class SetParentOperation(Operation.Operation):
## Initialises this SetParentOperation. """An operation that parents a scene node to another scene node."""
#
# \param node The node which will be reparented.
# \param parent_node The node which will be the parent.
def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None: def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None:
"""Initialises this SetParentOperation.
:param node: The node which will be reparented.
:param parent_node: The node which will be the parent.
"""
super().__init__() super().__init__()
self._node = node self._node = node
self._parent = parent_node self._parent = parent_node
self._old_parent = node.getParent() # To restore the previous parent in case of an undo. self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
## Undoes the set-parent operation, restoring the old parent.
def undo(self) -> None: def undo(self) -> None:
"""Undoes the set-parent operation, restoring the old parent."""
self._set_parent(self._old_parent) self._set_parent(self._old_parent)
## Re-applies the set-parent operation.
def redo(self) -> None: def redo(self) -> None:
"""Re-applies the set-parent operation."""
self._set_parent(self._parent) self._set_parent(self._parent)
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
#
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
def _set_parent(self, new_parent: Optional[SceneNode]) -> None: def _set_parent(self, new_parent: Optional[SceneNode]) -> None:
"""Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
:param new_parent: The new parent. Note: this argument can be None, which would hide the node from the scene.
"""
if new_parent: if new_parent:
current_parent = self._node.getParent() current_parent = self._node.getParent()
if current_parent: if current_parent:
@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation):
self._node.setParent(new_parent) self._node.setParent(new_parent)
## Returns a programmer-readable representation of this operation.
#
# \return A programmer-readable representation of this operation.
def __repr__(self) -> str: def __repr__(self) -> str:
"""Returns a programmer-readable representation of this operation.
:return: A programmer-readable representation of this operation.
"""
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent) return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)

View File

@ -44,8 +44,9 @@ class FirmwareUpdater(QObject):
def _updateFirmware(self) -> None: def _updateFirmware(self) -> None:
raise NotImplementedError("_updateFirmware needs to be implemented") raise NotImplementedError("_updateFirmware needs to be implemented")
## Cleanup after a succesful update
def _cleanupAfterUpdate(self) -> None: def _cleanupAfterUpdate(self) -> None:
"""Cleanup after a succesful update"""
# Clean up for next attempt. # Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread") self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
self._firmware_file = "" self._firmware_file = ""

View File

@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject):
def hotendID(self) -> Optional[str]: def hotendID(self) -> Optional[str]:
return self._hotend_id return self._hotend_id
## This method is intended to indicate whether the configuration is valid or not.
# The method checks if the mandatory fields are or not set
# At this moment is always valid since we allow to have empty material and variants.
def isValid(self) -> bool: def isValid(self) -> bool:
"""This method is intended to indicate whether the configuration is valid or not.
The method checks if the mandatory fields are or not set
At this moment is always valid since we allow to have empty material and variants.
"""
return True return True
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject):
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None: def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
self._extruder_configuration.setMaterial(material) self._extruder_configuration.setMaterial(material)
## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float) -> None: def updateHotendTemperature(self, temperature: float) -> None:
"""Update the hotend temperature. This only changes it locally."""
if self._hotend_temperature != temperature: if self._hotend_temperature != temperature:
self._hotend_temperature = temperature self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit() self.hotendTemperatureChanged.emit()
@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject):
self._target_hotend_temperature = temperature self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit() self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float) @pyqtSlot(float)
def setTargetHotendTemperature(self, temperature: float) -> None: def setTargetHotendTemperature(self, temperature: float) -> None:
"""Set the target hotend temperature. This ensures that it's actually sent to the remote."""
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
self.updateTargetHotendTemperature(temperature) self.updateTargetHotendTemperature(temperature)
@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject):
def isPreheating(self) -> bool: def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
## Pre-heats the extruder before printer.
#
# \param temperature The temperature to heat the extruder to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatHotend(self, temperature: float, duration: float) -> None: def preheatHotend(self, temperature: float, duration: float) -> None:
"""Pre-heats the extruder before printer.
:param temperature: The temperature to heat the extruder to, in degrees
Celsius.
:param duration: How long the bed should stay warm, in seconds.
"""
self._printer._controller.preheatHotend(self, temperature, duration) self._printer._controller.preheatHotend(self, temperature, duration)
@pyqtSlot() @pyqtSlot()

View File

@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject):
def buildplateConfiguration(self) -> str: def buildplateConfiguration(self) -> str:
return self._buildplate_configuration return self._buildplate_configuration
## This method is intended to indicate whether the configuration is valid or not.
# The method checks if the mandatory fields are or not set
def isValid(self) -> bool: def isValid(self) -> bool:
"""This method is intended to indicate whether the configuration is valid or not.
The method checks if the mandatory fields are or not set
"""
if not self._extruder_configurations: if not self._extruder_configurations:
return False return False
for configuration in self._extruder_configurations: for configuration in self._extruder_configurations:
@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject):
return True return True
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
def __hash__(self): def __hash__(self):
"""The hash function is used to compare and create unique sets. The configuration is unique if the configuration
of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
"""
extruder_hash = hash(0) extruder_hash = hash(0)
first_extruder = None first_extruder = None
for configuration in self._extruder_configurations: for configuration in self._extruder_configurations:

View File

@ -163,13 +163,15 @@ class PrinterOutputModel(QObject):
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None: def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
self._controller.moveHead(self, x, y, z, speed) self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatBed(self, temperature: float, duration: float) -> None: def preheatBed(self, temperature: float, duration: float) -> None:
"""Pre-heats the heated bed of the printer.
:param temperature: The temperature to heat the bed to, in degrees
Celsius.
:param duration: How long the bed should stay warm, in seconds.
"""
self._controller.preheatBed(self, temperature, duration) self._controller.preheatBed(self, temperature, duration)
@pyqtSlot() @pyqtSlot()
@ -200,8 +202,9 @@ class PrinterOutputModel(QObject):
self._unique_name = unique_name self._unique_name = unique_name
self.nameChanged.emit() self.nameChanged.emit()
## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature: float) -> None: def updateBedTemperature(self, temperature: float) -> None:
"""Update the bed temperature. This only changes it locally."""
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
self._bed_temperature = temperature self._bed_temperature = temperature
self.bedTemperatureChanged.emit() self.bedTemperatureChanged.emit()
@ -211,9 +214,10 @@ class PrinterOutputModel(QObject):
self._target_bed_temperature = temperature self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit() self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float) @pyqtSlot(float)
def setTargetBedTemperature(self, temperature: float) -> None: def setTargetBedTemperature(self, temperature: float) -> None:
"""Set the target bed temperature. This ensures that it's actually sent to the remote."""
self._controller.setTargetBedTemperature(self, temperature) self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature) self.updateTargetBedTemperature(temperature)

View File

@ -32,8 +32,9 @@ class NetworkMJPGImage(QQuickPaintedItem):
self.setAntialiasing(True) self.setAntialiasing(True)
## Ensure that close gets called when object is destroyed
def __del__(self) -> None: def __del__(self) -> None:
"""Ensure that close gets called when object is destroyed"""
self.stop() self.stop()

View File

@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def _compressGCode(self) -> Optional[bytes]: def _compressGCode(self) -> Optional[bytes]:
self._compressing_gcode = True self._compressing_gcode = True
## Mash the data into single string
max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
"""Mash the data into single string"""
file_data_bytes_list = [] file_data_bytes_list = []
batched_lines = [] batched_lines = []
batched_lines_count = 0 batched_lines_count = 0
@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
return request return request
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
"""This method was only available privately before, but it was actually called from SendMaterialJob.py.
We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
"""
return self._createFormPart(content_header, data, content_type) return self._createFormPart(content_header, data, content_type)
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
part.setBody(data) part.setBody(data)
return part return part
## Convenience function to get the username, either from the cloud or from the OS.
def _getUserName(self) -> str: def _getUserName(self) -> str:
"""Convenience function to get the username, either from the cloud or from the OS."""
# check first if we are logged in with the Ultimaker Account # check first if we are logged in with the Ultimaker Account
account = CuraApplication.getInstance().getCuraAPI().account # type: Account account = CuraApplication.getInstance().getCuraAPI().account # type: Account
if account and account.isLoggedIn: if account and account.isLoggedIn:
@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._createNetworkManager() self._createNetworkManager()
assert (self._manager is not None) assert (self._manager is not None)
## Sends a put request to the given path.
# \param url: The path after the API prefix.
# \param data: The data to be sent in the body
# \param content_type: The content type of the body data.
# \param on_finished: The function to call when the response is received.
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json", def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json",
on_finished: Optional[Callable[[QNetworkReply], None]] = None, on_finished: Optional[Callable[[QNetworkReply], None]] = None,
on_progress: Optional[Callable[[int, int], None]] = None) -> None: on_progress: Optional[Callable[[int, int], None]] = None) -> None:
"""Sends a put request to the given path.
:param url: The path after the API prefix.
:param data: The data to be sent in the body
:param content_type: The content type of the body data.
:param on_finished: The function to call when the response is received.
:param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
"""
self._validateManager() self._validateManager()
request = self._createEmptyRequest(url, content_type = content_type) request = self._createEmptyRequest(url, content_type = content_type)
@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if on_progress is not None: if on_progress is not None:
reply.uploadProgress.connect(on_progress) reply.uploadProgress.connect(on_progress)
## Sends a delete request to the given path.
# \param url: The path after the API prefix.
# \param on_finished: The function to be call when the response is received.
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
"""Sends a delete request to the given path.
:param url: The path after the API prefix.
:param on_finished: The function to be call when the response is received.
"""
self._validateManager() self._validateManager()
request = self._createEmptyRequest(url) request = self._createEmptyRequest(url)
@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
reply = self._manager.deleteResource(request) reply = self._manager.deleteResource(request)
self._registerOnFinishedCallback(reply, on_finished) self._registerOnFinishedCallback(reply, on_finished)
## Sends a get request to the given path.
# \param url: The path after the API prefix.
# \param on_finished: The function to be call when the response is received.
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
"""Sends a get request to the given path.
:param url: The path after the API prefix.
:param on_finished: The function to be call when the response is received.
"""
self._validateManager() self._validateManager()
request = self._createEmptyRequest(url) request = self._createEmptyRequest(url)
@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
reply = self._manager.get(request) reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished) self._registerOnFinishedCallback(reply, on_finished)
## Sends a post request to the given path.
# \param url: The path after the API prefix.
# \param data: The data to be sent in the body
# \param on_finished: The function to call when the response is received.
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
def post(self, url: str, data: Union[str, bytes], def post(self, url: str, data: Union[str, bytes],
on_finished: Optional[Callable[[QNetworkReply], None]], on_finished: Optional[Callable[[QNetworkReply], None]],
on_progress: Optional[Callable[[int, int], None]] = None) -> None: on_progress: Optional[Callable[[int, int], None]] = None) -> None:
"""Sends a post request to the given path.
:param url: The path after the API prefix.
:param data: The data to be sent in the body
:param on_finished: The function to call when the response is received.
:param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
"""
self._validateManager() self._validateManager()
request = self._createEmptyRequest(url) request = self._createEmptyRequest(url)
@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if on_finished is not None: if on_finished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
## This method checks if the name of the group stored in the definition container is correct.
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
# then all the container stacks are updated, both the current and the hidden ones.
def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None: def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
"""This method checks if the name of the group stored in the definition container is correct.
After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
then all the container stacks are updated, both the current and the hidden ones.
"""
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey() active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
if global_container_stack and device_id == active_machine_network_name: if global_container_stack and device_id == active_machine_network_name:
@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def getProperties(self): def getProperties(self):
return self._properties return self._properties
## Get the unique key of this machine
# \return key String containing the key of the machine.
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def key(self) -> str: def key(self) -> str:
"""Get the unique key of this machine
:return: key String containing the key of the machine.
"""
return self._id return self._id
## The IP address of the printer.
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def address(self) -> str: def address(self) -> str:
"""The IP address of the printer."""
return self._properties.get(b"address", b"").decode("utf-8") return self._properties.get(b"address", b"").decode("utf-8")
## Name of the printer (as returned from the ZeroConf properties)
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def name(self) -> str: def name(self) -> str:
"""Name of the printer (as returned from the ZeroConf properties)"""
return self._properties.get(b"name", b"").decode("utf-8") return self._properties.get(b"name", b"").decode("utf-8")
## Firmware version (as returned from the ZeroConf properties)
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def firmwareVersion(self) -> str: def firmwareVersion(self) -> str:
"""Firmware version (as returned from the ZeroConf properties)"""
return self._properties.get(b"firmware_version", b"").decode("utf-8") return self._properties.get(b"firmware_version", b"").decode("utf-8")
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def printerType(self) -> str: def printerType(self) -> str:
return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
## IP adress of this printer
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def ipAddress(self) -> str: def ipAddress(self) -> str:
"""IP adress of this printer"""
return self._address return self._address

View File

@ -2,15 +2,19 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
## Data class that represents a peripheral for a printer.
#
# Output device plug-ins may specify that the printer has a certain set of
# peripherals. This set is then possibly shown in the interface of the monitor
# stage.
class Peripheral: class Peripheral:
## Constructs the peripheral. """Data class that represents a peripheral for a printer.
# \param type A unique ID for the type of peripheral.
# \param name A human-readable name for the peripheral. Output device plug-ins may specify that the printer has a certain set of
peripherals. This set is then possibly shown in the interface of the monitor
stage.
"""
def __init__(self, peripheral_type: str, name: str) -> None: def __init__(self, peripheral_type: str, name: str) -> None:
"""Constructs the peripheral.
:param peripheral_type: A unique ID for the type of peripheral.
:param name: A human-readable name for the peripheral.
"""
self.type = peripheral_type self.type = peripheral_type
self.name = name self.name = name

View File

@ -24,8 +24,9 @@ if MYPY:
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## The current processing state of the backend.
class ConnectionState(IntEnum): class ConnectionState(IntEnum):
"""The current processing state of the backend."""
Closed = 0 Closed = 0
Connecting = 1 Connecting = 1
Connected = 2 Connected = 2
@ -40,17 +41,19 @@ class ConnectionType(IntEnum):
CloudConnection = 3 CloudConnection = 3
## Printer output device adds extra interface options on top of output device.
#
# The assumption is made the printer is a FDM printer.
#
# Note that a number of settings are marked as "final". This is because decorators
# are not inherited by children. To fix this we use the private counter part of those
# functions to actually have the implementation.
#
# For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter @signalemitter
class PrinterOutputDevice(QObject, OutputDevice): class PrinterOutputDevice(QObject, OutputDevice):
"""Printer output device adds extra interface options on top of output device.
The assumption is made the printer is a FDM printer.
Note that a number of settings are marked as "final". This is because decorators
are not inherited by children. To fix this we use the private counter part of those
functions to actually have the implementation.
For all other uses it should be used in the same way as a "regular" OutputDevice.
"""
printersChanged = pyqtSignal() printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str) connectionStateChanged = pyqtSignal(str)
@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice):
if self._monitor_item is None: if self._monitor_item is None:
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
## Attempt to establish connection
def connect(self) -> None: def connect(self) -> None:
"""Attempt to establish connection"""
self.setConnectionState(ConnectionState.Connecting) self.setConnectionState(ConnectionState.Connecting)
self._update_timer.start() self._update_timer.start()
## Attempt to close the connection
def close(self) -> None: def close(self) -> None:
"""Attempt to close the connection"""
self._update_timer.stop() self._update_timer.stop()
self.setConnectionState(ConnectionState.Closed) self.setConnectionState(ConnectionState.Closed)
## Ensure that close gets called when object is destroyed
def __del__(self) -> None: def __del__(self) -> None:
"""Ensure that close gets called when object is destroyed"""
self.close() self.close()
@pyqtProperty(bool, notify = acceptsCommandsChanged) @pyqtProperty(bool, notify = acceptsCommandsChanged)
def acceptsCommands(self) -> bool: def acceptsCommands(self) -> bool:
return self._accepts_commands return self._accepts_commands
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def _setAcceptsCommands(self, accepts_commands: bool) -> None: def _setAcceptsCommands(self, accepts_commands: bool) -> None:
"""Set a flag to signal the UI that the printer is not (yet) ready to receive commands"""
if self._accepts_commands != accepts_commands: if self._accepts_commands != accepts_commands:
self._accepts_commands = accepts_commands self._accepts_commands = accepts_commands
@ -241,16 +248,20 @@ class PrinterOutputDevice(QObject, OutputDevice):
# At this point there may be non-updated configurations # At this point there may be non-updated configurations
self._updateUniqueConfigurations() self._updateUniqueConfigurations()
## Set the device firmware name
#
# \param name The name of the firmware.
def _setFirmwareName(self, name: str) -> None: def _setFirmwareName(self, name: str) -> None:
"""Set the device firmware name
:param name: The name of the firmware.
"""
self._firmware_name = name self._firmware_name = name
## Get the name of device firmware
#
# This name can be used to define device type
def getFirmwareName(self) -> Optional[str]: def getFirmwareName(self) -> Optional[str]:
"""Get the name of device firmware
This name can be used to define device type
"""
return self._firmware_name return self._firmware_name
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]: def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:

View File

@ -10,15 +10,19 @@ class NoProfileException(Exception):
pass pass
## A type of plug-ins that reads profiles from a file.
#
# The profile is then stored as instance container of the type user profile.
class ProfileReader(PluginObject): class ProfileReader(PluginObject):
"""A type of plug-ins that reads profiles from a file.
The profile is then stored as instance container of the type user profile.
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
## Read profile data from a file and return a filled profile.
#
# \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles.
def read(self, file_name): def read(self, file_name):
"""Read profile data from a file and return a filled profile.
:return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles.
"""
raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.") raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.")

View File

@ -3,23 +3,29 @@
from UM.PluginObject import PluginObject from UM.PluginObject import PluginObject
## Base class for profile writer plugins.
#
# This class defines a write() function to write profiles to files with.
class ProfileWriter(PluginObject): class ProfileWriter(PluginObject):
## Initialises the profile writer. """Base class for profile writer plugins.
#
# This currently doesn't do anything since the writer is basically static. This class defines a write() function to write profiles to files with.
"""
def __init__(self): def __init__(self):
"""Initialises the profile writer.
This currently doesn't do anything since the writer is basically static.
"""
super().__init__() super().__init__()
## Writes a profile to the specified file path.
#
# The profile writer may write its own file format to the specified file.
#
# \param path \type{string} The file to output to.
# \param profiles \type{Profile} or \type{List} The profile(s) to write to the file.
# \return \code True \endcode if the writing was successful, or \code
# False \endcode if it wasn't.
def write(self, path, profiles): def write(self, path, profiles):
"""Writes a profile to the specified file path.
The profile writer may write its own file format to the specified file.
:param path: :type{string} The file to output to.
:param profiles: :type{Profile} or :type{List} The profile(s) to write to the file.
:return: True if the writing was successful, or False if it wasn't.
"""
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.") raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")

View File

@ -2,8 +2,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
class BuildPlateDecorator(SceneNodeDecorator): class BuildPlateDecorator(SceneNodeDecorator):
"""Make a SceneNode build plate aware CuraSceneNode objects all have this decorator."""
def __init__(self, build_plate_number: int = -1) -> None: def __init__(self, build_plate_number: int = -1) -> None:
super().__init__() super().__init__()
self._build_plate_number = build_plate_number self._build_plate_number = build_plate_number

View File

@ -23,9 +23,12 @@ if TYPE_CHECKING:
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
class ConvexHullDecorator(SceneNodeDecorator): class ConvexHullDecorator(SceneNodeDecorator):
"""The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._onChanged() self._onChanged()
## Force that a new (empty) object is created upon copy.
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
"""Force that a new (empty) object is created upon copy."""
return ConvexHullDecorator() return ConvexHullDecorator()
## The polygon representing the 2D adhesion area.
# If no adhesion is used, the regular convex hull is returned
def getAdhesionArea(self) -> Optional[Polygon]: def getAdhesionArea(self) -> Optional[Polygon]:
"""The polygon representing the 2D adhesion area.
If no adhesion is used, the regular convex hull is returned
"""
if self._node is None: if self._node is None:
return None return None
@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._add2DAdhesionMargin(hull) return self._add2DAdhesionMargin(hull)
## Get the unmodified 2D projected convex hull of the node (if any)
# In case of one-at-a-time, this includes adhesion and head+fans clearance
def getConvexHull(self) -> Optional[Polygon]: def getConvexHull(self) -> Optional[Polygon]:
"""Get the unmodified 2D projected convex hull of the node (if any)
In case of one-at-a-time, this includes adhesion and head+fans clearance
"""
if self._node is None: if self._node is None:
return None return None
if self._node.callDecoration("isNonPrintingMesh"): if self._node.callDecoration("isNonPrintingMesh"):
@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._compute2DConvexHull() return self._compute2DConvexHull()
## For one at the time this is the convex hull of the node with the full head size
# In case of printing all at once this is None.
def getConvexHullHeadFull(self) -> Optional[Polygon]: def getConvexHullHeadFull(self) -> Optional[Polygon]:
"""For one at the time this is the convex hull of the node with the full head size
In case of printing all at once this is None.
"""
if self._node is None: if self._node is None:
return None return None
@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return False return False
return bool(parent.callDecoration("isGroup")) return bool(parent.callDecoration("isGroup"))
## Get convex hull of the object + head size
# In case of printing all at once this is None.
# For one at the time this is area with intersection of mirrored head
def getConvexHullHead(self) -> Optional[Polygon]: def getConvexHullHead(self) -> Optional[Polygon]:
"""Get convex hull of the object + head size
In case of printing all at once this is None.
For one at the time this is area with intersection of mirrored head
"""
if self._node is None: if self._node is None:
return None return None
if self._node.callDecoration("isNonPrintingMesh"): if self._node.callDecoration("isNonPrintingMesh"):
@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return head_with_fans_with_adhesion_margin return head_with_fans_with_adhesion_margin
return None return None
## Get convex hull of the node
# In case of printing all at once this None??
# For one at the time this is the area without the head.
def getConvexHullBoundary(self) -> Optional[Polygon]: def getConvexHullBoundary(self) -> Optional[Polygon]:
"""Get convex hull of the node
In case of printing all at once this None??
For one at the time this is the area without the head.
"""
if self._node is None: if self._node is None:
return None return None
@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._compute2DConvexHull() return self._compute2DConvexHull()
return None return None
## Get the buildplate polygon where will be printed
# In case of printing all at once this is the same as convex hull (no individual adhesion)
# For one at the time this includes the adhesion area
def getPrintingArea(self) -> Optional[Polygon]: def getPrintingArea(self) -> Optional[Polygon]:
"""Get the buildplate polygon where will be printed
In case of printing all at once this is the same as convex hull (no individual adhesion)
For one at the time this includes the adhesion area
"""
if self._isSingularOneAtATimeNode(): if self._isSingularOneAtATimeNode():
# In one-at-a-time mode, every printed object gets it's own adhesion # In one-at-a-time mode, every printed object gets it's own adhesion
printing_area = self.getAdhesionArea() printing_area = self.getAdhesionArea()
@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
printing_area = self.getConvexHull() printing_area = self.getConvexHull()
return printing_area return printing_area
## The same as recomputeConvexHull, but using a timer if it was set.
def recomputeConvexHullDelayed(self) -> None: def recomputeConvexHullDelayed(self) -> None:
"""The same as recomputeConvexHull, but using a timer if it was set."""
if self._recompute_convex_hull_timer is not None: if self._recompute_convex_hull_timer is not None:
self._recompute_convex_hull_timer.start() self._recompute_convex_hull_timer.start()
else: else:
@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
return convex_hull.getMinkowskiHull(head_and_fans) return convex_hull.getMinkowskiHull(head_and_fans)
return None return None
## Compensate given 2D polygon with adhesion margin
# \return 2D polygon with added margin
def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon: def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon:
"""Compensate given 2D polygon with adhesion margin
:return: 2D polygon with added margin
"""
if not self._global_stack: if not self._global_stack:
return Polygon() return Polygon()
# Compensate for raft/skirt/brim # Compensate for raft/skirt/brim
@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
poly = poly.getMinkowskiHull(extra_margin_polygon) poly = poly.getMinkowskiHull(extra_margin_polygon)
return poly return poly
## Offset the convex hull with settings that influence the collision area.
#
# \param convex_hull Polygon of the original convex hull.
# \return New Polygon instance that is offset with everything that
# influences the collision area.
def _offsetHull(self, convex_hull: Polygon) -> Polygon: def _offsetHull(self, convex_hull: Polygon) -> Polygon:
"""Offset the convex hull with settings that influence the collision area.
:param convex_hull: Polygon of the original convex hull.
:return: New Polygon instance that is offset with everything that
influences the collision area.
"""
horizontal_expansion = max( horizontal_expansion = max(
self._getSettingProperty("xy_offset", "value"), self._getSettingProperty("xy_offset", "value"),
self._getSettingProperty("xy_offset_layer_0", "value") self._getSettingProperty("xy_offset_layer_0", "value")
@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._onChanged() self._onChanged()
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any: def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
"""Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property)."""
if self._global_stack is None or self._node is None: if self._global_stack is None or self._node is None:
return None return None
per_mesh_stack = self._node.callDecoration("getStack") per_mesh_stack = self._node.callDecoration("getStack")
@ -430,16 +452,18 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Limit_to_extruder is set. The global stack handles this then # Limit_to_extruder is set. The global stack handles this then
return self._global_stack.getProperty(setting_key, prop) return self._global_stack.getProperty(setting_key, prop)
## Returns True if node is a descendant or the same as the root node.
def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool: def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool:
"""Returns True if node is a descendant or the same as the root node."""
if node is None: if node is None:
return False return False
if root is node: if root is node:
return True return True
return self.__isDescendant(root, node.getParent()) return self.__isDescendant(root, node.getParent())
## True if print_sequence is one_at_a_time and _node is not part of a group
def _isSingularOneAtATimeNode(self) -> bool: def _isSingularOneAtATimeNode(self) -> bool:
"""True if print_sequence is one_at_a_time and _node is not part of a group"""
if self._node is None: if self._node is None:
return False return False
return self._global_stack is not None \ return self._global_stack is not None \
@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
"adhesion_type", "raft_margin", "print_sequence", "adhesion_type", "raft_margin", "print_sequence",
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
## Settings that change the convex hull.
#
# If these settings change, the convex hull should be recalculated.
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"} _influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
"""Settings that change the convex hull.
If these settings change, the convex hull should be recalculated.
"""

View File

@ -18,11 +18,13 @@ if TYPE_CHECKING:
class ConvexHullNode(SceneNode): class ConvexHullNode(SceneNode):
shader = None # To prevent the shader from being re-built over and over again, only load it once. shader = None # To prevent the shader from being re-built over and over again, only load it once.
## Convex hull node is a special type of scene node that is used to display an area, to indicate the
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
# to represent the raft as well.
def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None: def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
"""Convex hull node is a special type of scene node that is used to display an area, to indicate the
location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
to represent the raft as well.
"""
super().__init__(parent) super().__init__(parent)
self.setCalculateBoundingBox(False) self.setCalculateBoundingBox(False)

View File

@ -72,9 +72,10 @@ class CuraSceneController(QObject):
max_build_plate = max(build_plate_number, max_build_plate) max_build_plate = max(build_plate_number, max_build_plate)
return max_build_plate return max_build_plate
## Either select or deselect an item
@pyqtSlot(int) @pyqtSlot(int)
def changeSelection(self, index): def changeSelection(self, index):
"""Either select or deselect an item"""
modifiers = QApplication.keyboardModifiers() modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier ctrl_is_active = modifiers & Qt.ControlModifier
shift_is_active = modifiers & Qt.ShiftModifier shift_is_active = modifiers & Qt.ShiftModifier

View File

@ -15,9 +15,11 @@ from cura.Settings.ExtruderStack import ExtruderStack # For typing.
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
## Scene nodes that are models are only seen when selecting the corresponding build plate
# Note that many other nodes can just be UM SceneNode objects.
class CuraSceneNode(SceneNode): class CuraSceneNode(SceneNode):
"""Scene nodes that are models are only seen when selecting the corresponding build plate
Note that many other nodes can just be UM SceneNode objects.
"""
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
super().__init__(parent = parent, visible = visible, name = name) super().__init__(parent = parent, visible = visible, name = name)
if not no_setting_override: if not no_setting_override:
@ -36,9 +38,11 @@ class CuraSceneNode(SceneNode):
def isSelectable(self) -> bool: def isSelectable(self) -> bool:
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
def getPrintingExtruder(self) -> Optional[ExtruderStack]: def getPrintingExtruder(self) -> Optional[ExtruderStack]:
"""Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
"""
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None: if global_container_stack is None:
return None return None
@ -69,8 +73,9 @@ class CuraSceneNode(SceneNode):
# This point should never be reached # This point should never be reached
return None return None
## Return the color of the material used to print this model
def getDiffuseColor(self) -> List[float]: def getDiffuseColor(self) -> List[float]:
"""Return the color of the material used to print this model"""
printing_extruder = self.getPrintingExtruder() printing_extruder = self.getPrintingExtruder()
material_color = "#808080" # Fallback color material_color = "#808080" # Fallback color
@ -86,8 +91,9 @@ class CuraSceneNode(SceneNode):
1.0 1.0
] ]
## Return if any area collides with the convex hull of this scene node
def collidesWithAreas(self, areas: List[Polygon]) -> bool: def collidesWithAreas(self, areas: List[Polygon]) -> bool:
"""Return if any area collides with the convex hull of this scene node"""
convex_hull = self.callDecoration("getPrintingArea") convex_hull = self.callDecoration("getPrintingArea")
if convex_hull: if convex_hull:
if not convex_hull.isValid(): if not convex_hull.isValid():
@ -101,8 +107,9 @@ class CuraSceneNode(SceneNode):
return True return True
return False return False
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
def _calculateAABB(self) -> None: def _calculateAABB(self) -> None:
"""Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box"""
self._aabb = None self._aabb = None
if self._mesh_data: if self._mesh_data:
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation()) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
@ -122,8 +129,9 @@ class CuraSceneNode(SceneNode):
else: else:
self._aabb = self._aabb + child.getBoundingBox() self._aabb = self._aabb + child.getBoundingBox()
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
"""Taken from SceneNode, but replaced SceneNode with CuraSceneNode"""
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
copy.setTransformation(self.getLocalTransformation()) copy.setTransformation(self.getLocalTransformation())
copy.setMeshData(self._mesh_data) copy.setMeshData(self._mesh_data)

View File

@ -1,8 +1,9 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
## A decorator that stores the amount an object has been moved below the platform.
class ZOffsetDecorator(SceneNodeDecorator): class ZOffsetDecorator(SceneNodeDecorator):
"""A decorator that stores the amount an object has been moved below the platform."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._z_offset = 0. self._z_offset = 0.

View File

@ -33,12 +33,14 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Manager class that contains common actions to deal with containers in Cura.
#
# This is primarily intended as a class to be able to perform certain actions
# from within QML. We want to be able to trigger things like removing a container
# when a certain action happens. This can be done through this class.
class ContainerManager(QObject): class ContainerManager(QObject):
"""Manager class that contains common actions to deal with containers in Cura.
This is primarily intended as a class to be able to perform certain actions
from within QML. We want to be able to trigger things like removing a container
when a certain action happens. This can be done through this class.
"""
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
@ -67,21 +69,23 @@ class ContainerManager(QObject):
return "" return ""
return str(result) return str(result)
## Set a metadata entry of the specified container.
#
# This will set the specified entry of the container's metadata to the specified
# value. Note that entries containing dictionaries can have their entries changed
# by using "/" as a separator. For example, to change an entry "foo" in a
# dictionary entry "bar", you can specify "bar/foo" as entry name.
#
# \param container_node \type{ContainerNode}
# \param entry_name \type{str} The name of the metadata entry to change.
# \param entry_value The new value of the entry.
#
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
@pyqtSlot("QVariant", str, str) @pyqtSlot("QVariant", str, str)
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
"""Set a metadata entry of the specified container.
This will set the specified entry of the container's metadata to the specified
value. Note that entries containing dictionaries can have their entries changed
by using "/" as a separator. For example, to change an entry "foo" in a
dictionary entry "bar", you can specify "bar/foo" as entry name.
:param container_node: :type{ContainerNode}
:param entry_name: :type{str} The name of the metadata entry to change.
:param entry_value: The new value of the entry.
TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
"""
if container_node.container is None: if container_node.container is None:
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id)) Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
return False return False
@ -124,18 +128,20 @@ class ContainerManager(QObject):
def makeUniqueName(self, original_name: str) -> str: def makeUniqueName(self, original_name: str) -> str:
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name) return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog
#
# This will go through the list of available container types and generate a list of strings
# out of that. The strings are formatted as "description (*.extension)" and can be directly
# passed to a nameFilters property of a Qt File Dialog.
#
# \param type_name Which types of containers to list. These types correspond to the "type"
# key of the plugin metadata.
#
# \return A string list with name filters.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getContainerNameFilters(self, type_name: str) -> List[str]: def getContainerNameFilters(self, type_name: str) -> List[str]:
"""Get a list of string that can be used as name filters for a Qt File Dialog
This will go through the list of available container types and generate a list of strings
out of that. The strings are formatted as "description (*.extension)" and can be directly
passed to a nameFilters property of a Qt File Dialog.
:param type_name: Which types of containers to list. These types correspond to the "type"
key of the plugin metadata.
:return: A string list with name filters.
"""
if not self._container_name_filters: if not self._container_name_filters:
self._updateContainerNameFilters() self._updateContainerNameFilters()
@ -147,17 +153,18 @@ class ContainerManager(QObject):
filters.append("All Files (*)") filters.append("All Files (*)")
return filters return filters
## Export a container to a file
#
# \param container_id The ID of the container to export
# \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
# \param file_url_or_string The URL where to save the file.
#
# \return A dictionary containing a key "status" with a status code and a key "message" with a message
# explaining the status.
# The status code can be one of "error", "cancelled", "success"
@pyqtSlot(str, str, QUrl, result = "QVariantMap") @pyqtSlot(str, str, QUrl, result = "QVariantMap")
def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
"""Export a container to a file
:param container_id: The ID of the container to export
:param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
:param file_url_or_string: The URL where to save the file.
:return: A dictionary containing a key "status" with a status code and a key "message" with a message
explaining the status. The status code can be one of "error", "cancelled", "success"
"""
if not container_id or not file_type or not file_url_or_string: if not container_id or not file_type or not file_url_or_string:
return {"status": "error", "message": "Invalid arguments"} return {"status": "error", "message": "Invalid arguments"}
@ -214,14 +221,16 @@ class ContainerManager(QObject):
return {"status": "success", "message": "Successfully exported container", "path": file_url} return {"status": "success", "message": "Successfully exported container", "path": file_url}
## Imports a profile from a file
#
# \param file_url A URL that points to the file to import.
#
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
# containing a message for the user
@pyqtSlot(QUrl, result = "QVariantMap") @pyqtSlot(QUrl, result = "QVariantMap")
def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
"""Imports a profile from a file
:param file_url: A URL that points to the file to import.
:return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
containing a message for the user
"""
if not file_url_or_string: if not file_url_or_string:
return {"status": "error", "message": "Invalid path"} return {"status": "error", "message": "Invalid path"}
@ -266,14 +275,16 @@ class ContainerManager(QObject):
return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())} return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
## Update the current active quality changes container with the settings from the user container.
#
# This will go through the active global stack and all active extruder stacks and merge the changes from the user
# container into the quality_changes container. After that, the user container is cleared.
#
# \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool) @pyqtSlot(result = bool)
def updateQualityChanges(self) -> bool: def updateQualityChanges(self) -> bool:
"""Update the current active quality changes container with the settings from the user container.
This will go through the active global stack and all active extruder stacks and merge the changes from the user
container into the quality_changes container. After that, the user container is cleared.
:return: :type{bool} True if successful, False if not.
"""
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getMachineManager().activeMachine global_stack = application.getMachineManager().activeMachine
if not global_stack: if not global_stack:
@ -313,9 +324,10 @@ class ContainerManager(QObject):
return True return True
## Clear the top-most (user) containers of the active stacks.
@pyqtSlot() @pyqtSlot()
def clearUserContainers(self) -> None: def clearUserContainers(self) -> None:
"""Clear the top-most (user) containers of the active stacks."""
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.blurSettings.emit() machine_manager.blurSettings.emit()
@ -335,25 +347,28 @@ class ContainerManager(QObject):
for container in send_emits_containers: for container in send_emits_containers:
container.sendPostponedEmits() container.sendPostponedEmits()
## Get a list of materials that have the same GUID as the reference material
#
# \param material_node The node representing the material for which to get
# the same GUID.
# \param exclude_self Whether to include the name of the material you
# provided.
# \return A list of names of materials with the same GUID.
@pyqtSlot("QVariant", bool, result = "QStringList") @pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]: def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
"""Get a list of materials that have the same GUID as the reference material
:param material_node: The node representing the material for which to get
the same GUID.
:param exclude_self: Whether to include the name of the material you provided.
:return: A list of names of materials with the same GUID.
"""
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid) same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid)
if exclude_self: if exclude_self:
return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file}) return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
else: else:
return list({meta["name"] for meta in same_guid}) return list({meta["name"] for meta in same_guid})
## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def unlinkMaterial(self, material_node: "MaterialNode") -> None: def unlinkMaterial(self, material_node: "MaterialNode") -> None:
"""Unlink a material from all other materials by creating a new GUID
:param material_id: :type{str} the id of the material to create a new GUID for.
"""
# Get the material group # Get the material group
if material_node.container is None: # Failed to lazy-load this container. if material_node.container is None: # Failed to lazy-load this container.
return return
@ -428,9 +443,10 @@ class ContainerManager(QObject):
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list) name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
self._container_name_filters[name_filter] = entry self._container_name_filters[name_filter] = entry
## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result = "QVariantMap") @pyqtSlot(QUrl, result = "QVariantMap")
def importProfile(self, file_url: QUrl) -> Dict[str, str]: def importProfile(self, file_url: QUrl) -> Dict[str, str]:
"""Import single profile, file_url does not have to end with curaprofile"""
if not file_url.isValid(): if not file_url.isValid():
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
path = file_url.toLocalFile() path = file_url.toLocalFile()

View File

@ -44,14 +44,16 @@ class CuraContainerRegistry(ContainerRegistry):
# is added, we check to see if an extruder stack needs to be added. # is added, we check to see if an extruder stack needs to be added.
self.containerAdded.connect(self._onContainerAdded) self.containerAdded.connect(self._onContainerAdded)
## Overridden from ContainerRegistry
#
# Adds a container to the registry.
#
# This will also try to convert a ContainerStack to either Extruder or
# Global stack based on metadata information.
@override(ContainerRegistry) @override(ContainerRegistry)
def addContainer(self, container: ContainerInterface) -> None: def addContainer(self, container: ContainerInterface) -> None:
"""Overridden from ContainerRegistry
Adds a container to the registry.
This will also try to convert a ContainerStack to either Extruder or
Global stack based on metadata information.
"""
# Note: Intentional check with type() because we want to ignore subclasses # Note: Intentional check with type() because we want to ignore subclasses
if type(container) == ContainerStack: if type(container) == ContainerStack:
container = self._convertContainerStack(cast(ContainerStack, container)) container = self._convertContainerStack(cast(ContainerStack, container))
@ -66,13 +68,15 @@ class CuraContainerRegistry(ContainerRegistry):
super().addContainer(container) super().addContainer(container)
## Create a name that is not empty and unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param current_name \type{} Current name of the container, which may be an acceptable option
# \param new_name \type{string} Base name, which may not be unique
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
# \return \type{string} Name that is unique for the specified type and name/id
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
"""Create a name that is not empty and unique
:param container_type: :type{string} Type of the container (machine, quality, ...)
:param current_name: :type{} Current name of the container, which may be an acceptable option
:param new_name: :type{string} Base name, which may not be unique
:param fallback_name: :type{string} Name to use when (stripped) new_name is empty
:return: :type{string} Name that is unique for the specified type and name/id
"""
new_name = new_name.strip() new_name = new_name.strip()
num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name)
if num_check: if num_check:
@ -89,24 +93,28 @@ class CuraContainerRegistry(ContainerRegistry):
return unique_name return unique_name
## Check if a container with of a certain type and a certain name or id exists
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param container_name \type{string} Name to check
def _containerExists(self, container_type: str, container_name: str): def _containerExists(self, container_type: str, container_name: str):
"""Check if a container with of a certain type and a certain name or id exists
Both the id and the name are checked, because they may not be the same and it is better if they are both unique
:param container_type: :type{string} Type of the container (machine, quality, ...)
:param container_name: :type{string} Name to check
"""
container_class = ContainerStack if container_type == "machine" else InstanceContainer container_class = ContainerStack if container_type == "machine" else InstanceContainer
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
## Exports an profile to a file
#
# \param container_list \type{list} the containers to export. This is not
# necessarily in any order!
# \param file_name \type{str} the full path and filename to export to.
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
# \return True if the export succeeded, false otherwise.
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
"""Exports an profile to a file
:param container_list: :type{list} the containers to export. This is not
necessarily in any order!
:param file_name: :type{str} the full path and filename to export to.
:param file_type: :type{str} the file type with the format "<description> (*.<extension>)"
:return: True if the export succeeded, false otherwise.
"""
# Parse the fileType to deduce what plugin can save the file format. # Parse the fileType to deduce what plugin can save the file format.
# fileType has the format "<description> (*.<extension>)" # fileType has the format "<description> (*.<extension>)"
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
@ -150,11 +158,13 @@ class CuraContainerRegistry(ContainerRegistry):
m.show() m.show()
return True return True
## Gets the plugin object matching the criteria
# \param extension
# \param description
# \return The plugin object matching the given extension and description.
def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
"""Gets the plugin object matching the criteria
:param extension:
:param description:
:return: The plugin object matching the given extension and description.
"""
plugin_registry = PluginRegistry.getInstance() plugin_registry = PluginRegistry.getInstance()
for plugin_id, meta_data in self._getIOPlugins("profile_writer"): for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
@ -165,12 +175,14 @@ class CuraContainerRegistry(ContainerRegistry):
return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id))
return None return None
## Imports a profile from a file
#
# \param file_name The full path and filename of the profile to import.
# \return Dict with a 'status' key containing the string 'ok' or 'error',
# and a 'message' key containing a message for the user.
def importProfile(self, file_name: str) -> Dict[str, str]: def importProfile(self, file_name: str) -> Dict[str, str]:
"""Imports a profile from a file
:param file_name: The full path and filename of the profile to import.
:return: Dict with a 'status' key containing the string 'ok' or 'error',
and a 'message' key containing a message for the user.
"""
Logger.log("d", "Attempting to import profile %s", file_name) Logger.log("d", "Attempting to import profile %s", file_name)
if not file_name: if not file_name:
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
@ -338,12 +350,14 @@ class CuraContainerRegistry(ContainerRegistry):
self._registerSingleExtrusionMachinesExtruderStacks() self._registerSingleExtrusionMachinesExtruderStacks()
self._connectUpgradedExtruderStacksToMachines() self._connectUpgradedExtruderStacksToMachines()
## Check if the metadata for a container is okay before adding it.
#
# This overrides the one from UM.Settings.ContainerRegistry because we
# also require that the setting_version is correct.
@override(ContainerRegistry) @override(ContainerRegistry)
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
"""Check if the metadata for a container is okay before adding it.
This overrides the one from UM.Settings.ContainerRegistry because we
also require that the setting_version is correct.
"""
if metadata is None: if metadata is None:
return False return False
if "setting_version" not in metadata: if "setting_version" not in metadata:
@ -355,14 +369,16 @@ class CuraContainerRegistry(ContainerRegistry):
return False return False
return True return True
## Update an imported profile to match the current machine configuration.
#
# \param profile The profile to configure.
# \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers.
# \param new_name The new name for the profile.
#
# \return None if configuring was successful or an error message if an error occurred.
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
"""Update an imported profile to match the current machine configuration.
:param profile: The profile to configure.
:param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
:param new_name: The new name for the profile.
:return: None if configuring was successful or an error message if an error occurred.
"""
profile.setDirty(True) # Ensure the profiles are correctly saved profile.setDirty(True) # Ensure the profiles are correctly saved
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
@ -420,9 +436,11 @@ class CuraContainerRegistry(ContainerRegistry):
for stack in self.findContainerStacks(): for stack in self.findContainerStacks():
self.saveContainer(stack) self.saveContainer(stack)
## Gets a list of profile writer plugins
# \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type): def _getIOPlugins(self, io_type):
"""Gets a list of profile writer plugins
:return: List of tuples of (plugin_id, meta_data).
"""
plugin_registry = PluginRegistry.getInstance() plugin_registry = PluginRegistry.getInstance()
active_plugin_ids = plugin_registry.getActivePlugins() active_plugin_ids = plugin_registry.getActivePlugins()
@ -433,8 +451,9 @@ class CuraContainerRegistry(ContainerRegistry):
result.append( (plugin_id, meta_data) ) result.append( (plugin_id, meta_data) )
return result return result
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
"""Convert an "old-style" pure ContainerStack to either an Extruder or Global stack."""
assert type(container) == ContainerStack assert type(container) == ContainerStack
container_type = container.getMetaDataEntry("type") container_type = container.getMetaDataEntry("type")

View File

@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers
from . import Exceptions from . import Exceptions
## Base class for Cura related stacks that want to enforce certain containers are available.
#
# This class makes sure that the stack has the following containers set: user changes, quality
# changes, quality, material, variant, definition changes and finally definition. Initially,
# these will be equal to the empty instance container.
#
# The container types are determined based on the following criteria:
# - user: An InstanceContainer with the metadata entry "type" set to "user".
# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
# - quality: An InstanceContainer with the metadata entry "type" set to "quality".
# - material: An InstanceContainer with the metadata entry "type" set to "material".
# - variant: An InstanceContainer with the metadata entry "type" set to "variant".
# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
# - definition: A DefinitionContainer.
#
# Internally, this class ensures the mentioned containers are always there and kept in a specific order.
# This also means that operations on the stack that modifies the container ordering is prohibited and
# will raise an exception.
class CuraContainerStack(ContainerStack): class CuraContainerStack(ContainerStack):
"""Base class for Cura related stacks that want to enforce certain containers are available.
This class makes sure that the stack has the following containers set: user changes, quality
changes, quality, material, variant, definition changes and finally definition. Initially,
these will be equal to the empty instance container.
The container types are determined based on the following criteria:
- user: An InstanceContainer with the metadata entry "type" set to "user".
- quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
- quality: An InstanceContainer with the metadata entry "type" set to "quality".
- material: An InstanceContainer with the metadata entry "type" set to "material".
- variant: An InstanceContainer with the metadata entry "type" set to "variant".
- definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
- definition: A DefinitionContainer.
Internally, this class ensures the mentioned containers are always there and kept in a specific order.
This also means that operations on the stack that modifies the container ordering is prohibited and
will raise an exception.
"""
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
@ -61,101 +63,131 @@ class CuraContainerStack(ContainerStack):
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted. # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
pyqtContainersChanged = pyqtSignal() pyqtContainersChanged = pyqtSignal()
## Set the user changes container.
#
# \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user".
def setUserChanges(self, new_user_changes: InstanceContainer) -> None: def setUserChanges(self, new_user_changes: InstanceContainer) -> None:
"""Set the user changes container.
:param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user".
"""
self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes) self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes)
## Get the user changes container.
#
# \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
def userChanges(self) -> InstanceContainer: def userChanges(self) -> InstanceContainer:
"""Get the user changes container.
:return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges])
## Set the quality changes container.
#
# \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
"""Set the quality changes container.
:param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
"""
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
## Get the quality changes container.
#
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
def qualityChanges(self) -> InstanceContainer: def qualityChanges(self) -> InstanceContainer:
"""Get the quality changes container.
:return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges])
## Set the intent container.
#
# \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent".
def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None:
"""Set the intent container.
:param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent".
"""
self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit)
## Get the quality container.
#
# \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged)
def intent(self) -> InstanceContainer: def intent(self) -> InstanceContainer:
"""Get the quality container.
:return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent]) return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent])
## Set the quality container.
#
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None:
"""Set the quality container.
:param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality".
"""
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
## Get the quality container.
#
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
def quality(self) -> InstanceContainer: def quality(self) -> InstanceContainer:
"""Get the quality container.
:return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality]) return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality])
## Set the material container.
#
# \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None:
"""Set the material container.
:param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material".
"""
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
## Get the material container.
#
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
def material(self) -> InstanceContainer: def material(self) -> InstanceContainer:
"""Get the material container.
:return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) return cast(InstanceContainer, self._containers[_ContainerIndexes.Material])
## Set the variant container.
#
# \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant".
def setVariant(self, new_variant: InstanceContainer) -> None: def setVariant(self, new_variant: InstanceContainer) -> None:
"""Set the variant container.
:param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant".
"""
self.replaceContainer(_ContainerIndexes.Variant, new_variant) self.replaceContainer(_ContainerIndexes.Variant, new_variant)
## Get the variant container.
#
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
def variant(self) -> InstanceContainer: def variant(self) -> InstanceContainer:
"""Get the variant container.
:return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant])
## Set the definition changes container.
#
# \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
"""Set the definition changes container.
:param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
"""
self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
## Get the definition changes container.
#
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
def definitionChanges(self) -> InstanceContainer: def definitionChanges(self) -> InstanceContainer:
"""Get the definition changes container.
:return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
"""
return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges])
## Set the definition container.
#
# \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition".
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
"""Set the definition container.
:param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition".
"""
self.replaceContainer(_ContainerIndexes.Definition, new_definition) self.replaceContainer(_ContainerIndexes.Definition, new_definition)
def getDefinition(self) -> "DefinitionContainer": def getDefinition(self) -> "DefinitionContainer":
@ -171,14 +203,16 @@ class CuraContainerStack(ContainerStack):
def getTop(self) -> "InstanceContainer": def getTop(self) -> "InstanceContainer":
return self.userChanges return self.userChanges
## Check whether the specified setting has a 'user' value.
#
# A user value here is defined as the setting having a value in either
# the UserChanges or QualityChanges container.
#
# \return True if the setting has a user value, False if not.
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
def hasUserValue(self, key: str) -> bool: def hasUserValue(self, key: str) -> bool:
"""Check whether the specified setting has a 'user' value.
A user value here is defined as the setting having a value in either
the UserChanges or QualityChanges container.
:return: True if the setting has a user value, False if not.
"""
if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"): if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"):
return True return True
@ -187,51 +221,61 @@ class CuraContainerStack(ContainerStack):
return False return False
## Set a property of a setting.
#
# This will set a property of a specified setting. Since the container stack does not contain
# any settings itself, it is required to specify a container to set the property on. The target
# container is matched by container type.
#
# \param key The key of the setting to set.
# \param property_name The name of the property to set.
# \param new_value The new value to set the property to.
def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None:
"""Set a property of a setting.
This will set a property of a specified setting. Since the container stack does not contain
any settings itself, it is required to specify a container to set the property on. The target
container is matched by container type.
:param key: The key of the setting to set.
:param property_name: The name of the property to set.
:param new_value: The new value to set the property to.
"""
container_index = _ContainerIndexes.UserChanges container_index = _ContainerIndexes.UserChanges
self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache)
## Overridden from ContainerStack
#
# Since we have a fixed order of containers in the stack and this method would modify the container
# ordering, we disallow this operation.
@override(ContainerStack) @override(ContainerStack)
def addContainer(self, container: ContainerInterface) -> None: def addContainer(self, container: ContainerInterface) -> None:
"""Overridden from ContainerStack
Since we have a fixed order of containers in the stack and this method would modify the container
ordering, we disallow this operation.
"""
raise Exceptions.InvalidOperationError("Cannot add a container to Global stack") raise Exceptions.InvalidOperationError("Cannot add a container to Global stack")
## Overridden from ContainerStack
#
# Since we have a fixed order of containers in the stack and this method would modify the container
# ordering, we disallow this operation.
@override(ContainerStack) @override(ContainerStack)
def insertContainer(self, index: int, container: ContainerInterface) -> None: def insertContainer(self, index: int, container: ContainerInterface) -> None:
"""Overridden from ContainerStack
Since we have a fixed order of containers in the stack and this method would modify the container
ordering, we disallow this operation.
"""
raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack") raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack")
## Overridden from ContainerStack
#
# Since we have a fixed order of containers in the stack and this method would modify the container
# ordering, we disallow this operation.
@override(ContainerStack) @override(ContainerStack)
def removeContainer(self, index: int = 0) -> None: def removeContainer(self, index: int = 0) -> None:
"""Overridden from ContainerStack
Since we have a fixed order of containers in the stack and this method would modify the container
ordering, we disallow this operation.
"""
raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack") raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack")
## Overridden from ContainerStack
#
# Replaces the container at the specified index with another container.
# This version performs checks to make sure the new container has the expected metadata and type.
#
# \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
@override(ContainerStack) @override(ContainerStack)
def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None:
"""Overridden from ContainerStack
Replaces the container at the specified index with another container.
This version performs checks to make sure the new container has the expected metadata and type.
:throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
"""
expected_type = _ContainerIndexes.IndexTypeMap[index] expected_type = _ContainerIndexes.IndexTypeMap[index]
if expected_type == "definition": if expected_type == "definition":
if not isinstance(container, DefinitionContainer): if not isinstance(container, DefinitionContainer):
@ -245,16 +289,18 @@ class CuraContainerStack(ContainerStack):
super().replaceContainer(index, container, postpone_emit) super().replaceContainer(index, container, postpone_emit)
## Overridden from ContainerStack
#
# This deserialize will make sure the internal list of containers matches with what we expect.
# It will first check to see if the container at a certain index already matches with what we
# expect. If it does not, it will search for a matching container with the correct type. Should
# no container with the correct type be found, it will use the empty container.
#
# \throws InvalidContainerStackError Raised when no definition can be found for the stack.
@override(ContainerStack) @override(ContainerStack)
def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str:
"""Overridden from ContainerStack
This deserialize will make sure the internal list of containers matches with what we expect.
It will first check to see if the container at a certain index already matches with what we
expect. If it does not, it will search for a matching container with the correct type. Should
no container with the correct type be found, it will use the empty container.
:raise InvalidContainerStackError: Raised when no definition can be found for the stack.
"""
# update the serialized data first # update the serialized data first
serialized = super().deserialize(serialized, file_name) serialized = super().deserialize(serialized, file_name)
@ -298,10 +344,9 @@ class CuraContainerStack(ContainerStack):
## TODO; Deserialize the containers. ## TODO; Deserialize the containers.
return serialized return serialized
## protected:
# Helper to make sure we emit a PyQt signal on container changes.
def _onContainersChanged(self, container: Any) -> None: def _onContainersChanged(self, container: Any) -> None:
"""Helper to make sure we emit a PyQt signal on container changes."""
Application.getInstance().callLater(self.pyqtContainersChanged.emit) Application.getInstance().callLater(self.pyqtContainersChanged.emit)
# Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
@ -309,16 +354,18 @@ class CuraContainerStack(ContainerStack):
def _getMachineDefinition(self) -> DefinitionContainer: def _getMachineDefinition(self) -> DefinitionContainer:
return self.definition return self.definition
## Find the ID that should be used when searching for instance containers for a specified definition.
#
# This handles the situation where the definition specifies we should use a different definition when
# searching for instance containers.
#
# \param machine_definition The definition to find the "quality definition" for.
#
# \return The ID of the definition container to use when searching for instance containers.
@classmethod @classmethod
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str:
"""Find the ID that should be used when searching for instance containers for a specified definition.
This handles the situation where the definition specifies we should use a different definition when
searching for instance containers.
:param machine_definition: The definition to find the "quality definition" for.
:return: The ID of the definition container to use when searching for instance containers.
"""
quality_definition = machine_definition.getMetaDataEntry("quality_definition") quality_definition = machine_definition.getMetaDataEntry("quality_definition")
if not quality_definition: if not quality_definition:
return machine_definition.id #type: ignore return machine_definition.id #type: ignore
@ -330,17 +377,18 @@ class CuraContainerStack(ContainerStack):
return cls._findInstanceContainerDefinitionId(definitions[0]) return cls._findInstanceContainerDefinitionId(definitions[0])
## getProperty for extruder positions, with translation from -1 to default extruder number
def getExtruderPositionValueWithDefault(self, key): def getExtruderPositionValueWithDefault(self, key):
"""getProperty for extruder positions, with translation from -1 to default extruder number"""
value = self.getProperty(key, "value") value = self.getProperty(key, "value")
if value == -1: if value == -1:
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition) value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
return value return value
## private:
# Private helper class to keep track of container positions and their types.
class _ContainerIndexes: class _ContainerIndexes:
"""Private helper class to keep track of container positions and their types."""
UserChanges = 0 UserChanges = 0
QualityChanges = 1 QualityChanges = 1
Intent = 2 Intent = 2

View File

@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack from .ExtruderStack import ExtruderStack
## Contains helper functions to create new machines.
class CuraStackBuilder: class CuraStackBuilder:
"""Contains helper functions to create new machines."""
## Create a new instance of a machine.
#
# \param name The name of the new machine.
# \param definition_id The ID of the machine definition to use.
#
# \return The new global stack or None if an error occurred.
@classmethod @classmethod
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
"""Create a new instance of a machine.
:param name: The name of the new machine.
:param definition_id: The ID of the machine definition to use.
:return: The new global stack or None if an error occurred.
"""
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -71,12 +74,14 @@ class CuraStackBuilder:
return new_global_stack return new_global_stack
## Create a default Extruder Stack
#
# \param global_stack The global stack this extruder refers to.
# \param extruder_position The position of the current extruder.
@classmethod @classmethod
def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
"""Create a default Extruder Stack
:param global_stack: The global stack this extruder refers to.
:param extruder_position: The position of the current extruder.
"""
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -120,17 +125,6 @@ class CuraStackBuilder:
registry.addContainer(new_extruder) registry.addContainer(new_extruder)
## Create a new Extruder stack
#
# \param new_stack_id The ID of the new stack.
# \param extruder_definition The definition to base the new stack on.
# \param machine_definition_id The ID of the machine definition to use for the user container.
# \param position The position the extruder occupies in the machine.
# \param variant_container The variant selected for the current extruder.
# \param material_container The material selected for the current extruder.
# \param quality_container The quality selected for the current extruder.
#
# \return A new Extruder stack instance with the specified parameters.
@classmethod @classmethod
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface,
machine_definition_id: str, machine_definition_id: str,
@ -139,6 +133,19 @@ class CuraStackBuilder:
material_container: "InstanceContainer", material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> ExtruderStack: quality_container: "InstanceContainer") -> ExtruderStack:
"""Create a new Extruder stack
:param new_stack_id: The ID of the new stack.
:param extruder_definition: The definition to base the new stack on.
:param machine_definition_id: The ID of the machine definition to use for the user container.
:param position: The position the extruder occupies in the machine.
:param variant_container: The variant selected for the current extruder.
:param material_container: The material selected for the current extruder.
:param quality_container: The quality selected for the current extruder.
:return: A new Extruder stack instance with the specified parameters.
"""
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -167,29 +174,23 @@ class CuraStackBuilder:
return stack return stack
## Create a new Global stack
#
# \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on.
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
#
# \return A new Global stack instance with the specified parameters.
## Create a new Global stack
#
# \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on.
# \param variant_container The variant selected for the current stack.
# \param material_container The material selected for the current stack.
# \param quality_container The quality selected for the current stack.
#
# \return A new Global stack instance with the specified parameters.
@classmethod @classmethod
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
variant_container: "InstanceContainer", variant_container: "InstanceContainer",
material_container: "InstanceContainer", material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> GlobalStack: quality_container: "InstanceContainer") -> GlobalStack:
"""Create a new Global stack
:param new_stack_id: The ID of the new stack.
:param definition: The definition to base the new stack on.
:param variant_container: The variant selected for the current stack.
:param material_container: The material selected for the current stack.
:param quality_container: The quality selected for the current stack.
:return: A new Global stack instance with the specified parameters.
"""
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()

View File

@ -2,21 +2,25 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
## Raised when trying to perform an operation like add on a stack that does not allow that.
class InvalidOperationError(Exception): class InvalidOperationError(Exception):
"""Raised when trying to perform an operation like add on a stack that does not allow that."""
pass pass
## Raised when trying to replace a container with a container that does not have the expected type.
class InvalidContainerError(Exception): class InvalidContainerError(Exception):
"""Raised when trying to replace a container with a container that does not have the expected type."""
pass pass
## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.
class TooManyExtrudersError(Exception): class TooManyExtrudersError(Exception):
"""Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders."""
pass pass
## Raised when an extruder has no next stack set.
class NoGlobalStackError(Exception): class NoGlobalStackError(Exception):
"""Raised when an extruder has no next stack set."""
pass pass

View File

@ -19,13 +19,15 @@ if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
## Manages all existing extruder stacks.
#
# This keeps a list of extruder stacks for each machine.
class ExtruderManager(QObject): class ExtruderManager(QObject):
"""Manages all existing extruder stacks.
This keeps a list of extruder stacks for each machine.
"""
## Registers listeners and such to listen to changes to the extruders.
def __init__(self, parent = None): def __init__(self, parent = None):
"""Registers listeners and such to listen to changes to the extruders."""
if ExtruderManager.__instance is not None: if ExtruderManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ExtruderManager.__instance = self ExtruderManager.__instance = self
@ -43,20 +45,22 @@ class ExtruderManager(QObject):
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes.
extrudersChanged = pyqtSignal(QVariant) extrudersChanged = pyqtSignal(QVariant)
"""Signal to notify other components when the list of extruders for a machine definition changes."""
## Notify when the user switches the currently active extruder.
activeExtruderChanged = pyqtSignal() activeExtruderChanged = pyqtSignal()
"""Notify when the user switches the currently active extruder."""
## Gets the unique identifier of the currently active extruder stack.
#
# The currently active extruder stack is the stack that is currently being
# edited.
#
# \return The unique ID of the currently active extruder stack.
@pyqtProperty(str, notify = activeExtruderChanged) @pyqtProperty(str, notify = activeExtruderChanged)
def activeExtruderStackId(self) -> Optional[str]: def activeExtruderStackId(self) -> Optional[str]:
"""Gets the unique identifier of the currently active extruder stack.
The currently active extruder stack is the stack that is currently being
edited.
:return: The unique ID of the currently active extruder stack.
"""
if not self._application.getGlobalContainerStack(): if not self._application.getGlobalContainerStack():
return None # No active machine, so no active extruder. return None # No active machine, so no active extruder.
try: try:
@ -64,9 +68,10 @@ class ExtruderManager(QObject):
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None return None
## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]: def extruderIds(self) -> Dict[str, str]:
"""Gets a dict with the extruder stack ids with the extruder number as the key."""
extruder_stack_ids = {} # type: Dict[str, str] extruder_stack_ids = {} # type: Dict[str, str]
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
@ -75,11 +80,13 @@ class ExtruderManager(QObject):
return extruder_stack_ids return extruder_stack_ids
## Changes the active extruder by index.
#
# \param index The index of the new active extruder.
@pyqtSlot(int) @pyqtSlot(int)
def setActiveExtruderIndex(self, index: int) -> None: def setActiveExtruderIndex(self, index: int) -> None:
"""Changes the active extruder by index.
:param index: The index of the new active extruder.
"""
if self._active_extruder_index != index: if self._active_extruder_index != index:
self._active_extruder_index = index self._active_extruder_index = index
self.activeExtruderChanged.emit() self.activeExtruderChanged.emit()
@ -88,12 +95,13 @@ class ExtruderManager(QObject):
def activeExtruderIndex(self) -> int: def activeExtruderIndex(self) -> int:
return self._active_extruder_index return self._active_extruder_index
## Emitted whenever the selectedObjectExtruders property changes.
selectedObjectExtrudersChanged = pyqtSignal() selectedObjectExtrudersChanged = pyqtSignal()
"""Emitted whenever the selectedObjectExtruders property changes."""
## Provides a list of extruder IDs used by the current selected objects.
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
"""Provides a list of extruder IDs used by the current selected objects."""
if not self._selected_object_extruders: if not self._selected_object_extruders:
object_extruders = set() object_extruders = set()
@ -122,11 +130,13 @@ class ExtruderManager(QObject):
return self._selected_object_extruders return self._selected_object_extruders
## Reset the internal list used for the selectedObjectExtruders property
#
# This will trigger a recalculation of the extruders used for the
# selection.
def resetSelectedObjectExtruders(self) -> None: def resetSelectedObjectExtruders(self) -> None:
"""Reset the internal list used for the selectedObjectExtruders property
This will trigger a recalculation of the extruders used for the
selection.
"""
self._selected_object_extruders = [] self._selected_object_extruders = []
self.selectedObjectExtrudersChanged.emit() self.selectedObjectExtrudersChanged.emit()
@ -134,8 +144,9 @@ class ExtruderManager(QObject):
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
return self.getExtruderStack(self.activeExtruderIndex) return self.getExtruderStack(self.activeExtruderIndex)
## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]: def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
"""Get an extruder stack by index"""
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack: if global_container_stack:
if global_container_stack.getId() in self._extruder_trains: if global_container_stack.getId() in self._extruder_trains:
@ -162,12 +173,14 @@ class ExtruderManager(QObject):
if changed: if changed:
self.extrudersChanged.emit(machine_id) self.extrudersChanged.emit(machine_id)
## Gets a property of a setting for all extruders.
#
# \param setting_key \type{str} The setting to get the property of.
# \param property \type{str} The property to get.
# \return \type{List} the list of results
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]: def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]:
"""Gets a property of a setting for all extruders.
:param setting_key: :type{str} The setting to get the property of.
:param prop: :type{str} The property to get.
:return: :type{List} the list of results
"""
result = [] result = []
for extruder_stack in self.getActiveExtruderStacks(): for extruder_stack in self.getActiveExtruderStacks():
@ -182,17 +195,19 @@ class ExtruderManager(QObject):
else: else:
return value return value
## Gets the extruder stacks that are actually being used at the moment.
#
# An extruder stack is being used if it is the extruder to print any mesh
# with, or if it is the support infill extruder, the support interface
# extruder, or the bed adhesion extruder.
#
# If there are no extruders, this returns the global stack as a singleton
# list.
#
# \return A list of extruder stacks.
def getUsedExtruderStacks(self) -> List["ExtruderStack"]: def getUsedExtruderStacks(self) -> List["ExtruderStack"]:
"""Gets the extruder stacks that are actually being used at the moment.
An extruder stack is being used if it is the extruder to print any mesh
with, or if it is the support infill extruder, the support interface
extruder, or the bed adhesion extruder.
If there are no extruders, this returns the global stack as a singleton
list.
:return: A list of extruder stacks.
"""
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
@ -277,11 +292,13 @@ class ExtruderManager(QObject):
Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
return [] return []
## Get the extruder that the print will start with.
#
# This should mirror the implementation in CuraEngine of
# ``FffGcodeWriter::getStartExtruder()``.
def getInitialExtruderNr(self) -> int: def getInitialExtruderNr(self) -> int:
"""Get the extruder that the print will start with.
This should mirror the implementation in CuraEngine of
``FffGcodeWriter::getStartExtruder()``.
"""
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack() global_stack = application.getGlobalContainerStack()
@ -296,28 +313,34 @@ class ExtruderManager(QObject):
# REALLY no adhesion? Use the first used extruder. # REALLY no adhesion? Use the first used extruder.
return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value") return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value")
## Removes the container stack and user profile for the extruders for a specific machine.
#
# \param machine_id The machine to remove the extruders for.
def removeMachineExtruders(self, machine_id: str) -> None: def removeMachineExtruders(self, machine_id: str) -> None:
"""Removes the container stack and user profile for the extruders for a specific machine.
:param machine_id: The machine to remove the extruders for.
"""
for extruder in self.getMachineExtruders(machine_id): for extruder in self.getMachineExtruders(machine_id):
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
ContainerRegistry.getInstance().removeContainer(extruder.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId())
if machine_id in self._extruder_trains: if machine_id in self._extruder_trains:
del self._extruder_trains[machine_id] del self._extruder_trains[machine_id]
## Returns extruders for a specific machine.
#
# \param machine_id The machine to get the extruders of.
def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]: def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]:
"""Returns extruders for a specific machine.
:param machine_id: The machine to get the extruders of.
"""
if machine_id not in self._extruder_trains: if machine_id not in self._extruder_trains:
return [] return []
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
## Returns the list of active extruder stacks, taking into account the machine extruder count.
#
# \return \type{List[ContainerStack]} a list of
def getActiveExtruderStacks(self) -> List["ExtruderStack"]: def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
"""Returns the list of active extruder stacks, taking into account the machine extruder count.
:return: :type{List[ContainerStack]} a list of
"""
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
if not global_stack: if not global_stack:
return [] return []
@ -329,8 +352,9 @@ class ExtruderManager(QObject):
self.resetSelectedObjectExtruders() self.resetSelectedObjectExtruders()
## Adds the extruders to the selected machine.
def addMachineExtruders(self, global_stack: GlobalStack) -> None: def addMachineExtruders(self, global_stack: GlobalStack) -> None:
"""Adds the extruders to the selected machine."""
extruders_changed = False extruders_changed = False
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
global_stack_id = global_stack.getId() global_stack_id = global_stack.getId()
@ -396,26 +420,30 @@ class ExtruderManager(QObject):
raise IndexError(msg) raise IndexError(msg)
extruder_stack_0.definition = extruder_definition extruder_stack_0.definition = extruder_definition
## Get all extruder values for a certain setting.
#
# This is exposed to qml for display purposes
#
# \param key The key of the setting to retrieve values for.
#
# \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key: str) -> List: def getInstanceExtruderValues(self, key: str) -> List:
"""Get all extruder values for a certain setting.
This is exposed to qml for display purposes
:param key: The key of the setting to retrieve values for.
:return: String representing the extruder values
"""
return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key) return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
## Get the resolve value or value for a given key
#
# This is the effective value for a given key, it is used for values in the global stack.
# This is exposed to SettingFunction to use in value functions.
# \param key The key of the setting to get the value of.
#
# \return The effective value
@staticmethod @staticmethod
def getResolveOrValue(key: str) -> Any: def getResolveOrValue(key: str) -> Any:
"""Get the resolve value or value for a given key
This is the effective value for a given key, it is used for values in the global stack.
This is exposed to SettingFunction to use in value functions.
:param key: The key of the setting to get the value of.
:return: The effective value
"""
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
resolved_value = global_stack.getProperty(key, "value") resolved_value = global_stack.getProperty(key, "value")

View File

@ -22,10 +22,9 @@ if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
## Represents an Extruder and its related containers.
#
#
class ExtruderStack(CuraContainerStack): class ExtruderStack(CuraContainerStack):
"""Represents an Extruder and its related containers."""
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
@ -35,11 +34,13 @@ class ExtruderStack(CuraContainerStack):
enabledChanged = pyqtSignal() enabledChanged = pyqtSignal()
## Overridden from ContainerStack
#
# This will set the next stack and ensure that we register this stack as an extruder.
@override(ContainerStack) @override(ContainerStack)
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
"""Overridden from ContainerStack
This will set the next stack and ensure that we register this stack as an extruder.
"""
super().setNextStack(stack) super().setNextStack(stack)
stack.addExtruder(self) stack.addExtruder(self)
self.setMetaDataEntry("machine", stack.id) self.setMetaDataEntry("machine", stack.id)
@ -71,11 +72,13 @@ class ExtruderStack(CuraContainerStack):
compatibleMaterialDiameterChanged = pyqtSignal() compatibleMaterialDiameterChanged = pyqtSignal()
## Return the filament diameter that the machine requires.
#
# If the machine has no requirement for the diameter, -1 is returned.
# \return The filament diameter for the printer
def getCompatibleMaterialDiameter(self) -> float: def getCompatibleMaterialDiameter(self) -> float:
"""Return the filament diameter that the machine requires.
If the machine has no requirement for the diameter, -1 is returned.
:return: The filament diameter for the printer
"""
context = PropertyEvaluationContext(self) context = PropertyEvaluationContext(self)
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
@ -97,31 +100,35 @@ class ExtruderStack(CuraContainerStack):
approximateMaterialDiameterChanged = pyqtSignal() approximateMaterialDiameterChanged = pyqtSignal()
## Return the approximate filament diameter that the machine requires.
#
# The approximate material diameter is the material diameter rounded to
# the nearest millimetre.
#
# If the machine has no requirement for the diameter, -1 is returned.
#
# \return The approximate filament diameter for the printer
def getApproximateMaterialDiameter(self) -> float: def getApproximateMaterialDiameter(self) -> float:
"""Return the approximate filament diameter that the machine requires.
The approximate material diameter is the material diameter rounded to
the nearest millimetre.
If the machine has no requirement for the diameter, -1 is returned.
:return: The approximate filament diameter for the printer
"""
return round(self.getCompatibleMaterialDiameter()) return round(self.getCompatibleMaterialDiameter())
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter, approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
notify = approximateMaterialDiameterChanged) notify = approximateMaterialDiameterChanged)
## Overridden from ContainerStack
#
# It will perform a few extra checks when trying to get properties.
#
# The two extra checks it currently does is to ensure a next stack is set and to bypass
# the extruder when the property is not settable per extruder.
#
# \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without
# having a next stack set.
@override(ContainerStack) @override(ContainerStack)
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
"""Overridden from ContainerStack
It will perform a few extra checks when trying to get properties.
The two extra checks it currently does is to ensure a next stack is set and to bypass
the extruder when the property is not settable per extruder.
:throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without
having a next stack set.
"""
if not self._next_stack: if not self._next_stack:
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id)) raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))

View File

@ -29,9 +29,9 @@ if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
## Represents the Global or Machine stack and its related containers.
#
class GlobalStack(CuraContainerStack): class GlobalStack(CuraContainerStack):
"""Represents the Global or Machine stack and its related containers."""
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
@ -58,12 +58,14 @@ class GlobalStack(CuraContainerStack):
extrudersChanged = pyqtSignal() extrudersChanged = pyqtSignal()
configuredConnectionTypesChanged = pyqtSignal() configuredConnectionTypesChanged = pyqtSignal()
## Get the list of extruders of this stack.
#
# \return The extruders registered with this stack.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
@deprecated("Please use extruderList instead.", "4.4") @deprecated("Please use extruderList instead.", "4.4")
def extruders(self) -> Dict[str, "ExtruderStack"]: def extruders(self) -> Dict[str, "ExtruderStack"]:
"""Get the list of extruders of this stack.
:return: The extruders registered with this stack.
"""
return self._extruders return self._extruders
@pyqtProperty("QVariantList", notify = extrudersChanged) @pyqtProperty("QVariantList", notify = extrudersChanged)
@ -86,16 +88,18 @@ class GlobalStack(CuraContainerStack):
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 2 return 2
## The configured connection types can be used to find out if the global
# stack is configured to be connected with a printer, without having to
# know all the details as to how this is exactly done (and without
# actually setting the stack to be active).
#
# This data can then in turn also be used when the global stack is active;
# If we can't get a network connection, but it is configured to have one,
# we can display a different icon to indicate the difference.
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
def configuredConnectionTypes(self) -> List[int]: def configuredConnectionTypes(self) -> List[int]:
"""The configured connection types can be used to find out if the global
stack is configured to be connected with a printer, without having to
know all the details as to how this is exactly done (and without
actually setting the stack to be active).
This data can then in turn also be used when the global stack is active;
If we can't get a network connection, but it is configured to have one,
we can display a different icon to indicate the difference.
"""
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing). # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
# But we do want them returned as a list of ints (so the rest of the code can directly compare) # But we do want them returned as a list of ints (so the rest of the code can directly compare)
connection_types = self.getMetaDataEntry("connection_type", "").split(",") connection_types = self.getMetaDataEntry("connection_type", "").split(",")
@ -122,16 +126,18 @@ class GlobalStack(CuraContainerStack):
ConnectionType.CloudConnection.value] ConnectionType.CloudConnection.value]
return has_remote_connection return has_remote_connection
## \sa configuredConnectionTypes
def addConfiguredConnectionType(self, connection_type: int) -> None: def addConfiguredConnectionType(self, connection_type: int) -> None:
""":sa configuredConnectionTypes"""
configured_connection_types = self.configuredConnectionTypes configured_connection_types = self.configuredConnectionTypes
if connection_type not in configured_connection_types: if connection_type not in configured_connection_types:
# Store the values as a string. # Store the values as a string.
configured_connection_types.append(connection_type) configured_connection_types.append(connection_type)
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
## \sa configuredConnectionTypes
def removeConfiguredConnectionType(self, connection_type: int) -> None: def removeConfiguredConnectionType(self, connection_type: int) -> None:
""":sa configuredConnectionTypes"""
configured_connection_types = self.configuredConnectionTypes configured_connection_types = self.configuredConnectionTypes
if connection_type in configured_connection_types: if connection_type in configured_connection_types:
# Store the values as a string. # Store the values as a string.
@ -163,13 +169,15 @@ class GlobalStack(CuraContainerStack):
def preferred_output_file_formats(self) -> str: def preferred_output_file_formats(self) -> str:
return self.getMetaDataEntry("file_formats") return self.getMetaDataEntry("file_formats")
## Add an extruder to the list of extruders of this stack.
#
# \param extruder The extruder to add.
#
# \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we
# already have the maximum number of extruders.
def addExtruder(self, extruder: ContainerStack) -> None: def addExtruder(self, extruder: ContainerStack) -> None:
"""Add an extruder to the list of extruders of this stack.
:param extruder: The extruder to add.
:raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we
already have the maximum number of extruders.
"""
position = extruder.getMetaDataEntry("position") position = extruder.getMetaDataEntry("position")
if position is None: if position is None:
Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id) Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
@ -183,19 +191,21 @@ class GlobalStack(CuraContainerStack):
self.extrudersChanged.emit() self.extrudersChanged.emit()
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
## Overridden from ContainerStack
#
# This will return the value of the specified property for the specified setting,
# unless the property is "value" and that setting has a "resolve" function set.
# When a resolve is set, it will instead try and execute the resolve first and
# then fall back to the normal "value" property.
#
# \param key The setting key to get the property of.
# \param property_name The property to get the value of.
#
# \return The value of the property for the specified setting, or None if not found.
@override(ContainerStack) @override(ContainerStack)
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
"""Overridden from ContainerStack
This will return the value of the specified property for the specified setting,
unless the property is "value" and that setting has a "resolve" function set.
When a resolve is set, it will instead try and execute the resolve first and
then fall back to the normal "value" property.
:param key: The setting key to get the property of.
:param property_name: The property to get the value of.
:return: The value of the property for the specified setting, or None if not found.
"""
if not self.definition.findDefinitions(key = key): if not self.definition.findDefinitions(key = key):
return None return None
@ -235,11 +245,13 @@ class GlobalStack(CuraContainerStack):
context.popContainer() context.popContainer()
return result return result
## Overridden from ContainerStack
#
# This will simply raise an exception since the Global stack cannot have a next stack.
@override(ContainerStack) @override(ContainerStack)
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
"""Overridden from ContainerStack
This will simply raise an exception since the Global stack cannot have a next stack.
"""
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!") raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
# protected: # protected:
@ -267,9 +279,11 @@ class GlobalStack(CuraContainerStack):
return True return True
## Perform some sanity checks on the global stack
# Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
def isValid(self) -> bool: def isValid(self) -> bool:
"""Perform some sanity checks on the global stack
Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
"""
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId()) extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
@ -299,9 +313,10 @@ class GlobalStack(CuraContainerStack):
def hasVariantBuildplates(self) -> bool: def hasVariantBuildplates(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False)) return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = str) @pyqtSlot(result = str)
def getDefaultFirmwareName(self) -> str: def getDefaultFirmwareName(self) -> str:
"""Get default firmware file name if one is specified in the firmware"""
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value") machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
baudrate = 250000 baudrate = 250000

View File

@ -15,29 +15,32 @@ if TYPE_CHECKING:
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
## Front-end for querying which intents are available for a certain
# configuration.
class IntentManager(QObject): class IntentManager(QObject):
"""Front-end for querying which intents are available for a certain configuration.
"""
__instance = None __instance = None
## This class is a singleton.
@classmethod @classmethod
def getInstance(cls): def getInstance(cls):
"""This class is a singleton."""
if not cls.__instance: if not cls.__instance:
cls.__instance = IntentManager() cls.__instance = IntentManager()
return cls.__instance return cls.__instance
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories. intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
## Gets the metadata dictionaries of all intent profiles for a given
# configuration.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_base_file The base_file of the material.
# \return A list of metadata dictionaries matching the search criteria, or
# an empty list if nothing was found.
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
"""Gets the metadata dictionaries of all intent profiles for a given
configuration.
:param definition_id: ID of the printer.
:param nozzle_name: Name of the nozzle.
:param material_base_file: The base_file of the material.
:return: A list of metadata dictionaries matching the search criteria, or
an empty list if nothing was found.
"""
intent_metadatas = [] # type: List[Dict[str, Any]] intent_metadatas = [] # type: List[Dict[str, Any]]
try: try:
materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
@ -53,28 +56,32 @@ class IntentManager(QObject):
intent_metadatas.append(intent_node.getMetadata()) intent_metadatas.append(intent_node.getMetadata())
return intent_metadatas return intent_metadatas
## Collects and returns all intent categories available for the given
# parameters. Note that the 'default' category is always available.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_id ID of the material.
# \return A set of intent category names.
def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]: def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
"""Collects and returns all intent categories available for the given
parameters. Note that the 'default' category is always available.
:param definition_id: ID of the printer.
:param nozzle_name: Name of the nozzle.
:param material_id: ID of the material.
:return: A set of intent category names.
"""
categories = set() categories = set()
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id): for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
categories.add(intent["intent_category"]) categories.add(intent["intent_category"])
categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list. categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
return list(categories) return list(categories)
## List of intents to be displayed in the interface.
#
# For the interface this will have to be broken up into the different
# intent categories. That is up to the model there.
#
# \return A list of tuples of intent_category and quality_type. The actual
# instance may vary per extruder.
def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]: def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
"""List of intents to be displayed in the interface.
For the interface this will have to be broken up into the different
intent categories. That is up to the model there.
:return: A list of tuples of intent_category and quality_type. The actual
instance may vary per extruder.
"""
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack() global_stack = application.getGlobalContainerStack()
if global_stack is None: if global_stack is None:
@ -100,16 +107,18 @@ class IntentManager(QObject):
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"])) result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
return list(result) return list(result)
## List of intent categories available in either of the extruders.
#
# This is purposefully inconsistent with the way that the quality types
# are listed. The quality types will show all quality types available in
# the printer using any configuration. This will only list the intent
# categories that are available using the current configuration (but the
# union over the extruders).
# \return List of all categories in the current configurations of all
# extruders.
def currentAvailableIntentCategories(self) -> List[str]: def currentAvailableIntentCategories(self) -> List[str]:
"""List of intent categories available in either of the extruders.
This is purposefully inconsistent with the way that the quality types
are listed. The quality types will show all quality types available in
the printer using any configuration. This will only list the intent
categories that are available using the current configuration (but the
union over the extruders).
:return: List of all categories in the current configurations of all
extruders.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return ["default"] return ["default"]
@ -123,10 +132,12 @@ class IntentManager(QObject):
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id)) final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
return list(final_intent_categories) return list(final_intent_categories)
## The intent that gets selected by default when no intent is available for
# the configuration, an extruder can't match the intent that the user
# selects, or just when creating a new printer.
def getDefaultIntent(self) -> "InstanceContainer": def getDefaultIntent(self) -> "InstanceContainer":
"""The intent that gets selected by default when no intent is available for
the configuration, an extruder can't match the intent that the user
selects, or just when creating a new printer.
"""
return empty_intent_container return empty_intent_container
@pyqtProperty(str, notify = intentCategoryChanged) @pyqtProperty(str, notify = intentCategoryChanged)
@ -137,9 +148,10 @@ class IntentManager(QObject):
return "" return ""
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "") return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
## Apply intent on the stacks.
@pyqtSlot(str, str) @pyqtSlot(str, str)
def selectIntent(self, intent_category: str, quality_type: str) -> None: def selectIntent(self, intent_category: str, quality_type: str) -> None:
"""Apply intent on the stacks."""
Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type) Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
old_intent_category = self.currentIntentCategory old_intent_category = self.currentIntentCategory
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()

View File

@ -215,8 +215,9 @@ class MachineManager(QObject):
return set() return set()
return general_definition_containers[0].getAllKeys() return general_definition_containers[0].getAllKeys()
## Triggered when the global container stack is changed in CuraApplication.
def _onGlobalContainerChanged(self) -> None: def _onGlobalContainerChanged(self) -> None:
"""Triggered when the global container stack is changed in CuraApplication."""
if self._global_container_stack: if self._global_container_stack:
try: try:
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
@ -338,12 +339,15 @@ class MachineManager(QObject):
Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") Logger.log("w", "An extruder has an unknown material, switching it to the preferred material")
self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material) self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material)
## Given a definition id, return the machine with this id.
# Optional: add a list of keys and values to filter the list of machines with the given definition id
# \param definition_id \type{str} definition id that needs to look for
# \param metadata_filter \type{dict} list of metadata keys and values used for filtering
@staticmethod @staticmethod
def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
"""Given a definition id, return the machine with this id.
Optional: add a list of keys and values to filter the list of machines with the given definition id
:param definition_id: :type{str} definition id that needs to look for
:param metadata_filter: :type{dict} list of metadata keys and values used for filtering
"""
if metadata_filter is None: if metadata_filter is None:
metadata_filter = {} metadata_filter = {}
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
@ -397,9 +401,10 @@ class MachineManager(QObject):
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
return False return False
## Check if the global_container has instances in the user container
@pyqtProperty(bool, notify = activeStackValueChanged) @pyqtProperty(bool, notify = activeStackValueChanged)
def hasUserSettings(self) -> bool: def hasUserSettings(self) -> bool:
"""Check if the global_container has instances in the user container"""
if not self._global_container_stack: if not self._global_container_stack:
return False return False
@ -422,10 +427,12 @@ class MachineManager(QObject):
num_user_settings += stack.getTop().getNumInstances() num_user_settings += stack.getTop().getNumInstances()
return num_user_settings return num_user_settings
## Delete a user setting from the global stack and all extruder stacks.
# \param key \type{str} the name of the key to delete
@pyqtSlot(str) @pyqtSlot(str)
def clearUserSettingAllCurrentStacks(self, key: str) -> None: def clearUserSettingAllCurrentStacks(self, key: str) -> None:
"""Delete a user setting from the global stack and all extruder stacks.
:param key: :type{str} the name of the key to delete
"""
Logger.log("i", "Clearing the setting [%s] from all stacks", key) Logger.log("i", "Clearing the setting [%s] from all stacks", key)
if not self._global_container_stack: if not self._global_container_stack:
return return
@ -454,11 +461,13 @@ class MachineManager(QObject):
for container in send_emits_containers: for container in send_emits_containers:
container.sendPostponedEmits() container.sendPostponedEmits()
## Check if none of the stacks contain error states
# Note that the _stacks_have_errors is cached due to performance issues
# Calling _checkStack(s)ForErrors on every change is simply too expensive
@pyqtProperty(bool, notify = stacksValidationChanged) @pyqtProperty(bool, notify = stacksValidationChanged)
def stacksHaveErrors(self) -> bool: def stacksHaveErrors(self) -> bool:
"""Check if none of the stacks contain error states
Note that the _stacks_have_errors is cached due to performance issues
Calling _checkStack(s)ForErrors on every change is simply too expensive
"""
return bool(self._stacks_have_errors) return bool(self._stacks_have_errors)
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
@ -528,14 +537,16 @@ class MachineManager(QObject):
return material.getId() return material.getId()
return "" return ""
## Gets the layer height of the currently active quality profile.
#
# This is indicated together with the name of the active quality profile.
#
# \return The layer height of the currently active quality profile. If
# there is no quality profile, this returns the default layer height.
@pyqtProperty(float, notify = activeQualityGroupChanged) @pyqtProperty(float, notify = activeQualityGroupChanged)
def activeQualityLayerHeight(self) -> float: def activeQualityLayerHeight(self) -> float:
"""Gets the layer height of the currently active quality profile.
This is indicated together with the name of the active quality profile.
:return: The layer height of the currently active quality profile. If
there is no quality profile, this returns the default layer height.
"""
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId()) value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId())
@ -605,13 +616,15 @@ class MachineManager(QObject):
return result return result
## Returns whether there is anything unsupported in the current set-up.
#
# The current set-up signifies the global stack and all extruder stacks,
# so this indicates whether there is any container in any of the container
# stacks that is not marked as supported.
@pyqtProperty(bool, notify = activeQualityChanged) @pyqtProperty(bool, notify = activeQualityChanged)
def isCurrentSetupSupported(self) -> bool: def isCurrentSetupSupported(self) -> bool:
"""Returns whether there is anything unsupported in the current set-up.
The current set-up signifies the global stack and all extruder stacks,
so this indicates whether there is any container in any of the container
stacks that is not marked as supported.
"""
if not self._global_container_stack: if not self._global_container_stack:
return False return False
for stack in [self._global_container_stack] + self._global_container_stack.extruderList: for stack in [self._global_container_stack] + self._global_container_stack.extruderList:
@ -622,9 +635,10 @@ class MachineManager(QObject):
return False return False
return True return True
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
@pyqtSlot(str) @pyqtSlot(str)
def copyValueToExtruders(self, key: str) -> None: def copyValueToExtruders(self, key: str) -> None:
"""Copy the value of the setting of the current extruder to all other extruders as well as the global container."""
if self._active_container_stack is None or self._global_container_stack is None: if self._active_container_stack is None or self._global_container_stack is None:
return return
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
@ -634,9 +648,10 @@ class MachineManager(QObject):
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
## Copy the value of all manually changed settings of the current extruder to all other extruders.
@pyqtSlot() @pyqtSlot()
def copyAllValuesToExtruders(self) -> None: def copyAllValuesToExtruders(self) -> None:
"""Copy the value of all manually changed settings of the current extruder to all other extruders."""
if self._active_container_stack is None or self._global_container_stack is None: if self._active_container_stack is None or self._global_container_stack is None:
return return
@ -648,19 +663,23 @@ class MachineManager(QObject):
# Check if the value has to be replaced # Check if the value has to be replaced
extruder_stack.userChanges.setProperty(key, "value", new_value) extruder_stack.userChanges.setProperty(key, "value", new_value)
## Get the Definition ID to use to select quality profiles for the currently active machine
# \returns DefinitionID (string) if found, empty string otherwise
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
def activeQualityDefinitionId(self) -> str: def activeQualityDefinitionId(self) -> str:
"""Get the Definition ID to use to select quality profiles for the currently active machine
:returns: DefinitionID (string) if found, empty string otherwise
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
return "" return ""
return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
## Gets how the active definition calls variants
# Caveat: per-definition-variant-title is currently not translated (though the fallback is)
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
def activeDefinitionVariantsName(self) -> str: def activeDefinitionVariantsName(self) -> str:
"""Gets how the active definition calls variants
Caveat: per-definition-variant-title is currently not translated (though the fallback is)
"""
fallback_title = catalog.i18nc("@label", "Nozzle") fallback_title = catalog.i18nc("@label", "Nozzle")
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title) return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title)
@ -708,9 +727,10 @@ class MachineManager(QObject):
# This reuses the method and remove all printers recursively # This reuses the method and remove all printers recursively
self.removeMachine(hidden_containers[0].getId()) self.removeMachine(hidden_containers[0].getId())
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
@pyqtProperty(bool, notify = activeMaterialChanged) @pyqtProperty(bool, notify = activeMaterialChanged)
def variantBuildplateCompatible(self) -> bool: def variantBuildplateCompatible(self) -> bool:
"""The selected buildplate is compatible if it is compatible with all the materials in all the extruders"""
if not self._global_container_stack: if not self._global_container_stack:
return True return True
@ -727,10 +747,12 @@ class MachineManager(QObject):
return buildplate_compatible return buildplate_compatible
## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible
# for the other material but the buildplate is still usable
@pyqtProperty(bool, notify = activeMaterialChanged) @pyqtProperty(bool, notify = activeMaterialChanged)
def variantBuildplateUsable(self) -> bool: def variantBuildplateUsable(self) -> bool:
"""The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible
for the other material but the buildplate is still usable
"""
if not self._global_container_stack: if not self._global_container_stack:
return True return True
@ -751,11 +773,13 @@ class MachineManager(QObject):
return result return result
## Get the Definition ID of a machine (specified by ID)
# \param machine_id string machine id to get the definition ID of
# \returns DefinitionID if found, None otherwise
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]:
"""Get the Definition ID of a machine (specified by ID)
:param machine_id: string machine id to get the definition ID of
:returns: DefinitionID if found, None otherwise
"""
containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id)
if containers: if containers:
return containers[0].definition.getId() return containers[0].definition.getId()
@ -786,8 +810,9 @@ class MachineManager(QObject):
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
return result return result
## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed
def correctExtruderSettings(self) -> None: def correctExtruderSettings(self) -> None:
"""Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed"""
if self._global_container_stack is None: if self._global_container_stack is None:
return return
for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges):
@ -803,9 +828,11 @@ class MachineManager(QObject):
title = catalog.i18nc("@info:title", "Settings updated")) title = catalog.i18nc("@info:title", "Settings updated"))
caution_message.show() caution_message.show()
## Set the amount of extruders on the active machine (global stack)
# \param extruder_count int the number of extruders to set
def setActiveMachineExtruderCount(self, extruder_count: int) -> None: def setActiveMachineExtruderCount(self, extruder_count: int) -> None:
"""Set the amount of extruders on the active machine (global stack)
:param extruder_count: int the number of extruders to set
"""
if self._global_container_stack is None: if self._global_container_stack is None:
return return
extruder_manager = self._application.getExtruderManager() extruder_manager = self._application.getExtruderManager()
@ -902,9 +929,10 @@ class MachineManager(QObject):
def defaultExtruderPosition(self) -> str: def defaultExtruderPosition(self) -> str:
return self._default_extruder_position return self._default_extruder_position
## This will fire the propertiesChanged for all settings so they will be updated in the front-end
@pyqtSlot() @pyqtSlot()
def forceUpdateAllSettings(self) -> None: def forceUpdateAllSettings(self) -> None:
"""This will fire the propertiesChanged for all settings so they will be updated in the front-end"""
if self._global_container_stack is None: if self._global_container_stack is None:
return return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
@ -945,11 +973,13 @@ class MachineManager(QObject):
def _onMaterialNameChanged(self) -> None: def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit() self.activeMaterialChanged.emit()
## Get the signals that signal that the containers changed for all stacks.
#
# This includes the global stack and all extruder stacks. So if any
# container changed anywhere.
def _getContainerChangedSignals(self) -> List[Signal]: def _getContainerChangedSignals(self) -> List[Signal]:
"""Get the signals that signal that the containers changed for all stacks.
This includes the global stack and all extruder stacks. So if any
container changed anywhere.
"""
if self._global_container_stack is None: if self._global_container_stack is None:
return [] return []
return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]] return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]]
@ -962,18 +992,21 @@ class MachineManager(QObject):
container = extruder.userChanges container = extruder.userChanges
container.setProperty(setting_name, property_name, property_value) container.setProperty(setting_name, property_name, property_value)
## Reset all setting properties of a setting for all extruders.
# \param setting_name The ID of the setting to reset.
@pyqtSlot(str) @pyqtSlot(str)
def resetSettingForAllExtruders(self, setting_name: str) -> None: def resetSettingForAllExtruders(self, setting_name: str) -> None:
"""Reset all setting properties of a setting for all extruders.
:param setting_name: The ID of the setting to reset.
"""
if self._global_container_stack is None: if self._global_container_stack is None:
return return
for extruder in self._global_container_stack.extruderList: for extruder in self._global_container_stack.extruderList:
container = extruder.userChanges container = extruder.userChanges
container.removeInstance(setting_name) container.removeInstance(setting_name)
## Update _current_root_material_id when the current root material was changed.
def _onRootMaterialChanged(self) -> None: def _onRootMaterialChanged(self) -> None:
"""Update _current_root_material_id when the current root material was changed."""
self._current_root_material_id = {} self._current_root_material_id = {}
changed = False changed = False
@ -1135,8 +1168,9 @@ class MachineManager(QObject):
return False return False
return True return True
## Update current quality type and machine after setting material
def _updateQualityWithMaterial(self, *args: Any) -> None: def _updateQualityWithMaterial(self, *args: Any) -> None:
"""Update current quality type and machine after setting material"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return return
@ -1177,8 +1211,9 @@ class MachineManager(QObject):
current_quality_type, quality_type) current_quality_type, quality_type)
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
## Update the current intent after the quality changed
def _updateIntentWithQuality(self): def _updateIntentWithQuality(self):
"""Update the current intent after the quality changed"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return return
@ -1205,12 +1240,14 @@ class MachineManager(QObject):
category = current_category category = current_category
self.setIntentByCategory(category) self.setIntentByCategory(category)
## Update the material profile in the current stacks when the variant is
# changed.
# \param position The extruder stack to update. If provided with None, all
# extruder stacks will be updated.
@pyqtSlot() @pyqtSlot()
def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: def updateMaterialWithVariant(self, position: Optional[str] = None) -> None:
"""Update the material profile in the current stacks when the variant is
changed.
:param position: The extruder stack to update. If provided with None, all
extruder stacks will be updated.
"""
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if position is None: if position is None:
@ -1245,10 +1282,12 @@ class MachineManager(QObject):
material_node = nozzle_node.preferredMaterial(approximate_material_diameter) material_node = nozzle_node.preferredMaterial(approximate_material_diameter)
self._setMaterial(position_item, material_node) self._setMaterial(position_item, material_node)
## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
# instance with the same network key.
@pyqtSlot(str) @pyqtSlot(str)
def switchPrinterType(self, machine_name: str) -> None: def switchPrinterType(self, machine_name: str) -> None:
"""Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
instance with the same network key.
"""
# Don't switch if the user tries to change to the same type of printer # Don't switch if the user tries to change to the same type of printer
if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name: if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name:
return return
@ -1400,10 +1439,12 @@ class MachineManager(QObject):
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id] material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
self.setMaterial(position, material_node) self.setMaterial(position, material_node)
## Global_stack: if you want to provide your own global_stack instead of the current active one
# if you update an active machine, special measures have to be taken.
@pyqtSlot(str, "QVariant") @pyqtSlot(str, "QVariant")
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
"""Global_stack: if you want to provide your own global_stack instead of the current active one
if you update an active machine, special measures have to be taken.
"""
if global_stack is not None and global_stack != self._global_container_stack: if global_stack is not None and global_stack != self._global_container_stack:
global_stack.extruders[position].material = container_node.container global_stack.extruders[position].material = container_node.container
return return
@ -1449,10 +1490,12 @@ class MachineManager(QObject):
# Get all the quality groups for this global stack and filter out by quality_type # Get all the quality groups for this global stack and filter out by quality_type
self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type]) self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type])
## Optionally provide global_stack if you want to use your own
# The active global_stack is treated differently.
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None:
"""Optionally provide global_stack if you want to use your own
The active global_stack is treated differently.
"""
if global_stack is not None and global_stack != self._global_container_stack: if global_stack is not None and global_stack != self._global_container_stack:
if quality_group is None: if quality_group is None:
Logger.log("e", "Could not set quality group because quality group is None") Logger.log("e", "Could not set quality group because quality group is None")
@ -1514,15 +1557,17 @@ class MachineManager(QObject):
return {"main": main_part, return {"main": main_part,
"suffix": suffix_part} "suffix": suffix_part}
## Change the intent category of the current printer.
#
# All extruders can change their profiles. If an intent profile is
# available with the desired intent category, that one will get chosen.
# Otherwise the intent profile will be left to the empty profile, which
# represents the "default" intent category.
# \param intent_category The intent category to change to.
@pyqtSlot(str) @pyqtSlot(str)
def setIntentByCategory(self, intent_category: str) -> None: def setIntentByCategory(self, intent_category: str) -> None:
"""Change the intent category of the current printer.
All extruders can change their profiles. If an intent profile is
available with the desired intent category, that one will get chosen.
Otherwise the intent profile will be left to the empty profile, which
represents the "default" intent category.
:param intent_category: The intent category to change to.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return return
@ -1554,21 +1599,25 @@ class MachineManager(QObject):
else: # No intent had the correct category. else: # No intent had the correct category.
extruder.intent = empty_intent_container extruder.intent = empty_intent_container
## Get the currently activated quality group.
#
# If no printer is added yet or the printer doesn't have quality profiles,
# this returns ``None``.
# \return The currently active quality group.
def activeQualityGroup(self) -> Optional["QualityGroup"]: def activeQualityGroup(self) -> Optional["QualityGroup"]:
"""Get the currently activated quality group.
If no printer is added yet or the printer doesn't have quality profiles,
this returns ``None``.
:return: The currently active quality group.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack or global_stack.quality == empty_quality_container: if not global_stack or global_stack.quality == empty_quality_container:
return None return None
return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType) return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType)
## Get the name of the active quality group.
# \return The name of the active quality group.
@pyqtProperty(str, notify = activeQualityGroupChanged) @pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityGroupName(self) -> str: def activeQualityGroupName(self) -> str:
"""Get the name of the active quality group.
:return: The name of the active quality group.
"""
quality_group = self.activeQualityGroup() quality_group = self.activeQualityGroup()
if quality_group is None: if quality_group is None:
return "" return ""
@ -1641,9 +1690,10 @@ class MachineManager(QObject):
self.updateMaterialWithVariant(None) self.updateMaterialWithVariant(None)
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
## This function will translate any printer type name to an abbreviated printer type name
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getAbbreviatedMachineName(self, machine_type_name: str) -> str: def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
"""This function will translate any printer type name to an abbreviated printer type name"""
abbr_machine = "" abbr_machine = ""
for word in re.findall(r"[\w']+", machine_type_name): for word in re.findall(r"[\w']+", machine_type_name):
if word.lower() == "ultimaker": if word.lower() == "ultimaker":

View File

@ -10,10 +10,13 @@ from UM.Resources import Resources
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
## Are machine names valid?
#
# Performs checks based on the length of the name.
class MachineNameValidator(QObject): class MachineNameValidator(QObject):
"""Are machine names valid?
Performs checks based on the length of the name.
"""
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
@ -32,12 +35,13 @@ class MachineNameValidator(QObject):
validationChanged = pyqtSignal() validationChanged = pyqtSignal()
## Check if a specified machine name is allowed.
#
# \param name The machine name to check.
# \return ``QValidator.Invalid`` if it's disallowed, or
# ``QValidator.Acceptable`` if it's allowed.
def validate(self, name): def validate(self, name):
"""Check if a specified machine name is allowed.
:param name: The machine name to check.
:return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed.
"""
#Check for file name length of the current settings container (which is the longest file we're saving with the name). #Check for file name length of the current settings container (which is the longest file we're saving with the name).
try: try:
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
@ -50,9 +54,10 @@ class MachineNameValidator(QObject):
return QValidator.Acceptable #All checks succeeded. return QValidator.Acceptable #All checks succeeded.
## Updates the validation state of a machine name text field.
@pyqtSlot(str) @pyqtSlot(str)
def updateValidation(self, new_name): def updateValidation(self, new_name):
"""Updates the validation state of a machine name text field."""
is_valid = self.validate(new_name) is_valid = self.validate(new_name)
if is_valid == QValidator.Acceptable: if is_valid == QValidator.Acceptable:
self.validation_regex = "^.*$" #Matches anything. self.validation_regex = "^.*$" #Matches anything.

View File

@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the extruder a certain object should be printed with.
class SetObjectExtruderOperation(Operation): class SetObjectExtruderOperation(Operation):
"""Simple operation to set the extruder a certain object should be printed with."""
def __init__(self, node: SceneNode, extruder_id: str) -> None: def __init__(self, node: SceneNode, extruder_id: str) -> None:
self._node = node self._node = node
self._extruder_id = extruder_id self._extruder_id = extruder_id

View File

@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject):
settingsWithIntheritanceChanged = pyqtSignal() settingsWithIntheritanceChanged = pyqtSignal()
## Get the keys of all children settings with an override.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getChildrenKeysWithOverride(self, key: str) -> List[str]: def getChildrenKeysWithOverride(self, key: str) -> List[str]:
"""Get the keys of all children settings with an override."""
if self._global_container_stack is None: if self._global_container_stack is None:
return [] return []
definitions = self._global_container_stack.definition.findDefinitions(key=key) definitions = self._global_container_stack.definition.findDefinitions(key=key)
@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject):
def settingsWithInheritanceWarning(self) -> List[str]: def settingsWithInheritanceWarning(self) -> List[str]:
return self._settings_with_inheritance_warning return self._settings_with_inheritance_warning
## Check if a setting has an inheritance function that is overwritten
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
"""Check if a setting has an inheritance function that is overwritten"""
has_setting_function = False has_setting_function = False
if not stack: if not stack:
stack = self._active_container_stack stack = self._active_container_stack
@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject):
containers = [] # type: List[ContainerInterface] containers = [] # type: List[ContainerInterface]
## Check if the setting has a user state. If not, it is never overwritten.
has_user_state = stack.getProperty(key, "state") == InstanceState.User has_user_state = stack.getProperty(key, "state") == InstanceState.User
"""Check if the setting has a user state. If not, it is never overwritten."""
if not has_user_state: if not has_user_state:
return False return False
## If a setting is not enabled, don't label it as overwritten (It's never visible anyway). # If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
if not stack.getProperty(key, "enabled"): if not stack.getProperty(key, "enabled"):
return False return False
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
user_container = stack.getTop() user_container = stack.getTop()
"""Also check if the top container is not a setting function (this happens if the inheritance is restored)."""
if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction): if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction):
return False return False

View File

@ -15,21 +15,24 @@ from UM.Application import Application
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
# this stack still resolve.
@signalemitter @signalemitter
class SettingOverrideDecorator(SceneNodeDecorator): class SettingOverrideDecorator(SceneNodeDecorator):
## Event indicating that the user selected a different extruder. """A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
activeExtruderChanged = Signal()
the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
this stack still resolve.
"""
activeExtruderChanged = Signal()
"""Event indicating that the user selected a different extruder."""
## Non-printing meshes
#
# If these settings are True for any mesh, the mesh does not need a convex hull,
# and is sent to the slicer regardless of whether it fits inside the build volume.
# Note that Support Mesh is not in here because it actually generates
# g-code in the volume of the mesh.
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
"""Non-printing meshes
If these settings are True for any mesh, the mesh does not need a convex hull,
and is sent to the slicer regardless of whether it fits inside the build volume.
Note that Support Mesh is not in here because it actually generates
g-code in the volume of the mesh.
"""
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
def __init__(self): def __init__(self):
@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() return "SettingOverrideInstanceContainer-%s" % uuid.uuid1()
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
## Create a fresh decorator object
deep_copy = SettingOverrideDecorator() deep_copy = SettingOverrideDecorator()
"""Create a fresh decorator object"""
## Copy the instance
instance_container = copy.deepcopy(self._stack.getContainer(0), memo) instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
"""Copy the instance"""
# A unique name must be added, or replaceContainer will not replace it # A unique name must be added, or replaceContainer will not replace it
instance_container.setMetaDataEntry("id", self._generateUniqueName()) instance_container.setMetaDataEntry("id", self._generateUniqueName())
@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator):
return deep_copy return deep_copy
## Gets the currently active extruder to print this object with.
#
# \return An extruder's container stack.
def getActiveExtruder(self): def getActiveExtruder(self):
"""Gets the currently active extruder to print this object with.
:return: An extruder's container stack.
"""
return self._extruder_stack return self._extruder_stack
## Gets the signal that emits if the active extruder changed.
#
# This can then be accessed via a decorator.
def getActiveExtruderChangedSignal(self): def getActiveExtruderChangedSignal(self):
"""Gets the signal that emits if the active extruder changed.
This can then be accessed via a decorator.
"""
return self.activeExtruderChanged return self.activeExtruderChanged
## Gets the currently active extruders position
#
# \return An extruder's position, or None if no position info is available.
def getActiveExtruderPosition(self): def getActiveExtruderPosition(self):
"""Gets the currently active extruders position
:return: An extruder's position, or None if no position info is available.
"""
# for support_meshes, always use the support_extruder # for support_meshes, always use the support_extruder
if self.getStack().getProperty("support_mesh", "value"): if self.getStack().getProperty("support_mesh", "value"):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle() Application.getInstance().getBackend().tickle()
## Makes sure that the stack upon which the container stack is placed is
# kept up to date.
def _updateNextStack(self): def _updateNextStack(self):
"""Makes sure that the stack upon which the container stack is placed is
kept up to date.
"""
if self._extruder_stack: if self._extruder_stack:
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
if extruder_stack: if extruder_stack:
@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
else: else:
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
## Changes the extruder with which to print this node.
#
# \param extruder_stack_id The new extruder stack to print with.
def setActiveExtruder(self, extruder_stack_id): def setActiveExtruder(self, extruder_stack_id):
"""Changes the extruder with which to print this node.
:param extruder_stack_id: The new extruder stack to print with.
"""
self._extruder_stack = extruder_stack_id self._extruder_stack = extruder_stack_id
self._updateNextStack() self._updateNextStack()
ExtruderManager.getInstance().resetSelectedObjectExtruders() ExtruderManager.getInstance().resetSelectedObjectExtruders()

View File

@ -15,13 +15,15 @@ if TYPE_CHECKING:
from cura.MachineAction import MachineAction from cura.MachineAction import MachineAction
## Raised when trying to add an unknown machine action as a required action
class UnknownMachineActionError(Exception): class UnknownMachineActionError(Exception):
"""Raised when trying to add an unknown machine action as a required action"""
pass pass
## Raised when trying to add a machine action that does not have an unique key.
class NotUniqueMachineActionError(Exception): class NotUniqueMachineActionError(Exception):
"""Raised when trying to add a machine action that does not have an unique key."""
pass pass
@ -71,9 +73,11 @@ class MachineActionManager(QObject):
self._definition_ids_with_default_actions_added.add(definition_id) self._definition_ids_with_default_actions_added.add(definition_id)
Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id) Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
## Add a required action to a machine
# Raises an exception when the action is not recognised.
def addRequiredAction(self, definition_id: str, action_key: str) -> None: def addRequiredAction(self, definition_id: str, action_key: str) -> None:
"""Add a required action to a machine
Raises an exception when the action is not recognised.
"""
if action_key in self._machine_actions: if action_key in self._machine_actions:
if definition_id in self._required_actions: if definition_id in self._required_actions:
if self._machine_actions[action_key] not in self._required_actions[definition_id]: if self._machine_actions[action_key] not in self._required_actions[definition_id]:
@ -83,8 +87,9 @@ class MachineActionManager(QObject):
else: else:
raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id)) raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
## Add a supported action to a machine.
def addSupportedAction(self, definition_id: str, action_key: str) -> None: def addSupportedAction(self, definition_id: str, action_key: str) -> None:
"""Add a supported action to a machine."""
if action_key in self._machine_actions: if action_key in self._machine_actions:
if definition_id in self._supported_actions: if definition_id in self._supported_actions:
if self._machine_actions[action_key] not in self._supported_actions[definition_id]: if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
@ -94,8 +99,9 @@ class MachineActionManager(QObject):
else: else:
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
## Add an action to the first start list of a machine.
def addFirstStartAction(self, definition_id: str, action_key: str) -> None: def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
"""Add an action to the first start list of a machine."""
if action_key in self._machine_actions: if action_key in self._machine_actions:
if definition_id in self._first_start_actions: if definition_id in self._first_start_actions:
self._first_start_actions[definition_id].append(self._machine_actions[action_key]) self._first_start_actions[definition_id].append(self._machine_actions[action_key])
@ -104,57 +110,69 @@ class MachineActionManager(QObject):
else: else:
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
## Add a (unique) MachineAction
# if the Key of the action is not unique, an exception is raised.
def addMachineAction(self, action: "MachineAction") -> None: def addMachineAction(self, action: "MachineAction") -> None:
"""Add a (unique) MachineAction
if the Key of the action is not unique, an exception is raised.
"""
if action.getKey() not in self._machine_actions: if action.getKey() not in self._machine_actions:
self._machine_actions[action.getKey()] = action self._machine_actions[action.getKey()] = action
else: else:
raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey()) raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey())
## Get all actions supported by given machine
# \param definition_id The ID of the definition you want the supported actions of
# \returns set of supported actions.
@pyqtSlot(str, result = "QVariantList") @pyqtSlot(str, result = "QVariantList")
def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
"""Get all actions supported by given machine
:param definition_id: The ID of the definition you want the supported actions of
:returns: set of supported actions.
"""
if definition_id in self._supported_actions: if definition_id in self._supported_actions:
return list(self._supported_actions[definition_id]) return list(self._supported_actions[definition_id])
else: else:
return list() return list()
## Get all actions required by given machine
# \param definition_id The ID of the definition you want the required actions of
# \returns set of required actions.
def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
"""Get all actions required by given machine
:param definition_id: The ID of the definition you want the required actions of
:returns: set of required actions.
"""
if definition_id in self._required_actions: if definition_id in self._required_actions:
return self._required_actions[definition_id] return self._required_actions[definition_id]
else: else:
return list() return list()
## Get all actions that need to be performed upon first start of a given machine.
# Note that contrary to required / supported actions a list is returned (as it could be required to run the same
# action multiple times).
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
# \returns List of actions.
@pyqtSlot(str, result = "QVariantList") @pyqtSlot(str, result = "QVariantList")
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
"""Get all actions that need to be performed upon first start of a given machine.
Note that contrary to required / supported actions a list is returned (as it could be required to run the same
action multiple times).
:param definition_id: The ID of the definition that you want to get the "on added" actions for.
:returns: List of actions.
"""
if definition_id in self._first_start_actions: if definition_id in self._first_start_actions:
return self._first_start_actions[definition_id] return self._first_start_actions[definition_id]
else: else:
return [] return []
## Remove Machine action from manager
# \param action to remove
def removeMachineAction(self, action: "MachineAction") -> None: def removeMachineAction(self, action: "MachineAction") -> None:
"""Remove Machine action from manager
:param action: to remove
"""
try: try:
del self._machine_actions[action.getKey()] del self._machine_actions[action.getKey()]
except KeyError: except KeyError:
Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey()) Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey())
## Get MachineAction by key
# \param key String of key to select
# \return Machine action if found, None otherwise
def getMachineAction(self, key: str) -> Optional["MachineAction"]: def getMachineAction(self, key: str) -> Optional["MachineAction"]:
"""Get MachineAction by key
:param key: String of key to select
:return: Machine action if found, None otherwise
"""
if key in self._machine_actions: if key in self._machine_actions:
return self._machine_actions[key] return self._machine_actions[key]
else: else:

View File

@ -31,8 +31,9 @@ class _NodeInfo:
self.is_group = is_group # type: bool self.is_group = is_group # type: bool
## Keep track of all objects in the project
class ObjectsModel(ListModel): class ObjectsModel(ListModel):
"""Keep track of all objects in the project"""
NameRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 1
SelectedRole = Qt.UserRole + 2 SelectedRole = Qt.UserRole + 2
OutsideAreaRole = Qt.UserRole + 3 OutsideAreaRole = Qt.UserRole + 3

View File

@ -21,11 +21,13 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## A class for processing and the print times per build plate as well as managing the job name
#
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
# This job name is requested by the JobSpecs qml file.
class PrintInformation(QObject): class PrintInformation(QObject):
"""A class for processing and the print times per build plate as well as managing the job name
This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
This job name is requested by the JobSpecs qml file.
"""
UNTITLED_JOB_NAME = "Untitled" UNTITLED_JOB_NAME = "Untitled"
@ -380,10 +382,12 @@ class PrintInformation(QObject):
def baseName(self): def baseName(self):
return self._base_name return self._base_name
## Created an acronym-like abbreviated machine name from the currently
# active machine name.
# Called each time the global stack is switched.
def _defineAbbreviatedMachineName(self) -> None: def _defineAbbreviatedMachineName(self) -> None:
"""Created an acronym-like abbreviated machine name from the currently active machine name.
Called each time the global stack is switched.
"""
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack: if not global_container_stack:
self._abbr_machine = "" self._abbr_machine = ""
@ -392,8 +396,9 @@ class PrintInformation(QObject):
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
## Utility method that strips accents from characters (eg: â -> a)
def _stripAccents(self, to_strip: str) -> str: def _stripAccents(self, to_strip: str) -> str:
"""Utility method that strips accents from characters (eg: â -> a)"""
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
@pyqtSlot(result = "QVariantMap") @pyqtSlot(result = "QVariantMap")
@ -431,6 +436,7 @@ class PrintInformation(QObject):
return return
self._change_timer.start() self._change_timer.start()
## Listen to scene changes to check if we need to reset the print information
def _onSceneChanged(self) -> None: def _onSceneChanged(self) -> None:
"""Listen to scene changes to check if we need to reset the print information"""
self.setToZeroPrintInformation(self._active_build_plate) self.setToZeroPrintInformation(self._active_build_plate)

View File

@ -11,13 +11,15 @@ from typing import Callable
SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$") SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$")
## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported
# APIs, meaning that those APIs should be versioned and maintained.
#
# \param since_version The earliest version since when this API becomes supported. This means that since this version,
# this API function is supposed to behave the same. This parameter is not used. It's just a
# documentation.
def api(since_version: str) -> Callable: def api(since_version: str) -> Callable:
"""Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported
APIs, meaning that those APIs should be versioned and maintained.
:param since_version: The earliest version since when this API becomes supported. This means that since this version,
this API function is supposed to behave the same. This parameter is not used. It's just a
documentation.
"""
# Make sure that APi versions are semantic versions # Make sure that APi versions are semantic versions
if not SEMANTIC_VERSION_REGEX.fullmatch(since_version): if not SEMANTIC_VERSION_REGEX.fullmatch(since_version):
raise ValueError("API since_version [%s] is not a semantic version." % since_version) raise ValueError("API since_version [%s] is not a semantic version." % since_version)

View File

@ -26,12 +26,14 @@ def container_registry():
result.findContainersMetadata = MagicMock(return_value = [metadata_dict]) result.findContainersMetadata = MagicMock(return_value = [metadata_dict])
return result return result
## Creates a machine node without anything underneath it. No sub-nodes.
#
# For testing stuff with machine nodes without testing _loadAll(). You'll need
# to add subnodes manually in your test.
@pytest.fixture @pytest.fixture
def empty_machine_node(): def empty_machine_node():
"""Creates a machine node without anything underneath it. No sub-nodes.
For testing stuff with machine nodes without testing _loadAll(). You'll need
to add subnodes manually in your test.
"""
empty_container_registry = MagicMock() empty_container_registry = MagicMock()
empty_container_registry.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor. empty_container_registry.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor.
empty_container_registry.findInstanceContainersMetadata = MagicMock(return_value = []) empty_container_registry.findInstanceContainersMetadata = MagicMock(return_value = [])
@ -77,9 +79,13 @@ def test_metadataProperties(container_registry):
assert node.preferred_material == metadata_dict["preferred_material"] assert node.preferred_material == metadata_dict["preferred_material"]
assert node.preferred_quality_type == metadata_dict["preferred_quality_type"] assert node.preferred_quality_type == metadata_dict["preferred_quality_type"]
## Test getting quality groups when there are quality profiles available for
# the requested configurations on two extruders.
def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node):
"""Test getting quality groups when there are quality profiles available for
the requested configurations on two extruders.
"""
# Prepare a tree inside the machine node. # Prepare a tree inside the machine node.
extruder_0_node = MagicMock(quality_type = "quality_type_1") extruder_0_node = MagicMock(quality_type = "quality_type_1")
extruder_1_node = MagicMock(quality_type = "quality_type_1") # Same quality type, so this is the one that can be returned. extruder_1_node = MagicMock(quality_type = "quality_type_1") # Same quality type, so this is the one that can be returned.
@ -121,12 +127,15 @@ def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node):
assert result["quality_type_1"].name == global_node.getMetaDataEntry("name", "Unnamed Profile") assert result["quality_type_1"].name == global_node.getMetaDataEntry("name", "Unnamed Profile")
assert result["quality_type_1"].quality_type == "quality_type_1" assert result["quality_type_1"].quality_type == "quality_type_1"
## Test the "is_available" flag on quality groups.
#
# If a profile is available for a quality type on an extruder but not on all
# extruders, there should be a quality group for it but it should not be made
# available.
def test_getQualityGroupsAvailability(empty_machine_node): def test_getQualityGroupsAvailability(empty_machine_node):
"""Test the "is_available" flag on quality groups.
If a profile is available for a quality type on an extruder but not on all
extruders, there should be a quality group for it but it should not be made
available.
"""
# Prepare a tree inside the machine node. # Prepare a tree inside the machine node.
extruder_0_both = MagicMock(quality_type = "quality_type_both") # This quality type is available for both extruders. extruder_0_both = MagicMock(quality_type = "quality_type_both") # This quality type is available for both extruders.
extruder_1_both = MagicMock(quality_type = "quality_type_both") extruder_1_both = MagicMock(quality_type = "quality_type_both")

View File

@ -6,7 +6,7 @@ import pytest
from cura.Machines.QualityNode import QualityNode from cura.Machines.QualityNode import QualityNode
## Metadata for hypothetical containers that get put in the registry. # Metadata for hypothetical containers that get put in the registry.
metadatas = [ metadatas = [
{ {
"id": "matching_intent", # Matches our query. "id": "matching_intent", # Matches our query.

View File

@ -49,12 +49,14 @@ def machine_node():
mocked_machine_node.preferred_material = "preferred_material" mocked_machine_node.preferred_material = "preferred_material"
return mocked_machine_node return mocked_machine_node
## Constructs a variant node without any subnodes.
#
# This is useful for performing tests on VariantNode without being dependent
# on how _loadAll works.
@pytest.fixture @pytest.fixture
def empty_variant_node(machine_node): def empty_variant_node(machine_node):
"""Constructs a variant node without any subnodes.
This is useful for performing tests on VariantNode without being dependent
on how _loadAll works.
"""
container_registry = MagicMock( container_registry = MagicMock(
findContainersMetadata = MagicMock(return_value = [{"name": "test variant name"}]) findContainersMetadata = MagicMock(return_value = [{"name": "test variant name"}])
) )
@ -132,9 +134,12 @@ def test_materialAdded_update(container_registry, machine_node, metadata, change
for key in changed_material_list: for key in changed_material_list:
assert original_material_nodes[key] != variant_node.materials[key] assert original_material_nodes[key] != variant_node.materials[key]
## Tests the preferred material when the exact base file is available in the
# materials list for this node.
def test_preferredMaterialExactMatch(empty_variant_node): def test_preferredMaterialExactMatch(empty_variant_node):
"""Tests the preferred material when the exact base file is available in the
materials list for this node.
"""
empty_variant_node.materials = { empty_variant_node.materials = {
"some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3),
"preferred_material": MagicMock(getMetaDataEntry = lambda x: 3) # Exact match. "preferred_material": MagicMock(getMetaDataEntry = lambda x: 3) # Exact match.
@ -143,9 +148,12 @@ def test_preferredMaterialExactMatch(empty_variant_node):
assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material"], "It should match exactly on this one since it's the preferred material." assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material"], "It should match exactly on this one since it's the preferred material."
## Tests the preferred material when a submaterial is available in the
# materials list for this node.
def test_preferredMaterialSubmaterial(empty_variant_node): def test_preferredMaterialSubmaterial(empty_variant_node):
"""Tests the preferred material when a submaterial is available in the
materials list for this node.
"""
empty_variant_node.materials = { empty_variant_node.materials = {
"some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3),
"preferred_material_base_file_aa04": MagicMock(getMetaDataEntry = lambda x: 3) # This is a submaterial of the preferred material. "preferred_material_base_file_aa04": MagicMock(getMetaDataEntry = lambda x: 3) # This is a submaterial of the preferred material.
@ -154,9 +162,10 @@ def test_preferredMaterialSubmaterial(empty_variant_node):
assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_base_file_aa04"], "It should match on the submaterial just as well." assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_base_file_aa04"], "It should match on the submaterial just as well."
## Tests the preferred material matching on the approximate diameter of the
# filament.
def test_preferredMaterialDiameter(empty_variant_node): def test_preferredMaterialDiameter(empty_variant_node):
"""Tests the preferred material matching on the approximate diameter of the filament.
"""
empty_variant_node.materials = { empty_variant_node.materials = {
"some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3),
"preferred_material_wrong_diameter": MagicMock(getMetaDataEntry = lambda x: 2), # Approximate diameter is 2 instead of 3. "preferred_material_wrong_diameter": MagicMock(getMetaDataEntry = lambda x: 2), # Approximate diameter is 2 instead of 3.
@ -166,18 +175,22 @@ def test_preferredMaterialDiameter(empty_variant_node):
assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_correct_diameter"], "It should match only on the material with correct diameter." assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_correct_diameter"], "It should match only on the material with correct diameter."
## Tests the preferred material matching on a different material if the
# diameter is wrong.
def test_preferredMaterialDiameterNoMatch(empty_variant_node): def test_preferredMaterialDiameterNoMatch(empty_variant_node):
"""Tests the preferred material matching on a different material if the diameter is wrong."""
empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials = collections.OrderedDict()
empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 3) # This one first so that it gets iterated over first. empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 3) # This one first so that it gets iterated over first.
empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Wrong diameter. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Wrong diameter.
assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["some_different_material"], "It should match on another material with the correct diameter if the preferred one is unavailable." assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["some_different_material"], "It should match on another material with the correct diameter if the preferred one is unavailable."
## Tests that the material diameter is considered more important to match than
# the preferred diameter.
def test_preferredMaterialDiameterPreference(empty_variant_node): def test_preferredMaterialDiameterPreference(empty_variant_node):
"""Tests that the material diameter is considered more important to match than
the preferred diameter.
"""
empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials = collections.OrderedDict()
empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # This one first so that it gets iterated over first. empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # This one first so that it gets iterated over first.
empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Matches on ID but not diameter. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Matches on ID but not diameter.

View File

@ -5,18 +5,21 @@ import UM.PluginObject
from UM.Signal import Signal from UM.Signal import Signal
## Fake container class to add to the container registry.
#
# This allows us to test the container registry without testing the container
# class. If something is wrong in the container class it won't influence this
# test.
class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): class MockContainer(ContainerInterface, UM.PluginObject.PluginObject):
## Initialise a new definition container. """Fake container class to add to the container registry.
#
# The container will have the specified ID and all metadata in the This allows us to test the container registry without testing the container
# provided dictionary. class. If something is wrong in the container class it won't influence this
test.
"""
def __init__(self, metadata = None): def __init__(self, metadata = None):
"""Initialise a new definition container.
The container will have the specified ID and all metadata in the
provided dictionary.
"""
super().__init__() super().__init__()
if metadata is None: if metadata is None:
self._metadata = {} self._metadata = {}
@ -24,55 +27,69 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject):
self._metadata = metadata self._metadata = metadata
self._plugin_id = "MockContainerPlugin" self._plugin_id = "MockContainerPlugin"
## Gets the ID that was provided at initialisation.
#
# \return The ID of the container.
def getId(self): def getId(self):
"""Gets the ID that was provided at initialisation.
:return: The ID of the container.
"""
return self._metadata["id"] return self._metadata["id"]
## Gets all metadata of this container.
#
# This returns the metadata dictionary that was provided in the
# constructor of this mock container.
#
# \return The metadata for this container.
def getMetaData(self): def getMetaData(self):
"""Gets all metadata of this container.
This returns the metadata dictionary that was provided in the
constructor of this mock container.
:return: The metadata for this container.
"""
return self._metadata return self._metadata
## Gets a metadata entry from the metadata dictionary.
#
# \param key The key of the metadata entry.
# \return The value of the metadata entry, or None if there is no such
# entry.
def getMetaDataEntry(self, entry, default = None): def getMetaDataEntry(self, entry, default = None):
"""Gets a metadata entry from the metadata dictionary.
:param key: The key of the metadata entry.
:return: The value of the metadata entry, or None if there is no such
entry.
"""
if entry in self._metadata: if entry in self._metadata:
return self._metadata[entry] return self._metadata[entry]
return default return default
## Gets a human-readable name for this container.
# \return The name from the metadata, or "MockContainer" if there was no
# name provided.
def getName(self): def getName(self):
"""Gets a human-readable name for this container.
:return: The name from the metadata, or "MockContainer" if there was no
name provided.
"""
return self._metadata.get("name", "MockContainer") return self._metadata.get("name", "MockContainer")
## Get whether a container stack is enabled or not.
# \return Always returns True.
@property @property
def isEnabled(self): def isEnabled(self):
"""Get whether a container stack is enabled or not.
:return: Always returns True.
"""
return True return True
## Get whether the container item is stored on a read only location in the filesystem.
#
# \return Always returns False
def isReadOnly(self): def isReadOnly(self):
"""Get whether the container item is stored on a read only location in the filesystem.
:return: Always returns False
"""
return False return False
## Mock get path
def getPath(self): def getPath(self):
"""Mock get path"""
return "/path/to/the/light/side" return "/path/to/the/light/side"
## Mock set path
def setPath(self, path): def setPath(self, path):
"""Mock set path"""
pass pass
def getAllKeys(self): def getAllKeys(self):
@ -91,31 +108,38 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject):
return None return None
## Get the value of a container item.
#
# Since this mock container cannot contain any items, it always returns
# None.
#
# \return Always returns None.
def getValue(self, key): def getValue(self, key):
"""Get the value of a container item.
Since this mock container cannot contain any items, it always returns None.
:return: Always returns None.
"""
pass pass
## Get whether the container item has a specific property.
#
# This method is not implemented in the mock container.
def hasProperty(self, key, property_name): def hasProperty(self, key, property_name):
"""Get whether the container item has a specific property.
This method is not implemented in the mock container.
"""
return key in self.items return key in self.items
## Serializes the container to a string representation.
#
# This method is not implemented in the mock container.
def serialize(self, ignored_metadata_keys = None): def serialize(self, ignored_metadata_keys = None):
"""Serializes the container to a string representation.
This method is not implemented in the mock container.
"""
raise NotImplementedError() raise NotImplementedError()
## Deserializes the container from a string representation.
#
# This method is not implemented in the mock container.
def deserialize(self, serialized, file_name: Optional[str] = None): def deserialize(self, serialized, file_name: Optional[str] = None):
"""Deserializes the container from a string representation.
This method is not implemented in the mock container.
"""
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod

View File

@ -42,8 +42,9 @@ def test_createUniqueName(container_registry):
assert container_registry.createUniqueName("user", "test", "", "nope") == "nope" assert container_registry.createUniqueName("user", "test", "", "nope") == "nope"
## Tests whether addContainer properly converts to ExtruderStack.
def test_addContainerExtruderStack(container_registry, definition_container, definition_changes_container): def test_addContainerExtruderStack(container_registry, definition_container, definition_changes_container):
"""Tests whether addContainer properly converts to ExtruderStack."""
container_registry.addContainer(definition_container) container_registry.addContainer(definition_container)
container_registry.addContainer(definition_changes_container) container_registry.addContainer(definition_changes_container)
@ -61,8 +62,9 @@ def test_addContainerExtruderStack(container_registry, definition_container, def
assert type(mock_super_add_container.call_args_list[0][0][0]) == ExtruderStack assert type(mock_super_add_container.call_args_list[0][0][0]) == ExtruderStack
## Tests whether addContainer properly converts to GlobalStack.
def test_addContainerGlobalStack(container_registry, definition_container, definition_changes_container): def test_addContainerGlobalStack(container_registry, definition_container, definition_changes_container):
"""Tests whether addContainer properly converts to GlobalStack."""
container_registry.addContainer(definition_container) container_registry.addContainer(definition_container)
container_registry.addContainer(definition_changes_container) container_registry.addContainer(definition_changes_container)

View File

@ -57,9 +57,10 @@ def test_noCategory(file_path):
metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id") metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id")
assert "category" not in metadata[0] assert "category" not in metadata[0]
## Tests all definition containers
@pytest.mark.parametrize("file_path", machine_filepaths) @pytest.mark.parametrize("file_path", machine_filepaths)
def test_validateMachineDefinitionContainer(file_path, definition_container): def test_validateMachineDefinitionContainer(file_path, definition_container):
"""Tests all definition containers"""
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json": if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
return # Stop checking, these are root files. return # Stop checking, these are root files.
@ -85,13 +86,15 @@ def assertIsDefinitionValid(definition_container, file_path):
if "platform_texture" in metadata[0]: if "platform_texture" in metadata[0]:
assert metadata[0]["platform_texture"] in all_images assert metadata[0]["platform_texture"] in all_images
## Tests whether setting values are not being hidden by parent containers.
#
# When a definition container defines a "default_value" but inherits from a
# definition that defines a "value", the "default_value" is ineffective. This
# test fails on those things.
@pytest.mark.parametrize("file_path", definition_filepaths) @pytest.mark.parametrize("file_path", definition_filepaths)
def test_validateOverridingDefaultValue(file_path: str): def test_validateOverridingDefaultValue(file_path: str):
"""Tests whether setting values are not being hidden by parent containers.
When a definition container defines a "default_value" but inherits from a
definition that defines a "value", the "default_value" is ineffective. This
test fails on those things.
"""
with open(file_path, encoding = "utf-8") as f: with open(file_path, encoding = "utf-8") as f:
doc = json.load(f) doc = json.load(f)
@ -107,12 +110,14 @@ def test_validateOverridingDefaultValue(file_path: str):
faulty_keys.add(key) faulty_keys.add(key)
assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective. assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective.
## Get all settings and their properties from a definition we're inheriting
# from.
# \param definition_id The definition we're inheriting from.
# \return A dictionary of settings by key. Each setting is a dictionary of
# properties.
def getInheritedSettings(definition_id: str) -> Dict[str, Any]: def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
"""Get all settings and their properties from a definition we're inheriting from.
:param definition_id: The definition we're inheriting from.
:return: A dictionary of settings by key. Each setting is a dictionary of properties.
"""
definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json") definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json")
with open(definition_path, encoding = "utf-8") as f: with open(definition_path, encoding = "utf-8") as f:
doc = json.load(f) doc = json.load(f)
@ -127,13 +132,15 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
return result return result
## Put all settings in the main dictionary rather than in children dicts.
# \param settings Nested settings. The keys are the setting IDs. The values
# are dictionaries of properties per setting, including the "children"
# property.
# \return A dictionary of settings by key. Each setting is a dictionary of
# properties.
def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
"""Put all settings in the main dictionary rather than in children dicts.
:param settings: Nested settings. The keys are the setting IDs. The values
are dictionaries of properties per setting, including the "children" property.
:return: A dictionary of settings by key. Each setting is a dictionary of properties.
"""
result = {} result = {}
for entry, contents in settings.items(): for entry, contents in settings.items():
if "children" in contents: if "children" in contents:
@ -142,12 +149,16 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
result[entry] = contents result[entry] = contents
return result return result
## Make one dictionary override the other. Nested dictionaries override each
# other in the same way.
# \param base A dictionary of settings that will get overridden by the other.
# \param overrides A dictionary of settings that will override the other.
# \return Combined setting data.
def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]:
"""Make one dictionary override the other. Nested dictionaries override each
other in the same way.
:param base: A dictionary of settings that will get overridden by the other.
:param overrides: A dictionary of settings that will override the other.
:return: Combined setting data.
"""
result = {} result = {}
result.update(base) result.update(base)
for key, val in overrides.items(): for key, val in overrides.items():
@ -161,21 +172,25 @@ def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, An
result[key] = val result[key] = val
return result return result
## Verifies that definition contains don't have an ID field.
#
# ID fields are legacy. They should not be used any more. This is legacy that
# people don't seem to be able to get used to.
@pytest.mark.parametrize("file_path", definition_filepaths) @pytest.mark.parametrize("file_path", definition_filepaths)
def test_noId(file_path: str): def test_noId(file_path: str):
"""Verifies that definition contains don't have an ID field.
ID fields are legacy. They should not be used any more. This is legacy that
people don't seem to be able to get used to.
"""
with open(file_path, encoding = "utf-8") as f: with open(file_path, encoding = "utf-8") as f:
doc = json.load(f) doc = json.load(f)
assert "id" not in doc, "Definitions should not have an ID field." assert "id" not in doc, "Definitions should not have an ID field."
## Verifies that extruders say that they work on the same extruder_nr as what
# is listed in their machine definition.
@pytest.mark.parametrize("file_path", extruder_filepaths) @pytest.mark.parametrize("file_path", extruder_filepaths)
def test_extruderMatch(file_path: str): def test_extruderMatch(file_path: str):
"""Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition."""
extruder_id = os.path.basename(file_path).split(".")[0] extruder_id = os.path.basename(file_path).split(".")[0]
with open(file_path, encoding = "utf-8") as f: with open(file_path, encoding = "utf-8") as f:
doc = json.load(f) doc = json.load(f)

View File

@ -14,11 +14,13 @@ from cura.Settings.Exceptions import InvalidContainerError, InvalidOperationErro
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.cura_empty_instance_containers import empty_container from cura.Settings.cura_empty_instance_containers import empty_container
## Gets an instance container with a specified container type.
#
# \param container_type The type metadata for the instance container.
# \return An instance container instance.
def getInstanceContainer(container_type) -> InstanceContainer: def getInstanceContainer(container_type) -> InstanceContainer:
"""Gets an instance container with a specified container type.
:param container_type: The type metadata for the instance container.
:return: An instance container instance.
"""
container = InstanceContainer(container_id = "InstanceContainer") container = InstanceContainer(container_id = "InstanceContainer")
container.setMetaDataEntry("type", container_type) container.setMetaDataEntry("type", container_type)
return container return container
@ -32,10 +34,12 @@ class InstanceContainerSubClass(InstanceContainer):
super().__init__(container_id = "SubInstanceContainer") super().__init__(container_id = "SubInstanceContainer")
self.setMetaDataEntry("type", container_type) self.setMetaDataEntry("type", container_type)
#############################START OF TEST CASES################################ ############################START OF TEST CASES################################
## Tests whether adding a container is properly forbidden.
def test_addContainer(extruder_stack): def test_addContainer(extruder_stack):
"""Tests whether adding a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
extruder_stack.addContainer(unittest.mock.MagicMock()) extruder_stack.addContainer(unittest.mock.MagicMock())
@ -164,8 +168,10 @@ def test_constrainDefinitionInvalid(container, extruder_stack):
def test_constrainDefinitionValid(container, extruder_stack): def test_constrainDefinitionValid(container, extruder_stack):
extruder_stack.definition = container #Should not give an error. extruder_stack.definition = container #Should not give an error.
## Tests whether deserialising completes the missing containers with empty ones.
def test_deserializeCompletesEmptyContainers(extruder_stack): def test_deserializeCompletesEmptyContainers(extruder_stack):
"""Tests whether deserialising completes the missing containers with empty ones."""
extruder_stack._containers = [DefinitionContainer(container_id = "definition"), extruder_stack.definitionChanges] #Set the internal state of this stack manually. extruder_stack._containers = [DefinitionContainer(container_id = "definition"), extruder_stack.definitionChanges] #Set the internal state of this stack manually.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
@ -179,8 +185,10 @@ def test_deserializeCompletesEmptyContainers(extruder_stack):
continue continue
assert extruder_stack.getContainer(container_type_index) == empty_container #All others need to be empty. assert extruder_stack.getContainer(container_type_index) == empty_container #All others need to be empty.
## Tests whether an instance container with the wrong type gets removed when deserialising.
def test_deserializeRemovesWrongInstanceContainer(extruder_stack): def test_deserializeRemovesWrongInstanceContainer(extruder_stack):
"""Tests whether an instance container with the wrong type gets removed when deserialising."""
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type")
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -189,8 +197,10 @@ def test_deserializeRemovesWrongInstanceContainer(extruder_stack):
assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty.
## Tests whether a container with the wrong class gets removed when deserialising.
def test_deserializeRemovesWrongContainerClass(extruder_stack): def test_deserializeRemovesWrongContainerClass(extruder_stack):
"""Tests whether a container with the wrong class gets removed when deserialising."""
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class")
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -199,16 +209,20 @@ def test_deserializeRemovesWrongContainerClass(extruder_stack):
assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty.
## Tests whether an instance container in the definition spot results in an error.
def test_deserializeWrongDefinitionClass(extruder_stack): def test_deserializeWrongDefinitionClass(extruder_stack):
"""Tests whether an instance container in the definition spot results in an error."""
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container. with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container.
extruder_stack.deserialize("") extruder_stack.deserialize("")
## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.
def test_deserializeMoveInstanceContainer(extruder_stack): def test_deserializeMoveInstanceContainer(extruder_stack):
"""Tests whether an instance container with the wrong type is moved into the correct slot by deserialising."""
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot.
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -218,8 +232,10 @@ def test_deserializeMoveInstanceContainer(extruder_stack):
assert extruder_stack.quality == empty_container assert extruder_stack.quality == empty_container
assert extruder_stack.material != empty_container assert extruder_stack.material != empty_container
## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.
def test_deserializeMoveDefinitionContainer(extruder_stack): def test_deserializeMoveDefinitionContainer(extruder_stack):
"""Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising."""
extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
@ -228,8 +244,10 @@ def test_deserializeMoveDefinitionContainer(extruder_stack):
assert extruder_stack.material == empty_container assert extruder_stack.material == empty_container
assert extruder_stack.definition != empty_container assert extruder_stack.definition != empty_container
## Tests whether getProperty properly applies the stack-like behaviour on its containers.
def test_getPropertyFallThrough(global_stack, extruder_stack): def test_getPropertyFallThrough(global_stack, extruder_stack):
"""Tests whether getProperty properly applies the stack-like behaviour on its containers."""
# ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager
ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock()
@ -273,13 +291,17 @@ def test_getPropertyFallThrough(global_stack, extruder_stack):
extruder_stack.userChanges = mock_layer_heights[container_indices.UserChanges] extruder_stack.userChanges = mock_layer_heights[container_indices.UserChanges]
assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges
## Tests whether inserting a container is properly forbidden.
def test_insertContainer(extruder_stack): def test_insertContainer(extruder_stack):
"""Tests whether inserting a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
extruder_stack.insertContainer(0, unittest.mock.MagicMock()) extruder_stack.insertContainer(0, unittest.mock.MagicMock())
## Tests whether removing a container is properly forbidden.
def test_removeContainer(extruder_stack): def test_removeContainer(extruder_stack):
"""Tests whether removing a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
extruder_stack.removeContainer(unittest.mock.MagicMock()) extruder_stack.removeContainer(unittest.mock.MagicMock())

View File

@ -16,11 +16,13 @@ import UM.Settings.SettingDefinition #To add settings to the definition.
from cura.Settings.cura_empty_instance_containers import empty_container from cura.Settings.cura_empty_instance_containers import empty_container
## Gets an instance container with a specified container type.
#
# \param container_type The type metadata for the instance container.
# \return An instance container instance.
def getInstanceContainer(container_type) -> InstanceContainer: def getInstanceContainer(container_type) -> InstanceContainer:
"""Gets an instance container with a specified container type.
:param container_type: The type metadata for the instance container.
:return: An instance container instance.
"""
container = InstanceContainer(container_id = "InstanceContainer") container = InstanceContainer(container_id = "InstanceContainer")
container.setMetaDataEntry("type", container_type) container.setMetaDataEntry("type", container_type)
return container return container
@ -37,17 +39,19 @@ class InstanceContainerSubClass(InstanceContainer):
self.setMetaDataEntry("type", container_type) self.setMetaDataEntry("type", container_type)
#############################START OF TEST CASES################################ ############################START OF TEST CASES################################
## Tests whether adding a container is properly forbidden.
def test_addContainer(global_stack): def test_addContainer(global_stack):
"""Tests whether adding a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
global_stack.addContainer(unittest.mock.MagicMock()) global_stack.addContainer(unittest.mock.MagicMock())
## Tests adding extruders to the global stack.
def test_addExtruder(global_stack): def test_addExtruder(global_stack):
"""Tests adding extruders to the global stack."""
mock_definition = unittest.mock.MagicMock() mock_definition = unittest.mock.MagicMock()
mock_definition.getProperty = lambda key, property, context = None: 2 if key == "machine_extruder_count" and property == "value" else None mock_definition.getProperty = lambda key, property, context = None: 2 if key == "machine_extruder_count" and property == "value" else None
@ -213,9 +217,12 @@ def test_constrainDefinitionValid(container, global_stack):
global_stack.definition = container #Should not give an error. global_stack.definition = container #Should not give an error.
## Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the
# definition and the definition_changes (that cannot be empty after CURA-5281)
def test_deserializeCompletesEmptyContainers(global_stack): def test_deserializeCompletesEmptyContainers(global_stack):
"""Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the
definition and the definition_changes (that cannot be empty after CURA-5281)
"""
global_stack._containers = [DefinitionContainer(container_id = "definition"), global_stack.definitionChanges] #Set the internal state of this stack manually. global_stack._containers = [DefinitionContainer(container_id = "definition"), global_stack.definitionChanges] #Set the internal state of this stack manually.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
@ -229,8 +236,9 @@ def test_deserializeCompletesEmptyContainers(global_stack):
assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty. assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty.
## Tests whether an instance container with the wrong type gets removed when deserialising.
def test_deserializeRemovesWrongInstanceContainer(global_stack): def test_deserializeRemovesWrongInstanceContainer(global_stack):
"""Tests whether an instance container with the wrong type gets removed when deserialising."""
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type")
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -240,8 +248,9 @@ def test_deserializeRemovesWrongInstanceContainer(global_stack):
assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty.
## Tests whether a container with the wrong class gets removed when deserialising.
def test_deserializeRemovesWrongContainerClass(global_stack): def test_deserializeRemovesWrongContainerClass(global_stack):
"""Tests whether a container with the wrong class gets removed when deserialising."""
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class")
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -251,8 +260,9 @@ def test_deserializeRemovesWrongContainerClass(global_stack):
assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty.
## Tests whether an instance container in the definition spot results in an error.
def test_deserializeWrongDefinitionClass(global_stack): def test_deserializeWrongDefinitionClass(global_stack):
"""Tests whether an instance container in the definition spot results in an error."""
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
@ -260,8 +270,9 @@ def test_deserializeWrongDefinitionClass(global_stack):
global_stack.deserialize("") global_stack.deserialize("")
## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.
def test_deserializeMoveInstanceContainer(global_stack): def test_deserializeMoveInstanceContainer(global_stack):
"""Tests whether an instance container with the wrong type is moved into the correct slot by deserialising."""
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot.
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition")
@ -272,8 +283,9 @@ def test_deserializeMoveInstanceContainer(global_stack):
assert global_stack.material != empty_container assert global_stack.material != empty_container
## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.
def test_deserializeMoveDefinitionContainer(global_stack): def test_deserializeMoveDefinitionContainer(global_stack):
"""Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising."""
global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot.
with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize.
@ -283,8 +295,9 @@ def test_deserializeMoveDefinitionContainer(global_stack):
assert global_stack.definition != empty_container assert global_stack.definition != empty_container
## Tests whether getProperty properly applies the stack-like behaviour on its containers.
def test_getPropertyFallThrough(global_stack): def test_getPropertyFallThrough(global_stack):
"""Tests whether getProperty properly applies the stack-like behaviour on its containers."""
#A few instance container mocks to put in the stack. #A few instance container mocks to put in the stack.
mock_layer_heights = {} #For each container type, a mock container that defines layer height to something unique. mock_layer_heights = {} #For each container type, a mock container that defines layer height to something unique.
mock_no_settings = {} #For each container type, a mock container that has no settings at all. mock_no_settings = {} #For each container type, a mock container that has no settings at all.
@ -326,8 +339,9 @@ def test_getPropertyFallThrough(global_stack):
assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges
## In definitions, test whether having no resolve allows us to find the value.
def test_getPropertyNoResolveInDefinition(global_stack): def test_getPropertyNoResolveInDefinition(global_stack):
"""In definitions, test whether having no resolve allows us to find the value."""
value = unittest.mock.MagicMock() #Just sets the value for bed temperature. value = unittest.mock.MagicMock() #Just sets the value for bed temperature.
value.getProperty = lambda key, property, context = None: 10 if (key == "material_bed_temperature" and property == "value") else None value.getProperty = lambda key, property, context = None: 10 if (key == "material_bed_temperature" and property == "value") else None
@ -336,8 +350,9 @@ def test_getPropertyNoResolveInDefinition(global_stack):
assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value. assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value.
## In definitions, when the value is asked and there is a resolve function, it must get the resolve first.
def test_getPropertyResolveInDefinition(global_stack): def test_getPropertyResolveInDefinition(global_stack):
"""In definitions, when the value is asked and there is a resolve function, it must get the resolve first."""
resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature. resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature.
resolve_and_value.getProperty = lambda key, property, context = None: (7.5 if property == "resolve" else 5) if (key == "material_bed_temperature" and property in ("resolve", "value")) else None #7.5 resolve, 5 value. resolve_and_value.getProperty = lambda key, property, context = None: (7.5 if property == "resolve" else 5) if (key == "material_bed_temperature" and property in ("resolve", "value")) else None #7.5 resolve, 5 value.
@ -346,8 +361,9 @@ def test_getPropertyResolveInDefinition(global_stack):
assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition. assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition.
## In instance containers, when the value is asked and there is a resolve function, it must get the value first.
def test_getPropertyResolveInInstance(global_stack): def test_getPropertyResolveInInstance(global_stack):
"""In instance containers, when the value is asked and there is a resolve function, it must get the value first."""
container_indices = cura.Settings.CuraContainerStack._ContainerIndexes container_indices = cura.Settings.CuraContainerStack._ContainerIndexes
instance_containers = {} instance_containers = {}
for container_type in container_indices.IndexTypeMap: for container_type in container_indices.IndexTypeMap:
@ -373,8 +389,9 @@ def test_getPropertyResolveInInstance(global_stack):
assert global_stack.getProperty("material_bed_temperature", "value") == 5 assert global_stack.getProperty("material_bed_temperature", "value") == 5
## Tests whether the value in instances gets evaluated before the resolve in definitions.
def test_getPropertyInstancesBeforeResolve(global_stack): def test_getPropertyInstancesBeforeResolve(global_stack):
"""Tests whether the value in instances gets evaluated before the resolve in definitions."""
def getValueProperty(key, property, context = None): def getValueProperty(key, property, context = None):
if key != "material_bed_temperature": if key != "material_bed_temperature":
return None return None
@ -404,8 +421,9 @@ def test_getPropertyInstancesBeforeResolve(global_stack):
assert global_stack.getProperty("material_bed_temperature", "value") == 10 assert global_stack.getProperty("material_bed_temperature", "value") == 10
## Tests whether the hasUserValue returns true for settings that are changed in the user-changes container.
def test_hasUserValueUserChanges(global_stack): def test_hasUserValueUserChanges(global_stack):
"""Tests whether the hasUserValue returns true for settings that are changed in the user-changes container."""
container = unittest.mock.MagicMock() container = unittest.mock.MagicMock()
container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user") container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user")
container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set.
@ -416,8 +434,9 @@ def test_hasUserValueUserChanges(global_stack):
assert not global_stack.hasUserValue("") assert not global_stack.hasUserValue("")
## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container.
def test_hasUserValueQualityChanges(global_stack): def test_hasUserValueQualityChanges(global_stack):
"""Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container."""
container = unittest.mock.MagicMock() container = unittest.mock.MagicMock()
container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes") container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes")
container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set.
@ -428,8 +447,9 @@ def test_hasUserValueQualityChanges(global_stack):
assert not global_stack.hasUserValue("") assert not global_stack.hasUserValue("")
## Tests whether a container in some other place on the stack is correctly not recognised as user value.
def test_hasNoUserValue(global_stack): def test_hasNoUserValue(global_stack):
"""Tests whether a container in some other place on the stack is correctly not recognised as user value."""
container = unittest.mock.MagicMock() container = unittest.mock.MagicMock()
container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality") container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality")
container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set.
@ -438,20 +458,23 @@ def test_hasNoUserValue(global_stack):
assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value. assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value.
## Tests whether inserting a container is properly forbidden.
def test_insertContainer(global_stack): def test_insertContainer(global_stack):
"""Tests whether inserting a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
global_stack.insertContainer(0, unittest.mock.MagicMock()) global_stack.insertContainer(0, unittest.mock.MagicMock())
## Tests whether removing a container is properly forbidden.
def test_removeContainer(global_stack): def test_removeContainer(global_stack):
"""Tests whether removing a container is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
global_stack.removeContainer(unittest.mock.MagicMock()) global_stack.removeContainer(unittest.mock.MagicMock())
## Tests whether changing the next stack is properly forbidden.
def test_setNextStack(global_stack): def test_setNextStack(global_stack):
"""Tests whether changing the next stack is properly forbidden."""
with pytest.raises(InvalidOperationError): with pytest.raises(InvalidOperationError):
global_stack.setNextStack(unittest.mock.MagicMock()) global_stack.setNextStack(unittest.mock.MagicMock())

View File

@ -61,9 +61,10 @@ variant_filepaths = collectAllVariants()
intent_filepaths = collectAllIntents() intent_filepaths = collectAllIntents()
## Attempt to load all the quality profiles.
@pytest.mark.parametrize("file_name", quality_filepaths) @pytest.mark.parametrize("file_name", quality_filepaths)
def test_validateQualityProfiles(file_name): def test_validateQualityProfiles(file_name):
"""Attempt to load all the quality profiles."""
try: try:
with open(file_name, encoding = "utf-8") as data: with open(file_name, encoding = "utf-8") as data:
serialized = data.read() serialized = data.read()
@ -114,9 +115,10 @@ def test_validateIntentProfiles(file_name):
# File can't be read, header sections missing, whatever the case, this shouldn't happen! # File can't be read, header sections missing, whatever the case, this shouldn't happen!
assert False, "Got an exception while reading the file {file_name}: {err}".format(file_name = file_name, err = str(e)) assert False, "Got an exception while reading the file {file_name}: {err}".format(file_name = file_name, err = str(e))
## Attempt to load all the variant profiles.
@pytest.mark.parametrize("file_name", variant_filepaths) @pytest.mark.parametrize("file_name", variant_filepaths)
def test_validateVariantProfiles(file_name): def test_validateVariantProfiles(file_name):
"""Attempt to load all the variant profiles."""
try: try:
with open(file_name, encoding = "utf-8") as data: with open(file_name, encoding = "utf-8") as data:
serialized = data.read() serialized = data.read()

View File

@ -6,36 +6,43 @@ import numpy
from cura.Arranging.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
## Triangle of area 12
def gimmeTriangle(): def gimmeTriangle():
"""Triangle of area 12"""
return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32)
## Boring square
def gimmeSquare(): def gimmeSquare():
"""Boring square"""
return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)
## Triangle of area 12
def gimmeShapeArray(scale = 1.0): def gimmeShapeArray(scale = 1.0):
"""Triangle of area 12"""
vertices = gimmeTriangle() vertices = gimmeTriangle()
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
return shape_arr return shape_arr
## Boring square
def gimmeShapeArraySquare(scale = 1.0): def gimmeShapeArraySquare(scale = 1.0):
"""Boring square"""
vertices = gimmeSquare() vertices = gimmeSquare()
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
return shape_arr return shape_arr
## Smoke test for Arrange
def test_smoke_arrange(): def test_smoke_arrange():
"""Smoke test for Arrange"""
Arrange.create(fixed_nodes = []) Arrange.create(fixed_nodes = [])
## Smoke test for ShapeArray
def test_smoke_ShapeArray(): def test_smoke_ShapeArray():
"""Smoke test for ShapeArray"""
gimmeShapeArray() gimmeShapeArray()
## Test ShapeArray
def test_ShapeArray(): def test_ShapeArray():
"""Test ShapeArray"""
scale = 1 scale = 1
ar = Arrange(16, 16, 8, 8, scale = scale) ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst() ar.centerFirst()
@ -44,8 +51,9 @@ def test_ShapeArray():
count = len(numpy.where(shape_arr.arr == 1)[0]) count = len(numpy.where(shape_arr.arr == 1)[0])
assert count >= 10 # should approach 12 assert count >= 10 # should approach 12
## Test ShapeArray with scaling
def test_ShapeArray_scaling(): def test_ShapeArray_scaling():
"""Test ShapeArray with scaling"""
scale = 2 scale = 2
ar = Arrange(16, 16, 8, 8, scale = scale) ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst() ar.centerFirst()
@ -54,8 +62,9 @@ def test_ShapeArray_scaling():
count = len(numpy.where(shape_arr.arr == 1)[0]) count = len(numpy.where(shape_arr.arr == 1)[0])
assert count >= 40 # should approach 2*2*12 = 48 assert count >= 40 # should approach 2*2*12 = 48
## Test ShapeArray with scaling
def test_ShapeArray_scaling2(): def test_ShapeArray_scaling2():
"""Test ShapeArray with scaling"""
scale = 0.5 scale = 0.5
ar = Arrange(16, 16, 8, 8, scale = scale) ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst() ar.centerFirst()
@ -64,8 +73,9 @@ def test_ShapeArray_scaling2():
count = len(numpy.where(shape_arr.arr == 1)[0]) count = len(numpy.where(shape_arr.arr == 1)[0])
assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding
## Test centerFirst
def test_centerFirst(): def test_centerFirst():
"""Test centerFirst"""
ar = Arrange(300, 300, 150, 150, scale = 1) ar = Arrange(300, 300, 150, 150, scale = 1)
ar.centerFirst() ar.centerFirst()
assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[170][150]
@ -75,8 +85,9 @@ def test_centerFirst():
assert ar._priority[150][150] < ar._priority[150][130] assert ar._priority[150][150] < ar._priority[150][130]
assert ar._priority[150][150] < ar._priority[130][130] assert ar._priority[150][150] < ar._priority[130][130]
## Test centerFirst
def test_centerFirst_rectangular(): def test_centerFirst_rectangular():
"""Test centerFirst"""
ar = Arrange(400, 300, 200, 150, scale = 1) ar = Arrange(400, 300, 200, 150, scale = 1)
ar.centerFirst() ar.centerFirst()
assert ar._priority[150][200] < ar._priority[150][220] assert ar._priority[150][200] < ar._priority[150][220]
@ -86,15 +97,17 @@ def test_centerFirst_rectangular():
assert ar._priority[150][200] < ar._priority[130][200] assert ar._priority[150][200] < ar._priority[130][200]
assert ar._priority[150][200] < ar._priority[130][180] assert ar._priority[150][200] < ar._priority[130][180]
## Test centerFirst
def test_centerFirst_rectangular2(): def test_centerFirst_rectangular2():
"""Test centerFirst"""
ar = Arrange(10, 20, 5, 10, scale = 1) ar = Arrange(10, 20, 5, 10, scale = 1)
ar.centerFirst() ar.centerFirst()
assert ar._priority[10][5] < ar._priority[10][7] assert ar._priority[10][5] < ar._priority[10][7]
## Test backFirst
def test_backFirst(): def test_backFirst():
"""Test backFirst"""
ar = Arrange(300, 300, 150, 150, scale = 1) ar = Arrange(300, 300, 150, 150, scale = 1)
ar.backFirst() ar.backFirst()
assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[170][150]
@ -102,8 +115,9 @@ def test_backFirst():
assert ar._priority[150][150] > ar._priority[130][150] assert ar._priority[150][150] > ar._priority[130][150]
assert ar._priority[150][150] > ar._priority[130][130] assert ar._priority[150][150] > ar._priority[130][130]
## See if the result of bestSpot has the correct form
def test_smoke_bestSpot(): def test_smoke_bestSpot():
"""See if the result of bestSpot has the correct form"""
ar = Arrange(30, 30, 15, 15, scale = 1) ar = Arrange(30, 30, 15, 15, scale = 1)
ar.centerFirst() ar.centerFirst()
@ -114,8 +128,9 @@ def test_smoke_bestSpot():
assert hasattr(best_spot, "penalty_points") assert hasattr(best_spot, "penalty_points")
assert hasattr(best_spot, "priority") assert hasattr(best_spot, "priority")
## Real life test
def test_bestSpot(): def test_bestSpot():
"""Real life test"""
ar = Arrange(16, 16, 8, 8, scale = 1) ar = Arrange(16, 16, 8, 8, scale = 1)
ar.centerFirst() ar.centerFirst()
@ -131,8 +146,9 @@ def test_bestSpot():
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
ar.place(best_spot.x, best_spot.y, shape_arr) ar.place(best_spot.x, best_spot.y, shape_arr)
## Real life test rectangular build plate
def test_bestSpot_rectangular_build_plate(): def test_bestSpot_rectangular_build_plate():
"""Real life test rectangular build plate"""
ar = Arrange(16, 40, 8, 20, scale = 1) ar = Arrange(16, 40, 8, 20, scale = 1)
ar.centerFirst() ar.centerFirst()
@ -164,8 +180,9 @@ def test_bestSpot_rectangular_build_plate():
best_spot_x = ar.bestSpot(shape_arr) best_spot_x = ar.bestSpot(shape_arr)
ar.place(best_spot_x.x, best_spot_x.y, shape_arr) ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
## Real life test
def test_bestSpot_scale(): def test_bestSpot_scale():
"""Real life test"""
scale = 0.5 scale = 0.5
ar = Arrange(16, 16, 8, 8, scale = scale) ar = Arrange(16, 16, 8, 8, scale = scale)
ar.centerFirst() ar.centerFirst()
@ -182,8 +199,9 @@ def test_bestSpot_scale():
assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location
ar.place(best_spot.x, best_spot.y, shape_arr) ar.place(best_spot.x, best_spot.y, shape_arr)
## Real life test
def test_bestSpot_scale_rectangular(): def test_bestSpot_scale_rectangular():
"""Real life test"""
scale = 0.5 scale = 0.5
ar = Arrange(16, 40, 8, 20, scale = scale) ar = Arrange(16, 40, 8, 20, scale = scale)
ar.centerFirst() ar.centerFirst()
@ -205,8 +223,9 @@ def test_bestSpot_scale_rectangular():
best_spot = ar.bestSpot(shape_arr_square) best_spot = ar.bestSpot(shape_arr_square)
ar.place(best_spot.x, best_spot.y, shape_arr_square) ar.place(best_spot.x, best_spot.y, shape_arr_square)
## Try to place an object and see if something explodes
def test_smoke_place(): def test_smoke_place():
"""Try to place an object and see if something explodes"""
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
ar.centerFirst() ar.centerFirst()
@ -216,8 +235,9 @@ def test_smoke_place():
ar.place(0, 0, shape_arr) ar.place(0, 0, shape_arr)
assert numpy.any(ar._occupied) assert numpy.any(ar._occupied)
## See of our center has less penalty points than out of the center
def test_checkShape(): def test_checkShape():
"""See of our center has less penalty points than out of the center"""
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
ar.centerFirst() ar.centerFirst()
@ -228,8 +248,9 @@ def test_checkShape():
assert points2 > points assert points2 > points
assert points3 > points assert points3 > points
## See of our center has less penalty points than out of the center
def test_checkShape_rectangular(): def test_checkShape_rectangular():
"""See of our center has less penalty points than out of the center"""
ar = Arrange(20, 30, 10, 15) ar = Arrange(20, 30, 10, 15)
ar.centerFirst() ar.centerFirst()
@ -240,8 +261,9 @@ def test_checkShape_rectangular():
assert points2 > points assert points2 > points
assert points3 > points assert points3 > points
## Check that placing an object on occupied place returns None.
def test_checkShape_place(): def test_checkShape_place():
"""Check that placing an object on occupied place returns None."""
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
ar.centerFirst() ar.centerFirst()
@ -252,8 +274,9 @@ def test_checkShape_place():
assert points2 is None assert points2 is None
## Test the whole sequence
def test_smoke_place_objects(): def test_smoke_place_objects():
"""Test the whole sequence"""
ar = Arrange(20, 20, 10, 10, scale = 1) ar = Arrange(20, 20, 10, 10, scale = 1)
ar.centerFirst() ar.centerFirst()
shape_arr = gimmeShapeArray() shape_arr = gimmeShapeArray()
@ -268,26 +291,30 @@ def test_compare_occupied_and_priority_tables():
ar.centerFirst() ar.centerFirst()
assert ar._priority.shape == ar._occupied.shape assert ar._priority.shape == ar._occupied.shape
## Polygon -> array
def test_arrayFromPolygon(): def test_arrayFromPolygon():
"""Polygon -> array"""
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
array = ShapeArray.arrayFromPolygon([5, 5], vertices) array = ShapeArray.arrayFromPolygon([5, 5], vertices)
assert numpy.any(array) assert numpy.any(array)
## Polygon -> array
def test_arrayFromPolygon2(): def test_arrayFromPolygon2():
"""Polygon -> array"""
vertices = numpy.array([[-3, 1], [3, 1], [2, -3]]) vertices = numpy.array([[-3, 1], [3, 1], [2, -3]])
array = ShapeArray.arrayFromPolygon([5, 5], vertices) array = ShapeArray.arrayFromPolygon([5, 5], vertices)
assert numpy.any(array) assert numpy.any(array)
## Polygon -> array
def test_fromPolygon(): def test_fromPolygon():
"""Polygon -> array"""
vertices = numpy.array([[0, 0.5], [0, 0], [0.5, 0]]) vertices = numpy.array([[0, 0.5], [0, 0], [0.5, 0]])
array = ShapeArray.fromPolygon(vertices, scale=0.5) array = ShapeArray.fromPolygon(vertices, scale=0.5)
assert numpy.any(array.arr) assert numpy.any(array.arr)
## Line definition -> array with true/false
def test_check(): def test_check():
"""Line definition -> array with true/false"""
base_array = numpy.zeros([5, 5], dtype=float) base_array = numpy.zeros([5, 5], dtype=float)
p1 = numpy.array([0, 0]) p1 = numpy.array([0, 0])
p2 = numpy.array([4, 4]) p2 = numpy.array([4, 4])
@ -296,8 +323,9 @@ def test_check():
assert check_array[3][0] assert check_array[3][0]
assert not check_array[0][3] assert not check_array[0][3]
## Line definition -> array with true/false
def test_check2(): def test_check2():
"""Line definition -> array with true/false"""
base_array = numpy.zeros([5, 5], dtype=float) base_array = numpy.zeros([5, 5], dtype=float)
p1 = numpy.array([0, 3]) p1 = numpy.array([0, 3])
p2 = numpy.array([4, 3]) p2 = numpy.array([4, 3])
@ -306,8 +334,9 @@ def test_check2():
assert not check_array[3][0] assert not check_array[3][0]
assert check_array[3][4] assert check_array[3][4]
## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM
def test_parts_of_fromNode(): def test_parts_of_fromNode():
"""Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM"""
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)) p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32))
offset = 1 offset = 1