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
## 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:
"""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 = []
for root, dir_names, file_names in os.walk(work_dir):
for file_name in file_names:
@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> 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:
"""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:
with open(file_path, "r", encoding = "utf-8") as f:
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
## A specialised operation designed specifically to modify the previous operation.
class PlatformPhysicsOperation(Operation):
"""A specialised operation designed specifically to modify the previous operation."""
def __init__(self, node: SceneNode, translation: Vector) -> None:
super().__init__()
self._node = node

View File

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

View File

@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode
from UM.Operations import Operation
## An operation that parents a scene node to another scene node.
class SetParentOperation(Operation.Operation):
## Initialises this SetParentOperation.
#
# \param node The node which will be reparented.
# \param parent_node The node which will be the parent.
"""An operation that parents a scene node to another scene node."""
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__()
self._node = node
self._parent = parent_node
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:
"""Undoes the set-parent operation, restoring the old parent."""
self._set_parent(self._old_parent)
## Re-applies the set-parent operation.
def redo(self) -> None:
"""Re-applies the set-parent operation."""
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:
"""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:
current_parent = self._node.getParent()
if current_parent:
@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation):
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:
"""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)

View File

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

View File

@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject):
def hotendID(self) -> Optional[str]:
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:
"""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
def __str__(self) -> str:

View File

@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject):
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
self._extruder_configuration.setMaterial(material)
## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float) -> None:
"""Update the hotend temperature. This only changes it locally."""
if self._hotend_temperature != temperature:
self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit()
@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject):
self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float)
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.updateTargetHotendTemperature(temperature)
@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject):
def isPreheating(self) -> bool:
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)
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)
@pyqtSlot()

View File

@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject):
def buildplateConfiguration(self) -> str:
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:
"""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:
return False
for configuration in self._extruder_configurations:
@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject):
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):
"""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)
first_extruder = None
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:
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)
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)
@pyqtSlot()
@ -200,8 +202,9 @@ class PrinterOutputModel(QObject):
self._unique_name = unique_name
self.nameChanged.emit()
## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature: float) -> None:
"""Update the bed temperature. This only changes it locally."""
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
@ -211,9 +214,10 @@ class PrinterOutputModel(QObject):
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float)
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.updateTargetBedTemperature(temperature)

View File

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

View File

@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def _compressGCode(self) -> Optional[bytes]:
self._compressing_gcode = True
## Mash the data into single string
max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
"""Mash the data into single string"""
file_data_bytes_list = []
batched_lines = []
batched_lines_count = 0
@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
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:
"""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)
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
part.setBody(data)
return part
## Convenience function to get the username, either from the cloud or from the OS.
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
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
if account and account.isLoggedIn:
@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._createNetworkManager()
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",
on_finished: Optional[Callable[[QNetworkReply], 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()
request = self._createEmptyRequest(url, content_type = content_type)
@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if on_progress is not None:
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:
"""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()
request = self._createEmptyRequest(url)
@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
reply = self._manager.deleteResource(request)
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:
"""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()
request = self._createEmptyRequest(url)
@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
reply = self._manager.get(request)
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],
on_finished: Optional[Callable[[QNetworkReply], 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()
request = self._createEmptyRequest(url)
@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if on_finished is not None:
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:
"""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()
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
if global_container_stack and device_id == active_machine_network_name:
@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def getProperties(self):
return self._properties
## Get the unique key of this machine
# \return key String containing the key of the machine.
@pyqtProperty(str, constant = True)
def key(self) -> str:
"""Get the unique key of this machine
:return: key String containing the key of the machine.
"""
return self._id
## The IP address of the printer.
@pyqtProperty(str, constant = True)
def address(self) -> str:
"""The IP address of the printer."""
return self._properties.get(b"address", b"").decode("utf-8")
## Name of the printer (as returned from the ZeroConf properties)
@pyqtProperty(str, constant = True)
def name(self) -> str:
"""Name of the printer (as returned from the ZeroConf properties)"""
return self._properties.get(b"name", b"").decode("utf-8")
## Firmware version (as returned from the ZeroConf properties)
@pyqtProperty(str, constant = True)
def firmwareVersion(self) -> str:
"""Firmware version (as returned from the ZeroConf properties)"""
return self._properties.get(b"firmware_version", b"").decode("utf-8")
@pyqtProperty(str, constant = True)
def printerType(self) -> str:
return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
## IP adress of this printer
@pyqtProperty(str, constant = True)
def ipAddress(self) -> str:
"""IP adress of this printer"""
return self._address

View File

@ -2,15 +2,19 @@
# 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:
## Constructs the peripheral.
# \param type A unique ID for the type of peripheral.
# \param name A human-readable name for the peripheral.
"""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.
"""
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.name = name

View File

@ -24,8 +24,9 @@ if MYPY:
i18n_catalog = i18nCatalog("cura")
## The current processing state of the backend.
class ConnectionState(IntEnum):
"""The current processing state of the backend."""
Closed = 0
Connecting = 1
Connected = 2
@ -40,17 +41,19 @@ class ConnectionType(IntEnum):
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
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()
connectionStateChanged = pyqtSignal(str)
@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice):
if self._monitor_item is None:
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
## Attempt to establish connection
def connect(self) -> None:
"""Attempt to establish connection"""
self.setConnectionState(ConnectionState.Connecting)
self._update_timer.start()
## Attempt to close the connection
def close(self) -> None:
"""Attempt to close the connection"""
self._update_timer.stop()
self.setConnectionState(ConnectionState.Closed)
## Ensure that close gets called when object is destroyed
def __del__(self) -> None:
"""Ensure that close gets called when object is destroyed"""
self.close()
@pyqtProperty(bool, notify = acceptsCommandsChanged)
def acceptsCommands(self) -> bool:
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:
"""Set a flag to signal the UI that the printer is not (yet) ready to receive commands"""
if 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
self._updateUniqueConfigurations()
## Set the device firmware name
#
# \param name The name of the firmware.
def _setFirmwareName(self, name: str) -> None:
"""Set the device firmware name
:param name: The name of the firmware.
"""
self._firmware_name = name
## Get the name of device firmware
#
# This name can be used to define device type
def getFirmwareName(self) -> Optional[str]:
"""Get the name of device firmware
This name can be used to define device type
"""
return self._firmware_name
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:

View File

@ -10,15 +10,19 @@ class NoProfileException(Exception):
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):
"""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):
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):
"""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.")

View File

@ -3,23 +3,29 @@
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):
## Initialises the profile writer.
#
# This currently doesn't do anything since the writer is basically static.
"""Base class for profile writer plugins.
This class defines a write() function to write profiles to files with.
"""
def __init__(self):
"""Initialises the profile writer.
This currently doesn't do anything since the writer is basically static.
"""
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):
"""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.")

View File

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

View File

@ -23,9 +23,12 @@ if TYPE_CHECKING:
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):
"""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:
super().__init__()
@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._onChanged()
## Force that a new (empty) object is created upon copy.
def __deepcopy__(self, memo):
"""Force that a new (empty) object is created upon copy."""
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]:
"""The polygon representing the 2D adhesion area.
If no adhesion is used, the regular convex hull is returned
"""
if self._node is None:
return None
@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
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]:
"""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:
return None
if self._node.callDecoration("isNonPrintingMesh"):
@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
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]:
"""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:
return None
@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return False
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]:
"""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:
return None
if self._node.callDecoration("isNonPrintingMesh"):
@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return head_with_fans_with_adhesion_margin
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]:
"""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:
return None
@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._compute2DConvexHull()
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]:
"""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():
# In one-at-a-time mode, every printed object gets it's own adhesion
printing_area = self.getAdhesionArea()
@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
printing_area = self.getConvexHull()
return printing_area
## The same as recomputeConvexHull, but using a timer if it was set.
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:
self._recompute_convex_hull_timer.start()
else:
@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
return convex_hull.getMinkowskiHull(head_and_fans)
return None
## Compensate given 2D polygon with adhesion margin
# \return 2D polygon with added margin
def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon:
"""Compensate given 2D polygon with adhesion margin
:return: 2D polygon with added margin
"""
if not self._global_stack:
return Polygon()
# Compensate for raft/skirt/brim
@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
poly = poly.getMinkowskiHull(extra_margin_polygon)
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:
"""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(
self._getSettingProperty("xy_offset", "value"),
self._getSettingProperty("xy_offset_layer_0", "value")
@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
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:
"""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:
return None
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
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:
"""Returns True if node is a descendant or the same as the root node."""
if node is None:
return False
if root is node:
return True
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:
"""True if print_sequence is one_at_a_time and _node is not part of a group"""
if self._node is None:
return False
return self._global_stack is not None \
@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
"adhesion_type", "raft_margin", "print_sequence",
"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"}
"""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):
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:
"""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)
self.setCalculateBoundingBox(False)

