mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 06:57:28 -06:00
Merge pull request #7551 from Ultimaker/doxygen_to_restructuredtext_comments
Converted doxygen style comments to reStructuredText style
This commit is contained in:
commit
98587a9008
224 changed files with 5521 additions and 3874 deletions
|
@ -11,11 +11,13 @@ import os
|
|||
import sys
|
||||
|
||||
|
||||
## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
|
||||
#
|
||||
# \param work_dir The directory to look for JSON files recursively.
|
||||
# \return A list of JSON files in absolute paths that are found in the given directory.
|
||||
def find_json_files(work_dir: str) -> list:
|
||||
"""Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
|
||||
|
||||
:param work_dir: The directory to look for JSON files recursively.
|
||||
:return: A list of JSON files in absolute paths that are found in the given directory.
|
||||
"""
|
||||
|
||||
json_file_list = []
|
||||
for root, dir_names, file_names in os.walk(work_dir):
|
||||
for file_name in file_names:
|
||||
|
@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list:
|
|||
return json_file_list
|
||||
|
||||
|
||||
## Removes the given entries from the given JSON file. The file will modified in-place.
|
||||
#
|
||||
# \param file_path The JSON file to modify.
|
||||
# \param entries A list of strings as entries to remove.
|
||||
# \return None
|
||||
def remove_entries_from_json_file(file_path: str, entries: list) -> None:
|
||||
"""Removes the given entries from the given JSON file. The file will modified in-place.
|
||||
|
||||
:param file_path: The JSON file to modify.
|
||||
:param entries: A list of strings as entries to remove.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
package_dict = json.load(f, object_hook = collections.OrderedDict)
|
||||
|
|
|
@ -25,23 +25,27 @@ class SyncState:
|
|||
ERROR = 2
|
||||
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):
|
||||
"""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
|
||||
SYNC_INTERVAL = 30.0 # seconds
|
||||
Q_ENUMS(SyncState)
|
||||
|
||||
# Signal emitted when user logged in or out.
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
"""Signal emitted when user logged in or out"""
|
||||
|
||||
accessTokenChanged = pyqtSignal()
|
||||
syncRequested = pyqtSignal()
|
||||
"""Sync services may connect to this signal to receive sync triggers.
|
||||
|
@ -140,9 +144,10 @@ class Account(QObject):
|
|||
def _onAccessTokenChanged(self):
|
||||
self.accessTokenChanged.emit()
|
||||
|
||||
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||
@property
|
||||
def is_staging(self) -> bool:
|
||||
"""Indication whether the given authentication is applied against staging or not."""
|
||||
|
||||
return "staging" in self._oauth_root
|
||||
|
||||
@pyqtProperty(bool, notify=loginStateChanged)
|
||||
|
@ -227,10 +232,10 @@ class Account(QObject):
|
|||
def accessToken(self) -> Optional[str]:
|
||||
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)
|
||||
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()
|
||||
if not user_profile:
|
||||
return None
|
||||
|
|
|
@ -8,28 +8,37 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
"""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:
|
||||
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]]]:
|
||||
"""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()
|
||||
|
||||
## 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:
|
||||
"""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)
|
||||
|
|
|
@ -7,32 +7,43 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
"""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:
|
||||
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:
|
||||
"""Add items to the sidebar context menu.
|
||||
|
||||
:param menu_item: dict containing the menu item to add.
|
||||
"""
|
||||
|
||||
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:
|
||||
"""Get all custom items currently added to the sidebar context menu.
|
||||
|
||||
:return: List containing all custom context menu items.
|
||||
"""
|
||||
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
|
|
|
@ -9,18 +9,22 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
"""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:
|
||||
# API methods specific to the settings portion of the UI
|
||||
|
|
|
@ -13,13 +13,14 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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.
|
||||
__instance = None # type: "CuraAPI"
|
||||
|
@ -40,10 +41,8 @@ class CuraAPI(QObject):
|
|||
def __init__(self, application: Optional["CuraApplication"] = None) -> None:
|
||||
super().__init__(parent = CuraAPI._application)
|
||||
|
||||
# Accounts API
|
||||
self._account = Account(self._application)
|
||||
|
||||
# Backups API
|
||||
self._backups = Backups(self._application)
|
||||
|
||||
self._connectionStatus = ConnectionStatus()
|
||||
|
@ -56,6 +55,8 @@ class CuraAPI(QObject):
|
|||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def account(self) -> "Account":
|
||||
"""Accounts API"""
|
||||
|
||||
return self._account
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
|
@ -64,8 +65,12 @@ class CuraAPI(QObject):
|
|||
|
||||
@property
|
||||
def backups(self) -> "Backups":
|
||||
"""Backups API"""
|
||||
|
||||
return self._backups
|
||||
|
||||
@property
|
||||
def interface(self) -> "Interface":
|
||||
"""Interface API"""
|
||||
|
||||
return self._interface
|
||||
|
|
|
@ -16,17 +16,20 @@ from collections import namedtuple
|
|||
import numpy
|
||||
import copy
|
||||
|
||||
## Return object for bestSpot
|
||||
LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
|
||||
"""Return object for bestSpot"""
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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]
|
||||
|
||||
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
|
||||
|
@ -42,20 +45,20 @@ class Arrange:
|
|||
self._is_empty = True
|
||||
|
||||
@classmethod
|
||||
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8):
|
||||
"""
|
||||
Helper to create an Arranger instance
|
||||
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
|
||||
|
||||
Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the
|
||||
nodes yourself.
|
||||
:param scene_root: Root for finding all scene nodes
|
||||
:param fixed_nodes: Scene nodes to be placed
|
||||
:param scale:
|
||||
:param x:
|
||||
:param y:
|
||||
:param min_offset:
|
||||
:return:
|
||||
|
||||
:param scene_root: Root for finding all scene nodes default = None
|
||||
:param fixed_nodes: Scene nodes to be placed default = None
|
||||
:param scale: default = 0.5
|
||||
:param x: default = 350
|
||||
:param y: default = 250
|
||||
:param min_offset: default = 8
|
||||
"""
|
||||
|
||||
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
||||
arranger.centerFirst()
|
||||
|
||||
|
@ -90,19 +93,21 @@ class Arrange:
|
|||
arranger.place(0, 0, shape_arr, update_empty = False)
|
||||
return arranger
|
||||
|
||||
## This resets the optimization for finding location based on size
|
||||
def resetLastPriority(self):
|
||||
"""This resets the optimization for finding location based on size"""
|
||||
|
||||
self._last_priority = 0
|
||||
|
||||
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1):
|
||||
"""
|
||||
Find placement for a node (using offset shape) and place it (using hull shape)
|
||||
:param node:
|
||||
:param offset_shape_arr: hapeArray with offset, for placing the shape
|
||||
:param hull_shape_arr: ShapeArray without offset, used to find location
|
||||
:param step:
|
||||
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)
|
||||
|
||||
:param node: The node to be placed
|
||||
:param offset_shape_arr: shape array with offset, for placing the shape
|
||||
:param hull_shape_arr: shape array without offset, used to find location
|
||||
:param step: default = 1
|
||||
:return: the nodes that should be placed
|
||||
"""
|
||||
|
||||
best_spot = self.bestSpot(
|
||||
hull_shape_arr, start_prio = self._last_priority, step = step)
|
||||
x, y = best_spot.x, best_spot.y
|
||||
|
@ -129,10 +134,8 @@ class Arrange:
|
|||
return found_spot
|
||||
|
||||
def centerFirst(self):
|
||||
"""
|
||||
Fill priority, center is best. Lower value is better.
|
||||
:return:
|
||||
"""
|
||||
"""Fill priority, center is best. Lower value is better. """
|
||||
|
||||
# Square distance: creates a more round shape
|
||||
self._priority = numpy.fromfunction(
|
||||
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()
|
||||
|
||||
def backFirst(self):
|
||||
"""
|
||||
Fill priority, back is best. Lower value is better
|
||||
:return:
|
||||
"""
|
||||
"""Fill priority, back is best. Lower value is better """
|
||||
|
||||
self._priority = numpy.fromfunction(
|
||||
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.sort()
|
||||
|
||||
def checkShape(self, x, y, shape_arr):
|
||||
"""
|
||||
Return the amount of "penalty points" for polygon, which is the sum of priority
|
||||
def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]:
|
||||
"""Return the amount of "penalty points" for polygon, which is the sum of priority
|
||||
|
||||
:param x: x-coordinate to check shape
|
||||
:param y:
|
||||
:param shape_arr: the ShapeArray object to place
|
||||
:param y: y-coordinate to check shape
|
||||
:param shape_arr: the shape array object to place
|
||||
:return: None if occupied
|
||||
"""
|
||||
|
||||
x = int(self._scale * x)
|
||||
y = int(self._scale * y)
|
||||
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]]
|
||||
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
||||
|
||||
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
|
||||
"""
|
||||
Find "best" spot for ShapeArray
|
||||
:param shape_arr:
|
||||
def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion:
|
||||
"""Find "best" spot for ShapeArray
|
||||
|
||||
:param shape_arr: shape array
|
||||
:param start_prio: Start with this priority value (and skip the ones before)
|
||||
:param step: Slicing value, higher = more skips = faster but less accurate
|
||||
:return: namedtuple with properties x, y, penalty_points, priority.
|
||||
"""
|
||||
|
||||
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
|
||||
if start_idx_list:
|
||||
try:
|
||||
|
@ -211,15 +214,16 @@ class Arrange:
|
|||
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):
|
||||
"""
|
||||
Place the object.
|
||||
"""Place the object.
|
||||
|
||||
Marks the locations in self._occupied and self._priority
|
||||
|
||||
:param x:
|
||||
:param y:
|
||||
:param shape_arr:
|
||||
:param update_empty: updates the _is_empty, used when adding disallowed areas
|
||||
:return:
|
||||
"""
|
||||
|
||||
x = int(self._scale * x)
|
||||
y = int(self._scale * y)
|
||||
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||
|
|
|
@ -18,8 +18,9 @@ from cura.Arranging.ShapeArray import ShapeArray
|
|||
from typing import List
|
||||
|
||||
|
||||
## Do arrangements on multiple build plates (aka builtiplexer)
|
||||
class ArrangeArray:
|
||||
"""Do arrangements on multiple build plates (aka builtiplexer)"""
|
||||
|
||||
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None:
|
||||
self._x = x
|
||||
self._y = y
|
||||
|
|
|
@ -11,19 +11,24 @@ if TYPE_CHECKING:
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
|
||||
## Polygon representation as an array for use with Arrange
|
||||
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:
|
||||
self.arr = arr
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
self.scale = scale
|
||||
|
||||
## Instantiate from a bunch of vertices
|
||||
# \param vertices
|
||||
# \param scale scale the coordinates
|
||||
@classmethod
|
||||
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
|
||||
vertices = vertices * scale
|
||||
# flip y, x -> x, y
|
||||
|
@ -44,12 +49,16 @@ class ShapeArray:
|
|||
arr[0][0] = 1
|
||||
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
|
||||
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_x = transform._data[0][3]
|
||||
transform_y = transform._data[2][3]
|
||||
|
@ -88,14 +97,19 @@ class ShapeArray:
|
|||
|
||||
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
|
||||
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
|
||||
|
||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||
|
@ -111,16 +125,21 @@ class ShapeArray:
|
|||
|
||||
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
|
||||
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]:
|
||||
return None
|
||||
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||
|
|
|
@ -18,24 +18,26 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
# These files should be ignored when making a backup.
|
||||
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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")
|
||||
"""Re-use translation catalog"""
|
||||
|
||||
def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
|
||||
self._application = application
|
||||
self.zip_file = zip_file # type: Optional[bytes]
|
||||
self.meta_data = meta_data # type: Optional[Dict[str, str]]
|
||||
|
||||
## Create a back-up from the current user config folder.
|
||||
def makeFromCurrent(self) -> None:
|
||||
"""Create a back-up from the current user config folder."""
|
||||
|
||||
cura_release = self._application.getVersion()
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
||||
|
@ -77,10 +79,13 @@ class Backup:
|
|||
"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]:
|
||||
"""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))
|
||||
try:
|
||||
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
|
||||
|
@ -99,13 +104,17 @@ class Backup:
|
|||
"Could not create archive from user data directory: {}".format(error)))
|
||||
return None
|
||||
|
||||
## Show a UI message.
|
||||
def _showMessage(self, message: str) -> None:
|
||||
"""Show a UI message."""
|
||||
|
||||
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:
|
||||
"""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):
|
||||
# We can restore without the minimum required information.
|
||||
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
|
||||
|
@ -139,12 +148,14 @@ class Backup:
|
|||
|
||||
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
|
||||
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.
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
|
|
@ -24,6 +24,7 @@ class BackupsManager:
|
|||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||
"""
|
||||
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).
|
||||
"""
|
||||
|
||||
|
@ -37,6 +38,7 @@ class BackupsManager:
|
|||
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Restore a back-up from a given ZipFile.
|
||||
|
||||
: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.
|
||||
"""
|
||||
|
|
|
@ -44,8 +44,9 @@ catalog = i18nCatalog("cura")
|
|||
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):
|
||||
"""Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas."""
|
||||
|
||||
raftThicknessChanged = Signal()
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
|
||||
|
@ -113,7 +114,7 @@ class BuildVolume(SceneNode):
|
|||
self._has_errors = False
|
||||
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_change_timer = QTimer()
|
||||
|
@ -163,10 +164,12 @@ class BuildVolume(SceneNode):
|
|||
self._scene_objects = new_scene_objects
|
||||
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):
|
||||
"""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")
|
||||
if per_mesh_stack:
|
||||
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
|
@ -187,10 +190,14 @@ class BuildVolume(SceneNode):
|
|||
if 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:
|
||||
"""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)
|
||||
|
||||
def getDisallowedAreas(self) -> List[Polygon]:
|
||||
|
@ -226,9 +233,9 @@ class BuildVolume(SceneNode):
|
|||
|
||||
return True
|
||||
|
||||
## For every sliceable node, update node._outside_buildarea
|
||||
#
|
||||
def updateNodeBoundaryCheck(self):
|
||||
"""For every sliceable node, update node._outside_buildarea"""
|
||||
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
|
@ -295,8 +302,13 @@ class BuildVolume(SceneNode):
|
|||
for child_node in children:
|
||||
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:
|
||||
"""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:
|
||||
return
|
||||
|
||||
|
@ -484,8 +496,9 @@ class BuildVolume(SceneNode):
|
|||
self._disallowed_area_size = max(size, self._disallowed_area_size)
|
||||
return mb.build()
|
||||
|
||||
## Recalculates the build volume & disallowed areas.
|
||||
def rebuild(self) -> None:
|
||||
"""Recalculates the build volume & disallowed areas."""
|
||||
|
||||
if not self._width or not self._height or not self._depth:
|
||||
return
|
||||
|
||||
|
@ -586,8 +599,9 @@ class BuildVolume(SceneNode):
|
|||
def _onStackChanged(self):
|
||||
self._stack_change_timer.start()
|
||||
|
||||
## Update the build volume visualization
|
||||
def _onStackChangeTimerFinished(self) -> None:
|
||||
"""Update the build volume visualization"""
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
|
@ -712,15 +726,15 @@ class BuildVolume(SceneNode):
|
|||
self._depth = self._global_container_stack.getProperty("machine_depth", "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):
|
||||
"""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._updateRaftThickness()
|
||||
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
|
||||
|
@ -782,15 +796,14 @@ class BuildVolume(SceneNode):
|
|||
for extruder_id in result_areas_no_brim:
|
||||
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):
|
||||
"""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 = {}
|
||||
adhesion_extruder = None #type: ExtruderStack
|
||||
for extruder in used_extruders:
|
||||
|
@ -828,18 +841,18 @@ class BuildVolume(SceneNode):
|
|||
|
||||
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]]:
|
||||
"""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]]
|
||||
if not self._global_container_stack:
|
||||
return result
|
||||
|
@ -867,19 +880,18 @@ class BuildVolume(SceneNode):
|
|||
|
||||
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]]:
|
||||
"""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.
|
||||
machine_disallowed_polygons = []
|
||||
if self._global_container_stack is None:
|
||||
|
@ -1010,13 +1022,14 @@ class BuildVolume(SceneNode):
|
|||
|
||||
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]:
|
||||
"""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_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
|
||||
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
|
||||
|
||||
## 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):
|
||||
"""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:
|
||||
return 0
|
||||
|
||||
|
|
|
@ -150,8 +150,9 @@ class CrashHandler:
|
|||
self._sendCrashReport()
|
||||
os._exit(1)
|
||||
|
||||
## Backup the current resource directories and create clean ones.
|
||||
def _backupAndStartClean(self):
|
||||
"""Backup the current resource directories and create clean ones."""
|
||||
|
||||
Resources.factoryReset()
|
||||
self.early_crash_dialog.close()
|
||||
|
||||
|
@ -162,8 +163,9 @@ class CrashHandler:
|
|||
def _showDetailedReport(self):
|
||||
self.dialog.exec_()
|
||||
|
||||
## Creates a modal dialog.
|
||||
def _createDialog(self):
|
||||
"""Creates a modal dialog."""
|
||||
|
||||
self.dialog.setMinimumWidth(640)
|
||||
self.dialog.setMinimumHeight(640)
|
||||
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
|
||||
|
|
|
@ -43,9 +43,10 @@ class CuraActions(QObject):
|
|||
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||
|
||||
## Reset camera position and direction to default
|
||||
@pyqtSlot()
|
||||
def homeCamera(self) -> None:
|
||||
"""Reset camera position and direction to default"""
|
||||
|
||||
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
|
||||
camera = scene.getActiveCamera()
|
||||
if camera:
|
||||
|
@ -54,9 +55,10 @@ class CuraActions(QObject):
|
|||
camera.setPerspective(True)
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
|
||||
## Center all objects in the selection
|
||||
@pyqtSlot()
|
||||
def centerSelection(self) -> None:
|
||||
"""Center all objects in the selection"""
|
||||
|
||||
operation = GroupedOperation()
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
current_node = node
|
||||
|
@ -73,18 +75,21 @@ class CuraActions(QObject):
|
|||
operation.addOperation(center_operation)
|
||||
operation.push()
|
||||
|
||||
## Multiply all objects in the selection
|
||||
#
|
||||
# \param count The number of times to multiply the selection.
|
||||
@pyqtSlot(int)
|
||||
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
|
||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Delete all selected objects.
|
||||
@pyqtSlot()
|
||||
def deleteSelection(self) -> None:
|
||||
"""Delete all selected objects."""
|
||||
|
||||
if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
|
@ -106,11 +111,13 @@ class CuraActions(QObject):
|
|||
|
||||
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)
|
||||
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()
|
||||
|
||||
nodes_to_change = []
|
||||
|
|
|
@ -261,9 +261,12 @@ class CuraApplication(QtApplication):
|
|||
def ultimakerCloudAccountRootUrl(self) -> str:
|
||||
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):
|
||||
"""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()
|
||||
self._cli_parser.add_argument("--help", "-h",
|
||||
action = "store_true",
|
||||
|
@ -325,8 +328,9 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Single instance commands were sent, exiting")
|
||||
sys.exit(0)
|
||||
|
||||
# Adds expected directory names and search paths for Resources.
|
||||
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
|
||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
|
||||
Resources.addExpectedDirNameInData(dir_name)
|
||||
|
@ -368,9 +372,12 @@ class CuraApplication(QtApplication):
|
|||
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):
|
||||
"""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)
|
||||
|
||||
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("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
|
||||
|
||||
# Adds all resources and container related resources.
|
||||
def __addAllResourcesAndContainerResources(self) -> None:
|
||||
"""Adds all resources and container related resources."""
|
||||
|
||||
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||
Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
|
||||
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
|
||||
|
@ -406,8 +414,9 @@ class CuraApplication(QtApplication):
|
|||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||
|
||||
# Adds all empty containers.
|
||||
def __addAllEmptyContainers(self) -> None:
|
||||
"""Adds all empty containers."""
|
||||
|
||||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# 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.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):
|
||||
"""Initializes the version upgrade manager with by providing the paths for each resource type and the latest
|
||||
versions. """
|
||||
|
||||
self._version_upgrade_manager.setCurrentVersions(
|
||||
{
|
||||
("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:
|
||||
"""Runs preparations that needs to be done before the starting process."""
|
||||
|
||||
super().startSplashWindowPhase()
|
||||
|
||||
if not self.getIsHeadLess():
|
||||
|
@ -509,7 +520,7 @@ class CuraApplication(QtApplication):
|
|||
# Set the setting version for Preferences
|
||||
preferences = self.getPreferences()
|
||||
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")
|
||||
|
||||
|
@ -613,12 +624,13 @@ class CuraApplication(QtApplication):
|
|||
def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
|
||||
self._confirm_exit_dialog_callback(yes_or_no)
|
||||
|
||||
## Signal to connect preferences action in QML
|
||||
showPreferencesWindow = pyqtSignal()
|
||||
"""Signal to connect preferences action in QML"""
|
||||
|
||||
## Show the preferences window
|
||||
@pyqtSlot()
|
||||
def showPreferences(self) -> None:
|
||||
"""Show the preferences window"""
|
||||
|
||||
self.showPreferencesWindow.emit()
|
||||
|
||||
# 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..."))
|
||||
super().setGlobalContainerStack(stack)
|
||||
|
||||
## A reusable dialogbox
|
||||
#
|
||||
showMessageBox = pyqtSignal(str,str, str, str, int, int,
|
||||
arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"])
|
||||
"""A reusable dialogbox"""
|
||||
|
||||
def messageBox(self, title, text,
|
||||
informativeText = "",
|
||||
|
@ -717,9 +728,12 @@ class CuraApplication(QtApplication):
|
|||
def setDefaultPath(self, key, default_path):
|
||||
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:
|
||||
"""Handle loading of all plugin types (and the backend explicitly)
|
||||
|
||||
:py:class:`Uranium.UM.PluginRegistry`
|
||||
"""
|
||||
|
||||
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
|
@ -743,9 +757,12 @@ class CuraApplication(QtApplication):
|
|||
|
||||
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):
|
||||
"""Set a short, user-friendly hint about current loading status.
|
||||
|
||||
The way this message is displayed depends on application state
|
||||
"""
|
||||
|
||||
if self.started:
|
||||
Logger.info(hint)
|
||||
else:
|
||||
|
@ -830,12 +847,14 @@ class CuraApplication(QtApplication):
|
|||
|
||||
initializationFinished = pyqtSignal()
|
||||
|
||||
## Run Cura without GUI elements and interaction (server mode).
|
||||
def runWithoutGUI(self):
|
||||
"""Run Cura without GUI elements and interaction (server mode)."""
|
||||
|
||||
self.closeSplash()
|
||||
|
||||
## Run Cura with GUI (desktop mode).
|
||||
def runWithGUI(self):
|
||||
"""Run Cura with GUI (desktop mode)."""
|
||||
|
||||
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
|
||||
|
||||
controller = self.getController()
|
||||
|
@ -989,10 +1008,13 @@ class CuraApplication(QtApplication):
|
|||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||
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:
|
||||
"""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)
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
|
@ -1012,8 +1034,9 @@ class CuraApplication(QtApplication):
|
|||
self._simple_mode_settings_manager = SimpleModeSettingsManager()
|
||||
return self._simple_mode_settings_manager
|
||||
|
||||
## Handle Qt events
|
||||
def event(self, event):
|
||||
"""Handle Qt events"""
|
||||
|
||||
if event.type() == QEvent.FileOpen:
|
||||
if self._plugins_loaded:
|
||||
self._openFile(event.file())
|
||||
|
@ -1025,8 +1048,9 @@ class CuraApplication(QtApplication):
|
|||
def getAutoSave(self) -> Optional[AutoSave]:
|
||||
return self._auto_save
|
||||
|
||||
## Get print information (duration / material used)
|
||||
def getPrintInformation(self):
|
||||
"""Get print information (duration / material used)"""
|
||||
|
||||
return self._print_information
|
||||
|
||||
def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
|
||||
|
@ -1042,10 +1066,12 @@ class CuraApplication(QtApplication):
|
|||
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
|
||||
return self._cura_API
|
||||
|
||||
## Registers objects for the QML engine to use.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
def registerObjects(self, engine):
|
||||
"""Registers objects for the QML engine to use.
|
||||
|
||||
:param engine: The QML engine.
|
||||
"""
|
||||
|
||||
super().registerObjects(engine)
|
||||
|
||||
# global contexts
|
||||
|
@ -1181,8 +1207,9 @@ class CuraApplication(QtApplication):
|
|||
if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")):
|
||||
self._update_platform_activity_timer.start()
|
||||
|
||||
## Update scene bounding box for current build plate
|
||||
def updatePlatformActivity(self, node = None):
|
||||
"""Update scene bounding box for current build plate"""
|
||||
|
||||
count = 0
|
||||
scene_bounding_box = None
|
||||
is_block_slicing_node = False
|
||||
|
@ -1226,9 +1253,10 @@ class CuraApplication(QtApplication):
|
|||
self._platform_activity = True if count > 0 else False
|
||||
self.activityChanged.emit()
|
||||
|
||||
## Select all nodes containing mesh data in the scene.
|
||||
@pyqtSlot()
|
||||
def selectAll(self):
|
||||
"""Select all nodes containing mesh data in the scene."""
|
||||
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
|
@ -1247,9 +1275,10 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Selection.add(node)
|
||||
|
||||
## Reset all translation on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
def resetAllTranslation(self):
|
||||
"""Reset all translation on nodes with mesh data."""
|
||||
|
||||
Logger.log("i", "Resetting all scene translations")
|
||||
nodes = []
|
||||
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.push()
|
||||
|
||||
## Reset all transformations on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
def resetAll(self):
|
||||
"""Reset all transformations on nodes with mesh data."""
|
||||
|
||||
Logger.log("i", "Resetting all scene transformations")
|
||||
nodes = []
|
||||
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.push()
|
||||
|
||||
## Arrange all objects.
|
||||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
"""Arrange all objects."""
|
||||
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
|
@ -1358,17 +1389,21 @@ class CuraApplication(QtApplication):
|
|||
nodes_to_arrange.append(node)
|
||||
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:
|
||||
"""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
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self) -> None:
|
||||
"""Reload all mesh data on the screen from file."""
|
||||
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
|
@ -1478,8 +1513,9 @@ class CuraApplication(QtApplication):
|
|||
group_node.setName("MergedMesh") # add a specific name to distinguish this node
|
||||
|
||||
|
||||
## Updates origin position of all merged meshes
|
||||
def updateOriginOfMergedMeshes(self, _):
|
||||
"""Updates origin position of all merged meshes"""
|
||||
|
||||
group_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
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
|
||||
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
|
||||
:return: None
|
||||
:param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the
|
||||
meshes in a file
|
||||
"""
|
||||
|
||||
job_result = job.getResult() # nodes that exist inside the file read by this job
|
||||
if len(job_result) == 0:
|
||||
Logger.log("e", "Reloading the mesh failed.")
|
||||
|
@ -1645,12 +1682,15 @@ class CuraApplication(QtApplication):
|
|||
def additionalComponents(self):
|
||||
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")
|
||||
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:
|
||||
self._additional_components[area_id] = []
|
||||
self._additional_components[area_id].append(component)
|
||||
|
@ -1665,10 +1705,13 @@ class CuraApplication(QtApplication):
|
|||
|
||||
@pyqtSlot(QUrl, str)
|
||||
@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):
|
||||
"""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():
|
||||
return
|
||||
|
||||
|
@ -1844,9 +1887,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
@pyqtSlot(str, result=bool)
|
||||
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()
|
||||
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
|
||||
if workspace_reader is None:
|
||||
|
|
|
@ -24,11 +24,15 @@ class CuraPackageManager(PackageManager):
|
|||
|
||||
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]]]:
|
||||
"""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)
|
||||
container_stacks = self._application.getContainerRegistry().findContainerStacks()
|
||||
global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
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 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,
|
||||
center_position = None, layers=None, element_counts=None, attributes=None):
|
||||
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,
|
||||
|
|
|
@ -10,8 +10,9 @@ import numpy
|
|||
from typing import Dict, Optional
|
||||
|
||||
|
||||
## Builder class for constructing a LayerData object
|
||||
class LayerDataBuilder(MeshBuilder):
|
||||
"""Builder class for constructing a :py:class:`cura.LayerData.LayerData` object"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._layers = {} # type: Dict[int, Layer]
|
||||
|
@ -42,11 +43,13 @@ class LayerDataBuilder(MeshBuilder):
|
|||
|
||||
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):
|
||||
"""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
|
||||
index_count = 0
|
||||
for layer, data in self._layers.items():
|
||||
|
|
|
@ -7,8 +7,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
|||
from cura.LayerData import LayerData
|
||||
|
||||
|
||||
## Simple decorator to indicate a scene node holds layer data.
|
||||
class LayerDataDecorator(SceneNodeDecorator):
|
||||
"""Simple decorator to indicate a scene node holds layer data."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._layer_data = None # type: Optional[LayerData]
|
||||
|
|
|
@ -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)
|
||||
|
||||
## 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:
|
||||
"""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._types = line_types
|
||||
for i in range(len(self._types)):
|
||||
|
@ -83,19 +86,22 @@ class LayerPolygon:
|
|||
self._vertex_begin = 0
|
||||
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:
|
||||
"""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:
|
||||
self.buildCache()
|
||||
|
||||
|
@ -202,8 +208,12 @@ class LayerPolygon:
|
|||
def jumpCount(self):
|
||||
return self._jump_count
|
||||
|
||||
# Calculate normals for the entire polygon using numpy.
|
||||
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[:, 1] = 0.0 # We are only interested in 2D normals
|
||||
|
||||
|
@ -229,9 +239,10 @@ class LayerPolygon:
|
|||
|
||||
__color_map = None # type: numpy.ndarray
|
||||
|
||||
## Gets the instance of the VersionUpgradeManager, or creates one.
|
||||
@classmethod
|
||||
def getColorMap(cls) -> numpy.ndarray:
|
||||
"""Gets the instance of the VersionUpgradeManager, or creates one."""
|
||||
|
||||
if cls.__color_map is None:
|
||||
theme = cast(Theme, QtApplication.getInstance().getTheme())
|
||||
cls.__color_map = numpy.array([
|
||||
|
|
|
@ -11,16 +11,22 @@ from UM.PluginObject import PluginObject
|
|||
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):
|
||||
"""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:
|
||||
"""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__()
|
||||
self._key = key
|
||||
self._label = label
|
||||
|
@ -34,10 +40,14 @@ class MachineAction(QObject, PluginObject):
|
|||
def getKey(self) -> str:
|
||||
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:
|
||||
"""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
|
||||
|
||||
@pyqtProperty(str, notify = labelChanged)
|
||||
|
@ -49,17 +59,24 @@ class MachineAction(QObject, PluginObject):
|
|||
self._label = label
|
||||
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()
|
||||
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._reset()
|
||||
|
||||
## Protected implementation of reset.
|
||||
# /sa reset()
|
||||
def _reset(self) -> None:
|
||||
"""Protected implementation of reset.
|
||||
|
||||
See also :py:meth:`cura.MachineAction.MachineAction.reset`
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -72,8 +89,9 @@ class MachineAction(QObject, PluginObject):
|
|||
def finished(self) -> bool:
|
||||
return self._finished
|
||||
|
||||
## Protected helper to create a view object based on provided QML.
|
||||
def _createViewFromQML(self) -> Optional["QObject"]:
|
||||
"""Protected helper to create a view object based on provided QML."""
|
||||
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
|
|
|
@ -9,47 +9,59 @@ from UM.Logger import Logger
|
|||
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:
|
||||
## Creates a new node for the container tree.
|
||||
# \param container_id The ID of the container that this node should
|
||||
# represent.
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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 = None # type: Optional[InstanceContainer]
|
||||
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]:
|
||||
"""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]
|
||||
|
||||
## 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:
|
||||
"""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)
|
||||
if len(container_metadata) == 0:
|
||||
return 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
|
||||
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:
|
||||
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id)
|
||||
if len(container_list) == 0:
|
||||
|
|
|
@ -19,17 +19,16 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
"""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"]
|
||||
|
||||
@classmethod
|
||||
|
@ -43,13 +42,15 @@ class ContainerTree:
|
|||
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.
|
||||
|
||||
## 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"]:
|
||||
"""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()
|
||||
if global_stack is None:
|
||||
return {}
|
||||
|
@ -58,14 +59,15 @@ class ContainerTree:
|
|||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
||||
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"]:
|
||||
"""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()
|
||||
if global_stack is None:
|
||||
return []
|
||||
|
@ -74,31 +76,43 @@ class ContainerTree:
|
|||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
||||
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:
|
||||
"""Ran after completely starting up the application."""
|
||||
|
||||
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
|
||||
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
|
||||
|
||||
## Dictionary-like object that contains the machines.
|
||||
#
|
||||
# This handles the lazy loading of MachineNodes.
|
||||
class _MachineNodeMap:
|
||||
"""Dictionary-like object that contains the machines.
|
||||
|
||||
This handles the lazy loading of MachineNodes.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
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:
|
||||
"""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
|
||||
|
||||
## 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:
|
||||
"""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:
|
||||
start_time = time.time()
|
||||
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))
|
||||
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]:
|
||||
"""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:
|
||||
return default
|
||||
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:
|
||||
"""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
|
||||
|
||||
## Pre-loads all currently added printers as a background task so that
|
||||
# switching printers in the interface is faster.
|
||||
class _MachineNodeLoadJob(Job):
|
||||
## 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.
|
||||
"""Pre-loads all currently added printers as a background task so that switching printers in the interface is
|
||||
faster.
|
||||
"""
|
||||
|
||||
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.container_stacks = container_stacks
|
||||
super().__init__()
|
||||
|
||||
## Starts the background task.
|
||||
#
|
||||
# The ``JobQueue`` will schedule this on a different thread.
|
||||
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.
|
||||
if not isinstance(stack, GlobalStack):
|
||||
continue
|
||||
|
|
|
@ -11,10 +11,12 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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:
|
||||
super().__init__(container_id)
|
||||
self.quality = quality
|
||||
|
|
|
@ -13,16 +13,16 @@ from UM.Settings.SettingDefinition import SettingDefinition
|
|||
from UM.Settings.Validator import ValidatorState
|
||||
|
||||
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):
|
||||
"""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:
|
||||
super().__init__(parent)
|
||||
|
@ -92,24 +92,37 @@ class MachineErrorChecker(QObject):
|
|||
def needToWaitForResult(self) -> bool:
|
||||
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:
|
||||
"""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":
|
||||
return
|
||||
self.startErrorCheck()
|
||||
|
||||
# Starts the error check timer to schedule a new error check.
|
||||
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:
|
||||
self._need_to_check = True
|
||||
self.needToWaitForResultChanged.emit()
|
||||
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:
|
||||
"""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:
|
||||
self._need_to_check = True
|
||||
self.needToWaitForResultChanged.emit()
|
||||
|
|
|
@ -17,10 +17,12 @@ from cura.Machines.VariantNode import VariantNode
|
|||
import UM.FlameProfiler
|
||||
|
||||
|
||||
## This class represents a machine in the container tree.
|
||||
#
|
||||
# The subnodes of these nodes are variants.
|
||||
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:
|
||||
super().__init__(container_id)
|
||||
self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes.
|
||||
|
@ -47,20 +49,21 @@ class MachineNode(ContainerNode):
|
|||
|
||||
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]:
|
||||
"""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):
|
||||
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 {}
|
||||
|
@ -98,28 +101,26 @@ class MachineNode(ContainerNode):
|
|||
quality_groups[quality_type].is_available = True
|
||||
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]:
|
||||
"""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.
|
||||
|
||||
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())
|
||||
|
||||
## 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":
|
||||
"""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())))
|
||||
|
||||
## (Re)loads all variants under this printer.
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
"""(Re)loads all variants under this printer."""
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
if not self.has_variants:
|
||||
self.variants["empty"] = VariantNode("empty_variant", machine = self)
|
||||
|
|
|
@ -7,18 +7,21 @@ if TYPE_CHECKING:
|
|||
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:
|
||||
"""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")
|
||||
|
||||
def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
|
||||
|
|
|
@ -15,10 +15,12 @@ if TYPE_CHECKING:
|
|||
from cura.Machines.VariantNode import VariantNode
|
||||
|
||||
|
||||
## Represents a material in the container tree.
|
||||
#
|
||||
# Its subcontainers are quality profiles.
|
||||
class MaterialNode(ContainerNode):
|
||||
"""Represents a material in the container tree.
|
||||
|
||||
Its subcontainers are quality profiles.
|
||||
"""
|
||||
|
||||
def __init__(self, container_id: str, variant: "VariantNode") -> None:
|
||||
super().__init__(container_id)
|
||||
self.variant = variant
|
||||
|
@ -34,16 +36,16 @@ class MaterialNode(ContainerNode):
|
|||
container_registry.containerRemoved.connect(self._onRemoved)
|
||||
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:
|
||||
"""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():
|
||||
if self.variant.machine.preferred_quality_type == quality_node.quality_type:
|
||||
return quality_node
|
||||
|
@ -107,10 +109,13 @@ class MaterialNode(ContainerNode):
|
|||
if not self.qualities:
|
||||
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:
|
||||
"""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:
|
||||
# Remove myself from my parent.
|
||||
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.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:
|
||||
"""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:
|
||||
return
|
||||
|
||||
|
|
|
@ -13,11 +13,13 @@ from cura.Machines.ContainerTree import ContainerTree
|
|||
from cura.Machines.MaterialNode import MaterialNode
|
||||
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):
|
||||
"""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()
|
||||
enabledChanged = pyqtSignal()
|
||||
|
@ -121,10 +123,13 @@ class BaseMaterialsModel(ListModel):
|
|||
def enabled(self):
|
||||
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:
|
||||
"""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:
|
||||
return
|
||||
if material.variant.container_id != self._extruder_stack.variant.getId():
|
||||
|
@ -136,14 +141,15 @@ class BaseMaterialsModel(ListModel):
|
|||
return
|
||||
self._onChanged()
|
||||
|
||||
## Triggered when the list of favorite materials is changed.
|
||||
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:
|
||||
self._onChanged()
|
||||
|
||||
## This is an abstract method that needs to be implemented by the specific
|
||||
# models themselves.
|
||||
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(";"))
|
||||
|
||||
# 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()
|
||||
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):
|
||||
"""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
|
||||
if global_stack is None or not self._enabled:
|
||||
return False
|
||||
|
@ -178,9 +184,10 @@ class BaseMaterialsModel(ListModel):
|
|||
|
||||
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):
|
||||
"""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)
|
||||
if not metadata_list:
|
||||
return None
|
||||
|
|
|
@ -14,9 +14,8 @@ if TYPE_CHECKING:
|
|||
from UM.Settings.Interfaces import ContainerInterface
|
||||
|
||||
|
||||
## This model is used for the custom profile items in the profile drop down
|
||||
# menu.
|
||||
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:
|
||||
super().__init__(parent)
|
||||
|
|
|
@ -9,9 +9,9 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class DiscoveredCloudPrintersModel(ListModel):
|
||||
"""
|
||||
Model used to inform the application about newly added cloud printers, which are discovered from the user's account
|
||||
"""
|
||||
"""Model used to inform the application about newly added cloud printers, which are discovered from the user's
|
||||
account """
|
||||
|
||||
DeviceKeyRole = Qt.UserRole + 1
|
||||
DeviceNameRole = Qt.UserRole + 2
|
||||
DeviceTypeRole = Qt.UserRole + 3
|
||||
|
@ -31,18 +31,24 @@ class DiscoveredCloudPrintersModel(ListModel):
|
|||
self._application = application # type: CuraApplication
|
||||
|
||||
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",
|
||||
"name": "NG 001",
|
||||
"machine_type": "Ultimaker S5",
|
||||
"firmware_version": "5.5.12.202001"
|
||||
}
|
||||
|
||||
:param new_devices: List of dictionaries which contain information about added cloud printers.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
self._discovered_cloud_printers_list.extend(new_devices)
|
||||
self._update()
|
||||
|
||||
|
@ -51,21 +57,21 @@ class DiscoveredCloudPrintersModel(ListModel):
|
|||
|
||||
@pyqtSlot()
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
Clears the contents of the DiscoveredCloudPrintersModel.
|
||||
"""Clears the contents of the DiscoveredCloudPrintersModel.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
self._discovered_cloud_printers_list = []
|
||||
self._update()
|
||||
self.cloudPrintersDetectedChanged.emit(False)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
items = self._discovered_cloud_printers_list[:]
|
||||
items.sort(key = lambda k: k["name"])
|
||||
self.setItems(items)
|
||||
|
|
|
@ -115,12 +115,11 @@ class DiscoveredPrinter(QObject):
|
|||
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):
|
||||
"""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:
|
||||
super().__init__(parent)
|
||||
|
@ -265,8 +264,14 @@ class DiscoveredPrintersModel(QObject):
|
|||
del self._discovered_printer_by_ip_dict[ip_address]
|
||||
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")
|
||||
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())
|
||||
|
|
|
@ -15,27 +15,27 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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.
|
||||
IdRole = Qt.UserRole + 1
|
||||
|
||||
## Human-readable name of the extruder.
|
||||
NameRole = Qt.UserRole + 2
|
||||
"""Human-readable name of the extruder."""
|
||||
|
||||
## Colour of the material loaded in the extruder.
|
||||
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
|
||||
"""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.
|
||||
DefinitionRole = Qt.UserRole + 5
|
||||
|
@ -50,18 +50,18 @@ class ExtrudersModel(ListModel):
|
|||
MaterialBrandRole = Qt.UserRole + 9
|
||||
ColorNameRole = Qt.UserRole + 10
|
||||
|
||||
## Is the extruder enabled?
|
||||
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"]
|
||||
"""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):
|
||||
"""Initialises the extruders model, defining the roles and listening for changes in the data.
|
||||
|
||||
:param parent: Parent QtObject of this list.
|
||||
"""
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
|
@ -101,14 +101,15 @@ class ExtrudersModel(ListModel):
|
|||
def addOptionalExtruder(self):
|
||||
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):
|
||||
"""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()
|
||||
if machine_id is not None:
|
||||
if machine_manager.activeMachine is None:
|
||||
|
@ -146,11 +147,13 @@ class ExtrudersModel(ListModel):
|
|||
def _updateExtruders(self):
|
||||
self._update_extruder_timer.start()
|
||||
|
||||
## Update the list of extruders.
|
||||
#
|
||||
# This should be called whenever the list of extruders changes.
|
||||
@UM.FlameProfiler.profile
|
||||
def __updateExtruders(self):
|
||||
"""Update the list of extruders.
|
||||
|
||||
This should be called whenever the list of extruders changes.
|
||||
"""
|
||||
|
||||
extruders_changed = False
|
||||
|
||||
if self.count != 0:
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
import cura.CuraApplication # To listen to changes to the preferences.
|
||||
|
||||
## Model that shows the list of favorite materials.
|
||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||
"""Model that shows the list of favorite materials."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
|
||||
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:
|
||||
"""Triggered when any preference changes, but only handles it when the list of favourites is changed. """
|
||||
|
||||
if preference_key != "cura/favorite_materials":
|
||||
return
|
||||
self._onChanged()
|
||||
|
|
|
@ -11,13 +11,13 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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
|
||||
ContentRole = Qt.UserRole + 2
|
||||
|
@ -73,9 +73,10 @@ class FirstStartMachineActionsModel(ListModel):
|
|||
self._current_action_index += 1
|
||||
self.currentActionIndexChanged.emit()
|
||||
|
||||
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
|
||||
@pyqtSlot()
|
||||
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.currentActionIndexChanged.emit()
|
||||
|
||||
|
|
|
@ -43,8 +43,9 @@ class GlobalStacksModel(ListModel):
|
|||
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||
self._updateDelayed()
|
||||
|
||||
## Handler for container added/removed events from registry
|
||||
def _onContainerChanged(self, container) -> None:
|
||||
"""Handler for container added/removed events from registry"""
|
||||
|
||||
# We only need to update when the added / removed container GlobalStack
|
||||
if isinstance(container, GlobalStack):
|
||||
self._updateDelayed()
|
||||
|
|
|
@ -18,9 +18,9 @@ from UM.i18n import i18nCatalog
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Lists the intent categories that are available for the current printer
|
||||
# configuration.
|
||||
class IntentCategoryModel(ListModel):
|
||||
"""Lists the intent categories that are available for the current printer configuration. """
|
||||
|
||||
NameRole = Qt.UserRole + 1
|
||||
IntentCategoryRole = Qt.UserRole + 2
|
||||
WeightRole = Qt.UserRole + 3
|
||||
|
@ -31,10 +31,12 @@ class IntentCategoryModel(ListModel):
|
|||
|
||||
_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
|
||||
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:
|
||||
cls._translations["default"] = {
|
||||
"name": catalog.i18nc("@label", "Default")
|
||||
|
@ -53,9 +55,12 @@ class IntentCategoryModel(ListModel):
|
|||
}
|
||||
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:
|
||||
"""Creates a new model for a certain intent category.
|
||||
|
||||
:param intent_category: category to list the intent profiles for.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._intent_category = intent_category
|
||||
|
||||
|
@ -84,16 +89,18 @@ class IntentCategoryModel(ListModel):
|
|||
|
||||
self.update()
|
||||
|
||||
## Updates the list of intents if an intent profile was added or removed.
|
||||
def _onContainerChange(self, container: "ContainerInterface") -> None:
|
||||
"""Updates the list of intents if an intent profile was added or removed."""
|
||||
|
||||
if container.getMetaDataEntry("type") == "intent":
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self._update_timer.start()
|
||||
|
||||
## Updates the list of intents.
|
||||
def _update(self) -> None:
|
||||
"""Updates the list of intents."""
|
||||
|
||||
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
|
||||
result = []
|
||||
for category in available_categories:
|
||||
|
@ -109,9 +116,9 @@ class IntentCategoryModel(ListModel):
|
|||
result.sort(key = lambda k: k["weight"])
|
||||
self.setItems(result)
|
||||
|
||||
## Get a display value for a category.
|
||||
## for categories and keys
|
||||
@staticmethod
|
||||
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, {})
|
||||
return display_strings.get(key, default)
|
||||
|
|
|
@ -98,8 +98,9 @@ class IntentModel(ListModel):
|
|||
new_items = sorted(new_items, key = lambda x: x["layer_height"])
|
||||
self.setItems(new_items)
|
||||
|
||||
## Get the active materials for all extruders. No duplicates will be returned
|
||||
def _getActiveMaterials(self) -> Set["MaterialNode"]:
|
||||
"""Get the active materials for all extruders. No duplicates will be returned"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return set()
|
||||
|
|
|
@ -19,28 +19,31 @@ if TYPE_CHECKING:
|
|||
|
||||
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):
|
||||
## Triggered when a favorite is added or removed.
|
||||
# \param The base file of the material is provided as parameter when this
|
||||
# emits.
|
||||
favoritesChanged = pyqtSignal(str)
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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)
|
||||
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()
|
||||
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"):
|
||||
|
@ -48,11 +51,14 @@ class MaterialManagementModel(QObject):
|
|||
return False
|
||||
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)
|
||||
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()
|
||||
root_material_id = material_node.base_file
|
||||
if container_registry.isReadOnly(root_material_id):
|
||||
|
@ -60,18 +66,20 @@ class MaterialManagementModel(QObject):
|
|||
return
|
||||
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")
|
||||
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()
|
||||
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:
|
||||
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,
|
||||
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()
|
||||
|
||||
root_materials = container_registry.findContainers(id = base_file)
|
||||
|
@ -171,29 +181,32 @@ class MaterialManagementModel(QObject):
|
|||
|
||||
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)
|
||||
def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None,
|
||||
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)
|
||||
|
||||
## 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)
|
||||
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.
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
application.saveSettings()
|
||||
|
@ -218,10 +231,13 @@ class MaterialManagementModel(QObject):
|
|||
self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
|
||||
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)
|
||||
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()
|
||||
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
|
||||
if material_base_file not in favorites:
|
||||
|
@ -230,11 +246,13 @@ class MaterialManagementModel(QObject):
|
|||
application.saveSettings()
|
||||
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)
|
||||
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()
|
||||
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
|
||||
try:
|
||||
|
|
|
@ -9,11 +9,11 @@ from UM.Scene.Selection import Selection
|
|||
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):
|
||||
"""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()
|
||||
activeBuildPlateChanged = pyqtSignal()
|
||||
|
@ -39,9 +39,10 @@ class MultiBuildPlateModel(ListModel):
|
|||
self._max_build_plate = max_build_plate
|
||||
self.maxBuildPlateChanged.emit()
|
||||
|
||||
## Return the highest build plate number
|
||||
@pyqtProperty(int, notify = maxBuildPlateChanged)
|
||||
def maxBuildPlate(self):
|
||||
"""Return the highest build plate number"""
|
||||
|
||||
return self._max_build_plate
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
|
|
|
@ -26,10 +26,9 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
#
|
||||
# This the QML model for the quality management page.
|
||||
#
|
||||
class QualityManagementModel(ListModel):
|
||||
"""This the QML model for the quality management page."""
|
||||
|
||||
NameRole = Qt.UserRole + 1
|
||||
IsReadOnlyRole = Qt.UserRole + 2
|
||||
QualityGroupRole = Qt.UserRole + 3
|
||||
|
@ -74,11 +73,13 @@ class QualityManagementModel(ListModel):
|
|||
def _onChange(self) -> None:
|
||||
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)
|
||||
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))
|
||||
removed_quality_changes_ids = set()
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
|
@ -95,16 +96,19 @@ class QualityManagementModel(ListModel):
|
|||
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
|
||||
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)
|
||||
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))
|
||||
if new_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
|
||||
|
||||
## 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")
|
||||
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()
|
||||
if not global_stack:
|
||||
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())
|
||||
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)
|
||||
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()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
@ -220,14 +227,16 @@ class QualityManagementModel(ListModel):
|
|||
|
||||
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":
|
||||
"""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()
|
||||
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||
new_id = base_id + "_" + new_name
|
||||
|
@ -253,11 +262,13 @@ class QualityManagementModel(ListModel):
|
|||
quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
|
||||
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:
|
||||
"""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":
|
||||
self._update()
|
||||
|
||||
|
@ -366,18 +377,19 @@ class QualityManagementModel(ListModel):
|
|||
|
||||
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")
|
||||
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
|
||||
catalog = i18nCatalog("uranium")
|
||||
#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.
|
||||
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):
|
||||
"""Gets a list of profile reader or writer plugins
|
||||
|
||||
:return: List of tuples of (plugin_id, meta_data).
|
||||
"""
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
pr = PluginRegistry.getInstance()
|
||||
active_plugin_ids = pr.getActivePlugins()
|
||||
|
|
|
@ -10,10 +10,9 @@ from cura.Machines.ContainerTree import ContainerTree
|
|||
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):
|
||||
"""QML Model for all built-in quality profiles. This model is used for the drop-down quality menu."""
|
||||
|
||||
NameRole = Qt.UserRole + 1
|
||||
QualityTypeRole = Qt.UserRole + 2
|
||||
LayerHeightRole = Qt.UserRole + 3
|
||||
|
|
|
@ -10,10 +10,9 @@ from UM.Qt.ListModel import ListModel
|
|||
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):
|
||||
"""This model is used to show details settings of the selected quality in the quality management page."""
|
||||
|
||||
KeyRole = Qt.UserRole + 1
|
||||
LabelRole = Qt.UserRole + 2
|
||||
UnitRole = Qt.UserRole + 3
|
||||
|
|
|
@ -6,12 +6,12 @@ from typing import Any, Dict, Optional
|
|||
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):
|
||||
"""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:
|
||||
super().__init__(parent)
|
||||
|
|
|
@ -9,28 +9,34 @@ from UM.Util import parseBool
|
|||
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:
|
||||
## 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.
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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.node_for_global = None # type: Optional[ContainerNode]
|
||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||
|
|
|
@ -13,12 +13,14 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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:
|
||||
super().__init__(container_id)
|
||||
self.parent = parent
|
||||
|
|
|
@ -17,16 +17,16 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""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:
|
||||
super().__init__(container_id)
|
||||
self.machine = machine
|
||||
|
@ -39,9 +39,10 @@ class VariantNode(ContainerNode):
|
|||
container_registry.containerRemoved.connect(self._materialRemoved)
|
||||
self._loadAll()
|
||||
|
||||
## (Re)loads all materials under this variant.
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
"""(Re)loads all materials under this variant."""
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
if not self.machine.has_materials:
|
||||
|
@ -69,18 +70,18 @@ class VariantNode(ContainerNode):
|
|||
if not self.materials:
|
||||
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:
|
||||
"""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():
|
||||
if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
return material_node
|
||||
|
@ -107,10 +108,10 @@ class VariantNode(ContainerNode):
|
|||
))
|
||||
return fallback
|
||||
|
||||
## When a material gets added to the set of profiles, we need to update our
|
||||
# tree here.
|
||||
@UM.FlameProfiler.profile
|
||||
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":
|
||||
return # Not interested.
|
||||
if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):
|
||||
|
|
|
@ -16,23 +16,27 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin
|
|||
catalog = i18nCatalog("cura")
|
||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
## Class containing several helpers to deal with the authorization flow.
|
||||
class AuthorizationHelpers:
|
||||
"""Class containing several helpers to deal with the authorization flow."""
|
||||
|
||||
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||
self._settings = settings
|
||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||
|
||||
@property
|
||||
## The OAuth2 settings object.
|
||||
def settings(self) -> "OAuth2Settings":
|
||||
"""The OAuth2 settings object."""
|
||||
|
||||
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":
|
||||
"""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 = {
|
||||
"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 "",
|
||||
|
@ -46,10 +50,13 @@ class AuthorizationHelpers:
|
|||
except requests.exceptions.ConnectionError:
|
||||
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":
|
||||
"""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.")
|
||||
data = {
|
||||
"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")
|
||||
|
||||
@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":
|
||||
"""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
|
||||
|
||||
try:
|
||||
|
@ -89,10 +99,13 @@ class AuthorizationHelpers:
|
|||
scope=token_data["scope"],
|
||||
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"]:
|
||||
"""Calls the authentication API endpoint to get the token data.
|
||||
|
||||
:param access_token: The encoded JWT token.
|
||||
:return: Dict containing some profile data.
|
||||
"""
|
||||
|
||||
try:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
|
@ -115,16 +128,22 @@ class AuthorizationHelpers:
|
|||
)
|
||||
|
||||
@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:
|
||||
"""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))
|
||||
|
||||
@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:
|
||||
"""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()
|
||||
return b64encode(encoded, altchars = b"_-").decode()
|
||||
|
|
|
@ -14,9 +14,12 @@ if TYPE_CHECKING:
|
|||
|
||||
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):
|
||||
"""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:
|
||||
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.
|
||||
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]]:
|
||||
"""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")
|
||||
state = self._queryGet(query, "state")
|
||||
if state != self.state:
|
||||
|
@ -95,9 +101,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||
), token_response
|
||||
|
||||
## Handle all other non-existing server calls.
|
||||
@staticmethod
|
||||
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.")
|
||||
|
||||
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:
|
||||
self.wfile.write(data)
|
||||
|
||||
## Convenience helper for getting values from a pre-parsed query string
|
||||
@staticmethod
|
||||
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]
|
||||
|
|
|
@ -9,21 +9,26 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
## 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:
|
||||
"""Set the authorization helpers instance on the request handler."""
|
||||
|
||||
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:
|
||||
"""Set the authorization callback on the request handler."""
|
||||
|
||||
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
||||
|
||||
## Set the verification code on the request handler.
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
"""Set the verification code on the request handler."""
|
||||
|
||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
||||
|
||||
def setState(self, state: str) -> None:
|
||||
|
|
|
@ -26,9 +26,11 @@ if TYPE_CHECKING:
|
|||
|
||||
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:
|
||||
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
|
||||
account information.
|
||||
"""
|
||||
|
||||
# Emit signal when authentication is completed.
|
||||
onAuthStateChanged = Signal()
|
||||
|
||||
|
@ -60,11 +62,16 @@ class AuthorizationService:
|
|||
if self._preferences:
|
||||
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"]:
|
||||
"""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 no user profile was stored locally, we try to get it from JWT.
|
||||
try:
|
||||
|
@ -82,9 +89,12 @@ class AuthorizationService:
|
|||
|
||||
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"]:
|
||||
"""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 no auth data exists, we should always log in again.
|
||||
Logger.log("d", "There was no auth data or access token")
|
||||
|
@ -107,8 +117,9 @@ class AuthorizationService:
|
|||
self._storeAuthData(self._auth_data)
|
||||
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]:
|
||||
"""Get the access token as provided by the repsonse data."""
|
||||
|
||||
if self._auth_data is None:
|
||||
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||
return None
|
||||
|
@ -123,8 +134,9 @@ class AuthorizationService:
|
|||
|
||||
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:
|
||||
"""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:
|
||||
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
|
||||
return
|
||||
|
@ -136,14 +148,16 @@ class AuthorizationService:
|
|||
Logger.log("w", "Failed to get a new access token from the server.")
|
||||
self.onAuthStateChanged.emit(logged_in = False)
|
||||
|
||||
## Delete the authentication data that we have stored locally (eg; logout)
|
||||
def deleteAuthData(self) -> None:
|
||||
"""Delete the authentication data that we have stored locally (eg; logout)"""
|
||||
|
||||
if self._auth_data is not None:
|
||||
self._storeAuthData()
|
||||
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:
|
||||
"""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...")
|
||||
|
||||
# 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))
|
||||
return auth_url
|
||||
|
||||
## Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
"""Callback method for the authentication flow."""
|
||||
|
||||
if auth_response.success:
|
||||
self._storeAuthData(auth_response)
|
||||
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._server.stop() # Stop the web server at all times.
|
||||
|
||||
## Load authentication data from preferences.
|
||||
def loadAuthDataFromPreferences(self) -> None:
|
||||
"""Load authentication data from preferences."""
|
||||
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
||||
return
|
||||
|
@ -228,8 +244,9 @@ class AuthorizationService:
|
|||
except ValueError:
|
||||
Logger.logException("w", "Could not load auth data from preferences")
|
||||
|
||||
## Store authentication data in preferences.
|
||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||
"""Store authentication data in preferences."""
|
||||
|
||||
Logger.log("d", "Attempting to store the auth data")
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||
|
|
|
@ -20,18 +20,23 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
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",
|
||||
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
|
||||
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_thread = None # type: Optional[threading.Thread]
|
||||
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._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:
|
||||
"""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 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.
|
||||
|
@ -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.start()
|
||||
|
||||
## Stops the web server if it was running. It also does some cleanup.
|
||||
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...")
|
||||
|
||||
if self._web_server:
|
||||
|
|
|
@ -8,8 +8,9 @@ class BaseModel:
|
|||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
## OAuth OAuth2Settings data template.
|
||||
class OAuth2Settings(BaseModel):
|
||||
"""OAuth OAuth2Settings data template."""
|
||||
|
||||
CALLBACK_PORT = None # type: Optional[int]
|
||||
OAUTH_SERVER_URL = 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
|
||||
|
||||
|
||||
## User profile data template.
|
||||
class UserProfile(BaseModel):
|
||||
"""User profile data template."""
|
||||
|
||||
user_id = None # type: Optional[str]
|
||||
username = None # type: Optional[str]
|
||||
profile_image_url = None # type: Optional[str]
|
||||
|
||||
|
||||
## Authentication data template.
|
||||
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
|
||||
token_type = None # type: Optional[str]
|
||||
access_token = None # type: Optional[str]
|
||||
|
@ -40,22 +43,25 @@ class AuthenticationResponse(BaseModel):
|
|||
received_at = None # type: Optional[str]
|
||||
|
||||
|
||||
## Response status template.
|
||||
class ResponseStatus(BaseModel):
|
||||
"""Response status template."""
|
||||
|
||||
code = 200 # type: int
|
||||
message = "" # type: str
|
||||
|
||||
|
||||
## Response data template.
|
||||
class ResponseData(BaseModel):
|
||||
"""Response data template."""
|
||||
|
||||
status = None # type: ResponseStatus
|
||||
data_stream = None # type: Optional[bytes]
|
||||
redirect_uri = None # type: Optional[str]
|
||||
content_type = "text/html" # type: str
|
||||
|
||||
|
||||
## Possible HTTP responses.
|
||||
HTTP_STATUS = {
|
||||
"""Possible HTTP responses."""
|
||||
|
||||
"OK": ResponseStatus(code = 200, message = "OK"),
|
||||
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
|
||||
|
|
|
@ -7,18 +7,21 @@ from UM.Scene.Iterator import Iterator
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
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):
|
||||
"""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:
|
||||
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._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:
|
||||
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
|
||||
|
||||
node_list = []
|
||||
for node in self._scene_node.getChildren():
|
||||
if not issubclass(type(node), SceneNode):
|
||||
|
@ -75,10 +78,14 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||
return True
|
||||
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:
|
||||
"""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)
|
||||
for other_node in other_nodes:
|
||||
other_node_index = self._original_node_list.index(other_node)
|
||||
|
@ -86,14 +93,26 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||
return True
|
||||
return False
|
||||
|
||||
## Calculate score simply sums the number of other objects it 'blocks'
|
||||
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_b = sum(self._hit_map[self._original_node_list.index(b)])
|
||||
return score_a - score_b
|
||||
|
||||
## Checks if A can be printed before B
|
||||
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:
|
||||
return False
|
||||
|
||||
|
@ -116,12 +135,14 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||
return False
|
||||
|
||||
|
||||
## Internal object used to keep track of a possible order in which to print objects.
|
||||
class _ObjectOrder:
|
||||
## 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.
|
||||
"""Internal object used to keep track of a possible order in which to print objects."""
|
||||
|
||||
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.todo = todo
|
||||
|
|
|
@ -6,8 +6,9 @@ from UM.Operations.GroupedOperation import GroupedOperation
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
|
||||
## A specialised operation designed specifically to modify the previous operation.
|
||||
class PlatformPhysicsOperation(Operation):
|
||||
"""A specialised operation designed specifically to modify the previous operation."""
|
||||
|
||||
def __init__(self, node: SceneNode, translation: Vector) -> None:
|
||||
super().__init__()
|
||||
self._node = node
|
||||
|
|
|
@ -7,8 +7,9 @@ from UM.Operations.Operation import Operation
|
|||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## Simple operation to set the buildplate number of a scenenode.
|
||||
class SetBuildPlateNumberOperation(Operation):
|
||||
"""Simple operation to set the buildplate number of a scenenode."""
|
||||
|
||||
def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
|
||||
super().__init__()
|
||||
self._node = node
|
||||
|
|
|
@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode
|
|||
from UM.Operations import Operation
|
||||
|
||||
|
||||
|
||||
## An operation that parents a scene node to another scene node.
|
||||
class SetParentOperation(Operation.Operation):
|
||||
## Initialises this SetParentOperation.
|
||||
#
|
||||
# \param node The node which will be reparented.
|
||||
# \param parent_node The node which will be the parent.
|
||||
"""An operation that parents a scene node to another scene node."""
|
||||
|
||||
def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None:
|
||||
"""Initialises this SetParentOperation.
|
||||
|
||||
:param node: The node which will be reparented.
|
||||
:param parent_node: The node which will be the parent.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._node = node
|
||||
self._parent = parent_node
|
||||
self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
|
||||
|
||||
## Undoes the set-parent operation, restoring the old parent.
|
||||
def undo(self) -> None:
|
||||
"""Undoes the set-parent operation, restoring the old parent."""
|
||||
|
||||
self._set_parent(self._old_parent)
|
||||
|
||||
## Re-applies the set-parent operation.
|
||||
def redo(self) -> None:
|
||||
"""Re-applies the set-parent operation."""
|
||||
|
||||
self._set_parent(self._parent)
|
||||
|
||||
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
|
||||
#
|
||||
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
|
||||
def _set_parent(self, new_parent: Optional[SceneNode]) -> None:
|
||||
"""Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
|
||||
|
||||
:param new_parent: The new parent. Note: this argument can be None, which would hide the node from the scene.
|
||||
"""
|
||||
|
||||
if new_parent:
|
||||
current_parent = self._node.getParent()
|
||||
if current_parent:
|
||||
|
@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation):
|
|||
|
||||
self._node.setParent(new_parent)
|
||||
|
||||
## Returns a programmer-readable representation of this operation.
|
||||
#
|
||||
# \return A programmer-readable representation of this operation.
|
||||
def __repr__(self) -> str:
|
||||
"""Returns a programmer-readable representation of this operation.
|
||||
|
||||
:return: A programmer-readable representation of this operation.
|
||||
"""
|
||||
|
||||
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)
|
||||
|
|
|
@ -18,11 +18,15 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
if TYPE_CHECKING:
|
||||
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):
|
||||
"""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:
|
||||
super().__init__("picking", width, height)
|
||||
|
||||
|
@ -56,8 +60,14 @@ class PickingPass(RenderPass):
|
|||
batch.render(self._scene.getActiveCamera())
|
||||
self.release()
|
||||
|
||||
## Get the distance in mm from the camera to at a certain pixel coordinate.
|
||||
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()
|
||||
|
||||
window_size = self._renderer.getWindowSize()
|
||||
|
@ -72,8 +82,14 @@ class PickingPass(RenderPass):
|
|||
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm
|
||||
return distance
|
||||
|
||||
## Get the world coordinates of a picked point
|
||||
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)
|
||||
camera = self._scene.getActiveCamera()
|
||||
if camera:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# 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
|
||||
|
@ -21,9 +21,14 @@ if TYPE_CHECKING:
|
|||
from UM.Scene.Camera import Camera
|
||||
|
||||
|
||||
# Make color brighter by normalizing it (maximum factor 2.5 brighter)
|
||||
# color_list is a list of 4 elements: [r, g, b, a], each element is a float 0..1
|
||||
def prettier_color(color_list):
|
||||
def prettier_color(color_list: List[float]) -> List[float]:
|
||||
"""Make color brighter by normalizing
|
||||
|
||||
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])
|
||||
if maximum > 0:
|
||||
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]
|
||||
|
||||
|
||||
## 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):
|
||||
"""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:
|
||||
super().__init__("preview", width, height, 0)
|
||||
|
||||
|
|
|
@ -10,8 +10,14 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
|
|||
def __init__(self):
|
||||
super().__init__(QQuickImageProvider.Image)
|
||||
|
||||
## Request a new image.
|
||||
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
|
||||
# increment, we need to strip that first.
|
||||
uuid = id[id.find("/") + 1:]
|
||||
|
|
|
@ -44,8 +44,9 @@ class FirmwareUpdater(QObject):
|
|||
def _updateFirmware(self) -> None:
|
||||
raise NotImplementedError("_updateFirmware needs to be implemented")
|
||||
|
||||
## Cleanup after a succesful update
|
||||
def _cleanupAfterUpdate(self) -> None:
|
||||
"""Cleanup after a succesful update"""
|
||||
|
||||
# Clean up for next attempt.
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
self._firmware_file = ""
|
||||
|
|
|
@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject):
|
|||
def hotendID(self) -> Optional[str]:
|
||||
return self._hotend_id
|
||||
|
||||
## This method is intended to indicate whether the configuration is valid or not.
|
||||
# The method checks if the mandatory fields are or not set
|
||||
# At this moment is always valid since we allow to have empty material and variants.
|
||||
def isValid(self) -> bool:
|
||||
"""This method is intended to indicate whether the configuration is valid or not.
|
||||
|
||||
The method checks if the mandatory fields are or not set
|
||||
At this moment is always valid since we allow to have empty material and variants.
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject):
|
|||
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
|
||||
self._extruder_configuration.setMaterial(material)
|
||||
|
||||
## Update the hotend temperature. This only changes it locally.
|
||||
def updateHotendTemperature(self, temperature: float) -> None:
|
||||
"""Update the hotend temperature. This only changes it locally."""
|
||||
|
||||
if self._hotend_temperature != temperature:
|
||||
self._hotend_temperature = temperature
|
||||
self.hotendTemperatureChanged.emit()
|
||||
|
@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject):
|
|||
self._target_hotend_temperature = temperature
|
||||
self.targetHotendTemperatureChanged.emit()
|
||||
|
||||
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(float)
|
||||
def setTargetHotendTemperature(self, temperature: float) -> None:
|
||||
"""Set the target hotend temperature. This ensures that it's actually sent to the remote."""
|
||||
|
||||
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
|
||||
self.updateTargetHotendTemperature(temperature)
|
||||
|
||||
|
@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject):
|
|||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
## Pre-heats the extruder before printer.
|
||||
#
|
||||
# \param temperature The temperature to heat the extruder to, in degrees
|
||||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatHotend(self, temperature: float, duration: float) -> None:
|
||||
"""Pre-heats the extruder before printer.
|
||||
|
||||
:param temperature: The temperature to heat the extruder to, in degrees
|
||||
Celsius.
|
||||
:param duration: How long the bed should stay warm, in seconds.
|
||||
"""
|
||||
|
||||
self._printer._controller.preheatHotend(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
|
|
|
@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject):
|
|||
def buildplateConfiguration(self) -> str:
|
||||
return self._buildplate_configuration
|
||||
|
||||
## This method is intended to indicate whether the configuration is valid or not.
|
||||
# The method checks if the mandatory fields are or not set
|
||||
def isValid(self) -> bool:
|
||||
"""This method is intended to indicate whether the configuration is valid or not.
|
||||
|
||||
The method checks if the mandatory fields are or not set
|
||||
"""
|
||||
if not self._extruder_configurations:
|
||||
return False
|
||||
for configuration in self._extruder_configurations:
|
||||
|
@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject):
|
|||
|
||||
return True
|
||||
|
||||
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
||||
def __hash__(self):
|
||||
"""The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||
|
||||
of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
||||
"""
|
||||
extruder_hash = hash(0)
|
||||
first_extruder = None
|
||||
for configuration in self._extruder_configurations:
|
||||
|
|
|
@ -163,13 +163,15 @@ class PrinterOutputModel(QObject):
|
|||
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
||||
self._controller.moveHead(self, x, y, z, speed)
|
||||
|
||||
## Pre-heats the heated bed of the printer.
|
||||
#
|
||||
# \param temperature The temperature to heat the bed to, in degrees
|
||||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||
"""Pre-heats the heated bed of the printer.
|
||||
|
||||
:param temperature: The temperature to heat the bed to, in degrees
|
||||
Celsius.
|
||||
:param duration: How long the bed should stay warm, in seconds.
|
||||
"""
|
||||
|
||||
self._controller.preheatBed(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -200,8 +202,9 @@ class PrinterOutputModel(QObject):
|
|||
self._unique_name = unique_name
|
||||
self.nameChanged.emit()
|
||||
|
||||
## Update the bed temperature. This only changes it locally.
|
||||
def updateBedTemperature(self, temperature: float) -> None:
|
||||
"""Update the bed temperature. This only changes it locally."""
|
||||
|
||||
if self._bed_temperature != temperature:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
@ -211,9 +214,10 @@ class PrinterOutputModel(QObject):
|
|||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(float)
|
||||
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||
"""Set the target bed temperature. This ensures that it's actually sent to the remote."""
|
||||
|
||||
self._controller.setTargetBedTemperature(self, temperature)
|
||||
self.updateTargetBedTemperature(temperature)
|
||||
|
||||
|
|
|
@ -32,8 +32,9 @@ class NetworkMJPGImage(QQuickPaintedItem):
|
|||
|
||||
self.setAntialiasing(True)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
"""Ensure that close gets called when object is destroyed"""
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
|
|
|
@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
def _compressGCode(self) -> Optional[bytes]:
|
||||
self._compressing_gcode = True
|
||||
|
||||
## Mash the data into single string
|
||||
max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
|
||||
"""Mash the data into single string"""
|
||||
file_data_bytes_list = []
|
||||
batched_lines = []
|
||||
batched_lines_count = 0
|
||||
|
@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||
return request
|
||||
|
||||
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
|
||||
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
|
||||
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||
"""This method was only available privately before, but it was actually called from SendMaterialJob.py.
|
||||
|
||||
We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
|
||||
"""
|
||||
return self._createFormPart(content_header, data, content_type)
|
||||
|
||||
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||
|
@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
part.setBody(data)
|
||||
return part
|
||||
|
||||
## Convenience function to get the username, either from the cloud or from the OS.
|
||||
def _getUserName(self) -> str:
|
||||
"""Convenience function to get the username, either from the cloud or from the OS."""
|
||||
|
||||
# check first if we are logged in with the Ultimaker Account
|
||||
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||
if account and account.isLoggedIn:
|
||||
|
@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._createNetworkManager()
|
||||
assert (self._manager is not None)
|
||||
|
||||
## Sends a put request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param data: The data to be sent in the body
|
||||
# \param content_type: The content type of the body data.
|
||||
# \param on_finished: The function to call when the response is received.
|
||||
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json",
|
||||
on_finished: Optional[Callable[[QNetworkReply], None]] = None,
|
||||
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||
"""Sends a put request to the given path.
|
||||
|
||||
:param url: The path after the API prefix.
|
||||
:param data: The data to be sent in the body
|
||||
:param content_type: The content type of the body data.
|
||||
:param on_finished: The function to call when the response is received.
|
||||
:param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
"""
|
||||
self._validateManager()
|
||||
|
||||
request = self._createEmptyRequest(url, content_type = content_type)
|
||||
|
@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
|
||||
## Sends a delete request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param on_finished: The function to be call when the response is received.
|
||||
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
"""Sends a delete request to the given path.
|
||||
|
||||
:param url: The path after the API prefix.
|
||||
:param on_finished: The function to be call when the response is received.
|
||||
"""
|
||||
self._validateManager()
|
||||
|
||||
request = self._createEmptyRequest(url)
|
||||
|
@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
reply = self._manager.deleteResource(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
## Sends a get request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param on_finished: The function to be call when the response is received.
|
||||
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
"""Sends a get request to the given path.
|
||||
|
||||
:param url: The path after the API prefix.
|
||||
:param on_finished: The function to be call when the response is received.
|
||||
"""
|
||||
self._validateManager()
|
||||
|
||||
request = self._createEmptyRequest(url)
|
||||
|
@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
reply = self._manager.get(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
## Sends a post request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param data: The data to be sent in the body
|
||||
# \param on_finished: The function to call when the response is received.
|
||||
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
def post(self, url: str, data: Union[str, bytes],
|
||||
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||
|
||||
"""Sends a post request to the given path.
|
||||
|
||||
:param url: The path after the API prefix.
|
||||
:param data: The data to be sent in the body
|
||||
:param on_finished: The function to call when the response is received.
|
||||
:param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
"""
|
||||
|
||||
self._validateManager()
|
||||
|
||||
request = self._createEmptyRequest(url)
|
||||
|
@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
if on_finished is not None:
|
||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
||||
|
||||
## This method checks if the name of the group stored in the definition container is correct.
|
||||
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
||||
# then all the container stacks are updated, both the current and the hidden ones.
|
||||
def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
||||
"""This method checks if the name of the group stored in the definition container is correct.
|
||||
|
||||
After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
||||
then all the container stacks are updated, both the current and the hidden ones.
|
||||
"""
|
||||
|
||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
|
||||
if global_container_stack and device_id == active_machine_network_name:
|
||||
|
@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
def getProperties(self):
|
||||
return self._properties
|
||||
|
||||
## Get the unique key of this machine
|
||||
# \return key String containing the key of the machine.
|
||||
@pyqtProperty(str, constant = True)
|
||||
def key(self) -> str:
|
||||
"""Get the unique key of this machine
|
||||
|
||||
:return: key String containing the key of the machine.
|
||||
"""
|
||||
return self._id
|
||||
|
||||
## The IP address of the printer.
|
||||
@pyqtProperty(str, constant = True)
|
||||
def address(self) -> str:
|
||||
"""The IP address of the printer."""
|
||||
|
||||
return self._properties.get(b"address", b"").decode("utf-8")
|
||||
|
||||
## Name of the printer (as returned from the ZeroConf properties)
|
||||
@pyqtProperty(str, constant = True)
|
||||
def name(self) -> str:
|
||||
"""Name of the printer (as returned from the ZeroConf properties)"""
|
||||
|
||||
return self._properties.get(b"name", b"").decode("utf-8")
|
||||
|
||||
## Firmware version (as returned from the ZeroConf properties)
|
||||
@pyqtProperty(str, constant = True)
|
||||
def firmwareVersion(self) -> str:
|
||||
"""Firmware version (as returned from the ZeroConf properties)"""
|
||||
|
||||
return self._properties.get(b"firmware_version", b"").decode("utf-8")
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def printerType(self) -> str:
|
||||
return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
|
||||
|
||||
## IP adress of this printer
|
||||
@pyqtProperty(str, constant = True)
|
||||
def ipAddress(self) -> str:
|
||||
"""IP adress of this printer"""
|
||||
|
||||
return self._address
|
||||
|
|
|
@ -2,15 +2,19 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
|
||||
## Data class that represents a peripheral for a printer.
|
||||
#
|
||||
# Output device plug-ins may specify that the printer has a certain set of
|
||||
# peripherals. This set is then possibly shown in the interface of the monitor
|
||||
# stage.
|
||||
class Peripheral:
|
||||
## Constructs the peripheral.
|
||||
# \param type A unique ID for the type of peripheral.
|
||||
# \param name A human-readable name for the peripheral.
|
||||
"""Data class that represents a peripheral for a printer.
|
||||
|
||||
Output device plug-ins may specify that the printer has a certain set of
|
||||
peripherals. This set is then possibly shown in the interface of the monitor
|
||||
stage.
|
||||
"""
|
||||
|
||||
def __init__(self, peripheral_type: str, name: str) -> None:
|
||||
"""Constructs the peripheral.
|
||||
|
||||
:param peripheral_type: A unique ID for the type of peripheral.
|
||||
:param name: A human-readable name for the peripheral.
|
||||
"""
|
||||
self.type = peripheral_type
|
||||
self.name = name
|
||||
|
|
|
@ -24,8 +24,9 @@ if MYPY:
|
|||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
"""The current processing state of the backend."""
|
||||
|
||||
Closed = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
|
@ -40,17 +41,19 @@ class ConnectionType(IntEnum):
|
|||
CloudConnection = 3
|
||||
|
||||
|
||||
## Printer output device adds extra interface options on top of output device.
|
||||
#
|
||||
# The assumption is made the printer is a FDM printer.
|
||||
#
|
||||
# Note that a number of settings are marked as "final". This is because decorators
|
||||
# are not inherited by children. To fix this we use the private counter part of those
|
||||
# functions to actually have the implementation.
|
||||
#
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
"""Printer output device adds extra interface options on top of output device.
|
||||
|
||||
The assumption is made the printer is a FDM printer.
|
||||
|
||||
Note that a number of settings are marked as "final". This is because decorators
|
||||
are not inherited by children. To fix this we use the private counter part of those
|
||||
functions to actually have the implementation.
|
||||
|
||||
For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
"""
|
||||
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
|
@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
if self._monitor_item is None:
|
||||
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self) -> None:
|
||||
"""Attempt to establish connection"""
|
||||
|
||||
self.setConnectionState(ConnectionState.Connecting)
|
||||
self._update_timer.start()
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self) -> None:
|
||||
"""Attempt to close the connection"""
|
||||
|
||||
self._update_timer.stop()
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
"""Ensure that close gets called when object is destroyed"""
|
||||
|
||||
self.close()
|
||||
|
||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||
def acceptsCommands(self) -> bool:
|
||||
return self._accepts_commands
|
||||
|
||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
"""Set a flag to signal the UI that the printer is not (yet) ready to receive commands"""
|
||||
|
||||
if self._accepts_commands != accepts_commands:
|
||||
self._accepts_commands = accepts_commands
|
||||
|
||||
|
@ -241,16 +248,20 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
# At this point there may be non-updated configurations
|
||||
self._updateUniqueConfigurations()
|
||||
|
||||
## Set the device firmware name
|
||||
#
|
||||
# \param name The name of the firmware.
|
||||
def _setFirmwareName(self, name: str) -> None:
|
||||
"""Set the device firmware name
|
||||
|
||||
:param name: The name of the firmware.
|
||||
"""
|
||||
|
||||
self._firmware_name = name
|
||||
|
||||
## Get the name of device firmware
|
||||
#
|
||||
# This name can be used to define device type
|
||||
def getFirmwareName(self) -> Optional[str]:
|
||||
"""Get the name of device firmware
|
||||
|
||||
This name can be used to define device type
|
||||
"""
|
||||
|
||||
return self._firmware_name
|
||||
|
||||
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||
|
|
|
@ -10,15 +10,19 @@ class NoProfileException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
## A type of plug-ins that reads profiles from a file.
|
||||
#
|
||||
# The profile is then stored as instance container of the type user profile.
|
||||
class ProfileReader(PluginObject):
|
||||
"""A type of plug-ins that reads profiles from a file.
|
||||
|
||||
The profile is then stored as instance container of the type user profile.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Read profile data from a file and return a filled profile.
|
||||
#
|
||||
# \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles.
|
||||
def read(self, file_name):
|
||||
"""Read profile data from a file and return a filled profile.
|
||||
|
||||
:return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles.
|
||||
"""
|
||||
|
||||
raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.")
|
||||
|
|
|
@ -3,23 +3,29 @@
|
|||
|
||||
from UM.PluginObject import PluginObject
|
||||
|
||||
## Base class for profile writer plugins.
|
||||
#
|
||||
# This class defines a write() function to write profiles to files with.
|
||||
|
||||
class ProfileWriter(PluginObject):
|
||||
## Initialises the profile writer.
|
||||
#
|
||||
# This currently doesn't do anything since the writer is basically static.
|
||||
"""Base class for profile writer plugins.
|
||||
|
||||
This class defines a write() function to write profiles to files with.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialises the profile writer.
|
||||
|
||||
This currently doesn't do anything since the writer is basically static.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
## Writes a profile to the specified file path.
|
||||
#
|
||||
# The profile writer may write its own file format to the specified file.
|
||||
#
|
||||
# \param path \type{string} The file to output to.
|
||||
# \param profiles \type{Profile} or \type{List} The profile(s) to write to the file.
|
||||
# \return \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
def write(self, path, profiles):
|
||||
"""Writes a profile to the specified file path.
|
||||
|
||||
The profile writer may write its own file format to the specified file.
|
||||
|
||||
:param path: :type{string} The file to output to.
|
||||
:param profiles: :type{Profile} or :type{List} The profile(s) to write to the file.
|
||||
:return: True if the writing was successful, or False if it wasn't.
|
||||
"""
|
||||
|
||||
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")
|
||||
|
|
|
@ -2,8 +2,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
|||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
|
||||
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
|
||||
class BuildPlateDecorator(SceneNodeDecorator):
|
||||
"""Make a SceneNode build plate aware CuraSceneNode objects all have this decorator."""
|
||||
|
||||
def __init__(self, build_plate_number: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._build_plate_number = build_plate_number
|
||||
|
|
|
@ -23,9 +23,12 @@ if TYPE_CHECKING:
|
|||
from UM.Math.Matrix import Matrix
|
||||
|
||||
|
||||
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
|
||||
# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
|
||||
class ConvexHullDecorator(SceneNodeDecorator):
|
||||
"""The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
|
||||
|
||||
If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
self._onChanged()
|
||||
|
||||
## Force that a new (empty) object is created upon copy.
|
||||
def __deepcopy__(self, memo):
|
||||
"""Force that a new (empty) object is created upon copy."""
|
||||
|
||||
return ConvexHullDecorator()
|
||||
|
||||
## The polygon representing the 2D adhesion area.
|
||||
# If no adhesion is used, the regular convex hull is returned
|
||||
def getAdhesionArea(self) -> Optional[Polygon]:
|
||||
"""The polygon representing the 2D adhesion area.
|
||||
|
||||
If no adhesion is used, the regular convex hull is returned
|
||||
"""
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
|
@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
return self._add2DAdhesionMargin(hull)
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node (if any)
|
||||
# In case of one-at-a-time, this includes adhesion and head+fans clearance
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
"""Get the unmodified 2D projected convex hull of the node (if any)
|
||||
|
||||
In case of one-at-a-time, this includes adhesion and head+fans clearance
|
||||
"""
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
|
@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
return self._compute2DConvexHull()
|
||||
|
||||
## For one at the time this is the convex hull of the node with the full head size
|
||||
# In case of printing all at once this is None.
|
||||
def getConvexHullHeadFull(self) -> Optional[Polygon]:
|
||||
"""For one at the time this is the convex hull of the node with the full head size
|
||||
|
||||
In case of printing all at once this is None.
|
||||
"""
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
|
@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return False
|
||||
return bool(parent.callDecoration("isGroup"))
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is None.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
"""Get convex hull of the object + head size
|
||||
|
||||
In case of printing all at once this is None.
|
||||
For one at the time this is area with intersection of mirrored head
|
||||
"""
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
|
@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
|
||||
## Get convex hull of the node
|
||||
# In case of printing all at once this None??
|
||||
# For one at the time this is the area without the head.
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
"""Get convex hull of the node
|
||||
|
||||
In case of printing all at once this None??
|
||||
For one at the time this is the area without the head.
|
||||
"""
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
|
@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
||||
## Get the buildplate polygon where will be printed
|
||||
# In case of printing all at once this is the same as convex hull (no individual adhesion)
|
||||
# For one at the time this includes the adhesion area
|
||||
def getPrintingArea(self) -> Optional[Polygon]:
|
||||
"""Get the buildplate polygon where will be printed
|
||||
|
||||
In case of printing all at once this is the same as convex hull (no individual adhesion)
|
||||
For one at the time this includes the adhesion area
|
||||
"""
|
||||
if self._isSingularOneAtATimeNode():
|
||||
# In one-at-a-time mode, every printed object gets it's own adhesion
|
||||
printing_area = self.getAdhesionArea()
|
||||
|
@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
printing_area = self.getConvexHull()
|
||||
return printing_area
|
||||
|
||||
## The same as recomputeConvexHull, but using a timer if it was set.
|
||||
def recomputeConvexHullDelayed(self) -> None:
|
||||
"""The same as recomputeConvexHull, but using a timer if it was set."""
|
||||
|
||||
if self._recompute_convex_hull_timer is not None:
|
||||
self._recompute_convex_hull_timer.start()
|
||||
else:
|
||||
|
@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||
return None
|
||||
|
||||
## Compensate given 2D polygon with adhesion margin
|
||||
# \return 2D polygon with added margin
|
||||
def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon:
|
||||
"""Compensate given 2D polygon with adhesion margin
|
||||
|
||||
:return: 2D polygon with added margin
|
||||
"""
|
||||
if not self._global_stack:
|
||||
return Polygon()
|
||||
# Compensate for raft/skirt/brim
|
||||
|
@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
poly = poly.getMinkowskiHull(extra_margin_polygon)
|
||||
return poly
|
||||
|
||||
## Offset the convex hull with settings that influence the collision area.
|
||||
#
|
||||
# \param convex_hull Polygon of the original convex hull.
|
||||
# \return New Polygon instance that is offset with everything that
|
||||
# influences the collision area.
|
||||
def _offsetHull(self, convex_hull: Polygon) -> Polygon:
|
||||
"""Offset the convex hull with settings that influence the collision area.
|
||||
|
||||
:param convex_hull: Polygon of the original convex hull.
|
||||
:return: New Polygon instance that is offset with everything that
|
||||
influences the collision area.
|
||||
"""
|
||||
|
||||
horizontal_expansion = max(
|
||||
self._getSettingProperty("xy_offset", "value"),
|
||||
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||
|
@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
self._onChanged()
|
||||
|
||||
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
||||
def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
|
||||
"""Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property)."""
|
||||
|
||||
if self._global_stack is None or self._node is None:
|
||||
return None
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
|
@ -430,16 +452,18 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
# Limit_to_extruder is set. The global stack handles this then
|
||||
return self._global_stack.getProperty(setting_key, prop)
|
||||
|
||||
## Returns True if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool:
|
||||
"""Returns True if node is a descendant or the same as the root node."""
|
||||
|
||||
if node is None:
|
||||
return False
|
||||
if root is node:
|
||||
return True
|
||||
return self.__isDescendant(root, node.getParent())
|
||||
|
||||
## True if print_sequence is one_at_a_time and _node is not part of a group
|
||||
def _isSingularOneAtATimeNode(self) -> bool:
|
||||
"""True if print_sequence is one_at_a_time and _node is not part of a group"""
|
||||
|
||||
if self._node is None:
|
||||
return False
|
||||
return self._global_stack is not None \
|
||||
|
@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
"adhesion_type", "raft_margin", "print_sequence",
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||
|
||||
## Settings that change the convex hull.
|
||||
#
|
||||
# If these settings change, the convex hull should be recalculated.
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
"""Settings that change the convex hull.
|
||||
|
||||
If these settings change, the convex hull should be recalculated.
|
||||
"""
|
||||
|
|
|
@ -18,11 +18,13 @@ if TYPE_CHECKING:
|
|||
class ConvexHullNode(SceneNode):
|
||||
shader = None # To prevent the shader from being re-built over and over again, only load it once.
|
||||
|
||||
## Convex hull node is a special type of scene node that is used to display an area, to indicate the
|
||||
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||
# to represent the raft as well.
|
||||
def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
|
||||
"""Convex hull node is a special type of scene node that is used to display an area, to indicate the
|
||||
|
||||
location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||
then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||
to represent the raft as well.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
|
|
|
@ -72,9 +72,10 @@ class CuraSceneController(QObject):
|
|||
max_build_plate = max(build_plate_number, max_build_plate)
|
||||
return max_build_plate
|
||||
|
||||
## Either select or deselect an item
|
||||
@pyqtSlot(int)
|
||||
def changeSelection(self, index):
|
||||
"""Either select or deselect an item"""
|
||||
|
||||
modifiers = QApplication.keyboardModifiers()
|
||||
ctrl_is_active = modifiers & Qt.ControlModifier
|
||||
shift_is_active = modifiers & Qt.ShiftModifier
|
||||
|
|
|
@ -15,9 +15,11 @@ from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
|||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||
|
||||
|
||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||
# Note that many other nodes can just be UM SceneNode objects.
|
||||
class CuraSceneNode(SceneNode):
|
||||
"""Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||
|
||||
Note that many other nodes can just be UM SceneNode objects.
|
||||
"""
|
||||
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||
super().__init__(parent = parent, visible = visible, name = name)
|
||||
if not no_setting_override:
|
||||
|
@ -36,9 +38,11 @@ class CuraSceneNode(SceneNode):
|
|||
def isSelectable(self) -> bool:
|
||||
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
|
||||
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
|
||||
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
|
||||
def getPrintingExtruder(self) -> Optional[ExtruderStack]:
|
||||
"""Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
|
||||
|
||||
TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
|
||||
"""
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None:
|
||||
return None
|
||||
|
@ -69,8 +73,9 @@ class CuraSceneNode(SceneNode):
|
|||
# This point should never be reached
|
||||
return None
|
||||
|
||||
## Return the color of the material used to print this model
|
||||
def getDiffuseColor(self) -> List[float]:
|
||||
"""Return the color of the material used to print this model"""
|
||||
|
||||
printing_extruder = self.getPrintingExtruder()
|
||||
|
||||
material_color = "#808080" # Fallback color
|
||||
|
@ -86,8 +91,9 @@ class CuraSceneNode(SceneNode):
|
|||
1.0
|
||||
]
|
||||
|
||||
## Return if any area collides with the convex hull of this scene node
|
||||
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||
"""Return if any area collides with the convex hull of this scene node"""
|
||||
|
||||
convex_hull = self.callDecoration("getPrintingArea")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
|
@ -101,8 +107,9 @@ class CuraSceneNode(SceneNode):
|
|||
return True
|
||||
return False
|
||||
|
||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||
def _calculateAABB(self) -> None:
|
||||
"""Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box"""
|
||||
|
||||
self._aabb = None
|
||||
if self._mesh_data:
|
||||
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||
|
@ -122,8 +129,9 @@ class CuraSceneNode(SceneNode):
|
|||
else:
|
||||
self._aabb = self._aabb + child.getBoundingBox()
|
||||
|
||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||
"""Taken from SceneNode, but replaced SceneNode with CuraSceneNode"""
|
||||
|
||||
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
|
||||
copy.setTransformation(self.getLocalTransformation())
|
||||
copy.setMeshData(self._mesh_data)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
|
||||
## A decorator that stores the amount an object has been moved below the platform.
|
||||
class ZOffsetDecorator(SceneNodeDecorator):
|
||||
"""A decorator that stores the amount an object has been moved below the platform."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._z_offset = 0.
|
||||
|
|
|
@ -33,12 +33,14 @@ if TYPE_CHECKING:
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Manager class that contains common actions to deal with containers in Cura.
|
||||
#
|
||||
# This is primarily intended as a class to be able to perform certain actions
|
||||
# from within QML. We want to be able to trigger things like removing a container
|
||||
# when a certain action happens. This can be done through this class.
|
||||
class ContainerManager(QObject):
|
||||
"""Manager class that contains common actions to deal with containers in Cura.
|
||||
|
||||
This is primarily intended as a class to be able to perform certain actions
|
||||
from within QML. We want to be able to trigger things like removing a container
|
||||
when a certain action happens. This can be done through this class.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
if ContainerManager.__instance is not None:
|
||||
|
@ -67,21 +69,23 @@ class ContainerManager(QObject):
|
|||
return ""
|
||||
return str(result)
|
||||
|
||||
## Set a metadata entry of the specified container.
|
||||
#
|
||||
# This will set the specified entry of the container's metadata to the specified
|
||||
# value. Note that entries containing dictionaries can have their entries changed
|
||||
# by using "/" as a separator. For example, to change an entry "foo" in a
|
||||
# dictionary entry "bar", you can specify "bar/foo" as entry name.
|
||||
#
|
||||
# \param container_node \type{ContainerNode}
|
||||
# \param entry_name \type{str} The name of the metadata entry to change.
|
||||
# \param entry_value The new value of the entry.
|
||||
#
|
||||
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
|
||||
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
|
||||
@pyqtSlot("QVariant", str, str)
|
||||
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
|
||||
"""Set a metadata entry of the specified container.
|
||||
|
||||
This will set the specified entry of the container's metadata to the specified
|
||||
value. Note that entries containing dictionaries can have their entries changed
|
||||
by using "/" as a separator. For example, to change an entry "foo" in a
|
||||
dictionary entry "bar", you can specify "bar/foo" as entry name.
|
||||
|
||||
:param container_node: :type{ContainerNode}
|
||||
:param entry_name: :type{str} The name of the metadata entry to change.
|
||||
:param entry_value: The new value of the entry.
|
||||
|
||||
TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
|
||||
Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
|
||||
"""
|
||||
|
||||
if container_node.container is None:
|
||||
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
|
||||
return False
|
||||
|
@ -124,18 +128,20 @@ class ContainerManager(QObject):
|
|||
def makeUniqueName(self, original_name: str) -> str:
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name)
|
||||
|
||||
## Get a list of string that can be used as name filters for a Qt File Dialog
|
||||
#
|
||||
# This will go through the list of available container types and generate a list of strings
|
||||
# out of that. The strings are formatted as "description (*.extension)" and can be directly
|
||||
# passed to a nameFilters property of a Qt File Dialog.
|
||||
#
|
||||
# \param type_name Which types of containers to list. These types correspond to the "type"
|
||||
# key of the plugin metadata.
|
||||
#
|
||||
# \return A string list with name filters.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getContainerNameFilters(self, type_name: str) -> List[str]:
|
||||
"""Get a list of string that can be used as name filters for a Qt File Dialog
|
||||
|
||||
This will go through the list of available container types and generate a list of strings
|
||||
out of that. The strings are formatted as "description (*.extension)" and can be directly
|
||||
passed to a nameFilters property of a Qt File Dialog.
|
||||
|
||||
:param type_name: Which types of containers to list. These types correspond to the "type"
|
||||
key of the plugin metadata.
|
||||
|
||||
:return: A string list with name filters.
|
||||
"""
|
||||
|
||||
if not self._container_name_filters:
|
||||
self._updateContainerNameFilters()
|
||||
|
||||
|
@ -147,17 +153,18 @@ class ContainerManager(QObject):
|
|||
filters.append("All Files (*)")
|
||||
return filters
|
||||
|
||||
## Export a container to a file
|
||||
#
|
||||
# \param container_id The ID of the container to export
|
||||
# \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
|
||||
# \param file_url_or_string The URL where to save the file.
|
||||
#
|
||||
# \return A dictionary containing a key "status" with a status code and a key "message" with a message
|
||||
# explaining the status.
|
||||
# The status code can be one of "error", "cancelled", "success"
|
||||
@pyqtSlot(str, str, QUrl, result = "QVariantMap")
|
||||
def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
|
||||
"""Export a container to a file
|
||||
|
||||
:param container_id: The ID of the container to export
|
||||
:param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
|
||||
:param file_url_or_string: The URL where to save the file.
|
||||
|
||||
:return: A dictionary containing a key "status" with a status code and a key "message" with a message
|
||||
explaining the status. The status code can be one of "error", "cancelled", "success"
|
||||
"""
|
||||
|
||||
if not container_id or not file_type or not file_url_or_string:
|
||||
return {"status": "error", "message": "Invalid arguments"}
|
||||
|
||||
|
@ -214,14 +221,16 @@ class ContainerManager(QObject):
|
|||
|
||||
return {"status": "success", "message": "Successfully exported container", "path": file_url}
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_url A URL that points to the file to import.
|
||||
#
|
||||
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
|
||||
# containing a message for the user
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
|
||||
"""Imports a profile from a file
|
||||
|
||||
:param file_url: A URL that points to the file to import.
|
||||
|
||||
:return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
|
||||
containing a message for the user
|
||||
"""
|
||||
|
||||
if not file_url_or_string:
|
||||
return {"status": "error", "message": "Invalid path"}
|
||||
|
||||
|
@ -266,14 +275,16 @@ class ContainerManager(QObject):
|
|||
|
||||
return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
|
||||
|
||||
## Update the current active quality changes container with the settings from the user container.
|
||||
#
|
||||
# This will go through the active global stack and all active extruder stacks and merge the changes from the user
|
||||
# container into the quality_changes container. After that, the user container is cleared.
|
||||
#
|
||||
# \return \type{bool} True if successful, False if not.
|
||||
@pyqtSlot(result = bool)
|
||||
def updateQualityChanges(self) -> bool:
|
||||
"""Update the current active quality changes container with the settings from the user container.
|
||||
|
||||
This will go through the active global stack and all active extruder stacks and merge the changes from the user
|
||||
container into the quality_changes container. After that, the user container is cleared.
|
||||
|
||||
:return: :type{bool} True if successful, False if not.
|
||||
"""
|
||||
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getMachineManager().activeMachine
|
||||
if not global_stack:
|
||||
|
@ -313,9 +324,10 @@ class ContainerManager(QObject):
|
|||
|
||||
return True
|
||||
|
||||
## Clear the top-most (user) containers of the active stacks.
|
||||
@pyqtSlot()
|
||||
def clearUserContainers(self) -> None:
|
||||
"""Clear the top-most (user) containers of the active stacks."""
|
||||
|
||||
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
|
||||
machine_manager.blurSettings.emit()
|
||||
|
||||
|
@ -335,25 +347,28 @@ class ContainerManager(QObject):
|
|||
for container in send_emits_containers:
|
||||
container.sendPostponedEmits()
|
||||
|
||||
## Get a list of materials that have the same GUID as the reference material
|
||||
#
|
||||
# \param material_node The node representing the material for which to get
|
||||
# the same GUID.
|
||||
# \param exclude_self Whether to include the name of the material you
|
||||
# provided.
|
||||
# \return A list of names of materials with the same GUID.
|
||||
@pyqtSlot("QVariant", bool, result = "QStringList")
|
||||
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
|
||||
"""Get a list of materials that have the same GUID as the reference material
|
||||
|
||||
:param material_node: The node representing the material for which to get
|
||||
the same GUID.
|
||||
:param exclude_self: Whether to include the name of the material you provided.
|
||||
:return: A list of names of materials with the same GUID.
|
||||
"""
|
||||
|
||||
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid)
|
||||
if exclude_self:
|
||||
return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
|
||||
else:
|
||||
return list({meta["name"] for meta in same_guid})
|
||||
|
||||
## Unlink a material from all other materials by creating a new GUID
|
||||
# \param material_id \type{str} the id of the material to create a new GUID for.
|
||||
@pyqtSlot("QVariant")
|
||||
def unlinkMaterial(self, material_node: "MaterialNode") -> None:
|
||||
"""Unlink a material from all other materials by creating a new GUID
|
||||
|
||||
:param material_id: :type{str} the id of the material to create a new GUID for.
|
||||
"""
|
||||
# Get the material group
|
||||
if material_node.container is None: # Failed to lazy-load this container.
|
||||
return
|
||||
|
@ -428,9 +443,10 @@ class ContainerManager(QObject):
|
|||
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
|
||||
self._container_name_filters[name_filter] = entry
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||
"""Import single profile, file_url does not have to end with curaprofile"""
|
||||
|
||||
if not file_url.isValid():
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
path = file_url.toLocalFile()
|
||||
|
|
|
@ -44,14 +44,16 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# is added, we check to see if an extruder stack needs to be added.
|
||||
self.containerAdded.connect(self._onContainerAdded)
|
||||
|
||||
## Overridden from ContainerRegistry
|
||||
#
|
||||
# Adds a container to the registry.
|
||||
#
|
||||
# This will also try to convert a ContainerStack to either Extruder or
|
||||
# Global stack based on metadata information.
|
||||
@override(ContainerRegistry)
|
||||
def addContainer(self, container: ContainerInterface) -> None:
|
||||
"""Overridden from ContainerRegistry
|
||||
|
||||
Adds a container to the registry.
|
||||
|
||||
This will also try to convert a ContainerStack to either Extruder or
|
||||
Global stack based on metadata information.
|
||||
"""
|
||||
|
||||
# Note: Intentional check with type() because we want to ignore subclasses
|
||||
if type(container) == ContainerStack:
|
||||
container = self._convertContainerStack(cast(ContainerStack, container))
|
||||
|
@ -66,13 +68,15 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
super().addContainer(container)
|
||||
|
||||
## Create a name that is not empty and unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param current_name \type{} Current name of the container, which may be an acceptable option
|
||||
# \param new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
||||
"""Create a name that is not empty and unique
|
||||
|
||||
:param container_type: :type{string} Type of the container (machine, quality, ...)
|
||||
:param current_name: :type{} Current name of the container, which may be an acceptable option
|
||||
:param new_name: :type{string} Base name, which may not be unique
|
||||
:param fallback_name: :type{string} Name to use when (stripped) new_name is empty
|
||||
:return: :type{string} Name that is unique for the specified type and name/id
|
||||
"""
|
||||
new_name = new_name.strip()
|
||||
num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name)
|
||||
if num_check:
|
||||
|
@ -89,24 +93,28 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
return unique_name
|
||||
|
||||
## Check if a container with of a certain type and a certain name or id exists
|
||||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param container_name \type{string} Name to check
|
||||
def _containerExists(self, container_type: str, container_name: str):
|
||||
"""Check if a container with of a certain type and a certain name or id exists
|
||||
|
||||
Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
:param container_type: :type{string} Type of the container (machine, quality, ...)
|
||||
:param container_name: :type{string} Name to check
|
||||
"""
|
||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||
|
||||
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
|
||||
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
|
||||
|
||||
## Exports an profile to a file
|
||||
#
|
||||
# \param container_list \type{list} the containers to export. This is not
|
||||
# necessarily in any order!
|
||||
# \param file_name \type{str} the full path and filename to export to.
|
||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||
# \return True if the export succeeded, false otherwise.
|
||||
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
|
||||
"""Exports an profile to a file
|
||||
|
||||
:param container_list: :type{list} the containers to export. This is not
|
||||
necessarily in any order!
|
||||
:param file_name: :type{str} the full path and filename to export to.
|
||||
:param file_type: :type{str} the file type with the format "<description> (*.<extension>)"
|
||||
:return: True if the export succeeded, false otherwise.
|
||||
"""
|
||||
|
||||
# Parse the fileType to deduce what plugin can save the file format.
|
||||
# fileType has the format "<description> (*.<extension>)"
|
||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||
|
@ -150,11 +158,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
m.show()
|
||||
return True
|
||||
|
||||
## Gets the plugin object matching the criteria
|
||||
# \param extension
|
||||
# \param description
|
||||
# \return The plugin object matching the given extension and description.
|
||||
def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
|
||||
"""Gets the plugin object matching the criteria
|
||||
|
||||
:param extension:
|
||||
:param description:
|
||||
:return: The plugin object matching the given extension and description.
|
||||
"""
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
||||
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
||||
|
@ -165,12 +175,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id))
|
||||
return None
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_name The full path and filename of the profile to import.
|
||||
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||
# and a 'message' key containing a message for the user.
|
||||
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||
"""Imports a profile from a file
|
||||
|
||||
:param file_name: The full path and filename of the profile to import.
|
||||
:return: Dict with a 'status' key containing the string 'ok' or 'error',
|
||||
and a 'message' key containing a message for the user.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
@ -336,12 +348,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||
self._connectUpgradedExtruderStacksToMachines()
|
||||
|
||||
## Check if the metadata for a container is okay before adding it.
|
||||
#
|
||||
# This overrides the one from UM.Settings.ContainerRegistry because we
|
||||
# also require that the setting_version is correct.
|
||||
@override(ContainerRegistry)
|
||||
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
|
||||
"""Check if the metadata for a container is okay before adding it.
|
||||
|
||||
This overrides the one from UM.Settings.ContainerRegistry because we
|
||||
also require that the setting_version is correct.
|
||||
"""
|
||||
|
||||
if metadata is None:
|
||||
return False
|
||||
if "setting_version" not in metadata:
|
||||
|
@ -353,14 +367,16 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
return False
|
||||
return True
|
||||
|
||||
## Update an imported profile to match the current machine configuration.
|
||||
#
|
||||
# \param profile The profile to configure.
|
||||
# \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers.
|
||||
# \param new_name The new name for the profile.
|
||||
#
|
||||
# \return None if configuring was successful or an error message if an error occurred.
|
||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
|
||||
"""Update an imported profile to match the current machine configuration.
|
||||
|
||||
:param profile: The profile to configure.
|
||||
:param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
|
||||
:param new_name: The new name for the profile.
|
||||
|
||||
:return: None if configuring was successful or an error message if an error occurred.
|
||||
"""
|
||||
|
||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||
|
||||
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
|
||||
|
@ -418,9 +434,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
for stack in self.findContainerStacks():
|
||||
self.saveContainer(stack)
|
||||
|
||||
## Gets a list of profile writer plugins
|
||||
# \return List of tuples of (plugin_id, meta_data).
|
||||
def _getIOPlugins(self, io_type):
|
||||
"""Gets a list of profile writer plugins
|
||||
|
||||
:return: List of tuples of (plugin_id, meta_data).
|
||||
"""
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
active_plugin_ids = plugin_registry.getActivePlugins()
|
||||
|
||||
|
@ -431,8 +449,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
result.append( (plugin_id, meta_data) )
|
||||
return result
|
||||
|
||||
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
||||
def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
|
||||
"""Convert an "old-style" pure ContainerStack to either an Extruder or Global stack."""
|
||||
|
||||
assert type(container) == ContainerStack
|
||||
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
|
|
@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers
|
|||
from . import Exceptions
|
||||
|
||||
|
||||
## Base class for Cura related stacks that want to enforce certain containers are available.
|
||||
#
|
||||
# This class makes sure that the stack has the following containers set: user changes, quality
|
||||
# changes, quality, material, variant, definition changes and finally definition. Initially,
|
||||
# these will be equal to the empty instance container.
|
||||
#
|
||||
# The container types are determined based on the following criteria:
|
||||
# - user: An InstanceContainer with the metadata entry "type" set to "user".
|
||||
# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
|
||||
# - quality: An InstanceContainer with the metadata entry "type" set to "quality".
|
||||
# - material: An InstanceContainer with the metadata entry "type" set to "material".
|
||||
# - variant: An InstanceContainer with the metadata entry "type" set to "variant".
|
||||
# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
|
||||
# - definition: A DefinitionContainer.
|
||||
#
|
||||
# Internally, this class ensures the mentioned containers are always there and kept in a specific order.
|
||||
# This also means that operations on the stack that modifies the container ordering is prohibited and
|
||||
# will raise an exception.
|
||||
class CuraContainerStack(ContainerStack):
|
||||
"""Base class for Cura related stacks that want to enforce certain containers are available.
|
||||
|
||||
This class makes sure that the stack has the following containers set: user changes, quality
|
||||
changes, quality, material, variant, definition changes and finally definition. Initially,
|
||||
these will be equal to the empty instance container.
|
||||
|
||||
The container types are determined based on the following criteria:
|
||||
- user: An InstanceContainer with the metadata entry "type" set to "user".
|
||||
- quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
|
||||
- quality: An InstanceContainer with the metadata entry "type" set to "quality".
|
||||
- material: An InstanceContainer with the metadata entry "type" set to "material".
|
||||
- variant: An InstanceContainer with the metadata entry "type" set to "variant".
|
||||
- definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
|
||||
- definition: A DefinitionContainer.
|
||||
|
||||
Internally, this class ensures the mentioned containers are always there and kept in a specific order.
|
||||
This also means that operations on the stack that modifies the container ordering is prohibited and
|
||||
will raise an exception.
|
||||
"""
|
||||
|
||||
def __init__(self, container_id: str) -> None:
|
||||
super().__init__(container_id)
|
||||
|
||||
|
@ -61,101 +63,131 @@ class CuraContainerStack(ContainerStack):
|
|||
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
||||
pyqtContainersChanged = pyqtSignal()
|
||||
|
||||
## Set the user changes container.
|
||||
#
|
||||
# \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user".
|
||||
def setUserChanges(self, new_user_changes: InstanceContainer) -> None:
|
||||
"""Set the user changes container.
|
||||
|
||||
:param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes)
|
||||
|
||||
## Get the user changes container.
|
||||
#
|
||||
# \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
|
||||
def userChanges(self) -> InstanceContainer:
|
||||
"""Get the user changes container.
|
||||
|
||||
:return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges])
|
||||
|
||||
## Set the quality changes container.
|
||||
#
|
||||
# \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||
def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
|
||||
"""Set the quality changes container.
|
||||
|
||||
:param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
|
||||
|
||||
## Get the quality changes container.
|
||||
#
|
||||
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
|
||||
def qualityChanges(self) -> InstanceContainer:
|
||||
"""Get the quality changes container.
|
||||
|
||||
:return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges])
|
||||
|
||||
## Set the intent container.
|
||||
#
|
||||
# \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent".
|
||||
def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||
"""Set the intent container.
|
||||
|
||||
:param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit)
|
||||
|
||||
## Get the quality container.
|
||||
#
|
||||
# \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged)
|
||||
def intent(self) -> InstanceContainer:
|
||||
"""Get the quality container.
|
||||
|
||||
:return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent])
|
||||
|
||||
## Set the quality container.
|
||||
#
|
||||
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||
def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||
"""Set the quality container.
|
||||
|
||||
:param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
|
||||
|
||||
## Get the quality container.
|
||||
#
|
||||
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
|
||||
def quality(self) -> InstanceContainer:
|
||||
"""Get the quality container.
|
||||
|
||||
:return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality])
|
||||
|
||||
## Set the material container.
|
||||
#
|
||||
# \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
|
||||
def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||
"""Set the material container.
|
||||
|
||||
:param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
|
||||
|
||||
## Get the material container.
|
||||
#
|
||||
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
|
||||
def material(self) -> InstanceContainer:
|
||||
"""Get the material container.
|
||||
|
||||
:return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.Material])
|
||||
|
||||
## Set the variant container.
|
||||
#
|
||||
# \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant".
|
||||
def setVariant(self, new_variant: InstanceContainer) -> None:
|
||||
"""Set the variant container.
|
||||
|
||||
:param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.Variant, new_variant)
|
||||
|
||||
## Get the variant container.
|
||||
#
|
||||
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
|
||||
def variant(self) -> InstanceContainer:
|
||||
"""Get the variant container.
|
||||
|
||||
:return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant])
|
||||
|
||||
## Set the definition changes container.
|
||||
#
|
||||
# \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
|
||||
def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
|
||||
"""Set the definition changes container.
|
||||
|
||||
:param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
|
||||
|
||||
## Get the definition changes container.
|
||||
#
|
||||
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
|
||||
def definitionChanges(self) -> InstanceContainer:
|
||||
"""Get the definition changes container.
|
||||
|
||||
:return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
"""
|
||||
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges])
|
||||
|
||||
## Set the definition container.
|
||||
#
|
||||
# \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition".
|
||||
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
|
||||
"""Set the definition container.
|
||||
|
||||
:param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition".
|
||||
"""
|
||||
|
||||
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
|
||||
|
||||
def getDefinition(self) -> "DefinitionContainer":
|
||||
|
@ -171,14 +203,16 @@ class CuraContainerStack(ContainerStack):
|
|||
def getTop(self) -> "InstanceContainer":
|
||||
return self.userChanges
|
||||
|
||||
## Check whether the specified setting has a 'user' value.
|
||||
#
|
||||
# A user value here is defined as the setting having a value in either
|
||||
# the UserChanges or QualityChanges container.
|
||||
#
|
||||
# \return True if the setting has a user value, False if not.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def hasUserValue(self, key: str) -> bool:
|
||||
"""Check whether the specified setting has a 'user' value.
|
||||
|
||||
A user value here is defined as the setting having a value in either
|
||||
the UserChanges or QualityChanges container.
|
||||
|
||||
:return: True if the setting has a user value, False if not.
|
||||
"""
|
||||
|
||||
if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"):
|
||||
return True
|
||||
|
||||
|
@ -187,51 +221,61 @@ class CuraContainerStack(ContainerStack):
|
|||
|
||||
return False
|
||||
|
||||
## Set a property of a setting.
|
||||
#
|
||||
# This will set a property of a specified setting. Since the container stack does not contain
|
||||
# any settings itself, it is required to specify a container to set the property on. The target
|
||||
# container is matched by container type.
|
||||
#
|
||||
# \param key The key of the setting to set.
|
||||
# \param property_name The name of the property to set.
|
||||
# \param new_value The new value to set the property to.
|
||||
def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None:
|
||||
"""Set a property of a setting.
|
||||
|
||||
This will set a property of a specified setting. Since the container stack does not contain
|
||||
any settings itself, it is required to specify a container to set the property on. The target
|
||||
container is matched by container type.
|
||||
|
||||
:param key: The key of the setting to set.
|
||||
:param property_name: The name of the property to set.
|
||||
:param new_value: The new value to set the property to.
|
||||
"""
|
||||
|
||||
container_index = _ContainerIndexes.UserChanges
|
||||
self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
# ordering, we disallow this operation.
|
||||
@override(ContainerStack)
|
||||
def addContainer(self, container: ContainerInterface) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
ordering, we disallow this operation.
|
||||
"""
|
||||
|
||||
raise Exceptions.InvalidOperationError("Cannot add a container to Global stack")
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
# ordering, we disallow this operation.
|
||||
@override(ContainerStack)
|
||||
def insertContainer(self, index: int, container: ContainerInterface) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
ordering, we disallow this operation.
|
||||
"""
|
||||
|
||||
raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack")
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
# ordering, we disallow this operation.
|
||||
@override(ContainerStack)
|
||||
def removeContainer(self, index: int = 0) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
Since we have a fixed order of containers in the stack and this method would modify the container
|
||||
ordering, we disallow this operation.
|
||||
"""
|
||||
|
||||
raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack")
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# Replaces the container at the specified index with another container.
|
||||
# This version performs checks to make sure the new container has the expected metadata and type.
|
||||
#
|
||||
# \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
|
||||
@override(ContainerStack)
|
||||
def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
Replaces the container at the specified index with another container.
|
||||
This version performs checks to make sure the new container has the expected metadata and type.
|
||||
|
||||
:throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
|
||||
"""
|
||||
|
||||
expected_type = _ContainerIndexes.IndexTypeMap[index]
|
||||
if expected_type == "definition":
|
||||
if not isinstance(container, DefinitionContainer):
|
||||
|
@ -245,16 +289,18 @@ class CuraContainerStack(ContainerStack):
|
|||
|
||||
super().replaceContainer(index, container, postpone_emit)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# This deserialize will make sure the internal list of containers matches with what we expect.
|
||||
# It will first check to see if the container at a certain index already matches with what we
|
||||
# expect. If it does not, it will search for a matching container with the correct type. Should
|
||||
# no container with the correct type be found, it will use the empty container.
|
||||
#
|
||||
# \throws InvalidContainerStackError Raised when no definition can be found for the stack.
|
||||
@override(ContainerStack)
|
||||
def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
This deserialize will make sure the internal list of containers matches with what we expect.
|
||||
It will first check to see if the container at a certain index already matches with what we
|
||||
expect. If it does not, it will search for a matching container with the correct type. Should
|
||||
no container with the correct type be found, it will use the empty container.
|
||||
|
||||
:raise InvalidContainerStackError: Raised when no definition can be found for the stack.
|
||||
"""
|
||||
|
||||
# update the serialized data first
|
||||
serialized = super().deserialize(serialized, file_name)
|
||||
|
||||
|
@ -298,10 +344,9 @@ class CuraContainerStack(ContainerStack):
|
|||
## TODO; Deserialize the containers.
|
||||
return serialized
|
||||
|
||||
## protected:
|
||||
|
||||
# Helper to make sure we emit a PyQt signal on container changes.
|
||||
def _onContainersChanged(self, container: Any) -> None:
|
||||
"""Helper to make sure we emit a PyQt signal on container changes."""
|
||||
|
||||
Application.getInstance().callLater(self.pyqtContainersChanged.emit)
|
||||
|
||||
# Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
|
||||
|
@ -309,16 +354,18 @@ class CuraContainerStack(ContainerStack):
|
|||
def _getMachineDefinition(self) -> DefinitionContainer:
|
||||
return self.definition
|
||||
|
||||
## Find the ID that should be used when searching for instance containers for a specified definition.
|
||||
#
|
||||
# This handles the situation where the definition specifies we should use a different definition when
|
||||
# searching for instance containers.
|
||||
#
|
||||
# \param machine_definition The definition to find the "quality definition" for.
|
||||
#
|
||||
# \return The ID of the definition container to use when searching for instance containers.
|
||||
@classmethod
|
||||
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str:
|
||||
"""Find the ID that should be used when searching for instance containers for a specified definition.
|
||||
|
||||
This handles the situation where the definition specifies we should use a different definition when
|
||||
searching for instance containers.
|
||||
|
||||
:param machine_definition: The definition to find the "quality definition" for.
|
||||
|
||||
:return: The ID of the definition container to use when searching for instance containers.
|
||||
"""
|
||||
|
||||
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||
if not quality_definition:
|
||||
return machine_definition.id #type: ignore
|
||||
|
@ -330,17 +377,18 @@ class CuraContainerStack(ContainerStack):
|
|||
|
||||
return cls._findInstanceContainerDefinitionId(definitions[0])
|
||||
|
||||
## getProperty for extruder positions, with translation from -1 to default extruder number
|
||||
def getExtruderPositionValueWithDefault(self, key):
|
||||
"""getProperty for extruder positions, with translation from -1 to default extruder number"""
|
||||
|
||||
value = self.getProperty(key, "value")
|
||||
if value == -1:
|
||||
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||
return value
|
||||
|
||||
## private:
|
||||
|
||||
# Private helper class to keep track of container positions and their types.
|
||||
class _ContainerIndexes:
|
||||
"""Private helper class to keep track of container positions and their types."""
|
||||
|
||||
UserChanges = 0
|
||||
QualityChanges = 1
|
||||
Intent = 2
|
||||
|
|
|
@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack
|
|||
from .ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Contains helper functions to create new machines.
|
||||
class CuraStackBuilder:
|
||||
"""Contains helper functions to create new machines."""
|
||||
|
||||
|
||||
## Create a new instance of a machine.
|
||||
#
|
||||
# \param name The name of the new machine.
|
||||
# \param definition_id The ID of the machine definition to use.
|
||||
#
|
||||
# \return The new global stack or None if an error occurred.
|
||||
@classmethod
|
||||
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
|
||||
"""Create a new instance of a machine.
|
||||
|
||||
:param name: The name of the new machine.
|
||||
:param definition_id: The ID of the machine definition to use.
|
||||
|
||||
:return: The new global stack or None if an error occurred.
|
||||
"""
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
|
@ -71,12 +74,14 @@ class CuraStackBuilder:
|
|||
|
||||
return new_global_stack
|
||||
|
||||
## Create a default Extruder Stack
|
||||
#
|
||||
# \param global_stack The global stack this extruder refers to.
|
||||
# \param extruder_position The position of the current extruder.
|
||||
@classmethod
|
||||
def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
|
||||
"""Create a default Extruder Stack
|
||||
|
||||
:param global_stack: The global stack this extruder refers to.
|
||||
:param extruder_position: The position of the current extruder.
|
||||
"""
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
|
@ -120,17 +125,6 @@ class CuraStackBuilder:
|
|||
|
||||
registry.addContainer(new_extruder)
|
||||
|
||||
## Create a new Extruder stack
|
||||
#
|
||||
# \param new_stack_id The ID of the new stack.
|
||||
# \param extruder_definition The definition to base the new stack on.
|
||||
# \param machine_definition_id The ID of the machine definition to use for the user container.
|
||||
# \param position The position the extruder occupies in the machine.
|
||||
# \param variant_container The variant selected for the current extruder.
|
||||
# \param material_container The material selected for the current extruder.
|
||||
# \param quality_container The quality selected for the current extruder.
|
||||
#
|
||||
# \return A new Extruder stack instance with the specified parameters.
|
||||
@classmethod
|
||||
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface,
|
||||
machine_definition_id: str,
|
||||
|
@ -139,6 +133,19 @@ class CuraStackBuilder:
|
|||
material_container: "InstanceContainer",
|
||||
quality_container: "InstanceContainer") -> ExtruderStack:
|
||||
|
||||
"""Create a new Extruder stack
|
||||
|
||||
:param new_stack_id: The ID of the new stack.
|
||||
:param extruder_definition: The definition to base the new stack on.
|
||||
:param machine_definition_id: The ID of the machine definition to use for the user container.
|
||||
:param position: The position the extruder occupies in the machine.
|
||||
:param variant_container: The variant selected for the current extruder.
|
||||
:param material_container: The material selected for the current extruder.
|
||||
:param quality_container: The quality selected for the current extruder.
|
||||
|
||||
:return: A new Extruder stack instance with the specified parameters.
|
||||
"""
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
|
@ -167,29 +174,23 @@ class CuraStackBuilder:
|
|||
|
||||
return stack
|
||||
|
||||
## Create a new Global stack
|
||||
#
|
||||
# \param new_stack_id The ID of the new stack.
|
||||
# \param definition The definition to base the new stack on.
|
||||
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
||||
#
|
||||
# \return A new Global stack instance with the specified parameters.
|
||||
|
||||
## Create a new Global stack
|
||||
#
|
||||
# \param new_stack_id The ID of the new stack.
|
||||
# \param definition The definition to base the new stack on.
|
||||
# \param variant_container The variant selected for the current stack.
|
||||
# \param material_container The material selected for the current stack.
|
||||
# \param quality_container The quality selected for the current stack.
|
||||
#
|
||||
# \return A new Global stack instance with the specified parameters.
|
||||
@classmethod
|
||||
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
|
||||
variant_container: "InstanceContainer",
|
||||
material_container: "InstanceContainer",
|
||||
quality_container: "InstanceContainer") -> GlobalStack:
|
||||
|
||||
"""Create a new Global stack
|
||||
|
||||
:param new_stack_id: The ID of the new stack.
|
||||
:param definition: The definition to base the new stack on.
|
||||
:param variant_container: The variant selected for the current stack.
|
||||
:param material_container: The material selected for the current stack.
|
||||
:param quality_container: The quality selected for the current stack.
|
||||
|
||||
:return: A new Global stack instance with the specified parameters.
|
||||
"""
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
|
|
|
@ -2,21 +2,25 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
|
||||
## Raised when trying to perform an operation like add on a stack that does not allow that.
|
||||
class InvalidOperationError(Exception):
|
||||
"""Raised when trying to perform an operation like add on a stack that does not allow that."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
## Raised when trying to replace a container with a container that does not have the expected type.
|
||||
class InvalidContainerError(Exception):
|
||||
"""Raised when trying to replace a container with a container that does not have the expected type."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.
|
||||
class TooManyExtrudersError(Exception):
|
||||
"""Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
## Raised when an extruder has no next stack set.
|
||||
class NoGlobalStackError(Exception):
|
||||
"""Raised when an extruder has no next stack set."""
|
||||
|
||||
pass
|
||||
|
|
|
@ -19,13 +19,15 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Manages all existing extruder stacks.
|
||||
#
|
||||
# This keeps a list of extruder stacks for each machine.
|
||||
class ExtruderManager(QObject):
|
||||
"""Manages all existing extruder stacks.
|
||||
|
||||
This keeps a list of extruder stacks for each machine.
|
||||
"""
|
||||
|
||||
## Registers listeners and such to listen to changes to the extruders.
|
||||
def __init__(self, parent = None):
|
||||
"""Registers listeners and such to listen to changes to the extruders."""
|
||||
|
||||
if ExtruderManager.__instance is not None:
|
||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||
ExtruderManager.__instance = self
|
||||
|
@ -43,20 +45,22 @@ class ExtruderManager(QObject):
|
|||
|
||||
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
||||
|
||||
## Signal to notify other components when the list of extruders for a machine definition changes.
|
||||
extrudersChanged = pyqtSignal(QVariant)
|
||||
"""Signal to notify other components when the list of extruders for a machine definition changes."""
|
||||
|
||||
## Notify when the user switches the currently active extruder.
|
||||
activeExtruderChanged = pyqtSignal()
|
||||
"""Notify when the user switches the currently active extruder."""
|
||||
|
||||
## Gets the unique identifier of the currently active extruder stack.
|
||||
#
|
||||
# The currently active extruder stack is the stack that is currently being
|
||||
# edited.
|
||||
#
|
||||
# \return The unique ID of the currently active extruder stack.
|
||||
@pyqtProperty(str, notify = activeExtruderChanged)
|
||||
def activeExtruderStackId(self) -> Optional[str]:
|
||||
"""Gets the unique identifier of the currently active extruder stack.
|
||||
|
||||
The currently active extruder stack is the stack that is currently being
|
||||
edited.
|
||||
|
||||
:return: The unique ID of the currently active extruder stack.
|
||||
"""
|
||||
|
||||
if not self._application.getGlobalContainerStack():
|
||||
return None # No active machine, so no active extruder.
|
||||
try:
|
||||
|
@ -64,9 +68,10 @@ class ExtruderManager(QObject):
|
|||
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||
return None
|
||||
|
||||
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
def extruderIds(self) -> Dict[str, str]:
|
||||
"""Gets a dict with the extruder stack ids with the extruder number as the key."""
|
||||
|
||||
extruder_stack_ids = {} # type: Dict[str, str]
|
||||
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
|
@ -75,11 +80,13 @@ class ExtruderManager(QObject):
|
|||
|
||||
return extruder_stack_ids
|
||||
|
||||
## Changes the active extruder by index.
|
||||
#
|
||||
# \param index The index of the new active extruder.
|
||||
@pyqtSlot(int)
|
||||
def setActiveExtruderIndex(self, index: int) -> None:
|
||||
"""Changes the active extruder by index.
|
||||
|
||||
:param index: The index of the new active extruder.
|
||||
"""
|
||||
|
||||
if self._active_extruder_index != index:
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
|
@ -88,12 +95,13 @@ class ExtruderManager(QObject):
|
|||
def activeExtruderIndex(self) -> int:
|
||||
return self._active_extruder_index
|
||||
|
||||
## Emitted whenever the selectedObjectExtruders property changes.
|
||||
selectedObjectExtrudersChanged = pyqtSignal()
|
||||
"""Emitted whenever the selectedObjectExtruders property changes."""
|
||||
|
||||
## Provides a list of extruder IDs used by the current selected objects.
|
||||
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
|
||||
def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
|
||||
"""Provides a list of extruder IDs used by the current selected objects."""
|
||||
|
||||
if not self._selected_object_extruders:
|
||||
object_extruders = set()
|
||||
|
||||
|
@ -122,11 +130,13 @@ class ExtruderManager(QObject):
|
|||
|
||||
return self._selected_object_extruders
|
||||
|
||||
## Reset the internal list used for the selectedObjectExtruders property
|
||||
#
|
||||
# This will trigger a recalculation of the extruders used for the
|
||||
# selection.
|
||||
def resetSelectedObjectExtruders(self) -> None:
|
||||
"""Reset the internal list used for the selectedObjectExtruders property
|
||||
|
||||
This will trigger a recalculation of the extruders used for the
|
||||
selection.
|
||||
"""
|
||||
|
||||
self._selected_object_extruders = []
|
||||
self.selectedObjectExtrudersChanged.emit()
|
||||
|
||||
|
@ -134,8 +144,9 @@ class ExtruderManager(QObject):
|
|||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||
return self.getExtruderStack(self.activeExtruderIndex)
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||
"""Get an extruder stack by index"""
|
||||
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if global_container_stack.getId() in self._extruder_trains:
|
||||
|
@ -143,12 +154,14 @@ class ExtruderManager(QObject):
|
|||
return self._extruder_trains[global_container_stack.getId()][str(index)]
|
||||
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]:
|
||||
"""Gets a property of a setting for all extruders.
|
||||
|
||||
:param setting_key: :type{str} The setting to get the property of.
|
||||
:param prop: :type{str} The property to get.
|
||||
:return: :type{List} the list of results
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
for extruder_stack in self.getActiveExtruderStacks():
|
||||
|
@ -163,17 +176,19 @@ class ExtruderManager(QObject):
|
|||
else:
|
||||
return value
|
||||
|
||||
## Gets the extruder stacks that are actually being used at the moment.
|
||||
#
|
||||
# An extruder stack is being used if it is the extruder to print any mesh
|
||||
# with, or if it is the support infill extruder, the support interface
|
||||
# extruder, or the bed adhesion extruder.
|
||||
#
|
||||
# If there are no extruders, this returns the global stack as a singleton
|
||||
# list.
|
||||
#
|
||||
# \return A list of extruder stacks.
|
||||
def getUsedExtruderStacks(self) -> List["ExtruderStack"]:
|
||||
"""Gets the extruder stacks that are actually being used at the moment.
|
||||
|
||||
An extruder stack is being used if it is the extruder to print any mesh
|
||||
with, or if it is the support infill extruder, the support interface
|
||||
extruder, or the bed adhesion extruder.
|
||||
|
||||
If there are no extruders, this returns the global stack as a singleton
|
||||
list.
|
||||
|
||||
:return: A list of extruder stacks.
|
||||
"""
|
||||
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
|
@ -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)
|
||||
return []
|
||||
|
||||
## Get the extruder that the print will start with.
|
||||
#
|
||||
# This should mirror the implementation in CuraEngine of
|
||||
# ``FffGcodeWriter::getStartExtruder()``.
|
||||
def getInitialExtruderNr(self) -> int:
|
||||
"""Get the extruder that the print will start with.
|
||||
|
||||
This should mirror the implementation in CuraEngine of
|
||||
``FffGcodeWriter::getStartExtruder()``.
|
||||
"""
|
||||
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getGlobalContainerStack()
|
||||
|
||||
|
@ -277,28 +294,34 @@ class ExtruderManager(QObject):
|
|||
# REALLY no adhesion? Use the first used extruder.
|
||||
return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value")
|
||||
|
||||
## Removes the container stack and user profile for the extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to remove the extruders for.
|
||||
def removeMachineExtruders(self, machine_id: str) -> None:
|
||||
"""Removes the container stack and user profile for the extruders for a specific machine.
|
||||
|
||||
:param machine_id: The machine to remove the extruders for.
|
||||
"""
|
||||
|
||||
for extruder in self.getMachineExtruders(machine_id):
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
||||
if machine_id in self._extruder_trains:
|
||||
del self._extruder_trains[machine_id]
|
||||
|
||||
## Returns extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to get the extruders of.
|
||||
def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]:
|
||||
"""Returns extruders for a specific machine.
|
||||
|
||||
:param machine_id: The machine to get the extruders of.
|
||||
"""
|
||||
|
||||
if machine_id not in self._extruder_trains:
|
||||
return []
|
||||
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
|
||||
|
||||
## Returns the list of active extruder stacks, taking into account the machine extruder count.
|
||||
#
|
||||
# \return \type{List[ContainerStack]} a list of
|
||||
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||
"""Returns the list of active extruder stacks, taking into account the machine extruder count.
|
||||
|
||||
:return: :type{List[ContainerStack]} a list of
|
||||
"""
|
||||
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return []
|
||||
|
@ -310,8 +333,9 @@ class ExtruderManager(QObject):
|
|||
|
||||
self.resetSelectedObjectExtruders()
|
||||
|
||||
## Adds the extruders to the selected machine.
|
||||
def addMachineExtruders(self, global_stack: GlobalStack) -> None:
|
||||
"""Adds the extruders to the selected machine."""
|
||||
|
||||
extruders_changed = False
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
global_stack_id = global_stack.getId()
|
||||
|
@ -377,26 +401,30 @@ class ExtruderManager(QObject):
|
|||
raise IndexError(msg)
|
||||
extruder_stack_0.definition = extruder_definition
|
||||
|
||||
## Get all extruder values for a certain setting.
|
||||
#
|
||||
# This is exposed to qml for display purposes
|
||||
#
|
||||
# \param key The key of the setting to retrieve values for.
|
||||
#
|
||||
# \return String representing the extruder values
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def getInstanceExtruderValues(self, key: str) -> List:
|
||||
"""Get all extruder values for a certain setting.
|
||||
|
||||
This is exposed to qml for display purposes
|
||||
|
||||
:param key: The key of the setting to retrieve values for.
|
||||
|
||||
:return: String representing the extruder values
|
||||
"""
|
||||
|
||||
return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
|
||||
|
||||
## Get the resolve value or value for a given key
|
||||
#
|
||||
# This is the effective value for a given key, it is used for values in the global stack.
|
||||
# This is exposed to SettingFunction to use in value functions.
|
||||
# \param key The key of the setting to get the value of.
|
||||
#
|
||||
# \return The effective value
|
||||
@staticmethod
|
||||
def getResolveOrValue(key: str) -> Any:
|
||||
"""Get the resolve value or value for a given key
|
||||
|
||||
This is the effective value for a given key, it is used for values in the global stack.
|
||||
This is exposed to SettingFunction to use in value functions.
|
||||
:param key: The key of the setting to get the value of.
|
||||
|
||||
:return: The effective value
|
||||
"""
|
||||
|
||||
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
|
||||
resolved_value = global_stack.getProperty(key, "value")
|
||||
|
||||
|
|
|
@ -22,10 +22,9 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
## Represents an Extruder and its related containers.
|
||||
#
|
||||
#
|
||||
class ExtruderStack(CuraContainerStack):
|
||||
"""Represents an Extruder and its related containers."""
|
||||
|
||||
def __init__(self, container_id: str) -> None:
|
||||
super().__init__(container_id)
|
||||
|
||||
|
@ -35,11 +34,13 @@ class ExtruderStack(CuraContainerStack):
|
|||
|
||||
enabledChanged = pyqtSignal()
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# This will set the next stack and ensure that we register this stack as an extruder.
|
||||
@override(ContainerStack)
|
||||
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
This will set the next stack and ensure that we register this stack as an extruder.
|
||||
"""
|
||||
|
||||
super().setNextStack(stack)
|
||||
stack.addExtruder(self)
|
||||
self.setMetaDataEntry("machine", stack.id)
|
||||
|
@ -68,11 +69,13 @@ class ExtruderStack(CuraContainerStack):
|
|||
|
||||
compatibleMaterialDiameterChanged = pyqtSignal()
|
||||
|
||||
## Return the filament diameter that the machine requires.
|
||||
#
|
||||
# If the machine has no requirement for the diameter, -1 is returned.
|
||||
# \return The filament diameter for the printer
|
||||
def getCompatibleMaterialDiameter(self) -> float:
|
||||
"""Return the filament diameter that the machine requires.
|
||||
|
||||
If the machine has no requirement for the diameter, -1 is returned.
|
||||
:return: The filament diameter for the printer
|
||||
"""
|
||||
|
||||
context = PropertyEvaluationContext(self)
|
||||
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
|
||||
|
||||
|
@ -94,31 +97,35 @@ class ExtruderStack(CuraContainerStack):
|
|||
|
||||
approximateMaterialDiameterChanged = pyqtSignal()
|
||||
|
||||
## Return the approximate filament diameter that the machine requires.
|
||||
#
|
||||
# The approximate material diameter is the material diameter rounded to
|
||||
# the nearest millimetre.
|
||||
#
|
||||
# If the machine has no requirement for the diameter, -1 is returned.
|
||||
#
|
||||
# \return The approximate filament diameter for the printer
|
||||
def getApproximateMaterialDiameter(self) -> float:
|
||||
"""Return the approximate filament diameter that the machine requires.
|
||||
|
||||
The approximate material diameter is the material diameter rounded to
|
||||
the nearest millimetre.
|
||||
|
||||
If the machine has no requirement for the diameter, -1 is returned.
|
||||
|
||||
:return: The approximate filament diameter for the printer
|
||||
"""
|
||||
|
||||
return round(self.getCompatibleMaterialDiameter())
|
||||
|
||||
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
|
||||
notify = approximateMaterialDiameterChanged)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# It will perform a few extra checks when trying to get properties.
|
||||
#
|
||||
# The two extra checks it currently does is to ensure a next stack is set and to bypass
|
||||
# the extruder when the property is not settable per extruder.
|
||||
#
|
||||
# \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without
|
||||
# having a next stack set.
|
||||
@override(ContainerStack)
|
||||
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
It will perform a few extra checks when trying to get properties.
|
||||
|
||||
The two extra checks it currently does is to ensure a next stack is set and to bypass
|
||||
the extruder when the property is not settable per extruder.
|
||||
|
||||
:throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without
|
||||
having a next stack set.
|
||||
"""
|
||||
|
||||
if not self._next_stack:
|
||||
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))
|
||||
|
||||
|
|
|
@ -29,9 +29,9 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Represents the Global or Machine stack and its related containers.
|
||||
#
|
||||
class GlobalStack(CuraContainerStack):
|
||||
"""Represents the Global or Machine stack and its related containers."""
|
||||
|
||||
def __init__(self, container_id: str) -> None:
|
||||
super().__init__(container_id)
|
||||
|
||||
|
@ -58,12 +58,14 @@ class GlobalStack(CuraContainerStack):
|
|||
extrudersChanged = pyqtSignal()
|
||||
configuredConnectionTypesChanged = pyqtSignal()
|
||||
|
||||
## Get the list of extruders of this stack.
|
||||
#
|
||||
# \return The extruders registered with this stack.
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
@deprecated("Please use extruderList instead.", "4.4")
|
||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||
"""Get the list of extruders of this stack.
|
||||
|
||||
:return: The extruders registered with this stack.
|
||||
"""
|
||||
|
||||
return self._extruders
|
||||
|
||||
@pyqtProperty("QVariantList", notify = extrudersChanged)
|
||||
|
@ -86,16 +88,18 @@ class GlobalStack(CuraContainerStack):
|
|||
def getLoadingPriority(cls) -> int:
|
||||
return 2
|
||||
|
||||
## The configured connection types can be used to find out if the global
|
||||
# stack is configured to be connected with a printer, without having to
|
||||
# know all the details as to how this is exactly done (and without
|
||||
# actually setting the stack to be active).
|
||||
#
|
||||
# This data can then in turn also be used when the global stack is active;
|
||||
# If we can't get a network connection, but it is configured to have one,
|
||||
# we can display a different icon to indicate the difference.
|
||||
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
|
||||
def configuredConnectionTypes(self) -> List[int]:
|
||||
"""The configured connection types can be used to find out if the global
|
||||
|
||||
stack is configured to be connected with a printer, without having to
|
||||
know all the details as to how this is exactly done (and without
|
||||
actually setting the stack to be active).
|
||||
|
||||
This data can then in turn also be used when the global stack is active;
|
||||
If we can't get a network connection, but it is configured to have one,
|
||||
we can display a different icon to indicate the difference.
|
||||
"""
|
||||
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
|
||||
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
|
||||
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
|
||||
|
@ -122,16 +126,18 @@ class GlobalStack(CuraContainerStack):
|
|||
ConnectionType.CloudConnection.value]
|
||||
return has_remote_connection
|
||||
|
||||
## \sa configuredConnectionTypes
|
||||
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
||||
""":sa configuredConnectionTypes"""
|
||||
|
||||
configured_connection_types = self.configuredConnectionTypes
|
||||
if connection_type not in configured_connection_types:
|
||||
# Store the values as a string.
|
||||
configured_connection_types.append(connection_type)
|
||||
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
||||
|
||||
## \sa configuredConnectionTypes
|
||||
def removeConfiguredConnectionType(self, connection_type: int) -> None:
|
||||
""":sa configuredConnectionTypes"""
|
||||
|
||||
configured_connection_types = self.configuredConnectionTypes
|
||||
if connection_type in configured_connection_types:
|
||||
# Store the values as a string.
|
||||
|
@ -163,13 +169,15 @@ class GlobalStack(CuraContainerStack):
|
|||
def preferred_output_file_formats(self) -> str:
|
||||
return self.getMetaDataEntry("file_formats")
|
||||
|
||||
## Add an extruder to the list of extruders of this stack.
|
||||
#
|
||||
# \param extruder The extruder to add.
|
||||
#
|
||||
# \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we
|
||||
# already have the maximum number of extruders.
|
||||
def addExtruder(self, extruder: ContainerStack) -> None:
|
||||
"""Add an extruder to the list of extruders of this stack.
|
||||
|
||||
:param extruder: The extruder to add.
|
||||
|
||||
:raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we
|
||||
already have the maximum number of extruders.
|
||||
"""
|
||||
|
||||
position = extruder.getMetaDataEntry("position")
|
||||
if position is None:
|
||||
Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
|
||||
|
@ -183,19 +191,21 @@ class GlobalStack(CuraContainerStack):
|
|||
self.extrudersChanged.emit()
|
||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# This will return the value of the specified property for the specified setting,
|
||||
# unless the property is "value" and that setting has a "resolve" function set.
|
||||
# When a resolve is set, it will instead try and execute the resolve first and
|
||||
# then fall back to the normal "value" property.
|
||||
#
|
||||
# \param key The setting key to get the property of.
|
||||
# \param property_name The property to get the value of.
|
||||
#
|
||||
# \return The value of the property for the specified setting, or None if not found.
|
||||
@override(ContainerStack)
|
||||
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
This will return the value of the specified property for the specified setting,
|
||||
unless the property is "value" and that setting has a "resolve" function set.
|
||||
When a resolve is set, it will instead try and execute the resolve first and
|
||||
then fall back to the normal "value" property.
|
||||
|
||||
:param key: The setting key to get the property of.
|
||||
:param property_name: The property to get the value of.
|
||||
|
||||
:return: The value of the property for the specified setting, or None if not found.
|
||||
"""
|
||||
|
||||
if not self.definition.findDefinitions(key = key):
|
||||
return None
|
||||
|
||||
|
@ -235,11 +245,13 @@ class GlobalStack(CuraContainerStack):
|
|||
context.popContainer()
|
||||
return result
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
# This will simply raise an exception since the Global stack cannot have a next stack.
|
||||
@override(ContainerStack)
|
||||
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
|
||||
"""Overridden from ContainerStack
|
||||
|
||||
This will simply raise an exception since the Global stack cannot have a next stack.
|
||||
"""
|
||||
|
||||
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
||||
|
||||
# protected:
|
||||
|
@ -267,9 +279,11 @@ class GlobalStack(CuraContainerStack):
|
|||
|
||||
return True
|
||||
|
||||
## Perform some sanity checks on the global stack
|
||||
# Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
|
||||
def isValid(self) -> bool:
|
||||
"""Perform some sanity checks on the global stack
|
||||
|
||||
Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
|
||||
"""
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
|
||||
|
||||
|
@ -299,9 +313,10 @@ class GlobalStack(CuraContainerStack):
|
|||
def hasVariantBuildplates(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||
|
||||
## Get default firmware file name if one is specified in the firmware
|
||||
@pyqtSlot(result = str)
|
||||
def getDefaultFirmwareName(self) -> str:
|
||||
"""Get default firmware file name if one is specified in the firmware"""
|
||||
|
||||
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
|
||||
|
||||
baudrate = 250000
|
||||
|
|
|
@ -15,29 +15,32 @@ if TYPE_CHECKING:
|
|||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
|
||||
## Front-end for querying which intents are available for a certain
|
||||
# configuration.
|
||||
class IntentManager(QObject):
|
||||
"""Front-end for querying which intents are available for a certain configuration.
|
||||
"""
|
||||
__instance = None
|
||||
|
||||
## This class is a singleton.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
"""This class is a singleton."""
|
||||
|
||||
if not cls.__instance:
|
||||
cls.__instance = IntentManager()
|
||||
return cls.__instance
|
||||
|
||||
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
|
||||
|
||||
## Gets the metadata dictionaries of all intent profiles for a given
|
||||
# configuration.
|
||||
#
|
||||
# \param definition_id ID of the printer.
|
||||
# \param nozzle_name Name of the nozzle.
|
||||
# \param material_base_file The base_file of the material.
|
||||
# \return A list of metadata dictionaries matching the search criteria, or
|
||||
# an empty list if nothing was found.
|
||||
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
|
||||
"""Gets the metadata dictionaries of all intent profiles for a given
|
||||
|
||||
configuration.
|
||||
|
||||
:param definition_id: ID of the printer.
|
||||
:param nozzle_name: Name of the nozzle.
|
||||
:param material_base_file: The base_file of the material.
|
||||
:return: A list of metadata dictionaries matching the search criteria, or
|
||||
an empty list if nothing was found.
|
||||
"""
|
||||
intent_metadatas = [] # type: List[Dict[str, Any]]
|
||||
try:
|
||||
materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
|
||||
|
@ -53,28 +56,32 @@ class IntentManager(QObject):
|
|||
intent_metadatas.append(intent_node.getMetadata())
|
||||
return intent_metadatas
|
||||
|
||||
## Collects and returns all intent categories available for the given
|
||||
# parameters. Note that the 'default' category is always available.
|
||||
#
|
||||
# \param definition_id ID of the printer.
|
||||
# \param nozzle_name Name of the nozzle.
|
||||
# \param material_id ID of the material.
|
||||
# \return A set of intent category names.
|
||||
def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
|
||||
"""Collects and returns all intent categories available for the given
|
||||
|
||||
parameters. Note that the 'default' category is always available.
|
||||
|
||||
:param definition_id: ID of the printer.
|
||||
:param nozzle_name: Name of the nozzle.
|
||||
:param material_id: ID of the material.
|
||||
:return: A set of intent category names.
|
||||
"""
|
||||
categories = set()
|
||||
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
|
||||
categories.add(intent["intent_category"])
|
||||
categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
|
||||
return list(categories)
|
||||
|
||||
## List of intents to be displayed in the interface.
|
||||
#
|
||||
# For the interface this will have to be broken up into the different
|
||||
# intent categories. That is up to the model there.
|
||||
#
|
||||
# \return A list of tuples of intent_category and quality_type. The actual
|
||||
# instance may vary per extruder.
|
||||
def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
|
||||
"""List of intents to be displayed in the interface.
|
||||
|
||||
For the interface this will have to be broken up into the different
|
||||
intent categories. That is up to the model there.
|
||||
|
||||
:return: A list of tuples of intent_category and quality_type. The actual
|
||||
instance may vary per extruder.
|
||||
"""
|
||||
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
|
@ -100,16 +107,18 @@ class IntentManager(QObject):
|
|||
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
|
||||
return list(result)
|
||||
|
||||
## List of intent categories available in either of the extruders.
|
||||
#
|
||||
# This is purposefully inconsistent with the way that the quality types
|
||||
# are listed. The quality types will show all quality types available in
|
||||
# the printer using any configuration. This will only list the intent
|
||||
# categories that are available using the current configuration (but the
|
||||
# union over the extruders).
|
||||
# \return List of all categories in the current configurations of all
|
||||
# extruders.
|
||||
def currentAvailableIntentCategories(self) -> List[str]:
|
||||
"""List of intent categories available in either of the extruders.
|
||||
|
||||
This is purposefully inconsistent with the way that the quality types
|
||||
are listed. The quality types will show all quality types available in
|
||||
the printer using any configuration. This will only list the intent
|
||||
categories that are available using the current configuration (but the
|
||||
union over the extruders).
|
||||
:return: List of all categories in the current configurations of all
|
||||
extruders.
|
||||
"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return ["default"]
|
||||
|
@ -123,10 +132,12 @@ class IntentManager(QObject):
|
|||
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
|
||||
return list(final_intent_categories)
|
||||
|
||||
## The intent that gets selected by default when no intent is available for
|
||||
# the configuration, an extruder can't match the intent that the user
|
||||
# selects, or just when creating a new printer.
|
||||
def getDefaultIntent(self) -> "InstanceContainer":
|
||||
"""The intent that gets selected by default when no intent is available for
|
||||
|
||||
the configuration, an extruder can't match the intent that the user
|
||||
selects, or just when creating a new printer.
|
||||
"""
|
||||
return empty_intent_container
|
||||
|
||||
@pyqtProperty(str, notify = intentCategoryChanged)
|
||||
|
@ -137,9 +148,10 @@ class IntentManager(QObject):
|
|||
return ""
|
||||
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
|
||||
|
||||
## Apply intent on the stacks.
|
||||
@pyqtSlot(str, str)
|
||||
def selectIntent(self, intent_category: str, quality_type: str) -> None:
|
||||
"""Apply intent on the stacks."""
|
||||
|
||||
Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
|
||||
old_intent_category = self.currentIntentCategory
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
|
|
|
@ -215,8 +215,9 @@ class MachineManager(QObject):
|
|||
return set()
|
||||
return general_definition_containers[0].getAllKeys()
|
||||
|
||||
## Triggered when the global container stack is changed in CuraApplication.
|
||||
def _onGlobalContainerChanged(self) -> None:
|
||||
"""Triggered when the global container stack is changed in CuraApplication."""
|
||||
|
||||
if self._global_container_stack:
|
||||
try:
|
||||
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||
|
@ -338,12 +339,15 @@ class MachineManager(QObject):
|
|||
Logger.log("w", "An extruder has an unknown material, switching it to the preferred material")
|
||||
self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material)
|
||||
|
||||
## Given a definition id, return the machine with this id.
|
||||
# Optional: add a list of keys and values to filter the list of machines with the given definition id
|
||||
# \param definition_id \type{str} definition id that needs to look for
|
||||
# \param metadata_filter \type{dict} list of metadata keys and values used for filtering
|
||||
@staticmethod
|
||||
def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
|
||||
"""Given a definition id, return the machine with this id.
|
||||
|
||||
Optional: add a list of keys and values to filter the list of machines with the given definition id
|
||||
:param definition_id: :type{str} definition id that needs to look for
|
||||
:param metadata_filter: :type{dict} list of metadata keys and values used for filtering
|
||||
"""
|
||||
|
||||
if metadata_filter is None:
|
||||
metadata_filter = {}
|
||||
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||
|
@ -397,9 +401,10 @@ class MachineManager(QObject):
|
|||
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
|
||||
return False
|
||||
|
||||
## Check if the global_container has instances in the user container
|
||||
@pyqtProperty(bool, notify = activeStackValueChanged)
|
||||
def hasUserSettings(self) -> bool:
|
||||
"""Check if the global_container has instances in the user container"""
|
||||
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
|
||||
|
@ -422,10 +427,12 @@ class MachineManager(QObject):
|
|||
num_user_settings += stack.getTop().getNumInstances()
|
||||
return num_user_settings
|
||||
|
||||
## Delete a user setting from the global stack and all extruder stacks.
|
||||
# \param key \type{str} the name of the key to delete
|
||||
@pyqtSlot(str)
|
||||
def clearUserSettingAllCurrentStacks(self, key: str) -> None:
|
||||
"""Delete a user setting from the global stack and all extruder stacks.
|
||||
|
||||
:param key: :type{str} the name of the key to delete
|
||||
"""
|
||||
Logger.log("i", "Clearing the setting [%s] from all stacks", key)
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
@ -454,11 +461,13 @@ class MachineManager(QObject):
|
|||
for container in send_emits_containers:
|
||||
container.sendPostponedEmits()
|
||||
|
||||
## Check if none of the stacks contain error states
|
||||
# Note that the _stacks_have_errors is cached due to performance issues
|
||||
# Calling _checkStack(s)ForErrors on every change is simply too expensive
|
||||
@pyqtProperty(bool, notify = stacksValidationChanged)
|
||||
def stacksHaveErrors(self) -> bool:
|
||||
"""Check if none of the stacks contain error states
|
||||
|
||||
Note that the _stacks_have_errors is cached due to performance issues
|
||||
Calling _checkStack(s)ForErrors on every change is simply too expensive
|
||||
"""
|
||||
return bool(self._stacks_have_errors)
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
|
@ -532,14 +541,16 @@ class MachineManager(QObject):
|
|||
return material.getId()
|
||||
return ""
|
||||
|
||||
## Gets the layer height of the currently active quality profile.
|
||||
#
|
||||
# This is indicated together with the name of the active quality profile.
|
||||
#
|
||||
# \return The layer height of the currently active quality profile. If
|
||||
# there is no quality profile, this returns the default layer height.
|
||||
@pyqtProperty(float, notify = activeQualityGroupChanged)
|
||||
def activeQualityLayerHeight(self) -> float:
|
||||
"""Gets the layer height of the currently active quality profile.
|
||||
|
||||
This is indicated together with the name of the active quality profile.
|
||||
|
||||
:return: The layer height of the currently active quality profile. If
|
||||
there is no quality profile, this returns the default layer height.
|
||||
"""
|
||||
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId())
|
||||
|
@ -609,13 +620,15 @@ class MachineManager(QObject):
|
|||
|
||||
return result
|
||||
|
||||
## Returns whether there is anything unsupported in the current set-up.
|
||||
#
|
||||
# The current set-up signifies the global stack and all extruder stacks,
|
||||
# so this indicates whether there is any container in any of the container
|
||||
# stacks that is not marked as supported.
|
||||
@pyqtProperty(bool, notify = activeQualityChanged)
|
||||
def isCurrentSetupSupported(self) -> bool:
|
||||
"""Returns whether there is anything unsupported in the current set-up.
|
||||
|
||||
The current set-up signifies the global stack and all extruder stacks,
|
||||
so this indicates whether there is any container in any of the container
|
||||
stacks that is not marked as supported.
|
||||
"""
|
||||
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
for stack in [self._global_container_stack] + self._global_container_stack.extruderList:
|
||||
|
@ -626,9 +639,10 @@ class MachineManager(QObject):
|
|||
return False
|
||||
return True
|
||||
|
||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||
@pyqtSlot(str)
|
||||
def copyValueToExtruders(self, key: str) -> None:
|
||||
"""Copy the value of the setting of the current extruder to all other extruders as well as the global container."""
|
||||
|
||||
if self._active_container_stack is None or self._global_container_stack is None:
|
||||
return
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
|
@ -638,9 +652,10 @@ class MachineManager(QObject):
|
|||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
||||
|
||||
## Copy the value of all manually changed settings of the current extruder to all other extruders.
|
||||
@pyqtSlot()
|
||||
def copyAllValuesToExtruders(self) -> None:
|
||||
"""Copy the value of all manually changed settings of the current extruder to all other extruders."""
|
||||
|
||||
if self._active_container_stack is None or self._global_container_stack is None:
|
||||
return
|
||||
|
||||
|
@ -652,19 +667,23 @@ class MachineManager(QObject):
|
|||
# Check if the value has to be replaced
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||
|
||||
## Get the Definition ID to use to select quality profiles for the currently active machine
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeQualityDefinitionId(self) -> str:
|
||||
"""Get the Definition ID to use to select quality profiles for the currently active machine
|
||||
|
||||
:returns: DefinitionID (string) if found, empty string otherwise
|
||||
"""
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return ""
|
||||
return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||
|
||||
## Gets how the active definition calls variants
|
||||
# Caveat: per-definition-variant-title is currently not translated (though the fallback is)
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeDefinitionVariantsName(self) -> str:
|
||||
"""Gets how the active definition calls variants
|
||||
|
||||
Caveat: per-definition-variant-title is currently not translated (though the fallback is)
|
||||
"""
|
||||
fallback_title = catalog.i18nc("@label", "Nozzle")
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title)
|
||||
|
@ -712,9 +731,10 @@ class MachineManager(QObject):
|
|||
# This reuses the method and remove all printers recursively
|
||||
self.removeMachine(hidden_containers[0].getId())
|
||||
|
||||
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
||||
@pyqtProperty(bool, notify = activeMaterialChanged)
|
||||
def variantBuildplateCompatible(self) -> bool:
|
||||
"""The selected buildplate is compatible if it is compatible with all the materials in all the extruders"""
|
||||
|
||||
if not self._global_container_stack:
|
||||
return True
|
||||
|
||||
|
@ -731,10 +751,12 @@ class MachineManager(QObject):
|
|||
|
||||
return buildplate_compatible
|
||||
|
||||
## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible
|
||||
# for the other material but the buildplate is still usable
|
||||
@pyqtProperty(bool, notify = activeMaterialChanged)
|
||||
def variantBuildplateUsable(self) -> bool:
|
||||
"""The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible
|
||||
|
||||
for the other material but the buildplate is still usable
|
||||
"""
|
||||
if not self._global_container_stack:
|
||||
return True
|
||||
|
||||
|
@ -755,11 +777,13 @@ class MachineManager(QObject):
|
|||
|
||||
return result
|
||||
|
||||
## Get the Definition ID of a machine (specified by ID)
|
||||
# \param machine_id string machine id to get the definition ID of
|
||||
# \returns DefinitionID if found, None otherwise
|
||||
@pyqtSlot(str, result = str)
|
||||
def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]:
|
||||
"""Get the Definition ID of a machine (specified by ID)
|
||||
|
||||
:param machine_id: string machine id to get the definition ID of
|
||||
:returns: DefinitionID if found, None otherwise
|
||||
"""
|
||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||
if containers:
|
||||
return containers[0].definition.getId()
|
||||
|
@ -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)
|
||||
return result
|
||||
|
||||
## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed
|
||||
def correctExtruderSettings(self) -> None:
|
||||
"""Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed"""
|
||||
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges):
|
||||
|
@ -807,9 +832,11 @@ class MachineManager(QObject):
|
|||
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||
caution_message.show()
|
||||
|
||||
## Set the amount of extruders on the active machine (global stack)
|
||||
# \param extruder_count int the number of extruders to set
|
||||
def setActiveMachineExtruderCount(self, extruder_count: int) -> None:
|
||||
"""Set the amount of extruders on the active machine (global stack)
|
||||
|
||||
:param extruder_count: int the number of extruders to set
|
||||
"""
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
extruder_manager = self._application.getExtruderManager()
|
||||
|
@ -906,9 +933,10 @@ class MachineManager(QObject):
|
|||
def defaultExtruderPosition(self) -> str:
|
||||
return self._default_extruder_position
|
||||
|
||||
## This will fire the propertiesChanged for all settings so they will be updated in the front-end
|
||||
@pyqtSlot()
|
||||
def forceUpdateAllSettings(self) -> None:
|
||||
"""This will fire the propertiesChanged for all settings so they will be updated in the front-end"""
|
||||
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
|
@ -949,11 +977,13 @@ class MachineManager(QObject):
|
|||
def _onMaterialNameChanged(self) -> None:
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
## Get the signals that signal that the containers changed for all stacks.
|
||||
#
|
||||
# This includes the global stack and all extruder stacks. So if any
|
||||
# container changed anywhere.
|
||||
def _getContainerChangedSignals(self) -> List[Signal]:
|
||||
"""Get the signals that signal that the containers changed for all stacks.
|
||||
|
||||
This includes the global stack and all extruder stacks. So if any
|
||||
container changed anywhere.
|
||||
"""
|
||||
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]]
|
||||
|
@ -966,18 +996,21 @@ class MachineManager(QObject):
|
|||
container = extruder.userChanges
|
||||
container.setProperty(setting_name, property_name, property_value)
|
||||
|
||||
## Reset all setting properties of a setting for all extruders.
|
||||
# \param setting_name The ID of the setting to reset.
|
||||
@pyqtSlot(str)
|
||||
def resetSettingForAllExtruders(self, setting_name: str) -> None:
|
||||
"""Reset all setting properties of a setting for all extruders.
|
||||
|
||||
:param setting_name: The ID of the setting to reset.
|
||||
"""
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
for extruder in self._global_container_stack.extruderList:
|
||||
container = extruder.userChanges
|
||||
container.removeInstance(setting_name)
|
||||
|
||||
## Update _current_root_material_id when the current root material was changed.
|
||||
def _onRootMaterialChanged(self) -> None:
|
||||
"""Update _current_root_material_id when the current root material was changed."""
|
||||
|
||||
self._current_root_material_id = {}
|
||||
|
||||
changed = False
|
||||
|
@ -1139,8 +1172,9 @@ class MachineManager(QObject):
|
|||
return False
|
||||
return True
|
||||
|
||||
## Update current quality type and machine after setting material
|
||||
def _updateQualityWithMaterial(self, *args: Any) -> None:
|
||||
"""Update current quality type and machine after setting material"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
@ -1181,8 +1215,9 @@ class MachineManager(QObject):
|
|||
current_quality_type, quality_type)
|
||||
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
|
||||
|
||||
## Update the current intent after the quality changed
|
||||
def _updateIntentWithQuality(self):
|
||||
"""Update the current intent after the quality changed"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
@ -1209,12 +1244,14 @@ class MachineManager(QObject):
|
|||
category = current_category
|
||||
self.setIntentByCategory(category)
|
||||
|
||||
## Update the material profile in the current stacks when the variant is
|
||||
# changed.
|
||||
# \param position The extruder stack to update. If provided with None, all
|
||||
# extruder stacks will be updated.
|
||||
@pyqtSlot()
|
||||
def updateMaterialWithVariant(self, position: Optional[str] = None) -> None:
|
||||
"""Update the material profile in the current stacks when the variant is
|
||||
|
||||
changed.
|
||||
:param position: The extruder stack to update. If provided with None, all
|
||||
extruder stacks will be updated.
|
||||
"""
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
if position is None:
|
||||
|
@ -1249,10 +1286,12 @@ class MachineManager(QObject):
|
|||
material_node = nozzle_node.preferredMaterial(approximate_material_diameter)
|
||||
self._setMaterial(position_item, material_node)
|
||||
|
||||
## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
|
||||
# instance with the same network key.
|
||||
@pyqtSlot(str)
|
||||
def switchPrinterType(self, machine_name: str) -> None:
|
||||
"""Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
|
||||
|
||||
instance with the same network key.
|
||||
"""
|
||||
# Don't switch if the user tries to change to the same type of printer
|
||||
if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name:
|
||||
return
|
||||
|
@ -1402,10 +1441,12 @@ class MachineManager(QObject):
|
|||
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
|
||||
self.setMaterial(position, material_node)
|
||||
|
||||
## Global_stack: if you want to provide your own global_stack instead of the current active one
|
||||
# if you update an active machine, special measures have to be taken.
|
||||
@pyqtSlot(str, "QVariant")
|
||||
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||
"""Global_stack: if you want to provide your own global_stack instead of the current active one
|
||||
|
||||
if you update an active machine, special measures have to be taken.
|
||||
"""
|
||||
if global_stack is not None and global_stack != self._global_container_stack:
|
||||
global_stack.extruderList[int(position)].material = container_node.container
|
||||
return
|
||||
|
@ -1451,10 +1492,12 @@ class MachineManager(QObject):
|
|||
# Get all the quality groups for this global stack and filter out by quality_type
|
||||
self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type])
|
||||
|
||||
## Optionally provide global_stack if you want to use your own
|
||||
# The active global_stack is treated differently.
|
||||
@pyqtSlot(QObject)
|
||||
def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||
"""Optionally provide global_stack if you want to use your own
|
||||
|
||||
The active global_stack is treated differently.
|
||||
"""
|
||||
if global_stack is not None and global_stack != self._global_container_stack:
|
||||
if quality_group is None:
|
||||
Logger.log("e", "Could not set quality group because quality group is None")
|
||||
|
@ -1516,15 +1559,17 @@ class MachineManager(QObject):
|
|||
return {"main": main_part,
|
||||
"suffix": suffix_part}
|
||||
|
||||
## Change the intent category of the current printer.
|
||||
#
|
||||
# All extruders can change their profiles. If an intent profile is
|
||||
# available with the desired intent category, that one will get chosen.
|
||||
# Otherwise the intent profile will be left to the empty profile, which
|
||||
# represents the "default" intent category.
|
||||
# \param intent_category The intent category to change to.
|
||||
@pyqtSlot(str)
|
||||
def setIntentByCategory(self, intent_category: str) -> None:
|
||||
"""Change the intent category of the current printer.
|
||||
|
||||
All extruders can change their profiles. If an intent profile is
|
||||
available with the desired intent category, that one will get chosen.
|
||||
Otherwise the intent profile will be left to the empty profile, which
|
||||
represents the "default" intent category.
|
||||
:param intent_category: The intent category to change to.
|
||||
"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
@ -1556,21 +1601,25 @@ class MachineManager(QObject):
|
|||
else: # No intent had the correct category.
|
||||
extruder.intent = empty_intent_container
|
||||
|
||||
## Get the currently activated quality group.
|
||||
#
|
||||
# If no printer is added yet or the printer doesn't have quality profiles,
|
||||
# this returns ``None``.
|
||||
# \return The currently active quality group.
|
||||
def activeQualityGroup(self) -> Optional["QualityGroup"]:
|
||||
"""Get the currently activated quality group.
|
||||
|
||||
If no printer is added yet or the printer doesn't have quality profiles,
|
||||
this returns ``None``.
|
||||
:return: The currently active quality group.
|
||||
"""
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack or global_stack.quality == empty_quality_container:
|
||||
return None
|
||||
return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType)
|
||||
|
||||
## Get the name of the active quality group.
|
||||
# \return The name of the active quality group.
|
||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||
def activeQualityGroupName(self) -> str:
|
||||
"""Get the name of the active quality group.
|
||||
|
||||
:return: The name of the active quality group.
|
||||
"""
|
||||
quality_group = self.activeQualityGroup()
|
||||
if quality_group is None:
|
||||
return ""
|
||||
|
@ -1643,9 +1692,10 @@ class MachineManager(QObject):
|
|||
self.updateMaterialWithVariant(None)
|
||||
self._updateQualityWithMaterial()
|
||||
|
||||
## This function will translate any printer type name to an abbreviated printer type name
|
||||
@pyqtSlot(str, result = str)
|
||||
def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
|
||||
"""This function will translate any printer type name to an abbreviated printer type name"""
|
||||
|
||||
abbr_machine = ""
|
||||
for word in re.findall(r"[\w']+", machine_type_name):
|
||||
if word.lower() == "ultimaker":
|
||||
|
|
|
@ -10,10 +10,13 @@ from UM.Resources import Resources
|
|||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
## Are machine names valid?
|
||||
#
|
||||
# Performs checks based on the length of the name.
|
||||
|
||||
class MachineNameValidator(QObject):
|
||||
"""Are machine names valid?
|
||||
|
||||
Performs checks based on the length of the name.
|
||||
"""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
|
@ -32,12 +35,13 @@ class MachineNameValidator(QObject):
|
|||
|
||||
validationChanged = pyqtSignal()
|
||||
|
||||
## Check if a specified machine name is allowed.
|
||||
#
|
||||
# \param name The machine name to check.
|
||||
# \return ``QValidator.Invalid`` if it's disallowed, or
|
||||
# ``QValidator.Acceptable`` if it's allowed.
|
||||
def validate(self, name):
|
||||
"""Check if a specified machine name is allowed.
|
||||
|
||||
:param name: The machine name to check.
|
||||
:return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed.
|
||||
"""
|
||||
|
||||
#Check for file name length of the current settings container (which is the longest file we're saving with the name).
|
||||
try:
|
||||
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||
|
@ -50,9 +54,10 @@ class MachineNameValidator(QObject):
|
|||
|
||||
return QValidator.Acceptable #All checks succeeded.
|
||||
|
||||
## Updates the validation state of a machine name text field.
|
||||
@pyqtSlot(str)
|
||||
def updateValidation(self, new_name):
|
||||
"""Updates the validation state of a machine name text field."""
|
||||
|
||||
is_valid = self.validate(new_name)
|
||||
if is_valid == QValidator.Acceptable:
|
||||
self.validation_regex = "^.*$" #Matches anything.
|
||||
|
|
|
@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation
|
|||
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
## Simple operation to set the extruder a certain object should be printed with.
|
||||
|
||||
class SetObjectExtruderOperation(Operation):
|
||||
"""Simple operation to set the extruder a certain object should be printed with."""
|
||||
|
||||
def __init__(self, node: SceneNode, extruder_id: str) -> None:
|
||||
self._node = node
|
||||
self._extruder_id = extruder_id
|
||||
|
|
|
@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject):
|
|||
|
||||
settingsWithIntheritanceChanged = pyqtSignal()
|
||||
|
||||
## Get the keys of all children settings with an override.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getChildrenKeysWithOverride(self, key: str) -> List[str]:
|
||||
"""Get the keys of all children settings with an override."""
|
||||
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key=key)
|
||||
|
@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject):
|
|||
def settingsWithInheritanceWarning(self) -> List[str]:
|
||||
return self._settings_with_inheritance_warning
|
||||
|
||||
## Check if a setting has an inheritance function that is overwritten
|
||||
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
|
||||
"""Check if a setting has an inheritance function that is overwritten"""
|
||||
|
||||
has_setting_function = False
|
||||
if not stack:
|
||||
stack = self._active_container_stack
|
||||
|
@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject):
|
|||
|
||||
containers = [] # type: List[ContainerInterface]
|
||||
|
||||
## Check if the setting has a user state. If not, it is never overwritten.
|
||||
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||
"""Check if the setting has a user state. If not, it is never overwritten."""
|
||||
|
||||
if not has_user_state:
|
||||
return False
|
||||
|
||||
## If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
||||
# If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
||||
if not stack.getProperty(key, "enabled"):
|
||||
return False
|
||||
|
||||
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
||||
user_container = stack.getTop()
|
||||
"""Also check if the top container is not a setting function (this happens if the inheritance is restored)."""
|
||||
|
||||
if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction):
|
||||
return False
|
||||
|
||||
|
|
|
@ -15,21 +15,24 @@ from UM.Application import Application
|
|||
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
|
||||
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
|
||||
# this stack still resolve.
|
||||
@signalemitter
|
||||
class SettingOverrideDecorator(SceneNodeDecorator):
|
||||
## Event indicating that the user selected a different extruder.
|
||||
activeExtruderChanged = Signal()
|
||||
"""A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
|
||||
|
||||
the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
|
||||
this stack still resolve.
|
||||
"""
|
||||
activeExtruderChanged = Signal()
|
||||
"""Event indicating that the user selected a different extruder."""
|
||||
|
||||
## Non-printing meshes
|
||||
#
|
||||
# If these settings are True for any mesh, the mesh does not need a convex hull,
|
||||
# and is sent to the slicer regardless of whether it fits inside the build volume.
|
||||
# Note that Support Mesh is not in here because it actually generates
|
||||
# g-code in the volume of the mesh.
|
||||
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
"""Non-printing meshes
|
||||
|
||||
If these settings are True for any mesh, the mesh does not need a convex hull,
|
||||
and is sent to the slicer regardless of whether it fits inside the build volume.
|
||||
Note that Support Mesh is not in here because it actually generates
|
||||
g-code in the volume of the mesh.
|
||||
"""
|
||||
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
|
||||
|
||||
def __init__(self):
|
||||
|
@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
return "SettingOverrideInstanceContainer-%s" % uuid.uuid1()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
## Create a fresh decorator object
|
||||
deep_copy = SettingOverrideDecorator()
|
||||
"""Create a fresh decorator object"""
|
||||
|
||||
## Copy the instance
|
||||
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
|
||||
"""Copy the instance"""
|
||||
|
||||
# A unique name must be added, or replaceContainer will not replace it
|
||||
instance_container.setMetaDataEntry("id", self._generateUniqueName())
|
||||
|
@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
|
||||
return deep_copy
|
||||
|
||||
## Gets the currently active extruder to print this object with.
|
||||
#
|
||||
# \return An extruder's container stack.
|
||||
def getActiveExtruder(self):
|
||||
"""Gets the currently active extruder to print this object with.
|
||||
|
||||
:return: An extruder's container stack.
|
||||
"""
|
||||
|
||||
return self._extruder_stack
|
||||
|
||||
## Gets the signal that emits if the active extruder changed.
|
||||
#
|
||||
# This can then be accessed via a decorator.
|
||||
def getActiveExtruderChangedSignal(self):
|
||||
"""Gets the signal that emits if the active extruder changed.
|
||||
|
||||
This can then be accessed via a decorator.
|
||||
"""
|
||||
|
||||
return self.activeExtruderChanged
|
||||
|
||||
## Gets the currently active extruders position
|
||||
#
|
||||
# \return An extruder's position, or None if no position info is available.
|
||||
def getActiveExtruderPosition(self):
|
||||
"""Gets the currently active extruders position
|
||||
|
||||
:return: An extruder's position, or None if no position info is available.
|
||||
"""
|
||||
|
||||
# for support_meshes, always use the support_extruder
|
||||
if self.getStack().getProperty("support_mesh", "value"):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
Application.getInstance().getBackend().needsSlicing()
|
||||
Application.getInstance().getBackend().tickle()
|
||||
|
||||
## Makes sure that the stack upon which the container stack is placed is
|
||||
# kept up to date.
|
||||
def _updateNextStack(self):
|
||||
"""Makes sure that the stack upon which the container stack is placed is
|
||||
|
||||
kept up to date.
|
||||
"""
|
||||
if self._extruder_stack:
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
|
||||
if extruder_stack:
|
||||
|
@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
else:
|
||||
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
|
||||
## Changes the extruder with which to print this node.
|
||||
#
|
||||
# \param extruder_stack_id The new extruder stack to print with.
|
||||
def setActiveExtruder(self, extruder_stack_id):
|
||||
"""Changes the extruder with which to print this node.
|
||||
|
||||
:param extruder_stack_id: The new extruder stack to print with.
|
||||
"""
|
||||
|
||||
self._extruder_stack = extruder_stack_id
|
||||
self._updateNextStack()
|
||||
ExtruderManager.getInstance().resetSelectedObjectExtruders()
|
||||
|
|
|
@ -30,11 +30,17 @@ class Snapshot:
|
|||
|
||||
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
|
||||
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()
|
||||
active_camera = scene.getActiveCamera()
|
||||
render_width, render_height = active_camera.getWindowSize()
|
||||
|
|
|
@ -15,13 +15,15 @@ if TYPE_CHECKING:
|
|||
from cura.MachineAction import MachineAction
|
||||
|
||||
|
||||
## Raised when trying to add an unknown machine action as a required action
|
||||
class UnknownMachineActionError(Exception):
|
||||
"""Raised when trying to add an unknown machine action as a required action"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
## Raised when trying to add a machine action that does not have an unique key.
|
||||
class NotUniqueMachineActionError(Exception):
|
||||
"""Raised when trying to add a machine action that does not have an unique key."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
@ -71,9 +73,11 @@ class MachineActionManager(QObject):
|
|||
self._definition_ids_with_default_actions_added.add(definition_id)
|
||||
Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
|
||||
|
||||
## Add a required action to a machine
|
||||
# Raises an exception when the action is not recognised.
|
||||
def addRequiredAction(self, definition_id: str, action_key: str) -> None:
|
||||
"""Add a required action to a machine
|
||||
|
||||
Raises an exception when the action is not recognised.
|
||||
"""
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._required_actions:
|
||||
if self._machine_actions[action_key] not in self._required_actions[definition_id]:
|
||||
|
@ -83,8 +87,9 @@ class MachineActionManager(QObject):
|
|||
else:
|
||||
raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
|
||||
|
||||
## Add a supported action to a machine.
|
||||
def addSupportedAction(self, definition_id: str, action_key: str) -> None:
|
||||
"""Add a supported action to a machine."""
|
||||
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._supported_actions:
|
||||
if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
|
||||
|
@ -94,8 +99,9 @@ class MachineActionManager(QObject):
|
|||
else:
|
||||
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
|
||||
|
||||
## Add an action to the first start list of a machine.
|
||||
def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
|
||||
"""Add an action to the first start list of a machine."""
|
||||
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._first_start_actions:
|
||||
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
|
||||
|
@ -104,57 +110,69 @@ class MachineActionManager(QObject):
|
|||
else:
|
||||
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
|
||||
|
||||
## Add a (unique) MachineAction
|
||||
# if the Key of the action is not unique, an exception is raised.
|
||||
def addMachineAction(self, action: "MachineAction") -> None:
|
||||
"""Add a (unique) MachineAction
|
||||
|
||||
if the Key of the action is not unique, an exception is raised.
|
||||
"""
|
||||
if action.getKey() not in self._machine_actions:
|
||||
self._machine_actions[action.getKey()] = action
|
||||
else:
|
||||
raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey())
|
||||
|
||||
## Get all actions supported by given machine
|
||||
# \param definition_id The ID of the definition you want the supported actions of
|
||||
# \returns set of supported actions.
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
"""Get all actions supported by given machine
|
||||
|
||||
:param definition_id: The ID of the definition you want the supported actions of
|
||||
:returns: set of supported actions.
|
||||
"""
|
||||
if definition_id in self._supported_actions:
|
||||
return list(self._supported_actions[definition_id])
|
||||
else:
|
||||
return list()
|
||||
|
||||
## Get all actions required by given machine
|
||||
# \param definition_id The ID of the definition you want the required actions of
|
||||
# \returns set of required actions.
|
||||
def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
"""Get all actions required by given machine
|
||||
|
||||
:param definition_id: The ID of the definition you want the required actions of
|
||||
:returns: set of required actions.
|
||||
"""
|
||||
if definition_id in self._required_actions:
|
||||
return self._required_actions[definition_id]
|
||||
else:
|
||||
return list()
|
||||
|
||||
## Get all actions that need to be performed upon first start of a given machine.
|
||||
# Note that contrary to required / supported actions a list is returned (as it could be required to run the same
|
||||
# action multiple times).
|
||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||
# \returns List of actions.
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
"""Get all actions that need to be performed upon first start of a given machine.
|
||||
|
||||
Note that contrary to required / supported actions a list is returned (as it could be required to run the same
|
||||
action multiple times).
|
||||
:param definition_id: The ID of the definition that you want to get the "on added" actions for.
|
||||
:returns: List of actions.
|
||||
"""
|
||||
if definition_id in self._first_start_actions:
|
||||
return self._first_start_actions[definition_id]
|
||||
else:
|
||||
return []
|
||||
|
||||
## Remove Machine action from manager
|
||||
# \param action to remove
|
||||
def removeMachineAction(self, action: "MachineAction") -> None:
|
||||
"""Remove Machine action from manager
|
||||
|
||||
:param action: to remove
|
||||
"""
|
||||
try:
|
||||
del self._machine_actions[action.getKey()]
|
||||
except KeyError:
|
||||
Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey())
|
||||
|
||||
## Get MachineAction by key
|
||||
# \param key String of key to select
|
||||
# \return Machine action if found, None otherwise
|
||||
def getMachineAction(self, key: str) -> Optional["MachineAction"]:
|
||||
"""Get MachineAction by key
|
||||
|
||||
:param key: String of key to select
|
||||
:return: Machine action if found, None otherwise
|
||||
"""
|
||||
if key in self._machine_actions:
|
||||
return self._machine_actions[key]
|
||||
else:
|
||||
|
|
|
@ -31,8 +31,9 @@ class _NodeInfo:
|
|||
self.is_group = is_group # type: bool
|
||||
|
||||
|
||||
## Keep track of all objects in the project
|
||||
class ObjectsModel(ListModel):
|
||||
"""Keep track of all objects in the project"""
|
||||
|
||||
NameRole = Qt.UserRole + 1
|
||||
SelectedRole = Qt.UserRole + 2
|
||||
OutsideAreaRole = Qt.UserRole + 3
|
||||
|
|
|
@ -21,11 +21,13 @@ if TYPE_CHECKING:
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## A class for processing and the print times per build plate as well as managing the job name
|
||||
#
|
||||
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||
# This job name is requested by the JobSpecs qml file.
|
||||
class PrintInformation(QObject):
|
||||
"""A class for processing and the print times per build plate as well as managing the job name
|
||||
|
||||
This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||
This job name is requested by the JobSpecs qml file.
|
||||
"""
|
||||
|
||||
|
||||
UNTITLED_JOB_NAME = "Untitled"
|
||||
|
||||
|
@ -380,10 +382,12 @@ class PrintInformation(QObject):
|
|||
def baseName(self):
|
||||
return self._base_name
|
||||
|
||||
## Created an acronym-like abbreviated machine name from the currently
|
||||
# active machine name.
|
||||
# Called each time the global stack is switched.
|
||||
def _defineAbbreviatedMachineName(self) -> None:
|
||||
"""Created an acronym-like abbreviated machine name from the currently active machine name.
|
||||
|
||||
Called each time the global stack is switched.
|
||||
"""
|
||||
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
self._abbr_machine = ""
|
||||
|
@ -392,8 +396,9 @@ class PrintInformation(QObject):
|
|||
|
||||
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, to_strip: str) -> str:
|
||||
"""Utility method that strips accents from characters (eg: â -> a)"""
|
||||
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||
|
||||
@pyqtSlot(result = "QVariantMap")
|
||||
|
@ -431,6 +436,7 @@ class PrintInformation(QObject):
|
|||
return
|
||||
self._change_timer.start()
|
||||
|
||||
## Listen to scene changes to check if we need to reset the print information
|
||||
def _onSceneChanged(self) -> None:
|
||||
"""Listen to scene changes to check if we need to reset the print information"""
|
||||
|
||||
self.setToZeroPrintInformation(self._active_build_plate)
|
||||
|
|
|
@ -11,13 +11,15 @@ from typing import Callable
|
|||
SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$")
|
||||
|
||||
|
||||
## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported
|
||||
# APIs, meaning that those APIs should be versioned and maintained.
|
||||
#
|
||||
# \param since_version The earliest version since when this API becomes supported. This means that since this version,
|
||||
# this API function is supposed to behave the same. This parameter is not used. It's just a
|
||||
# documentation.
|
||||
def api(since_version: str) -> Callable:
|
||||
"""Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported
|
||||
|
||||
APIs, meaning that those APIs should be versioned and maintained.
|
||||
|
||||
:param since_version: The earliest version since when this API becomes supported. This means that since this version,
|
||||
this API function is supposed to behave the same. This parameter is not used. It's just a
|
||||
documentation.
|
||||
"""
|
||||
# Make sure that APi versions are semantic versions
|
||||
if not SEMANTIC_VERSION_REGEX.fullmatch(since_version):
|
||||
raise ValueError("API since_version [%s] is not a semantic version." % since_version)
|
||||
|
|
|
@ -32,8 +32,9 @@ except ImportError:
|
|||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||
class ThreeMFReader(MeshReader):
|
||||
"""Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -55,13 +56,17 @@ class ThreeMFReader(MeshReader):
|
|||
return Matrix()
|
||||
|
||||
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()
|
||||
"""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
|
||||
temp_mat._data[0, 0] = split_transformation[0]
|
||||
temp_mat._data[1, 0] = split_transformation[1]
|
||||
|
@ -80,9 +85,11 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
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]:
|
||||
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
|
||||
:returns: Scene node.
|
||||
"""
|
||||
try:
|
||||
node_name = savitar_node.getName()
|
||||
node_id = savitar_node.getId()
|
||||
|
@ -243,15 +250,17 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
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:
|
||||
"""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 = {
|
||||
"micron": 0.001,
|
||||
"millimeter": 1,
|
||||
|
|
|
@ -89,8 +89,9 @@ class ExtruderInfo:
|
|||
self.intent_info = None
|
||||
|
||||
|
||||
## Base implementation for reading 3MF workspace files.
|
||||
class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
"""Base implementation for reading 3MF workspace files."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -130,18 +131,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._old_new_materials = {}
|
||||
self._machine_info = None
|
||||
|
||||
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||
def getNewId(self, old_id: str):
|
||||
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
|
||||
This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||
"""
|
||||
if old_id not in self._id_mapping:
|
||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||
return self._id_mapping[old_id]
|
||||
|
||||
## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
|
||||
#
|
||||
# In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
|
||||
#
|
||||
def _determineGlobalAndExtruderStackFiles(self, project_file_name: str, file_list: List[str]) -> Tuple[str, List[str]]:
|
||||
"""Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
|
||||
|
||||
In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
|
||||
"""
|
||||
|
||||
archive = zipfile.ZipFile(project_file_name, "r")
|
||||
|
||||
global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)]
|
||||
|
@ -181,10 +185,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
return global_stack_file_list[0], extruder_stack_file_list
|
||||
|
||||
## read some info so we can make decisions
|
||||
# \param file_name
|
||||
# \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||
"""Read some info so we can make decisions
|
||||
|
||||
:param file_name:
|
||||
:param show_dialog: In case we use preRead() to check if a file is a valid project file,
|
||||
we don't want to show a dialog.
|
||||
"""
|
||||
self._clearState()
|
||||
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
|
@ -578,15 +585,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
## Read the project file
|
||||
# Add all the definitions / materials / quality changes that do not exist yet. Then it loads
|
||||
# all the stacks into the container registry. In some cases it will reuse the container for the global stack.
|
||||
# It handles old style project files containing .stack.cfg as well as new style project files
|
||||
# containing global.cfg / extruder.cfg
|
||||
#
|
||||
# \param file_name
|
||||
@call_on_qt_thread
|
||||
def read(self, file_name):
|
||||
"""Read the project file
|
||||
|
||||
Add all the definitions / materials / quality changes that do not exist yet. Then it loads
|
||||
all the stacks into the container registry. In some cases it will reuse the container for the global stack.
|
||||
It handles old style project files containing .stack.cfg as well as new style project files
|
||||
containing global.cfg / extruder.cfg
|
||||
|
||||
:param file_name:
|
||||
"""
|
||||
application = CuraApplication.getInstance()
|
||||
|
||||
try:
|
||||
|
@ -868,19 +877,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
self._machine_info.quality_changes_info.name = quality_changes_name
|
||||
|
||||
## Helper class to create a new quality changes profile.
|
||||
#
|
||||
# This will then later be filled with the appropriate data.
|
||||
# \param quality_type The quality type of the new profile.
|
||||
# \param intent_category The intent category of the new profile.
|
||||
# \param name The name for the profile. This will later be made unique so
|
||||
# it doesn't need to be unique yet.
|
||||
# \param global_stack The global stack showing the configuration that the
|
||||
# profile should be created for.
|
||||
# \param extruder_stack The extruder stack showing the configuration that
|
||||
# the profile should be created for. If this is None, it will be created
|
||||
# for the global stack.
|
||||
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
|
||||
"""Helper class to create a new quality changes profile.
|
||||
|
||||
This will then later be filled with the appropriate data.
|
||||
|
||||
:param quality_type: The quality type of the new profile.
|
||||
:param intent_category: The intent category of the new profile.
|
||||
:param name: The name for the profile. This will later be made unique so
|
||||
it doesn't need to be unique yet.
|
||||
:param global_stack: The global stack showing the configuration that the
|
||||
profile should be created for.
|
||||
:param extruder_stack: The extruder stack showing the configuration that
|
||||
the profile should be created for. If this is None, it will be created
|
||||
for the global stack.
|
||||
"""
|
||||
|
||||
container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||
new_id = base_id + "_" + name
|
||||
|
@ -1089,9 +1101,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
def _getXmlProfileClass(self):
|
||||
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
|
||||
|
||||
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
|
||||
@staticmethod
|
||||
def _getContainerIdListFromSerialized(serialized):
|
||||
"""Get the list of ID's of all containers in a container stack by partially parsing it's serialized data."""
|
||||
|
||||
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
|
||||
parser.read_string(serialized)
|
||||
|
||||
|
|
|
@ -229,9 +229,10 @@ class WorkspaceDialog(QObject):
|
|||
if key in self._result:
|
||||
self._result[key] = strategy
|
||||
|
||||
## Close the backend: otherwise one could end up with "Slicing..."
|
||||
@pyqtSlot()
|
||||
def closeBackend(self):
|
||||
"""Close the backend: otherwise one could end up with "Slicing..."""
|
||||
|
||||
Application.getInstance().getBackend().close()
|
||||
|
||||
def setMaterialConflict(self, material_conflict):
|
||||
|
@ -283,8 +284,9 @@ class WorkspaceDialog(QObject):
|
|||
self.showDialogSignal.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
## Used to notify the dialog so the lock can be released.
|
||||
def notifyClosed(self):
|
||||
"""Used to notify the dialog so the lock can be released."""
|
||||
|
||||
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
||||
self._visible = False
|
||||
try:
|
||||
|
@ -319,8 +321,9 @@ class WorkspaceDialog(QObject):
|
|||
self._view.hide()
|
||||
self.hide()
|
||||
|
||||
## Block thread until the dialog is closed.
|
||||
def waitForClose(self):
|
||||
"""Block thread until the dialog is closed."""
|
||||
|
||||
if self._visible:
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
self._lock.acquire()
|
||||
|
|
|
@ -107,11 +107,13 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
import json
|
||||
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
|
||||
|
||||
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
# \param container That follows the \type{ContainerInterface} to archive.
|
||||
# \param archive The archive to write to.
|
||||
@staticmethod
|
||||
def _writeContainerToArchive(container, archive):
|
||||
"""Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
|
||||
:param container: That follows the :type{ContainerInterface} to archive.
|
||||
:param archive: The archive to write to.
|
||||
"""
|
||||
if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||
return # Empty file, do nothing.
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue