Merge pull request #7551 from Ultimaker/doxygen_to_restructuredtext_comments

Converted doxygen style comments to reStructuredText style
This commit is contained in:
Nino van Hooff 2020-05-29 16:46:25 +02:00 committed by GitHub
commit 98587a9008
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
224 changed files with 5521 additions and 3874 deletions

View file

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

View file

@ -25,23 +25,27 @@ class SyncState:
ERROR = 2 ERROR = 2
IDLE = 3 IDLE = 3
## The account API provides a version-proof bridge to use Ultimaker Accounts
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.account.login()
# api.account.logout()
# api.account.userProfile # Who is logged in``
#
class Account(QObject): class Account(QObject):
"""The account API provides a version-proof bridge to use Ultimaker Accounts
Usage:
.. code-block:: python
from cura.API import CuraAPI
api = CuraAPI()
api.account.login()
api.account.logout()
api.account.userProfile # Who is logged in
"""
# The interval in which sync services are automatically triggered # The interval in which sync services are automatically triggered
SYNC_INTERVAL = 30.0 # seconds SYNC_INTERVAL = 30.0 # seconds
Q_ENUMS(SyncState) Q_ENUMS(SyncState)
# Signal emitted when user logged in or out.
loginStateChanged = pyqtSignal(bool) loginStateChanged = pyqtSignal(bool)
"""Signal emitted when user logged in or out"""
accessTokenChanged = pyqtSignal() accessTokenChanged = pyqtSignal()
syncRequested = pyqtSignal() syncRequested = pyqtSignal()
"""Sync services may connect to this signal to receive sync triggers. """Sync services may connect to this signal to receive sync triggers.
@ -140,9 +144,10 @@ class Account(QObject):
def _onAccessTokenChanged(self): def _onAccessTokenChanged(self):
self.accessTokenChanged.emit() self.accessTokenChanged.emit()
## Returns a boolean indicating whether the given authentication is applied against staging or not.
@property @property
def is_staging(self) -> bool: def is_staging(self) -> bool:
"""Indication whether the given authentication is applied against staging or not."""
return "staging" in self._oauth_root return "staging" in self._oauth_root
@pyqtProperty(bool, notify=loginStateChanged) @pyqtProperty(bool, notify=loginStateChanged)
@ -227,10 +232,10 @@ class Account(QObject):
def accessToken(self) -> Optional[str]: def accessToken(self) -> Optional[str]:
return self._authorization_service.getAccessToken() return self._authorization_service.getAccessToken()
# Get the profile of the logged in user
# @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
@pyqtProperty("QVariantMap", notify = loginStateChanged) @pyqtProperty("QVariantMap", notify = loginStateChanged)
def userProfile(self) -> Optional[Dict[str, Optional[str]]]: def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
"""None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """
user_profile = self._authorization_service.getUserProfile() user_profile = self._authorization_service.getUserProfile()
if not user_profile: if not user_profile:
return None return None

View file

@ -8,28 +8,37 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The back-ups API provides a version-proof bridge between Cura's
# BackupManager and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: class Backups:
"""The back-ups API provides a version-proof bridge between Cura's
BackupManager and plug-ins that hook into it.
Usage:
.. code-block:: python
from cura.API import CuraAPI
api = CuraAPI()
api.backups.createBackup()
api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
"""
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
self.manager = BackupsManager(application) self.manager = BackupsManager(application)
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up.
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
"""Create a new back-up using the BackupsManager.
:return: Tuple containing a ZIP file with the back-up data and a dict with metadata about the back-up.
"""
return self.manager.createBackup() return self.manager.createBackup()
## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number.
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
"""Restore a back-up using the BackupsManager.
:param zip_file: A ZIP file containing the actual back-up data.
:param meta_data: Some metadata needed for restoring a back-up, like the Cura version number.
"""
return self.manager.restoreBackup(zip_file, meta_data) return self.manager.restoreBackup(zip_file, meta_data)

View file

@ -7,32 +7,43 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The Interface.Settings API provides a version-proof bridge between Cura's
# (currently) sidebar UI and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.getContextMenuItems()
# data = {
# "name": "My Plugin Action",
# "iconName": "my-plugin-icon",
# "actions": my_menu_actions,
# "menu_item": MyPluginAction(self)
# }
# api.interface.settings.addContextMenuItem(data)``
class Settings: class Settings:
"""The Interface.Settings API provides a version-proof bridge
between Cura's
(currently) sidebar UI and plug-ins that hook into it.
Usage:
.. code-block:: python
from cura.API import CuraAPI
api = CuraAPI()
api.interface.settings.getContextMenuItems()
data = {
"name": "My Plugin Action",
"iconName": "my-plugin-icon",
"actions": my_menu_actions,
"menu_item": MyPluginAction(self)
}
api.interface.settings.addContextMenuItem(data)
"""
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
self.application = application self.application = application
## Add items to the sidebar context menu.
# \param menu_item dict containing the menu item to add.
def addContextMenuItem(self, menu_item: dict) -> None: def addContextMenuItem(self, menu_item: dict) -> None:
"""Add items to the sidebar context menu.
:param menu_item: dict containing the menu item to add.
"""
self.application.addSidebarCustomMenuItem(menu_item) self.application.addSidebarCustomMenuItem(menu_item)
## Get all custom items currently added to the sidebar context menu.
# \return List containing all custom context menu items.
def getContextMenuItems(self) -> list: def getContextMenuItems(self) -> list:
"""Get all custom items currently added to the sidebar context menu.
:return: List containing all custom context menu items.
"""
return self.application.getSidebarCustomMenuItems() return self.application.getSidebarCustomMenuItems()

View file

@ -9,18 +9,22 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The Interface class serves as a common root for the specific API
# methods for each interface element.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.addContextMenuItem()
# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
# # etc.``
class Interface: class Interface:
"""The Interface class serves as a common root for the specific API
methods for each interface element.
Usage:
.. code-block:: python
from cura.API import CuraAPI
api = CuraAPI()
api.interface.settings.addContextMenuItem()
api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
# etc
"""
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
# API methods specific to the settings portion of the UI # API methods specific to the settings portion of the UI

View file

@ -13,13 +13,14 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The official Cura API that plug-ins can use to interact with Cura.
#
# Python does not technically prevent talking to other classes as well, but
# this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable.
class CuraAPI(QObject): class CuraAPI(QObject):
"""The official Cura API that plug-ins can use to interact with Cura.
Python does not technically prevent talking to other classes as well, but this API provides a version-safe
interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can
cause plug-ins to be unstable.
"""
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
__instance = None # type: "CuraAPI" __instance = None # type: "CuraAPI"
@ -40,10 +41,8 @@ class CuraAPI(QObject):
def __init__(self, application: Optional["CuraApplication"] = None) -> None: def __init__(self, application: Optional["CuraApplication"] = None) -> None:
super().__init__(parent = CuraAPI._application) super().__init__(parent = CuraAPI._application)
# Accounts API
self._account = Account(self._application) self._account = Account(self._application)
# Backups API
self._backups = Backups(self._application) self._backups = Backups(self._application)
self._connectionStatus = ConnectionStatus() self._connectionStatus = ConnectionStatus()
@ -56,6 +55,8 @@ class CuraAPI(QObject):
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
def account(self) -> "Account": def account(self) -> "Account":
"""Accounts API"""
return self._account return self._account
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
@ -64,8 +65,12 @@ class CuraAPI(QObject):
@property @property
def backups(self) -> "Backups": def backups(self) -> "Backups":
"""Backups API"""
return self._backups return self._backups
@property @property
def interface(self) -> "Interface": def interface(self) -> "Interface":
"""Interface API"""
return self._interface return self._interface

View file

@ -16,17 +16,20 @@ from collections import namedtuple
import numpy import numpy
import copy import copy
## Return object for bestSpot
LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
"""Return object for bestSpot"""
class Arrange: class Arrange:
""" """
The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find good locations for objects that you try to put
on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic. on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic.
Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance. .. note::
Make sure the scale is the same between :py:class:`cura.Arranging.ShapeArray.ShapeArray` objects and the :py:class:`cura.Arranging.Arrange.Arrange` instance.
""" """
build_volume = None # type: Optional[BuildVolume] build_volume = None # type: Optional[BuildVolume]
def __init__(self, x, y, offset_x, offset_y, scale = 0.5): def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
@ -42,20 +45,20 @@ class Arrange:
self._is_empty = True self._is_empty = True
@classmethod @classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8): def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange":
""" """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance
Helper to create an Arranger instance
Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the
nodes yourself. nodes yourself.
:param scene_root: Root for finding all scene nodes
:param fixed_nodes: Scene nodes to be placed :param scene_root: Root for finding all scene nodes default = None
:param scale: :param fixed_nodes: Scene nodes to be placed default = None
:param x: :param scale: default = 0.5
:param y: :param x: default = 350
:param min_offset: :param y: default = 250
:return: :param min_offset: default = 8
""" """
arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
@ -90,19 +93,21 @@ class Arrange:
arranger.place(0, 0, shape_arr, update_empty = False) arranger.place(0, 0, shape_arr, update_empty = False)
return arranger return arranger
## This resets the optimization for finding location based on size
def resetLastPriority(self): def resetLastPriority(self):
"""This resets the optimization for finding location based on size"""
self._last_priority = 0 self._last_priority = 0
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1): def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
""" """Find placement for a node (using offset shape) and place it (using hull shape)
Find placement for a node (using offset shape) and place it (using hull shape)
:param node: :param node: The node to be placed
:param offset_shape_arr: hapeArray with offset, for placing the shape :param offset_shape_arr: shape array with offset, for placing the shape
:param hull_shape_arr: ShapeArray without offset, used to find location :param hull_shape_arr: shape array without offset, used to find location
:param step: :param step: default = 1
:return: the nodes that should be placed :return: the nodes that should be placed
""" """
best_spot = self.bestSpot( best_spot = self.bestSpot(
hull_shape_arr, start_prio = self._last_priority, step = step) hull_shape_arr, start_prio = self._last_priority, step = step)
x, y = best_spot.x, best_spot.y x, y = best_spot.x, best_spot.y
@ -129,10 +134,8 @@ class Arrange:
return found_spot return found_spot
def centerFirst(self): def centerFirst(self):
""" """Fill priority, center is best. Lower value is better. """
Fill priority, center is best. Lower value is better.
:return:
"""
# Square distance: creates a more round shape # Square distance: creates a more round shape
self._priority = numpy.fromfunction( self._priority = numpy.fromfunction(
lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32) lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
@ -140,23 +143,22 @@ class Arrange:
self._priority_unique_values.sort() self._priority_unique_values.sort()
def backFirst(self): def backFirst(self):
""" """Fill priority, back is best. Lower value is better """
Fill priority, back is best. Lower value is better
:return:
"""
self._priority = numpy.fromfunction( self._priority = numpy.fromfunction(
lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32) lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort() self._priority_unique_values.sort()
def checkShape(self, x, y, shape_arr): def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]:
""" """Return the amount of "penalty points" for polygon, which is the sum of priority
Return the amount of "penalty points" for polygon, which is the sum of priority
:param x: x-coordinate to check shape :param x: x-coordinate to check shape
:param y: :param y: y-coordinate to check shape
:param shape_arr: the ShapeArray object to place :param shape_arr: the shape array object to place
:return: None if occupied :return: None if occupied
""" """
x = int(self._scale * x) x = int(self._scale * x)
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x offset_x = x + self._offset_x + shape_arr.offset_x
@ -180,14 +182,15 @@ class Arrange:
offset_x:offset_x + shape_arr.arr.shape[1]] offset_x:offset_x + shape_arr.arr.shape[1]]
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
def bestSpot(self, shape_arr, start_prio = 0, step = 1): def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion:
""" """Find "best" spot for ShapeArray
Find "best" spot for ShapeArray
:param shape_arr: :param shape_arr: shape array
:param start_prio: Start with this priority value (and skip the ones before) :param start_prio: Start with this priority value (and skip the ones before)
:param step: Slicing value, higher = more skips = faster but less accurate :param step: Slicing value, higher = more skips = faster but less accurate
:return: namedtuple with properties x, y, penalty_points, priority. :return: namedtuple with properties x, y, penalty_points, priority.
""" """
start_idx_list = numpy.where(self._priority_unique_values == start_prio) start_idx_list = numpy.where(self._priority_unique_values == start_prio)
if start_idx_list: if start_idx_list:
try: try:
@ -211,15 +214,16 @@ class Arrange:
return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-( return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
def place(self, x, y, shape_arr, update_empty = True): def place(self, x, y, shape_arr, update_empty = True):
""" """Place the object.
Place the object.
Marks the locations in self._occupied and self._priority Marks the locations in self._occupied and self._priority
:param x: :param x:
:param y: :param y:
:param shape_arr: :param shape_arr:
:param update_empty: updates the _is_empty, used when adding disallowed areas :param update_empty: updates the _is_empty, used when adding disallowed areas
:return:
""" """
x = int(self._scale * x) x = int(self._scale * x)
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x offset_x = x + self._offset_x + shape_arr.offset_x

View file

@ -18,8 +18,9 @@ from cura.Arranging.ShapeArray import ShapeArray
from typing import List from typing import List
## Do arrangements on multiple build plates (aka builtiplexer)
class ArrangeArray: class ArrangeArray:
"""Do arrangements on multiple build plates (aka builtiplexer)"""
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None: def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None:
self._x = x self._x = x
self._y = y self._y = y

View file

@ -11,19 +11,24 @@ if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
## Polygon representation as an array for use with Arrange
class ShapeArray: class ShapeArray:
"""Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`"""
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None: def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
self.arr = arr self.arr = arr
self.offset_x = offset_x self.offset_x = offset_x
self.offset_y = offset_y self.offset_y = offset_y
self.scale = scale self.scale = scale
## Instantiate from a bunch of vertices
# \param vertices
# \param scale scale the coordinates
@classmethod @classmethod
def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray": def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
"""Instantiate from a bunch of vertices
:param vertices:
:param scale: scale the coordinates
:return: a shape array instantiated from a bunch of vertices
"""
# scale # scale
vertices = vertices * scale vertices = vertices * scale
# flip y, x -> x, y # flip y, x -> x, y
@ -44,12 +49,16 @@ class ShapeArray:
arr[0][0] = 1 arr[0][0] = 1
return cls(arr, offset_x, offset_y) return cls(arr, offset_x, offset_y)
## Instantiate an offset and hull ShapeArray from a scene node.
# \param node source node where the convex hull must be present
# \param min_offset offset for the offset ShapeArray
# \param scale scale the coordinates
@classmethod @classmethod
def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]: def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]:
"""Instantiate an offset and hull ShapeArray from a scene node.
:param node: source node where the convex hull must be present
:param min_offset: offset for the offset ShapeArray
:param scale: scale the coordinates
:return: A tuple containing an offset and hull shape array
"""
transform = node._transformation transform = node._transformation
transform_x = transform._data[0][3] transform_x = transform._data[0][3]
transform_y = transform._data[2][3] transform_y = transform._data[2][3]
@ -88,14 +97,19 @@ class ShapeArray:
return offset_shape_arr, hull_shape_arr return offset_shape_arr, hull_shape_arr
## Create np.array with dimensions defined by shape
# Fills polygon defined by vertices with ones, all other values zero
# Only works correctly for convex hull vertices
# Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array
# \param shape numpy format shape, [x-size, y-size]
# \param vertices
@classmethod @classmethod
def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array: def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
"""Create :py:class:`numpy.ndarray` with dimensions defined by shape
Fills polygon defined by vertices with ones, all other values zero
Only works correctly for convex hull vertices
Originally from: `Stackoverflow - generating a filled polygon inside a numpy array <https://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array>`_
:param shape: numpy format shape, [x-size, y-size]
:param vertices:
:return: numpy array with dimensions defined by shape
"""
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
@ -111,16 +125,21 @@ class ShapeArray:
return base_array return base_array
## Return indices that mark one side of the line, used by arrayFromPolygon
# Uses the line defined by p1 and p2 to check array of
# input indices against interpolated value
# Returns boolean array, with True inside and False outside of shape
# Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array
# \param p1 2-tuple with x, y for point 1
# \param p2 2-tuple with x, y for point 2
# \param base_array boolean array to project the line on
@classmethod @classmethod
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]: def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
"""Return indices that mark one side of the line, used by arrayFromPolygon
Uses the line defined by p1 and p2 to check array of
input indices against interpolated value
Returns boolean array, with True inside and False outside of shape
Originally from: `Stackoverflow - generating a filled polygon inside a numpy array <https://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array>`_
:param p1: 2-tuple with x, y for point 1
:param p2: 2-tuple with x, y for point 2
:param base_array: boolean array to project the line on
:return: A numpy array with indices that mark one side of the line
"""
if p1[0] == p2[0] and p1[1] == p2[1]: if p1[0] == p2[0] and p1[1] == p2[1]:
return None return None
idxs = numpy.indices(base_array.shape) # Create 3D array of indices idxs = numpy.indices(base_array.shape) # Create 3D array of indices

View file