View File

@ -72,9 +72,10 @@ class CuraSceneController(QObject):
max_build_plate = max(build_plate_number, max_build_plate)
return max_build_plate
## Either select or deselect an item
@pyqtSlot(int)
def changeSelection(self, index):
"""Either select or deselect an item"""
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier
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.
## 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):
"""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:
super().__init__(parent = parent, visible = visible, name = name)
if not no_setting_override:
@ -36,9 +38,11 @@ class CuraSceneNode(SceneNode):
def isSelectable(self) -> bool:
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]:
"""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()
if global_container_stack is None:
return None
@ -69,8 +73,9 @@ class CuraSceneNode(SceneNode):
# This point should never be reached
return None
## Return the color of the material used to print this model
def getDiffuseColor(self) -> List[float]:
"""Return the color of the material used to print this model"""
printing_extruder = self.getPrintingExtruder()
material_color = "#808080" # Fallback color
@ -86,8 +91,9 @@ class CuraSceneNode(SceneNode):
1.0
]
## Return if any area collides with the convex hull of this scene node
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")
if convex_hull:
if not convex_hull.isValid():
@ -101,8 +107,9 @@ class CuraSceneNode(SceneNode):
return True
return False
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
def _calculateAABB(self) -> None:
"""Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box"""
self._aabb = None
if self._mesh_data:
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
@ -122,8 +129,9 @@ class CuraSceneNode(SceneNode):
else:
self._aabb = self._aabb + child.getBoundingBox()
## Taken from SceneNode, but replaced SceneNode with 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.setTransformation(self.getLocalTransformation())
copy.setMeshData(self._mesh_data)

View File

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

View File

@ -33,12 +33,14 @@ if TYPE_CHECKING:
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):
"""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:
if ContainerManager.__instance is not None:
@ -67,21 +69,23 @@ class ContainerManager(QObject):
return ""
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)
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:
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
return False
@ -124,18 +128,20 @@ class ContainerManager(QObject):
def makeUniqueName(self, original_name: str) -> str:
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")
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:
self._updateContainerNameFilters()
@ -147,17 +153,18 @@ class ContainerManager(QObject):
filters.append("All Files (*)")
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")
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:
return {"status": "error", "message": "Invalid arguments"}
@ -214,14 +221,16 @@ class ContainerManager(QObject):
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")
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:
return {"status": "error", "message": "Invalid path"}
@ -266,14 +275,16 @@ class ContainerManager(QObject):
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)
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()
global_stack = application.getMachineManager().activeMachine
if not global_stack:
@ -313,9 +324,10 @@ class ContainerManager(QObject):
return True
## Clear the top-most (user) containers of the active stacks.
@pyqtSlot()
def clearUserContainers(self) -> None:
"""Clear the top-most (user) containers of the active stacks."""
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.blurSettings.emit()
@ -335,25 +347,28 @@ class ContainerManager(QObject):
for container in send_emits_containers:
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")
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)
if exclude_self:
return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
else:
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")
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
if material_node.container is None: # Failed to lazy-load this container.
return
@ -428,9 +443,10 @@ class ContainerManager(QObject):
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
self._container_name_filters[name_filter] = entry
## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result = "QVariantMap")
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():
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
path = file_url.toLocalFile()

File diff suppressed because it is too large Load Diff

View File

@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers
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):
"""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:
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.
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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
## 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)
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])
## 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:
"""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)
def getDefinition(self) -> "DefinitionContainer":
@ -171,14 +203,16 @@ class CuraContainerStack(ContainerStack):
def getTop(self) -> "InstanceContainer":
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)
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"):
return True
@ -187,51 +221,61 @@ class CuraContainerStack(ContainerStack):
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:
"""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
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)
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")
## 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)
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")
## 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)
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")
## 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)
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]
if expected_type == "definition":
if not isinstance(container, DefinitionContainer):
@ -245,16 +289,18 @@ class CuraContainerStack(ContainerStack):
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)
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
serialized = super().deserialize(serialized, file_name)
@ -298,10 +344,9 @@ class CuraContainerStack(ContainerStack):
## TODO; Deserialize the containers.
return serialized
## protected:
# Helper to make sure we emit a PyQt signal on container changes.
def _onContainersChanged(self, container: Any) -> None:
"""Helper to make sure we emit a PyQt signal on container changes."""
Application.getInstance().callLater(self.pyqtContainersChanged.emit)
# 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:
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
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")
if not quality_definition:
return machine_definition.id #type: ignore
@ -330,17 +377,18 @@ class CuraContainerStack(ContainerStack):
return cls._findInstanceContainerDefinitionId(definitions[0])
## getProperty for extruder positions, with translation from -1 to default extruder number
def getExtruderPositionValueWithDefault(self, key):
"""getProperty for extruder positions, with translation from -1 to default extruder number"""
value = self.getProperty(key, "value")
if value == -1:
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
return value
## private:
# Private helper class to keep track of container positions and their types.
class _ContainerIndexes:
"""Private helper class to keep track of container positions and their types."""
UserChanges = 0
QualityChanges = 1
Intent = 2

View File

@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack
## Contains helper functions to create new machines.
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
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
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
@ -71,12 +74,14 @@ class CuraStackBuilder:
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
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
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
@ -120,17 +125,6 @@ class CuraStackBuilder:
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
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface,
machine_definition_id: str,
@ -139,6 +133,19 @@ class CuraStackBuilder:
material_container: "InstanceContainer",
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
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
@ -167,29 +174,23 @@ class CuraStackBuilder:
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
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
variant_container: "InstanceContainer",
material_container: "InstanceContainer",
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
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()

View File

@ -2,21 +2,25 @@
# 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):
"""Raised when trying to perform an operation like add on a stack that does not allow that."""
pass
## Raised when trying to replace a container with a container that does not have the expected type.
class InvalidContainerError(Exception):
"""Raised when trying to replace a container with a container that does not have the expected type."""
pass
## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.
class TooManyExtrudersError(Exception):
"""Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders."""
pass
## Raised when an extruder has no next stack set.
class NoGlobalStackError(Exception):
"""Raised when an extruder has no next stack set."""
pass

View File

