Merge remote-tracking branch 'origin/doxygen_to_restructuredtext_comments' into doxygen_to_restructuredtext_comments

This commit is contained in:
Jelle Spijker 2020-05-08 19:00:47 +02:00
commit a503149452
No known key found for this signature in database
GPG Key ID: 6662DC033BE6B99A
30 changed files with 540 additions and 397 deletions

View File

@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader
import zipfile import zipfile
## A plugin that reads profile data from Cura profile files.
#
# It reads a profile from a .curaprofile file, and returns it as a profile
# instance.
class CuraProfileReader(ProfileReader): class CuraProfileReader(ProfileReader):
## Initialises the cura profile reader. """A plugin that reads profile data from Cura profile files.
# This does nothing since the only other function is basically stateless.
It reads a profile from a .curaprofile file, and returns it as a profile
instance.
"""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialises the cura profile reader.
This does nothing since the only other function is basically stateless.
"""
super().__init__() super().__init__()
## Reads a cura profile from a file and returns it.
#
# \param file_name The file to read the cura profile from.
# \return The cura profiles that were in the file, if any. If the file
# could not be read or didn't contain a valid profile, ``None`` is
# returned.
def read(self, file_name: str) -> List[Optional[InstanceContainer]]: def read(self, file_name: str) -> List[Optional[InstanceContainer]]:
"""Reads a cura profile from a file and returns it.
:param file_name: The file to read the cura profile from.
:return: The cura profiles that were in the file, if any. If the file
could not be read or didn't contain a valid profile, ``None`` is
returned.
"""
try: try:
with zipfile.ZipFile(file_name, "r") as archive: with zipfile.ZipFile(file_name, "r") as archive:
results = [] # type: List[Optional[InstanceContainer]] results = [] # type: List[Optional[InstanceContainer]]
@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader):
serialized_bytes = fhandle.read() serialized_bytes = fhandle.read()
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)] return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)]
## Convert a profile from an old Cura to this Cura if needed.
#
# \param serialized The profile data to convert in the serialized on-disk
# format.
# \param profile_id The name of the profile.
# \return List of serialized profile strings and matching profile names.
def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]: def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]:
"""Convert a profile from an old Cura to this Cura if needed.
:param serialized: The profile data to convert in the serialized on-disk format.
:param profile_id: The name of the profile.
:return: List of serialized profile strings and matching profile names.
"""
parser = configparser.ConfigParser(interpolation = None) parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader):
else: else:
return [(serialized, profile_id)] return [(serialized, profile_id)]
## Load a profile from a serialized string.
#
# \param serialized The profile data to read.
# \param profile_id The name of the profile.
# \return The profile that was stored in the string.
def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]: def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]:
"""Load a profile from a serialized string.
:param serialized: The profile data to read.
:param profile_id: The name of the profile.
:return: The profile that was stored in the string.
"""
# Create an empty profile. # Create an empty profile.
profile = InstanceContainer(profile_id) profile = InstanceContainer(profile_id)
profile.setMetaDataEntry("type", "quality_changes") profile.setMetaDataEntry("type", "quality_changes")
@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader):
profile.setMetaDataEntry("definition", active_quality_definition) profile.setMetaDataEntry("definition", active_quality_definition)
return profile return profile
## Upgrade a serialized profile to the current profile format.
#
# \param serialized The profile data to convert.
# \param profile_id The name of the profile.
# \param source_version The profile version of 'serialized'.
# \return List of serialized profile strings and matching profile names.
def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]: def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]:
"""Upgrade a serialized profile to the current profile format.
:param serialized: The profile data to convert.
:param profile_id: The name of the profile.
:param source_version: The profile version of 'serialized'.
:return: List of serialized profile strings and matching profile names.
"""
source_version = main_version * 1000000 + setting_version source_version = main_version * 1000000 + setting_version
from UM.VersionUpgradeManager import VersionUpgradeManager from UM.VersionUpgradeManager import VersionUpgradeManager

View File

@ -6,15 +6,18 @@ from UM.Logger import Logger
from cura.ReaderWriters.ProfileWriter import ProfileWriter from cura.ReaderWriters.ProfileWriter import ProfileWriter
import zipfile import zipfile
## Writes profiles to Cura's own profile format with config files.
class CuraProfileWriter(ProfileWriter): class CuraProfileWriter(ProfileWriter):
## Writes a profile to the specified file path. """Writes profiles to Cura's own profile format with config files."""
#
# \param path \type{string} The file to output to.
# \param profiles \type{Profile} \type{List} The profile(s) to write to that file.
# \return \code True \endcode if the writing was successful, or \code
# False \endcode if it wasn't.
def write(self, path, profiles): def write(self, path, profiles):
"""Writes a profile to the specified file path.
:param path: :type{string} The file to output to.
:param profiles: :type{Profile} :type{List} The profile(s) to write to that file.
:return: True if the writing was successful, or
False if it wasn't.
"""
if type(profiles) != list: if type(profiles) != list:
profiles = [profiles] profiles = [profiles]

View File

