mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-24 23:29:00 +08:00
Merge remote-tracking branch 'origin/doxygen_to_restructuredtext_comments' into doxygen_to_restructuredtext_comments
This commit is contained in:
commit
a503149452
@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader
|
||||
|
||||
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):
|
||||
## Initialises the cura profile reader.
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialises the cura profile reader.
|
||||
|
||||
This does nothing since the only other function is basically stateless.
|
||||
"""
|
||||
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]]:
|
||||
"""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:
|
||||
with zipfile.ZipFile(file_name, "r") as archive:
|
||||
results = [] # type: List[Optional[InstanceContainer]]
|
||||
@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader):
|
||||
serialized_bytes = fhandle.read()
|
||||
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]]:
|
||||
"""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.read_string(serialized)
|
||||
|
||||
@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader):
|
||||
else:
|
||||
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]:
|
||||
"""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.
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.setMetaDataEntry("type", "quality_changes")
|
||||
@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader):
|
||||
profile.setMetaDataEntry("definition", active_quality_definition)
|
||||
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]]:
|
||||
"""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
|
||||
|
||||
from UM.VersionUpgradeManager import VersionUpgradeManager
|
||||
|
@ -6,15 +6,18 @@ from UM.Logger import Logger
|
||||
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||
import zipfile
|
||||
|
||||
## Writes profiles to Cura's own profile format with config files.
|
||||
class CuraProfileWriter(ProfileWriter):
|
||||
## 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 \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
"""Writes profiles to Cura's own profile format with config files."""
|
||||
|
||||
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:
|
||||
profiles = [profiles]
|
||||
|
||||
|
@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||
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):
|
||||
"""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:
|
||||
super().__init__()
|
||||
@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension):
|
||||
self._check_job = None
|
||||
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):
|
||||
"""Callback for the message that is spawned when there is a new version."""
|
||||
|
||||
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
|
||||
machine_id = message.getMachineId()
|
||||
download_url = message.getDownloadUrl()
|
||||
@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension):
|
||||
def _onJobFinished(self, *args, **kwargs):
|
||||
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):
|
||||
"""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()
|
||||
if container_name in self._checked_printer_names:
|
||||
return
|
||||
|
@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This job checks if there is an update available on the provided URL.
|
||||
class FirmwareUpdateCheckerJob(Job):
|
||||
"""This job checks if there is an update available on the provided URL."""
|
||||
|
||||
STRING_ZERO_VERSION = "0.0.0"
|
||||
STRING_EPSILON_VERSION = "0.0.1"
|
||||
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
||||
|
@ -19,8 +19,10 @@ if MYPY:
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Upgrade the firmware of a machine by USB with this action.
|
||||
|
||||
class FirmwareUpdaterMachineAction(MachineAction):
|
||||
"""Upgrade the firmware of a machine by USB with this action."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
|
||||
self._qml_url = "FirmwareUpdaterMachineAction.qml"
|
||||
|
@ -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.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):
|
||||
"""A file reader that reads gzipped g-code.
|
||||
|
||||
If you're zipping g-code, you might as well use gzip!
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
MimeTypeDatabase.addMimeType(
|
||||
|
@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from UM.i18n import i18nCatalog
|
||||
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):
|
||||
"""A file writer that writes gzipped g-code.
|
||||
|
||||
If you're zipping g-code, you might as well use gzip!
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
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:
|
||||
"""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:
|
||||
Logger.log("e", "GCodeGzWriter does not support text mode.")
|
||||
self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode."))
|
||||
|
@ -12,40 +12,48 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
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):
|
||||
## 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!
|
||||
version = 3
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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 = {
|
||||
re.escape("\\\\"): "\\", #The escape character.
|
||||
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.
|
||||
}
|
||||
"""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):
|
||||
"""Initialises the g-code reader as a profile reader."""
|
||||
|
||||
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):
|
||||
"""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":
|
||||
return None
|
||||
|
||||
@ -94,22 +102,28 @@ class GCodeProfileReader(ProfileReader):
|
||||
profiles.append(readQualityProfileFromString(profile_string))
|
||||
return profiles
|
||||
|
||||
## Unescape a string which has been escaped for use in a gcode comment.
|
||||
#
|
||||
# \param string The string to unescape.
|
||||
# \return \type{str} The unscaped string.
|
||||
def unescapeGcodeComment(string):
|
||||
|
||||
def unescapeGcodeComment(string: str) -> str:
|
||||
"""Unescape a string which has been escaped for use in a gcode comment.
|
||||
|
||||
:param string: The string to unescape.
|
||||
:return: The unescaped string.
|
||||
"""
|
||||
|
||||
# Un-escape the serialized profile.
|
||||
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
|
||||
|
||||
# Perform the replacement with a regular expression.
|
||||
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
|
||||
|
||||
## Read in a profile from a serialized string.
|
||||
#
|
||||
# \param profile_string The profile data in serialized form.
|
||||
# \return \type{Profile} the resulting Profile object or None if it could not be read.
|
||||
def readQualityProfileFromString(profile_string):
|
||||
|
||||
def readQualityProfileFromString(profile_string) -> InstanceContainer:
|
||||
"""Read in a profile from a serialized 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
|
||||
profile = InstanceContainer("")
|
||||
try:
|
||||
|
@ -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])])
|
||||
|
||||
|
||||
## This parser is intended to interpret the common firmware codes among all the
|
||||
# different flavors
|
||||
class FlavorParser:
|
||||
"""This parser is intended to interpret the common firmware codes among all the different flavors"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||
@ -212,8 +211,9 @@ class FlavorParser:
|
||||
# G0 and G1 should be handled exactly the same.
|
||||
_gCode1 = _gCode0
|
||||
|
||||
## Home the head.
|
||||
def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
"""Home the head."""
|
||||
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
@ -221,21 +221,26 @@ class FlavorParser:
|
||||
position.f,
|
||||
position.e)
|
||||
|
||||
## Set the absolute positioning
|
||||
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_extrusion = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
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_extrusion = False
|
||||
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:
|
||||
"""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:
|
||||
# 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
|
||||
@ -291,8 +296,9 @@ class FlavorParser:
|
||||
_type_keyword = ";TYPE:"
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self) -> Dict[int, List[float]]:
|
||||
"""For showing correct x, y offsets for each extruder"""
|
||||
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
from . import FlavorParser
|
||||
|
||||
## This parser is intended to interpret the RepRap Firmware g-code flavor.
|
||||
|
||||
class RepRapFlavorParser(FlavorParser.FlavorParser):
|
||||
"""This parser is intended to interpret the RepRap Firmware g-code flavor."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
|
||||
# Set relative extrusion mode
|
||||
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):
|
||||
"""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
|
||||
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):
|
||||
"""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
|
||||
return position
|
@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree
|
||||
from UM.i18n import i18nCatalog
|
||||
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
|
||||
# g-code.
|
||||
#
|
||||
# Note that the keys of this dictionary are regex strings. The values are
|
||||
# not.
|
||||
class GCodeWriter(MeshWriter):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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 = {
|
||||
re.escape("\\"): "\\\\", # The escape character.
|
||||
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.
|
||||
}
|
||||
"""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_"
|
||||
|
||||
@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter):
|
||||
|
||||
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):
|
||||
"""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:
|
||||
Logger.log("e", "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."))
|
||||
return False
|
||||
|
||||
## Create a new container with container 2 as base and container 1 written over it.
|
||||
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())
|
||||
|
||||
# The metadata includes id, name and definition
|
||||
@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter):
|
||||
|
||||
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):
|
||||
"""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()
|
||||
|
||||
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||
|
@ -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.
|
||||
|
||||
|
||||
## 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):
|
||||
## Initialises the legacy profile reader.
|
||||
#
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialises the legacy profile reader.
|
||||
|
||||
This does nothing since the only other function is basically stateless.
|
||||
"""
|
||||
|
||||
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]:
|
||||
"""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 = {}
|
||||
if "defaults" in json:
|
||||
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]
|
||||
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):
|
||||
"""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!
|
||||
for option in config_parser.options(config_section):
|
||||
copied_locals[option] = config_parser.get(config_section, option)
|
||||
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):
|
||||
"""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":
|
||||
return None
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
@ -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.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
|
||||
@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [
|
||||
)
|
||||
]
|
||||
|
||||
## Test cases where a key error is expected.
|
||||
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
|
||||
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
|
||||
"""Test cases where a key error is expected."""
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
|
@ -23,9 +23,11 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
"""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:
|
||||
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
||||
self._qml_url = "MachineSettingsAction.qml"
|
||||
@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction):
|
||||
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
||||
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:
|
||||
"""Triggered when the global container stack changes or when the g-code
|
||||
|
||||
flavour setting is changed.
|
||||
"""
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
@ -18,8 +18,8 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class ModelChecker(QObject, Extension):
|
||||
## Signal that gets emitted when anything changed that we need to check.
|
||||
onChanged = pyqtSignal()
|
||||
"""Signal that gets emitted when anything changed that we need to check."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension):
|
||||
if not isinstance(args[0], Camera):
|
||||
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):
|
||||
"""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)
|
||||
self._createView()
|
||||
|
||||
@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension):
|
||||
if node.callDecoration("isSliceable"):
|
||||
yield node
|
||||
|
||||
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||
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.")
|
||||
|
||||
# Create the plugin dialog component
|
||||
|
@ -1,72 +1,72 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
## Stage for monitoring a 3D printing while it's printing.
|
||||
class MonitorStage(CuraStage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# 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)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
class MonitorStage(CuraStage):
|
||||
"""Stage for monitoring a 3D printing while it's printing."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# 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)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
|
@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit
|
||||
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):
|
||||
"""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):
|
||||
super().__init__(parent = parent, *args, **kwargs)
|
||||
|
||||
|
@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance
|
||||
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):
|
||||
"""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):
|
||||
super().__init__()
|
||||
self._model = None
|
||||
@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool):
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
## Gets the active extruder of the currently selected object.
|
||||
#
|
||||
# \return The active extruder of the currently selected object.
|
||||
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)
|
||||
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):
|
||||
"""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)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
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:
|
||||
"""Returns True when the mesh_type was changed, False when current mesh_type == mesh_type"""
|
||||
|
||||
old_mesh_type = self.getMeshType()
|
||||
if old_mesh_type == mesh_type:
|
||||
return False
|
||||
|
@ -27,9 +27,8 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
"""Extension type plugin that enables pre-written scripts to post process g-code files."""
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
## Execute all post-processing scripts on the gcode.
|
||||
def execute(self, output_device) -> None:
|
||||
"""Execute all post-processing scripts on the gcode."""
|
||||
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
# If the scene does not have a gcode, do nothing
|
||||
if not hasattr(scene, "gcode_dict"):
|
||||
@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
self.selectedIndexChanged.emit() #Ensure that settings are updated
|
||||
self._propertyChanged()
|
||||
|
||||
## Remove a script from the active script list by index.
|
||||
@pyqtSlot(int)
|
||||
def removeScriptByIndex(self, index: int) -> None:
|
||||
"""Remove a script from the active script list by index."""
|
||||
|
||||
self._script_list.pop(index)
|
||||
if len(self._script_list) - 1 < self._selected_script_index:
|
||||
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._propertyChanged()
|
||||
|
||||
## Load all scripts from all paths where scripts can be found.
|
||||
#
|
||||
# This should probably only be done on init.
|
||||
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.
|
||||
return
|
||||
|
||||
@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
|
||||
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:
|
||||
"""Load all scripts from provided path.
|
||||
|
||||
This should probably only be done on init.
|
||||
:param path: Path to check for scripts.
|
||||
"""
|
||||
|
||||
if ApplicationMetadata.IsEnterpriseVersion:
|
||||
# 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:
|
||||
TrustBasics.removeCached(path)
|
||||
|
||||
## Load all scripts in the scripts folders
|
||||
scripts = pkgutil.iter_modules(path = [path])
|
||||
"""Load all scripts in the scripts folders"""
|
||||
for loader, script_name, ispkg in scripts:
|
||||
# Iterate over all scripts.
|
||||
if script_name not in sys.modules:
|
||||
@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
self.scriptListChanged.emit()
|
||||
self._propertyChanged()
|
||||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
"""When the global container stack is changed, swap out the list of active scripts."""
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
# We do want to listen to other events.
|
||||
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:
|
||||
"""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.")
|
||||
|
||||
self.loadAllScripts()
|
||||
@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
# Create the save button component
|
||||
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
|
||||
|
||||
## Show the (GUI) popup of the post processing plugin.
|
||||
def showPopup(self) -> None:
|
||||
"""Show the (GUI) popup of the post processing plugin."""
|
||||
|
||||
if self._view is None:
|
||||
self._createView()
|
||||
if self._view is None:
|
||||
@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
return
|
||||
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:
|
||||
"""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()
|
||||
if global_container_stack is not None:
|
||||
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
|
||||
|
@ -23,9 +23,10 @@ if TYPE_CHECKING:
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
|
||||
|
||||
## Base class for scripts. All scripts should inherit the script class.
|
||||
@signalemitter
|
||||
class Script:
|
||||
"""Base class for scripts. All scripts should inherit the script class."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._stack = None # type: Optional[ContainerStack]
|
||||
@ -78,13 +79,15 @@ class Script:
|
||||
if global_container_stack is not None:
|
||||
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]:
|
||||
"""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 = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
|
||||
return setting_data
|
||||
@ -104,15 +107,18 @@ class Script:
|
||||
return self._stack.getId()
|
||||
return None
|
||||
|
||||
## Convenience function that retrieves value of a setting from the stack.
|
||||
def getSettingValueByKey(self, key: str) -> Any:
|
||||
"""Convenience function that retrieves value of a setting from the stack."""
|
||||
|
||||
if self._stack is not None:
|
||||
return self._stack.getProperty(key, "value")
|
||||
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:
|
||||
"""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(';')):
|
||||
return default
|
||||
sub_part = line[line.find(key) + 1:]
|
||||
@ -127,20 +133,23 @@ class Script:
|
||||
except ValueError: #Not a number at all.
|
||||
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:
|
||||
"""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.
|
||||
comment = ""
|
||||
if ";" in line:
|
||||
@ -179,7 +188,9 @@ class Script:
|
||||
|
||||
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]:
|
||||
"""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()
|
||||
|
@ -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]):
|
||||
"""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")
|
||||
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||
later_retract = self.getSettingValueByKey("later_retract")
|
||||
|
@ -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]:
|
||||
"""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")
|
||||
for line in lines:
|
||||
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 0, 0
|
||||
|
||||
## Inserts the pause commands.
|
||||
# \param data: List of layers.
|
||||
# \return New list of layers.
|
||||
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_height = self.getSettingValueByKey("pause_height")
|
||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||
|
@ -5,8 +5,10 @@ import math
|
||||
|
||||
from ..Script import Script
|
||||
|
||||
## Continues retracting during all travel moves.
|
||||
|
||||
class RetractContinue(Script):
|
||||
"""Continues retracting during all travel moves."""
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Retract Continue",
|
||||
|
@ -5,11 +5,14 @@ import re #To perform the search and replace.
|
||||
|
||||
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):
|
||||
"""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):
|
||||
return """{
|
||||
"name": "Search and Replace",
|
||||
|
@ -1,19 +1,21 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
## Stage for preparing model (slicing).
|
||||
class PrepareStage(CuraStage):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
def _engineCreated(self):
|
||||
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)
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
class PrepareStage(CuraStage):
|
||||
"""Stage for preparing model (slicing)."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
def _engineCreated(self):
|
||||
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)
|
@ -12,37 +12,45 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
"""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:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
self._application.engineCreatedSignal.connect(self._engineCreated)
|
||||
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:
|
||||
"""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()
|
||||
|
||||
## 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:
|
||||
"""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:
|
||||
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
|
||||
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:
|
||||
"""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())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")
|
||||
|
@ -10,12 +10,14 @@ import glob
|
||||
import os
|
||||
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):
|
||||
"""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):
|
||||
drives = {}
|
||||
for volume in glob.glob("/media/*"):
|
||||
|
@ -9,8 +9,10 @@ import os
|
||||
|
||||
import plistlib
|
||||
|
||||
## Support for removable devices on Mac OSX
|
||||
|
||||
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
"""Support for removable devices on Mac OSX"""
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
|
||||
|
@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||
self._writing = False
|
||||
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):
|
||||
"""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)
|
||||
if self._writing:
|
||||
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))
|
||||
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):
|
||||
"""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 child in BreadthFirstIterator(root):
|
||||
if child.getMeshData():
|
||||
|
@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
|
||||
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore
|
||||
|
||||
|
||||
## Removable drive support for windows
|
||||
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
"""Removable drive support for windows"""
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user