@ -19,13 +19,15 @@ if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
## Manages all existing extruder stacks.
#
# This keeps a list of extruder stacks for each machine.
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):
"""Registers listeners and such to listen to changes to the extruders."""
if ExtruderManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ExtruderManager.__instance = self
@ -43,20 +45,22 @@ class ExtruderManager(QObject):
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes.
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()
"""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)
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():
return None # No active machine, so no active extruder.
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.
return None
## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged)
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]
global_container_stack = self._application.getGlobalContainerStack()
@ -75,11 +80,13 @@ class ExtruderManager(QObject):
return extruder_stack_ids
## Changes the active extruder by index.
#
# \param index The index of the new active extruder.
@pyqtSlot(int)
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:
self._active_extruder_index = index
self.activeExtruderChanged.emit()
@ -88,12 +95,13 @@ class ExtruderManager(QObject):
def activeExtruderIndex(self) -> int:
return self._active_extruder_index
## Emitted whenever the selectedObjectExtruders property changes.
selectedObjectExtrudersChanged = pyqtSignal()
"""Emitted whenever the selectedObjectExtruders property changes."""
## Provides a list of extruder IDs used by the current selected objects.
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
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:
object_extruders = set()
@ -122,11 +130,13 @@ class ExtruderManager(QObject):
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:
"""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.selectedObjectExtrudersChanged.emit()
@ -134,8 +144,9 @@ class ExtruderManager(QObject):
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
return self.getExtruderStack(self.activeExtruderIndex)
## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
"""Get an extruder stack by index"""
global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack:
if global_container_stack.getId() in self._extruder_trains:
@ -162,12 +173,14 @@ class ExtruderManager(QObject):
if changed:
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]:
"""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 = []
for extruder_stack in self.getActiveExtruderStacks():
@ -182,17 +195,19 @@ class ExtruderManager(QObject):
else:
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"]:
"""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()
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)
return []
## Get the extruder that the print will start with.
#
# This should mirror the implementation in CuraEngine of
# ``FffGcodeWriter::getStartExtruder()``.
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()
global_stack = application.getGlobalContainerStack()
@ -296,28 +313,34 @@ class ExtruderManager(QObject):
# REALLY no adhesion? Use the first used extruder.
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:
"""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):
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
ContainerRegistry.getInstance().removeContainer(extruder.getId())
if machine_id in self._extruder_trains:
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"]:
"""Returns extruders for a specific machine.
:param machine_id: The machine to get the extruders of.
"""
if machine_id not in self._extruder_trains:
return []
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"]:
"""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()
if not global_stack:
return []
@ -329,8 +352,9 @@ class ExtruderManager(QObject):
self.resetSelectedObjectExtruders()
## Adds the extruders to the selected machine.
def addMachineExtruders(self, global_stack: GlobalStack) -> None:
"""Adds the extruders to the selected machine."""
extruders_changed = False
container_registry = ContainerRegistry.getInstance()
global_stack_id = global_stack.getId()
@ -396,26 +420,30 @@ class ExtruderManager(QObject):
raise IndexError(msg)
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")
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)
## 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
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())
resolved_value = global_stack.getProperty(key, "value")

View File