@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## This Extension checks for new versions of the firmware based on the latest checked version number.
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications.
class FirmwareUpdateChecker(Extension): class FirmwareUpdateChecker(Extension):
"""This Extension checks for new versions of the firmware based on the latest checked version number.
The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
to change it to work for other applications.
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension):
self._check_job = None self._check_job = None
self._checked_printer_names = set() # type: Set[str] self._checked_printer_names = set() # type: Set[str]
## Callback for the message that is spawned when there is a new version.
def _onActionTriggered(self, message, action): def _onActionTriggered(self, message, action):
"""Callback for the message that is spawned when there is a new version."""
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD: if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
machine_id = message.getMachineId() machine_id = message.getMachineId()
download_url = message.getDownloadUrl() download_url = message.getDownloadUrl()
@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension):
def _onJobFinished(self, *args, **kwargs): def _onJobFinished(self, *args, **kwargs):
self._check_job = None self._check_job = None
## Connect with software.ultimaker.com, load latest.version and check version info.
# If the version info is different from the current version, spawn a message to
# allow the user to download it.
#
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
# This is used when checking for a new firmware version at startup.
def checkFirmwareVersion(self, container = None, silent = False): def checkFirmwareVersion(self, container = None, silent = False):
"""Connect with software.ultimaker.com, load latest.version and check version info.
If the version info is different from the current version, spawn a message to
allow the user to download it.
:param silent: type(boolean) Suppresses messages other than "new version found" messages.
This is used when checking for a new firmware version at startup.
"""
container_name = container.definition.getName() container_name = container.definition.getName()
if container_name in self._checked_printer_names: if container_name in self._checked_printer_names:
return return

View File

@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## This job checks if there is an update available on the provided URL.
class FirmwareUpdateCheckerJob(Job): class FirmwareUpdateCheckerJob(Job):
"""This job checks if there is an update available on the provided URL."""
STRING_ZERO_VERSION = "0.0.0" STRING_ZERO_VERSION = "0.0.0"
STRING_EPSILON_VERSION = "0.0.1" STRING_EPSILON_VERSION = "0.0.1"
ZERO_VERSION = Version(STRING_ZERO_VERSION) ZERO_VERSION = Version(STRING_ZERO_VERSION)

View File

@ -19,8 +19,10 @@ if MYPY:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class FirmwareUpdaterMachineAction(MachineAction): class FirmwareUpdaterMachineAction(MachineAction):
"""Upgrade the firmware of a machine by USB with this action."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware")) super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
self._qml_url = "FirmwareUpdaterMachineAction.qml" self._qml_url = "FirmwareUpdaterMachineAction.qml"

View File

@ -7,10 +7,13 @@ from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementin
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database. from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
## A file reader that reads gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader): class GCodeGzReader(MeshReader):
"""A file reader that reads gzipped g-code.
If you're zipping g-code, you might as well use gzip!
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType( MimeTypeDatabase.addMimeType(

View File

@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## A file writer that writes gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter): class GCodeGzWriter(MeshWriter):
"""A file writer that writes gzipped g-code.
If you're zipping g-code, you might as well use gzip!
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(add_to_recent_files = False) super().__init__(add_to_recent_files = False)
## Writes the gzipped g-code to a stream.
#
# Note that even though the function accepts a collection of nodes, the
# entire scene is always written to the file since it is not possible to
# separate the g-code for just specific nodes.
#
# \param stream The stream to write the gzipped g-code to.
# \param nodes This is ignored.
# \param mode Additional information on what type of stream to use. This
# must always be binary mode.
# \return Whether the write was successful.
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
"""Writes the gzipped g-code to a stream.
Note that even though the function accepts a collection of nodes, the
entire scene is always written to the file since it is not possible to
separate the g-code for just specific nodes.
:param stream: The stream to write the gzipped g-code to.
:param nodes: This is ignored.
:param mode: Additional information on what type of stream to use. This
must always be binary mode.
:return: Whether the write was successful.
"""
if mode != MeshWriter.OutputMode.BinaryMode: if mode != MeshWriter.OutputMode.BinaryMode:
Logger.log("e", "GCodeGzWriter does not support text mode.") Logger.log("e", "GCodeGzWriter does not support text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode.")) self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode."))

View File

