Merge branch 'master' into feature_decline_slice_info

This commit is contained in:
alekseisasin 2017-12-21 10:24:35 +01:00 committed by GitHub
commit d7146a90f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
256 changed files with 17379 additions and 4694 deletions

36
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,36 @@
<!--
The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out.
Before filing, please check if the issue already exists (either open or closed).
It is also helpful to attach a project (.3MF) file and Cura log file so we can debug issues quicker.
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations.
Thank you for using Cura!
-->
**Application Version**
<!-- The version of the application this issue occurs with -->
**Platform**
<!-- Information about the platform the issue occurs on -->
**Qt**
<!-- The version of Qt used (not necessary if you're using the version from Ultimaker's website) -->
**PyQt**
<!-- The version of PyQt used (not necessary if you're using the version from Ultimaker's website) -->
**Display Driver**
<!-- Video driver name and version -->
**Steps to Reproduce**
<!-- Add the steps needed that lead up to the issue (replace this text) -->
**Actual Results**
<!-- What happens after the above steps have been followed (replace this text) -->
**Expected results**
<!-- What should happen after the above steps have been followed (replace this text) -->
**Additional Information**
<!-- Extra information relevant to the issue, like screenshots (replace this text) -->

23
.gitignore vendored
View file

@ -33,18 +33,23 @@ cura.desktop
.settings
#Externally located plug-ins.
plugins/CuraSolidWorksPlugin
plugins/Doodle3D-cura-plugin
plugins/GodMode
plugins/PostProcessingPlugin
plugins/X3GWriter
plugins/FlatProfileExporter
plugins/ProfileFlattener
plugins/cura-god-mode-plugin
plugins/cura-big-flame-graph
plugins/cura-god-mode-plugin
plugins/cura-siemensnx-plugin
plugins/CuraVariSlicePlugin
plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin
plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator
plugins/CuraSolidWorksPlugin
plugins/CuraVariSlicePlugin
plugins/Doodle3D-cura-plugin
plugins/FlatProfileExporter
plugins/GodMode
plugins/OctoPrintPlugin
plugins/PostProcessingPlugin
plugins/ProfileFlattener
plugins/X3GWriter
#Build stuff
CMakeCache.txt

173
CHANGES
View file

@ -1,173 +0,0 @@
Cura 15.06 Beta
===============
This is the *Beta* version of Cura 15.06.
Cura 15.06 is a new release built from the ground up on a completely new
framework called Uranium. This framework has been designed to make it easier to
extend Cura with additional functionality as well as provide a cleaner UI.
Changes since 15.05.95
----------------------
* Fixed: Selection ghost remains visible after deleting an object
* Fixed: Window does not show up immediately after starting application on OSX
* Fixed: Added display of rotation angle during rotation
* Fixed: Object changes position while rotating/scaling
* Fixed: Loading improvements in the layer view
* Fixed: Added application icons
* Fixed: Improved feedback when loading models
* Fixed: Eject device on MacOSX now provides proper feedback
* Fixed: Make it possible to show retraction settings for UM2
* Fixed: Opening the machine preferences page will switch to the first available machine
* Fixed: Improved tool handle hit area size
* Fixed: Render lines with a thickness based on screen DPI
Changes since 15.05.94
----------------------
* Added Russian translations
* Fixed: Infill not displayed in layer view
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
* Fixed: Improved font rendering on Windows
* Fixed: Help > Show Documentation crashes Cura on Windows
* Fixed: "There is no disk in the drive" repeating messages on Windows
* Fixed: Retraction settings not visible for Ultimaker2
* Fixed: Display rotation angle when rotating an object
* Fixed: Time/Quality slider values are properly rounded
* Fixed: Improved clarity of buttons and text
* Fixed: No indication that anything is happening when loading a model
* Fixed: Eject device now works on Windows
Changes since 15.05.93
----------------------
* Fixed: No shortcuts for moving up/down layers in layer view.
* Fixed: Last view layers could not be scrolled through in layer view.
* Fixed: Files provided on command line would not actually show up on the build
platform.
* Fixed: Render a ghost of the selection in Layer view to make the actual object
position clear.
* Fixed: Showing a menu would clear the selection.
* Fixed: Size and scaling factor display for scale tool.
* Fixed: Missing background for additional tool controls.
* Fixed: Loading message times out when loading large files.
* Fixed: Show recent files in the file menu.
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
prevent issues with missing DLL's.
* Fixed: Collapsed/expanded state of setting categories not stored.
Changes since 15.05.91
----------------------
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
higher.
* Fixed: Need to deselect before selecting a different object.
* Fixed: Object can be moved on Z axis.
* Fixed: Error values should be considered invalid values and will not trigger a
slice.
* Fixed: Text fields used a locale-aware validator while the underlying code did
not.
* Fixed: Text fields will trigger a slice on text change, not only after focus
change/enter press.
* Fixed: Rotate Tool snaps to incorrect value.
* Fixed: Object Collision would only moved objects to the right.
* Fixed: Object Collision would move the selected object when it should not.
* Fixed: Camera panning now works correctly instead of doing nothing.
* Fixed: Camera would flip around center point at maximum rotation.
* Fixed: Build platform grid blocked view from below objects.
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
window
Changes since 15.05.90
----------------------
* Fixed: Additional UI elements for tools and views not loading.
* Fixed: Double click needed to change setting dialog page.
* Fixed: Context menu entries (reload, center object, etc.) not working.
* Fixed: "Open With" or passing files from command line not working.
* Fixed: "Reload All" would not reload files.
In addition, a lot of work has gone into getting a usable Mac OSX version.
New Features
------------
* Plugin based system
The Uranium framework provides us with a plugin-based system
that provides additional flexibility when extending Cura. Think
of new views, tools, file formats, etc. This is probably the
biggest new feature.
* Improved UI
The UI has received a complete overhaul.
* Time-Quality Slider
The 4 static quick print profiles have been replaced with
a slider that should make it easier to find the right spot
between print time and print quality.
* More Settings
The Advanced mode is now configurable and can show many
additional settings that were previously not available, while at
the same time not overwhelming new users with too many settings.
Custom set of visible settings can be created by the user.
* Support for high-DPI screens
The refreshed UI has been designed with high-DPI screens in
mind which should improve the experience of Cura on such
devices.
* Improved language support
(Not yet available for the Beta release.)
* Improved support structure generation
The new version of the CuraEngine now features improved
support generation algorithms and additional options for support
structure generation.
* Experimental Feature: Wire Printing
Wire Printing has been added as an experimental new feature. It
will print objects as a structure of lines. It can be enabled by
from Advanced Mode -> Fixes -> Wire Printing.
* Undo/Redo
It is now possible to undo and redo most scene operations, like
moving or rotating objects.
Features from earlier versions not (yet) in this release
--------------------------------------------------------
* The All-at-once/One-at-a-time toggle is not available.
We are working on an improved implementation of this mechanism
but it will not be available for this release.
* No dual extrusion features are available yet.
We are working on a completely new workflow for this but this
needs additional time.
* “Lay Flat” has been removed.
The existing implementation was unfortunately not salvageable.
We will be looking into an improved implementation for this
feature.
* "Split Object Into Parts" has been removed.
Due to the same reason as Lay Flat.
* Support for AMF and DAE file formats has been removed.
Both of these will be implemented as plugins in the future.
* Support for directly loading a GCode file is not yet available.
This will be implemented as a plugin in the future.
* Support for PNG, JPG and other image formats has been removed.
These can be supported by a plugin with an improved UI.
* Support for loading Minecraft levels has been removed.
This can be implemented as a plugin.
* Windows XP support has been dropped.
Microsoft is no longer supporting xp, so they no longer back
port certain features that we require.
* X-Ray view is missing.
Will be implemented as a (you might have guessed it) plugin.
* Fixes: Follow Mesh Surface
Has been removed from the engine, the same result can be
achieved using no infill or top/bottom layers.
Known Issues
------------
For an up to date list of all known issues, please see
https://github.com/Ultimaker/Cura/issues and
https://github.com/Ultimaker/Uranium/issues .
* Some OBJ files are rendered as black objects due to missing
normals.
* Disabling plugins does not work correctly yet.
* Unicorn occasionally still requires feeding. Do not feed it
after midnight.

View file