@ -22,10 +22,9 @@ if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
## Represents an Extruder and its related containers.
#
#
class ExtruderStack(CuraContainerStack):
"""Represents an Extruder and its related containers."""
def __init__(self, container_id: str) -> None:
super().__init__(container_id)
@ -35,11 +34,13 @@ class ExtruderStack(CuraContainerStack):
enabledChanged = pyqtSignal()
## Overridden from ContainerStack
#
# This will set the next stack and ensure that we register this stack as an extruder.
@override(ContainerStack)
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)
stack.addExtruder(self)
self.setMetaDataEntry("machine", stack.id)
@ -71,11 +72,13 @@ class ExtruderStack(CuraContainerStack):
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:
"""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.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
@ -97,31 +100,35 @@ class ExtruderStack(CuraContainerStack):
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:
"""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())
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
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)
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:
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
## Represents the Global or Machine stack and its related containers.
#
class GlobalStack(CuraContainerStack):
"""Represents the Global or Machine stack and its related containers."""
def __init__(self, container_id: str) -> None:
super().__init__(container_id)
@ -58,12 +58,14 @@ class GlobalStack(CuraContainerStack):
extrudersChanged = pyqtSignal()
configuredConnectionTypesChanged = pyqtSignal()
## Get the list of extruders of this stack.
#
# \return The extruders registered with this stack.
@pyqtProperty("QVariantMap", notify = extrudersChanged)
@deprecated("Please use extruderList instead.", "4.4")
def extruders(self) -> Dict[str, "ExtruderStack"]:
"""Get the list of extruders of this stack.
:return: The extruders registered with this stack.
"""
return self._extruders
@pyqtProperty("QVariantList", notify = extrudersChanged)
@ -86,16 +88,18 @@ class GlobalStack(CuraContainerStack):
def getLoadingPriority(cls) -> int:
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)
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).
# 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(",")
@ -122,16 +126,18 @@ class GlobalStack(CuraContainerStack):
ConnectionType.CloudConnection.value]
return has_remote_connection
## \sa configuredConnectionTypes
def addConfiguredConnectionType(self, connection_type: int) -> None:
""":sa configuredConnectionTypes"""
configured_connection_types = self.configuredConnectionTypes
if connection_type not in configured_connection_types:
# Store the values as a string.
configured_connection_types.append(connection_type)
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
## \sa configuredConnectionTypes
def removeConfiguredConnectionType(self, connection_type: int) -> None:
""":sa configuredConnectionTypes"""
configured_connection_types = self.configuredConnectionTypes
if connection_type in configured_connection_types:
# Store the values as a string.
@ -163,13 +169,15 @@ class GlobalStack(CuraContainerStack):
def preferred_output_file_formats(self) -> str:
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:
"""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")
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)
@ -183,19 +191,21 @@ class GlobalStack(CuraContainerStack):
self.extrudersChanged.emit()
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)
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):
return None
@ -235,11 +245,13 @@ class GlobalStack(CuraContainerStack):
context.popContainer()
return result
## Overridden from ContainerStack
#
# This will simply raise an exception since the Global stack cannot have a next stack.
@override(ContainerStack)
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!")
# protected:
@ -267,9 +279,11 @@ class GlobalStack(CuraContainerStack):
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:
"""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()
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
@ -299,9 +313,10 @@ class GlobalStack(CuraContainerStack):
def hasVariantBuildplates(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = 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")
baudrate = 250000

View File

@ -15,29 +15,32 @@ if TYPE_CHECKING:
from UM.Settings.InstanceContainer import InstanceContainer
## Front-end for querying which intents are available for a certain
# configuration.
class IntentManager(QObject):
"""Front-end for querying which intents are available for a certain configuration.
"""
__instance = None
## This class is a singleton.
@classmethod
def getInstance(cls):
"""This class is a singleton."""
if not cls.__instance:
cls.__instance = IntentManager()
return cls.__instance
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]]:
"""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]]
try:
materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
@ -53,28 +56,32 @@ class IntentManager(QObject):
intent_metadatas.append(intent_node.getMetadata())
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]:
"""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()
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
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.
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]]:
"""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()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
@ -100,16 +107,18 @@ class IntentManager(QObject):
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
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]:
"""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()
if global_stack is None:
return ["default"]
@ -123,10 +132,12 @@ class IntentManager(QObject):
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
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":
"""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
@pyqtProperty(str, notify = intentCategoryChanged)
@ -137,9 +148,10 @@ class IntentManager(QObject):
return ""
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
## Apply intent on the stacks.
@pyqtSlot(str, str)
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)
old_intent_category = self.currentIntentCategory
application = cura.CuraApplication.CuraApplication.getInstance()

View File

@ -215,8 +215,9 @@ class MachineManager(QObject):
return set()
return general_definition_containers[0].getAllKeys()
## Triggered when the global container stack is changed in CuraApplication.
def _onGlobalContainerChanged(self) -> None:
"""Triggered when the global container stack is changed in CuraApplication."""
if self._global_container_stack:
try:
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")
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
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:
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))
return False
## Check if the global_container has instances in the user container
@pyqtProperty(bool, notify = activeStackValueChanged)
def hasUserSettings(self) -> bool:
"""Check if the global_container has instances in the user container"""
if not self._global_container_stack:
return False
@ -422,10 +427,12 @@ class MachineManager(QObject):
num_user_settings += stack.getTop().getNumInstances()
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)
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)
if not self._global_container_stack:
return
@ -454,11 +461,13 @@ class MachineManager(QObject):
for container in send_emits_containers:
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)
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)
@pyqtProperty(str, notify = globalContainerChanged)
@ -528,14 +537,16 @@ class MachineManager(QObject):
return material.getId()
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)
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:
return 0
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
## 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)
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:
return False
for stack in [self._global_container_stack] + self._global_container_stack.extruderList:
@ -622,9 +635,10 @@ class MachineManager(QObject):
return False
return True
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
@pyqtSlot(str)
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:
return
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:
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()
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:
return
@ -648,19 +663,23 @@ class MachineManager(QObject):
# Check if the value has to be replaced
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)
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()
if not global_stack:
return ""
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)
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")
if self._global_container_stack:
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
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)
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:
return True
@ -727,10 +747,12 @@ class MachineManager(QObject):
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)
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:
return True
@ -751,11 +773,13 @@ class MachineManager(QObject):
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)
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)
if containers:
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)
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:
"""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:
return
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"))
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:
"""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:
return
extruder_manager = self._application.getExtruderManager()
@ -902,9 +929,10 @@ class MachineManager(QObject):
def defaultExtruderPosition(self) -> str:
return self._default_extruder_position
## This will fire the propertiesChanged for all settings so they will be updated in the front-end
@pyqtSlot()
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:
return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
@ -945,11 +973,13 @@ class MachineManager(QObject):
def _onMaterialNameChanged(self) -> None:
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]:
"""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:
return []
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.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)
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:
return
for extruder in self._global_container_stack.extruderList:
container = extruder.userChanges
container.removeInstance(setting_name)
## Update _current_root_material_id when the current root material was changed.
def _onRootMaterialChanged(self) -> None:
"""Update _current_root_material_id when the current root material was changed."""
self._current_root_material_id = {}
changed = False
@ -1135,8 +1168,9 @@ class MachineManager(QObject):
return False
return True
## Update current quality type and machine after setting material
def _updateQualityWithMaterial(self, *args: Any) -> None:
"""Update current quality type and machine after setting material"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
@ -1177,8 +1211,9 @@ class MachineManager(QObject):
current_quality_type, quality_type)
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
## Update the current intent after the quality changed
def _updateIntentWithQuality(self):
"""Update the current intent after the quality changed"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
@ -1205,12 +1240,14 @@ class MachineManager(QObject):
category = current_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()
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:
return
if position is None:
@ -1245,10 +1282,12 @@ class MachineManager(QObject):
material_node = nozzle_node.preferredMaterial(approximate_material_diameter)
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)
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
if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name:
return
@ -1400,10 +1439,12 @@ class MachineManager(QObject):
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
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")
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:
global_stack.extruders[position].material = container_node.container
return
@ -1449,10 +1490,12 @@ class MachineManager(QObject):
# Get all the quality groups for this global stack and filter out by 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)
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 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,
"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)
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()
if global_stack is None:
return
@ -1554,21 +1599,25 @@ class MachineManager(QObject):
else: # No intent had the correct category.
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"]:
"""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()
if not global_stack or global_stack.quality == empty_quality_container:
return None
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)
def activeQualityGroupName(self) -> str:
"""Get the name of the active quality group.
:return: The name of the active quality group.
"""
quality_group = self.activeQualityGroup()
if quality_group is None:
return ""
@ -1641,9 +1690,10 @@ class MachineManager(QObject):
self.updateMaterialWithVariant(None)
self._updateQualityWithMaterial()
## This function will translate any printer type name to an abbreviated printer type name
@pyqtSlot(str, result = 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 = ""
for word in re.findall(r"[\w']+", machine_type_name):
if word.lower() == "ultimaker":

View File

@ -10,10 +10,13 @@ from UM.Resources import Resources
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
## Are machine names valid?
#
# Performs checks based on the length of the name.
class MachineNameValidator(QObject):
"""Are machine names valid?
Performs checks based on the length of the name.
"""
def __init__(self, parent = None):
super().__init__(parent)
@ -32,12 +35,13 @@ class MachineNameValidator(QObject):
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):
"""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).
try:
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
@ -50,9 +54,10 @@ class MachineNameValidator(QObject):
return QValidator.Acceptable #All checks succeeded.
## Updates the validation state of a machine name text field.
@pyqtSlot(str)
def updateValidation(self, new_name):
"""Updates the validation state of a machine name text field."""
is_valid = self.validate(new_name)
if is_valid == QValidator.Acceptable:
self.validation_regex = "^.*$" #Matches anything.