@ -12,40 +12,48 @@ catalog = i18nCatalog("cura")
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
## A class that reads profile data from g-code files.
#
# It reads the profile data from g-code files and stores it in a new profile.
# This class currently does not process the rest of the g-code in any way.
class GCodeProfileReader(ProfileReader): class GCodeProfileReader(ProfileReader):
## The file format version of the serialized g-code. """A class that reads profile data from g-code files.
#
# It can only read settings with the same version as the version it was It reads the profile data from g-code files and stores it in a new profile.
# written with. If the file format is changed in a way that breaks reverse This class currently does not process the rest of the g-code in any way.
# compatibility, increment this version number! """
version = 3
version = 3
"""The file format version of the serialized g-code.
It can only read settings with the same version as the version it was
written with. If the file format is changed in a way that breaks reverse
compatibility, increment this version number!
"""
## Dictionary that defines how characters are escaped when embedded in
# g-code.
#
# Note that the keys of this dictionary are regex strings. The values are
# not.
escape_characters = { escape_characters = {
re.escape("\\\\"): "\\", #The escape character. re.escape("\\\\"): "\\", #The escape character.
re.escape("\\n"): "\n", #Newlines. They break off the comment. re.escape("\\n"): "\n", #Newlines. They break off the comment.
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors. re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
} }
"""Dictionary that defines how characters are escaped when embedded in
g-code.
Note that the keys of this dictionary are regex strings. The values are
not.
"""
## Initialises the g-code reader as a profile reader.
def __init__(self): def __init__(self):
"""Initialises the g-code reader as a profile reader."""
super().__init__() super().__init__()
## Reads a g-code file, loading the profile from it.
#
# \param file_name The name of the file to read the profile from.
# \return The profile that was in the specified file, if any. If the
# specified file was no g-code or contained no parsable profile, \code
# None \endcode is returned.
def read(self, file_name): def read(self, file_name):
"""Reads a g-code file, loading the profile from it.
:param file_name: The name of the file to read the profile from.
:return: The profile that was in the specified file, if any. If the
specified file was no g-code or contained no parsable profile,
None is returned.
"""
if file_name.split(".")[-1] != "gcode": if file_name.split(".")[-1] != "gcode":
return None return None
@ -94,22 +102,28 @@ class GCodeProfileReader(ProfileReader):
profiles.append(readQualityProfileFromString(profile_string)) profiles.append(readQualityProfileFromString(profile_string))
return profiles return profiles
## Unescape a string which has been escaped for use in a gcode comment.
# def unescapeGcodeComment(string: str) -> str:
# \param string The string to unescape. """Unescape a string which has been escaped for use in a gcode comment.
# \return \type{str} The unscaped string.
def unescapeGcodeComment(string): :param string: The string to unescape.
:return: The unescaped string.
"""
# Un-escape the serialized profile. # Un-escape the serialized profile.
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys())) pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
# Perform the replacement with a regular expression. # Perform the replacement with a regular expression.
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string) return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
## Read in a profile from a serialized string.
# def readQualityProfileFromString(profile_string) -> InstanceContainer:
# \param profile_string The profile data in serialized form. """Read in a profile from a serialized string.
# \return \type{Profile} the resulting Profile object or None if it could not be read.
def readQualityProfileFromString(profile_string): :param profile_string: The profile data in serialized form.
:return: The resulting Profile object or None if it could not be read.
"""
# Create an empty profile - the id and name will be changed by the ContainerRegistry # Create an empty profile - the id and name will be changed by the ContainerRegistry
profile = InstanceContainer("") profile = InstanceContainer("")
try: try:

View File

@ -28,9 +28,8 @@ PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optiona
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])]) Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
## This parser is intended to interpret the common firmware codes among all the
# different flavors
class FlavorParser: class FlavorParser:
"""This parser is intended to interpret the common firmware codes among all the different flavors"""
def __init__(self) -> None: def __init__(self) -> None:
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage) CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
@ -212,8 +211,9 @@ class FlavorParser:
# G0 and G1 should be handled exactly the same. # G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0 _gCode1 = _gCode0
## Home the head.
def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Home the head."""
return self._position( return self._position(
params.x if params.x is not None else position.x, params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y, params.y if params.y is not None else position.y,
@ -221,21 +221,26 @@ class FlavorParser:
position.f, position.f,
position.e) position.e)
## Set the absolute positioning
def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Set the absolute positioning"""
self._is_absolute_positioning = True self._is_absolute_positioning = True
self._is_absolute_extrusion = True self._is_absolute_extrusion = True
return position return position
## Set the relative positioning
def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Set the relative positioning"""
self._is_absolute_positioning = False self._is_absolute_positioning = False
self._is_absolute_extrusion = False self._is_absolute_extrusion = False
return position return position
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
"""Reset the current position to the values specified.
For example: G92 X10 will set the X to 10 without any physical motion.
"""
if params.e is not None: if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
@ -291,8 +296,9 @@ class FlavorParser:
_type_keyword = ";TYPE:" _type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:" _layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self) -> Dict[int, List[float]]: def _extruderOffsets(self) -> Dict[int, List[float]]:
"""For showing correct x, y offsets for each extruder"""
result = {} result = {}
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [ result[int(extruder.getMetaData().get("position", "0"))] = [

View File

@ -3,8 +3,10 @@
from . import FlavorParser from . import FlavorParser
## This parser is intended to interpret the RepRap Firmware g-code flavor.
class RepRapFlavorParser(FlavorParser.FlavorParser): class RepRapFlavorParser(FlavorParser.FlavorParser):
"""This parser is intended to interpret the RepRap Firmware g-code flavor."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
# Set relative extrusion mode # Set relative extrusion mode
self._is_absolute_extrusion = False self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z to absolute
# For absolute E, M82 is used
def _gCode90(self, position, params, path): def _gCode90(self, position, params, path):
"""Set the absolute positioning
RepRapFlavor code G90 sets position of X, Y, Z to absolute
For absolute E, M82 is used
"""
self._is_absolute_positioning = True self._is_absolute_positioning = True
return position return position
## Set the relative positioning
# RepRapFlavor code G91 sets position of X, Y, Z to relative
# For relative E, M83 is used
def _gCode91(self, position, params, path): def _gCode91(self, position, params, path):
"""Set the relative positioning
RepRapFlavor code G91 sets position of X, Y, Z to relative
For relative E, M83 is used
"""
self._is_absolute_positioning = False self._is_absolute_positioning = False
return position return position

View File

@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Writes g-code to a file.
#
# While this poses as a mesh writer, what this really does is take the g-code
# in the entire scene and write it to an output device. Since the g-code of a
# single mesh isn't separable from the rest what with rafts and travel moves
# and all, it doesn't make sense to write just a single mesh.
#
# So this plug-in takes the g-code that is stored in the root of the scene
# node tree, adds a bit of extra information about the profiles and writes
# that to the output device.
class GCodeWriter(MeshWriter):
## The file format version of the serialised g-code.
#
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 3
## Dictionary that defines how characters are escaped when embedded in class GCodeWriter(MeshWriter):
# g-code. """Writes g-code to a file.
#
# Note that the keys of this dictionary are regex strings. The values are While this poses as a mesh writer, what this really does is take the g-code
# not. in the entire scene and write it to an output device. Since the g-code of a
single mesh isn't separable from the rest what with rafts and travel moves
and all, it doesn't make sense to write just a single mesh.
So this plug-in takes the g-code that is stored in the root of the scene
node tree, adds a bit of extra information about the profiles and writes
that to the output device.
"""
version = 3
"""The file format version of the serialised g-code.
It can only read settings with the same version as the version it was
written with. If the file format is changed in a way that breaks reverse
compatibility, increment this version number!
"""
escape_characters = { escape_characters = {
re.escape("\\"): "\\\\", # The escape character. re.escape("\\"): "\\\\", # The escape character.
re.escape("\n"): "\\n", # Newlines. They break off the comment. re.escape("\n"): "\\n", # Newlines. They break off the comment.
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors. re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
} }
"""Dictionary that defines how characters are escaped when embedded in
g-code.
Note that the keys of this dictionary are regex strings. The values are
not.
"""
_setting_keyword = ";SETTING_" _setting_keyword = ";SETTING_"
@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter):
self._application = Application.getInstance() self._application = Application.getInstance()
## Writes the g-code for the entire scene to a stream.
#
# Note that even though the function accepts a collection of nodes, the
# entire scene is always written to the file since it is not possible to
# separate the g-code for just specific nodes.
#
# \param stream The stream to write the g-code to.
# \param nodes This is ignored.
# \param mode Additional information on how to format the g-code in the
# file. This must always be text mode.
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
"""Writes the g-code for the entire scene to a stream.
Note that even though the function accepts a collection of nodes, the
entire scene is always written to the file since it is not possible to
separate the g-code for just specific nodes.
:param stream: The stream to write the g-code to.
:param nodes: This is ignored.
:param mode: Additional information on how to format the g-code in the
file. This must always be text mode.
"""
if mode != MeshWriter.OutputMode.TextMode: if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCodeWriter does not support non-text mode.") Logger.log("e", "GCodeWriter does not support non-text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode.")) self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
@ -88,8 +96,9 @@ class GCodeWriter(MeshWriter):
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False return False
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2): def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
"""Create a new container with container 2 as base and container 1 written over it."""
flat_container = InstanceContainer(instance_container2.getName()) flat_container = InstanceContainer(instance_container2.getName())
# The metadata includes id, name and definition # The metadata includes id, name and definition
@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter):
return flat_container return flat_container
## Serialises a container stack to prepare it for writing at the end of the
# g-code.
#
# The settings are serialised, and special characters (including newline)
# are escaped.
#
# \param settings A container stack to serialise.
# \return A serialised string of the settings.
def _serialiseSettings(self, stack): def _serialiseSettings(self, stack):
"""Serialises a container stack to prepare it for writing at the end of the g-code.
The settings are serialised, and special characters (including newline)
are escaped.
:param stack: A container stack to serialise.
:return: A serialised string of the settings.
"""
container_registry = self._application.getContainerRegistry() container_registry = self._application.getContainerRegistry()
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line. prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.

View File

@ -16,58 +16,67 @@ from UM.Settings.InstanceContainer import InstanceContainer # The new profile t
from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement. from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement.
## A plugin that reads profile data from legacy Cura versions.
#
# It reads a profile from an .ini file, and performs some translations on it.
# Not all translations are correct, mind you, but it is a best effort.
class LegacyProfileReader(ProfileReader): class LegacyProfileReader(ProfileReader):
## Initialises the legacy profile reader. """A plugin that reads profile data from legacy Cura versions.
#
# This does nothing since the only other function is basically stateless. It reads a profile from an .ini file, and performs some translations on it.
Not all translations are correct, mind you, but it is a best effort.
"""
def __init__(self): def __init__(self):
"""Initialises the legacy profile reader.
This does nothing since the only other function is basically stateless.
"""
super().__init__() super().__init__()
## Prepares the default values of all legacy settings.
#
# These are loaded from the Dictionary of Doom.
#
# \param json The JSON file to load the default setting values from. This
# should not be a URL but a pre-loaded JSON handle.
# \return A dictionary of the default values of the legacy Cura version.
def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]:
"""Prepares the default values of all legacy settings.
These are loaded from the Dictionary of Doom.
:param json: The JSON file to load the default setting values from. This
should not be a URL but a pre-loaded JSON handle.
:return: A dictionary of the default values of the legacy Cura version.
"""
defaults = {} defaults = {}
if "defaults" in json: if "defaults" in json:
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
defaults[key] = json["defaults"][key] defaults[key] = json["defaults"][key]
return defaults return defaults
## Prepares the local variables that can be used in evaluation of computing
# new setting values from the old ones.
#
# This fills a dictionary with all settings from the legacy Cura version
# and their values, so that they can be used in evaluating the new setting
# values as Python code.
#
# \param config_parser The ConfigParser that finds the settings in the
# legacy profile.
# \param config_section The section in the profile where the settings
# should be found.
# \param defaults The default values for all settings in the legacy Cura.
# \return A set of local variables, one for each setting in the legacy
# profile.
def prepareLocals(self, config_parser, config_section, defaults): def prepareLocals(self, config_parser, config_section, defaults):
"""Prepares the local variables that can be used in evaluation of computing
new setting values from the old ones.
This fills a dictionary with all settings from the legacy Cura version
and their values, so that they can be used in evaluating the new setting
values as Python code.
:param config_parser: The ConfigParser that finds the settings in the
legacy profile.
:param config_section: The section in the profile where the settings
should be found.
:param defaults: The default values for all settings in the legacy Cura.
:return: A set of local variables, one for each setting in the legacy
profile.
"""
copied_locals = defaults.copy() # Don't edit the original! copied_locals = defaults.copy() # Don't edit the original!
for option in config_parser.options(config_section): for option in config_parser.options(config_section):
copied_locals[option] = config_parser.get(config_section, option) copied_locals[option] = config_parser.get(config_section, option)
return copied_locals return copied_locals
## Reads a legacy Cura profile from a file and returns it.
#
# \param file_name The file to read the legacy Cura profile from.
# \return The legacy Cura profile that was in the file, if any. If the
# file could not be read or didn't contain a valid profile, \code None
# \endcode is returned.
def read(self, file_name): def read(self, file_name):
"""Reads a legacy Cura profile from a file and returns it.
:param file_name: The file to read the legacy Cura profile from.
:return: The legacy Cura profile that was in the file, if any. If the
file could not be read or didn't contain a valid profile, None is returned.
"""
if file_name.split(".")[-1] != "ini": if file_name.split(".")[-1] != "ini":
return None return None
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()

View File

@ -13,7 +13,7 @@ import UM.PluginRegistry # To mock the plug-in registry out.
import UM.Settings.ContainerRegistry # To mock the container registry out. import UM.Settings.ContainerRegistry # To mock the container registry out.
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
@pytest.fixture @pytest.fixture
@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [
) )
] ]
## Test cases where a key error is expected.
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData) @pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults): def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
"""Test cases where a key error is expected."""
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
parser.read_dict(parser_data) parser.read_dict(parser_data)

View File

@ -23,9 +23,11 @@ if TYPE_CHECKING:
catalog = UM.i18n.i18nCatalog("cura") catalog = UM.i18n.i18nCatalog("cura")
## This action allows for certain settings that are "machine only") to be modified.
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
class MachineSettingsAction(MachineAction): class MachineSettingsAction(MachineAction):
"""This action allows for certain settings that are "machine only") to be modified.
It automatically detects machine definitions that it knows how to change and attaches itself to those.
"""
def __init__(self, parent: Optional["QObject"] = None) -> None: def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings")) super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml" self._qml_url = "MachineSettingsAction.qml"
@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction):
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine": if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey()) self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
## Triggered when the global container stack changes or when the g-code
# flavour setting is changed.
def _updateHasMaterialsInContainerTree(self) -> None: def _updateHasMaterialsInContainerTree(self) -> None:
"""Triggered when the global container stack changes or when the g-code
flavour setting is changed.
"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return return

View File

@ -18,8 +18,8 @@ catalog = i18nCatalog("cura")
class ModelChecker(QObject, Extension): class ModelChecker(QObject, Extension):
## Signal that gets emitted when anything changed that we need to check.
onChanged = pyqtSignal() onChanged = pyqtSignal()
"""Signal that gets emitted when anything changed that we need to check."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension):
if not isinstance(args[0], Camera): if not isinstance(args[0], Camera):
self._change_timer.start() self._change_timer.start()
## Called when plug-ins are initialized.
#
# This makes sure that we listen to changes of the material and that the
# button is created that indicates warnings with the current set-up.
def _pluginsInitialized(self): def _pluginsInitialized(self):
"""Called when plug-ins are initialized.
This makes sure that we listen to changes of the material and that the
button is created that indicates warnings with the current set-up.
"""
Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged) Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged)
self._createView() self._createView()
@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension):
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
yield node yield node
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self): def _createView(self):
"""Creates the view used by show popup.
The view is saved because of the fairly aggressive garbage collection.
"""
Logger.log("d", "Creating model checker view.") Logger.log("d", "Creating model checker view.")
# Create the plugin dialog component # Create the plugin dialog component

View File

@ -1,72 +1,72 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os.path import os.path
from UM.Application import Application from UM.Application import Application
from cura.Stages.CuraStage import CuraStage from cura.Stages.CuraStage import CuraStage
## Stage for monitoring a 3D printing while it's printing. class MonitorStage(CuraStage):
class MonitorStage(CuraStage): """Stage for monitoring a 3D printing while it's printing."""
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail # Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None self._printer_output_device = None
self._active_print_job = None self._active_print_job = None
self._active_printer = None self._active_printer = None
def _setActivePrintJob(self, print_job): def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job: if self._active_print_job != print_job:
self._active_print_job = print_job self._active_print_job = print_job
def _setActivePrinter(self, printer): def _setActivePrinter(self, printer):
if self._active_printer != printer: if self._active_printer != printer:
if self._active_printer: if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged) self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer self._active_printer = printer
if self._active_printer: if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob) self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes. # Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged) self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else: else:
self._setActivePrintJob(None) self._setActivePrintJob(None)
def _onActivePrintJobChanged(self): def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob) self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self): def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter) self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self): def _onOutputDevicesChanged(self):
try: try:
# We assume that you are monitoring the device with the highest priority. # We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device: if new_output_device != self._printer_output_device:
if self._printer_output_device: if self._printer_output_device:
try: try:
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
except TypeError: except TypeError:
# Ignore stupid "Not connected" errors. # Ignore stupid "Not connected" errors.
pass pass
self._printer_output_device = new_output_device self._printer_output_device = new_output_device
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._setActivePrinter(self._printer_output_device.activePrinter) self._setActivePrinter(self._printer_output_device.activePrinter)
except IndexError: except IndexError:
pass pass
def _onEngineCreated(self): def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early) # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged() self._onOutputDevicesChanged()
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId()) plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
if plugin_path is not None: if plugin_path is not None:
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml") menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
main_component_path = os.path.join(plugin_path, "MonitorMain.qml") main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
self.addDisplayComponent("menu", menu_component_path) self.addDisplayComponent("menu", menu_component_path)
self.addDisplayComponent("main", main_component_path) self.addDisplayComponent("main", main_component_path)