@ -18,24 +18,26 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The back-up class holds all data about a back-up.
#
# It is also responsible for reading and writing the zip file to the user data
# folder.
class Backup: class Backup:
# These files should be ignored when making a backup. """The back-up class holds all data about a back-up.
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
It is also responsible for reading and writing the zip file to the user data folder.
"""
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
"""These files should be ignored when making a backup."""
# Re-use translation catalog.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
"""Re-use translation catalog"""
def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
self._application = application self._application = application
self.zip_file = zip_file # type: Optional[bytes] self.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[Dict[str, str]] self.meta_data = meta_data # type: Optional[Dict[str, str]]
## Create a back-up from the current user config folder.
def makeFromCurrent(self) -> None: def makeFromCurrent(self) -> None:
"""Create a back-up from the current user config folder."""
cura_release = self._application.getVersion() cura_release = self._application.getVersion()
version_data_dir = Resources.getDataStoragePath() version_data_dir = Resources.getDataStoragePath()
@ -66,7 +68,7 @@ class Backup:
material_count = len([s for s in files if "materials/" in s]) - 1 material_count = len([s for s in files if "materials/" in s]) - 1
profile_count = len([s for s in files if "quality_changes/" in s]) - 1 profile_count = len([s for s in files if "quality_changes/" in s]) - 1
plugin_count = len([s for s in files if "plugin.json" in s]) plugin_count = len([s for s in files if "plugin.json" in s])
# Store the archive and metadata so the BackupManager can fetch them when needed. # Store the archive and metadata so the BackupManager can fetch them when needed.
self.zip_file = buffer.getvalue() self.zip_file = buffer.getvalue()
self.meta_data = { self.meta_data = {
@ -77,10 +79,13 @@ class Backup:
"plugin_count": str(plugin_count) "plugin_count": str(plugin_count)
} }
## Make a full archive from the given root path with the given name.
# \param root_path The root directory to archive recursively.
# \return The archive as bytes.
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
"""Make a full archive from the given root path with the given name.
:param root_path: The root directory to archive recursively.
:return: The archive as bytes.
"""
ignore_string = re.compile("|".join(self.IGNORED_FILES)) ignore_string = re.compile("|".join(self.IGNORED_FILES))
try: try:
archive = ZipFile(buffer, "w", ZIP_DEFLATED) archive = ZipFile(buffer, "w", ZIP_DEFLATED)
@ -99,13 +104,17 @@ class Backup:
"Could not create archive from user data directory: {}".format(error))) "Could not create archive from user data directory: {}".format(error)))
return None return None
## Show a UI message.
def _showMessage(self, message: str) -> None: def _showMessage(self, message: str) -> None:
"""Show a UI message."""
Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
## Restore this back-up.
# \return Whether we had success or not.
def restore(self) -> bool: def restore(self) -> bool:
"""Restore this back-up.
:return: Whether we had success or not.
"""
if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
# We can restore without the minimum required information. # We can restore without the minimum required information.
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
@ -139,12 +148,14 @@ class Backup:
return extracted return extracted
## Extract the whole archive to the given target path.
# \param archive The archive as ZipFile.
# \param target_path The target path.
# \return Whether we had success or not.
@staticmethod @staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool: def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
"""Extract the whole archive to the given target path.
:param archive: The archive as ZipFile.
:param target_path: The target path.
:return: Whether we had success or not.
"""
# Implement security recommendations: Sanity check on zip files will make it harder to spoof. # Implement security recommendations: Sanity check on zip files will make it harder to spoof.
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication

View file

@ -24,6 +24,7 @@ class BackupsManager:
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
""" """
Get a back-up of the current configuration. Get a back-up of the current configuration.
:return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version). :return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version).
""" """
@ -37,6 +38,7 @@ class BackupsManager:
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
""" """
Restore a back-up from a given ZipFile. Restore a back-up from a given ZipFile.
:param zip_file: A bytes object containing the actual back-up. :param zip_file: A bytes object containing the actual back-up.
:param meta_data: A dict containing some metadata that is needed to restore the back-up correctly. :param meta_data: A dict containing some metadata that is needed to restore the back-up correctly.
""" """

View file

@ -44,8 +44,9 @@ catalog = i18nCatalog("cura")
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
class BuildVolume(SceneNode): class BuildVolume(SceneNode):
"""Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas."""
raftThicknessChanged = Signal() raftThicknessChanged = Signal()
def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None: def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
@ -113,7 +114,7 @@ class BuildVolume(SceneNode):
self._has_errors = False self._has_errors = False
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
#Objects loaded at the moment. We are connected to the property changed events of these objects. # Objects loaded at the moment. We are connected to the property changed events of these objects.
self._scene_objects = set() # type: Set[SceneNode] self._scene_objects = set() # type: Set[SceneNode]
self._scene_change_timer = QTimer() self._scene_change_timer = QTimer()
@ -163,10 +164,12 @@ class BuildVolume(SceneNode):
self._scene_objects = new_scene_objects self._scene_objects = new_scene_objects
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
## Updates the listeners that listen for changes in per-mesh stacks.
#
# \param node The node for which the decorators changed.
def _updateNodeListeners(self, node: SceneNode): def _updateNodeListeners(self, node: SceneNode):
"""Updates the listeners that listen for changes in per-mesh stacks.
:param node: The node for which the decorators changed.
"""
per_mesh_stack = node.callDecoration("getStack") per_mesh_stack = node.callDecoration("getStack")
if per_mesh_stack: if per_mesh_stack:
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged) per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
@ -187,10 +190,14 @@ class BuildVolume(SceneNode):
if shape: if shape:
self._shape = shape self._shape = shape
## Get the length of the 3D diagonal through the build volume.
#
# This gives a sense of the scale of the build volume in general.
def getDiagonalSize(self) -> float: def getDiagonalSize(self) -> float:
"""Get the length of the 3D diagonal through the build volume.
This gives a sense of the scale of the build volume in general.
:return: length of the 3D diagonal through the build volume
"""
return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth) return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
def getDisallowedAreas(self) -> List[Polygon]: def getDisallowedAreas(self) -> List[Polygon]:
@ -226,9 +233,9 @@ class BuildVolume(SceneNode):
return True return True
## For every sliceable node, update node._outside_buildarea
#
def updateNodeBoundaryCheck(self): def updateNodeBoundaryCheck(self):
"""For every sliceable node, update node._outside_buildarea"""
if not self._global_container_stack: if not self._global_container_stack:
return return
@ -295,8 +302,13 @@ class BuildVolume(SceneNode):
for child_node in children: for child_node in children:
child_node.setOutsideBuildArea(group_node.isOutsideBuildArea()) child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
## Update the outsideBuildArea of a single node, given bounds or current build volume
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None: def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None:
"""Update the outsideBuildArea of a single node, given bounds or current build volume
:param node: single node
:param bounds: bounds or current build volume
"""
if not isinstance(node, CuraSceneNode) or self._global_container_stack is None: if not isinstance(node, CuraSceneNode) or self._global_container_stack is None:
return return
@ -484,8 +496,9 @@ class BuildVolume(SceneNode):
self._disallowed_area_size = max(size, self._disallowed_area_size) self._disallowed_area_size = max(size, self._disallowed_area_size)
return mb.build() return mb.build()
## Recalculates the build volume & disallowed areas.
def rebuild(self) -> None: def rebuild(self) -> None:
"""Recalculates the build volume & disallowed areas."""
if not self._width or not self._height or not self._depth: if not self._width or not self._height or not self._depth:
return return
@ -574,7 +587,7 @@ class BuildVolume(SceneNode):
def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float: def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
extra_z = 0.0 extra_z = 0.0
for extruder in extruders: for extruder in extruders:
if extruder.getProperty("retraction_hop_enabled", "value"): if extruder.getProperty("retraction_hop_enabled", "value"):
@ -586,8 +599,9 @@ class BuildVolume(SceneNode):
def _onStackChanged(self): def _onStackChanged(self):
self._stack_change_timer.start() self._stack_change_timer.start()
## Update the build volume visualization
def _onStackChangeTimerFinished(self) -> None: def _onStackChangeTimerFinished(self) -> None:
"""Update the build volume visualization"""
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getActiveExtruderStacks() extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
@ -712,15 +726,15 @@ class BuildVolume(SceneNode):
self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value")
## Calls _updateDisallowedAreas and makes sure the changes appear in the
# scene.
#
# This is required for a signal to trigger the update in one go. The
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
# since there may be other changes before it needs to be rebuilt, which
# would hit performance.
def _updateDisallowedAreasAndRebuild(self): def _updateDisallowedAreasAndRebuild(self):
"""Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene.
This is required for a signal to trigger the update in one go. The
:py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call
:py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt,
which would hit performance.
"""
self._updateDisallowedAreas() self._updateDisallowedAreas()
self._updateRaftThickness() self._updateRaftThickness()
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks()) self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
@ -782,15 +796,14 @@ class BuildVolume(SceneNode):
for extruder_id in result_areas_no_brim: for extruder_id in result_areas_no_brim:
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
## Computes the disallowed areas for objects that are printed with print
# features.
#
# This means that the brim, travel avoidance and such will be applied to
# these features.
#
# \return A dictionary with for each used extruder ID the disallowed areas
# where that extruder may not print.
def _computeDisallowedAreasPrinted(self, used_extruders): def _computeDisallowedAreasPrinted(self, used_extruders):
"""Computes the disallowed areas for objects that are printed with print features.
This means that the brim, travel avoidance and such will be applied to these features.
:return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
"""
result = {} result = {}
adhesion_extruder = None #type: ExtruderStack adhesion_extruder = None #type: ExtruderStack
for extruder in used_extruders: for extruder in used_extruders:
@ -828,18 +841,18 @@ class BuildVolume(SceneNode):
return result return result
## Computes the disallowed areas for the prime blobs.
#
# These are special because they are not subject to things like brim or
# travel avoidance. They do get a dilute with the border size though
# because they may not intersect with brims and such of other objects.
#
# \param border_size The size with which to offset the disallowed areas
# due to skirt, brim, travel avoid distance, etc.
# \param used_extruders The extruder stacks to generate disallowed areas
# for.
# \return A dictionary with for each used extruder ID the prime areas.
def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
"""Computes the disallowed areas for the prime blobs.
These are special because they are not subject to things like brim or travel avoidance. They do get a dilute
with the border size though because they may not intersect with brims and such of other objects.
:param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
, etc.
:param used_extruders: The extruder stacks to generate disallowed areas for.
:return: A dictionary with for each used extruder ID the prime areas.
"""
result = {} # type: Dict[str, List[Polygon]] result = {} # type: Dict[str, List[Polygon]]
if not self._global_container_stack: if not self._global_container_stack:
return result return result
@ -867,19 +880,18 @@ class BuildVolume(SceneNode):
return result return result
## Computes the disallowed areas that are statically placed in the machine.
#
# It computes different disallowed areas depending on the offset of the
# extruder. The resulting dictionary will therefore have an entry for each
# extruder that is used.
#
# \param border_size The size with which to offset the disallowed areas
# due to skirt, brim, travel avoid distance, etc.
# \param used_extruders The extruder stacks to generate disallowed areas
# for.
# \return A dictionary with for each used extruder ID the disallowed areas
# where that extruder may not print.
def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
"""Computes the disallowed areas that are statically placed in the machine.
It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will
therefore have an entry for each extruder that is used.
:param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
, etc.
:param used_extruders: The extruder stacks to generate disallowed areas for.
:return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
"""
# Convert disallowed areas to polygons and dilate them. # Convert disallowed areas to polygons and dilate them.
machine_disallowed_polygons = [] machine_disallowed_polygons = []
if self._global_container_stack is None: if self._global_container_stack is None:
@ -1010,13 +1022,14 @@ class BuildVolume(SceneNode):
return result return result
## Private convenience function to get a setting from every extruder.
#
# For single extrusion machines, this gets the setting from the global
# stack.
#
# \return A sequence of setting values, one for each extruder.
def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]: def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]:
"""Private convenience function to get a setting from every extruder.
For single extrusion machines, this gets the setting from the global stack.
:return: A sequence of setting values, one for each extruder.
"""
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value") all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)): for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
@ -1101,12 +1114,13 @@ class BuildVolume(SceneNode):
return move_from_wall_radius return move_from_wall_radius
## Calculate the disallowed radius around the edge.
#
# This disallowed radius is to allow for space around the models that is
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
# and travel avoid distance.
def getEdgeDisallowedSize(self): def getEdgeDisallowedSize(self):
"""Calculate the disallowed radius around the edge.
This disallowed radius is to allow for space around the models that is not part of the collision radius,
such as bed adhesion (skirt/brim/raft) and travel avoid distance.
"""
if not self._global_container_stack or not self._global_container_stack.extruderList: if not self._global_container_stack or not self._global_container_stack.extruderList:
return 0 return 0

View file

@ -150,8 +150,9 @@ class CrashHandler:
self._sendCrashReport() self._sendCrashReport()
os._exit(1) os._exit(1)
## Backup the current resource directories and create clean ones.
def _backupAndStartClean(self): def _backupAndStartClean(self):
"""Backup the current resource directories and create clean ones."""
Resources.factoryReset() Resources.factoryReset()
self.early_crash_dialog.close() self.early_crash_dialog.close()
@ -162,8 +163,9 @@ class CrashHandler:
def _showDetailedReport(self): def _showDetailedReport(self):
self.dialog.exec_() self.dialog.exec_()
## Creates a modal dialog.
def _createDialog(self): def _createDialog(self):
"""Creates a modal dialog."""
self.dialog.setMinimumWidth(640) self.dialog.setMinimumWidth(640)
self.dialog.setMinimumHeight(640) self.dialog.setMinimumHeight(640)
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
@ -235,7 +237,7 @@ class CrashHandler:
scope.set_tag("locale_os", self.data["locale_os"]) scope.set_tag("locale_os", self.data["locale_os"])
scope.set_tag("locale_cura", self.cura_locale) scope.set_tag("locale_cura", self.cura_locale)
scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion) scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion)
scope.set_user({"id": str(uuid.getnode())}) scope.set_user({"id": str(uuid.getnode())})
return group return group

View file

@ -43,9 +43,10 @@ class CuraActions(QObject):
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {}) event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
## Reset camera position and direction to default
@pyqtSlot() @pyqtSlot()
def homeCamera(self) -> None: def homeCamera(self) -> None:
"""Reset camera position and direction to default"""
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
camera = scene.getActiveCamera() camera = scene.getActiveCamera()
if camera: if camera:
@ -54,9 +55,10 @@ class CuraActions(QObject):
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))
## Center all objects in the selection
@pyqtSlot() @pyqtSlot()
def centerSelection(self) -> None: def centerSelection(self) -> None:
"""Center all objects in the selection"""
operation = GroupedOperation() operation = GroupedOperation()
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
current_node = node current_node = node
@ -73,18 +75,21 @@ class CuraActions(QObject):
operation.addOperation(center_operation) operation.addOperation(center_operation)
operation.push() operation.push()
## Multiply all objects in the selection
#
# \param count The number of times to multiply the selection.
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
"""Multiply all objects in the selection
:param count: The number of times to multiply the selection.
"""
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
job.start() job.start()
## Delete all selected objects.
@pyqtSlot() @pyqtSlot()
def deleteSelection(self) -> None: def deleteSelection(self) -> None:
"""Delete all selected objects."""
if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled(): if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled():
return return
@ -106,11 +111,13 @@ class CuraActions(QObject):
op.push() op.push()
## Set the extruder that should be used to print the selection.
#
# \param extruder_id The ID of the extruder stack to use for the selected objects.
@pyqtSlot(str) @pyqtSlot(str)
def setExtruderForSelection(self, extruder_id: str) -> None: def setExtruderForSelection(self, extruder_id: str) -> None:
"""Set the extruder that should be used to print the selection.
:param extruder_id: The ID of the extruder stack to use for the selected objects.
"""
operation = GroupedOperation() operation = GroupedOperation()
nodes_to_change = [] nodes_to_change = []

View file

@ -261,9 +261,12 @@ class CuraApplication(QtApplication):
def ultimakerCloudAccountRootUrl(self) -> str: def ultimakerCloudAccountRootUrl(self) -> str:
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
# Adds command line options to the command line parser. This should be called after the application is created and
# before the pre-start.
def addCommandLineOptions(self): def addCommandLineOptions(self):
"""Adds command line options to the command line parser.
This should be called after the application is created and before the pre-start.
"""
super().addCommandLineOptions() super().addCommandLineOptions()
self._cli_parser.add_argument("--help", "-h", self._cli_parser.add_argument("--help", "-h",
action = "store_true", action = "store_true",
@ -325,8 +328,9 @@ class CuraApplication(QtApplication):
Logger.log("i", "Single instance commands were sent, exiting") Logger.log("i", "Single instance commands were sent, exiting")
sys.exit(0) sys.exit(0)
# Adds expected directory names and search paths for Resources.
def __addExpectedResourceDirsAndSearchPaths(self): def __addExpectedResourceDirsAndSearchPaths(self):
"""Adds expected directory names and search paths for Resources."""
# this list of dir names will be used by UM to detect an old cura directory # this list of dir names will be used by UM to detect an old cura directory
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]: for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
@ -368,9 +372,12 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("[int]", None, str, None) SettingDefinition.addSettingType("[int]", None, str, None)
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction.
def _initializeSettingFunctions(self): def _initializeSettingFunctions(self):
"""Adds custom property types, settings types, and extra operators (functions).
Whom need to be registered in SettingDefinition and SettingFunction.
"""
self._cura_formula_functions = CuraFormulaFunctions(self) self._cura_formula_functions = CuraFormulaFunctions(self)
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
@ -380,8 +387,9 @@ class CuraApplication(QtApplication):
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex) SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder) SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
# Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None: def __addAllResourcesAndContainerResources(self) -> None:
"""Adds all resources and container related resources."""
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality") Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants") Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
@ -406,8 +414,9 @@ class CuraApplication(QtApplication):
Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware") Resources.addType(self.ResourceTypes.Firmware, "firmware")
# Adds all empty containers.
def __addAllEmptyContainers(self) -> None: def __addAllEmptyContainers(self) -> None:
"""Adds all empty containers."""
# Add empty variant, material and quality containers. # Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # We need them to simplify the switching between materials.
@ -432,9 +441,10 @@ class CuraApplication(QtApplication):
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
# Initializes the version upgrade manager with by providing the paths for each resource type and the latest
# versions.
def __setLatestResouceVersionsForVersionUpgrade(self): def __setLatestResouceVersionsForVersionUpgrade(self):
"""Initializes the version upgrade manager with by providing the paths for each resource type and the latest
versions. """
self._version_upgrade_manager.setCurrentVersions( self._version_upgrade_manager.setCurrentVersions(
{ {
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"), ("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
@ -449,8 +459,9 @@ class CuraApplication(QtApplication):
} }
) )
# Runs preparations that needs to be done before the starting process.
def startSplashWindowPhase(self) -> None: def startSplashWindowPhase(self) -> None:
"""Runs preparations that needs to be done before the starting process."""
super().startSplashWindowPhase() super().startSplashWindowPhase()
if not self.getIsHeadLess(): if not self.getIsHeadLess():
@ -509,7 +520,7 @@ class CuraApplication(QtApplication):
# Set the setting version for Preferences # Set the setting version for Preferences
preferences = self.getPreferences() preferences = self.getPreferences()
preferences.addPreference("metadata/setting_version", 0) preferences.addPreference("metadata/setting_version", 0)
preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. preferences.setValue("metadata/setting_version", self.SettingVersion) # Don't make it equal to the default so that the setting version always gets written to the file.
preferences.addPreference("cura/active_mode", "simple") preferences.addPreference("cura/active_mode", "simple")
@ -613,12 +624,13 @@ class CuraApplication(QtApplication):
def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None: def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
self._confirm_exit_dialog_callback(yes_or_no) self._confirm_exit_dialog_callback(yes_or_no)
## Signal to connect preferences action in QML
showPreferencesWindow = pyqtSignal() showPreferencesWindow = pyqtSignal()
"""Signal to connect preferences action in QML"""
## Show the preferences window
@pyqtSlot() @pyqtSlot()
def showPreferences(self) -> None: def showPreferences(self) -> None:
"""Show the preferences window"""
self.showPreferencesWindow.emit() self.showPreferencesWindow.emit()
# This is called by drag-and-dropping curapackage files. # This is called by drag-and-dropping curapackage files.
@ -636,10 +648,9 @@ class CuraApplication(QtApplication):
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine...")) self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine..."))
super().setGlobalContainerStack(stack) super().setGlobalContainerStack(stack)
## A reusable dialogbox
#
showMessageBox = pyqtSignal(str,str, str, str, int, int, showMessageBox = pyqtSignal(str,str, str, str, int, int,
arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"]) arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"])
"""A reusable dialogbox"""
def messageBox(self, title, text, def messageBox(self, title, text,
informativeText = "", informativeText = "",
@ -717,9 +728,12 @@ class CuraApplication(QtApplication):
def setDefaultPath(self, key, default_path): def setDefaultPath(self, key, default_path):
self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile()) self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile())
## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistry
def _loadPlugins(self) -> None: def _loadPlugins(self) -> None:
"""Handle loading of all plugin types (and the backend explicitly)
:py:class:`Uranium.UM.PluginRegistry`
"""
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion) self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_reader", self._addProfileReader)
@ -743,9 +757,12 @@ class CuraApplication(QtApplication):
self._plugins_loaded = True self._plugins_loaded = True
## Set a short, user-friendly hint about current loading status.
# The way this message is displayed depends on application state
def _setLoadingHint(self, hint: str): def _setLoadingHint(self, hint: str):
"""Set a short, user-friendly hint about current loading status.
The way this message is displayed depends on application state
"""
if self.started: if self.started:
Logger.info(hint) Logger.info(hint)
else: else:
@ -830,12 +847,14 @@ class CuraApplication(QtApplication):
initializationFinished = pyqtSignal() initializationFinished = pyqtSignal()
## Run Cura without GUI elements and interaction (server mode).
def runWithoutGUI(self): def runWithoutGUI(self):
"""Run Cura without GUI elements and interaction (server mode)."""
self.closeSplash() self.closeSplash()
## Run Cura with GUI (desktop mode).
def runWithGUI(self): def runWithGUI(self):
"""Run Cura with GUI (desktop mode)."""
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
controller = self.getController() controller = self.getController()
@ -989,10 +1008,13 @@ class CuraApplication(QtApplication):
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager return self._setting_inheritance_manager
## Get the machine action manager
# We ignore any *args given to this, as we also register the machine manager as qml singleton.
# It wants to give this function an engine and script engine, but we don't care about that.
def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager: def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
"""Get the machine action manager
We ignore any *args given to this, as we also register the machine manager as qml singleton.
It wants to give this function an engine and script engine, but we don't care about that.
"""
return cast(MachineActionManager.MachineActionManager, self._machine_action_manager) return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
@ -1012,8 +1034,9 @@ class CuraApplication(QtApplication):
self._simple_mode_settings_manager = SimpleModeSettingsManager() self._simple_mode_settings_manager = SimpleModeSettingsManager()
return self._simple_mode_settings_manager return self._simple_mode_settings_manager
## Handle Qt events
def event(self, event): def event(self, event):
"""Handle Qt events"""
if event.type() == QEvent.FileOpen: if event.type() == QEvent.FileOpen:
if self._plugins_loaded: if self._plugins_loaded:
self._openFile(event.file()) self._openFile(event.file())
@ -1025,8 +1048,9 @@ class CuraApplication(QtApplication):
def getAutoSave(self) -> Optional[AutoSave]: def getAutoSave(self) -> Optional[AutoSave]:
return self._auto_save return self._auto_save
## Get print information (duration / material used)
def getPrintInformation(self): def getPrintInformation(self):
"""Get print information (duration / material used)"""
return self._print_information return self._print_information
def getQualityProfilesDropDownMenuModel(self, *args, **kwargs): def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
@ -1042,10 +1066,12 @@ class CuraApplication(QtApplication):
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI": def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
return self._cura_API return self._cura_API
## Registers objects for the QML engine to use.
#
# \param engine The QML engine.
def registerObjects(self, engine): def registerObjects(self, engine):
"""Registers objects for the QML engine to use.
:param engine: The QML engine.
"""
super().registerObjects(engine) super().registerObjects(engine)
# global contexts # global contexts
@ -1181,8 +1207,9 @@ class CuraApplication(QtApplication):
if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")): if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")):
self._update_platform_activity_timer.start() self._update_platform_activity_timer.start()
## Update scene bounding box for current build plate
def updatePlatformActivity(self, node = None): def updatePlatformActivity(self, node = None):
"""Update scene bounding box for current build plate"""
count = 0 count = 0
scene_bounding_box = None scene_bounding_box = None
is_block_slicing_node = False is_block_slicing_node = False
@ -1226,9 +1253,10 @@ class CuraApplication(QtApplication):
self._platform_activity = True if count > 0 else False self._platform_activity = True if count > 0 else False
self.activityChanged.emit() self.activityChanged.emit()
## Select all nodes containing mesh data in the scene.
@pyqtSlot() @pyqtSlot()
def selectAll(self): def selectAll(self):
"""Select all nodes containing mesh data in the scene."""
if not self.getController().getToolsEnabled(): if not self.getController().getToolsEnabled():
return return
@ -1247,9 +1275,10 @@ class CuraApplication(QtApplication):
Selection.add(node) Selection.add(node)
## Reset all translation on nodes with mesh data.
@pyqtSlot() @pyqtSlot()
def resetAllTranslation(self): def resetAllTranslation(self):
"""Reset all translation on nodes with mesh data."""
Logger.log("i", "Resetting all scene translations") Logger.log("i", "Resetting all scene translations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
@ -1275,9 +1304,10 @@ class CuraApplication(QtApplication):
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0))) op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0)))
op.push() op.push()
## Reset all transformations on nodes with mesh data.
@pyqtSlot() @pyqtSlot()
def resetAll(self): def resetAll(self):
"""Reset all transformations on nodes with mesh data."""
Logger.log("i", "Resetting all scene transformations") Logger.log("i", "Resetting all scene transformations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
@ -1303,9 +1333,10 @@ class CuraApplication(QtApplication):
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
op.push() op.push()
## Arrange all objects.
@pyqtSlot() @pyqtSlot()
def arrangeObjectsToAllBuildPlates(self) -> None: def arrangeObjectsToAllBuildPlates(self) -> None:
"""Arrange all objects."""
nodes_to_arrange = [] nodes_to_arrange = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
@ -1358,17 +1389,21 @@ class CuraApplication(QtApplication):
nodes_to_arrange.append(node) nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, fixed_nodes = []) self.arrange(nodes_to_arrange, fixed_nodes = [])
## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
"""Arrange a set of nodes given a set of fixed nodes
:param nodes: nodes that we have to place
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
"""
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start() job.start()
## Reload all mesh data on the screen from file.
@pyqtSlot() @pyqtSlot()
def reloadAll(self) -> None: def reloadAll(self) -> None:
"""Reload all mesh data on the screen from file."""
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
has_merged_nodes = False has_merged_nodes = False
@ -1478,8 +1513,9 @@ class CuraApplication(QtApplication):
group_node.setName("MergedMesh") # add a specific name to distinguish this node group_node.setName("MergedMesh") # add a specific name to distinguish this node
## Updates origin position of all merged meshes
def updateOriginOfMergedMeshes(self, _): def updateOriginOfMergedMeshes(self, _):
"""Updates origin position of all merged meshes"""
group_nodes = [] group_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
@ -1597,9 +1633,10 @@ class CuraApplication(QtApplication):
scene from its source file. The function gets all the nodes that exist in the file through the job result, and scene from its source file. The function gets all the nodes that exist in the file through the job result, and
then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node. then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
:param job: The ReadMeshJob running in the background that reads all the meshes in a file :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the
:return: None meshes in a file
""" """
job_result = job.getResult() # nodes that exist inside the file read by this job job_result = job.getResult() # nodes that exist inside the file read by this job
if len(job_result) == 0: if len(job_result) == 0:
Logger.log("e", "Reloading the mesh failed.") Logger.log("e", "Reloading the mesh failed.")
@ -1645,12 +1682,15 @@ class CuraApplication(QtApplication):
def additionalComponents(self): def additionalComponents(self):
return self._additional_components return self._additional_components
## Add a component to a list of components to be reparented to another area in the GUI.
# The actual reparenting is done by the area itself.
# \param area_id \type{str} Identifying name of the area to which the component should be reparented
# \param component \type{QQuickComponent} The component that should be reparented
@pyqtSlot(str, "QVariant") @pyqtSlot(str, "QVariant")
def addAdditionalComponent(self, area_id, component): def addAdditionalComponent(self, area_id: str, component):
"""Add a component to a list of components to be reparented to another area in the GUI.
The actual reparenting is done by the area itself.
:param area_id: dentifying name of the area to which the component should be reparented
:param (QQuickComponent) component: The component that should be reparented
"""
if area_id not in self._additional_components: if area_id not in self._additional_components:
self._additional_components[area_id] = [] self._additional_components[area_id] = []
self._additional_components[area_id].append(component) self._additional_components[area_id].append(component)
@ -1665,10 +1705,13 @@ class CuraApplication(QtApplication):
@pyqtSlot(QUrl, str) @pyqtSlot(QUrl, str)
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
## Open a local file
# \param project_mode How to handle project files. Either None(default): Follow user preference, "open_as_model" or
# "open_as_project". This parameter is only considered if the file is a project file.
def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None): def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None):
"""Open a local file
:param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model"
or "open_as_project". This parameter is only considered if the file is a project file.
"""
if not file.isValid(): if not file.isValid():
return return
@ -1844,9 +1887,8 @@ class CuraApplication(QtApplication):
@pyqtSlot(str, result=bool) @pyqtSlot(str, result=bool)
def checkIsValidProjectFile(self, file_url): def checkIsValidProjectFile(self, file_url):
""" """Checks if the given file URL is a valid project file. """
Checks if the given file URL is a valid project file.
"""
file_path = QUrl(file_url).toLocalFile() file_path = QUrl(file_url).toLocalFile()
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path) workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
if workspace_reader is None: if workspace_reader is None:

View file

@ -24,11 +24,15 @@ class CuraPackageManager(PackageManager):
super().initialize() super().initialize()
## Returns a list of where the package is used
# empty if it is never used.
# It loops through all the package contents and see if some of the ids are used.
# The list consists of 3-tuples: (global_stack, extruder_nr, container_id)
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]: def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
"""Returns a list of where the package is used
It loops through all the package contents and see if some of the ids are used.
:param package_id: package id to search for
:return: empty if it is never used, otherwise a list consisting of 3-tuples
"""
ids = self.getPackageContainerIds(package_id) ids = self.getPackageContainerIds(package_id)
container_stacks = self._application.getContainerRegistry().findContainerStacks() container_stacks = self._application.getContainerRegistry().findContainerStacks()
global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)] global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]

View file

@ -76,7 +76,7 @@ class Layer:
def createMeshOrJumps(self, make_mesh: bool) -> MeshData: def createMeshOrJumps(self, make_mesh: bool) -> MeshData:
builder = MeshBuilder() builder = MeshBuilder()
line_count = 0 line_count = 0
if make_mesh: if make_mesh:
for polygon in self._polygons: for polygon in self._polygons:
@ -87,7 +87,7 @@ class Layer:
# Reserve the necessary space for the data upfront # Reserve the necessary space for the data upfront
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count) builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
for polygon in self._polygons: for polygon in self._polygons:
# Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps. # Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps.
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
@ -96,7 +96,7 @@ class Layer:
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()] points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
# Line types of the points we want to draw # Line types of the points we want to draw
line_types = polygon.types[index_mask] line_types = polygon.types[index_mask]
# Shift the z-axis according to previous implementation. # Shift the z-axis according to previous implementation.
if make_mesh: if make_mesh:
points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01 points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01
@ -118,5 +118,5 @@ class Layer:
f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0) f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0)
builder.addFacesWithColor(f_points, f_indices, f_colors) builder.addFacesWithColor(f_points, f_indices, f_colors)
return builder.build() return builder.build()

View file

@ -3,9 +3,12 @@
from UM.Mesh.MeshData import MeshData from UM.Mesh.MeshData import MeshData
## Class to holds the layer mesh and information about the layers.
# Immutable, use LayerDataBuilder to create one of these.
class LayerData(MeshData): class LayerData(MeshData):
"""Class to holds the layer mesh and information about the layers.
Immutable, use :py:class:`cura.LayerDataBuilder.LayerDataBuilder` to create one of these.
"""
def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None, def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None,
center_position = None, layers=None, element_counts=None, attributes=None): center_position = None, layers=None, element_counts=None, attributes=None):
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,

View file

@ -10,8 +10,9 @@ import numpy
from typing import Dict, Optional from typing import Dict, Optional
## Builder class for constructing a LayerData object
class LayerDataBuilder(MeshBuilder): class LayerDataBuilder(MeshBuilder):
"""Builder class for constructing a :py:class:`cura.LayerData.LayerData` object"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._layers = {} # type: Dict[int, Layer] self._layers = {} # type: Dict[int, Layer]
@ -42,11 +43,13 @@ class LayerDataBuilder(MeshBuilder):
self._layers[layer].setThickness(thickness) self._layers[layer].setThickness(thickness)
## Return the layer data as LayerData.
#
# \param material_color_map: [r, g, b, a] for each extruder row.
# \param line_type_brightness: compatibility layer view uses line type brightness of 0.5
def build(self, material_color_map, line_type_brightness = 1.0): def build(self, material_color_map, line_type_brightness = 1.0):
"""Return the layer data as :py:class:`cura.LayerData.LayerData`.
:param material_color_map: [r, g, b, a] for each extruder row.
:param line_type_brightness: compatibility layer view uses line type brightness of 0.5
"""
vertex_count = 0 vertex_count = 0
index_count = 0 index_count = 0
for layer, data in self._layers.items(): for layer, data in self._layers.items():