@ -1,22 +1,16 @@
Cura
====
This is the new, shiny frontend for Cura. [daid/Cura](https://github.com/daid/Cura.git) is the old legacy Cura that everyone knows and loves/hates.
We re-worked the whole GUI code at Ultimaker, because the old code started to become a unmaintainable.
This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
Logging Issues
------------
Use [this](https://github.com/Ultimaker/Uranium/wiki/Bug-Reporting-Template) template to report issues. New issues that do not adhere to this template will take us a lot longer to handle and will therefore have a lower pirority.
For crashes and similar issues, please attach the following information:
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
* The Cura GUI log file, located at
* %APPDATA%\cura\\`<Cura version>`\cura.log (Windows), or usually C:\Users\\`<your username>`\AppData\Roaming\cura\\`<Cura version>`\cura.log
* $User/Library/Application Support/cura/`<Cura version>`/cura.log (OSX)
* $USER/.local/share/cura/`<Cura version>`/cura.log (Ubuntu/Linux)
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$USER/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$USER/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
@ -24,53 +18,26 @@ For additional support, you could also ask in the #cura channel on FreeNode IRC.
Dependencies
------------
* [Uranium](https://github.com/Ultimaker/Uranium)
Cura is built on top of the Uranium framework.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine)
This will be needed at runtime to perform the actual slicing.
* [PySerial](https://github.com/pyserial/pyserial)
Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf)
Only required to detect mDNS-enabled printers
Configuring Cura
----------------
Link your CuraEngine backend by inserting the following lines in `$HOME/.config/cura/config.cfg` :
```
[backend]
location = /[path_to_the..]/CuraEngine/build/CuraEngine
```
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers
Build scripts
-------------
Please checkout [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
Please checkout [cura-build](https://github.com/Ultimaker/cura-build)
Third party plugins
Plugins
-------------
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code.
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
* [OctoPrint Plugin](https://github.com/fieldofview/OctoPrintPlugin): Send printjobs directly to OctoPrint and monitor their progress in Cura.
* [Electric Print Cost Calculator Plugin](https://github.com/zoff99/ElectricPrintCostCalculator): Calculate the electric costs of a print.
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.
Making profiles for other printers
----------------------------------
If your make of printer is not in the list of supported printers, and using the "Custom FDM Printer" does not offer enough flexibility, you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original.def.json) as a template.
Supported printers
-------------
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support for new machines.
* Change the machine ID to something unique
* Change the machine_name to your printer's name
* If you have a 3D model of your platform you can put it in resources/meshes and put its name under platform
* Set your machine's dimensions with machine_width, machine_depth, and machine_height
* If your printer's origin is in the center of the bed, set machine_center_is_zero to true.
* Set your print head dimensions with the machine_head_shape parameters
* Set the start and end gcode in machine_start_gcode and machine_end_gcode
Once you are done, put the profile you have made into resources/definitions, or in definitions in your cura profile folder.
If you want to make a definition for a multi-extrusion printer, have a look at [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original_dual.def.json) as a template, along with the two extruder definitions it references [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_1st.def.json) and [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_2nd.def.json)
Configuring Cura
----------------
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
Translating Cura
----------------
@ -93,3 +60,7 @@ To submit your translation, ideally you would make two pull requests where all `
After the translation is submitted, the Cura maintainers will check for its completeness and check whether it is consistent. We will take special care to look for common mistakes, such as translating mark-up `<message>` code and such. We are often not fluent in every language, so we expect the translator and the international users to make corrections where necessary. Of course, there will always be some mistakes in every translation.
When the next Cura release comes around, some of the texts will have changed and some new texts will have been added. Around the time when the beta is released we will invoke a string freeze, meaning that no developer is allowed to make changes to the texts. Then we will update the translation template `.pot` files and ask all our translators to update their translations. If you are unable to update the translation in time for the actual release, we will remove the language from the drop-down menu in the Preferences window. The translation stays in Cura however, so that someone might pick it up again later and update it with the newest texts. Also, users who had previously selected the language can still continue Cura in their language but English text will appear among the original text.
License
----------------
Cura is released under the terms of the LGPLv3 or higher. A copy of this license should be included with the software.

View file

@ -1,6 +1,6 @@
[Desktop Entry]
Name=Cura
Name[de]=Cura
Name=Ultimaker Cura
Name[de]=Ultimaker Cura
GenericName=3D Printing Software
GenericName[de]=3D-Druck-Software
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.

View file

@ -52,6 +52,8 @@ class Arrange:
# Place all objects fixed nodes
for fixed_node in fixed_nodes:
vertices = fixed_node.callDecoration("getConvexHull")
if not vertices:
continue
points = copy.deepcopy(vertices._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr)

View file

@ -513,14 +513,13 @@ class BuildVolume(SceneNode):
update_disallowed_areas = False
update_raft_thickness = False
update_extra_z_clearance = True
for setting_key in self._changed_settings_since_last_rebuild:
if setting_key == "print_sequence":
machine_height = self._global_container_stack.getProperty("machine_height", "value")
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence",
"value") == "one_at_a_time" and len(
self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"),
machine_height)
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
if self._height < machine_height:
self._build_volume_message.show()
else:
@ -528,9 +527,20 @@ class BuildVolume(SceneNode):
else:
self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide()
update_disallowed_areas = True
rebuild_me = True
if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings:
# sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
if setting_key in self._machine_settings:
self._height = self._global_container_stack.getProperty("machine_height", "value")
self._width = self._global_container_stack.getProperty("machine_width", "value")
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
update_extra_z_clearance = True
update_disallowed_areas = True
rebuild_me = True
if setting_key in self._skirt_settings + self._prime_settings + self._tower_settings + self._ooze_shield_settings + self._distance_settings + self._extruder_settings:
update_disallowed_areas = True
rebuild_me = True
@ -969,6 +979,7 @@ class BuildVolume(SceneNode):
def _clamp(self, value, min_value, max_value):
return max(min(value, max_value), min_value)
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]

View file

@ -37,7 +37,7 @@ else:
# List of exceptions that should be considered "fatal" and abort the program.
# These are primarily some exception types that we simply cannot really recover from
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
fatal_exception_types = [
MemoryError,
SyntaxError,
@ -53,13 +53,13 @@ class CrashHandler:
self.exception_type = exception_type
self.value = value
self.traceback = tb
self.dialog = QDialog()
self.dialog = None # Don't create a QDialog before there is a QApplication
# While we create the GUI, the information will be stored for sending afterwards
self.data = dict()
self.data["time_stamp"] = time.time()
Logger.log("c", "An uncaught exception has occurred!")
Logger.log("c", "An uncaught error has occurred!")
for line in traceback.format_exception(exception_type, value, tb):
for part in line.rstrip("\n").split("\n"):
Logger.log("c", part)
@ -71,6 +71,7 @@ class CrashHandler:
if not application:
sys.exit(1)
self.dialog = QDialog()
self._createDialog()
## Creates a modal dialog.
@ -90,7 +91,7 @@ class CrashHandler:
def _messageWidget(self):
label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal exception has occurred. Please send us this Crash Report to fix the problem</p></b>
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
"""))
@ -143,7 +144,7 @@ class CrashHandler:
def _exceptionInfoWidget(self):
group = QGroupBox()
group.setTitle(catalog.i18nc("@title:groupbox", "Exception traceback"))
group.setTitle(catalog.i18nc("@title:groupbox", "Error traceback"))
layout = QVBoxLayout()
text_area = QTextEdit()
@ -288,4 +289,4 @@ class CrashHandler:
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
if self.dialog:
self.dialog.exec_()
os._exit(1)
os._exit(1)

View file

@ -1,6 +1,5 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtNetwork import QLocalServer
from PyQt5.QtNetwork import QLocalSocket
@ -32,6 +31,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray
from cura.ConvexHullDecorator import ConvexHullDecorator
@ -95,10 +95,11 @@ numpy.seterr(all="ignore")
MYPY = False
if not MYPY:
try:
from cura.CuraVersion import CuraVersion, CuraBuildType
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode
except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
CuraDebugMode = False
class CuraApplication(QtApplication):
@ -126,7 +127,8 @@ class CuraApplication(QtApplication):
# Cura will always show the Add Machine Dialog upon start.
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
def __init__(self):
def __init__(self, **kwargs):
# this list of dir names will be used by UM to detect an old cura directory
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
Resources.addExpectedDirNameInData(dir_name)
@ -155,7 +157,6 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("extruder", None, str, Validator)
SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None)
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
@ -171,16 +172,18 @@ class CuraApplication(QtApplication):
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.DefinitionChangesContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer, "variant")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer, "material")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer, "user")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack, "machine")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
## Initialise the version upgrade manager with Cura's storage paths.
import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
# Needs to be here to prevent circular dependencies.
import UM.VersionUpgradeManager
UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
{
@ -205,8 +208,12 @@ class CuraApplication(QtApplication):
self._additional_components = {} # Components to add to certain areas in the interface
super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType,
tray_icon_name = "cura-icon-32.png")
super().__init__(name = "cura",
version = CuraVersion,
buildtype = CuraBuildType,
is_debug_mode = CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
self.default_theme = "cura-light"
@ -225,7 +232,9 @@ class CuraApplication(QtApplication):
"TranslateTool",
"FileLogger",
"XmlMaterialProfile",
"PluginBrowser"
"PluginBrowser",
"PrepareStage",
"MonitorStage"
])
self._physics = None
self._volume = None
@ -261,17 +270,17 @@ class CuraApplication(QtApplication):
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
empty_variant_container = copy.deepcopy(empty_container)
empty_variant_container._id = "empty_variant"
empty_variant_container.setMetaDataEntry("id", "empty_variant")
empty_variant_container.addMetaDataEntry("type", "variant")
ContainerRegistry.getInstance().addContainer(empty_variant_container)
empty_material_container = copy.deepcopy(empty_container)
empty_material_container._id = "empty_material"
empty_material_container.setMetaDataEntry("id", "empty_material")
empty_material_container.addMetaDataEntry("type", "material")
ContainerRegistry.getInstance().addContainer(empty_material_container)
empty_quality_container = copy.deepcopy(empty_container)
empty_quality_container._id = "empty_quality"
empty_quality_container.setMetaDataEntry("id", "empty_quality")
empty_quality_container.setName("Not Supported")
empty_quality_container.addMetaDataEntry("quality_type", "not_supported")
empty_quality_container.addMetaDataEntry("type", "quality")
@ -279,12 +288,12 @@ class CuraApplication(QtApplication):
ContainerRegistry.getInstance().addContainer(empty_quality_container)
empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container._id = "empty_quality_changes"
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
with ContainerRegistry.getInstance().lockFile():
ContainerRegistry.getInstance().load()
ContainerRegistry.getInstance().loadAllMetadata()
# set the setting version for Preferences
preferences = Preferences.getInstance()
@ -307,6 +316,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/material_settings", "{}")
preferences.addPreference("view/invert_zoom", False)
preferences.addPreference("cura/sidebar_collapse", False)
self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement")
@ -386,7 +396,6 @@ class CuraApplication(QtApplication):
def needToShowUserAgreement(self):
return self._need_to_show_user_agreement
def setNeedToShowUserAgreement(self, set_value = True):
self._need_to_show_user_agreement = set_value
@ -394,7 +403,11 @@ class CuraApplication(QtApplication):
@pyqtSlot()
def closeApplication(self):
Logger.log("i", "Close application")
self._main_window.close()
main_window = self.getMainWindow()
if main_window is not None:
main_window.close()
else:
self.exit(0)
## A reusable dialogbox
#
@ -465,69 +478,10 @@ class CuraApplication(QtApplication):
if not self._started: # Do not do saving during application start
return
# Lock file for "more" atomically loading and saving to/from config dir.
with ContainerRegistry.getInstance().lockFile():
for instance in ContainerRegistry.getInstance().findInstanceContainers():
if not instance.isDirty():
continue
try:
data = instance.serialize()
except NotImplementedError:
continue
except Exception:
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
continue
mime_type = ContainerRegistry.getMimeTypeForContainer(type(instance))
file_name = urllib.parse.quote_plus(instance.getId()) + "." + mime_type.preferredSuffix
instance_type = instance.getMetaDataEntry("type")
path = None
if instance_type == "material":
path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
elif instance_type == "quality" or instance_type == "quality_changes":
path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
elif instance_type == "user":
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
elif instance_type == "variant":
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
elif instance_type == "definition_changes":
path = Resources.getStoragePath(self.ResourceTypes.DefinitionChangesContainer, file_name)
if path:
instance.setPath(path)
with SaveFile(path, "wt") as f:
f.write(data)
for stack in ContainerRegistry.getInstance().findContainerStacks():
self.saveStack(stack)
ContainerRegistry.getInstance().saveDirtyContainers()
def saveStack(self, stack):
if not stack.isDirty():
return
try:
data = stack.serialize()
except NotImplementedError:
return
except Exception:
Logger.logException("e", "An exception occurred when serializing container %s", stack.getId())
return
mime_type = ContainerRegistry.getMimeTypeForContainer(type(stack))
file_name = urllib.parse.quote_plus(stack.getId()) + "." + mime_type.preferredSuffix
path = None
if isinstance(stack, GlobalStack):
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
elif isinstance(stack, ExtruderStack):
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
else:
path = Resources.getStoragePath(Resources.ContainerStacks, file_name)
stack.setPath(path)
with SaveFile(path, "wt") as f:
f.write(data)
ContainerRegistry.getInstance().saveContainer(stack)
@pyqtSlot(str, result = QUrl)
def getDefaultPath(self, key):
@ -561,11 +515,10 @@ class CuraApplication(QtApplication):
self._plugins_loaded = True
@classmethod
def addCommandLineOptions(self, parser):
super().addCommandLineOptions(parser)
def addCommandLineOptions(self, parser, parsed_command_line = {}):
super().addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
parser.add_argument("--single-instance", action="store_true", default=False)
parser.add_argument("--headless", action = "store_true", default=False)
# Set up a local socket server which listener which coordinates single instances Curas and accepts commands.
def _setUpSingleInstanceServer(self):
@ -619,13 +572,16 @@ class CuraApplication(QtApplication):
# This should be called directly before creating an instance of CuraApplication.
# \returns \type{bool} True if the whole Cura app should continue running.
@classmethod
def preStartUp(cls):
def preStartUp(cls, parser = None, parsed_command_line = {}):
# Peek the arguments and look for the 'single-instance' flag.
parser = argparse.ArgumentParser(prog="cura") # pylint: disable=bad-whitespace
CuraApplication.addCommandLineOptions(parser)
parsed_command_line = vars(parser.parse_args())
if not parser:
parser = argparse.ArgumentParser(prog = "cura", add_help = False) # pylint: disable=bad-whitespace
CuraApplication.addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
# Important: It is important to keep this line here!
# In Uranium we allow to pass unknown arguments to the final executable or script.
parsed_command_line.update(vars(parser.parse_known_args()[0]))
if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]:
if parsed_command_line["single_instance"]:
Logger.log("i", "Checking for the presence of an ready running Cura instance.")
single_instance_socket = QLocalSocket()
Logger.log("d", "preStartUp(): full server name: " + single_instance_socket.fullServerName())
@ -657,21 +613,36 @@ class CuraApplication(QtApplication):
return False
return True
def preRun(self):
# Last check for unknown commandline arguments
parser = self.getCommandlineParser()
parser.add_argument("--help", "-h",
action='store_true',
default = False,
help = "Show this help message and exit."
)
parsed_args = vars(parser.parse_args()) # This won't allow unknown arguments
if parsed_args["help"]:
parser.print_help()
sys.exit(0)
def run(self):
self.preRun()
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
self._setUpSingleInstanceServer()
controller = self.getController()
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
controller.setCameraTool("CameraTool")
controller.setSelectionTool("SelectionTool")
t = controller.getTool("TranslateTool")
if t:
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis])
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
Selection.selectionChanged.connect(self.onSelectionChanged)
@ -712,11 +683,12 @@ class CuraApplication(QtApplication):
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
run_headless = self.getCommandLineOption("headless", False)
if not run_headless:
run_without_gui = self.getCommandLineOption("headless", False) or self.getCommandLineOption("invisible", False)
if not run_without_gui:
self.initializeEngine()
controller.setActiveStage("PrepareStage")
if run_headless or self._engine.rootObjects:
if run_without_gui or self._engine.rootObjects:
self.closeSplash()
for file_name in self.getCommandLineOption("file", []):
@ -728,7 +700,7 @@ class CuraApplication(QtApplication):
self.exec_()
def getMachineManager(self, *args):
def getMachineManager(self, *args) -> MachineManager:
if self._machine_manager is None:
self._machine_manager = MachineManager.createMachineManager()
return self._machine_manager
@ -1414,7 +1386,8 @@ class CuraApplication(QtApplication):
# If a model is to small then it will not contain any points
if offset_shape_arr is None and hull_shape_arr is None:
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
title=self._i18n_catalog.i18nc("@info:title", "Warning")).show()
title=self._i18n_catalog.i18nc("@info:title", "Warning")
).show()
return
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher

View file

@ -1,12 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.PluginObject import PluginObject
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.Application import Application
import os
@ -26,9 +24,6 @@ class MachineAction(QObject, PluginObject):
self._key = key
self._label = label
self._qml_url = ""
self._component = None
self._context = None
self._view = None
self._finished = False
@ -52,7 +47,6 @@ class MachineAction(QObject, PluginObject):
# /sa _reset
@pyqtSlot()
def reset(self):
self._component = None
self._finished = False
self._reset()
@ -78,7 +72,6 @@ class MachineAction(QObject, PluginObject):
@pyqtProperty(QObject, constant = True)
def displayItem(self):
if not self._component:
if not self._view:
self._createViewFromQML()
return self._view
return self._view

View file

@ -16,6 +16,7 @@ import math
import os.path
import unicodedata
import json
import re #To create abbreviations for printer names.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
@ -238,7 +239,7 @@ class PrintInformation(QObject):
pass
active_material_id = Application.getInstance().getMachineManager().activeMaterialId
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id = active_material_id)
if active_material_containers:
self._active_material_container = active_material_containers[0]
@ -316,15 +317,14 @@ class PrintInformation(QObject):
return
global_stack_name = global_container_stack.getName()
split_name = global_stack_name.split(" ")
abbr_machine = ""
for word in split_name:
for word in re.findall(r"[\w']+", global_stack_name):
if word.lower() == "ultimaker":
abbr_machine += "UM"
elif word.isdigit():
abbr_machine += word
else:
stripped_word = self._stripAccents(word.strip("()[]{}#").upper())
stripped_word = self._stripAccents(word.upper())
# - use only the first character if the word is too long (> 3 characters)
# - use the whole word if it's not too long (<= 3 characters)
if len(stripped_word) > 3:

View file

@ -3,19 +3,15 @@
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger
from UM.Signal import signalemitter
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
import os
i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device.
@ -63,11 +59,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._camera_active = False
self._monitor_view_qml_path = ""
self._monitor_component = None
self._monitor_item = None
self._control_view_qml_path = ""
self._control_component = None
self._control_item = None
self._qml_context = None
@ -155,54 +149,31 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time.
if not self._monitor_component:
if not self._monitor_item:
self._createMonitorViewFromQML()
return self._monitor_item
@pyqtProperty(QObject, constant=True)
def controlItem(self):
if not self._control_component:
if not self._control_item:
self._createControlViewFromQML()
return self._control_item
def _createControlViewFromQML(self):
if not self._control_view_qml_path:
return
path = QUrl.fromLocalFile(self._control_view_qml_path)
# Because of garbage collection we need to keep this referenced by python.
self._control_component = QQmlComponent(Application.getInstance()._engine, path)
# Check if the context was already requested before (Printer output device might have multiple items in the future)
if self._qml_context is None:
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("OutputDevice", self)
self._control_item = self._control_component.create(self._qml_context)
if self._control_item is None:
Logger.log("e", "QQmlComponent status %s", self._control_component.status())
Logger.log("e", "QQmlComponent error string %s", self._control_component.errorString())
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
"OutputDevice": self
})
def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path:
return
path = QUrl.fromLocalFile(self._monitor_view_qml_path)
# Because of garbage collection we need to keep this referenced by python.
self._monitor_component = QQmlComponent(Application.getInstance()._engine, path)
# Check if the context was already requested before (Printer output device might have multiple items in the future)
if self._qml_context is None:
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("OutputDevice", self)
self._monitor_item = self._monitor_component.create(self._qml_context)
if self._monitor_item is None:
Logger.log("e", "QQmlComponent status %s", self._monitor_component.status())
Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString())
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
"OutputDevice": self
})
@pyqtProperty(str, notify=printerTypeChanged)
def printerType(self):
@ -469,9 +440,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
continue
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0].getName())
result.append(containers[0]["name"])
else:
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
return result
@ -491,9 +462,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
result.append("#00000000") #No material.
continue
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0].getMetaDataEntry("color_code"))
result.append(containers[0]["color_code"])
else:
result.append("#00000000") #Unknown material.
return result

View file

@ -1,13 +1,12 @@
# Copyright (c) 2016 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# This collects a lot of quality and quality changes related code which was split between ContainerManager
# and the MachineManager and really needs to usable from both.
from typing import List, Optional, Dict, TYPE_CHECKING
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.ExtruderManager import ExtruderManager
@ -33,16 +32,16 @@ class QualityManager:
# \param quality_name
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
# specified then the currently selected machine definition is used.
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
# the current set of selected materials is used.
# \param material_containers_metadata If nothing is specified then the
# current set of selected materials is used.
# \return the matching quality container \type{InstanceContainer}
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[InstanceContainer]:
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers_metadata: Optional[List[Dict[str, Any]]] = None) -> Optional[InstanceContainer]:
criteria = {"type": "quality", "name": quality_name}
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
# Fall back to using generic materials and qualities if nothing could be found.
if not result and material_containers and len(material_containers) == 1:
basic_materials = self._getBasicMaterials(material_containers[0])
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
return result[0] if result else None
@ -108,18 +107,18 @@ class QualityManager:
# \param quality_type \type{str} the name of the quality type to search for.
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
# specified then the currently selected machine definition is used.
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
# the current set of selected materials is used.
# \param material_containers_metadata If nothing is specified then the
# current set of selected materials is used.
# \return the matching quality container \type{InstanceContainer}
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> InstanceContainer:
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs) -> InstanceContainer:
criteria = kwargs
criteria["type"] = "quality"
if quality_type:
criteria["quality_type"] = quality_type
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
result = self._getFilteredContainersForStack(machine_definition, material_containers_metadata, **criteria)
# Fall back to using generic materials and qualities if nothing could be found.
if not result and material_containers and len(material_containers) == 1:
basic_materials = self._getBasicMaterials(material_containers[0])
if not result and material_containers_metadata and len(material_containers_metadata) == 1:
basic_materials = self._getBasicMaterialMetadatas(material_containers_metadata[0])
if basic_materials:
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
return result[0] if result else None
@ -131,9 +130,9 @@ class QualityManager:
# \return \type{List[InstanceContainer]} the list of suitable qualities.
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
criteria = {"type": "quality"}
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
result = self._getFilteredContainersForStack(machine_definition, [material_container.getMetaData()], **criteria)
if not result:
basic_materials = self._getBasicMaterials(material_container)
basic_materials = self._getBasicMaterialMetadatas(material_container.getMetaData())
if basic_materials:
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
@ -202,22 +201,34 @@ class QualityManager:
## Fetch more basic versions of a material.
#
# This tries to find a generic or basic version of the given material.
# \param material_container \type{InstanceContainer} the material
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
def _getBasicMaterials(self, material_container: InstanceContainer):
base_material = material_container.getMetaDataEntry("material")
material_container_definition = material_container.getDefinition()
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
else:
# \param material_container \type{Dict[str, Any]} The metadata of a
# material to find the basic versions of.
# \return \type{List[Dict[str, Any]]} A list of the metadata of basic
# materials, or an empty list if none could be found.
def _getBasicMaterialMetadatas(self, material_container: Dict[str, Any]) -> List[Dict[str, Any]]:
if "definition" not in material_container:
definition_id = "fdmprinter"
else:
material_container_definition = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = material_container["definition"])
if not material_container_definition:
definition_id = "fdmprinter"
else:
material_container_definition = material_container_definition[0]
if "has_machine_quality" not in material_container_definition:
definition_id = "fdmprinter"
else:
definition_id = material_container_definition.get("quality_definition", material_container_definition["id"])
base_material = material_container.get("material")
if base_material:
# There is a basic material specified
criteria = { "type": "material", "name": base_material, "definition": definition_id }
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
containers = [basic_material for basic_material in containers if
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
"variant")]
criteria = {
"type": "material",
"name": base_material,
"definition": definition_id,
"variant": material_container.get("variant")
}
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
return containers
return []
@ -225,29 +236,25 @@ class QualityManager:
def _getFilteredContainers(self, **kwargs):
return self._getFilteredContainersForStack(None, None, **kwargs)
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_metadata: Optional[List[Dict[str, Any]]] = None, **kwargs):
# Fill in any default values.
if machine_definition is None:
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition_id is not None:
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition_id)[0]
# for convenience
if material_containers is None:
material_containers = []
if not material_containers:
if not material_metadata:
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks:
material_containers = [stack.material for stack in active_stacks]
material_metadata = [stack.material.getMetaData() for stack in active_stacks]
criteria = kwargs
filter_by_material = False
machine_definition = self.getParentMachineDefinition(machine_definition)
criteria["definition"] = machine_definition.getId()
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainersMetadata(**criteria)
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
@ -261,12 +268,12 @@ class QualityManager:
# Stick the material IDs in a set
material_ids = set()
for material_instance in material_containers:
for material_instance in material_metadata:
if material_instance is not None:
# Add the parent material too.
for basic_material in self._getBasicMaterials(material_instance):
material_ids.add(basic_material.getId())
material_ids.add(material_instance.getId())
for basic_material in self._getBasicMaterialMetadatas(material_instance):
material_ids.add(basic_material["id"])
material_ids.add(material_instance["id"])
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
result = []
@ -292,13 +299,13 @@ class QualityManager:
# We have a normal (whole) machine defintion
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
if quality_definition is not None:
parent_machine_definition = container_registry.findDefinitionContainers(id=quality_definition)[0]
parent_machine_definition = container_registry.findDefinitionContainers(id = quality_definition)[0]
return self.getParentMachineDefinition(parent_machine_definition)
else:
return machine_definition
else:
# This looks like an extruder. Find the rest of the machine.
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
parent_machine = self.getParentMachineDefinition(whole_machine)
if whole_machine is parent_machine:
# This extruder already belongs to a 'parent' machine def.
@ -307,7 +314,7 @@ class QualityManager:
# Look up the corresponding extruder definition in the parent machine definition.
extruder_position = machine_definition.getMetaDataEntry("position")
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
return container_registry.findDefinitionContainers(id=parent_extruder_id)[0]
return container_registry.findDefinitionContainers(id = parent_extruder_id)[0]
## Get the whole/global machine definition from an extruder definition.
#
@ -321,5 +328,5 @@ class QualityManager:
return machine_definition
else:
container_registry = ContainerRegistry.getInstance()
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
whole_machine = container_registry.findDefinitionContainers(id = machine_entry)[0]
return whole_machine

View file

@ -1,10 +1,11 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import copy
import os.path
import urllib
import uuid
from typing import Dict, Union
from typing import Any, Dict, List, Union
from PyQt5.QtCore import QObject, QUrl, QVariant
from UM.FlameProfiler import pyqtSlot
@ -55,13 +56,24 @@ class ContainerManager(QObject):
# \return The ID of the new container, or an empty string if duplication failed.
@pyqtSlot(str, result = str)
def duplicateContainer(self, container_id):
containers = self._container_registry.findContainers(None, id = container_id)
#TODO: It should be able to duplicate a container of which only the metadata is known.
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
return ""
container = containers[0]
new_container = self.duplicateContainerInstance(container)
return new_container.getId()
## Create a duplicate of the given container instance
#
# This will create and add a duplicate of the container that was passed.
#
# \param container \type{ContainerInterface} The container to duplicate.
#
# \return The duplicated container, or None if duplication failed.
def duplicateContainerInstance(self, container):
new_container = None
new_name = self._container_registry.uniqueName(container.getName())
# Only InstanceContainer has a duplicate method at the moment.
@ -73,10 +85,11 @@ class ContainerManager(QObject):
new_container.deserialize(container.serialize())
new_container.setName(new_name)
# TODO: we probably don't want to add it to the registry here!
if new_container:
self._container_registry.addContainer(new_container)
return new_container.getId()
return new_container
## Change the name of a specified container to a new name.
#
@ -87,14 +100,14 @@ class ContainerManager(QObject):
# \return True if successful, False if not.
@pyqtSlot(str, str, str, result = bool)
def renameContainer(self, container_id, new_id, new_name):
containers = self._container_registry.findContainers(None, id = container_id)
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could rename container %s because it was not found.", container_id)
return False
container = containers[0]
# First, remove the container from the registry. This will clean up any files related to the container.
self._container_registry.removeContainer(container)
self._container_registry.removeContainer(container_id)
# Ensure we have a unique name for the container
new_name = self._container_registry.uniqueName(new_name)
@ -115,9 +128,9 @@ class ContainerManager(QObject):
# \return True if the container was successfully removed, False if not.
@pyqtSlot(str, result = bool)
def removeContainer(self, container_id):
containers = self._container_registry.findContainers(None, id = container_id)
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could remove container %s because it was not found.", container_id)
Logger.log("w", "Could not remove container %s because it was not found.", container_id)
return False
self._container_registry.removeContainer(containers[0].getId())
@ -135,14 +148,14 @@ class ContainerManager(QObject):
# \return True if successfully merged, False if not.
@pyqtSlot(str, result = bool)
def mergeContainers(self, merge_into_id, merge_id):
containers = self._container_registry.findContainers(None, id = merge_into_id)
containers = self._container_registry.findContainers(id = merge_into_id)
if not containers:
Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
return False
merge_into = containers[0]
containers = self._container_registry.findContainers(None, id = merge_id)
containers = self._container_registry.findContainers(id = merge_id)
if not containers:
Logger.log("w", "Could not merge container %s because it was not found", merge_id)
return False
@ -164,13 +177,13 @@ class ContainerManager(QObject):
# \return True if successful, False if not.
@pyqtSlot(str, result = bool)
def clearContainer(self, container_id):
containers = self._container_registry.findContainers(None, id = container_id)
if not containers:
Logger.log("w", "Could clear container %s because it was not found.", container_id)
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot clear read-only container %s", container_id)
return False
if containers[0].isReadOnly():
Logger.log("w", "Cannot clear read-only container %s", container_id)
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could clear container %s because it was not found.", container_id)
return False
containers[0].clear()
@ -179,16 +192,12 @@ class ContainerManager(QObject):
@pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id, entry_name):
containers = self._container_registry.findContainers(None, id=container_id)
if not containers:
metadatas = self._container_registry.findContainersMetadata(id = container_id)
if not metadatas:
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
return ""
result = containers[0].getMetaDataEntry(entry_name)
if result is not None:
return str(result)
else:
return ""
return str(metadatas[0].get(entry_name, ""))
## Set a metadata entry of the specified container.
#
@ -204,17 +213,17 @@ class ContainerManager(QObject):
# \return True if successful, False if not.
@pyqtSlot(str, str, str, result = bool)
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
containers = self._container_registry.findContainers(None, id = container_id)
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
return False
containers = self._container_registry.findContainers(id = container_id) #We need the complete container, since we need to know whether the container is read-only or not.
if not containers:
Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
return False
container = containers[0]
if container.isReadOnly():
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
return False
entries = entry_name.split("/")
entry_name = entries.pop()
@ -254,17 +263,17 @@ class ContainerManager(QObject):
# \return True if successful, False if not.
@pyqtSlot(str, str, str, str, result = bool)
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
containers = self._container_registry.findContainers(None, id = container_id)
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
return False
containers = self._container_registry.findContainers(id = container_id)
if not containers:
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
return False
container = containers[0]
if container.isReadOnly():
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
return False
container.setProperty(setting_key, property_name, property_value)
basefile = container.getMetaDataEntry("base_file", container_id)
@ -300,35 +309,30 @@ class ContainerManager(QObject):
## Set the name of the specified container.
@pyqtSlot(str, str, result = bool)
def setContainerName(self, container_id, new_name):
containers = self._container_registry.findContainers(None, id = container_id)
if self._container_registry.isReadOnly(container_id):
Logger.log("w", "Cannot set name of read-only container %s.", container_id)
return False
containers = self._container_registry.findContainers(id = container_id) #We need to get the full container, not just metadata, since we need to know whether it's read-only.
if not containers:
Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
return False
container = containers[0]
if container.isReadOnly():
Logger.log("w", "Cannot set name of read-only container %s.", container_id)
return False
container.setName(new_name)
containers[0].setName(new_name)
return True
## Find instance containers matching certain criteria.
#
# This effectively forwards to ContainerRegistry::findInstanceContainers.
# This effectively forwards to
# ContainerRegistry::findInstanceContainersMetadata.
#
# \param criteria A dict of key - value pairs to search for.
#
# \return A list of container IDs that match the given criteria.
@pyqtSlot("QVariantMap", result = "QVariantList")
def findInstanceContainers(self, criteria):
result = []
for entry in self._container_registry.findInstanceContainers(**criteria):
result.append(entry.getId())
return result
return [entry["id"] for entry in self._container_registry.findInstanceContainersMetadata(**criteria)]
@pyqtSlot(str, result = bool)
def isContainerUsed(self, container_id):
@ -336,15 +340,17 @@ class ContainerManager(QObject):
# check if this is a material container. If so, check if any material with the same base is being used by any
# stacks.
container_ids_to_check = [container_id]
container_results = self._container_registry.findInstanceContainers(id = container_id, type = "material")
container_results = self._container_registry.findInstanceContainersMetadata(id = container_id, type = "material")
if container_results:
this_container = container_results[0]
material_base_file = this_container.getMetaDataEntry("base_file", this_container.getId())
material_base_file = this_container["id"]
if "base_file" in this_container:
material_base_file = this_container["base_file"]
# check all material container IDs with the same base
material_containers = self._container_registry.findInstanceContainers(base_file = material_base_file,
material_containers = self._container_registry.findInstanceContainersMetadata(base_file = material_base_file,
type = "material")
if material_containers:
container_ids_to_check = [container.getId() for container in material_containers]
container_ids_to_check = [container["id"] for container in material_containers]
all_stacks = self._container_registry.findContainerStacks()
for stack in all_stacks:
@ -412,7 +418,7 @@ class ContainerManager(QObject):
else:
mime_type = self._container_name_filters[file_type]["mime"]
containers = self._container_registry.findContainers(None, id = container_id)
containers = self._container_registry.findContainers(id = container_id)
if not containers:
return { "status": "error", "message": "Container not found"}
container = containers[0]
@ -508,7 +514,7 @@ class ContainerManager(QObject):
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
# Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges
if not quality_changes or quality_changes.isReadOnly():
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue
@ -616,9 +622,9 @@ class ContainerManager(QObject):
elif activate_quality:
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
containers = self._container_registry.findInstanceContainers(type = "quality", definition = definition_id, quality_type = activate_quality_type)
containers = self._container_registry.findInstanceContainersMetadata(type = "quality", definition = definition_id, quality_type = activate_quality_type)
if containers:
self._machine_manager.setActiveQuality(containers[0].getId())
self._machine_manager.setActiveQuality(containers[0]["id"])
self._machine_manager.activeQualityChanged.emit()
return containers_found
@ -653,11 +659,13 @@ class ContainerManager(QObject):
container_registry = self._container_registry
containers_to_rename = self._container_registry.findInstanceContainers(type = "quality_changes", name = quality_name)
containers_to_rename = self._container_registry.findInstanceContainersMetadata(type = "quality_changes", name = quality_name)
for container in containers_to_rename:
stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
stack_id = global_stack.getId()
if "extruder" in container:
stack_id = container["extruder"]
container_registry.renameContainer(container["id"], new_name, self._createUniqueId(stack_id, new_name))
if not containers_to_rename:
Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
@ -679,30 +687,32 @@ class ContainerManager(QObject):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack or not quality_name:
return ""
machine_definition = global_stack.getBottom()
machine_definition = global_stack.definition
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
material_containers = [stack.material for stack in active_stacks]
if active_stacks is None:
return ""
material_metadatas = [stack.material.getMetaData() for stack in active_stacks]
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
material_containers)
material_metadatas)
return result[0].getName() if result else ""
## Duplicate a quality or quality changes profile specific to a machine type
#
# \param quality_name \type{str} the name of the quality or quality changes container to duplicate.
# \param base_name \type{str} the desired name for the new container.
# \param machine_definition \type{DefinitionContainer}
# \param material_instances \type{List[InstanceContainer]}
# \return \type{str} the name of the newly created container.
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
# \param quality_name The name of the quality or quality changes container to duplicate.
# \param base_name The desired name for the new container.
# \param machine_definition The machine with the specific machine type.
# \param material_metadatas Metadata of materials
# \return List of duplicated quality profiles.
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name: str, base_name: str, machine_definition: DefinitionContainer, material_metadatas: List[Dict[str, Any]]) -> List[InstanceContainer]:
Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
if base_name is None:
base_name = quality_name
# Try to find a Quality with the name.
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_metadatas)
if container:
Logger.log("d", "We found a quality to duplicate.")
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
@ -711,7 +721,7 @@ class ContainerManager(QObject):
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
# Duplicate a quality profile
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition):
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition) -> List[InstanceContainer]:
if base_name is None:
base_name = quality_container.getName()
new_name = self._container_registry.uniqueName(base_name)
@ -735,7 +745,7 @@ class ContainerManager(QObject):
return new_change_instances
# Duplicate a quality changes container
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition):
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition) -> List[InstanceContainer]:
new_change_instances = []
for container in QualityManager.getInstance().findQualityChangesByName(quality_changes_name,
machine_definition):
@ -754,27 +764,57 @@ class ContainerManager(QObject):
# \return \type{str} the id of the newly created container.
@pyqtSlot(str, result = str)
def duplicateMaterial(self, material_id: str) -> str:
containers = self._container_registry.findInstanceContainers(id=material_id)
if not containers:
original = self._container_registry.findContainersMetadata(id = material_id)
if not original:
Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
return ""
original = original[0]
base_container_id = original.get("base_file")
base_container = self._container_registry.findContainers(id = base_container_id)
if not base_container:
Logger.log("d", "Unable to duplicate the material with id {material_id}, because base_file {base_container_id} doesn't exist.".format(material_id = material_id, base_container_id = base_container_id))
return ""
base_container = base_container[0]
#We'll copy all containers with the same base.
#This way the correct variant and machine still gets assigned when loading the copy of the material.
containers_to_copy = self._container_registry.findInstanceContainers(base_file = base_container_id)
# Ensure all settings are saved.
Application.getInstance().saveSettings()
# Create a new ID & container to hold the data.
new_id = self._container_registry.uniqueName(material_id)
container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer
duplicated_container = container_type(new_id)
new_containers = []
new_base_id = self._container_registry.uniqueName(base_container.getId())
new_base_container = copy.deepcopy(base_container)
new_base_container.getMetaData()["id"] = new_base_id
new_base_container.getMetaData()["base_file"] = new_base_id
new_containers.append(new_base_container)
# Instead of duplicating we load the data from the basefile again.
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
# are also correctly created.
with open(containers[0].getPath(), encoding="utf-8") as f:
duplicated_container.deserialize(f.read())
duplicated_container.setDirty(True)
self._container_registry.addContainer(duplicated_container)
return self._getMaterialContainerIdForActiveMachine(new_id)
#Clone all of them.
clone_of_original = None #Keeping track of which one is the clone of the original material, since we need to return that.
for container_to_copy in containers_to_copy:
#Create unique IDs for every clone.
current_id = container_to_copy.getId()
new_id = new_base_id
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant"):
variant = self._container_registry.findContainers(id = container_to_copy.getMetaDataEntry("variant"))[0]
new_id += "_" + variant.getName().replace(" ", "_")
if current_id == material_id:
clone_of_original = new_id
new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id
new_container.getMetaData()["base_file"] = new_base_id
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
ContainerRegistry.getInstance().addContainer(container_to_add)
return self._getMaterialContainerIdForActiveMachine(clone_of_original)
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
#
@ -789,12 +829,12 @@ class ContainerManager(QObject):
return ""
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter)
containers = self._container_registry.findInstanceContainersMetadata(id = "generic_pla*", approximate_diameter = approximate_diameter)
if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
return ""
base_file = containers[0].getMetaDataEntry("base_file")
base_file = containers[0].get("base_file")
containers = self._container_registry.findInstanceContainers(id = base_file)
if not containers:
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
@ -835,14 +875,14 @@ class ContainerManager(QObject):
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
if has_machine_materials or has_variant_materials:
if has_variants:
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
else:
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
materials = self._container_registry.findInstanceContainersMetadata(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
if materials:
return materials[0].getId()
return materials[0]["id"]
Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file)
Logger.log("w", "Unable to find a suitable container based on %s for the current machine.", base_file)
return "" # do not activate a new material if a container can not be found
return base_file
@ -853,25 +893,25 @@ class ContainerManager(QObject):
# \return \type{list} a list of names of materials with the same GUID
@pyqtSlot(str, result = "QStringList")
def getLinkedMaterials(self, material_id: str):
containers = self._container_registry.findInstanceContainers(id=material_id)
containers = self._container_registry.findInstanceContainersMetadata(id = material_id)
if not containers:
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
return []
material_container = containers[0]
material_base_file = material_container.getMetaDataEntry("base_file", "")
material_guid = material_container.getMetaDataEntry("GUID", "")
material_base_file = material_container.get("base_file", "")
material_guid = material_container.get("GUID", "")
if not material_guid:
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
return []
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid)
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_guid)
linked_material_names = []
for container in containers:
if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId():
if container["id"] in [material_id, material_base_file] or container.get("base_file") != container["id"]:
continue
linked_material_names.append(container.getName())
linked_material_names.append(container["name"])
return linked_material_names
## Unlink a material from all other materials by creating a new GUID
@ -957,14 +997,6 @@ class ContainerManager(QObject):
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
self._container_name_filters[name_filter] = entry
## Get containers filtered by machine type and material if required.
#
# \param kwargs Initial search criteria that the containers need to match.
#
# \return A list of containers matching the search criteria.
def _getFilteredContainers(self, **kwargs):
return QualityManager.getInstance()._getFilteredContainers(**kwargs)
## Creates a unique ID for a container by prefixing the name with the stack ID.
#
# This method creates a unique ID for a container by prefixing it with a specified stack ID.
@ -1004,9 +1036,9 @@ class ContainerManager(QObject):
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
if not machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
quality_changes.setDefinition("fdmprinter")
else:
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition).getId())
from cura.CuraApplication import CuraApplication
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)

View file

@ -36,6 +36,11 @@ class CuraContainerRegistry(ContainerRegistry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
# is added, we check to see if an extruder stack needs to be added.
self.containerAdded.connect(self._onContainerAdded)
## Overridden from ContainerRegistry
#
# Adds a container to the registry.
@ -44,7 +49,6 @@ class CuraContainerRegistry(ContainerRegistry):
# Global stack based on metadata information.
@override(ContainerRegistry)
def addContainer(self, container):
# Note: Intentional check with type() because we want to ignore subclasses
if type(container) == ContainerStack:
container = self._convertContainerStack(container)
@ -89,8 +93,8 @@ class CuraContainerRegistry(ContainerRegistry):
def _containerExists(self, container_type, container_name):
container_class = ContainerStack if container_type == "machine" else InstanceContainer
return self.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainers(container_class, name = container_name, type = container_type)
return self.findContainersMetadata(id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
## Exports an profile to a file
#
@ -119,7 +123,7 @@ class CuraContainerRegistry(ContainerRegistry):
found_containers = []
extruder_positions = []
for instance_id in instance_ids:
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
if containers:
found_containers.append(containers[0])
@ -129,9 +133,9 @@ class CuraContainerRegistry(ContainerRegistry):
# Global stack
extruder_positions.append(-1)
else:
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id)
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = extruder_id)
if extruder_containers:
extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0)))
extruder_positions.append(int(extruder_containers[0].get("position", 0)))
else:
extruder_positions.append(0)
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
@ -205,53 +209,41 @@ class CuraContainerRegistry(ContainerRegistry):
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
if profile_or_list: # Success!
if profile_or_list:
name_seed = os.path.splitext(os.path.basename(file_name))[0]
new_name = self.uniqueName(name_seed)
# Ensure it is always a list of profiles
if type(profile_or_list) is not list:
profile = profile_or_list
profile_or_list = [profile_or_list]
result = self._configureProfile(profile, name_seed, new_name)
if result is not None:
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, result)}
# Import all profiles
for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0:
# This is assumed to be the global profile
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName())}
else:
profile_index = -1
global_profile = None
for profile in profile_or_list:
if profile_index >= 0:
if len(machine_extruders) > profile_index:
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
# Ensure the extruder profiles get non-conflicting names
# NB: these are not user-facing
if "extruder" in profile.getMetaData():
profile.setMetaDataEntry("extruder", extruder_id)
else:
profile.addMetaDataEntry("extruder", extruder_id)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
elif profile_index == 0:
# Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
profile._id = self.uniqueName("temporary_profile")
self.addContainer(profile)
ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
self.removeContainer(profile.getId())
break
else:
# The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
break
elif len(machine_extruders) > profile_index:
# This is assumed to be an extruder profile
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom())
if not profile.getMetaDataEntry("extruder"):
profile.addMetaDataEntry("extruder", extruder_id)
else:
global_profile = profile
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
profile.setMetaDataEntry("extruder", extruder_id)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
result = self._configureProfile(profile, profile_id, new_name)
if result is not None:
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, result)}
else: #More extruders in the imported file than in the machine.
continue #Delete the additional profiles.
profile_index += 1
result = self._configureProfile(profile, profile_id, new_name)
if result is not None:
return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!",
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
file_name, result)}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
# If it hasn't returned by now, none of the plugins loaded the profile successfully.
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
@ -270,7 +262,6 @@ class CuraContainerRegistry(ContainerRegistry):
#
# \return None if configuring was successful or an error message if an error occurred.
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
profile.setReadOnly(False)
profile.setDirty(True) # Ensure the profiles are correctly saved
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
@ -288,7 +279,7 @@ class CuraContainerRegistry(ContainerRegistry):
quality_type_criteria = {"quality_type": quality_type}
if self._machineHasOwnQualities():
profile.setDefinition(self._activeQualityDefinition())
profile.setDefinition(self._activeQualityDefinition().getId())
if self._machineHasOwnMaterials():
active_material_id = self._activeMaterialId()
if active_material_id and active_material_id != "empty": # only update if there is an active material
@ -298,7 +289,7 @@ class CuraContainerRegistry(ContainerRegistry):
quality_type_criteria["definition"] = profile.getDefinition().getId()
else:
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
profile.setDefinition("fdmprinter")
quality_type_criteria["definition"] = "fdmprinter"
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
@ -345,7 +336,7 @@ class CuraContainerRegistry(ContainerRegistry):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
definition = self.findDefinitionContainers(id=definition_id)[0]
definition = self.findDefinitionContainers(id = definition_id)[0]
if definition:
return definition
@ -408,6 +399,21 @@ class CuraContainerRegistry(ContainerRegistry):
if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
def _onContainerAdded(self, container):
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
# is added, we check to see if an extruder stack needs to be added.
if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine":
return
machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains")
if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}:
return
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId())
if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
new_extruder_id = extruder_id
@ -423,7 +429,6 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_stack.setName(extruder_definition.getName())
extruder_stack.setDefinition(extruder_definition)
extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
extruder_stack.setNextStack(machine)
# create empty user changes container otherwise
user_container = InstanceContainer(extruder_stack.id + "_user")
@ -431,7 +436,7 @@ class CuraContainerRegistry(ContainerRegistry):
user_container.addMetaDataEntry("machine", extruder_stack.getId())
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(machine.definition)
user_container.setDefinition(machine.definition.getId())
if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
@ -442,8 +447,8 @@ class CuraContainerRegistry(ContainerRegistry):
user_container.addInstance(machine.userChanges.getInstance(user_setting_key))
machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
extruder_stack.setUserChanges(user_container)
self.addContainer(user_container)
extruder_stack.setUserChanges(user_container)
variant_id = "default"
if machine.variant.getId() not in ("empty", "empty_variant"):
@ -489,6 +494,9 @@ class CuraContainerRegistry(ContainerRegistry):
self.addContainer(extruder_stack)
# Set next stack at the end
extruder_stack.setNextStack(machine)
return extruder_stack
def _findQualityChangesContainerInCuraFolder(self, name):
@ -530,13 +538,13 @@ class CuraContainerRegistry(ContainerRegistry):
# set after upgrading, because the proper global stack was not yet loaded. This method
# makes sure those extruders also get the right stack set.
def _connectUpgradedExtruderStacksToMachines(self):
extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack)
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
for extruder_stack in extruder_stacks:
if extruder_stack.getNextStack():
# Has the right next stack, so ignore it.
continue
machines = ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack.getMetaDataEntry("machine", ""))
machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", ""))
if machines:
extruder_stack.setNextStack(machines[0])
else:

View file

@ -14,7 +14,7 @@ from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackErro
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
from . import Exceptions
@ -246,7 +246,7 @@ class CuraContainerStack(ContainerStack):
## Set the definition container.
#
# \param new_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes".
def setDefinition(self, new_definition: DefinitionContainer) -> None:
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
## Set the definition container by an ID.
@ -377,7 +377,7 @@ class CuraContainerStack(ContainerStack):
if not container or not isinstance(container, DefinitionContainer):
definition = self.findContainer(container_type = DefinitionContainer)
if not definition:
raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self._id))
raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self.getId()))
new_containers[index] = definition
continue
@ -487,11 +487,17 @@ class CuraContainerStack(ContainerStack):
search_criteria.pop("name", None)
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if materials:
return materials[0]
if not materials:
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None
for material in materials:
# Prefer a read-only material
if ContainerRegistry.getInstance().isReadOnly(material.getId()):
return material
return materials[0]
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
return None
## Find the quality that should be used as "default" quality.
#
@ -502,7 +508,7 @@ class CuraContainerStack(ContainerStack):
def findDefaultQuality(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition()
registry = ContainerRegistry.getInstance()
material_container = self.material if self.material != self._empty_instance_container else None
material_container = self.material if self.material.getId() not in (self._empty_material.getId(), self._empty_instance_container.getId()) else None
search_criteria = {"type": "quality"}
@ -546,7 +552,7 @@ class CuraContainerStack(ContainerStack):
material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
if definition.getMetaDataEntry("has_machine_quality"):
if self.material != self._empty_instance_container:
material_search_criteria["definition"] = material_container.getDefinition().id
material_search_criteria["definition"] = material_container.getMetaDataEntry("definition")
if definition.getMetaDataEntry("has_variants"):
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
@ -557,10 +563,10 @@ class CuraContainerStack(ContainerStack):
material_search_criteria["variant"] = self.variant.id
else:
material_search_criteria["definition"] = "fdmprinter"
material_containers = registry.findInstanceContainers(**material_search_criteria)
material_containers = registry.findInstanceContainersMetadata(**material_search_criteria)
# Try all materials to see if there is a quality profile available.
for material_container in material_containers:
search_criteria["material"] = material_container.getId()
search_criteria["material"] = material_container["id"]
containers = registry.findInstanceContainers(**search_criteria)
if containers:

View file

@ -3,7 +3,7 @@
from UM.Logger import Logger
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -34,7 +34,7 @@ class CuraStackBuilder:
# Make sure the new name does not collide with any definition or (quality) profile
# createUniqueName() only looks at other stacks, but not at definitions or quality profiles
# Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true
if registry.findContainers(id = generated_name):
if registry.findContainersMetadata(id = generated_name):
generated_name = registry.uniqueName(generated_name)
new_global_stack = cls.createGlobalStack(
@ -55,12 +55,12 @@ class CuraStackBuilder:
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id)
new_extruder = cls.createExtruderStack(
new_extruder_id,
definition=extruder_definition,
machine_definition=machine_definition,
quality="default",
material="default",
variant="default",
next_stack=new_global_stack
definition = extruder_definition,
machine_definition_id = machine_definition.getId(),
quality = "default",
material = "default",
variant = "default",
next_stack = new_global_stack
)
new_global_stack.addExtruder(new_extruder)
else:
@ -74,7 +74,7 @@ class CuraStackBuilder:
new_extruder = cls.createExtruderStack(
new_extruder_id,
definition = extruder_definition,
machine_definition = machine_definition,
machine_definition_id = machine_definition.getId(),
quality = "default",
material = "default",
variant = "default",
@ -88,12 +88,13 @@ class CuraStackBuilder:
#
# \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on.
# \param machine_definition The machine definition to use for the user container.
# \param machine_definition_id The ID of the machine definition to use for
# the user container.
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
#
# \return A new Global stack instance with the specified parameters.
@classmethod
def createExtruderStack(cls, new_stack_id: str, definition: DefinitionContainer, machine_definition: DefinitionContainer, **kwargs) -> ExtruderStack:
def createExtruderStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, machine_definition_id: str, **kwargs) -> ExtruderStack:
stack = ExtruderStack(new_stack_id)
stack.setName(definition.getName())
stack.setDefinition(definition)
@ -108,7 +109,7 @@ class CuraStackBuilder:
user_container.addMetaDataEntry("extruder", new_stack_id)
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(machine_definition)
user_container.setDefinition(machine_definition_id)
stack.setUserChanges(user_container)
@ -148,7 +149,7 @@ class CuraStackBuilder:
#
# \return A new Global stack instance with the specified parameters.
@classmethod
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainer, **kwargs) -> GlobalStack:
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, **kwargs) -> GlobalStack:
stack = GlobalStack(new_stack_id)
stack.setDefinition(definition)
@ -157,7 +158,7 @@ class CuraStackBuilder:
user_container.addMetaDataEntry("machine", new_stack_id)
from cura.CuraApplication import CuraApplication
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(definition)
user_container.setDefinition(definition.getId())
stack.setUserChanges(user_container)
@ -193,8 +194,7 @@ class CuraStackBuilder:
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
definition_changes_container = InstanceContainer(unique_container_name)
definition = container_stack.getBottom()
definition_changes_container.setDefinition(definition)
definition_changes_container.setDefinition(container_stack.getBottom().getId())
definition_changes_container.addMetaDataEntry("type", "definition_changes")
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)

View file

@ -356,14 +356,16 @@ class ExtruderManager(QObject):
# \return \type{List[ContainerStack]} a list of
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return None
result = []
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
if global_stack and global_stack.getId() in self._extruder_trains:
if global_stack.getId() in self._extruder_trains:
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
result.append(self._extruder_trains[global_stack.getId()][extruder])
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
return result[:machine_extruder_count]
def __globalContainerStackChanged(self) -> None:

View file

@ -8,8 +8,8 @@ from UM.i18n import i18nCatalog
import UM.Qt.ListModel
from UM.Application import Application
import UM.FlameProfiler
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack #To listen to changes on the extruders.
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
catalog = i18nCatalog("cura")
@ -68,7 +68,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
self._update_extruder_timer.setSingleShot(True)
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
self._add_global = False
self._simple_names = False
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
@ -76,21 +75,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# Listen to changes
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) # When the machine is swapped we must update the active machine extruders
ExtruderManager.getInstance().extrudersChanged.connect(self._extrudersChanged) # When the extruders change we must link to the stack-changed signal of the new extruder
Application.getInstance().getExtruderManager().extrudersChanged.connect(self._extrudersChanged) # When the extruders change we must link to the stack-changed signal of the new extruder
Application.getInstance().getContainerRegistry().containerMetaDataChanged.connect(self._onExtruderStackContainersChanged) # When meta data from a material container changes we must update
self._extrudersChanged() # Also calls _updateExtruders
def setAddGlobal(self, add):
if add != self._add_global:
self._add_global = add
self._updateExtruders()
self.addGlobalChanged.emit()
addGlobalChanged = pyqtSignal()
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
def addGlobal(self):
return self._add_global
addOptionalExtruderChanged = pyqtSignal()
def setAddOptionalExtruder(self, add_optional_extruder):
@ -140,8 +128,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# Link to new extruders
self._active_machine_extruders = []
extruder_manager = ExtruderManager.getInstance()
extruder_manager = Application.getInstance().getExtruderManager()
for extruder in extruder_manager.getExtruderStacks():
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
continue
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
self._active_machine_extruders.append(extruder)
@ -173,24 +163,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
# TODO: remove this - CURA-4482
if self._add_global:
material = global_container_stack.material
color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0]
item = {
"id": global_container_stack.getId(),
"name": catalog.i18nc("@menuitem", "Global"),
"color": color,
"index": -1,
"definition": ""
}
items.append(item)
extruders_changed = True
# get machine extruder count for verification
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()):
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
try:
position = int(position)

View file

@ -43,15 +43,11 @@ class GlobalStack(CuraContainerStack):
def getLoadingPriority(cls) -> int:
return 2
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
configuration_type = None
try:
parser = self._readAndValidateSerialized(serialized)
configuration_type = parser["metadata"].get("type")
if configuration_type == "machine":
configuration_type = "machine_stack"
except Exception as e:
Logger.log("e", "Could not get configuration type: %s", e)
@classmethod
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
configuration_type = super().getConfigurationTypeFromSerialized(serialized)
if configuration_type == "machine":
return "machine_stack"
return configuration_type
## Add an extruder to the list of extruders of this stack.
@ -67,7 +63,7 @@ class GlobalStack(CuraContainerStack):
return
if any(item.getId() == extruder.id for item in self._extruders.values()):
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self._id)
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self.getId())
return
self._extruders[position] = extruder

View file

@ -32,6 +32,7 @@ from .CuraStackBuilder import CuraStackBuilder
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.Settings.ProfilesModel import ProfilesModel
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
@ -60,9 +61,11 @@ class MachineManager(QObject):
self._instance_container_timer = QTimer()
self._instance_container_timer.setInterval(250)
self._instance_container_timer.setSingleShot(True)
self._instance_container_timer.timeout.connect(self.__onInstanceContainersChanged)
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
Application.getInstance().getContainerRegistry().containerLoadComplete.connect(self._onInstanceContainersChanged)
self._connected_to_profiles_model = False
## When the global container is changed, active material probably needs to be updated.
self.globalContainerChanged.connect(self.activeMaterialChanged)
@ -104,7 +107,7 @@ class MachineManager(QObject):
# There might already be some output devices by the time the signal is connected
self._onOutputDevicesChanged()
if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacks(id = active_machine_id):
if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id):
# An active machine was saved, so restore it.
self.setActiveMachine(active_machine_id)
# Make sure _active_container_stack is properly initiated
@ -117,6 +120,10 @@ class MachineManager(QObject):
"The selected material is incompatible with the selected machine or configuration."),
title = catalog.i18nc("@info:title", "Incompatible Material"))
containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId)
if containers:
containers[0].nameChanged.connect(self._onMaterialNameChanged)
globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value)
activeMaterialChanged = pyqtSignal()
activeVariantChanged = pyqtSignal()
@ -167,7 +174,7 @@ class MachineManager(QObject):
if not self._global_container_stack:
return
containers = ContainerRegistry.getInstance().findInstanceContainers(type="variant", definition=self._global_container_stack.getBottom().getId(), name=hotend_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id)
if containers: # New material ID is known
extruder_manager = ExtruderManager.getInstance()
machine_id = self.activeMachineId
@ -179,10 +186,10 @@ class MachineManager(QObject):
break
if matching_extruder and matching_extruder.variant.getName() != hotend_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
self._auto_hotends_changed[str(index)] = containers[0].getId()
self._auto_hotends_changed[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.definition.getId(), hotend_id))
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
if not self._global_container_stack:
@ -192,7 +199,7 @@ class MachineManager(QObject):
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
definition_id = self.activeQualityDefinitionId
extruder_manager = ExtruderManager.getInstance()
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", definition = definition_id, GUID = material_id)
if containers: # New material ID is known
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
matching_extruder = None
@ -203,15 +210,15 @@ class MachineManager(QObject):
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
if self._global_container_stack.getBottom().getMetaDataEntry("has_variants") and matching_extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.getBottom(), matching_extruder.variant)
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_extruder.variant)
for container in containers:
if container.getMetaDataEntry("variant") == variant_id:
self._auto_materials_changed[str(index)] = container.getId()
if container.get("variant") == variant_id:
self._auto_materials_changed[str(index)] = container["id"]
break
else:
# Just use the first result we found.
self._auto_materials_changed[str(index)] = containers[0].getId()
self._auto_materials_changed[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
@ -329,14 +336,24 @@ class MachineManager(QObject):
# on _active_container_stack. If it changes, then the properties change.
self.activeQualityChanged.emit()
def __onInstanceContainersChanged(self):
def __emitChangedSignals(self):
self.activeQualityChanged.emit()
self.activeVariantChanged.emit()
self.activeMaterialChanged.emit()
self._updateStacksHaveErrors() # Prevents unwanted re-slices after changing machine
self._error_check_timer.start()
def _onProfilesModelChanged(self, *args):
self.__emitChangedSignals()
def _onInstanceContainersChanged(self, container):
# This should not trigger the ProfilesModel to be created, or there will be an infinite recursion
if not self._connected_to_profiles_model and ProfilesModel.hasInstance():
# This triggers updating the qualityModel in SidebarSimple whenever ProfilesModel is updated
Logger.log("d", "Connecting profiles model...")
ProfilesModel.getInstance().itemsChanged.connect(self._onProfilesModelChanged)
self._connected_to_profiles_model = True
self._instance_container_timer.start()
def _onPropertyChanged(self, key: str, property_name: str):
@ -352,11 +369,13 @@ class MachineManager(QObject):
self.blurSettings.emit() # Ensure no-one has focus.
self._cancelDelayedActiveContainerStackChanges()
containers = ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
container_registry = ContainerRegistry.getInstance()
containers = container_registry.findContainerStacks(id = stack_id)
if containers:
Application.getInstance().setGlobalContainerStack(containers[0])
self.__onInstanceContainersChanged()
self.__emitChangedSignals()
@pyqtSlot(str, str)
def addMachine(self, name: str, definition_id: str) -> None:
@ -498,6 +517,7 @@ class MachineManager(QObject):
@pyqtProperty("QVariantList", notify=activeVariantChanged)
def activeVariantNames(self) -> List[str]:
result = []
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks is not None:
for stack in active_stacks:
@ -510,6 +530,7 @@ class MachineManager(QObject):
@pyqtProperty("QVariantList", notify = activeMaterialChanged)
def activeMaterialNames(self) -> List[str]:
result = []
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
if active_stacks is not None:
for stack in active_stacks:
@ -530,6 +551,7 @@ class MachineManager(QObject):
@pyqtProperty("QVariantMap", notify = activeVariantChanged)
def allActiveVariantIds(self) -> Dict[str, str]:
result = {}
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
if active_stacks is not None: #If we have a global stack.
for stack in active_stacks:
@ -548,10 +570,8 @@ class MachineManager(QObject):
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
def allActiveMaterialIds(self) -> Dict[str, str]:
result = {}
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
result[self._global_container_stack.getId()] = self._global_container_stack.material.getId()
if active_stacks is not None: # If we have extruder stacks
for stack in active_stacks:
material_container = stack.material
@ -703,10 +723,7 @@ class MachineManager(QObject):
## Check if a container is read_only
@pyqtSlot(str, result = bool)
def isReadOnly(self, container_id: str) -> bool:
containers = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not containers or not self._active_container_stack:
return True
return containers[0].isReadOnly()
return ContainerRegistry.getInstance().isReadOnly(container_id)
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
@pyqtSlot(str)
@ -733,6 +750,9 @@ class MachineManager(QObject):
old_material = self._active_container_stack.material
old_quality = self._active_container_stack.quality
old_quality_type = None
if old_quality and old_quality.getId() != self._empty_quality_container.getId():
old_quality_type = old_quality.getMetaDataEntry("quality_type")
old_quality_changes = self._active_container_stack.qualityChanges
if not old_material:
Logger.log("w", "While trying to set the active material, no material was found to replace it.")
@ -771,15 +791,30 @@ class MachineManager(QObject):
if quality_type:
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
quality_manager.getWholeMachineDefinition(global_stack.definition),
[material_container])
[material_container.getMetaData()])
if not candidate_quality or isinstance(candidate_quality, type(self._empty_quality_changes_container)):
if not candidate_quality or candidate_quality.getId() == self._empty_quality_changes_container:
Logger.log("d", "Attempting to find fallback quality")
# Fall back to a quality (which must be compatible with all other extruders)
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders(
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks())
if new_qualities:
new_quality_id = new_qualities[0].getId() # Just pick the first available one
quality_types = sorted([q.getMetaDataEntry("quality_type") for q in new_qualities], reverse = True)
quality_type_to_use = None
if quality_types:
# try to use the same quality as before, otherwise the first one in the quality_types
quality_type_to_use = quality_types[0]
if old_quality_type is not None and old_quality_type in quality_type_to_use:
quality_type_to_use = old_quality_type
new_quality = None
for q in new_qualities:
if quality_type_to_use is not None and q.getMetaDataEntry("quality_type") == quality_type_to_use:
new_quality = q
break
if new_quality is not None:
new_quality_id = new_quality.getId() # Just pick the first available one
else:
Logger.log("w", "No quality profile found that matches the current machine and extruders.")
else:
@ -804,7 +839,7 @@ class MachineManager(QObject):
preferred_material_name = None
if old_material:
preferred_material_name = old_material.getName()
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.getBottom(), self._global_container_stack, containers[0], preferred_material_name).id
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.definition, self._global_container_stack, containers[0], preferred_material_name).id
self.setActiveMaterial(preferred_material_id)
else:
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
@ -816,15 +851,15 @@ class MachineManager(QObject):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.blurSettings.emit()
containers = ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id)
if not containers or not self._global_container_stack:
return
# Quality profile come in two flavours: type=quality and type=quality_changes
# If we found a quality_changes profile then look up its parent quality profile.
container_type = containers[0].getMetaDataEntry("type")
quality_name = containers[0].getName()
quality_type = containers[0].getMetaDataEntry("quality_type")
container_type = containers[0].get("type")
quality_name = containers[0]["name"]
quality_type = containers[0].get("quality_type")
# Get quality container and optionally the quality_changes container.
if container_type == "quality":
@ -832,7 +867,7 @@ class MachineManager(QObject):
elif container_type == "quality_changes":
new_quality_settings_list = self._determineQualityAndQualityChangesForQualityChanges(quality_name)
else:
Logger.log("e", "Tried to set quality to a container that is not of the right type")
Logger.log("e", "Tried to set quality to a container that is not of the right type: {container_id}".format(container_id = containers[0]["id"]))
return
# Check if it was at all possible to find new settings
@ -921,18 +956,18 @@ class MachineManager(QObject):
if not global_container_stack:
return []
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
# find qualities for extruders
for extruder_stack in extruder_stacks:
material = extruder_stack.material
material_metadata = extruder_stack.material.getMetaData()
# TODO: fix this
if self._new_material_container and extruder_stack.getId() == self._active_container_stack.getId():
material = self._new_material_container
material_metadata = self._new_material_container.getMetaData()
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material_metadata])
if not quality:
# No quality profile is found for this quality type.
@ -982,12 +1017,6 @@ class MachineManager(QObject):
Logger.log("e", "Could not find the global quality changes container with name %s", quality_changes_name)
return None
material = global_container_stack.material
# find a quality type that matches both machine and materials
if self._new_material_container and self._active_container_stack.getId() == global_container_stack.getId():
material = self._new_material_container
# For the global stack, find a quality which matches the quality_type in
# the quality changes profile and also satisfies any material constraints.
quality_type = global_quality_changes.getMetaDataEntry("quality_type")
@ -1007,12 +1036,12 @@ class MachineManager(QObject):
if not quality_changes:
quality_changes = self._empty_quality_changes_container
material = extruder_stack.material
material_metadata = extruder_stack.material.getMetaData()
if self._new_material_container and self._active_container_stack.getId() == extruder_stack.getId():
material = self._new_material_container
material_metadata = self._new_material_container.getMetaData()
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material_metadata])
if not quality:
# No quality profile found for this quality type.
@ -1025,7 +1054,7 @@ class MachineManager(QObject):
})
# append the global quality changes
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material], global_quality = "True")
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, global_quality = "True")
# if there is not global quality but we're using a single extrusion machine, copy the quality of the first extruder - CURA-4482
if not global_quality and len(extruder_stacks) == 1:
@ -1079,18 +1108,14 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = globalContainerChanged)
def activeDefinitionId(self) -> str:
if self._global_container_stack:
definition = self._global_container_stack.getBottom()
if definition:
return definition.id
return self._global_container_stack.definition.id
return ""
@pyqtProperty(str, notify=globalContainerChanged)
def activeDefinitionName(self) -> str:
if self._global_container_stack:
definition = self._global_container_stack.getBottom()
if definition:
return definition.getName()
return self._global_container_stack.definition.getName()
return ""
@ -1100,7 +1125,7 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = globalContainerChanged)
def activeQualityDefinitionId(self) -> str:
if self._global_container_stack:
return self.getQualityDefinitionId(self._global_container_stack.getBottom())
return self.getQualityDefinitionId(self._global_container_stack.definition)
return ""
## Get the Definition ID to use to select quality profiles for machines of the specified definition
@ -1118,7 +1143,7 @@ class MachineManager(QObject):
if self._active_container_stack:
variant = self._active_container_stack.variant
if variant:
return self.getQualityVariantId(self._global_container_stack.getBottom(), variant)
return self.getQualityVariantId(self._global_container_stack.definition, variant)
return ""
## Get the Variant ID to use to select quality profiles for variants of the specified definitions
@ -1142,7 +1167,7 @@ class MachineManager(QObject):
def activeDefinitionVariantsName(self) -> str:
fallback_title = catalog.i18nc("@label", "Nozzle")
if self._global_container_stack:
return self._global_container_stack.getBottom().getMetaDataEntry("variants_name", fallback_title)
return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title)
return fallback_title
@ -1151,7 +1176,7 @@ class MachineManager(QObject):
container_registry = ContainerRegistry.getInstance()
machine_stack = container_registry.findContainerStacks(id = machine_id)
if machine_stack:
new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].getBottom().getName())
new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].definition.getName())
machine_stack[0].setName(new_name)
self.globalContainerChanged.emit()
@ -1162,15 +1187,15 @@ class MachineManager(QObject):
# activate a new machine before removing a machine because this is safer
if activate_new_machine:
machine_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
other_machine_stacks = [s for s in machine_stacks if s.getId() != machine_id]
machine_stacks = ContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
if other_machine_stacks:
self.setActiveMachine(other_machine_stacks[0].getId())
self.setActiveMachine(other_machine_stacks[0]["id"])
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", machine = machine_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
for container in containers:
ContainerRegistry.getInstance().removeContainer(container.getId())
ContainerRegistry.getInstance().removeContainer(container["id"])
ContainerRegistry.getInstance().removeContainer(machine_id)
@pyqtProperty(bool, notify = globalContainerChanged)
@ -1207,9 +1232,9 @@ class MachineManager(QObject):
# \returns DefinitionID (string) if found, None otherwise
@pyqtSlot(str, result = str)
def getDefinitionByMachineId(self, machine_id: str) -> str:
containers = ContainerRegistry.getInstance().findContainerStacks(id=machine_id)
containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
if containers:
return containers[0].getBottom().getId()
return containers[0].definition.getId()
@staticmethod
def createMachineManager():

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, List
from UM.Settings.ContainerRegistry import ContainerRegistry #To listen for changes to the materials.
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel #We're extending this class.
@ -18,8 +19,19 @@ class MaterialsModel(InstanceContainersModel):
# \param container The container whose metadata was changed.
def _onContainerMetaDataChanged(self, container):
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
self._update()
self._container_change_timer.start()
def _onContainerChanged(self, container):
if container.getMetaDataEntry("type", "") == "material":
super()._onContainerChanged(container)
## Group brand together
def _sortKey(self, item) -> List[Any]:
result = []
result.append(item["metadata"]["brand"])
result.append(item["metadata"]["material"])
result.append(item["metadata"]["name"])
result.append(item["metadata"]["color_name"])
result.append(item["metadata"]["id"])
result.extend(super()._sortKey(item))
return result

View file

@ -49,6 +49,10 @@ class ProfilesModel(InstanceContainersModel):
ProfilesModel.__instance = cls()
return ProfilesModel.__instance
@classmethod
def hasInstance(cls) -> bool:
return ProfilesModel.__instance is not None
__instance = None # type: "ProfilesModel"
## Fetch the list of containers to display.
@ -57,8 +61,7 @@ class ProfilesModel(InstanceContainersModel):
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return []
return {}, {}
global_stack_definition = global_container_stack.definition
# Get the list of extruders and place the selected extruder at the front of the list.
@ -88,11 +91,10 @@ class ProfilesModel(InstanceContainersModel):
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
result.append(not_supported_container)
return result
return {item.getId():item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
## Re-computes the items in this model, and adds the layer height role.
def _recomputeItems(self):
# Some globals that we can re-use.
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
@ -112,8 +114,12 @@ class ProfilesModel(InstanceContainersModel):
# active machine and material, and later yield the right ones.
tmp_all_quality_items = OrderedDict()
for item in super()._recomputeItems():
profile = container_registry.findContainers(id=item["id"])
quality_type = profile[0].getMetaDataEntry("quality_type") if profile else ""
profiles = container_registry.findContainersMetadata(id = item["id"])
if not profiles or "quality_type" not in profiles[0]:
quality_type = ""
else:
quality_type = profiles[0]["quality_type"]
if quality_type not in tmp_all_quality_items:
tmp_all_quality_items[quality_type] = {"suitable_container": None, "all_containers": []}

View file

@ -18,7 +18,7 @@ class QualityAndUserProfilesModel(ProfilesModel):
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
return []
return {}, {}
# Fetch the list of quality changes.
quality_manager = QualityManager.getInstance()
@ -35,10 +35,12 @@ class QualityAndUserProfilesModel(ProfilesModel):
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
filtered_quality_changes = [qc for qc in quality_changes_list if
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
return quality_list + filtered_quality_changes
result = filtered_quality_changes
result.update({q.getId():q for q in quality_list})
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.

View file

@ -92,7 +92,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
items = []
settings = collections.OrderedDict()
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
containers = self._container_registry.findInstanceContainers(id = self._quality_id)

View file

@ -22,16 +22,26 @@ class SettingOverrideDecorator(SceneNodeDecorator):
## Event indicating that the user selected a different extruder.
activeExtruderChanged = Signal()
## Non-printing meshes
#
# If these settings are True for any mesh, the mesh does not need a convex hull,
# and is sent to the slicer regardless of whether it fits inside the build volume.
# Note that Support Mesh is not in here because it actually generates
# g-code in the volume of the mesh.
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
def __init__(self):
super().__init__()
self._stack = PerObjectContainerStack(stack_id = id(self))
self._stack = PerObjectContainerStack(stack_id = "per_object_stack_" + str(id(self)))
self._stack.setDirty(False) # This stack does not need to be saved.
self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer"))
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
self._is_non_printing_mesh = False
self._stack.propertyChanged.connect(self._onSettingChanged)
ContainerRegistry.getInstance().addContainer(self._stack)
Application.getInstance().getContainerRegistry().addContainer(self._stack)
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
self.activeExtruderChanged.connect(self._updateNextStack)
@ -49,6 +59,10 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# Properly set the right extruder on the copy
deep_copy.setActiveExtruder(self._extruder_stack)
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet.
deep_copy._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
return deep_copy
## Gets the currently active extruder to print this object with.
@ -72,9 +86,14 @@ class SettingOverrideDecorator(SceneNodeDecorator):
container_stack = containers[0]
return container_stack.getMetaDataEntry("position", default=None)
def isNonPrintingMesh(self):
return self._is_non_printing_mesh
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
# Trigger slice/need slicing if the value has changed.
if property_name == "value":
self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()

View file

@ -1,7 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Application import Application
from cura.QualityManager import QualityManager
from cura.Settings.ProfilesModel import ProfilesModel
from cura.Settings.ExtruderManager import ExtruderManager
@ -12,13 +12,23 @@ class UserProfilesModel(ProfilesModel):
def __init__(self, parent = None):
super().__init__(parent)
#Need to connect to the metaDataChanged signal of the active materials.
self.__current_extruders = []
self.__current_materials = []
Application.getInstance().getExtruderManager().extrudersChanged.connect(self.__onExtrudersChanged)
self.__onExtrudersChanged()
self.__current_materials = [extruder.material for extruder in self.__current_extruders]
for material in self.__current_materials:
material.metaDataChanged.connect(self._onContainerChanged)
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
def _fetchInstanceContainers(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
return []
return {}, {}
# Fetch the list of quality changes.
quality_manager = QualityManager.getInstance()
@ -35,10 +45,36 @@ class UserProfilesModel(ProfilesModel):
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
filtered_quality_changes = [qc for qc in quality_changes_list if
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
return filtered_quality_changes
return filtered_quality_changes, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
## Called when a container changed on an extruder stack.
#
# If it's the material we need to connect to the metaDataChanged signal of
# that.
def __onContainerChanged(self, new_container):
#Careful not to update when a quality or quality changes profile changed!
#If you then update you're going to have an infinite recursion because the update may change the container.
if new_container.getMetaDataEntry("type") == "material":
for material in self.__current_materials:
material.metaDataChanged.disconnect(self._onContainerChanged)
self.__current_materials = [extruder.material for extruder in self.__current_extruders]
for material in self.__current_materials:
material.metaDataChanged.connect(self._onContainerChanged)
## Called when the current set of extruders change.
#
# This makes sure that we are listening to the signal for when the
# materials change.
def __onExtrudersChanged(self):
for extruder in self.__current_extruders:
extruder.containersChanged.disconnect(self.__onContainerChanged)
self.__current_extruders = Application.getInstance().getExtruderManager().getExtruderStacks()
for extruder in self.__current_extruders:
extruder.containersChanged.connect(self.__onContainerChanged)

22
cura/Stages/CuraStage.py Normal file
View file

@ -0,0 +1,22 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, QUrl, QObject
from UM.Stage import Stage
class CuraStage(Stage):
def __init__(self, parent = None):
super().__init__(parent)
@pyqtProperty(str, constant = True)
def stageId(self):
return self.getPluginId()
@pyqtProperty(QUrl, constant = True)
def mainComponent(self):
return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True)
def sidebarComponent(self):
return self.getDisplayComponent("sidebar")

2
cura/Stages/__init__.py Normal file
View file

@ -0,0 +1,2 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

View file

@ -2,17 +2,45 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import argparse
import os
import sys
import platform
import faulthandler
from UM.Platform import Platform
parser = argparse.ArgumentParser(prog = "cura",
add_help = False)
parser.add_argument('--debug',
action='store_true',
default = False,
help = "Turn on the debug mode by setting this option."
)
known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]:
def get_cura_dir_path():
if Platform.isWindows():
return os.path.expanduser("~/AppData/Roaming/cura/")
elif Platform.isLinux():
return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
import platform
import faulthandler
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
if platform.linux_distribution()[0] in ("debian", "Ubuntu", "LinuxMint"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
linux_distro_name = platform.linux_distribution()[0].lower()
if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes
from ctypes.util import find_library
libGL = find_library("GL")
@ -22,9 +50,10 @@ if Platform.isLinux(): # Needed for platform.linux_distribution, which is not av
if Platform.isWindows() and hasattr(sys, "frozen"):
try:
del os.environ["PYTHONPATH"]
except KeyError: pass
except KeyError:
pass
#WORKAROUND: GITHUB-704 GITHUB-708
# WORKAROUND: GITHUB-704 GITHUB-708
# It looks like setuptools creates a .pth file in
# the default /usr/lib which causes the default site-packages
# to be inserted into sys.path before PYTHONPATH.
@ -45,7 +74,8 @@ def exceptHook(hook_type, value, traceback):
_crash_handler = CrashHandler(hook_type, value, traceback)
_crash_handler.show()
sys.excepthook = exceptHook
if not known_args["debug"]:
sys.excepthook = exceptHook
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
@ -55,29 +85,14 @@ import Arcus #@UnusedImport
import cura.CuraApplication
import cura.Settings.CuraContainerRegistry
def get_cura_dir_path():
if Platform.isWindows():
return os.path.expanduser("~/AppData/Local/cura/")
elif Platform.isLinux():
return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
faulthandler.enable()
# Force an instance of CuraContainerRegistry to be created and reused later.
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
# This prestart up check is needed to determine if we should start the application at all.
if not cura.CuraApplication.CuraApplication.preStartUp():
# This pre-start up check is needed to determine if we should start the application at all.
if not cura.CuraApplication.CuraApplication.preStartUp(parser = parser, parsed_command_line = known_args):
sys.exit(0)
app = cura.CuraApplication.CuraApplication.getInstance()
app = cura.CuraApplication.CuraApplication.getInstance(parser = parser, parsed_command_line = known_args)
app.run()

View file

@ -126,11 +126,7 @@ Section "Install Arduino Drivers"
SectionEnd
Section "Open STL files with Cura"
WriteRegStr HKCR .stl "" "Cura STL model file"
DeleteRegValue HKCR .stl "Content Type"
WriteRegStr HKCR "Cura STL model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
WriteRegStr HKCR "Cura STL model file\shell" "" "open"
WriteRegStr HKCR "Cura STL model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
${registerExtension} "$INSTDIR\Cura.exe" ".stl" "STL_File"
SectionEnd
Section /o "Open OBJ files with Cura"

View file

@ -117,7 +117,7 @@ class ThreeMFReader(MeshReader):
# Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
um_node.callDecoration("getStack").getTop().setDefinition(definition)
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
setting_container = um_node.callDecoration("getStack").getTop()

View file

@ -122,7 +122,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
return WorkspaceReader.PreReadResult.failed
machine_name = ""
machine_type = ""
variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
@ -133,9 +132,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# A few lists of containers in this project files.
# When loading the global stack file, it may be associated with those containers, which may or may not be
# in Cura already, so we need to provide them as alternative search lists.
definition_container_list = []
instance_container_list = []
material_container_list = []
resolve_strategy_keys = ["machine", "material", "quality_changes"]
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
@ -149,21 +146,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
for each_definition_container_file in definition_container_files:
container_id = self._stripFileToId(each_definition_container_file)
definitions = self._container_registry.findDefinitionContainers(id=container_id)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"),
file_name = each_definition_container_file)
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"), file_name = each_definition_container_file)
definition_container = definition_container.getMetaData()
else:
definition_container = definitions[0]
definition_container_list.append(definition_container)
definition_container_type = definition_container.getMetaDataEntry("type")
definition_container_type = definition_container.get("type")
if definition_container_type == "machine":
machine_type = definition_container.getName()
variant_type_name = definition_container.getMetaDataEntry("variants_name", variant_type_name)
machine_type = definition_container["name"]
variant_type_name = definition_container.get("variants_name", variant_type_name)
machine_definition_container_count += 1
elif definition_container_type == "extruder":
@ -187,11 +183,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
container_id = self._stripFileToId(material_container_file)
materials = self._container_registry.findInstanceContainers(id=container_id)
material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
if materials:
if self._container_registry.findContainersMetadata(id = container_id): #This material already exists.
containers_found_dict["material"] = True
if not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
material_conflict = True
Job.yieldThread()
@ -449,6 +444,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stacks = []
extruder_stacks_added = []
container_stacks_added = []
machine_extruder_count = None
containers_added = []
@ -461,16 +457,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack_id_map = {} # new and old ExtruderStack IDs map
if self._resolve_strategies["machine"] == "new":
# We need a new id if the id already exists
if self._container_registry.findContainerStacks(id = global_stack_id_original):
if self._container_registry.findContainerStacksMetadata(id = global_stack_id_original):
global_stack_id_new = self.getNewId(global_stack_id_original)
global_stack_need_rename = True
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original)
if self._container_registry.findContainerStacksMetadata(name = global_stack_id_original):
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original)
for each_extruder_stack_file in extruder_stack_files:
old_container_id = self._stripFileToId(each_extruder_stack_file)
new_container_id = old_container_id
if self._container_registry.findContainerStacks(id = old_container_id):
if self._container_registry.findContainerStacksMetadata(id = old_container_id):
# get a new name for this extruder
new_container_id = self.getNewId(old_container_id)
@ -484,7 +481,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
for definition_container_file in definition_container_files:
container_id = self._stripFileToId(definition_container_file)
definitions = self._container_registry.findDefinitionContainers(id = container_id)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
@ -511,7 +508,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
containers_to_add.append(material_container)
else:
material_container = materials[0]
if not material_container.isReadOnly(): # Only create new materials if they are not read only.
if not self._container_registry.isReadOnly(container_id): # Only create new materials if they are not read only.
if self._resolve_strategies["material"] == "override":
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = material_container_file)
@ -578,7 +575,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if old_extruder_id:
new_extruder_id = extruder_stack_id_map[old_extruder_id]
new_id = new_extruder_id + "_current_settings"
instance_container._id = new_id
instance_container.setMetaDataEntry("id", new_id)
instance_container.setName(new_id)
instance_container.setMetaDataEntry("extruder", new_extruder_id)
containers_to_add.append(instance_container)
@ -587,7 +584,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_id:
new_machine_id = self.getNewId(machine_id)
new_id = new_machine_id + "_current_settings"
instance_container._id = new_id
instance_container.setMetaDataEntry("id", new_id)
instance_container.setName(new_id)
instance_container.setMetaDataEntry("machine", new_machine_id)
containers_to_add.append(instance_container)
@ -636,8 +633,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The ID already exists, but nothing in the values changed, so do nothing.
pass
quality_and_definition_changes_instance_containers.append(instance_container)
if container_type == "definition_changes":
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
if definition_changes_extruder_count is not None:
machine_extruder_count = definition_changes_extruder_count
else:
existing_container = self._container_registry.findInstanceContainers(id = container_id)
existing_container = self._container_registry.findInstanceContainersMetadata(id = container_id)
if not existing_container:
containers_to_add.append(instance_container)
if global_stack_need_rename:
@ -663,14 +666,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# HACK
# There is a machine, check if it has authentication data. If so, keep that data.
network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"),
file_name = global_stack_file)
network_authentication_id = stack.getMetaDataEntry("network_authentication_id")
network_authentication_key = stack.getMetaDataEntry("network_authentication_key")
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"), file_name = global_stack_file)
if network_authentication_id:
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
stack.addMetaDataEntry("network_authentication_id", network_authentication_id)
if network_authentication_key:
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
stack.addMetaDataEntry("network_authentication_key", network_authentication_key)
elif self._resolve_strategies["machine"] == "new":
# create a new global stack
@ -794,8 +796,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if stack.quality.getId() in ("empty", "empty_quality"):
has_not_supported = True
break
# We filter out extruder stacks that are not actually used, for example the UM3 and custom FDM printer extruder count setting.
extruder_stacks_in_use = extruder_stacks
if machine_extruder_count is not None:
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
extruder_stacks)
extruder_stacks_in_use)
if not has_not_supported:
has_not_supported = not available_quality
@ -803,10 +811,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if has_not_supported:
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
for stack in [global_stack] + extruder_stacks:
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
for stack in [global_stack] + extruder_stacks:
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True
@ -827,6 +835,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
preferred_quality_id = global_stack.getMetaDataEntry("preferred_quality", None)
if preferred_quality_id is not None:
definition_id = global_stack.definition.getId()
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
definition_id = "fdmprinter"
@ -838,7 +847,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack.quality = containers[0]
global_stack.qualityChanges = empty_quality_changes_container
# also find the quality containers for the extruders
for extruder_stack in extruder_stacks:
for extruder_stack in extruder_stacks_in_use:
search_criteria = {"id": preferred_quality_id,
"type": "quality",
"definition": definition_id}
@ -859,27 +868,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# be correct. For example, for UM2, the quality container can be "draft" while it should be "um2_draft"
# instead.
# In this code branch, we try to fix those incorrect quality containers.
search_criteria = {"type": "quality",
"quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
search_criteria["definition"] = global_stack.definition.getId()
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
search_criteria["definition"] = "fdmprinter"
#
# ***IMPORTANT***: We only do this fix for single-extrusion machines.
# We will first find the correct quality profile for the extruder, then apply the same
# quality profile for the global stack.
#
if len(extruder_stacks) == 1:
extruder_stack = extruder_stacks[0]
containers = self._container_registry.findInstanceContainers(**search_criteria)
containers = [c for c in containers if not c.getMetaDataEntry("material", "")]
if not containers:
# cannot find machine-specific qualities, so just use fdmprinter to search again
search_criteria["definition"] = "fdmprinter"
containers = self._container_registry.findInstanceContainers(**search_criteria)
containers = [c for c in containers if not c.getMetaDataEntry("material", "")]
if containers:
new_quality_container = containers[0]
global_stack.quality = new_quality_container
for extruder_stack in extruder_stacks:
search_criteria = {"type": "quality",
"quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
search_criteria = {"type": "quality", "quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
search_criteria["definition"] = global_stack.definition.getId()
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
search_criteria["definition"] = "fdmprinter"
@ -888,7 +885,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
extruder_stack.quality = containers[0]
new_quality_container = containers[0]
extruder_stack.quality = new_quality_container
global_stack.quality = new_quality_container
# Replacing the old containers if resolve is "new".
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those

View file

@ -1,12 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.FlameProfiler import pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry

View file

@ -59,7 +59,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser()
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getStaticVersion())
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(Application.getInstance().getIsDebugMode()))
version_file_string = StringIO()
version_config_parser.write(version_file_string)
@ -95,7 +97,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
# Do not include the network authentication keys
ignore_keys = ["network_authentication_id", "network_authentication_key"]
ignore_keys = {"network_authentication_id", "network_authentication_key"}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
archive.writestr(file_in_archive, serialized_data)

View file

@ -8,8 +8,7 @@ from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Version import Version
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
from PyQt5.QtCore import pyqtSlot, QObject
import os.path
import collections

View file

@ -1,3 +1,72 @@
[3.1.0]
*Profile added for 0.25 mm print core
This new print core gives extra fine line widths which gives prints extra definition and surface quality.
*Profile added for Breakaway material
New material profile for Breakaway material, a new dry post processing support material, which can be used for models with flat surface area overhangs.
*Layer view
The existing Layer View has been updated in order to see a live simulation of all the paths within a layer.
*Quick camera controls
New buttons have been added to the interface that can quickly reposition the camera view of the buildplate.
*Lock model on platform
The move tool has a new option to lock a selected model to the platform.
*Faster profile switching speed
Duplicating and removing a profile could take Ultimaker Cura quite some time, it now happens instantly.
*Faster printer selection
Removing a printer from the library is now instant. No more unresponsive screens.
*Faster processing speed
A 5 - 10 % speed increase when calculating normals, loading models, and slicing.
*Feedrate visualization
Feedrate visualization has been added to the Layer view. Using this gives the user an idea of the print speeds per model part, allowing for better control over prints.
*Jogging
It allows the printhead to be moved with on-screen controls. Contributed by fieldOfView.
*Large model loading
A new feature has been added which unloads the layer view when switching to solid mode, speeding Ultimaker Cura back up without losing your G-code/layer view information.
*Scripts folder
A scripts folder is now available in the Ultimaker Cura configuration folder. This folder can be loaded with post processing plugins scripts, which will automatically show in Ultimaker Cura. Contributed by fieldOfView.
*Optimized workflow for crash reporting
Crash reports are automatically generated and allow the user, in case of a crash, to easily send their report with a description to developers.
*Floating models enabled
In previous releases, models were dropped to the build plate when support was disabled. Models now float when the setting is enabled (even if creates an impossible-to-print situation). This can be used to stack separate models on top of each other.
*Slicing tolerance
A new setting that affects the intersect point to influence the dimensional accuracy for diagonal surfaces. The user can select the behaviour: Inclusive makes gaps narrower, Exclusive makes gaps wider, and Middle is the fastest to process. This can be used to create better tolerances for printed screw holes. Contributed by BagelOrb.
*Optimized zig zag patterns
Zig zag patterns now print more consistently. Lines now have a 5 micron tolerance in which they are printed any way, resulting in longer connected lines. Contributed by smartavionics.
*Aligned z-seam inner wall moves
Inner wall travel moves are aligned with the z-seam. This reduces the number of travel moves and reduces the chance of more unwanted seams.
*Relative positioning of infill patterns
Infill patterns are now positioned relative to the center of loaded models and an offset can be applied to control the infill more precisely and adjust it to preference or strength. Contributed by smartavionics.
*Line resolution
Enables the user to specify the minimum allowed distance value between two points in G-code to create lower or higher resolution polygons.
*Custom mode changes
If profile settings have been modified in recommended mode under custom mode, a reset icon will appear to notify the user. Click the icon to show the changes that have been made, and revert back to the default profile settings.
*Bugfixes
- Fix for layer numbers being displayed incorrectly when switching between solid and layer mode
- Fix for Ultimaker Cura engine crashes on certain models
- Fix for Uninstalling previous versions of Cura on Windows platforms
- Fix for displaying visible settings
- Fix for importing legacy .ini files
- Prevent skipping user agreement dialog by pressing escape
[3.0.4]
*Bug fixes
- Fixed OpenGL issue that prevents Cura from starting.
@ -40,7 +109,7 @@ The build plate now shows graduations of 10 mm and 1 mm for easy model positioni
Extruder tabs have become buttons and icons have been updated.
*Add an "Export to Cura" button in SOLIDWORKS
SOLIDWORKS plugin can now be installed using an automatic installer.
A macro can be added to your SOLIDWORKS installation that loads your model into Ultimaker Cura.
*Siemens NX macro
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
@ -571,10 +640,10 @@ The new GUI allows custom profiles to load easily and intuitively, directly from
*3MF File Loading Support
Were happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
*Intuitive Cut-Off Object Bottom
*Intuitive Cut-Off Object Bottom
Weve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
*64-bit Windows Builds
*64-bit Windows Builds
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
*Automatic calculations
@ -639,3 +708,165 @@ Prints the outer walls with a jittering motion to give your object a diffuse fin
*Wire Printing
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
[15.06 Beta]
Cura 15.06 is a new release built from the ground up on a completely new
framework called Uranium. This framework has been designed to make it easier to
extend Cura with additional functionality as well as provide a cleaner UI.
[15.05.95]
* Fixed: Selection ghost remains visible after deleting an object
* Fixed: Window does not show up immediately after starting application on OSX
* Fixed: Added display of rotation angle during rotation
* Fixed: Object changes position while rotating/scaling
* Fixed: Loading improvements in the layer view
* Fixed: Added application icons
* Fixed: Improved feedback when loading models
* Fixed: Eject device on MacOSX now provides proper feedback
* Fixed: Make it possible to show retraction settings for UM2
* Fixed: Opening the machine preferences page will switch to the first available machine
* Fixed: Improved tool handle hit area size
* Fixed: Render lines with a thickness based on screen DPI
[15.05.94]
* Added Russian translations
* Fixed: Infill not displayed in layer view
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
* Fixed: Improved font rendering on Windows
* Fixed: Help > Show Documentation crashes Cura on Windows
* Fixed: "There is no disk in the drive" repeating messages on Windows
* Fixed: Retraction settings not visible for Ultimaker2
* Fixed: Display rotation angle when rotating an object
* Fixed: Time/Quality slider values are properly rounded
* Fixed: Improved clarity of buttons and text
* Fixed: No indication that anything is happening when loading a model
* Fixed: Eject device now works on Windows
[15.05.93]
* Fixed: No shortcuts for moving up/down layers in layer view.
* Fixed: Last view layers could not be scrolled through in layer view.
* Fixed: Files provided on command line would not actually show up on the build
platform.
* Fixed: Render a ghost of the selection in Layer view to make the actual object
position clear.
* Fixed: Showing a menu would clear the selection.
* Fixed: Size and scaling factor display for scale tool.
* Fixed: Missing background for additional tool controls.
* Fixed: Loading message times out when loading large files.
* Fixed: Show recent files in the file menu.
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
prevent issues with missing DLL's.
* Fixed: Collapsed/expanded state of setting categories not stored.
[15.05.91]
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
higher.
* Fixed: Need to deselect before selecting a different object.
* Fixed: Object can be moved on Z axis.
* Fixed: Error values should be considered invalid values and will not trigger a
slice.
* Fixed: Text fields used a locale-aware validator while the underlying code did
not.
* Fixed: Text fields will trigger a slice on text change, not only after focus
change/enter press.
* Fixed: Rotate Tool snaps to incorrect value.
* Fixed: Object Collision would only moved objects to the right.
* Fixed: Object Collision would move the selected object when it should not.
* Fixed: Camera panning now works correctly instead of doing nothing.
* Fixed: Camera would flip around center point at maximum rotation.
* Fixed: Build platform grid blocked view from below objects.
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
window
[15.05.90]
* Fixed: Additional UI elements for tools and views not loading.
* Fixed: Double click needed to change setting dialog page.
* Fixed: Context menu entries (reload, center object, etc.) not working.
* Fixed: "Open With" or passing files from command line not working.
* Fixed: "Reload All" would not reload files.
In addition, a lot of work has gone into getting a usable Mac OSX version.
New Features
------------
* Plugin based system
The Uranium framework provides us with a plugin-based system
that provides additional flexibility when extending Cura. Think
of new views, tools, file formats, etc. This is probably the
biggest new feature.
* Improved UI
The UI has received a complete overhaul.
* Time-Quality Slider
The 4 static quick print profiles have been replaced with
a slider that should make it easier to find the right spot
between print time and print quality.
* More Settings
The Advanced mode is now configurable and can show many
additional settings that were previously not available, while at
the same time not overwhelming new users with too many settings.
Custom set of visible settings can be created by the user.
* Support for high-DPI screens
The refreshed UI has been designed with high-DPI screens in
mind which should improve the experience of Cura on such
devices.
* Improved language support
(Not yet available for the Beta release.)
* Improved support structure generation
The new version of the CuraEngine now features improved
support generation algorithms and additional options for support
structure generation.
* Experimental Feature: Wire Printing
Wire Printing has been added as an experimental new feature. It
will print objects as a structure of lines. It can be enabled by
from Advanced Mode -> Fixes -> Wire Printing.
* Undo/Redo
It is now possible to undo and redo most scene operations, like
moving or rotating objects.
Features from earlier versions not (yet) in this release
--------------------------------------------------------
* The All-at-once/One-at-a-time toggle is not available.
We are working on an improved implementation of this mechanism
but it will not be available for this release.
* No dual extrusion features are available yet.
We are working on a completely new workflow for this but this
needs additional time.
* “Lay Flat” has been removed.
The existing implementation was unfortunately not salvageable.
We will be looking into an improved implementation for this
feature.
* "Split Object Into Parts" has been removed.
Due to the same reason as Lay Flat.
* Support for AMF and DAE file formats has been removed.
Both of these will be implemented as plugins in the future.
* Support for directly loading a GCode file is not yet available.
This will be implemented as a plugin in the future.
* Support for PNG, JPG and other image formats has been removed.
These can be supported by a plugin with an improved UI.
* Support for loading Minecraft levels has been removed.
This can be implemented as a plugin.
* Windows XP support has been dropped.
Microsoft is no longer supporting xp, so they no longer back
port certain features that we require.
* X-Ray view is missing.
Will be implemented as a (you might have guessed it) plugin.
* Fixes: Follow Mesh Surface
Has been removed from the engine, the same result can be
achieved using no infill or top/bottom layers.
Known Issues
------------
* Some OBJ files are rendered as black objects due to missing
normals.
* Disabling plugins does not work correctly yet.
* Unicorn occasionally still requires feeding. Do not feed it
after midnight.

View file

@ -86,6 +86,7 @@ class CuraEngineBackend(QObject, Backend):
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)

View file

@ -61,7 +61,9 @@ class ProcessSlicedLayersJob(Job):
def run(self):
start_time = time()
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "SimulationView":
view.resetLayerData()
self._progress_message.show()
Job.yieldThread()
if self._abort_requested:
@ -226,10 +228,6 @@ class ProcessSlicedLayersJob(Job):
if self._progress_message:
self._progress_message.setProgress(100)
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "SimulationView":
view.resetLayerData()
if self._progress_message:
self._progress_message.hide()

View file

@ -19,6 +19,10 @@ from UM.Settings.SettingRelation import RelationType
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
NON_PRINTING_MESH_SETTINGS = ["anti_overhang_mesh", "infill_mesh", "cutting_mesh"]
class StartJobResult(IntEnum):
Finished = 1
Error = 2
@ -45,14 +49,6 @@ class GcodeStartEndFormatter(Formatter):
## Job class that builds up the message of scene data to send to CuraEngine.
class StartSliceJob(Job):
## Meshes that are sent to the engine regardless of being outside of the
# build volume.
#
# If these settings are True for any mesh, the build volume is ignored.
# Note that Support Mesh is not in here because it actually generates
# g-code in the volume of the mesh.
_not_printed_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
def __init__(self, slice_message):
super().__init__()
@ -139,13 +135,25 @@ class StartSliceJob(Job):
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
else:
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False)\
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
if node.callDecoration("isSliceable") and type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
if not getattr(node, "_outside_buildarea", False) or not is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
Job.yieldThread()
#If the list doesn't have any model with suitable settings then clean the list
# otherwise CuraEngine will crash
if not has_printing_mesh:
temp_list.clear()
if temp_list:
object_groups.append(temp_list)
@ -205,32 +213,53 @@ class StartSliceJob(Job):
def isCancelled(self):
return self._is_cancelled
def _expandGcodeTokens(self, key, value, settings):
## Creates a dictionary of tokens to replace in g-code pieces.
#
# This indicates what should be replaced in the start and end g-codes.
# \param stack The stack to get the settings from to replace the tokens
# with.
# \return A dictionary of replacement tokens to the values they should be
# replaced with.
def _buildReplacementTokens(self, stack) -> dict:
result = {}
for key in stack.getAllKeys():
result[key] = stack.getProperty(key, "value")
Job.yieldThread()
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
result["print_temperature"] = result["material_print_temperature"]
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
return result
## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in.
# \param settings A dictionary of tokens to replace and their respective
# replacement strings.
def _expandGcodeTokens(self, value: str, settings: dict):
try:
# any setting can be used as a token
fmt = GcodeStartEndFormatter()
return str(fmt.format(value, **settings)).encode("utf-8")
return str(fmt.format(value, **settings))
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
return str(value).encode("utf-8")
return str(value)
## Create extruder message from stack
def _buildExtruderMessage(self, stack):
message = self._slice_message.addRepeatedMessage("extruders")
message.id = int(stack.getMetaDataEntry("position"))
material_instance_container = stack.findContainer({"type": "material"})
settings = self._buildReplacementTokens(stack)
settings = {}
for key in stack.getAllKeys():
settings[key] = stack.getProperty(key, "value")
Job.yieldThread()
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
settings["print_bed_temperature"] = settings["material_bed_temperature"] #Renamed settings.
settings["print_temperature"] = settings["material_print_temperature"]
settings["time"] = time.strftime("%H:%M:%S") #Some extra settings.
settings["date"] = time.strftime("%d-%m-%Y")
settings["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
# Replace the setting tokens in start and end g-code.
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
for key, value in settings.items():
# Do not send settings that are not settable_per_extruder.
@ -238,13 +267,7 @@ class StartSliceJob(Job):
continue
setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key
if key == "material_guid" and material_instance_container:
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
elif key == "machine_extruder_start_code" or key == "machine_extruder_end_code":
setting.value = self._expandGcodeTokens(key, value, settings)
else:
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
setting.value = str(value).encode("utf-8")
Job.yieldThread()
## Sends all global settings to the engine.
@ -252,33 +275,28 @@ class StartSliceJob(Job):
# The settings are taken from the global stack. This does not include any
# per-extruder settings or per-object settings.
def _buildGlobalSettingsMessage(self, stack):
keys = stack.getAllKeys()
settings = {}
for key in keys:
settings[key] = stack.getProperty(key, "value")
Job.yieldThread()
settings = self._buildReplacementTokens(stack)
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
settings["print_bed_temperature"] = settings["material_bed_temperature"]
settings["print_temperature"] = settings["material_print_temperature"]
# Find the correct temperatures from the first used extruder
extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
settings["time"] = time.strftime('%H:%M:%S')
settings["date"] = time.strftime('%d-%m-%Y')
settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
# Replace the setting tokens in start and end g-code.
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings)
for key, value in settings.items(): #Add all submessages for each individual setting.
# Add all sub-messages for each individual setting.
for key, value in settings.items():
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
setting_message.name = key
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
setting_message.value = self._expandGcodeTokens(key, value, settings)
else:
setting_message.value = str(value).encode("utf-8")
setting_message.value = str(value).encode("utf-8")
Job.yieldThread()
## Sends for some settings which extruder they should fallback to if not
@ -348,3 +366,4 @@ class StartSliceJob(Job):
relations_set.add(relation.target.key)
self._addRelations(relations_set, relation.target.relations)

View file

@ -0,0 +1,462 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
# This parser is intented for interpret the common firmware codes among all the different flavors
class FlavorParser:
def __init__(self):
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._filament_diameter = 2.85
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
@staticmethod
def _getValue(line, code):
n = line.find(code)
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[5] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
extrusion_values = numpy.empty((count, 1), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
extrusion_values[i] = point[4]
if i > 0:
line_feedrates[i - 1] = point[3]
line_types[i - 1] = point[5]
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
else:
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _createEmptyLayer(self, layer_number):
self._layer_data_builder.addLayer(layer_number)
self._layer_data_builder.setLayerHeight(layer_number, 0)
self._layer_data_builder.setLayerThickness(layer_number, 0)
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
# Area of the filament
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
# Length of the extruded filament
de = current_extrusion - previous_extrusion
# Volumne of the extruded filament
dVe = de * Af
# Length of the printed line
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
# When the extruder recovers from a retraction, we get zero distance
if dX == 0:
return 0.1
# Area of the printed line. This area is a rectangle
Ae = dVe / dX
# This area is a rectangle with area equal to layer_thickness * layer_width
line_width = Ae / layer_thickness
# A threshold is set to avoid weird paths in the GCode
if line_width > 1.2:
return 0.35
return line_width
def _gCode0(self, position, params, path):
x, y, z, f, e = position
if self._is_absolute_positioning:
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else z
else:
x += params.x if params.x is not None else 0
y += params.y if params.y is not None else 0
z += params.z if params.z is not None else 0
f = params.f if params.f is not None else f
if params.e is not None:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
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,
params.z if params.z is not None else position.z,
position.f,
position.e)
## Set the absolute positioning
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
def _gCode91(self, position, params, path):
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, params, path):
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
position.e[self._extruder_number] = params.e
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,
params.z if params.z is not None else position.z,
params.f if params.f is not None else position.f,
position.e)
def processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, f, e = None, None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "F":
f = float(item[1:]) / 60
if item[0] == "E":
e = float(item[1:])
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
self._center_is_zero = True
params = self._position(x, y, z, f, e)
return func(position, params, path)
return position
def processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
def processMCode(self, M, line, position, path):
pass
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def processGCodeFile(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, 0, [0])
current_path = []
min_layer_number = 0
negative_layers = 0
previous_layer = 0
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
# as in ProcessSlicedLayersJob
if layer_number < min_layer_number:
min_layer_number = layer_number
if layer_number < 0:
layer_number += abs(min_layer_number)
negative_layers += 1
else:
layer_number += negative_layers
# In case there is a gap in the layer count, empty layers are created
for empty_layer in range(previous_layer + 1, layer_number):
self._createEmptyLayer(empty_layer)
self._layer_number = layer_number
previous_layer = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
# When find a movement, the new posistion is calculated and added to the current_path, but
# don't need to create a polygon until the end of the layer
current_position = self.processGCode(G, line, current_position, current_path)
continue
# When changing the extruder, the polygon with the stored paths is computed
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self.processTCode(T, line, current_position, current_path)
if line.startswith("M"):
M = self._getInt(line, "M")
self.processMCode(M, line, current_position, current_path)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -1,471 +1,44 @@
# Copyright (c) 2017 Aleph Objects, Inc.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.FileHandler.FileReader import FileReader
from UM.Mesh.MeshReader import MeshReader
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
from . import MarlinFlavorParser, RepRapFlavorParser
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
_flavor_default = "Marlin"
_flavor_keyword = ";FLAVOR:"
_flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(),
"Marlin" : MarlinFlavorParser.MarlinFlavorParser()}
def __init__(self):
super(GCodeReader, self).__init__()
self._supported_extensions = [".gcode", ".g"]
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
self._flavor_reader = None
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._filament_diameter = 2.85
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
# PreRead is used to get the correct flavor. If not, Marlin is set by default
def preRead(self, file_name, *args, **kwargs):
with open(file_name, "r") as file:
for line in file:
if line[:len(self._flavor_keyword)] == self._flavor_keyword:
try:
self._flavor_reader = self._flavor_readers_dict[line[len(self._flavor_keyword):].rstrip()]
return FileReader.PreReadResult.accepted
except:
# If there is no entry in the dictionary for this flavor, just skip and select the by-default flavor
break
@staticmethod
def _getValue(line, code):
n = line.find(code)
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[5] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
extrusion_values = numpy.empty((count, 1), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
extrusion_values[i] = point[4]
if i > 0:
line_feedrates[i - 1] = point[3]
line_types[i - 1] = point[5]
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
else:
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _createEmptyLayer(self, layer_number):
self._layer_data_builder.addLayer(layer_number)
self._layer_data_builder.setLayerHeight(layer_number, 0)
self._layer_data_builder.setLayerThickness(layer_number, 0)
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
# Area of the filament
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
# Length of the extruded filament
de = current_extrusion - previous_extrusion
# Volumne of the extruded filament
dVe = de * Af
# Length of the printed line
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
# When the extruder recovers from a retraction, we get zero distance
if dX == 0:
return 0.1
# Area of the printed line. This area is a rectangle
Ae = dVe / dX
# This area is a rectangle with area equal to layer_thickness * layer_width
line_width = Ae / layer_thickness
# A threshold is set to avoid weird paths in the GCode
if line_width > 1.2:
return 0.35
return line_width
def _gCode0(self, position, params, path):
x, y, z, f, e = position
if self._is_absolute_positioning:
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else z
else:
x += params.x if params.x is not None else 0
y += params.y if params.y is not None else 0
z += params.z if params.z is not None else 0
f = params.f if params.f is not None else f
if params.e is not None:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
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,
0,
position.f,
position.e)
## Set the absolute positioning
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
return position
## Set the relative positioning
def _gCode91(self, position, params, path):
self._is_absolute_positioning = 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, params, path):
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
position.e[self._extruder_number] = params.e
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,
params.z if params.z is not None else position.z,
params.f if params.f is not None else position.f,
position.e)
def _processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, f, e = None, None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "F":
f = float(item[1:]) / 60
if item[0] == "E":
e = float(item[1:])
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
self._center_is_zero = True
params = self._position(x, y, z, f, e)
return func(position, params, path)
return position
def _processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
def _processMCode(self, M):
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
# If no flavor is found in the GCode, then we use the by-default
self._flavor_reader = self._flavor_readers_dict[self._flavor_default]
return FileReader.PreReadResult.accepted
def read(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
last_z = 0
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, 0, [0])
current_path = []
min_layer_number = 0
negative_layers = 0
previous_layer = 0
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
last_z = current_position.z
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
# as in ProcessSlicedLayersJob
if layer_number < min_layer_number:
min_layer_number = layer_number
if layer_number < 0:
layer_number += abs(min_layer_number)
negative_layers += 1
else:
layer_number += negative_layers
# In case there is a gap in the layer count, empty layers are created
for empty_layer in range(previous_layer + 1, layer_number):
self._createEmptyLayer(empty_layer)
self._layer_number = layer_number
previous_layer = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
# When find a movement, the new posistion is calculated and added to the current_path, but
# don't need to create a polygon until the end of the layer
current_position = self._processGCode(G, line, current_position, current_path)
continue
# When changing the extruder, the polygon with the stored paths is computed
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self._processTCode(T, line, current_position, current_path)
if line.startswith("M"):
M = self._getInt(line, "M")
self._processMCode(M)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node
return self._flavor_reader.processGCodeFile(file_name)

View file

@ -0,0 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the Marlin/Sprinter Firmware flavor
class MarlinFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()

View file

@ -0,0 +1,32 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the RepRap Firmware flavor
class RepRapFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()
def processMCode(self, M, line, position, path):
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z, and E to absolute
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = 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):
self._is_absolute_positioning = False
return position

View file

@ -74,12 +74,13 @@ class GCodeWriter(MeshWriter):
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
flat_container = InstanceContainer(instance_container2.getName())
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition())
else:
flat_container.setDefinition(instance_container2.getDefinition())
# The metadata includes id, name and definition
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition().getId())
for key in instance_container2.getAllKeys():
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))

View file

@ -43,7 +43,7 @@ UM.Dialog
TextField {
id: peak_height
objectName: "Peak_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
validator: RegExpValidator {regExp: /^-?\d{1,3}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onPeakHeightChanged(text) }
}
@ -66,7 +66,7 @@ UM.Dialog
TextField {
id: base_height
objectName: "Base_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
validator: RegExpValidator {regExp: /^\d{1,3}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onBaseHeightChanged(text) }
}
@ -90,7 +90,7 @@ UM.Dialog
id: width
objectName: "Width"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onWidthChanged(text) }
}
@ -113,7 +113,7 @@ UM.Dialog
id: depth
objectName: "Depth"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onDepthChanged(text) }
}

View file

@ -4,8 +4,7 @@
import os
import threading
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import Qt, pyqtSignal, QObject
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
@ -81,14 +80,9 @@ class ImageReaderUI(QObject):
def _createConfigUI(self):
if self._ui_view is None:
Logger.log("d", "Creating ImageReader config UI")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._ui_context.setContextProperty("manager", self)
self._ui_view = component.create(self._ui_context)
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
self._disable_size_callbacks = False
@pyqtSlot()
@ -107,7 +101,7 @@ class ImageReaderUI(QObject):
def onWidthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._width = float(value)
self._width = float(value.replace(",", "."))
else:
self._width = 0
@ -120,7 +114,7 @@ class ImageReaderUI(QObject):
def onDepthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._depth = float(value)
self._depth = float(value.replace(",", "."))
else:
self._depth = 0
@ -132,14 +126,14 @@ class ImageReaderUI(QObject):
@pyqtSlot(str)
def onBaseHeightChanged(self, value):
if (len(value) > 0):
self.base_height = float(value)
self.base_height = float(value.replace(",", "."))
else:
self.base_height = 0
@pyqtSlot(str)
def onPeakHeightChanged(self, value):
if (len(value) > 0):
self.peak_height = float(value)
self.peak_height = float(value.replace(",", "."))
else:
self.peak_height = 0
@ -149,7 +143,4 @@ class ImageReaderUI(QObject):
@pyqtSlot(int)
def onImageColorInvertChanged(self, value):
if (value == 1):
self.image_color_invert = True
else:
self.image_color_invert = False
self.image_color_invert = (value == 1)

View file

@ -1,7 +1,8 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser # For reading the legacy profile INI files.
import io
import json # For reading the Dictionary of Doom.
import math # For mathematical operations included in the Dictionary of Doom.
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
@ -9,8 +10,10 @@ import os.path # For concatenating the path to the plugin and the relative path
from UM.Application import Application # To get the machine manager to create the new profile in.
from UM.Logger import Logger # Logging errors.
from UM.PluginRegistry import PluginRegistry # For getting the path to this plugin's directory.
from UM.Settings.ContainerRegistry import ContainerRegistry #To create unique profile IDs.
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.ProfileReader import ProfileReader # The plug-in type to implement.
from cura.Settings.ExtruderManager import ExtruderManager #To get the current extruder definition.
## A plugin that reads profile data from legacy Cura versions.
@ -76,12 +79,13 @@ class LegacyProfileReader(ProfileReader):
raise Exception("Unable to import legacy profile. Multi extrusion is not supported")
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
profile = InstanceContainer("Imported Legacy Profile") # Create an empty profile.
container_registry = ContainerRegistry.getInstance()
profile_id = container_registry.uniqueName("Imported Legacy Profile")
profile = InstanceContainer(profile_id) # Create an empty profile.
parser = configparser.ConfigParser(interpolation = None)
try:
with open(file_name) as f:
parser.readfp(f) # Parse the INI file.
parser.read([file_name]) # Parse the INI file.
except Exception as e:
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
return None
@ -120,8 +124,8 @@ class LegacyProfileReader(ProfileReader):
if "translation" not in dict_of_doom:
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer_definition = global_container_stack.getBottom()
profile.setDefinition(current_printer_definition)
current_printer_definition = global_container_stack.definition
profile.setDefinition(current_printer_definition.getId())
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
@ -138,7 +142,40 @@ class LegacyProfileReader(ProfileReader):
if len(profile.getAllKeys()) == 0:
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
profile.setDirty(True)
profile.addMetaDataEntry("type", "quality_changes")
profile.addMetaDataEntry("type", "profile")
# don't know what quality_type it is based on, so use "normal" by default
profile.addMetaDataEntry("quality_type", "normal")
return profile
profile.setName(profile_id)
profile.setDirty(True)
#Serialise and deserialise in order to perform the version upgrade.
parser = configparser.ConfigParser(interpolation=None)
data = profile.serialize()
parser.read_string(data)
parser["general"]["version"] = "1"
if parser.has_section("values"):
parser["settings"] = parser["values"]
del parser["values"]
stream = io.StringIO()
parser.write(stream)
data = stream.getvalue()
profile.deserialize(data)
#We need to return one extruder stack and one global stack.
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
global_profile.setDirty(True)
#Only the extruder stack has an extruder metadata entry.
profile.addMetaDataEntry("extruder", ExtruderManager.getInstance().getActiveExtruderStack().definition.getId())
#Split all settings into per-extruder and global settings.
for setting_key in profile.getAllKeys():
settable_per_extruder = global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
global_profile.removeInstance(setting_key)
else:
profile.removeInstance(setting_key)
return [global_profile, profile]

View file

@ -188,29 +188,32 @@ class MachineSettingsAction(MachineAction):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
stacks = ExtruderManager.getInstance().getExtruderStacks()
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_container = self._global_container_stack.material
if has_materials:
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.setMetaDataEntry("has_materials", True)
else:
self._global_container_stack.addMetaDataEntry("has_materials", True)
# Set the material container to a sane default
if material_container == self._empty_container:
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
self._global_container_stack.material = materials[0]
# Set the material container for each extruder to a sane default
for stack in stacks:
material_container = stack.material
if material_container == self._empty_container:
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
stack.material = materials[0]
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials")
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
for stack in stacks:
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit()

View file

@ -22,7 +22,7 @@ Cura.MachineAction
onModelChanged:
{
var extruderCount = base.extrudersModel.rowCount();
base.extruderTabsCount = extruderCount > 1 ? extruderCount : 0;
base.extruderTabsCount = extruderCount;
}
}
@ -241,7 +241,6 @@ Cura.MachineAction
UM.TooltipArea
{
visible: manager.definedExtruderCount > 1
height: childrenRect.height
width: childrenRect.width
text: machineExtruderCountProvider.properties.description
@ -271,6 +270,20 @@ Cura.MachineAction
}
}
}
Connections
{
target: manager
onDefinedExtruderCountChanged:
{
extruderCountModel.clear();
for(var i = 0; i < manager.definedExtruderCount; ++i)
{
extruderCountModel.append({text: String(i + 1), value: i});
}
}
}
currentIndex: machineExtruderCountProvider.properties.value - 1
onActivated:
{
@ -283,6 +296,7 @@ Cura.MachineAction
Loader
{
id: materialDiameterField
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string unit: catalog.i18nc("@label", "mm")
@ -290,15 +304,6 @@ Cura.MachineAction
property var afterOnEditingFinished: manager.updateMaterialForDiameter
property string label: catalog.i18nc("@label", "Material diameter")
}
Loader
{
id: nozzleSizeField
visible: !Cura.MachineManager.hasVariants && machineExtruderCountProvider.properties.value == 1
sourceComponent: numericTextFieldWithUnit
property string settingKey: "machine_nozzle_size"
property string label: catalog.i18nc("@label", "Nozzle size")
property string unit: catalog.i18nc("@label", "mm")
}
}
}
@ -355,7 +360,7 @@ Cura.MachineAction
if(currentIndex > 0)
{
contentItem.forceActiveFocus();
ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
}
}
@ -441,7 +446,7 @@ Cura.MachineAction
property int areaHeight: parent.height - y
property string settingKey: "machine_extruder_start_code"
property bool isExtruderSetting: true
}
}
}
Column {
height: parent.height
@ -585,11 +590,11 @@ Cura.MachineAction
propertyProvider.setPropertyValue("value", text);
if(_forceUpdateOnChange)
{
var extruderIndex = ExtruderManager.activeExtruderIndex;
var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
manager.forceUpdate();
if(ExtruderManager.activeExtruderIndex != extruderIndex)
if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
{
ExtruderManager.setActiveExtruderIndex(extruderIndex)
Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
}
}
if(_afterOnEditingFinished)
@ -723,7 +728,7 @@ Cura.MachineAction
width: gcodeArea.width
text: _tooltip
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false: isExtruderSetting
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false : isExtruderSetting
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
UM.SettingPropertyProvider
@ -735,7 +740,7 @@ Cura.MachineAction
{
if(settingsTabs.currentIndex > 0)
{
return Cura.MachineManager.activeStackId;
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
}
return "";
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 Ultimaker B.V.
import QtQuick 2.2
import QtQuick.Controls 1.1
import UM 1.3 as UM
import Cura 1.0 as Cura
Item
{
width: parent.width
height: parent.height
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle
{
id: viewportOverlay
color: UM.Theme.getColor("viewport_overlay")
width: parent.width
height: parent.height
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel.accepted = true
}
}
Loader
{
id: monitorViewComponent
width: parent.width
height: parent.height
property real maximumWidth: parent.width
property real maximumHeight: parent.height
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem: null
visible: sourceComponent != null
}
}

View file

@ -0,0 +1,73 @@
# 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 UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
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._setComponents)
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
return "tab_status_connected"
if output_device.jobState == "paused":
return "tab_status_paused"
if output_device.jobState == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -0,0 +1,20 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import MonitorStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Monitor"),
"weight": 1
}
}
def register(app):
return {
"stage": MonitorStage.MonitorStage()
}

View file

@ -0,0 +1,8 @@
{
"name": "Monitor Stage",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a monitor stage in Cura.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -26,17 +26,92 @@ Item {
spacing: UM.Theme.getSize("default_margin").height
Row
{
spacing: UM.Theme.getSize("default_margin").width
Label
{
text: catalog.i18nc("@label","Mesh Type")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
height: UM.Theme.getSize("setting").height
verticalAlignment: Text.AlignVCenter
}
ComboBox
{
id: meshTypeSelection
style: UM.Theme.styles.combobox
onActivated: {
UM.ActiveTool.setProperty("MeshType", model.get(index).type)
}
model: ListModel
{
id: meshTypeModel
Component.onCompleted:
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypeModel.append({
type: "support_mesh",
text: catalog.i18nc("@label", "Print as support")
});
meshTypeModel.append({
type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models")
});
meshTypeModel.append({
type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
});
meshTypeModel.append({
type: "infill_mesh",
text: catalog.i18nc("@label", "Modify settings for infill of other models")
});
meshTypeSelection.updateCurrentIndex();
}
}
function updateCurrentIndex()
{
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
for(var index=0; index < meshTypeSelection.model.count; index++)
{
if(meshTypeSelection.model.get(index).type == mesh_type)
{
meshTypeSelection.currentIndex = index;
return;
}
}
meshTypeSelection.currentIndex = 0;
}
}
Connections
{
target: UM.Selection
onSelectionChanged: meshTypeSelection.updateCurrentIndex()
}
}
Column
{
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
// It kinda looks ugly otherwise (big panel, no content on it)
id: currentSettings
property int maximumHeight: 200 * screenScaleFactor
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
ScrollView
{
height: parent.height
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("setting").height
width: UM.Theme.getSize("setting").width
style: UM.Theme.styles.scrollview
ListView
@ -49,6 +124,7 @@ Item {
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{
@ -58,6 +134,7 @@ Item {
delegate: Row
{
spacing: - UM.Theme.getSize("default_margin").width
Loader
{
id: settingLoader
@ -68,6 +145,7 @@ Item {
property var settingDefinitionsModel: addedSettingsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
property var externalResetHandler: false
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
@ -112,7 +190,7 @@ Item {
Button
{
width: (UM.Theme.getSize("setting").height / 2) | 0
width: Math.floor(UM.Theme.getSize("setting").height / 2)
height: UM.Theme.getSize("setting").height
onClicked: addedSettingsModel.setVisible(model.key, false)
@ -125,7 +203,7 @@ Item {
{
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: parent.height / 2
height: width
sourceSize.width: width
sourceSize.height: width
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
@ -201,9 +279,9 @@ Item {
Button
{
id: customise_settings_button;
height: UM.Theme.getSize("setting").height;
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
id: customiseSettingsButton;
height: UM.Theme.getSize("setting_control").height;
visible: currentSettings.visible
text: catalog.i18nc("@action:button", "Select settings");
@ -223,21 +301,12 @@ Item {
{
text: control.text;
color: UM.Theme.getColor("setting_control_text");
font: UM.Theme.getFont("default")
anchors.centerIn: parent
}
}
onClicked: settingPickDialog.visible = true;
Connections
{
target: UM.Preferences;
onPreferenceChanged:
{
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
}
}
}
}
@ -325,7 +394,7 @@ Item {
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude: [ "machine_settings", "command_line_settings" ]
exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
}
delegate:Loader
{

View file

@ -8,6 +8,7 @@ from UM.Application import Application
from UM.Preferences import Preferences
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.SettingInstance import SettingInstance
from UM.Event import Event
@ -18,7 +19,7 @@ class PerObjectSettingsTool(Tool):
super().__init__()
self._model = None
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder", "MeshType")
self._advanced_mode = False
self._multi_extrusion = False
@ -70,6 +71,39 @@ class PerObjectSettingsTool(Tool):
selected_object.addDecorator(SettingOverrideDecorator())
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
def setMeshType(self, mesh_type):
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())
stack = selected_object.callDecoration("getStack")
settings = stack.getTop()
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
if property_key != mesh_type:
if settings.getInstance(property_key):
settings.removeInstance(property_key)
else:
if not (settings.getInstance(property_key) and settings.getProperty(property_key, "value")):
definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
def getMeshType(self):
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:
return ""
settings = stack.getTop()
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
if settings.getInstance(property_key) and settings.getProperty(property_key, "value"):
return property_key
return ""
def _onPreferenceChanged(self, preference):
if preference == "cura/active_mode":
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1

View file

@ -11,7 +11,6 @@ from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
import json
import os
@ -39,8 +38,6 @@ class PluginBrowser(QObject, Extension):
self._plugins_metadata = []
self._plugins_model = None
self._qml_component = None
self._qml_context = None
self._dialog = None
self._download_progress = 0
@ -111,17 +108,8 @@ class PluginBrowser(QObject, Extension):
def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name)
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name))
self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't)
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("manager", self)
dialog = self._qml_component.create(self._qml_context)
if dialog is None:
Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
dialog = Application.getInstance().createQmlComponent(path, {"manager": self})
return dialog
def setIsDownloading(self, is_downloading):

View file

@ -0,0 +1,18 @@
# 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 UM.Resources import Resources
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):
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)

View file

@ -0,0 +1,20 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import PrepareStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Prepare"),
"weight": 0
}
}
def register(app):
return {
"stage": PrepareStage.PrepareStage()
}

View file

@ -0,0 +1,8 @@
{
"name": "Prepare Stage",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a prepare stage in Cura.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -92,7 +92,7 @@ class SimulationPass(RenderPass):
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay)
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
head_position = None # Indicates the current position of the print head
nozzle_node = None
@ -149,7 +149,7 @@ class SimulationPass(RenderPass):
self._current_shader = self._layer_shader
self._switching_layers = True
layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end), backface_cull = True)
layers_batch.addItem(node.getWorldTransformation(), layer_data)
layers_batch.render(self._scene.getActiveCamera())

View file

@ -154,6 +154,7 @@ class SimulationView(View):
def _onSceneChanged(self, node):
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self):
return self._busy

View file

@ -97,6 +97,8 @@ Item
// if we are in compatibility mode, we only show the "line type"
property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
@ -170,6 +172,9 @@ Item
{
// update visibility of legends
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
@ -450,11 +455,42 @@ Item
}
}
// Gradient colors for feedrate and thickness
// Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: gradient
visible: viewSettings.show_gradient
id: feedrateGradient
visible: viewSettings.show_feedrate_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0.5, 0, 1)
}
GradientStop {
position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0.25, 1, 0, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
}
}
// Gradient colors for layer thickness
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: thicknessGradient
visible: viewSettings.show_thickness_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
@ -468,15 +504,15 @@ Item
}
GradientStop {
position: 0.25
color: Qt.rgba(0.75, 0.5, 0.25, 1)
color: Qt.rgba(0.5, 0.5, 0, 1)
}
GradientStop {
position: 0.5
color: Qt.rgba(0.5, 1, 0.5, 1)
color: Qt.rgba(0, 1, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0.25, 0.5, 0.75, 1)
color: Qt.rgba(0, 0.5, 0.5, 1)
}
GradientStop {
position: 1.0
@ -503,9 +539,9 @@ Item
id: pathSlider
height: UM.Theme.getSize("slider_handle").width
anchors.right: playButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.left: parent.left
anchors.left: playButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
visible: !UM.SimulationView.compatibilityMode
// custom properties
@ -539,7 +575,7 @@ Item
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
top: !UM.SimulationView.compatibilityMode ? playButton.bottom : parent.top
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
@ -577,13 +613,10 @@ Item
// Play simulation button
Button {
id: playButton
implicitWidth: Math.floor(UM.Theme.getSize("button").width * 0.75)
implicitHeight: Math.floor(UM.Theme.getSize("button").height * 0.75)
iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.tool_button
style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode
anchors {
horizontalCenter: layerSlider.horizontalCenter
verticalCenter: pathSlider.verticalCenter
}

View file

@ -38,12 +38,25 @@ vertex41core =
out highp vec3 f_vertex;
out highp vec3 f_normal;
vec4 gradientColor(float abs_value, float min_value, float max_value)
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = value;
float green = 1-abs(1-4*value);
if (value > 0.375)
{
green = 0.5;
}
float blue = max(1-4*value, 0);
return vec4(red, green, blue, 1.0);
}
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = 1-value;
float blue = max(1-2*value, 0);
return vec4(red, green, blue, 1.0);
}
@ -64,10 +77,10 @@ vertex41core =
v_color = a_color;
break;
case 2: // "Feedrate"
v_color = gradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
break;
case 3: // "Layer thickness"
v_color = gradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
v_color = layerThicknessGradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
break;
}
@ -174,20 +187,27 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -7,73 +9,71 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 30 30"
width="6mm"
height="6mm"
viewBox="0 0 5.9999999 6"
version="1.1"
id="svg4620"
sodipodi:docname="simulation_pause.svg"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)">
id="svg877"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_pause2.svg">
<defs
id="defs871" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-5.3551409"
inkscape:cy="17.386031"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata4626">
id="metadata874">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4624" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1137"
id="namedview4622"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="22.250293"
inkscape:cx="8.1879003"
inkscape:cy="12.643765"
inkscape:window-x="2872"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4620">
<sodipodi:guide
position="15,15"
orientation="1,0"
id="guide4628"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="15,15"
orientation="0,1"
id="guide4630"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5192"
width="2"
height="20"
x="19"
y="5" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5192-5"
width="2"
height="20"
x="9"
y="5" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.163774,-122.8006)">
<g
id="g825"
transform="matrix(0.26458333,0,0,0.26458333,10.185689,121.85192)">
<rect
y="5"
x="19"
height="20"
width="2.7552757"
id="rect5192"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="5"
x="9"
height="20"
width="2.7552757"
id="rect5192-5"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -7,76 +9,70 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 30 30"
width="6mm"
height="6mm"
viewBox="0 0 6 6"
version="1.1"
id="svg3765"
sodipodi:docname="simulation_resume.svg"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)">
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_resume2.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-32.404712"
inkscape:cy="14.267522"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3771">
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3769" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1137"
id="namedview3767"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="23.327047"
inkscape:cx="10.788646"
inkscape:cy="14.67951"
inkscape:window-x="2872"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg3765">
<sodipodi:guide
position="15,15"
orientation="0,1"
id="guide4592"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="15,15"
orientation="1,0"
id="guide4594"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<path
sodipodi:type="star"
id="path3783"
sodipodi:sides="3"
sodipodi:cx="12.732001"
sodipodi:cy="14.695877"
sodipodi:r1="13.891838"
sodipodi:r2="6.945919"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 26.623839,14.695877 -20.8377567,12.030685 0,-24.0613696 z"
inkscape:transform-center-x="-2.9211205"
style="fill:none;stroke:#000000;stroke-width:2.32790732;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.84110413,0,0,0.87756418,1.775541,2.1034247)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(81.024887,-389.647)">
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.50520164;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path847"
sodipodi:sides="3"
sodipodi:cx="-78.732257"
sodipodi:cy="392.65222"
sodipodi:r1="3.0592039"
sodipodi:r2="1.5296021"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -75.67305,392.65222 -4.588806,2.64935 v -5.2987 z"
inkscape:transform-center-x="0.75529536"
inkscape:transform-center-y="0.40090429" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

@ -104,7 +104,9 @@ class SliceInfo(Extension):
"type": extruder.material.getMetaData().get("material", ""),
"brand": extruder.material.getMetaData().get("brand", "")
}
extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))]
extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

View file

@ -27,31 +27,39 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
self._extruders_model = ExtrudersModel()
self._theme = None
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
if not self._theme:
self._theme = Application.getInstance().getTheme()
if not self._enabled_shader:
self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
theme = Application.getInstance().getTheme()
self._enabled_shader.setUniformValue("u_overhangColor", Color(*theme.getColor("model_overhang").getRgb()))
self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").getRgb()))
if not self._disabled_shader:
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
theme = Application.getInstance().getTheme()
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*theme.getColor("model_unslicable").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*self._theme.getColor("model_unslicable").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_width", 50.0)
if not self._non_printing_shader:
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if Preferences.getInstance().getValue("view/show_overhang"):
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
angle = support_angle_stack.getProperty("support_angle", "value")
# Make sure the overhang angle is valid before passing it to the shader
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
@ -68,11 +76,19 @@ class SolidView(View):
uniforms = {}
shade_factor = 1.0
per_mesh_stack = node.callDecoration("getStack")
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0
extruder_id = node.callDecoration("getActiveExtruder")
if extruder_id:
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
try:
material_color = self._extruders_model.getItem(extruder_index)["color"]
except KeyError:
@ -94,19 +110,17 @@ class SolidView(View):
except ValueError:
pass
if hasattr(node, "_outside_buildarea"):
if node._outside_buildarea:
renderer.queueNode(node, shader = self._disabled_shader)
if node.callDecoration("isNonPrintingMesh"):
if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")):
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
else:
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms)
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def endRendering(self):
pass
#def _onPreferenceChanged(self, preference):
#if preference == "view/show_overhang": ## Todo: This a printer only setting. Should be removed from Uranium.
#self._enabled_material = None

View file

@ -1,17 +1,15 @@
from cura.MachineAction import MachineAction
import os.path
import time
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
import os.path
import time
from UM.i18n import i18nCatalog
from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction):
@ -136,17 +134,14 @@ class DiscoverUM3Action(MachineAction):
def _createAdditionalComponentsView(self):
Logger.log("d", "Creating additional ui components for UM3.")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml"))
self.__additional_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't)
self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext())
self.__additional_components_context.setContextProperty("manager", self)
self.__additional_components_view = self.__additional_component.create(self.__additional_components_context)
# Create networking dialog
path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")
self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self})
if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.")
return
# Create extra components
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -8,12 +8,10 @@ import time
from enum import Enum
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
from PyQt5.QtCore import QUrl, QByteArray, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.Application import Application
from UM.Decorators import override
from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice import OutputDeviceError
@ -131,7 +129,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
@pyqtProperty(QObject, notify=selectedPrinterChanged)
def controlItem(self):
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if not self._control_component:
if not self._control_item:
self._createControlViewFromQML()
name = self._selected_printer.get("friendly_name")
if name == self._automatic_printer.get("friendly_name") or name == "":
@ -235,17 +233,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
def spawnPrintView(self):
if self._print_view is None:
path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._print_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._print_context.setContextProperty("OutputDevice", self)
self._print_view = component.create(self._print_context)
if component.isError():
Logger.log("e", " Errors creating component: \n%s", "\n".join(
[e.toString() for e in component.errors()]))
path = os.path.join(self._plugin_path, "PrintWindow.qml")
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._print_view is not None:
self._print_view.show()
@ -263,6 +252,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
self._error_message.show()
return
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1:
@ -707,7 +698,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
if self._reply:
self._reply.abort()
self._stage = OutputStage.ready
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):

View file

@ -672,7 +672,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state)
return
Application.getInstance().showPrintMonitor.emit(True)
Application.getInstance().getController().setActiveStage("MonitorStage")
self._print_finished = True
self.writeStarted.emit(self)
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
@ -726,10 +726,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
remote_material_guid,
material.getMetaDataEntry("GUID"))
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", GUID = remote_material_guid, read_only = True)
remote_material_name = "Unknown"
if remote_materials:
remote_material_name = remote_materials[0].getName()
remote_material_name = remote_materials[0]["name"]
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
try:
@ -767,7 +767,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
if button == QMessageBox.Yes:
self.startPrint()
else:
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
# For some unknown reason Cura on OSX will hang if we do the call back code
# immediately without first returning and leaving QML's event system.
QTimer.singleShot(100, delayedCallback)
@ -850,7 +850,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
self._write_finished = True # post_reply does not always exist, so make sure we unblock writing
if self._post_reply:
self._finalizePostReply()
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
## Attempt to start a new print.
# This function can fail to actually start a print due to not being authenticated or another print already
@ -971,7 +971,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Send all material profiles to the printer.
def sendMaterialProfiles(self):
for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material"):
registry = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance()
for container in registry.findInstanceContainers(type = "material"):
try:
xml_data = container.serialize()
if xml_data == "" or xml_data is None:
@ -980,7 +981,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
if names:
# There are other materials that share this GUID.
if not container.isReadOnly():
if not registry.isReadOnly(container.getId()):
continue # If it's not readonly, it's created by user, so skip it.
material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

View file

@ -43,6 +43,7 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
# authentication requests.
self._old_printers = []
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addPrinterSignal.connect(self.addPrinter)
@ -300,6 +301,9 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
if type_of_device:
if type_of_device == b"printer":
address = '.'.join(map(lambda n: str(n), info.address))
if address in self._excluded_addresses:
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
return False # When getting the localhost IP, then try to reconnect
self.addPrinterSignal.emit(str(name), address, info.properties)
else:
Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )

View file

@ -13,6 +13,7 @@ from UM.Application import Application
from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from UM.Message import Message
from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
@ -52,6 +53,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._heatup_wait_start_time = time.time()
self.jobStateChanged.connect(self._onJobStateChanged)
## Queue for commands that need to be send. Used when command is sent when a print is active.
self._command_queue = queue.Queue()
@ -60,7 +63,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Set when print is started in order to check running time.
self._print_start_time = None
self._print_start_time_100 = None
self._print_estimated_time = None
## Keep track where in the provided g-code the print is
self._gcode_position = 0
@ -125,6 +128,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _homeBed(self):
self._sendCommand("G28 Z")
## Updates the target bed temperature from the printer, and emit a signal if it was changed.
#
# /param temperature The new target temperature of the bed.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature == temperature:
return False
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
return True
## Updates the target hotend temperature from the printer, and emit a signal if it was changed.
#
# /param index The index of the hotend.
# /param temperature The new target temperature of the hotend.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetHotendTemperature(self, index, temperature):
if self._target_hotend_temperatures[index] == temperature:
return False
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
return True
## A name for the device.
@pyqtProperty(str, constant = True)
def name(self):
@ -164,7 +190,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110")
self._gcode_position = 0
self._print_start_time_100 = None
self._is_printing = True
self._print_start_time = time.time()
@ -394,7 +419,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._listen_thread.join()
except:
pass
self._serial.close()
if self._serial is not None: # Avoid a race condition when a thread can change the value of self._serial to None
self._serial.close()
self._listen_thread = threading.Thread(target = self._listen)
self._listen_thread.daemon = True
@ -446,7 +472,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
#
# \param nodes A collection of scene nodes to send. This is ignored.
# \param file_name \type{string} A suggestion for a file name to write.
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored.
# \param kwargs Keyword arguments.
@ -462,7 +487,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._error_message.show()
return
Application.getInstance().showPrintMonitor.emit(True)
self.setJobName(file_name)
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
Application.getInstance().getController().setActiveStage("MonitorStage")
self.startPrint()
def _setEndstopState(self, endstop_key, value):
@ -482,6 +510,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Listen thread function.
def _listen(self):
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
container_stack = Application.getInstance().getGlobalContainerStack()
temperature_request_timeout = time.time()
ok_timeout = time.time()
while self._connection_state == ConnectionState.connected:
@ -511,16 +540,40 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): # Temperature message
temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
temperature_set = False
try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
for match in temperature_matches:
if match[0]:
extruder_nr = int(match[0])
if extruder_nr >= container_stack.getProperty("machine_extruder_count", "value"):
continue
if match[1]:
self._setHotendTemperature(extruder_nr, float(match[1]))
temperature_set = True
if match[2]:
self._updateTargetHotendTemperature(extruder_nr, float(match[2]))
else:
requested_temperatures = match
if not temperature_set and requested_temperatures:
if requested_temperatures[1]:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[1]))
if requested_temperatures[2]:
self._updateTargetHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[2]))
except:
pass
if b"B:" in line: # Check if it's a bed temperature
Logger.log("w", "Could not parse hotend temperatures from response: %s", line)
# Check if there's also a bed temperature
temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line)
if container_stack.getProperty("machine_heated_bed", "value") and len(temperature_matches) > 0:
match = temperature_matches[0]
try:
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
except Exception as e:
pass
#TODO: temperature changed callback
if match[0]:
self._setBedTemperature(float(match[0]))
if match[1]:
self._updateTargetBedTemperature(float(match[1]))
except:
Logger.log("w", "Could not parse bed temperature from response: %s", line)
elif b"_min" in line or b"_max" in line:
tag, value = line.split(b":", 1)
self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value))
@ -559,8 +612,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _sendNextGcodeLine(self):
if self._gcode_position >= len(self._gcode):
return
if self._gcode_position == 100:
self._print_start_time_100 = time.time()
line = self._gcode[self._gcode_position]
if ";" in line:
@ -584,8 +635,18 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
progress = (self._gcode_position / len(self._gcode))
elapsed_time = int(time.time() - self._print_start_time)
self.setTimeElapsed(elapsed_time)
estimated_time = self._print_estimated_time
if progress > .1:
estimated_time = self._print_estimated_time * (1-progress) + elapsed_time
self.setTimeTotal(estimated_time)
self._gcode_position += 1
self.setProgress((self._gcode_position / len(self._gcode)) * 100)
self.setProgress(progress * 100)
self.progressChanged.emit()
## Set the state of the print.
@ -600,6 +661,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elif job_state == "abort":
self.cancelPrint()
def _onJobStateChanged(self):
# clear the job name & times when printing is done or aborted
if self._job_state == "ready":
self.setJobName("")
self.setTimeElapsed(0)
self.setTimeTotal(0)
## Set the progress of the print.
# It will be normalized (based on max_progress) to range 0 - 100
def setProgress(self, progress, max_progress = 100):
@ -630,7 +698,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._is_printing = False
self._is_paused = False
self._updateJobState("ready")
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
## Check if the process did not encounter an error yet.
def hasError(self):

View file

@ -16,13 +16,11 @@ from cura.CuraApplication import CuraApplication
import threading
import platform
import glob
import time
import os.path
import serial.tools.list_ports
from UM.Extension import Extension
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
@ -99,7 +97,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
@pyqtSlot(str)
def updateAllFirmware(self, file_name):
if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
return

View file

@ -9,13 +9,12 @@ from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSlot
import os.path
class UserAgreement(QObject, Extension):
def __init__(self, parent = None):
def __init__(self):
super(UserAgreement, self).__init__()
self._user_agreement_window = None
self._user_agreement_context = None
@ -33,8 +32,8 @@ class UserAgreement(QObject, Extension):
self._user_agreement_window.show()
@pyqtSlot(bool)
def didAgree(self, userChoice):
if userChoice:
def didAgree(self, user_choice):
if user_choice:
Logger.log("i", "User agreed to the user agreement")
Preferences.getInstance().setValue("general/accepted_user_agreement", True)
self._user_agreement_window.hide()

View file

@ -43,7 +43,7 @@ UM.Dialog
anchors.right: parent.right
text: catalog.i18nc("@action:button", "I understand and agree")
onClicked: {
manager.didAgree(true)
baseDialog.accepted()
}
}
@ -52,13 +52,12 @@ UM.Dialog
anchors.left: parent.left
text: catalog.i18nc("@action:button", "I don't agree")
onClicked: {
manager.didAgree(false)
baseDialog.rejected()
}
}
}
onClosing: {
manager.didAgree(false)
}
onAccepted: manager.didAgree(true)
onRejected: manager.didAgree(false)
onClosing: manager.didAgree(false)
}

View file

@ -98,7 +98,7 @@ class Profile:
config.add_section("metadata")
config.set("metadata", "quality_type", "normal") #This feature doesn't exist in 2.1 yet, so we don't know the actual quality type. For now, always base it on normal.
config.set("metadata", "type", "quality_changes")
config.set("metadata", "type", "quality")
if self._weight:
config.set("metadata", "weight", str(self._weight))
if self._machine_variant_name:

View file

@ -122,6 +122,26 @@ class VersionUpgrade30to31(VersionUpgrade):
if len(all_quality_changes) <= 1 and not parser.has_option("metadata", "extruder"):
self._createExtruderQualityChangesForSingleExtrusionMachine(filename, parser)
if parser["metadata"]["type"] == "definition_changes":
if parser["general"]["definition"] == "custom":
# We are only interested in machine_nozzle_size
if parser.has_option("values", "machine_nozzle_size"):
machine_nozzle_size = parser["values"]["machine_nozzle_size"]
machine_extruder_count = '1' # by default it is 1 and the value cannot be stored in the global stack
if parser.has_option("values", "machine_extruder_count"):
machine_extruder_count = parser["values"]["machine_extruder_count"]
if machine_extruder_count == '1':
definition_name = parser["general"]["name"]
machine_extruders = self._getSingleExtrusionMachineExtruders(definition_name)
# For single extruder machine we need only first extruder
if len(machine_extruders) !=0:
self._updateSingleExtruderDefinitionFile(machine_extruders, machine_nozzle_size)
parser.remove_option("values", "machine_nozzle_size")
# Update version numbers
parser["general"]["version"] = "2"
parser["metadata"]["setting_version"] = "4"
@ -179,7 +199,7 @@ class VersionUpgrade30to31(VersionUpgrade):
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:
@ -200,12 +220,137 @@ class VersionUpgrade30to31(VersionUpgrade):
return quality_changes_containers
def _getSingleExtrusionMachineExtruders(self, definition_name):
machine_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.MachineStack)
machine_instance_id = None
# Find machine instances
for item in os.listdir(machine_instances_dir):
file_path = os.path.join(machine_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "machine" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "id"):
continue
id = parser["general"]["id"]
if id + "_settings" != definition_name:
continue
else:
machine_instance_id = id
break
if machine_instance_id is not None:
extruders_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
#"machine",[extruders]
extruder_instances = []
# Find all custom extruders for found machines
for item in os.listdir(extruders_instances_dir):
file_path = os.path.join(extruders_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "extruder_train" != parser["metadata"]["type"]:
continue
if not parser.has_option("metadata", "machine"):
continue
if not parser.has_option("metadata", "position"):
continue
if machine_instance_id != parser["metadata"]["machine"]:
continue
extruder_instances.append(parser)
return extruder_instances
# Find extruder definition at index 0 and update its values
def _updateSingleExtruderDefinitionFile(self, extruder_instances_per_machine, machine_nozzle_size):
defintion_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
for item in os.listdir(defintion_instances_dir):
file_path = os.path.join(defintion_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("general", "name"):
continue
name = parser["general"]["name"]
custom_extruder_at_0_position = None
for extruder_instance in extruder_instances_per_machine:
definition_position = extruder_instance["metadata"]["position"]
if definition_position == "0":
custom_extruder_at_0_position = extruder_instance
break
# If not null, then parsed file is for first extuder and then can be updated. I need to update only
# first, because this update for single extuder machine
if custom_extruder_at_0_position is not None:
#Add new value
parser["values"]["machine_nozzle_size"] = machine_nozzle_size
definition_output = io.StringIO()
parser.write(definition_output)
with open(file_path, "w") as f:
f.write(definition_output.getvalue())
return True
return False
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
# Why is this here?!
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
# This file can be recognized by it's lack of a machine name in the target filename.
# So when we detect that situation here, we don't create the file and return.
if machine_name == "":
return
new_filename = machine_name + "_" + "fdmextruder" + suffix
extruder_quality_changes_parser = configparser.ConfigParser()
extruder_quality_changes_parser = configparser.ConfigParser(interpolation = None)
extruder_quality_changes_parser.add_section("general")
extruder_quality_changes_parser["general"]["version"] = str(2)
extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"]
@ -224,4 +369,4 @@ class VersionUpgrade30to31(VersionUpgrade):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w") as f:
f.write(extruder_quality_changes_output.getvalue())
f.write(extruder_quality_changes_output.getvalue())

View file

@ -3,7 +3,10 @@
import copy
import io
from typing import List, Optional
import json #To parse the product-to-id mapping file.
import os.path #To find the product-to-id mapping.
import sys
from typing import Any, Dict, List, Optional
import xml.etree.ElementTree as ET
from UM.Resources import Resources
@ -14,7 +17,6 @@ import UM.Dictionary
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
## Handles serializing and deserializing material containers from an XML file
class XmlMaterialProfile(InstanceContainer):
CurrentFdmMaterialVersion = "1.3"
@ -42,25 +44,18 @@ class XmlMaterialProfile(InstanceContainer):
def getInheritedFiles(self):
return self._inherited_files
## Overridden from InstanceContainer
def setReadOnly(self, read_only):
super().setReadOnly(read_only)
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
container._read_only = read_only # prevent loop instead of calling setReadOnly
## Overridden from InstanceContainer
# set the meta data for all machine / variant combinations
def setMetaDataEntry(self, key, value):
if self.isReadOnly():
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
super().setMetaDataEntry(key, value)
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile.
# Update all containers that share basefile
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
for container in registry.findInstanceContainers(base_file = basefile):
if container.getMetaDataEntry(key, None) != value: # Prevent recursion
container.setMetaDataEntry(key, value)
@ -68,7 +63,8 @@ class XmlMaterialProfile(InstanceContainer):
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
# The function is a bit tricky. It will not set the name of all containers if it has the correct name itself.
def setName(self, new_name):
if self.isReadOnly():
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
# Not only is this faster, it also prevents a major loop that causes a stack overflow.
@ -77,10 +73,10 @@ class XmlMaterialProfile(InstanceContainer):
super().setName(new_name)
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
basefile = self.getMetaDataEntry("base_file", self.getId()) # if basefile is self.getId, this is a basefile.
# Update the basefile as well, this is actually what we're trying to do
# Update all containers that share GUID and basefile
containers = ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile)
containers = registry.findInstanceContainers(base_file = basefile)
for container in containers:
container.setName(new_name)
@ -88,33 +84,20 @@ class XmlMaterialProfile(InstanceContainer):
def setDirty(self, dirty):
super().setDirty(dirty)
base_file = self.getMetaDataEntry("base_file", None)
if base_file is not None and base_file != self._id:
containers = ContainerRegistry.getInstance().findContainers(id=base_file)
registry = ContainerRegistry.getInstance()
if base_file is not None and base_file != self.getId() and not registry.isReadOnly(base_file):
containers = registry.findContainers(id = base_file)
if containers:
base_container = containers[0]
if not base_container.isReadOnly():
base_container.setDirty(dirty)
## Overridden from InstanceContainer
# def setProperty(self, key, property_name, property_value, container = None):
# if self.isReadOnly():
# return
#
# super().setProperty(key, property_name, property_value)
#
# basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
# for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
# if not container.isReadOnly():
# container.setDirty(True)
containers[0].setDirty(dirty)
## Overridden from InstanceContainer
# base file: common settings + supported machines
# machine / variant combination: only changes for itself.
def serialize(self, ignored_metadata_keys: Optional[List] = None):
def serialize(self, ignored_metadata_keys: Optional[set] = None):
registry = ContainerRegistry.getInstance()
base_file = self.getMetaDataEntry("base_file", "")
if base_file and self.id != base_file:
if base_file and self.getId() != base_file:
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
# we should only serialize the "base" material definition, since that can then take care of
# serializing the machine/nozzle specific profiles.
@ -132,8 +115,8 @@ class XmlMaterialProfile(InstanceContainer):
metadata = copy.deepcopy(self.getMetaData())
# setting_version is derived from the "version" tag in the schema, so don't serialize it into a file
if ignored_metadata_keys is None:
ignored_metadata_keys = []
ignored_metadata_keys = ignored_metadata_keys + ["setting_version"]
ignored_metadata_keys = set()
ignored_metadata_keys |= {"setting_version"}
# remove the keys that we want to ignore in the metadata
for key in ignored_metadata_keys:
if key in metadata:
@ -146,6 +129,9 @@ class XmlMaterialProfile(InstanceContainer):
metadata.pop("type", "")
metadata.pop("base_file", "")
metadata.pop("approximate_diameter", "")
metadata.pop("id", "")
metadata.pop("container_type", "")
metadata.pop("name", "")
## Begin Name Block
builder.start("name")
@ -163,7 +149,7 @@ class XmlMaterialProfile(InstanceContainer):
builder.end("color")
builder.start("label")
builder.data(self._name)
builder.data(self.getName())
builder.end("label")
builder.end("name")
@ -195,16 +181,16 @@ class XmlMaterialProfile(InstanceContainer):
## Begin Settings Block
builder.start("settings")
if self.getDefinition().id == "fdmprinter":
if self.getDefinition().getId() == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self._id)
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId())
for container in all_containers:
definition_id = container.getDefinition().id
definition_id = container.getDefinition().getId()
if definition_id == "fdmprinter":
continue
@ -222,17 +208,16 @@ class XmlMaterialProfile(InstanceContainer):
machine_container_map[definition_id] = container
# Map machine human-readable names to IDs
product_id_map = {}
for container in registry.findDefinitionContainers(type = "machine"):
product_id_map[container.getName()] = container.getId()
product_id_map = self.getProductIdMap()
for definition_id, container in machine_container_map.items():
definition = container.getDefinition()
try:
product = UM.Dictionary.findKey(product_id_map, definition_id)
except ValueError:
# An unknown product id; export it anyway
product = definition_id
product = definition_id
for product_name, product_id_list in product_id_map.items():
if definition_id in product_id_list:
product = product_name
break
builder.start("machine")
builder.start("machine_identifier", {
@ -242,7 +227,7 @@ class XmlMaterialProfile(InstanceContainer):
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().id == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
@ -250,11 +235,12 @@ class XmlMaterialProfile(InstanceContainer):
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainers(id = hotend.getMetaDataEntry("variant"))
variant_containers = registry.findInstanceContainersMetadata(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
builder.start("hotend", {"id": variant_containers[0].getName()})
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_containers[0]["name"]})
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
compatible = hotend.getMetaDataEntry("compatible")
@ -397,15 +383,18 @@ class XmlMaterialProfile(InstanceContainer):
first.append(element)
def clearData(self):
self._metadata = {}
self._name = ""
self._metadata = {
"id": self.getId(),
"name": ""
}
self._definition = None
self._instances = {}
self._read_only = False
self._dirty = False
self._path = ""
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
@classmethod
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
return "materials"
@classmethod
@ -415,9 +404,9 @@ class XmlMaterialProfile(InstanceContainer):
version = XmlMaterialProfile.Version
# get setting version
if "version" in data.attrib:
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion(data.attrib["version"])
setting_version = cls.xmlVersionToSettingVersion(data.attrib["version"])
else:
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion("1.2")
setting_version = cls.xmlVersionToSettingVersion("1.2")
return version * 1000000 + setting_version
@ -431,15 +420,18 @@ class XmlMaterialProfile(InstanceContainer):
try:
data = ET.fromstring(serialized)
except:
Logger.logException("e", "An exception occured while parsing the material profile")
Logger.logException("e", "An exception occurred while parsing the material profile")
return
# Reset previous metadata
old_id = self.getId()
self.clearData() # Ensure any previous data is gone.
meta_data = {}
meta_data["type"] = "material"
meta_data["base_file"] = self.id
meta_data["base_file"] = self.getId()
meta_data["status"] = "unknown" # TODO: Add material verification
meta_data["id"] = old_id
meta_data["container_type"] = XmlMaterialProfile
common_setting_values = {}
@ -454,8 +446,8 @@ class XmlMaterialProfile(InstanceContainer):
else:
meta_data["setting_version"] = self.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
for entry in metadata:
meta_data["name"] = "Unknown Material" #In case the name tag is missing.
for entry in data.iterfind("./um:metadata/*", self.__namespaces):
tag_name = _tag_without_namespace(entry)
if tag_name == "name":
@ -465,9 +457,9 @@ class XmlMaterialProfile(InstanceContainer):
label = entry.find("./um:label", self.__namespaces)
if label is not None:
self._name = label.text
meta_data["name"] = label.text
else:
self._name = self._profile_name(material.text, color.text)
meta_data["name"] = self._profile_name(material.text, color.text)
meta_data["brand"] = brand.text
meta_data["material"] = material.text
meta_data["color_name"] = color.text
@ -499,8 +491,7 @@ class XmlMaterialProfile(InstanceContainer):
meta_data["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
meta_data["properties"] = property_values
self.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0])
meta_data["definition"] = "fdmprinter"
common_compatibility = True
settings = data.iterfind("./um:settings/um:setting", self.__namespaces)
@ -518,9 +509,7 @@ class XmlMaterialProfile(InstanceContainer):
self._dirty = False
# Map machine human-readable names to IDs
product_id_map = {}
for container in ContainerRegistry.getInstance().findDefinitionContainers(type = "machine"):
product_id_map[container.getName()] = container.getId()
product_id_map = self.getProductIdMap()
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
for machine in machines:
@ -542,112 +531,284 @@ class XmlMaterialProfile(InstanceContainer):
identifiers = machine.iterfind("./um:machine_identifier", self.__namespaces)
for identifier in identifiers:
machine_id = product_id_map.get(identifier.get("product"), None)
if machine_id is None:
# Lets try again with some naive heuristics.
machine_id = identifier.get("product").replace(" ", "").lower()
machine_id_list = product_id_map.get(identifier.get("product"), [])
if not machine_id_list:
machine_id_list = self.getPossibleDefinitionIDsFromName(identifier.get("product"))
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id)
if not definitions:
# Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition = definitions[0]
machine_manufacturer = identifier.get("manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility:
new_material_id = self.id + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
is_new_material = False
if found_materials:
new_material = found_materials[0]
else:
new_material = XmlMaterialProfile(new_material_id)
is_new_material = True
# Update the private directly, as we want to prevent the lookup that is done when using setName
new_material._name = self.getName()
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.setDefinition(definition)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_material.getMetaData()["compatible"] = machine_compatibility
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material.setCachedValues(cached_machine_setting_properties)
new_material._dirty = False
if is_new_material:
containers_to_add.append(new_material)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
for machine_id in machine_id_list:
definitions = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(definition = definition.id, name = hotend_id)
Logger.log("d", "Found definition for machine ID %s", machine_id)
definition = definitions[0]
if not variant_containers:
#Logger.log("d", "No variants found with ID or name %s for machine %s", hotend_id, definition.id)
continue
machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
hotend_compatibility = machine_compatibility
hotend_setting_values = {}
settings = hotend.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
hotend_compatibility = self._parseCompatibleValue(entry.text)
if machine_compatibility:
new_material_id = self.getId() + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
if ContainerRegistry.getInstance().isLoaded(new_material_id):
new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0]
is_new_material = False
else:
Logger.log("d", "Unsupported material setting %s", key)
new_material = XmlMaterialProfile(new_material_id)
is_new_material = True
new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.getMetaData()["id"] = new_material_id
new_material.getMetaData()["name"] = self.getName()
new_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_material.getMetaData()["compatible"] = machine_compatibility
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material.getMetaData()["definition"] = machine_id
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_hotend_id)
is_new_material = False
if found_materials:
new_hotend_material = found_materials[0]
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
is_new_material = True
new_material.setCachedValues(cached_machine_setting_properties)
# Update the private directly, as we want to prevent the lookup that is done when using setName
new_hotend_material._name = self.getName()
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.setDefinition(definition)
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material._dirty = False
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
cached_hotend_setting_properties.update(hotend_setting_values)
if is_new_material:
containers_to_add.append(new_material)
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
continue
new_hotend_material._dirty = False
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
if is_new_material:
containers_to_add.append(new_hotend_material)
if not variant_containers:
continue
hotend_compatibility = machine_compatibility
hotend_setting_values = {}
settings = hotend.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
hotend_compatibility = self._parseCompatibleValue(entry.text)
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_id = self.getId() + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
if ContainerRegistry.getInstance().isLoaded(new_hotend_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_id)[0]
is_new_material = False
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
is_new_material = True
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.getMetaData()["id"] = new_hotend_id
new_hotend_material.getMetaData()["name"] = self.getName()
new_hotend_material.getMetaData()["variant"] = variant_containers[0]["id"]
new_hotend_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_hotend_material.getMetaData()["definition"] = machine_id
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
cached_hotend_setting_properties.update(hotend_setting_values)
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
new_hotend_material._dirty = False
if is_new_material:
containers_to_add.append(new_hotend_material)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
break
for container_to_add in containers_to_add:
ContainerRegistry.getInstance().addContainer(container_to_add)
@classmethod
def deserializeMetadata(cls, serialized: str, container_id: str) -> List[Dict[str, Any]]:
result_metadata = [] #All the metadata that we found except the base (because the base is returned).
#Update the serialized data to the latest version.
serialized = cls._updateSerialized(serialized)
base_metadata = {
"type": "material",
"status": "unknown", #TODO: Add material verification.
"container_type": XmlMaterialProfile,
"id": container_id,
"base_file": container_id
}
try:
data = ET.fromstring(serialized)
except:
Logger.logException("e", "An exception occurred while parsing the material profile")
return []
#TODO: Implement the <inherits> tag. It's unused at the moment though.
if "version" in data.attrib:
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion(data.attrib["version"])
else:
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
for entry in data.iterfind("./um:metadata/*", cls.__namespaces):
tag_name = _tag_without_namespace(entry)
if tag_name == "name":
brand = entry.find("./um:brand", cls.__namespaces)
material = entry.find("./um:material", cls.__namespaces)
color = entry.find("./um:color", cls.__namespaces)
label = entry.find("./um:label", cls.__namespaces)
if label is not None:
base_metadata["name"] = label.text
else:
base_metadata["name"] = cls._profile_name(material.text, color.text)
base_metadata["brand"] = brand.text
base_metadata["material"] = material.text
base_metadata["color_name"] = color.text
continue
#Setting_version is derived from the "version" tag in the schema earlier, so don't set it here.
if tag_name == "setting_version":
continue
base_metadata[tag_name] = entry.text
if "description" not in base_metadata:
base_metadata["description"] = ""
if "adhesion_info" not in base_metadata:
base_metadata["adhesion_info"] = ""
property_values = {}
properties = data.iterfind("./um:properties/*", cls.__namespaces)
for entry in properties:
tag_name = _tag_without_namespace(entry)
property_values[tag_name] = entry.text
base_metadata["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
base_metadata["properties"] = property_values
base_metadata["definition"] = "fdmprinter"
compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces)
try:
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text)
except StopIteration: #No 'hardware compatible' setting.
common_compatibility = True
base_metadata["compatible"] = common_compatibility
result_metadata.append(base_metadata)
# Map machine human-readable names to IDs
product_id_map = cls.getProductIdMap()
for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces):
machine_compatibility = common_compatibility
for entry in machine.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
machine_compatibility = cls._parseCompatibleValue(entry.text)
for identifier in machine.iterfind("./um:machine_identifier", cls.__namespaces):
machine_id_list = product_id_map.get(identifier.get("product"), [])
if not machine_id_list:
machine_id_list = cls.getPossibleDefinitionIDsFromName(identifier.get("product"))
for machine_id in machine_id_list:
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadata:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition_metadata = definition_metadata[0]
machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility:
new_material_id = container_id + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_material_id)
if found_materials:
new_material_metadata = found_materials[0]
else:
new_material_metadata = {}
new_material_metadata.update(base_metadata)
new_material_metadata["id"] = new_material_id
new_material_metadata["compatible"] = machine_compatibility
new_material_metadata["machine_manufacturer"] = machine_manufacturer
new_material_metadata["definition"] = machine_id
if len(found_materials) == 0: #This is a new material.
result_metadata.append(new_material_metadata)
for hotend in machine.iterfind("./um:hotend", cls.__namespaces):
hotend_id = hotend.get("id")
if hotend_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
hotend_compatibility = machine_compatibility
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
hotend_compatibility = cls._parseCompatibleValue(entry.text)
new_hotend_id = container_id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_id)
if found_materials:
new_hotend_material_metadata = found_materials[0]
else:
new_hotend_material_metadata = {}
new_hotend_material_metadata.update(base_metadata)
if variant_containers:
new_hotend_material_metadata["variant"] = variant_containers[0]["id"]
else:
new_hotend_material_metadata["variant"] = hotend_id
_with_missing_variants.append(new_hotend_material_metadata)
new_hotend_material_metadata["compatible"] = hotend_compatibility
new_hotend_material_metadata["machine_manufacturer"] = machine_manufacturer
new_hotend_material_metadata["id"] = new_hotend_id
new_hotend_material_metadata["definition"] = machine_id
if len(found_materials) == 0:
result_metadata.append(new_hotend_material_metadata)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
break
return result_metadata
def _addSettingElement(self, builder, instance):
try:
key = UM.Dictionary.findKey(self.__material_settings_setting_map, instance.definition.key)
@ -658,16 +819,58 @@ class XmlMaterialProfile(InstanceContainer):
builder.data(str(instance.value))
builder.end("setting")
def _profile_name(self, material_name, color_name):
@classmethod
def _profile_name(cls, material_name, color_name):
if color_name != "Generic":
return "%s %s" % (color_name, material_name)
else:
return material_name
@classmethod
def getPossibleDefinitionIDsFromName(cls, name):
name_parts = name.lower().split(" ")
merged_name_parts = []
for part in name_parts:
if len(part) == 0:
continue
if len(merged_name_parts) == 0:
merged_name_parts.append(part)
continue
if part.isdigit():
# for names with digit(s) such as Ultimaker 3 Extended, we generate an ID like
# "ultimaker3_extended", ignoring the space between "Ultimaker" and "3".
merged_name_parts[-1] = merged_name_parts[-1] + part
else:
merged_name_parts.append(part)
id_list = [name.lower().replace(" ", ""), # simply removing all spaces
name.lower().replace(" ", "_"), # simply replacing all spaces with underscores
"_".join(merged_name_parts),
]
return id_list
## Gets a mapping from product names in the XML files to their definition
# IDs.
#
# This loads the mapping from a file.
@classmethod
def getProductIdMap(cls) -> Dict[str, List[str]]:
product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json")
with open(product_to_id_file) as f:
product_to_id_map = json.load(f)
product_to_id_map = {key: [value] for key, value in product_to_id_map.items()}
return product_to_id_map
## Parse the value of the "material compatible" property.
def _parseCompatibleValue(self, value: str):
@classmethod
def _parseCompatibleValue(cls, value: str):
return value in {"yes", "unknown"}
## Small string representation for debugging.
def __str__(self):
return "<XmlMaterialProfile '{my_id}' ('{name}') from base file '{base_file}'>".format(my_id = self.getId(), name = self.getName(), base_file = self.getMetaDataEntry("base_file"))
# Map XML file setting names to internal names
__material_settings_setting_map = {
"print temperature": "default_material_print_temperature",
@ -716,4 +919,22 @@ def _indent(elem, level = 0):
# We are only interested in the actual tag name, so discard everything
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]
return element.tag[element.tag.rfind("}") + 1:]
#While loading XML profiles, some of these profiles don't know what variant
#they belong to. We'd like to search by the machine ID and the variant's
#name, but we don't know the variant's ID. Not all variants have been loaded
#yet so we can't run a filter on the name and machine. The ID is unknown
#so we can't lazily load the variant either. So we have to wait until all
#the rest is loaded properly and then assign the correct variant to the
#material files that were missing it.
_with_missing_variants = []
def _fillMissingVariants():
registry = ContainerRegistry.getInstance()
for variant_metadata in _with_missing_variants:
variants = registry.findContainersMetadata(definition = variant_metadata["definition"], name = variant_metadata["variant"])
if not variants:
Logger.log("w", "Could not find variant for variant-specific material {material_id}.".format(material_id = variant_metadata["id"]))
continue
variant_metadata["variant"] = variants[0]["id"]
ContainerRegistry.allMetadataLoaded.connect(_fillMissingVariants)

View file

@ -0,0 +1,12 @@
{
"Ultimaker 2": "ultimaker2",
"Ultimaker 2 Extended": "ultimaker2_extended",
"Ultimaker 2 Extended+": "ultimaker2_extended_plus",
"Ultimaker 2 Go": "ultimaker2_go",
"Ultimaker 2+": "ultimaker2_plus",
"Ultimaker 3": "ultimaker3",
"Ultimaker 3 Extended": "ultimaker3_extended",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus",
"IMADE3D JellyBOX": "imade3d_jellybox"
}

View file

@ -1,5 +1,4 @@
{
"id": "101Hero",
"version": 2,
"name": "101Hero",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "3Dator",
"version": 2,
"name": "3Dator",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "PRi3",
"name": "ABAX PRi3",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "PRi5",
"name": "ABAX PRi5",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "Titan",
"name": "ABAX Titan",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "alya3dp",
"name": "ALYA",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bfb",
"name": "BFB",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bq_hephestos",
"name": "BQ Prusa i3 Hephestos",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bq_hephestos_2",
"version": 2,
"name": "BQ Hephestos 2",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bq_hephestos_xl",
"version": 2,
"name": "BQ Prusa i3 Hephestos XL",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bq_witbox",
"version": 2,
"name": "BQ Witbox",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "bq_witbox_2",
"version": 2,
"name": "BQ Witbox 2",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "cartesio",
"name": "Cartesio",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "creality_cr10",
"name": "Creality CR-10",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "creality_cr10s4",
"name": "Creality CR-10 S4",
"version": 2,
"inherits": "creality_cr10",

View file

@ -1,5 +1,4 @@
{
"id": "creality_cr10s5",
"name": "Creality CR-10 S5",
"version": 2,
"inherits": "creality_cr10",

View file

@ -1,5 +1,4 @@
{
"id": "custom",
"version": 2,
"name": "Custom FDM printer",
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "Dagoma_discoeasy200",
"name": "Dagoma DiscoEasy200",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "Delta_Go",
"name": "Delta Go",
"version": 2,
"inherits": "fdmprinter",

View file

@ -1,5 +1,4 @@
{
"id": "deltabot",
"name": "DeltaBot",
"version": 2,
"inherits": "fdmprinter",

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