View File

@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## The per object setting visibility handler ensures that only setting
# definitions that have a matching instance Container are returned as visible.
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler): class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
"""The per object setting visibility handler ensures that only setting
definitions that have a matching instance Container are returned as visible.
"""
def __init__(self, parent = None, *args, **kwargs): def __init__(self, parent = None, *args, **kwargs):
super().__init__(parent = parent, *args, **kwargs) super().__init__(parent = parent, *args, **kwargs)

View File

@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance
from UM.Event import Event from UM.Event import Event
## This tool allows the user to add & change settings per node in the scene.
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
class PerObjectSettingsTool(Tool): class PerObjectSettingsTool(Tool):
"""This tool allows the user to add & change settings per node in the scene.
The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._model = None self._model = None
@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool):
except AttributeError: except AttributeError:
return "" return ""
## Gets the active extruder of the currently selected object.
#
# \return The active extruder of the currently selected object.
def getSelectedActiveExtruder(self): def getSelectedActiveExtruder(self):
"""Gets the active extruder of the currently selected object.
:return: The active extruder of the currently selected object.
"""
selected_object = Selection.getSelectedObject(0) selected_object = Selection.getSelectedObject(0)
return selected_object.callDecoration("getActiveExtruder") return selected_object.callDecoration("getActiveExtruder")
## Changes the active extruder of the currently selected object.
#
# \param extruder_stack_id The ID of the extruder to print the currently
# selected object with.
def setSelectedActiveExtruder(self, extruder_stack_id): def setSelectedActiveExtruder(self, extruder_stack_id):
"""Changes the active extruder of the currently selected object.
:param extruder_stack_id: The ID of the extruder to print the currently
selected object with.
"""
selected_object = Selection.getSelectedObject(0) selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack: if not stack:
selected_object.addDecorator(SettingOverrideDecorator()) selected_object.addDecorator(SettingOverrideDecorator())
selected_object.callDecoration("setActiveExtruder", extruder_stack_id) selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
def setMeshType(self, mesh_type: str) -> bool: def setMeshType(self, mesh_type: str) -> bool:
"""Returns True when the mesh_type was changed, False when current mesh_type == mesh_type"""
old_mesh_type = self.getMeshType() old_mesh_type = self.getMeshType()
if old_mesh_type == mesh_type: if old_mesh_type == mesh_type:
return False return False

View File

@ -27,9 +27,8 @@ if TYPE_CHECKING:
from .Script import Script from .Script import Script
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files.
class PostProcessingPlugin(QObject, Extension): class PostProcessingPlugin(QObject, Extension):
"""Extension type plugin that enables pre-written scripts to post process g-code files."""
def __init__(self, parent = None) -> None: def __init__(self, parent = None) -> None:
QObject.__init__(self, parent) QObject.__init__(self, parent)
Extension.__init__(self) Extension.__init__(self)
@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension):
except IndexError: except IndexError:
return "" return ""
## Execute all post-processing scripts on the gcode.
def execute(self, output_device) -> None: def execute(self, output_device) -> None:
"""Execute all post-processing scripts on the gcode."""
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
# If the scene does not have a gcode, do nothing # If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"): if not hasattr(scene, "gcode_dict"):
@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension):
self.selectedIndexChanged.emit() #Ensure that settings are updated self.selectedIndexChanged.emit() #Ensure that settings are updated
self._propertyChanged() self._propertyChanged()
## Remove a script from the active script list by index.
@pyqtSlot(int) @pyqtSlot(int)
def removeScriptByIndex(self, index: int) -> None: def removeScriptByIndex(self, index: int) -> None:
"""Remove a script from the active script list by index."""
self._script_list.pop(index) self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index: if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1 self._selected_script_index = len(self._script_list) - 1
@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension):
self.selectedIndexChanged.emit() # Ensure that settings are updated self.selectedIndexChanged.emit() # Ensure that settings are updated
self._propertyChanged() self._propertyChanged()
## Load all scripts from all paths where scripts can be found.
#
# This should probably only be done on init.
def loadAllScripts(self) -> None: def loadAllScripts(self) -> None:
"""Load all scripts from all paths where scripts can be found.
This should probably only be done on init.
"""
if self._loaded_scripts: # Already loaded. if self._loaded_scripts: # Already loaded.
return return
@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension):
self.loadScripts(path) self.loadScripts(path)
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadScripts(self, path: str) -> None: def loadScripts(self, path: str) -> None:
"""Load all scripts from provided path.
This should probably only be done on init.
:param path: Path to check for scripts.
"""
if ApplicationMetadata.IsEnterpriseVersion: if ApplicationMetadata.IsEnterpriseVersion:
# Delete all __pycache__ not in installation folder, as it may present a security risk. # Delete all __pycache__ not in installation folder, as it may present a security risk.
@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension):
if not is_in_installation_path: if not is_in_installation_path:
TrustBasics.removeCached(path) TrustBasics.removeCached(path)
## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path]) scripts = pkgutil.iter_modules(path = [path])
"""Load all scripts in the scripts folders"""
for loader, script_name, ispkg in scripts: for loader, script_name, ispkg in scripts:
# Iterate over all scripts. # Iterate over all scripts.
if script_name not in sys.modules: if script_name not in sys.modules:
@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension):
self.scriptListChanged.emit() self.scriptListChanged.emit()
self._propertyChanged() self._propertyChanged()
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self) -> None: def _onGlobalContainerStackChanged(self) -> None:
"""When the global container stack is changed, swap out the list of active scripts."""
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata) self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension):
# We do want to listen to other events. # We do want to listen to other events.
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata) self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self) -> None: def _createView(self) -> None:
"""Creates the view used by show popup.
The view is saved because of the fairly aggressive garbage collection.
"""
Logger.log("d", "Creating post processing plugin view.") Logger.log("d", "Creating post processing plugin view.")
self.loadAllScripts() self.loadAllScripts()
@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension):
# Create the save button component # Create the save button component
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin.
def showPopup(self) -> None: def showPopup(self) -> None:
"""Show the (GUI) popup of the post processing plugin."""
if self._view is None: if self._view is None:
self._createView() self._createView()
if self._view is None: if self._view is None:
@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension):
return return
self._view.show() self._view.show()
## Property changed: trigger re-slice
# To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
def _propertyChanged(self) -> None: def _propertyChanged(self) -> None:
"""Property changed: trigger re-slice
To do this we use the global container stack propertyChanged.
Re-slicing is necessary for setting changes in this plugin, because the changes
are applied only once per "fresh" gcode
"""
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is not None: if global_container_stack is not None:
global_container_stack.propertyChanged.emit("post_processing_plugin", "value") global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View File

@ -23,9 +23,10 @@ if TYPE_CHECKING:
from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.Interfaces import DefinitionContainerInterface
## Base class for scripts. All scripts should inherit the script class.
@signalemitter @signalemitter
class Script: class Script:
"""Base class for scripts. All scripts should inherit the script class."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._stack = None # type: Optional[ContainerStack] self._stack = None # type: Optional[ContainerStack]
@ -78,13 +79,15 @@ class Script:
if global_container_stack is not None: if global_container_stack is not None:
global_container_stack.propertyChanged.emit(key, property_name) global_container_stack.propertyChanged.emit(key, property_name)
## Needs to return a dict that can be used to construct a settingcategory file.
# See the example script for an example.
# It follows the same style / guides as the Uranium settings.
# Scripts can either override getSettingData directly, or use getSettingDataString
# to return a string that will be parsed as json. The latter has the benefit over
# returning a dict in that the order of settings is maintained.
def getSettingData(self) -> Dict[str, Any]: def getSettingData(self) -> Dict[str, Any]:
"""Needs to return a dict that can be used to construct a settingcategory file.
See the example script for an example.
It follows the same style / guides as the Uranium settings.
Scripts can either override getSettingData directly, or use getSettingDataString
to return a string that will be parsed as json. The latter has the benefit over
returning a dict in that the order of settings is maintained.
"""
setting_data_as_string = self.getSettingDataString() setting_data_as_string = self.getSettingDataString()
setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
return setting_data return setting_data
@ -104,15 +107,18 @@ class Script:
return self._stack.getId() return self._stack.getId()
return None return None
## Convenience function that retrieves value of a setting from the stack.
def getSettingValueByKey(self, key: str) -> Any: def getSettingValueByKey(self, key: str) -> Any:
"""Convenience function that retrieves value of a setting from the stack."""
if self._stack is not None: if self._stack is not None:
return self._stack.getProperty(key, "value") return self._stack.getProperty(key, "value")
return None return None
## Convenience function that finds the value in a line of g-code.
# When requesting key = x from line "G1 X100" the value 100 is returned.
def getValue(self, line: str, key: str, default = None) -> Any: def getValue(self, line: str, key: str, default = None) -> Any:
"""Convenience function that finds the value in a line of g-code.
When requesting key = x from line "G1 X100" the value 100 is returned.
"""
if not key in line or (';' in line and line.find(key) > line.find(';')): if not key in line or (';' in line and line.find(key) > line.find(';')):
return default return default
sub_part = line[line.find(key) + 1:] sub_part = line[line.find(key) + 1:]
@ -127,20 +133,23 @@ class Script:
except ValueError: #Not a number at all. except ValueError: #Not a number at all.
return default return default
## Convenience function to produce a line of g-code.
#
# You can put in an original g-code line and it'll re-use all the values
# in that line.
# All other keyword parameters are put in the result in g-code's format.
# For instance, if you put ``G=1`` in the parameters, it will output
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
# ``G1 X100``. The parameters G and M will always be put first. The
# parameters T and S will be put second (or first if there is no G or M).
# The rest of the parameters will be put in arbitrary order.
# \param line The original g-code line that must be modified. If not
# provided, an entirely new g-code line will be produced.
# \return A line of g-code with the desired parameters filled in.
def putValue(self, line: str = "", **kwargs) -> str: def putValue(self, line: str = "", **kwargs) -> str:
"""Convenience function to produce a line of g-code.
You can put in an original g-code line and it'll re-use all the values
in that line.
All other keyword parameters are put in the result in g-code's format.
For instance, if you put ``G=1`` in the parameters, it will output
``G1``. If you put ``G=1, X=100`` in the parameters, it will output
``G1 X100``. The parameters G and M will always be put first. The
parameters T and S will be put second (or first if there is no G or M).
The rest of the parameters will be put in arbitrary order.
:param line: The original g-code line that must be modified. If not
provided, an entirely new g-code line will be produced.
:return: A line of g-code with the desired parameters filled in.
"""
#Strip the comment. #Strip the comment.
comment = "" comment = ""
if ";" in line: if ";" in line:
@ -179,7 +188,9 @@ class Script:
return result return result
## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data: List[str]) -> List[str]: def execute(self, data: List[str]) -> List[str]:
"""This is called when the script is executed.
It gets a list of g-code strings and needs to return a (modified) list.
"""
raise NotImplementedError() raise NotImplementedError()