View file

@ -7,8 +7,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from cura.LayerData import LayerData from cura.LayerData import LayerData
## Simple decorator to indicate a scene node holds layer data.
class LayerDataDecorator(SceneNodeDecorator): class LayerDataDecorator(SceneNodeDecorator):
"""Simple decorator to indicate a scene node holds layer data."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._layer_data = None # type: Optional[LayerData] self._layer_data = None # type: Optional[LayerData]

View file

@ -26,14 +26,17 @@ class LayerPolygon:
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
## LayerPolygon, used in ProcessSlicedLayersJob
# \param extruder The position of the extruder
# \param line_types array with line_types
# \param data new_points
# \param line_widths array with line widths
# \param line_thicknesses: array with type as index and thickness as value
# \param line_feedrates array with line feedrates
def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
"""LayerPolygon, used in ProcessSlicedLayersJob
:param extruder: The position of the extruder
:param line_types: array with line_types
:param data: new_points
:param line_widths: array with line widths
:param line_thicknesses: array with type as index and thickness as value
:param line_feedrates: array with line feedrates
"""
self._extruder = extruder self._extruder = extruder
self._types = line_types self._types = line_types
for i in range(len(self._types)): for i in range(len(self._types)):
@ -59,21 +62,21 @@ class LayerPolygon:
# re-used and can save alot of memory usage. # re-used and can save alot of memory usage.
self._color_map = LayerPolygon.getColorMap() self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] # type: numpy.ndarray self._colors = self._color_map[self._types] # type: numpy.ndarray
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool) self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None # type: Optional[numpy.ndarray] self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
def buildCache(self) -> None: def buildCache(self) -> None:
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool) self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
self._index_begin = 0 self._index_begin = 0
self._index_end = mesh_line_count self._index_end = mesh_line_count
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool) self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
# Only if the type of line segment changes do we need to add an extra vertex to change colors # Only if the type of line segment changes do we need to add an extra vertex to change colors
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
@ -83,19 +86,22 @@ class LayerPolygon:
self._vertex_begin = 0 self._vertex_begin = 0
self._vertex_end = numpy.sum( self._build_cache_needed_points ) self._vertex_end = numpy.sum( self._build_cache_needed_points )
## Set all the arrays provided by the function caller, representing the LayerPolygon
# The arrays are either by vertex or by indices.
#
# \param vertex_offset : determines where to start and end filling the arrays
# \param index_offset : determines where to start and end filling the arrays
# \param vertices : vertex numpy array to be filled
# \param colors : vertex numpy array to be filled
# \param line_dimensions : vertex numpy array to be filled
# \param feedrates : vertex numpy array to be filled
# \param extruders : vertex numpy array to be filled
# \param line_types : vertex numpy array to be filled
# \param indices : index numpy array to be filled
def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None: def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
"""Set all the arrays provided by the function caller, representing the LayerPolygon
The arrays are either by vertex or by indices.
:param vertex_offset: determines where to start and end filling the arrays
:param index_offset: determines where to start and end filling the arrays
:param vertices: vertex numpy array to be filled
:param colors: vertex numpy array to be filled
:param line_dimensions: vertex numpy array to be filled
:param feedrates: vertex numpy array to be filled
:param extruders: vertex numpy array to be filled
:param line_types: vertex numpy array to be filled
:param indices: index numpy array to be filled
"""
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
self.buildCache() self.buildCache()
@ -105,16 +111,16 @@ class LayerPolygon:
line_mesh_mask = self._build_cache_line_mesh_mask line_mesh_mask = self._build_cache_line_mesh_mask
needed_points_list = self._build_cache_needed_points needed_points_list = self._build_cache_needed_points
# Index to the points we need to represent the line mesh. This is constructed by generating simple # Index to the points we need to represent the line mesh. This is constructed by generating simple
# start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] # start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1]
# Then then the indices for the points we don't need are thrown away based on the pre-calculated list. # Then then the indices for the points we don't need are thrown away based on the pre-calculated list.
index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))] index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
self._vertex_begin += vertex_offset self._vertex_begin += vertex_offset
self._vertex_end += vertex_offset self._vertex_end += vertex_offset
# Points are picked based on the index list to get the vertices needed. # Points are picked based on the index list to get the vertices needed.
vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :] vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
@ -136,14 +142,14 @@ class LayerPolygon:
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
self._index_begin += index_offset self._index_begin += index_offset
self._index_end += index_offset self._index_end += index_offset
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
# When the line type changes the index needs to be increased by 2. # When the line type changes the index needs to be increased by 2.
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
# Each line segment goes from it's starting point p to p+1, offset by the vertex index. # Each line segment goes from it's starting point p to p+1, offset by the vertex index.
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
self._build_cache_line_mesh_mask = None self._build_cache_line_mesh_mask = None
self._build_cache_needed_points = None self._build_cache_needed_points = None
@ -189,7 +195,7 @@ class LayerPolygon:
@property @property
def lineFeedrates(self): def lineFeedrates(self):
return self._line_feedrates return self._line_feedrates
@property @property
def jumpMask(self): def jumpMask(self):
return self._jump_mask return self._jump_mask
@ -202,8 +208,12 @@ class LayerPolygon:
def jumpCount(self): def jumpCount(self):
return self._jump_count return self._jump_count
# Calculate normals for the entire polygon using numpy.
def getNormals(self) -> numpy.ndarray: def getNormals(self) -> numpy.ndarray:
"""Calculate normals for the entire polygon using numpy.
:return: normals for the entire polygon
"""
normals = numpy.copy(self._data) normals = numpy.copy(self._data)
normals[:, 1] = 0.0 # We are only interested in 2D normals normals[:, 1] = 0.0 # We are only interested in 2D normals
@ -229,9 +239,10 @@ class LayerPolygon:
__color_map = None # type: numpy.ndarray __color_map = None # type: numpy.ndarray
## Gets the instance of the VersionUpgradeManager, or creates one.
@classmethod @classmethod
def getColorMap(cls) -> numpy.ndarray: def getColorMap(cls) -> numpy.ndarray:
"""Gets the instance of the VersionUpgradeManager, or creates one."""
if cls.__color_map is None: if cls.__color_map is None:
theme = cast(Theme, QtApplication.getInstance().getTheme()) theme = cast(Theme, QtApplication.getInstance().getTheme())
cls.__color_map = numpy.array([ cls.__color_map = numpy.array([

View file

@ -11,16 +11,22 @@ from UM.PluginObject import PluginObject
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
# updating the firmware, connecting with remote devices or doing bed leveling. A machine action can also have a
# qml, which should contain a "Cura.MachineAction" item. When activated, the item will be displayed in a dialog
# and this object will be added as "manager" (so all pyqtSlot() functions can be called by calling manager.func())
class MachineAction(QObject, PluginObject): class MachineAction(QObject, PluginObject):
"""Machine actions are actions that are added to a specific machine type.
Examples of such actions are updating the firmware, connecting with remote devices or doing bed leveling. A
machine action can also have a qml, which should contain a :py:class:`cura.MachineAction.MachineAction` item.
When activated, the item will be displayed in a dialog and this object will be added as "manager" (so all
pyqtSlot() functions can be called by calling manager.func())
"""
## Create a new Machine action.
# \param key unique key of the machine action
# \param label Human readable label used to identify the machine action.
def __init__(self, key: str, label: str = "") -> None: def __init__(self, key: str, label: str = "") -> None:
"""Create a new Machine action.
:param key: unique key of the machine action
:param label: Human readable label used to identify the machine action.
"""
super().__init__() super().__init__()
self._key = key self._key = key
self._label = label self._label = label
@ -34,10 +40,14 @@ class MachineAction(QObject, PluginObject):
def getKey(self) -> str: def getKey(self) -> str:
return self._key return self._key
## Whether this action needs to ask the user anything.
# If not, we shouldn't present the user with certain screens which otherwise show up.
# Defaults to true to be in line with the old behaviour.
def needsUserInteraction(self) -> bool: def needsUserInteraction(self) -> bool:
"""Whether this action needs to ask the user anything.
If not, we shouldn't present the user with certain screens which otherwise show up.
:return: Defaults to true to be in line with the old behaviour.
"""
return True return True
@pyqtProperty(str, notify = labelChanged) @pyqtProperty(str, notify = labelChanged)
@ -49,17 +59,24 @@ class MachineAction(QObject, PluginObject):
self._label = label self._label = label
self.labelChanged.emit() self.labelChanged.emit()
## Reset the action to it's default state.
# This should not be re-implemented by child classes, instead re-implement _reset.
# /sa _reset
@pyqtSlot() @pyqtSlot()
def reset(self) -> None: def reset(self) -> None:
"""Reset the action to it's default state.
This should not be re-implemented by child classes, instead re-implement _reset.
:py:meth:`cura.MachineAction.MachineAction._reset`
"""
self._finished = False self._finished = False
self._reset() self._reset()
## Protected implementation of reset.
# /sa reset()
def _reset(self) -> None: def _reset(self) -> None:
"""Protected implementation of reset.
See also :py:meth:`cura.MachineAction.MachineAction.reset`
"""
pass pass
@pyqtSlot() @pyqtSlot()
@ -72,8 +89,9 @@ class MachineAction(QObject, PluginObject):
def finished(self) -> bool: def finished(self) -> bool:
return self._finished return self._finished
## Protected helper to create a view object based on provided QML.
def _createViewFromQML(self) -> Optional["QObject"]: def _createViewFromQML(self) -> Optional["QObject"]:
"""Protected helper to create a view object based on provided QML."""
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None: if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId()) Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())

View file

@ -9,47 +9,59 @@ from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
## A node in the container tree. It represents one container.
#
# The container it represents is referenced by its container_id. During normal
# use of the tree, this container is not constructed. Only when parts of the
# tree need to get loaded in the container stack should it get constructed.
class ContainerNode: class ContainerNode:
## Creates a new node for the container tree. """A node in the container tree. It represents one container.
# \param container_id The ID of the container that this node should
# represent. The container it represents is referenced by its container_id. During normal use of the tree, this container is
not constructed. Only when parts of the tree need to get loaded in the container stack should it get constructed.
"""
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
"""Creates a new node for the container tree.
:param container_id: The ID of the container that this node should represent.
"""
self.container_id = container_id self.container_id = container_id
self._container = None # type: Optional[InstanceContainer] self._container = None # type: Optional[InstanceContainer]
self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node. self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node.
## Gets the metadata of the container that this node represents.
# Getting the metadata from the container directly is about 10x as fast.
# \return The metadata of the container in this node.
def getMetadata(self) -> Dict[str, Any]: def getMetadata(self) -> Dict[str, Any]:
"""Gets the metadata of the container that this node represents.
Getting the metadata from the container directly is about 10x as fast.
:return: The metadata of the container in this node.
"""
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0] return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
## Get an entry from the metadata of the container that this node contains.
#
# This is just a convenience function.
# \param entry The metadata entry key to return.
# \param default If the metadata is not present or the container is not
# found, the value of this default is returned.
# \return The value of the metadata entry, or the default if it was not
# present.
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
"""Get an entry from the metadata of the container that this node contains.
This is just a convenience function.
:param entry: The metadata entry key to return.
:param default: If the metadata is not present or the container is not found, the value of this default is
returned.
:return: The value of the metadata entry, or the default if it was not present.
"""
container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id) container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)
if len(container_metadata) == 0: if len(container_metadata) == 0:
return default return default
return container_metadata[0].get(entry, default) return container_metadata[0].get(entry, default)
## The container that this node's container ID refers to.
#
# This can be used to finally instantiate the container in order to put it
# in the container stack.
# \return A container.
@property @property
def container(self) -> Optional[InstanceContainer]: def container(self) -> Optional[InstanceContainer]:
"""The container that this node's container ID refers to.
This can be used to finally instantiate the container in order to put it in the container stack.
:return: A container.
"""
if not self._container: if not self._container:
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id) container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id)
if len(container_list) == 0: if len(container_list) == 0:

View file

@ -19,17 +19,16 @@ if TYPE_CHECKING:
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
## This class contains a look-up tree for which containers are available at
# which stages of configuration.
#
# The tree starts at the machine definitions. For every distinct definition
# there will be one machine node here.
#
# All of the fallbacks for material choices, quality choices, etc. should be
# encoded in this tree. There must always be at least one child node (for
# nodes that have children) but that child node may be a node representing the
# empty instance container.
class ContainerTree: class ContainerTree:
"""This class contains a look-up tree for which containers are available at which stages of configuration.
The tree starts at the machine definitions. For every distinct definition there will be one machine node here.
All of the fallbacks for material choices, quality choices, etc. should be encoded in this tree. There must
always be at least one child node (for nodes that have children) but that child node may be a node representing
the empty instance container.
"""
__instance = None # type: Optional["ContainerTree"] __instance = None # type: Optional["ContainerTree"]
@classmethod @classmethod
@ -43,13 +42,15 @@ class ContainerTree:
self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed. self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed.
cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed. cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed.
## Get the quality groups available for the currently activated printer.
#
# This contains all quality groups, enabled or disabled. To check whether
# the quality group can be activated, test for the
# ``QualityGroup.is_available`` property.
# \return For every quality type, one quality group.
def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]: def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
"""Get the quality groups available for the currently activated printer.
This contains all quality groups, enabled or disabled. To check whether the quality group can be activated,
test for the ``QualityGroup.is_available`` property.
:return: For every quality type, one quality group.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return {} return {}
@ -58,14 +59,15 @@ class ContainerTree:
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled) return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
## Get the quality changes groups available for the currently activated
# printer.
#
# This contains all quality changes groups, enabled or disabled. To check
# whether the quality changes group can be activated, test for the
# ``QualityChangesGroup.is_available`` property.
# \return A list of all quality changes groups.
def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]: def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
"""Get the quality changes groups available for the currently activated printer.
This contains all quality changes groups, enabled or disabled. To check whether the quality changes group can
be activated, test for the ``QualityChangesGroup.is_available`` property.
:return: A list of all quality changes groups.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return [] return []
@ -74,31 +76,43 @@ class ContainerTree:
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled) return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
## Ran after completely starting up the application.
def _onStartupFinished(self) -> None: def _onStartupFinished(self) -> None:
"""Ran after completely starting up the application."""
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks. currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added)) JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
## Dictionary-like object that contains the machines.
#
# This handles the lazy loading of MachineNodes.
class _MachineNodeMap: class _MachineNodeMap:
"""Dictionary-like object that contains the machines.
This handles the lazy loading of MachineNodes.
"""
def __init__(self) -> None: def __init__(self) -> None:
self._machines = {} # type: Dict[str, MachineNode] self._machines = {} # type: Dict[str, MachineNode]
## Returns whether a printer with a certain definition ID exists. This
# is regardless of whether or not the printer is loaded yet.
# \param definition_id The definition to look for.
# \return Whether or not a printer definition exists with that name.
def __contains__(self, definition_id: str) -> bool: def __contains__(self, definition_id: str) -> bool:
"""Returns whether a printer with a certain definition ID exists.
This is regardless of whether or not the printer is loaded yet.
:param definition_id: The definition to look for.
:return: Whether or not a printer definition exists with that name.
"""
return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0 return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0
## Returns a machine node for the specified definition ID.
#
# If the machine node wasn't loaded yet, this will load it lazily.
# \param definition_id The definition to look for.
# \return A machine node for that definition.
def __getitem__(self, definition_id: str) -> MachineNode: def __getitem__(self, definition_id: str) -> MachineNode:
"""Returns a machine node for the specified definition ID.
If the machine node wasn't loaded yet, this will load it lazily.
:param definition_id: The definition to look for.
:return: A machine node for that definition.
"""
if definition_id not in self._machines: if definition_id not in self._machines:
start_time = time.time() start_time = time.time()
self._machines[definition_id] = MachineNode(definition_id) self._machines[definition_id] = MachineNode(definition_id)
@ -106,46 +120,58 @@ class ContainerTree:
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time)) Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
return self._machines[definition_id] return self._machines[definition_id]
## Gets a machine node for the specified definition ID, with default.
#
# The default is returned if there is no definition with the specified
# ID. If the machine node wasn't loaded yet, this will load it lazily.
# \param definition_id The definition to look for.
# \param default The machine node to return if there is no machine
# with that definition (can be ``None`` optionally or if not
# provided).
# \return A machine node for that definition, or the default if there
# is no definition with the provided definition_id.
def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]: def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]:
"""Gets a machine node for the specified definition ID, with default.
The default is returned if there is no definition with the specified ID. If the machine node wasn't
loaded yet, this will load it lazily.
:param definition_id: The definition to look for.
:param default: The machine node to return if there is no machine with that definition (can be ``None``
optionally or if not provided).
:return: A machine node for that definition, or the default if there is no definition with the provided
definition_id.
"""
if definition_id not in self: if definition_id not in self:
return default return default
return self[definition_id] return self[definition_id]
## Returns whether we've already cached this definition's node.
# \param definition_id The definition that we may have cached.
# \return ``True`` if it's cached.
def is_loaded(self, definition_id: str) -> bool: def is_loaded(self, definition_id: str) -> bool:
"""Returns whether we've already cached this definition's node.
:param definition_id: The definition that we may have cached.
:return: ``True`` if it's cached.
"""
return definition_id in self._machines return definition_id in self._machines
## Pre-loads all currently added printers as a background task so that
# switching printers in the interface is faster.
class _MachineNodeLoadJob(Job): class _MachineNodeLoadJob(Job):
## Creates a new background task. """Pre-loads all currently added printers as a background task so that switching printers in the interface is
# \param tree_root The container tree instance. This cannot be faster.
# obtained through the singleton static function since the instance """
# may not yet be constructed completely.
# \param container_stacks All of the stacks to pre-load the container
# trees for. This needs to be provided from here because the stacks
# need to be constructed on the main thread because they are QObject.
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None: def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
"""Creates a new background task.
:param tree_root: The container tree instance. This cannot be obtained through the singleton static
function since the instance may not yet be constructed completely.
:param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided
from here because the stacks need to be constructed on the main thread because they are QObject.
"""
self.tree_root = tree_root self.tree_root = tree_root
self.container_stacks = container_stacks self.container_stacks = container_stacks
super().__init__() super().__init__()
## Starts the background task.
#
# The ``JobQueue`` will schedule this on a different thread.
def run(self) -> None: def run(self) -> None:
"""Starts the background task.
The ``JobQueue`` will schedule this on a different thread.
"""
for stack in self.container_stacks: # Load all currently-added containers. for stack in self.container_stacks: # Load all currently-added containers.
if not isinstance(stack, GlobalStack): if not isinstance(stack, GlobalStack):
continue continue