View File

@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the extruder a certain object should be printed with.
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:
self._node = node
self._extruder_id = extruder_id

View File

@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject):
settingsWithIntheritanceChanged = pyqtSignal()
## Get the keys of all children settings with an override.
@pyqtSlot(str, result = "QStringList")
def getChildrenKeysWithOverride(self, key: str) -> List[str]:
"""Get the keys of all children settings with an override."""
if self._global_container_stack is None:
return []
definitions = self._global_container_stack.definition.findDefinitions(key=key)
@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject):
def settingsWithInheritanceWarning(self) -> List[str]:
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:
"""Check if a setting has an inheritance function that is overwritten"""
has_setting_function = False
if not stack:
stack = self._active_container_stack
@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject):
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
"""Check if the setting has a user state. If not, it is never overwritten."""
if not has_user_state:
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"):
return False
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
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):
return False

View File

@ -15,21 +15,24 @@ from UM.Application import Application
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
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
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
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 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"}
def __init__(self):
@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
return "SettingOverrideInstanceContainer-%s" % uuid.uuid1()
def __deepcopy__(self, memo):
## Create a fresh decorator object
deep_copy = SettingOverrideDecorator()
"""Create a fresh decorator object"""
## Copy the instance
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
"""Copy the instance"""
# A unique name must be added, or replaceContainer will not replace it
instance_container.setMetaDataEntry("id", self._generateUniqueName())
@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator):
return deep_copy
## Gets the currently active extruder to print this object with.
#
# \return An extruder's container stack.
def getActiveExtruder(self):
"""Gets the currently active extruder to print this object with.
:return: An extruder's container 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):
"""Gets the signal that emits if the active extruder changed.
This can then be accessed via a decorator.
"""
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):
"""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
if self.getStack().getProperty("support_mesh", "value"):
global_container_stack = Application.getInstance().getGlobalContainerStack()
@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
## Makes sure that the stack upon which the container stack is placed is
# kept up to date.
def _updateNextStack(self):
"""Makes sure that the stack upon which the container stack is placed is
kept up to date.
"""
if self._extruder_stack:
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
if extruder_stack:
@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
else:
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):
"""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._updateNextStack()
ExtruderManager.getInstance().resetSelectedObjectExtruders()

View File

@ -15,13 +15,15 @@ if TYPE_CHECKING:
from cura.MachineAction import MachineAction
## Raised when trying to add an unknown machine action as a required action
class UnknownMachineActionError(Exception):
"""Raised when trying to add an unknown machine action as a required action"""
pass
## Raised when trying to add a machine action that does not have an unique key.
class NotUniqueMachineActionError(Exception):
"""Raised when trying to add a machine action that does not have an unique key."""
pass
@ -71,9 +73,11 @@ class MachineActionManager(QObject):
self._definition_ids_with_default_actions_added.add(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:
"""Add a required action to a machine
Raises an exception when the action is not recognised.
"""
if action_key in self._machine_actions:
if definition_id in self._required_actions:
if self._machine_actions[action_key] not in self._required_actions[definition_id]:
@ -83,8 +87,9 @@ class MachineActionManager(QObject):
else:
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:
"""Add a supported action to a machine."""
if action_key in self._machine_actions:
if definition_id in self._supported_actions:
if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
@ -94,8 +99,9 @@ class MachineActionManager(QObject):
else:
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:
"""Add an action to the first start list of a machine."""
if action_key in self._machine_actions:
if definition_id in self._first_start_actions:
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
@ -104,57 +110,69 @@ class MachineActionManager(QObject):
else:
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:
"""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:
self._machine_actions[action.getKey()] = action
else:
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")
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:
return list(self._supported_actions[definition_id])
else:
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"]:
"""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:
return self._required_actions[definition_id]
else:
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")
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:
return self._first_start_actions[definition_id]
else:
return []
## Remove Machine action from manager
# \param action to remove
def removeMachineAction(self, action: "MachineAction") -> None:
"""Remove Machine action from manager
:param action: to remove
"""
try:
del self._machine_actions[action.getKey()]
except KeyError:
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"]:
"""Get MachineAction by key
:param key: String of key to select
:return: Machine action if found, None otherwise
"""
if key in self._machine_actions:
return self._machine_actions[key]
else:

View File

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

View File

@ -21,11 +21,13 @@ if TYPE_CHECKING:
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):
"""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"
@ -380,10 +382,12 @@ class PrintInformation(QObject):
def baseName(self):
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:
"""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()
if not global_container_stack:
self._abbr_machine = ""
@ -392,8 +396,9 @@ class PrintInformation(QObject):
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:
"""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')
@pyqtSlot(result = "QVariantMap")
@ -431,6 +436,7 @@ class PrintInformation(QObject):
return
self._change_timer.start()
## Listen to scene changes to check if we need to reset the print information
def _onSceneChanged(self) -> None:
"""Listen to scene changes to check if we need to reset the print information"""
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]+)?$")
## 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:
"""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
if not SEMANTIC_VERSION_REGEX.fullmatch(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])
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
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.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor.
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_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):
"""Test getting quality groups when there are quality profiles available for
the requested configurations on two extruders.
"""
# Prepare a tree inside the machine node.
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.
@ -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"].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):
"""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.
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")