View File

@ -63,10 +63,12 @@ class FilamentChange(Script):
} }
}""" }"""
## Inserts the filament change g-code at specific layer numbers.
# \param data A list of layers of g-code.
# \return A similar list, with filament change commands inserted.
def execute(self, data: List[str]): def execute(self, data: List[str]):
"""Inserts the filament change g-code at specific layer numbers.
:param data: A list of layers of g-code.
:return: A similar list, with filament change commands inserted.
"""
layer_nums = self.getSettingValueByKey("layer_number") layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract") initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract") later_retract = self.getSettingValueByKey("later_retract")

View File

@ -134,9 +134,8 @@ class PauseAtHeight(Script):
} }
}""" }"""
## Get the X and Y values for a layer (will be used to get X and Y of the
# layer after the pause).
def getNextXY(self, layer: str) -> Tuple[float, float]: def getNextXY(self, layer: str) -> Tuple[float, float]:
"""Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause)."""
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
@ -145,10 +144,12 @@ class PauseAtHeight(Script):
return x, y return x, y
return 0, 0 return 0, 0
## Inserts the pause commands.
# \param data: List of layers.
# \return New list of layers.
def execute(self, data: List[str]) -> List[str]: def execute(self, data: List[str]) -> List[str]:
"""Inserts the pause commands.
:param data: List of layers.
:return: New list of layers.
"""
pause_at = self.getSettingValueByKey("pause_at") pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height") pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer") pause_layer = self.getSettingValueByKey("pause_layer")