View file

@ -11,10 +11,12 @@ if TYPE_CHECKING:
from cura.Machines.QualityNode import QualityNode from cura.Machines.QualityNode import QualityNode
## This class represents an intent profile in the container tree.
#
# This class has no more subnodes.
class IntentNode(ContainerNode): class IntentNode(ContainerNode):
"""This class represents an intent profile in the container tree.
This class has no more subnodes.
"""
def __init__(self, container_id: str, quality: "QualityNode") -> None: def __init__(self, container_id: str, quality: "QualityNode") -> None:
super().__init__(container_id) super().__init__(container_id)
self.quality = quality self.quality = quality

View file

@ -13,16 +13,16 @@ from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.Validator import ValidatorState from UM.Settings.Validator import ValidatorState
import cura.CuraApplication import cura.CuraApplication
#
# This class performs setting error checks for the currently active machine.
#
# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
# for it to finish the complete work.
#
class MachineErrorChecker(QObject): class MachineErrorChecker(QObject):
"""This class performs setting error checks for the currently active machine.
The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. The idea
here is to split the whole error check into small tasks, each of which only checks a single setting key in a
stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should be
good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
for it to finish the complete work.
"""
def __init__(self, parent: Optional[QObject] = None) -> None: def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -92,24 +92,37 @@ class MachineErrorChecker(QObject):
def needToWaitForResult(self) -> bool: def needToWaitForResult(self) -> bool:
return self._need_to_check or self._check_in_progress return self._need_to_check or self._check_in_progress
# Start the error check for property changed
# this is seperate from the startErrorCheck because it ignores a number property types
def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None: def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
"""Start the error check for property changed
this is seperate from the startErrorCheck because it ignores a number property types
:param key:
:param property_name:
"""
if property_name != "value": if property_name != "value":
return return
self.startErrorCheck() self.startErrorCheck()
# Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args: Any) -> None: def startErrorCheck(self, *args: Any) -> None:
"""Starts the error check timer to schedule a new error check.
:param args:
"""
if not self._check_in_progress: if not self._check_in_progress:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
self._error_check_timer.start() self._error_check_timer.start()
# This function is called by the timer to reschedule a new error check.
# If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
# to notify the current check to stop and start a new one.
def _rescheduleCheck(self) -> None: def _rescheduleCheck(self) -> None:
"""This function is called by the timer to reschedule a new error check.
If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
to notify the current check to stop and start a new one.
"""
if self._check_in_progress and not self._need_to_check: if self._check_in_progress and not self._need_to_check:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()

View file