View File

@ -6,7 +6,7 @@ import pytest
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 = [
{
"id": "matching_intent", # Matches our query.

View File

@ -49,12 +49,14 @@ def machine_node():
mocked_machine_node.preferred_material = "preferred_material"
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
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(
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:
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):
"""Tests the preferred material when the exact base file is available in the
materials list for this node.
"""
empty_variant_node.materials = {
"some_different_material": MagicMock(getMetaDataEntry = lambda x: 3),
"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."
## Tests the preferred material when a submaterial is available in the
# materials list for this 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 = {
"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.
@ -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."
## Tests the preferred material matching on the approximate diameter of the
# filament.
def test_preferredMaterialDiameter(empty_variant_node):
"""Tests the preferred material matching on the approximate diameter of the filament.
"""
empty_variant_node.materials = {
"some_different_material": MagicMock(getMetaDataEntry = lambda x: 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."
## Tests the preferred material matching on a different material if the
# diameter is wrong.
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["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.
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):
"""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["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.

View File

@ -5,18 +5,21 @@ import UM.PluginObject
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):
## Initialise a new definition container.
#
# The container will have the specified ID and all metadata in the
# provided dictionary.
"""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.
"""
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__()
if metadata is None:
self._metadata = {}
@ -24,55 +27,69 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject):
self._metadata = metadata
self._plugin_id = "MockContainerPlugin"
## Gets the ID that was provided at initialisation.
#
# \return The ID of the container.
def getId(self):
"""Gets the ID that was provided at initialisation.
:return: The ID of the container.
"""
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):
"""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
## 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):
"""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:
return self._metadata[entry]
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):
"""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")
## Get whether a container stack is enabled or not.
# \return Always returns True.
@property
def isEnabled(self):
"""Get whether a container stack is enabled or not.
:return: Always returns 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):
"""Get whether the container item is stored on a read only location in the filesystem.
:return: Always returns False
"""
return False
## Mock get path
def getPath(self):
"""Mock get path"""
return "/path/to/the/light/side"
## Mock set path
def setPath(self, path):
"""Mock set path"""
pass
def getAllKeys(self):
@ -91,31 +108,38 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject):
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):
"""Get the value of a container item.
Since this mock container cannot contain any items, it always returns None.
:return: Always returns None.
"""
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):
"""Get whether the container item has a specific property.
This method is not implemented in the mock container.
"""
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):
"""Serializes the container to a string representation.
This method is not implemented in the mock container.
"""
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):
"""Deserializes the container from a string representation.
This method is not implemented in the mock container.
"""
raise NotImplementedError()
@classmethod

View File

@ -42,8 +42,9 @@ def test_createUniqueName(container_registry):
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):
"""Tests whether addContainer properly converts to ExtruderStack."""
container_registry.addContainer(definition_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
## Tests whether addContainer properly converts to GlobalStack.
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_changes_container)

View File

@ -57,9 +57,10 @@ def test_noCategory(file_path):
metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id")
assert "category" not in metadata[0]
## Tests all definition containers
@pytest.mark.parametrize("file_path", machine_filepaths)
def test_validateMachineDefinitionContainer(file_path, definition_container):
"""Tests all definition containers"""
file_name = os.path.basename(file_path)
if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
return # Stop checking, these are root files.
@ -85,13 +86,15 @@ def assertIsDefinitionValid(definition_container, file_path):
if "platform_texture" in metadata[0]:
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)
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:
doc = json.load(f)
@ -107,12 +110,14 @@ def test_validateOverridingDefaultValue(file_path: str):
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.
## 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]:
"""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")
with open(definition_path, encoding = "utf-8") as f:
doc = json.load(f)
@ -127,13 +132,15 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
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]:
"""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 = {}
for entry, contents in settings.items():
if "children" in contents:
@ -142,12 +149,16 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
result[entry] = contents
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]:
"""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.update(base)
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
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)
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:
doc = json.load(f)
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)
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]
with open(file_path, encoding = "utf-8") as 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.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:
"""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.setMetaDataEntry("type", container_type)
return container
@ -32,10 +34,12 @@ class InstanceContainerSubClass(InstanceContainer):
super().__init__(container_id = "SubInstanceContainer")
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):
"""Tests whether adding a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
extruder_stack.addContainer(unittest.mock.MagicMock())
@ -164,8 +168,10 @@ def test_constrainDefinitionInvalid(container, extruder_stack):
def test_constrainDefinitionValid(container, extruder_stack):
extruder_stack.definition = container #Should not give an error.
## Tests whether deserialising completes the missing containers with empty ones.
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.
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
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):
"""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.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.
## Tests whether a container with the wrong class gets removed when deserialising.
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.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.
## Tests whether an instance container in the definition spot results in an error.
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.
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.
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):
"""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.Definition] = DefinitionContainer(container_id = "some definition")
@ -218,8 +232,10 @@ def test_deserializeMoveInstanceContainer(extruder_stack):
assert extruder_stack.quality == 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):
"""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.
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.definition != empty_container
## Tests whether getProperty properly applies the stack-like behaviour on its containers.
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
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]
assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges
## Tests whether inserting a container is properly forbidden.
def test_insertContainer(extruder_stack):
"""Tests whether inserting a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
extruder_stack.insertContainer(0, unittest.mock.MagicMock())
## Tests whether removing a container is properly forbidden.
def test_removeContainer(extruder_stack):
"""Tests whether removing a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
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
## 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:
"""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.setMetaDataEntry("type", container_type)
return container
@ -37,17 +39,19 @@ class InstanceContainerSubClass(InstanceContainer):
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):
"""Tests whether adding a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
global_stack.addContainer(unittest.mock.MagicMock())
## Tests adding extruders to the global stack.
def test_addExtruder(global_stack):
"""Tests adding extruders to the global stack."""
mock_definition = unittest.mock.MagicMock()
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.
## 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):
"""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.
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.
## Tests whether an instance container with the wrong type gets removed when deserialising.
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.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.
## Tests whether a container with the wrong class gets removed when deserialising.
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.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.
## Tests whether an instance container in the definition spot results in an error.
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.
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("")
## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.
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.Definition] = DefinitionContainer(container_id = "some definition")
@ -272,8 +283,9 @@ def test_deserializeMoveInstanceContainer(global_stack):
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):
"""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.
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
## Tests whether getProperty properly applies the stack-like behaviour on its containers.
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.
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.
@ -326,8 +339,9 @@ def test_getPropertyFallThrough(global_stack):
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):
"""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.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.
## In definitions, when the value is asked and there is a resolve function, it must get the resolve first.
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.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.
## 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):
"""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
instance_containers = {}
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
## Tests whether the value in instances gets evaluated before the resolve in definitions.
def test_getPropertyInstancesBeforeResolve(global_stack):
"""Tests whether the value in instances gets evaluated before the resolve in definitions."""
def getValueProperty(key, property, context = None):
if key != "material_bed_temperature":
return None
@ -404,8 +421,9 @@ def test_getPropertyInstancesBeforeResolve(global_stack):
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):
"""Tests whether the hasUserValue returns true for settings that are changed in the user-changes container."""
container = unittest.mock.MagicMock()
container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user")
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("")
## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container.
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.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes")
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("")
## Tests whether a container in some other place on the stack is correctly not recognised as user value.
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.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality")
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.
## Tests whether inserting a container is properly forbidden.
def test_insertContainer(global_stack):
"""Tests whether inserting a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
global_stack.insertContainer(0, unittest.mock.MagicMock())
## Tests whether removing a container is properly forbidden.
def test_removeContainer(global_stack):
"""Tests whether removing a container is properly forbidden."""
with pytest.raises(InvalidOperationError):
global_stack.removeContainer(unittest.mock.MagicMock())
## Tests whether changing the next stack is properly forbidden.
def test_setNextStack(global_stack):
"""Tests whether changing the next stack is properly forbidden."""
with pytest.raises(InvalidOperationError):
global_stack.setNextStack(unittest.mock.MagicMock())

View File

@ -61,9 +61,10 @@ variant_filepaths = collectAllVariants()
intent_filepaths = collectAllIntents()
## Attempt to load all the quality profiles.
@pytest.mark.parametrize("file_name", quality_filepaths)
def test_validateQualityProfiles(file_name):
"""Attempt to load all the quality profiles."""
try:
with open(file_name, encoding = "utf-8") as data:
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!
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)
def test_validateVariantProfiles(file_name):
"""Attempt to load all the variant profiles."""
try:
with open(file_name, encoding = "utf-8") as data:
serialized = data.read()

View File

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