View File

@ -5,8 +5,10 @@ import math
from ..Script import Script from ..Script import Script
## Continues retracting during all travel moves.
class RetractContinue(Script): class RetractContinue(Script):
"""Continues retracting during all travel moves."""
def getSettingDataString(self): def getSettingDataString(self):
return """{ return """{
"name": "Retract Continue", "name": "Retract Continue",

View File

@ -5,11 +5,14 @@ import re #To perform the search and replace.
from ..Script import Script from ..Script import Script
## Performs a search-and-replace on all g-code.
#
# Due to technical limitations, the search can't cross the border between
# layers.
class SearchAndReplace(Script): class SearchAndReplace(Script):
"""Performs a search-and-replace on all g-code.
Due to technical limitations, the search can't cross the border between
layers.
"""
def getSettingDataString(self): def getSettingDataString(self):
return """{ return """{
"name": "Search and Replace", "name": "Search and Replace",

View File

@ -1,19 +1,21 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os.path import os.path
from UM.Application import Application from UM.Application import Application
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.Stages.CuraStage import CuraStage from cura.Stages.CuraStage import CuraStage
## Stage for preparing model (slicing).
class PrepareStage(CuraStage): class PrepareStage(CuraStage):
def __init__(self, parent = None): """Stage for preparing model (slicing)."""
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated) def __init__(self, parent = None):
super().__init__(parent)
def _engineCreated(self): Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml") def _engineCreated(self):
self.addDisplayComponent("menu", menu_component_path) menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml")
self.addDisplayComponent("menu", menu_component_path)
self.addDisplayComponent("main", main_component_path) self.addDisplayComponent("main", main_component_path)

View File

@ -12,37 +12,45 @@ if TYPE_CHECKING:
from UM.View.View import View from UM.View.View import View
## Displays a preview of what you're about to print.
#
# The Python component of this stage just loads PreviewMain.qml for display
# when the stage is selected, and makes sure that it reverts to the previous
# view when the previous stage is activated.
class PreviewStage(CuraStage): class PreviewStage(CuraStage):
"""Displays a preview of what you're about to print.
The Python component of this stage just loads PreviewMain.qml for display
when the stage is selected, and makes sure that it reverts to the previous
view when the previous stage is activated.
"""
def __init__(self, application: QtApplication, parent = None) -> None: def __init__(self, application: QtApplication, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = application self._application = application
self._application.engineCreatedSignal.connect(self._engineCreated) self._application.engineCreatedSignal.connect(self._engineCreated)
self._previously_active_view = None # type: Optional[View] self._previously_active_view = None # type: Optional[View]
## When selecting the stage, remember which was the previous view so that
# we can revert to that view when we go out of the stage later.
def onStageSelected(self) -> None: def onStageSelected(self) -> None:
"""When selecting the stage, remember which was the previous view so that
we can revert to that view when we go out of the stage later.
"""
self._previously_active_view = self._application.getController().getActiveView() self._previously_active_view = self._application.getController().getActiveView()
## Called when going to a different stage (away from the Preview Stage).
#
# When going to a different stage, the view should be reverted to what it
# was before. Normally, that just reverts it to solid view.
def onStageDeselected(self) -> None: def onStageDeselected(self) -> None:
"""Called when going to a different stage (away from the Preview Stage).
When going to a different stage, the view should be reverted to what it
was before. Normally, that just reverts it to solid view.
"""
if self._previously_active_view is not None: if self._previously_active_view is not None:
self._application.getController().setActiveView(self._previously_active_view.getPluginId()) self._application.getController().setActiveView(self._previously_active_view.getPluginId())
self._previously_active_view = None self._previously_active_view = None
## Delayed load of the QML files.
#
# We need to make sure that the QML engine is running before we can load
# these.
def _engineCreated(self) -> None: def _engineCreated(self) -> None:
"""Delayed load of the QML files.
We need to make sure that the QML engine is running before we can load
these.
"""
plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId()) plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId())
if plugin_path is not None: if plugin_path is not None:
menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml") menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")