@ -17,10 +17,12 @@ from cura.Machines.VariantNode import VariantNode
import UM.FlameProfiler import UM.FlameProfiler
## This class represents a machine in the container tree.
#
# The subnodes of these nodes are variants.
class MachineNode(ContainerNode): class MachineNode(ContainerNode):
"""This class represents a machine in the container tree.
The subnodes of these nodes are variants.
"""
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes. self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes.
@ -47,20 +49,21 @@ class MachineNode(ContainerNode):
self._loadAll() self._loadAll()
## Get the available quality groups for this machine.
#
# This returns all quality groups, regardless of whether they are
# available to the combination of extruders or not. On the resulting
# quality groups, the is_available property is set to indicate whether the
# quality group can be selected according to the combination of extruders
# in the parameters.
# \param variant_names The names of the variants loaded in each extruder.
# \param material_bases The base file names of the materials loaded in
# each extruder.
# \param extruder_enabled Whether or not the extruders are enabled. This
# allows the function to set the is_available properly.
# \return For each available quality type, a QualityGroup instance.
def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]: def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]:
"""Get the available quality groups for this machine.
This returns all quality groups, regardless of whether they are available to the combination of extruders or
not. On the resulting quality groups, the is_available property is set to indicate whether the quality group
can be selected according to the combination of extruders in the parameters.
:param variant_names: The names of the variants loaded in each extruder.
:param material_bases: The base file names of the materials loaded in each extruder.
:param extruder_enabled: Whether or not the extruders are enabled. This allows the function to set the
is_available properly.
:return: For each available quality type, a QualityGroup instance.
"""
if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled): if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled):
Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").") Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").")
return {} return {}
@ -98,28 +101,26 @@ class MachineNode(ContainerNode):
quality_groups[quality_type].is_available = True quality_groups[quality_type].is_available = True
return quality_groups return quality_groups
## Returns all of the quality changes groups available to this printer.
#
# The quality changes groups store which quality type and intent category
# they were made for, but not which material and nozzle. Instead for the
# quality type and intent category, the quality changes will always be
# available but change the quality type and intent category when
# activated.
#
# The quality changes group does depend on the printer: Which quality
# definition is used.
#
# The quality changes groups that are available do depend on the quality
# types that are available, so it must still be known which extruders are
# enabled and which materials and variants are loaded in them. This allows
# setting the correct is_available flag.
# \param variant_names The names of the variants loaded in each extruder.
# \param material_bases The base file names of the materials loaded in
# each extruder.
# \param extruder_enabled For each extruder whether or not they are
# enabled.
# \return List of all quality changes groups for the printer.
def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]: def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]:
"""Returns all of the quality changes groups available to this printer.
The quality changes groups store which quality type and intent category they were made for, but not which
material and nozzle. Instead for the quality type and intent category, the quality changes will always be
available but change the quality type and intent category when activated.
The quality changes group does depend on the printer: Which quality definition is used.
The quality changes groups that are available do depend on the quality types that are available, so it must
still be known which extruders are enabled and which materials and variants are loaded in them. This allows
setting the correct is_available flag.
:param variant_names: The names of the variants loaded in each extruder.
:param material_bases: The base file names of the materials loaded in each extruder.
:param extruder_enabled: For each extruder whether or not they are enabled.
:return: List of all quality changes groups for the printer.
"""
machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder. machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder.
groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group. groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
@ -147,18 +148,19 @@ class MachineNode(ContainerNode):
return list(groups_by_name.values()) return list(groups_by_name.values())
## Gets the preferred global quality node, going by the preferred quality
# type.
#
# If the preferred global quality is not in there, an arbitrary global
# quality is taken.
# If there are no global qualities, an empty quality is returned.
def preferredGlobalQuality(self) -> "QualityNode": def preferredGlobalQuality(self) -> "QualityNode":
"""Gets the preferred global quality node, going by the preferred quality type.
If the preferred global quality is not in there, an arbitrary global quality is taken. If there are no global
qualities, an empty quality is returned.
"""
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values()))) return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
## (Re)loads all variants under this printer.
@UM.FlameProfiler.profile @UM.FlameProfiler.profile
def _loadAll(self) -> None: def _loadAll(self) -> None:
"""(Re)loads all variants under this printer."""
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
if not self.has_variants: if not self.has_variants:
self.variants["empty"] = VariantNode("empty_variant", machine = self) self.variants["empty"] = VariantNode("empty_variant", machine = self)

View file

@ -7,18 +7,21 @@ if TYPE_CHECKING:
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
#
# Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information:
# - name: "generic_abs", root_material_id
# - root_material_node: MaterialNode of "generic_abs"
# - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs",
# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
#
class MaterialGroup: class MaterialGroup:
"""A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information:
- name: "generic_abs", root_material_id
- root_material_node: MaterialNode of "generic_abs"
- derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", so
"generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
"""
__slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list")
def __init__(self, name: str, root_material_node: "MaterialNode") -> None: def __init__(self, name: str, root_material_node: "MaterialNode") -> None:

View file

@ -15,10 +15,12 @@ if TYPE_CHECKING:
from cura.Machines.VariantNode import VariantNode from cura.Machines.VariantNode import VariantNode
## Represents a material in the container tree.
#
# Its subcontainers are quality profiles.
class MaterialNode(ContainerNode): class MaterialNode(ContainerNode):
"""Represents a material in the container tree.
Its subcontainers are quality profiles.
"""
def __init__(self, container_id: str, variant: "VariantNode") -> None: def __init__(self, container_id: str, variant: "VariantNode") -> None:
super().__init__(container_id) super().__init__(container_id)
self.variant = variant self.variant = variant
@ -34,16 +36,16 @@ class MaterialNode(ContainerNode):
container_registry.containerRemoved.connect(self._onRemoved) container_registry.containerRemoved.connect(self._onRemoved)
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged) container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
## Finds the preferred quality for this printer with this material and this
# variant loaded.
#
# If the preferred quality is not available, an arbitrary quality is
# returned. If there is a configuration mistake (like a typo in the
# preferred quality) this returns a random available quality. If there are
# no available qualities, this will return the empty quality node.
# \return The node for the preferred quality, or any arbitrary quality if
# there is no match.
def preferredQuality(self) -> QualityNode: def preferredQuality(self) -> QualityNode:
"""Finds the preferred quality for this printer with this material and this variant loaded.
If the preferred quality is not available, an arbitrary quality is returned. If there is a configuration
mistake (like a typo in the preferred quality) this returns a random available quality. If there are no
available qualities, this will return the empty quality node.
:return: The node for the preferred quality, or any arbitrary quality if there is no match.
"""
for quality_id, quality_node in self.qualities.items(): for quality_id, quality_node in self.qualities.items():
if self.variant.machine.preferred_quality_type == quality_node.quality_type: if self.variant.machine.preferred_quality_type == quality_node.quality_type:
return quality_node return quality_node
@ -107,10 +109,13 @@ class MaterialNode(ContainerNode):
if not self.qualities: if not self.qualities:
self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self) self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self)
## Triggered when any container is removed, but only handles it when the
# container is removed that this node represents.
# \param container The container that was allegedly removed.
def _onRemoved(self, container: ContainerInterface) -> None: def _onRemoved(self, container: ContainerInterface) -> None:
"""Triggered when any container is removed, but only handles it when the container is removed that this node
represents.
:param container: The container that was allegedly removed.
"""
if container.getId() == self.container_id: if container.getId() == self.container_id:
# Remove myself from my parent. # Remove myself from my parent.
if self.base_file in self.variant.materials: if self.base_file in self.variant.materials:
@ -119,13 +124,15 @@ class MaterialNode(ContainerNode):
self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant) self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant)
self.materialChanged.emit(self) self.materialChanged.emit(self)
## Triggered when any metadata changed in any container, but only handles
# it when the metadata of this node is changed.
# \param container The container whose metadata changed.
# \param kwargs Key-word arguments provided when changing the metadata.
# These are ignored. As far as I know they are never provided to this
# call.
def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None: def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None:
"""Triggered when any metadata changed in any container, but only handles it when the metadata of this node is
changed.
:param container: The container whose metadata changed.
:param kwargs: Key-word arguments provided when changing the metadata. These are ignored. As far as I know they
are never provided to this call.
"""
if container.getId() != self.container_id: if container.getId() != self.container_id:
return return

View file

@ -13,11 +13,13 @@ from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
class BaseMaterialsModel(ListModel): class BaseMaterialsModel(ListModel):
"""This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
Those 2 models are used by the material drop down menu to show generic materials and branded materials
separately. The extruder position defined here is being used to bound a menu to the correct extruder. This is
used in the top bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
"""
extruderPositionChanged = pyqtSignal() extruderPositionChanged = pyqtSignal()
enabledChanged = pyqtSignal() enabledChanged = pyqtSignal()
@ -121,10 +123,13 @@ class BaseMaterialsModel(ListModel):
def enabled(self): def enabled(self):
return self._enabled return self._enabled
## Triggered when a list of materials changed somewhere in the container
# tree. This change may trigger an _update() call when the materials
# changed for the configuration that this model is looking for.
def _materialsListChanged(self, material: MaterialNode) -> None: def _materialsListChanged(self, material: MaterialNode) -> None:
"""Triggered when a list of materials changed somewhere in the container
tree. This change may trigger an _update() call when the materials changed for the configuration that this
model is looking for.
"""
if self._extruder_stack is None: if self._extruder_stack is None:
return return
if material.variant.container_id != self._extruder_stack.variant.getId(): if material.variant.container_id != self._extruder_stack.variant.getId():
@ -136,14 +141,15 @@ class BaseMaterialsModel(ListModel):
return return
self._onChanged() self._onChanged()
## Triggered when the list of favorite materials is changed.
def _favoritesChanged(self, material_base_file: str) -> None: def _favoritesChanged(self, material_base_file: str) -> None:
"""Triggered when the list of favorite materials is changed."""
if material_base_file in self._available_materials: if material_base_file in self._available_materials:
self._onChanged() self._onChanged()
## This is an abstract method that needs to be implemented by the specific
# models themselves.
def _update(self): def _update(self):
"""This is an abstract method that needs to be implemented by the specific models themselves. """
self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";")) self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
# Update the available materials (ContainerNode) for the current active machine and extruder setup. # Update the available materials (ContainerNode) for the current active machine and extruder setup.
@ -164,10 +170,10 @@ class BaseMaterialsModel(ListModel):
approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter() approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter()
self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter} self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter}
## This method is used by all material models in the beginning of the
# _update() method in order to prevent errors. It's the same in all models
# so it's placed here for easy access.
def _canUpdate(self): def _canUpdate(self):
"""This method is used by all material models in the beginning of the _update() method in order to prevent
errors. It's the same in all models so it's placed here for easy access. """
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if global_stack is None or not self._enabled: if global_stack is None or not self._enabled:
return False return False
@ -178,9 +184,10 @@ class BaseMaterialsModel(ListModel):
return True return True
## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node): def _createMaterialItem(self, root_material_id, container_node):
"""This is another convenience function which is shared by all material models so it's put here to avoid having
so much duplicated code. """
metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id) metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id)
if not metadata_list: if not metadata_list:
return None return None

View file

@ -14,9 +14,8 @@ if TYPE_CHECKING:
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
## This model is used for the custom profile items in the profile drop down
# menu.
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel): class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
"""This model is used for the custom profile items in the profile drop down menu."""
def __init__(self, parent: Optional["QObject"] = None) -> None: def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent) super().__init__(parent)

View file

@ -9,9 +9,9 @@ if TYPE_CHECKING:
class DiscoveredCloudPrintersModel(ListModel): class DiscoveredCloudPrintersModel(ListModel):
""" """Model used to inform the application about newly added cloud printers, which are discovered from the user's
Model used to inform the application about newly added cloud printers, which are discovered from the user's account account """
"""
DeviceKeyRole = Qt.UserRole + 1 DeviceKeyRole = Qt.UserRole + 1
DeviceNameRole = Qt.UserRole + 2 DeviceNameRole = Qt.UserRole + 2
DeviceTypeRole = Qt.UserRole + 3 DeviceTypeRole = Qt.UserRole + 3
@ -31,18 +31,24 @@ class DiscoveredCloudPrintersModel(ListModel):
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None: def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None:
""" """Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel.
Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel.
Example new_devices entry:
.. code-block:: python
:param new_devices: List of dictionaries which contain information about added cloud printers. Example:
{ {
"key": "YjW8pwGYcaUvaa0YgVyWeFkX3z", "key": "YjW8pwGYcaUvaa0YgVyWeFkX3z",
"name": "NG 001", "name": "NG 001",
"machine_type": "Ultimaker S5", "machine_type": "Ultimaker S5",
"firmware_version": "5.5.12.202001" "firmware_version": "5.5.12.202001"
} }
:param new_devices: List of dictionaries which contain information about added cloud printers.
:return: None :return: None
""" """
self._discovered_cloud_printers_list.extend(new_devices) self._discovered_cloud_printers_list.extend(new_devices)
self._update() self._update()
@ -51,21 +57,21 @@ class DiscoveredCloudPrintersModel(ListModel):
@pyqtSlot() @pyqtSlot()
def clear(self) -> None: def clear(self) -> None:
""" """Clears the contents of the DiscoveredCloudPrintersModel.
Clears the contents of the DiscoveredCloudPrintersModel.
:return: None :return: None
""" """
self._discovered_cloud_printers_list = [] self._discovered_cloud_printers_list = []
self._update() self._update()
self.cloudPrintersDetectedChanged.emit(False) self.cloudPrintersDetectedChanged.emit(False)
def _update(self) -> None: def _update(self) -> None:
""" """Sorts the newly discovered cloud printers by name and then updates the ListModel.
Sorts the newly discovered cloud printers by name and then updates the ListModel.
:return: None :return: None
""" """
items = self._discovered_cloud_printers_list[:] items = self._discovered_cloud_printers_list[:]
items.sort(key = lambda k: k["name"]) items.sort(key = lambda k: k["name"])
self.setItems(items) self.setItems(items)

View file

@ -115,12 +115,11 @@ class DiscoveredPrinter(QObject):
return catalog.i18nc("@label", "Available networked printers") return catalog.i18nc("@label", "Available networked printers")
#
# Discovered printers are all the printers that were found on the network, which provide a more convenient way
# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then
# add that printer to Cura as the active one).
#
class DiscoveredPrintersModel(QObject): class DiscoveredPrintersModel(QObject):
"""Discovered printers are all the printers that were found on the network, which provide a more convenient way to
add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then add
that printer to Cura as the active one).
"""
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -213,7 +212,7 @@ class DiscoveredPrintersModel(QObject):
@pyqtProperty("QVariantMap", notify = discoveredPrintersChanged) @pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]: def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
return self._discovered_printer_by_ip_dict return self._discovered_printer_by_ip_dict
@pyqtProperty("QVariantList", notify = discoveredPrintersChanged) @pyqtProperty("QVariantList", notify = discoveredPrintersChanged)
def discoveredPrinters(self) -> List["DiscoveredPrinter"]: def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
item_list = list( item_list = list(
@ -265,8 +264,14 @@ class DiscoveredPrintersModel(QObject):
del self._discovered_printer_by_ip_dict[ip_address] del self._discovered_printer_by_ip_dict[ip_address]
self.discoveredPrintersChanged.emit() self.discoveredPrintersChanged.emit()
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
# This function invokes the given discovered printer's "create_callback" to do this.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None: def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
"""A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
This function invokes the given discovered printer's "create_callback" to do this
:param discovered_printer:
"""
discovered_printer.create_callback(discovered_printer.getKey()) discovered_printer.create_callback(discovered_printer.getKey())

View file

@ -15,27 +15,27 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Model that holds extruders.
#
# This model is designed for use by any list of extruders, but specifically
# intended for drop-down lists of the current machine's extruders in place of
# settings.
class ExtrudersModel(ListModel): class ExtrudersModel(ListModel):
"""Model that holds extruders.
This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the
current machine's extruders in place of settings.
"""
# The ID of the container stack for the extruder. # The ID of the container stack for the extruder.
IdRole = Qt.UserRole + 1 IdRole = Qt.UserRole + 1
## Human-readable name of the extruder.
NameRole = Qt.UserRole + 2 NameRole = Qt.UserRole + 2
"""Human-readable name of the extruder."""
## Colour of the material loaded in the extruder.
ColorRole = Qt.UserRole + 3 ColorRole = Qt.UserRole + 3
"""Colour of the material loaded in the extruder."""
## Index of the extruder, which is also the value of the setting itself.
#
# An index of 0 indicates the first extruder, an index of 1 the second
# one, and so on. This is the value that will be saved in instance
# containers.
IndexRole = Qt.UserRole + 4 IndexRole = Qt.UserRole + 4
"""Index of the extruder, which is also the value of the setting itself.
An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will
be saved in instance containers. """
# The ID of the definition of the extruder. # The ID of the definition of the extruder.
DefinitionRole = Qt.UserRole + 5 DefinitionRole = Qt.UserRole + 5
@ -50,18 +50,18 @@ class ExtrudersModel(ListModel):
MaterialBrandRole = Qt.UserRole + 9 MaterialBrandRole = Qt.UserRole + 9
ColorNameRole = Qt.UserRole + 10 ColorNameRole = Qt.UserRole + 10
## Is the extruder enabled?
EnabledRole = Qt.UserRole + 11 EnabledRole = Qt.UserRole + 11
"""Is the extruder enabled?"""
## List of colours to display if there is no material or the material has no known
# colour.
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
"""List of colours to display if there is no material or the material has no known colour. """
## Initialises the extruders model, defining the roles and listening for
# changes in the data.
#
# \param parent Parent QtObject of this list.
def __init__(self, parent = None): def __init__(self, parent = None):
"""Initialises the extruders model, defining the roles and listening for changes in the data.
:param parent: Parent QtObject of this list.
"""
super().__init__(parent) super().__init__(parent)
self.addRoleName(self.IdRole, "id") self.addRoleName(self.IdRole, "id")
@ -101,14 +101,15 @@ class ExtrudersModel(ListModel):
def addOptionalExtruder(self): def addOptionalExtruder(self):
return self._add_optional_extruder return self._add_optional_extruder
## Links to the stack-changed signal of the new extruders when an extruder
# is swapped out or added in the current machine.
#
# \param machine_id The machine for which the extruders changed. This is
# filled by the ExtruderManager.extrudersChanged signal when coming from
# that signal. Application.globalContainerStackChanged doesn't fill this
# signal; it's assumed to be the current printer in that case.
def _extrudersChanged(self, machine_id = None): def _extrudersChanged(self, machine_id = None):
"""Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the
current machine.
:param machine_id: The machine for which the extruders changed. This is filled by the
ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged
doesn't fill this signal; it's assumed to be the current printer in that case.
"""
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
if machine_id is not None: if machine_id is not None:
if machine_manager.activeMachine is None: if machine_manager.activeMachine is None:
@ -146,11 +147,13 @@ class ExtrudersModel(ListModel):
def _updateExtruders(self): def _updateExtruders(self):
self._update_extruder_timer.start() self._update_extruder_timer.start()
## Update the list of extruders.
#
# This should be called whenever the list of extruders changes.
@UM.FlameProfiler.profile @UM.FlameProfiler.profile
def __updateExtruders(self): def __updateExtruders(self):
"""Update the list of extruders.
This should be called whenever the list of extruders changes.
"""
extruders_changed = False extruders_changed = False
if self.count != 0: if self.count != 0:

View file

@ -4,16 +4,17 @@
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
import cura.CuraApplication # To listen to changes to the preferences. import cura.CuraApplication # To listen to changes to the preferences.
## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel): class FavoriteMaterialsModel(BaseMaterialsModel):
"""Model that shows the list of favorite materials."""
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged) cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
self._onChanged() self._onChanged()
## Triggered when any preference changes, but only handles it when the list
# of favourites is changed.
def _onFavoritesChanged(self, preference_key: str) -> None: def _onFavoritesChanged(self, preference_key: str) -> None:
"""Triggered when any preference changes, but only handles it when the list of favourites is changed. """
if preference_key != "cura/favorite_materials": if preference_key != "cura/favorite_materials":
return return
self._onChanged() self._onChanged()

View file

@ -11,13 +11,13 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
#
# This model holds all first-start machine actions for the currently active machine. It has 2 roles:
# - title : the title/name of the action
# - content : the QObject of the QML content of the action
# - action : the MachineAction object itself
#
class FirstStartMachineActionsModel(ListModel): class FirstStartMachineActionsModel(ListModel):
"""This model holds all first-start machine actions for the currently active machine. It has 2 roles:
- title : the title/name of the action
- content : the QObject of the QML content of the action
- action : the MachineAction object itself
"""
TitleRole = Qt.UserRole + 1 TitleRole = Qt.UserRole + 1
ContentRole = Qt.UserRole + 2 ContentRole = Qt.UserRole + 2
@ -73,9 +73,10 @@ class FirstStartMachineActionsModel(ListModel):
self._current_action_index += 1 self._current_action_index += 1
self.currentActionIndexChanged.emit() self.currentActionIndexChanged.emit()
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
@pyqtSlot() @pyqtSlot()
def reset(self) -> None: def reset(self) -> None:
"""Resets the current action index to 0 so the wizard panel can show actions from the beginning."""
self._current_action_index = 0 self._current_action_index = 0
self.currentActionIndexChanged.emit() self.currentActionIndexChanged.emit()

View file

@ -43,8 +43,9 @@ class GlobalStacksModel(ListModel):
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._updateDelayed() self._updateDelayed()
## Handler for container added/removed events from registry
def _onContainerChanged(self, container) -> None: def _onContainerChanged(self, container) -> None:
"""Handler for container added/removed events from registry"""
# We only need to update when the added / removed container GlobalStack # We only need to update when the added / removed container GlobalStack
if isinstance(container, GlobalStack): if isinstance(container, GlobalStack):
self._updateDelayed() self._updateDelayed()

View file

@ -18,9 +18,9 @@ from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Lists the intent categories that are available for the current printer
# configuration.
class IntentCategoryModel(ListModel): class IntentCategoryModel(ListModel):
"""Lists the intent categories that are available for the current printer configuration. """
NameRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 1
IntentCategoryRole = Qt.UserRole + 2 IntentCategoryRole = Qt.UserRole + 2
WeightRole = Qt.UserRole + 3 WeightRole = Qt.UserRole + 3
@ -31,10 +31,12 @@ class IntentCategoryModel(ListModel):
_translations = collections.OrderedDict() # type: "collections.OrderedDict[str,Dict[str,Optional[str]]]" _translations = collections.OrderedDict() # type: "collections.OrderedDict[str,Dict[str,Optional[str]]]"
# Translations to user-visible string. Ordered by weight.
# TODO: Create a solution for this name and weight to be used dynamically.
@classmethod @classmethod
def _get_translations(cls): def _get_translations(cls):
"""Translations to user-visible string. Ordered by weight.
TODO: Create a solution for this name and weight to be used dynamically.
"""
if len(cls._translations) == 0: if len(cls._translations) == 0:
cls._translations["default"] = { cls._translations["default"] = {
"name": catalog.i18nc("@label", "Default") "name": catalog.i18nc("@label", "Default")
@ -53,9 +55,12 @@ class IntentCategoryModel(ListModel):
} }
return cls._translations return cls._translations
## Creates a new model for a certain intent category.
# \param The category to list the intent profiles for.
def __init__(self, intent_category: str) -> None: def __init__(self, intent_category: str) -> None:
"""Creates a new model for a certain intent category.
:param intent_category: category to list the intent profiles for.
"""
super().__init__() super().__init__()
self._intent_category = intent_category self._intent_category = intent_category
@ -84,16 +89,18 @@ class IntentCategoryModel(ListModel):
self.update() self.update()
## Updates the list of intents if an intent profile was added or removed.
def _onContainerChange(self, container: "ContainerInterface") -> None: def _onContainerChange(self, container: "ContainerInterface") -> None:
"""Updates the list of intents if an intent profile was added or removed."""
if container.getMetaDataEntry("type") == "intent": if container.getMetaDataEntry("type") == "intent":
self.update() self.update()
def update(self): def update(self):
self._update_timer.start() self._update_timer.start()
## Updates the list of intents.
def _update(self) -> None: def _update(self) -> None:
"""Updates the list of intents."""
available_categories = IntentManager.getInstance().currentAvailableIntentCategories() available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
result = [] result = []
for category in available_categories: for category in available_categories:
@ -109,9 +116,9 @@ class IntentCategoryModel(ListModel):
result.sort(key = lambda k: k["weight"]) result.sort(key = lambda k: k["weight"])
self.setItems(result) self.setItems(result)
## Get a display value for a category.
## for categories and keys
@staticmethod @staticmethod
def translation(category: str, key: str, default: Optional[str] = None): def translation(category: str, key: str, default: Optional[str] = None):
"""Get a display value for a category.for categories and keys"""
display_strings = IntentCategoryModel._get_translations().get(category, {}) display_strings = IntentCategoryModel._get_translations().get(category, {})
return display_strings.get(key, default) return display_strings.get(key, default)

View file

@ -98,8 +98,9 @@ class IntentModel(ListModel):
new_items = sorted(new_items, key = lambda x: x["layer_height"]) new_items = sorted(new_items, key = lambda x: x["layer_height"])
self.setItems(new_items) self.setItems(new_items)
## Get the active materials for all extruders. No duplicates will be returned
def _getActiveMaterials(self) -> Set["MaterialNode"]: def _getActiveMaterials(self) -> Set["MaterialNode"]:
"""Get the active materials for all extruders. No duplicates will be returned"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return set() return set()

View file

@ -19,28 +19,31 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Proxy class to the materials page in the preferences.
#
# This class handles the actions in that page, such as creating new materials,
# renaming them, etc.
class MaterialManagementModel(QObject): class MaterialManagementModel(QObject):
## Triggered when a favorite is added or removed. """Proxy class to the materials page in the preferences.
# \param The base file of the material is provided as parameter when this
# emits. This class handles the actions in that page, such as creating new materials, renaming them, etc.
favoritesChanged = pyqtSignal(str) """
favoritesChanged = pyqtSignal(str)
"""Triggered when a favorite is added or removed.
:param The base file of the material is provided as parameter when this emits
"""
## Can a certain material be deleted, or is it still in use in one of the
# container stacks anywhere?
#
# We forbid the user from deleting a material if it's in use in any stack.
# Deleting it while it's in use can lead to corrupted stacks. In the
# future we might enable this functionality again (deleting the material
# from those stacks) but for now it is easier to prevent the user from
# doing this.
# \param material_node The ContainerTree node of the material to check.
# \return Whether or not the material can be removed.
@pyqtSlot("QVariant", result = bool) @pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool: def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
"""Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
We forbid the user from deleting a material if it's in use in any stack. Deleting it while it's in use can
lead to corrupted stacks. In the future we might enable this functionality again (deleting the material from
those stacks) but for now it is easier to prevent the user from doing this.
:param material_node: The ContainerTree node of the material to check.
:return: Whether or not the material can be removed.
"""
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)} ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"): for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
@ -48,11 +51,14 @@ class MaterialManagementModel(QObject):
return False return False
return True return True
## Change the user-visible name of a material.
# \param material_node The ContainerTree node of the material to rename.
# \param name The new name for the material.
@pyqtSlot("QVariant", str) @pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
"""Change the user-visible name of a material.
:param material_node: The ContainerTree node of the material to rename.
:param name: The new name for the material.
"""
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
root_material_id = material_node.base_file root_material_id = material_node.base_file
if container_registry.isReadOnly(root_material_id): if container_registry.isReadOnly(root_material_id):
@ -60,18 +66,20 @@ class MaterialManagementModel(QObject):
return return
return container_registry.findContainers(id = root_material_id)[0].setName(name) return container_registry.findContainers(id = root_material_id)[0].setName(name)
## Deletes a material from Cura.
#
# This function does not do any safety checking any more. Please call this
# function only if:
# - The material is not read-only.
# - The material is not used in any stacks.
# If the material was not lazy-loaded yet, this will fully load the
# container. When removing this material node, all other materials with
# the same base fill will also be removed.
# \param material_node The material to remove.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode") -> None: def removeMaterial(self, material_node: "MaterialNode") -> None:
"""Deletes a material from Cura.
This function does not do any safety checking any more. Please call this function only if:
- The material is not read-only.
- The material is not used in any stacks.
If the material was not lazy-loaded yet, this will fully load the container. When removing this material
node, all other materials with the same base fill will also be removed.
:param material_node: The material to remove.
"""
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file) materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
@ -89,17 +97,19 @@ class MaterialManagementModel(QObject):
for material_metadata in materials_this_base_file: for material_metadata in materials_this_base_file:
container_registry.removeContainer(material_metadata["id"]) container_registry.removeContainer(material_metadata["id"])
## Creates a duplicate of a material with the same GUID and base_file
# metadata.
# \param base_file: The base file of the material to duplicate.
# \param new_base_id A new material ID for the base material. The IDs of
# the submaterials will be based off this one. If not provided, a material
# ID will be generated automatically.
# \param new_metadata Metadata for the new material. If not provided, this
# will be duplicated from the original material.
# \return The root material ID of the duplicate material.
def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None, def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None,
new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
"""Creates a duplicate of a material with the same GUID and base_file metadata
:param base_file: The base file of the material to duplicate.
:param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this
one. If not provided, a material ID will be generated automatically.
:param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original
material.
:return: The root material ID of the duplicate material.
"""
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
root_materials = container_registry.findContainers(id = base_file) root_materials = container_registry.findContainers(id = base_file)
@ -171,29 +181,32 @@ class MaterialManagementModel(QObject):
return new_base_id return new_base_id
## Creates a duplicate of a material with the same GUID and base_file
# metadata.
# \param material_node The node representing the material to duplicate.
# \param new_base_id A new material ID for the base material. The IDs of
# the submaterials will be based off this one. If not provided, a material
# ID will be generated automatically.
# \param new_metadata Metadata for the new material. If not provided, this
# will be duplicated from the original material.
# \return The root material ID of the duplicate material.
@pyqtSlot("QVariant", result = str) @pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None,
new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
"""Creates a duplicate of a material with the same GUID and base_file metadata
:param material_node: The node representing the material to duplicate.
:param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this
one. If not provided, a material ID will be generated automatically.
:param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original
material.
:return: The root material ID of the duplicate material.
"""
return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata) return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
## Create a new material by cloning the preferred material for the current
# material diameter and generate a new GUID.
#
# The material type is explicitly left to be the one from the preferred
# material, since this allows the user to still have SOME profiles to work
# with.
# \return The ID of the newly created material.
@pyqtSlot(result = str) @pyqtSlot(result = str)
def createMaterial(self) -> str: def createMaterial(self) -> str:
"""Create a new material by cloning the preferred material for the current material diameter and generate a new
GUID.
The material type is explicitly left to be the one from the preferred material, since this allows the user to
still have SOME profiles to work with.
:return: The ID of the newly created material.
"""
# Ensure all settings are saved. # Ensure all settings are saved.
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
application.saveSettings() application.saveSettings()
@ -218,10 +231,13 @@ class MaterialManagementModel(QObject):
self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata) self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
return new_id return new_id
## Adds a certain material to the favorite materials.
# \param material_base_file The base file of the material to add.
@pyqtSlot(str) @pyqtSlot(str)
def addFavorite(self, material_base_file: str) -> None: def addFavorite(self, material_base_file: str) -> None:
"""Adds a certain material to the favorite materials.
:param material_base_file: The base file of the material to add.
"""
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
if material_base_file not in favorites: if material_base_file not in favorites:
@ -230,11 +246,13 @@ class MaterialManagementModel(QObject):
application.saveSettings() application.saveSettings()
self.favoritesChanged.emit(material_base_file) self.favoritesChanged.emit(material_base_file)
## Removes a certain material from the favorite materials.
#
# If the material was not in the favorite materials, nothing happens.
@pyqtSlot(str) @pyqtSlot(str)
def removeFavorite(self, material_base_file: str) -> None: def removeFavorite(self, material_base_file: str) -> None:
"""Removes a certain material from the favorite materials.
If the material was not in the favorite materials, nothing happens.
"""
application = cura.CuraApplication.CuraApplication.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
try: try:

View file

@ -9,11 +9,11 @@ from UM.Scene.Selection import Selection
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
#
# This is the model for multi build plate feature.
# This has nothing to do with the build plate types you can choose on the sidebar for a machine.
#
class MultiBuildPlateModel(ListModel): class MultiBuildPlateModel(ListModel):
"""This is the model for multi build plate feature.
This has nothing to do with the build plate types you can choose on the sidebar for a machine.
"""
maxBuildPlateChanged = pyqtSignal() maxBuildPlateChanged = pyqtSignal()
activeBuildPlateChanged = pyqtSignal() activeBuildPlateChanged = pyqtSignal()
@ -39,9 +39,10 @@ class MultiBuildPlateModel(ListModel):
self._max_build_plate = max_build_plate self._max_build_plate = max_build_plate
self.maxBuildPlateChanged.emit() self.maxBuildPlateChanged.emit()
## Return the highest build plate number
@pyqtProperty(int, notify = maxBuildPlateChanged) @pyqtProperty(int, notify = maxBuildPlateChanged)
def maxBuildPlate(self): def maxBuildPlate(self):
"""Return the highest build plate number"""
return self._max_build_plate return self._max_build_plate
def setActiveBuildPlate(self, nr): def setActiveBuildPlate(self, nr):

View file

@ -26,10 +26,9 @@ if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
#
# This the QML model for the quality management page.
#
class QualityManagementModel(ListModel): class QualityManagementModel(ListModel):
"""This the QML model for the quality management page."""
NameRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 1
IsReadOnlyRole = Qt.UserRole + 2 IsReadOnlyRole = Qt.UserRole + 2
QualityGroupRole = Qt.UserRole + 3 QualityGroupRole = Qt.UserRole + 3
@ -74,11 +73,13 @@ class QualityManagementModel(ListModel):
def _onChange(self) -> None: def _onChange(self) -> None:
self._update_timer.start() self._update_timer.start()
## Deletes a custom profile. It will be gone forever.
# \param quality_changes_group The quality changes group representing the
# profile to delete.
@pyqtSlot(QObject) @pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
"""Deletes a custom profile. It will be gone forever.
:param quality_changes_group: The quality changes group representing the profile to delete.
"""
Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name)) Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
removed_quality_changes_ids = set() removed_quality_changes_ids = set()
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
@ -95,16 +96,19 @@ class QualityManagementModel(ListModel):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids: if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = empty_quality_changes_container extruder_stack.qualityChanges = empty_quality_changes_container
## Rename a custom profile.
#
# Because the names must be unique, the new name may not actually become
# the name that was given. The actual name is returned by this function.
# \param quality_changes_group The custom profile that must be renamed.
# \param new_name The desired name for the profile.
# \return The actual new name of the profile, after making the name
# unique.
@pyqtSlot(QObject, str, result = str) @pyqtSlot(QObject, str, result = str)
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str: def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
"""Rename a custom profile.
Because the names must be unique, the new name may not actually become the name that was given. The actual
name is returned by this function.
:param quality_changes_group: The custom profile that must be renamed.
:param new_name: The desired name for the profile.
:return: The actual new name of the profile, after making the name unique.
"""
Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name)) Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
if new_name == quality_changes_group.name: if new_name == quality_changes_group.name:
Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name)) Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
@ -138,13 +142,16 @@ class QualityManagementModel(ListModel):
return new_name return new_name
## Duplicates a given quality profile OR quality changes profile.
# \param new_name The desired name of the new profile. This will be made
# unique, so it might end up with a different name.
# \param quality_model_item The item of this model to duplicate, as
# dictionary. See the descriptions of the roles of this list model.
@pyqtSlot(str, "QVariantMap") @pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None: def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
"""Duplicates a given quality profile OR quality changes profile.
:param new_name: The desired name of the new profile. This will be made unique, so it might end up with a
different name.
:param quality_model_item: The item of this model to duplicate, as dictionary. See the descriptions of the
roles of this list model.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.") Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
@ -170,18 +177,18 @@ class QualityManagementModel(ListModel):
new_id = container_registry.uniqueName(container.getId()) new_id = container_registry.uniqueName(container.getId())
container_registry.addContainer(container.duplicate(new_id, new_name)) container_registry.addContainer(container.duplicate(new_id, new_name))
## Create quality changes containers from the user containers in the active
# stacks.
#
# This will go through the global and extruder stacks and create
# quality_changes containers from the user containers in each stack. These
# then replace the quality_changes containers in the stack and clear the
# user settings.
# \param base_name The new name for the quality changes profile. The final
# name of the profile might be different from this, because it needs to be
# made unique.
@pyqtSlot(str) @pyqtSlot(str)
def createQualityChanges(self, base_name: str) -> None: def createQualityChanges(self, base_name: str) -> None:
"""Create quality changes containers from the user containers in the active stacks.
This will go through the global and extruder stacks and create quality_changes containers from the user
containers in each stack. These then replace the quality_changes containers in the stack and clear the user
settings.
:param base_name: The new name for the quality changes profile. The final name of the profile might be
different from this, because it needs to be made unique.
"""
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
@ -220,14 +227,16 @@ class QualityManagementModel(ListModel):
container_registry.addContainer(new_changes) container_registry.addContainer(new_changes)
## Create a quality changes container with the given set-up.
# \param quality_type The quality type of the new container.
# \param intent_category The intent category of the new container.
# \param new_name The name of the container. This name must be unique.
# \param machine The global stack to create the profile for.
# \param extruder_stack The extruder stack to create the profile for. If
# not provided, only a global container will be created.
def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer": def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
"""Create a quality changes container with the given set-up.
:param quality_type: The quality type of the new container.
:param intent_category: The intent category of the new container.
:param new_name: The name of the container. This name must be unique.
:param machine: The global stack to create the profile for.
:param extruder_stack: The extruder stack to create the profile for. If not provided, only a global container will be created.
"""
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId() base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
new_id = base_id + "_" + new_name new_id = base_id + "_" + new_name
@ -253,11 +262,13 @@ class QualityManagementModel(ListModel):
quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion) quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
return quality_changes return quality_changes
## Triggered when any container changed.
#
# This filters the updates to the container manager: When it applies to
# the list of quality changes, we need to update our list.
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None: def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
"""Triggered when any container changed.
This filters the updates to the container manager: When it applies to the list of quality changes, we need to
update our list.
"""
if container.getMetaDataEntry("type") == "quality_changes": if container.getMetaDataEntry("type") == "quality_changes":
self._update() self._update()
@ -366,18 +377,19 @@ class QualityManagementModel(ListModel):
self.setItems(item_list) self.setItems(item_list)
# TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later.
#
## Gets a list of the possible file filters that the plugins have
# registered they can read or write. The convenience meta-filters
# "All Supported Types" and "All Files" are added when listing
# readers, but not when listing writers.
#
# \param io_type \type{str} name of the needed IO type
# \return A list of strings indicating file name filters for a file
# dialog.
@pyqtSlot(str, result = "QVariantList") @pyqtSlot(str, result = "QVariantList")
def getFileNameFilters(self, io_type): def getFileNameFilters(self, io_type):
"""Gets a list of the possible file filters that the plugins have registered they can read or write.
The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers,
but not when listing writers.
:param io_type: name of the needed IO type
:return: A list of strings indicating file name filters for a file dialog.
TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later.
"""
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("uranium") catalog = i18nCatalog("uranium")
#TODO: This function should be in UM.Resources! #TODO: This function should be in UM.Resources!
@ -394,9 +406,11 @@ class QualityManagementModel(ListModel):
filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers. filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers.
return filters return filters
## Gets a list of profile reader or writer plugins
# \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type): def _getIOPlugins(self, io_type):
"""Gets a list of profile reader or writer plugins
:return: List of tuples of (plugin_id, meta_data).
"""
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
pr = PluginRegistry.getInstance() pr = PluginRegistry.getInstance()
active_plugin_ids = pr.getActivePlugins() active_plugin_ids = pr.getActivePlugins()