View File

@ -10,12 +10,14 @@ import glob
import os import os
import subprocess import subprocess
## Support for removable devices on Linux.
#
# TODO: This code uses the most basic interfaces for handling this.
# We should instead use UDisks2 to handle mount/unmount and hotplugging events.
#
class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Support for removable devices on Linux.
TODO: This code uses the most basic interfaces for handling this.
We should instead use UDisks2 to handle mount/unmount and hotplugging events.
"""
def checkRemovableDrives(self): def checkRemovableDrives(self):
drives = {} drives = {}
for volume in glob.glob("/media/*"): for volume in glob.glob("/media/*"):

View File

@ -9,8 +9,10 @@ import os
import plistlib import plistlib
## Support for removable devices on Mac OSX
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Support for removable devices on Mac OSX"""
def checkRemovableDrives(self): def checkRemovableDrives(self):
drives = {} drives = {}
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)

View File

@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice):
self._writing = False self._writing = False
self._stream = None self._stream = None
## Request the specified nodes to be written to the removable drive.
#
# \param nodes A collection of scene nodes that should be written to the
# removable drive.
# \param file_name \type{string} A suggestion for the file name to write
# to. If none is provided, a file name will be made from the names of the
# meshes.
# \param limit_mimetypes Should we limit the available MIME types to the
# MIME types available to the currently active machine?
#
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
"""Request the specified nodes to be written to the removable drive.
:param nodes: A collection of scene nodes that should be written to the
removable drive.
:param file_name: :type{string} A suggestion for the file name to write to.
If none is provided, a file name will be made from the names of the
meshes.
:param limit_mimetypes: Should we limit the available MIME types to the
MIME types available to the currently active machine?
"""
filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do)
if self._writing: if self._writing:
raise OutputDeviceError.DeviceBusyError() raise OutputDeviceError.DeviceBusyError()
@ -106,14 +108,14 @@ class RemovableDriveOutputDevice(OutputDevice):
Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
## Generate a file name automatically for the specified nodes to be saved
# in.
#
# The name generated will be the name of one of the nodes. Which node that
# is can not be guaranteed.
#
# \param nodes A collection of nodes for which to generate a file name.
def _automaticFileName(self, nodes): def _automaticFileName(self, nodes):
"""Generate a file name automatically for the specified nodes to be saved in.
The name generated will be the name of one of the nodes. Which node that
is can not be guaranteed.
:param nodes: A collection of nodes for which to generate a file name.
"""
for root in nodes: for root in nodes:
for child in BreadthFirstIterator(root): for child in BreadthFirstIterator(root):
if child.getMeshData(): if child.getMeshData():

View File

@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore
## Removable drive support for windows
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
"""Removable drive support for windows"""
def checkRemovableDrives(self): def checkRemovableDrives(self):
drives = {} drives = {}