View file

@ -10,10 +10,9 @@ from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
#
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
#
class QualityProfilesDropDownMenuModel(ListModel): class QualityProfilesDropDownMenuModel(ListModel):
"""QML Model for all built-in quality profiles. This model is used for the drop-down quality menu."""
NameRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 1
QualityTypeRole = Qt.UserRole + 2 QualityTypeRole = Qt.UserRole + 2
LayerHeightRole = Qt.UserRole + 3 LayerHeightRole = Qt.UserRole + 3

View file

@ -10,10 +10,9 @@ from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
#
# This model is used to show details settings of the selected quality in the quality management page.
#
class QualitySettingsModel(ListModel): class QualitySettingsModel(ListModel):
"""This model is used to show details settings of the selected quality in the quality management page."""
KeyRole = Qt.UserRole + 1 KeyRole = Qt.UserRole + 1
LabelRole = Qt.UserRole + 2 LabelRole = Qt.UserRole + 2
UnitRole = Qt.UserRole + 3 UnitRole = Qt.UserRole + 3

View file

@ -6,12 +6,12 @@ from typing import Any, Dict, Optional
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
## Data struct to group several quality changes instance containers together.
#
# Each group represents one "custom profile" as the user sees it, which
# contains an instance container for the global stack and one instance
# container per extruder.
class QualityChangesGroup(QObject): class QualityChangesGroup(QObject):
"""Data struct to group several quality changes instance containers together.
Each group represents one "custom profile" as the user sees it, which contains an instance container for the
global stack and one instance container per extruder.
"""
def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None: def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None:
super().__init__(parent) super().__init__(parent)

View file

@ -9,28 +9,34 @@ from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
## A QualityGroup represents a group of quality containers that must be applied
# to each ContainerStack when it's used.
#
# A concrete example: When there are two extruders and the user selects the
# quality type "normal", this quality type must be applied to all stacks in a
# machine, although each stack can have different containers. So one global
# profile gets put on the global stack and one extruder profile gets put on
# each extruder stack. This quality group then contains the following
# profiles (for instance):
# GlobalStack ExtruderStack 1 ExtruderStack 2
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
#
# The purpose of these quality groups is to group the containers that can be
# applied to a configuration, so that when a quality level is selected, the
# container can directly be applied to each stack instead of looking them up
# again.
class QualityGroup: class QualityGroup:
## Constructs a new group. """A QualityGroup represents a group of quality containers that must be applied to each ContainerStack when it's
# \param name The user-visible name for the group. used.
# \param quality_type The quality level that each profile in this group
# has. A concrete example: When there are two extruders and the user selects the quality type "normal", this quality
type must be applied to all stacks in a machine, although each stack can have different containers. So one global
profile gets put on the global stack and one extruder profile gets put on each extruder stack. This quality group
then contains the following profiles (for instance):
- GlobalStack
- ExtruderStack 1
- ExtruderStack 2
quality container:
- um3_global_normal
- um3_aa04_pla_normal
- um3_aa04_abs_normal
The purpose of these quality groups is to group the containers that can be applied to a configuration,
so that when a quality level is selected, the container can directly be applied to each stack instead of looking
them up again.
"""
def __init__(self, name: str, quality_type: str) -> None: def __init__(self, name: str, quality_type: str) -> None:
"""Constructs a new group.
:param name: The user-visible name for the group.
:param quality_type: The quality level that each profile in this group has.
"""
self.name = name self.name = name
self.node_for_global = None # type: Optional[ContainerNode] self.node_for_global = None # type: Optional[ContainerNode]
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]

View file

@ -13,12 +13,14 @@ if TYPE_CHECKING:
from cura.Machines.MachineNode import MachineNode from cura.Machines.MachineNode import MachineNode
## Represents a quality profile in the container tree.
#
# This may either be a normal quality profile or a global quality profile.
#
# Its subcontainers are intent profiles.
class QualityNode(ContainerNode): class QualityNode(ContainerNode):
"""Represents a quality profile in the container tree.
This may either be a normal quality profile or a global quality profile.
Its subcontainers are intent profiles.
"""
def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None: def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None:
super().__init__(container_id) super().__init__(container_id)
self.parent = parent self.parent = parent

View file

@ -17,16 +17,16 @@ if TYPE_CHECKING:
from cura.Machines.MachineNode import MachineNode from cura.Machines.MachineNode import MachineNode
## This class represents an extruder variant in the container tree.
#
# The subnodes of these nodes are materials.
#
# This node contains materials with ALL filament diameters underneath it. The
# tree of this variant is not specific to one global stack, so because the
# list of materials can be different per stack depending on the compatible
# material diameter setting, we cannot filter them here. Filtering must be
# done in the model.
class VariantNode(ContainerNode): class VariantNode(ContainerNode):
"""This class represents an extruder variant in the container tree.
The subnodes of these nodes are materials.
This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific
to one global stack, so because the list of materials can be different per stack depending on the compatible
material diameter setting, we cannot filter them here. Filtering must be done in the model.
"""
def __init__(self, container_id: str, machine: "MachineNode") -> None: def __init__(self, container_id: str, machine: "MachineNode") -> None:
super().__init__(container_id) super().__init__(container_id)
self.machine = machine self.machine = machine
@ -39,9 +39,10 @@ class VariantNode(ContainerNode):
container_registry.containerRemoved.connect(self._materialRemoved) container_registry.containerRemoved.connect(self._materialRemoved)
self._loadAll() self._loadAll()
## (Re)loads all materials under this variant.
@UM.FlameProfiler.profile @UM.FlameProfiler.profile
def _loadAll(self) -> None: def _loadAll(self) -> None:
"""(Re)loads all materials under this variant."""
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
if not self.machine.has_materials: if not self.machine.has_materials:
@ -69,29 +70,29 @@ class VariantNode(ContainerNode):
if not self.materials: if not self.materials:
self.materials["empty_material"] = MaterialNode("empty_material", variant = self) self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
## Finds the preferred material for this printer with this nozzle in one of
# the extruders.
#
# If the preferred material is not available, an arbitrary material is
# returned. If there is a configuration mistake (like a typo in the
# preferred material) this returns a random available material. If there
# are no available materials, this will return the empty material node.
# \param approximate_diameter The desired approximate diameter of the
# material.
# \return The node for the preferred material, or any arbitrary material
# if there is no match.
def preferredMaterial(self, approximate_diameter: int) -> MaterialNode: def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
"""Finds the preferred material for this printer with this nozzle in one of the extruders.
If the preferred material is not available, an arbitrary material is returned. If there is a configuration
mistake (like a typo in the preferred material) this returns a random available material. If there are no
available materials, this will return the empty material node.
:param approximate_diameter: The desired approximate diameter of the material.
:return: The node for the preferred material, or any arbitrary material if there is no match.
"""
for base_material, material_node in self.materials.items(): for base_material, material_node in self.materials.items():
if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
return material_node return material_node
# First fallback: Check if we should be checking for the 175 variant. # First fallback: Check if we should be checking for the 175 variant.
if approximate_diameter == 2: if approximate_diameter == 2:
preferred_material = self.machine.preferred_material + "_175" preferred_material = self.machine.preferred_material + "_175"
for base_material, material_node in self.materials.items(): for base_material, material_node in self.materials.items():
if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
return material_node return material_node
# Second fallback: Choose any material with matching diameter. # Second fallback: Choose any material with matching diameter.
for material_node in self.materials.values(): for material_node in self.materials.values():
if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
@ -107,10 +108,10 @@ class VariantNode(ContainerNode):
)) ))
return fallback return fallback
## When a material gets added to the set of profiles, we need to update our
# tree here.
@UM.FlameProfiler.profile @UM.FlameProfiler.profile
def _materialAdded(self, container: ContainerInterface) -> None: def _materialAdded(self, container: ContainerInterface) -> None:
"""When a material gets added to the set of profiles, we need to update our tree here."""
if container.getMetaDataEntry("type") != "material": if container.getMetaDataEntry("type") != "material":
return # Not interested. return # Not interested.
if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()): if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):

View file

@ -16,23 +16,27 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
## Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers: class AuthorizationHelpers:
"""Class containing several helpers to deal with the authorization flow."""
def __init__(self, settings: "OAuth2Settings") -> None: def __init__(self, settings: "OAuth2Settings") -> None:
self._settings = settings self._settings = settings
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
@property @property
## The OAuth2 settings object.
def settings(self) -> "OAuth2Settings": def settings(self) -> "OAuth2Settings":
"""The OAuth2 settings object."""
return self._settings return self._settings
## Request the access token from the authorization server.
# \param authorization_code: The authorization code from the 1st step.
# \param verification_code: The verification code needed for the PKCE
# extension.
# \return An AuthenticationResponse object.
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
"""Request the access token from the authorization server.
:param authorization_code: The authorization code from the 1st step.
:param verification_code: The verification code needed for the PKCE extension.
:return: An AuthenticationResponse object.
"""
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
@ -46,10 +50,13 @@ class AuthorizationHelpers:
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
## Request the access token from the authorization server using a refresh token.
# \param refresh_token:
# \return An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
"""Request the access token from the authorization server using a refresh token.
:param refresh_token:
:return: An AuthenticationResponse object.
"""
Logger.log("d", "Refreshing the access token.") Logger.log("d", "Refreshing the access token.")
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -64,10 +71,13 @@ class AuthorizationHelpers:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
@staticmethod @staticmethod
## Parse the token response from the authorization server into an AuthenticationResponse object.
# \param token_response: The JSON string data response from the authorization server.
# \return An AuthenticationResponse object.
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
"""Parse the token response from the authorization server into an AuthenticationResponse object.
:param token_response: The JSON string data response from the authorization server.
:return: An AuthenticationResponse object.
"""
token_data = None token_data = None
try: try:
@ -89,10 +99,13 @@ class AuthorizationHelpers:
scope=token_data["scope"], scope=token_data["scope"],
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
## Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token.
# \return Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]: def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
"""Calls the authentication API endpoint to get the token data.
:param access_token: The encoded JWT token.
:return: Dict containing some profile data.
"""
try: try:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
"Authorization": "Bearer {}".format(access_token) "Authorization": "Bearer {}".format(access_token)
@ -115,16 +128,22 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
## Generate a verification code of arbitrary length.
# \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
# leave it at 32
def generateVerificationCode(code_length: int = 32) -> str: def generateVerificationCode(code_length: int = 32) -> str:
"""Generate a verification code of arbitrary length.
:param code_length:: How long should the code be? This should never be lower than 16, but it's probably
better to leave it at 32
"""
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod
## Generates a base64 encoded sha512 encrypted version of a given string.
# \param verification_code:
# \return The encrypted code in base64 format.
def generateVerificationCodeChallenge(verification_code: str) -> str: def generateVerificationCodeChallenge(verification_code: str) -> str:
"""Generates a base64 encoded sha512 encrypted version of a given string.
:param verification_code:
:return: The encrypted code in base64 format.
"""
encoded = sha512(verification_code.encode()).digest() encoded = sha512(verification_code.encode()).digest()
return b64encode(encoded, altchars = b"_-").decode() return b64encode(encoded, altchars = b"_-").decode()

View file

@ -14,9 +14,12 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## This handler handles all HTTP requests on the local web server.
# It also requests the access token for the 2nd stage of the OAuth flow.
class AuthorizationRequestHandler(BaseHTTPRequestHandler): class AuthorizationRequestHandler(BaseHTTPRequestHandler):
"""This handler handles all HTTP requests on the local web server.
It also requests the access token for the 2nd stage of the OAuth flow.
"""
def __init__(self, request, client_address, server) -> None: def __init__(self, request, client_address, server) -> None:
super().__init__(request, client_address, server) super().__init__(request, client_address, server)
@ -55,10 +58,13 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# This will cause the server to shut down, so we do it at the very end of the request handling. # This will cause the server to shut down, so we do it at the very end of the request handling.
self.authorization_callback(token_response) self.authorization_callback(token_response)
## Handler for the callback URL redirect.
# \param query Dict containing the HTTP query parameters.
# \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
"""Handler for the callback URL redirect.
:param query: Dict containing the HTTP query parameters.
:return: HTTP ResponseData containing a success page to show to the user.
"""
code = self._queryGet(query, "code") code = self._queryGet(query, "code")
state = self._queryGet(query, "state") state = self._queryGet(query, "state")
if state != self.state: if state != self.state:
@ -95,9 +101,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
), token_response ), token_response
## Handle all other non-existing server calls.
@staticmethod @staticmethod
def _handleNotFound() -> ResponseData: def _handleNotFound() -> ResponseData:
"""Handle all other non-existing server calls."""
return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.") return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.")
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None: def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
@ -110,7 +117,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
def _sendData(self, data: bytes) -> None: def _sendData(self, data: bytes) -> None:
self.wfile.write(data) self.wfile.write(data)
## Convenience helper for getting values from a pre-parsed query string
@staticmethod @staticmethod
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]: def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]:
"""Convenience helper for getting values from a pre-parsed query string"""
return query_data.get(key, [default])[0] return query_data.get(key, [default])[0]

View file

@ -9,21 +9,26 @@ if TYPE_CHECKING:
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
## The authorization request callback handler server.
# This subclass is needed to be able to pass some data to the request handler.
# This cannot be done on the request handler directly as the HTTPServer
# creates an instance of the handler after init.
class AuthorizationRequestServer(HTTPServer): class AuthorizationRequestServer(HTTPServer):
## Set the authorization helpers instance on the request handler. """The authorization request callback handler server.
This subclass is needed to be able to pass some data to the request handler. This cannot be done on the request
handler directly as the HTTPServer creates an instance of the handler after init.
"""
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None: def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
"""Set the authorization helpers instance on the request handler."""
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
## Set the authorization callback on the request handler.
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None: def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
"""Set the authorization callback on the request handler."""
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None: def setVerificationCode(self, verification_code: str) -> None:
"""Set the verification code on the request handler."""
self.RequestHandlerClass.verification_code = verification_code # type: ignore self.RequestHandlerClass.verification_code = verification_code # type: ignore
def setState(self, state: str) -> None: def setState(self, state: str) -> None:

View file

@ -26,9 +26,11 @@ if TYPE_CHECKING:
MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff" MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff"
## The authorization service is responsible for handling the login flow,
# storing user credentials and providing account information.
class AuthorizationService: class AuthorizationService:
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
account information.
"""
# Emit signal when authentication is completed. # Emit signal when authentication is completed.
onAuthStateChanged = Signal() onAuthStateChanged = Signal()
@ -60,11 +62,16 @@ class AuthorizationService:
if self._preferences: if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
## Get the user profile as obtained from the JWT (JSON Web Token).
# If the JWT is not yet parsed, calling this will take care of that.
# \return UserProfile if a user is logged in, None otherwise.
# \sa _parseJWT
def getUserProfile(self) -> Optional["UserProfile"]: def getUserProfile(self) -> Optional["UserProfile"]:
"""Get the user profile as obtained from the JWT (JSON Web Token).
If the JWT is not yet parsed, calling this will take care of that.
:return: UserProfile if a user is logged in, None otherwise.
See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT`
"""
if not self._user_profile: if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT. # If no user profile was stored locally, we try to get it from JWT.
try: try:
@ -82,9 +89,12 @@ class AuthorizationService:
return self._user_profile return self._user_profile
## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
# \return UserProfile if it was able to parse, None otherwise.
def _parseJWT(self) -> Optional["UserProfile"]: def _parseJWT(self) -> Optional["UserProfile"]:
"""Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
:return: UserProfile if it was able to parse, None otherwise.
"""
if not self._auth_data or self._auth_data.access_token is None: if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again. # If no auth data exists, we should always log in again.
Logger.log("d", "There was no auth data or access token") Logger.log("d", "There was no auth data or access token")
@ -107,8 +117,9 @@ class AuthorizationService:
self._storeAuthData(self._auth_data) self._storeAuthData(self._auth_data)
return self._auth_helpers.parseJWT(self._auth_data.access_token) return self._auth_helpers.parseJWT(self._auth_data.access_token)
## Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]: def getAccessToken(self) -> Optional[str]:
"""Get the access token as provided by the repsonse data."""
if self._auth_data is None: if self._auth_data is None:
Logger.log("d", "No auth data to retrieve the access_token from") Logger.log("d", "No auth data to retrieve the access_token from")
return None return None
@ -123,8 +134,9 @@ class AuthorizationService:
return self._auth_data.access_token if self._auth_data else None return self._auth_data.access_token if self._auth_data else None
## Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None: def refreshAccessToken(self) -> None:
"""Try to refresh the access token. This should be used when it has expired."""
if self._auth_data is None or self._auth_data.refresh_token is None: if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.log("w", "Unable to refresh access token, since there is no refresh token.") Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
return return
@ -136,14 +148,16 @@ class AuthorizationService:
Logger.log("w", "Failed to get a new access token from the server.") Logger.log("w", "Failed to get a new access token from the server.")
self.onAuthStateChanged.emit(logged_in = False) self.onAuthStateChanged.emit(logged_in = False)
## Delete the authentication data that we have stored locally (eg; logout)
def deleteAuthData(self) -> None: def deleteAuthData(self) -> None:
"""Delete the authentication data that we have stored locally (eg; logout)"""
if self._auth_data is not None: if self._auth_data is not None:
self._storeAuthData() self._storeAuthData()
self.onAuthStateChanged.emit(logged_in = False) self.onAuthStateChanged.emit(logged_in = False)
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None: def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None:
"""Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login."""
Logger.log("d", "Starting new OAuth2 flow...") Logger.log("d", "Starting new OAuth2 flow...")
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2. # Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
@ -197,8 +211,9 @@ class AuthorizationService:
auth_url = "{}?next={}".format(MYCLOUD_LOGOFF_URL, quote_plus(auth_url)) auth_url = "{}?next={}".format(MYCLOUD_LOGOFF_URL, quote_plus(auth_url))
return auth_url return auth_url
## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
"""Callback method for the authentication flow."""
if auth_response.success: if auth_response.success:
self._storeAuthData(auth_response) self._storeAuthData(auth_response)
self.onAuthStateChanged.emit(logged_in = True) self.onAuthStateChanged.emit(logged_in = True)
@ -206,8 +221,9 @@ class AuthorizationService:
self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message) self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
self._server.stop() # Stop the web server at all times. self._server.stop() # Stop the web server at all times.
## Load authentication data from preferences.
def loadAuthDataFromPreferences(self) -> None: def loadAuthDataFromPreferences(self) -> None:
"""Load authentication data from preferences."""
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to load authentication data, since no preference has been set!") Logger.log("e", "Unable to load authentication data, since no preference has been set!")
return return
@ -228,13 +244,14 @@ class AuthorizationService:
except ValueError: except ValueError:
Logger.logException("w", "Could not load auth data from preferences") Logger.logException("w", "Could not load auth data from preferences")
## Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None: def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
"""Store authentication data in preferences."""
Logger.log("d", "Attempting to store the auth data") Logger.log("d", "Attempting to store the auth data")
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!") Logger.log("e", "Unable to save authentication data, since no preference has been set!")
return return
self._auth_data = auth_data self._auth_data = auth_data
if auth_data: if auth_data:
self._user_profile = self.getUserProfile() self._user_profile = self.getUserProfile()

View file

@ -20,18 +20,23 @@ if TYPE_CHECKING:
class LocalAuthorizationServer: class LocalAuthorizationServer:
## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
# Once the flow is completed, this server should be closed down again by
# calling stop()
# \param auth_helpers An instance of the authorization helpers class.
# \param auth_state_changed_callback A callback function to be called when
# the authorization state changes.
# \param daemon Whether the server thread should be run in daemon mode.
# Note: Daemon threads are abruptly stopped at shutdown. Their resources
# (e.g. open files) may never be released.
def __init__(self, auth_helpers: "AuthorizationHelpers", def __init__(self, auth_helpers: "AuthorizationHelpers",
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
daemon: bool) -> None: daemon: bool) -> None:
"""The local LocalAuthorizationServer takes care of the oauth2 callbacks.
Once the flow is completed, this server should be closed down again by calling
:py:meth:`cura.OAuth2.LocalAuthorizationServer.LocalAuthorizationServer.stop()`
:param auth_helpers: An instance of the authorization helpers class.
:param auth_state_changed_callback: A callback function to be called when the authorization state changes.
:param daemon: Whether the server thread should be run in daemon mode.
.. note::
Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released.
"""
self._web_server = None # type: Optional[AuthorizationRequestServer] self._web_server = None # type: Optional[AuthorizationRequestServer]
self._web_server_thread = None # type: Optional[threading.Thread] self._web_server_thread = None # type: Optional[threading.Thread]
self._web_server_port = auth_helpers.settings.CALLBACK_PORT self._web_server_port = auth_helpers.settings.CALLBACK_PORT
@ -39,10 +44,13 @@ class LocalAuthorizationServer:
self._auth_state_changed_callback = auth_state_changed_callback self._auth_state_changed_callback = auth_state_changed_callback
self._daemon = daemon self._daemon = daemon
## Starts the local web server to handle the authorization callback.
# \param verification_code The verification code part of the OAuth2 client identification.
# \param state The unique state code (to ensure that the request we get back is really from the server.
def start(self, verification_code: str, state: str) -> None: def start(self, verification_code: str, state: str) -> None:
"""Starts the local web server to handle the authorization callback.
:param verification_code: The verification code part of the OAuth2 client identification.
:param state: The unique state code (to ensure that the request we get back is really from the server.
"""
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though. # We still inject the new verification code though.
@ -66,8 +74,9 @@ class LocalAuthorizationServer:
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
self._web_server_thread.start() self._web_server_thread.start()
## Stops the web server if it was running. It also does some cleanup.
def stop(self) -> None: def stop(self) -> None:
"""Stops the web server if it was running. It also does some cleanup."""
Logger.log("d", "Stopping local oauth2 web server...") Logger.log("d", "Stopping local oauth2 web server...")
if self._web_server: if self._web_server:

View file

@ -8,8 +8,9 @@ class BaseModel:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
## OAuth OAuth2Settings data template.
class OAuth2Settings(BaseModel): class OAuth2Settings(BaseModel):
"""OAuth OAuth2Settings data template."""
CALLBACK_PORT = None # type: Optional[int] CALLBACK_PORT = None # type: Optional[int]
OAUTH_SERVER_URL = None # type: Optional[str] OAUTH_SERVER_URL = None # type: Optional[str]
CLIENT_ID = None # type: Optional[str] CLIENT_ID = None # type: Optional[str]
@ -20,16 +21,18 @@ class OAuth2Settings(BaseModel):
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
## User profile data template.
class UserProfile(BaseModel): class UserProfile(BaseModel):
"""User profile data template."""
user_id = None # type: Optional[str] user_id = None # type: Optional[str]
username = None # type: Optional[str] username = None # type: Optional[str]
profile_image_url = None # type: Optional[str] profile_image_url = None # type: Optional[str]
## Authentication data template.
class AuthenticationResponse(BaseModel): class AuthenticationResponse(BaseModel):
"""Data comes from the token response with success flag and error message added.""" """Authentication data template."""
# Data comes from the token response with success flag and error message added.
success = True # type: bool success = True # type: bool
token_type = None # type: Optional[str] token_type = None # type: Optional[str]
access_token = None # type: Optional[str] access_token = None # type: Optional[str]
@ -40,22 +43,25 @@ class AuthenticationResponse(BaseModel):
received_at = None # type: Optional[str] received_at = None # type: Optional[str]
## Response status template.
class ResponseStatus(BaseModel): class ResponseStatus(BaseModel):
"""Response status template."""
code = 200 # type: int code = 200 # type: int
message = "" # type: str message = "" # type: str
## Response data template.
class ResponseData(BaseModel): class ResponseData(BaseModel):
"""Response data template."""
status = None # type: ResponseStatus status = None # type: ResponseStatus
data_stream = None # type: Optional[bytes] data_stream = None # type: Optional[bytes]
redirect_uri = None # type: Optional[str] redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str content_type = "text/html" # type: str
## Possible HTTP responses.
HTTP_STATUS = { HTTP_STATUS = {
"""Possible HTTP responses."""
"OK": ResponseStatus(code = 200, message = "OK"), "OK": ResponseStatus(code = 200, message = "OK"),
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT") "REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")

View file

@ -7,18 +7,21 @@ from UM.Scene.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key from functools import cmp_to_key
## Iterator that returns a list of nodes in the order that they need to be printed
# If there is no solution an empty list is returned.
# Take note that the list of nodes can have children (that may or may not contain mesh data)
class OneAtATimeIterator(Iterator.Iterator): class OneAtATimeIterator(Iterator.Iterator):
"""Iterator that returns a list of nodes in the order that they need to be printed
If there is no solution an empty list is returned.
Take note that the list of nodes can have children (that may or may not contain mesh data)
"""
def __init__(self, scene_node) -> None: def __init__(self, scene_node) -> None:
super().__init__(scene_node) # Call super to make multiple inheritance work. super().__init__(scene_node) # Call super to make multiple inheritance work.
self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which. self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which.
self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions. self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions.
## Fills the ``_node_stack`` with a list of scene nodes that need to be
# printed in order.
def _fillStack(self) -> None: def _fillStack(self) -> None:
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode): if not issubclass(type(node), SceneNode):
@ -75,10 +78,14 @@ class OneAtATimeIterator(Iterator.Iterator):
return True return True
return False return False
## Check for a node whether it hits any of the other nodes.
# \param node The node to check whether it collides with the other nodes.
# \param other_nodes The nodes to check for collisions.
def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
"""Check for a node whether it hits any of the other nodes.
:param node: The node to check whether it collides with the other nodes.
:param other_nodes: The nodes to check for collisions.
:return: returns collision between nodes
"""
node_index = self._original_node_list.index(node) node_index = self._original_node_list.index(node)
for other_node in other_nodes: for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node) other_node_index = self._original_node_list.index(other_node)
@ -86,14 +93,26 @@ class OneAtATimeIterator(Iterator.Iterator):
return True return True
return False return False
## Calculate score simply sums the number of other objects it 'blocks'
def _calculateScore(self, a: SceneNode, b: SceneNode) -> int: def _calculateScore(self, a: SceneNode, b: SceneNode) -> int:
"""Calculate score simply sums the number of other objects it 'blocks'
:param a: node
:param b: node
:return: sum of the number of other objects
"""
score_a = sum(self._hit_map[self._original_node_list.index(a)]) score_a = sum(self._hit_map[self._original_node_list.index(a)])
score_b = sum(self._hit_map[self._original_node_list.index(b)]) score_b = sum(self._hit_map[self._original_node_list.index(b)])
return score_a - score_b return score_a - score_b
## Checks if A can be printed before B
def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: def _checkHit(self, a: SceneNode, b: SceneNode) -> bool:
"""Checks if a can be printed before b
:param a: node
:param b: node
:return: true if a can be printed before b
"""
if a == b: if a == b:
return False return False
@ -116,12 +135,14 @@ class OneAtATimeIterator(Iterator.Iterator):
return False return False
## Internal object used to keep track of a possible order in which to print objects.
class _ObjectOrder: class _ObjectOrder:
## Creates the _ObjectOrder instance. """Internal object used to keep track of a possible order in which to print objects."""
# \param order List of indices in which to print objects, ordered by printing
# order.
# \param todo: List of indices which are not yet inserted into the order list.
def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None: def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None:
"""Creates the _ObjectOrder instance.
:param order: List of indices in which to print objects, ordered by printing order.
:param todo: List of indices which are not yet inserted into the order list.
"""
self.order = order self.order = order
self.todo = todo self.todo = todo

View file

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

View file

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

View file

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

View file

@ -18,11 +18,15 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.ShaderProgram import ShaderProgram
## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture.
# The texture is used to map a 2d location (eg the mouse location) to a world space position
#
# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
class PickingPass(RenderPass): class PickingPass(RenderPass):
"""A :py:class:`Uranium.UM.View.RenderPass` subclass that renders a the distance of selectable objects from the
active camera to a texture.
The texture is used to map a 2d location (eg the mouse location) to a world space position
.. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
"""
def __init__(self, width: int, height: int) -> None: def __init__(self, width: int, height: int) -> None:
super().__init__("picking", width, height) super().__init__("picking", width, height)
@ -56,8 +60,14 @@ class PickingPass(RenderPass):
batch.render(self._scene.getActiveCamera()) batch.render(self._scene.getActiveCamera())
self.release() self.release()
## Get the distance in mm from the camera to at a certain pixel coordinate.
def getPickedDepth(self, x: int, y: int) -> float: def getPickedDepth(self, x: int, y: int) -> float:
"""Get the distance in mm from the camera to at a certain pixel coordinate.
:param x: x component of coordinate vector in pixels
:param y: y component of coordinate vector in pixels
:return: distance in mm from the camera to pixel coordinate
"""
output = self.getOutput() output = self.getOutput()
window_size = self._renderer.getWindowSize() window_size = self._renderer.getWindowSize()
@ -72,8 +82,14 @@ class PickingPass(RenderPass):
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm
return distance return distance
## Get the world coordinates of a picked point
def getPickedPosition(self, x: int, y: int) -> Vector: def getPickedPosition(self, x: int, y: int) -> Vector:
"""Get the world coordinates of a picked point
:param x: x component of coordinate vector in pixels
:param y: y component of coordinate vector in pixels
:return: vector of the world coordinate
"""
distance = self.getPickedDepth(x, y) distance = self.getPickedDepth(x, y)
camera = self._scene.getActiveCamera() camera = self._scene.getActiveCamera()
if camera: if camera:

View file

@ -95,15 +95,15 @@ class PlatformPhysics:
# Ignore root, ourselves and anything that is not a normal SceneNode. # Ignore root, ourselves and anything that is not a normal SceneNode.
if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"): if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"):
continue continue
# Ignore collisions of a group with it's own children # Ignore collisions of a group with it's own children
if other_node in node.getAllChildren() or node in other_node.getAllChildren(): if other_node in node.getAllChildren() or node in other_node.getAllChildren():
continue continue
# Ignore collisions within a group # Ignore collisions within a group
if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None): if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
continue continue
# Ignore nodes that do not have the right properties set. # Ignore nodes that do not have the right properties set.
if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
continue continue

View file

@ -1,7 +1,7 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING, cast from typing import Optional, TYPE_CHECKING, cast, List
from UM.Application import Application from UM.Application import Application
@ -21,9 +21,14 @@ if TYPE_CHECKING:
from UM.Scene.Camera import Camera from UM.Scene.Camera import Camera
# Make color brighter by normalizing it (maximum factor 2.5 brighter) def prettier_color(color_list: List[float]) -> List[float]:
# color_list is a list of 4 elements: [r, g, b, a], each element is a float 0..1 """Make color brighter by normalizing
def prettier_color(color_list):
maximum factor 2.5 brighter
:param color_list: a list of 4 elements: [r, g, b, a], each element is a float 0..1
:return: a normalized list of 4 elements: [r, g, b, a], each element is a float 0..1
"""
maximum = max(color_list[:3]) maximum = max(color_list[:3])
if maximum > 0: if maximum > 0:
factor = min(1 / maximum, 2.5) factor = min(1 / maximum, 2.5)
@ -32,11 +37,14 @@ def prettier_color(color_list):
return [min(i * factor, 1.0) for i in color_list] return [min(i * factor, 1.0) for i in color_list]
## A render pass subclass that renders slicable objects with default parameters.
# It uses the active camera by default, but it can be overridden to use a different camera.
#
# This is useful to get a preview image of a scene taken from a different location as the active camera.
class PreviewPass(RenderPass): class PreviewPass(RenderPass):
"""A :py:class:`Uranium.UM.View.RenderPass` subclass that renders slicable objects with default parameters.
It uses the active camera by default, but it can be overridden to use a different camera.
This is useful to get a preview image of a scene taken from a different location as the active camera.
"""
def __init__(self, width: int, height: int) -> None: def __init__(self, width: int, height: int) -> None:
super().__init__("preview", width, height, 0) super().__init__("preview", width, height, 0)

View file

@ -10,8 +10,14 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
def __init__(self): def __init__(self):
super().__init__(QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)
## Request a new image.
def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]: def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]:
"""Request a new image.
:param id: id of the requested image
:param size: is not used defaults to QSize(15, 15)
:return: an tuple containing the image and size
"""
# The id will have an uuid and an increment separated by a slash. As we don't care about the value of the # The id will have an uuid and an increment separated by a slash. As we don't care about the value of the
# increment, we need to strip that first. # increment, we need to strip that first.
uuid = id[id.find("/") + 1:] uuid = id[id.find("/") + 1:]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
class SliceableObjectDecorator(SceneNodeDecorator): class SliceableObjectDecorator(SceneNodeDecorator):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
def isSliceable(self) -> bool: def isSliceable(self) -> bool:
return True return True

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,11 +30,17 @@ class Snapshot:
return min_x, max_x, min_y, max_y return min_x, max_x, min_y, max_y
## Return a QImage of the scene
# Uses PreviewPass that leaves out some elements
# Aspect ratio assumes a square
@staticmethod @staticmethod
def snapshot(width = 300, height = 300): def snapshot(width = 300, height = 300):
"""Return a QImage of the scene
Uses PreviewPass that leaves out some elements Aspect ratio assumes a square
:param width: width of the aspect ratio default 300
:param height: height of the aspect ratio default 300
:return: None when there is no model on the build plate otherwise it will return an image
"""
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
active_camera = scene.getActiveCamera() active_camera = scene.getActiveCamera()
render_width, render_height = active_camera.getWindowSize() render_width, render_height = active_camera.getWindowSize()

View file

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

View file

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

View file

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

View file

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

View file

@ -32,8 +32,9 @@ except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
class ThreeMFReader(MeshReader): class ThreeMFReader(MeshReader):
"""Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -55,13 +56,17 @@ class ThreeMFReader(MeshReader):
return Matrix() return Matrix()
split_transformation = transformation.split() split_transformation = transformation.split()
## Transformation is saved as:
## M00 M01 M02 0.0
## M10 M11 M12 0.0
## M20 M21 M22 0.0
## M30 M31 M32 1.0
## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix() temp_mat = Matrix()
"""Transformation is saved as:
M00 M01 M02 0.0
M10 M11 M12 0.0
M20 M21 M22 0.0
M30 M31 M32 1.0
We switch the row & cols as that is how everyone else uses matrices!
"""
# Rotation & Scale # Rotation & Scale
temp_mat._data[0, 0] = split_transformation[0] temp_mat._data[0, 0] = split_transformation[0]
temp_mat._data[1, 0] = split_transformation[1] temp_mat._data[1, 0] = split_transformation[1]
@ -80,9 +85,11 @@ class ThreeMFReader(MeshReader):
return temp_mat return temp_mat
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
# \returns Scene node.
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
:returns: Scene node.
"""
try: try:
node_name = savitar_node.getName() node_name = savitar_node.getName()
node_id = savitar_node.getId() node_id = savitar_node.getId()
@ -243,15 +250,17 @@ class ThreeMFReader(MeshReader):
return result return result
## Create a scale vector based on a unit string.
# The core spec defines the following:
# * micron
# * millimeter (default)
# * centimeter
# * inch
# * foot
# * meter
def _getScaleFromUnit(self, unit: Optional[str]) -> Vector: def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
"""Create a scale vector based on a unit string.
.. The core spec defines the following:
* micron
* millimeter (default)
* centimeter
* inch
* foot
* meter
"""
conversion_to_mm = { conversion_to_mm = {
"micron": 0.001, "micron": 0.001,
"millimeter": 1, "millimeter": 1,

Some files were not shown because too many files have changed in this diff Show more