Merge remote-tracking branch 'origin/master' into CL-1266_cloud_association_for_manual_ips

This commit is contained in:
Ian Paschal 2019-03-27 16:53:31 +01:00
commit 49cb3de562
1047 changed files with 52357 additions and 5122 deletions

View file

@ -17,6 +17,7 @@ if(CURA_DEBUGMODE)
set(_cura_debugmode "ON") set(_cura_debugmode "ON")
endif() endif()
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura") set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
set(CURA_VERSION "master" CACHE STRING "Version name of Cura") set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'") set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")

19
contributing.md Normal file
View file

@ -0,0 +1,19 @@
Submitting bug reports
----------------------
Please submit bug reports for all of Cura and CuraEngine to the [Cura repository](https://github.com/Ultimaker/Cura/issues). There will be a template there to fill in. Depending on the type of issue, we will usually ask for the [Cura log](Logging Issues) or a project file.
If a bug report would contain private information, such as a proprietary 3D model, you may also e-mail us. Ask for contact information in the issue.
Bugs related to supporting certain types of printers can usually not be solved by the Cura maintainers, since we don't have access to every 3D printer model in the world either. We have to rely on external contributors to fix this. If it's something simple and obvious, such as a mistake in the start g-code, then we can directly fix it for you, but e.g. issues with USB cable connectivity are impossible for us to debug.
Requesting features
-------------------
The issue template in the Cura repository does not apply to feature requests. You can ignore it.
When requesting a feature, please describe clearly what you need and why you think this is valuable to users or what problem it solves.
Making pull requests
--------------------
If you want to propose a change to Cura's source code, please create a pull request in the appropriate repository (being [Cura](https://github.com/Ultimaker/Cura), [Uranium](https://github.com/Ultimaker/Uranium), [CuraEngine](https://github.com/Ultimaker/CuraEngine), [fdm_materials](https://github.com/Ultimaker/fdm_materials), [libArcus](https://github.com/Ultimaker/libArcus), [cura-build](https://github.com/Ultimaker/cura-build), [cura-build-environment](https://github.com/Ultimaker/cura-build-environment), [libSavitar](https://github.com/Ultimaker/libSavitar), [libCharon](https://github.com/Ultimaker/libCharon) or [cura-binary-data](https://github.com/Ultimaker/cura-binary-data)) and if your change requires changes on multiple of these repositories, please link them together so that we know to merge them together.
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`.

View file

@ -4,19 +4,31 @@
# --------- # ---------
# General constants used in Cura # General constants used in Cura
# --------- # ---------
DEFAULT_CURA_APP_NAME = "cura"
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura" DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master" DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = "" DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "6.0.0" DEFAULT_CURA_SDK_VERSION = "6.0.0"
try:
from cura.CuraVersion import CuraAppName # type: ignore
if CuraAppName == "":
CuraAppName = DEFAULT_CURA_APP_NAME
except ImportError:
CuraAppName = DEFAULT_CURA_APP_NAME
try: try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore from cura.CuraVersion import CuraAppDisplayName # type: ignore
if CuraAppDisplayName == "":
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
except ImportError: except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
try: try:
from cura.CuraVersion import CuraVersion # type: ignore from cura.CuraVersion import CuraVersion # type: ignore
if CuraVersion == "":
CuraVersion = DEFAULT_CURA_VERSION
except ImportError: except ImportError:
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value] CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
@ -32,5 +44,7 @@ except ImportError:
try: try:
from cura.CuraVersion import CuraSDKVersion # type: ignore from cura.CuraVersion import CuraSDKVersion # type: ignore
if CuraSDKVersion == "":
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
except ImportError: except ImportError:
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION CuraSDKVersion = DEFAULT_CURA_SDK_VERSION

View file

@ -217,11 +217,6 @@ class Arrange:
prio_slice = self._priority[min_y:max_y, min_x:max_x] prio_slice = self._priority[min_y:max_y, min_x:max_x]
prio_slice[new_occupied] = 999 prio_slice[new_occupied] = 999
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
# numpy.set_printoptions(linewidth=500, edgeitems=200)
# print(self._occupied.shape)
# print(self._occupied)
@property @property
def isEmpty(self): def isEmpty(self):
return self._is_empty return self._is_empty

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
@ -48,7 +48,6 @@ class ArrangeArray:
return self._count return self._count
def get(self, index): def get(self, index):
print(self._arrange)
return self._arrange[index] return self._arrange[index]
def getFirstEmpty(self): def getFirstEmpty(self):

View file

@ -740,13 +740,17 @@ class BuildVolume(SceneNode):
prime_tower_collision = False prime_tower_collision = False
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
for extruder_id in prime_tower_areas: for extruder_id in prime_tower_areas:
for prime_tower_area in prime_tower_areas[extruder_id]: for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
for area in result_areas[extruder_id]: for area in result_areas[extruder_id]:
if prime_tower_area.intersectsPolygon(area) is not None: if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True prime_tower_collision = True
break break
if prime_tower_collision: #Already found a collision. if prime_tower_collision: #Already found a collision.
break break
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
prime_tower_areas[extruder_id][i_area] = prime_tower_area.getMinkowskiHull(
Polygon.approximatedCircle(disallowed_border_size))
if not prime_tower_collision: if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
@ -786,6 +790,16 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2 prime_tower_y = prime_tower_y + machine_depth / 2
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
brim_size = (
extruder.getProperty("brim_line_count", "value") *
extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
extruder.getProperty("initial_layer_line_width_factor", "value")
)
prime_tower_x -= brim_size
prime_tower_y += brim_size
if self._global_container_stack.getProperty("prime_tower_circular", "value"): if self._global_container_stack.getProperty("prime_tower_circular", "value"):
radius = prime_tower_size / 2 radius = prime_tower_size / 2
prime_tower_area = Polygon.approximatedCircle(radius) prime_tower_area = Polygon.approximatedCircle(radius)
@ -1025,7 +1039,9 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the skirt with. # We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0 bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "brim": elif (adhesion_type == "brim" or
(self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and
self._global_container_stack.getProperty("adhesion_type", "value") != "raft")):
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value") brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0 bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
@ -1084,7 +1100,7 @@ class BuildVolume(SceneNode):
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"] _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"] _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.

View file

@ -1,10 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING, Optional, Callable from typing import cast, TYPE_CHECKING, Optional, Callable, List
import numpy import numpy
@ -38,10 +38,8 @@ from UM.Settings.Validator import Validator
from UM.Message import Message from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Decorators import deprecated
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
@ -136,7 +134,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions. # SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings. # changes of the settings.
SettingVersion = 6 SettingVersion = 7
Created = False Created = False
@ -156,7 +154,7 @@ class CuraApplication(QtApplication):
Q_ENUMS(ResourceTypes) Q_ENUMS(ResourceTypes)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(name = "cura", super().__init__(name = ApplicationMetadata.CuraAppName,
app_display_name = ApplicationMetadata.CuraAppDisplayName, app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion, version = ApplicationMetadata.CuraVersion,
api_version = ApplicationMetadata.CuraSDKVersion, api_version = ApplicationMetadata.CuraSDKVersion,
@ -309,7 +307,8 @@ class CuraApplication(QtApplication):
super().initialize() super().initialize()
self.__sendCommandToSingleInstance() self.__sendCommandToSingleInstance()
self.__initializeSettingDefinitionsAndFunctions() self._initializeSettingDefinitions()
self._initializeSettingFunctions()
self.__addAllResourcesAndContainerResources() self.__addAllResourcesAndContainerResources()
self.__addAllEmptyContainers() self.__addAllEmptyContainers()
self.__setLatestResouceVersionsForVersionUpgrade() self.__setLatestResouceVersionsForVersionUpgrade()
@ -338,31 +337,40 @@ class CuraApplication(QtApplication):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
Resources.addSearchPath(resource_path) Resources.addSearchPath(resource_path)
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in @classmethod
# SettingDefinition and SettingFunction. def _initializeSettingDefinitions(cls):
def __initializeSettingDefinitionsAndFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
# Need to do this before ContainerRegistry tries to load the machines # Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default=True,
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True) read_only=True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default=True,
read_only=True)
# this setting can be changed for each group in one-at-a-time mode # this setting can be changed for each group in one-at-a-time mode
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default=True,
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True) read_only=True)
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default=True,
read_only=True)
# From which stack the setting would inherit if not defined per object (handled in the engine) # From which stack the setting would inherit if not defined per object (handled in the engine)
# AND for settings which are not settable_per_mesh: # AND for settings which are not settable_per_mesh:
# which extruder is the only extruder this setting is obtained from # which extruder is the only extruder this setting is obtained from
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1", depends_on = "value") SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default="-1",
depends_on="value")
# For settings which are not settable_per_mesh and not settable_per_extruder: # For settings which are not settable_per_mesh and not settable_per_extruder:
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders # A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value") SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default=None,
depends_on="value")
SettingDefinition.addSettingType("extruder", None, str, Validator) SettingDefinition.addSettingType("extruder", None, str, Validator)
SettingDefinition.addSettingType("optional_extruder", None, str, None) SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None) SettingDefinition.addSettingType("[int]", None, str, None)
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction.
def _initializeSettingFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
@ -435,6 +443,7 @@ class CuraApplication(QtApplication):
def startSplashWindowPhase(self) -> None: def startSplashWindowPhase(self) -> None:
super().startSplashWindowPhase() super().startSplashWindowPhase()
if not self.getIsHeadLess():
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
self.setRequiredPlugins([ self.setRequiredPlugins([
@ -666,7 +675,7 @@ class CuraApplication(QtApplication):
## Handle loading of all plugin types (and the backend explicitly) ## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistry # \sa PluginRegistry
def _loadPlugins(self): def _loadPlugins(self) -> None:
self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter) self._plugin_registry.addType("profile_writer", self._addProfileWriter)
@ -1107,88 +1116,6 @@ class CuraApplication(QtApplication):
self._platform_activity = True if count > 0 else False self._platform_activity = True if count > 0 else False
self.activityChanged.emit() self.activityChanged.emit()
# Remove all selected objects from the scene.
@pyqtSlot()
@deprecated("Moved to CuraActions", "2.6")
def deleteSelection(self):
if not self.getController().getToolsEnabled():
return
removed_group_nodes = []
op = GroupedOperation()
nodes = Selection.getAllSelectedObjects()
for node in nodes:
op.addOperation(RemoveSceneNodeOperation(node))
group_node = node.getParent()
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
if len(remaining_nodes_in_group) == 1:
removed_group_nodes.append(group_node)
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node))
op.push()
## Remove an object from the scene.
# Note that this only removes an object if it is selected.
@pyqtSlot("quint64")
@deprecated("Use deleteSelection instead", "2.6")
def deleteObject(self, object_id):
if not self.getController().getToolsEnabled():
return
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if node:
op = GroupedOperation()
op.addOperation(RemoveSceneNodeOperation(node))
group_node = node.getParent()
if group_node:
# Note that at this point the node has not yet been deleted
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node))
op.push()
## Create a number of copies of existing object.
# \param object_id
# \param count number of copies
# \param min_offset minimum offset to other objects.
@pyqtSlot("quint64", int)
@deprecated("Use CuraActions::multiplySelection", "2.6")
def multiplyObject(self, object_id, count, min_offset = 8):
node = self.getController().getScene().findObject(object_id)
if not node:
node = Selection.getSelectedObject(0)
while node.getParent() and node.getParent().callDecoration("isGroup"):
node = node.getParent()
job = MultiplyObjectsJob([node], count, min_offset)
job.start()
return
## Center object on platform.
@pyqtSlot("quint64")
@deprecated("Use CuraActions::centerSelection", "2.6")
def centerObject(self, object_id):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if not node:
return
if node.getParent() and node.getParent().callDecoration("isGroup"):
node = node.getParent()
if node:
op = SetTransformOperation(node, Vector())
op.push()
## Select all nodes containing mesh data in the scene. ## Select all nodes containing mesh data in the scene.
@pyqtSlot() @pyqtSlot()
def selectAll(self): def selectAll(self):
@ -1268,61 +1195,74 @@ class CuraApplication(QtApplication):
## Arrange all objects. ## Arrange all objects.
@pyqtSlot() @pyqtSlot()
def arrangeObjectsToAllBuildPlates(self): def arrangeObjectsToAllBuildPlates(self) -> None:
nodes = [] nodes_to_arrange = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) parent_node = node.getParent()
if parent_node and parent_node.callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data continue # i.e. node with layer data
bounding_box = node.getBoundingBox()
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
nodes.append(node) nodes_to_arrange.append(node)
job = ArrangeObjectsAllBuildPlatesJob(nodes) job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
job.start() job.start()
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
# Single build plate # Single build plate
@pyqtSlot() @pyqtSlot()
def arrangeAll(self): def arrangeAll(self) -> None:
nodes = [] nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
parent_node = node.getParent()
if parent_node and parent_node.callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.isSelectable():
continue # i.e. node with layer data continue # i.e. node with layer data
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data continue # i.e. node with layer data
if node.callDecoration("getBuildPlateNumber") == active_build_plate: if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: bounding_box = node.getBoundingBox()
nodes.append(node) if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
self.arrange(nodes, fixed_nodes = []) nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, fixed_nodes = [])
## Arrange a set of nodes given a set of fixed nodes ## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place # \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes, fixed_nodes): def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start() job.start()
## Reload all mesh data on the screen from file. ## Reload all mesh data on the screen from file.
@pyqtSlot() @pyqtSlot()
def reloadAll(self): def reloadAll(self) -> None:
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
has_merged_nodes = False has_merged_nodes = False
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, CuraSceneNode) or not node.getMeshData(): if not isinstance(node, CuraSceneNode) or not node.getMeshData():
if node.getName() == "MergedMesh": if node.getName() == "MergedMesh":
has_merged_nodes = True has_merged_nodes = True
@ -1337,7 +1277,7 @@ class CuraApplication(QtApplication):
file_name = node.getMeshData().getFileName() file_name = node.getMeshData().getFileName()
if file_name: if file_name:
job = ReadMeshJob(file_name) job = ReadMeshJob(file_name)
job._node = node job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished) job.finished.connect(self._reloadMeshFinished)
if has_merged_nodes: if has_merged_nodes:
job.finished.connect(self.updateOriginOfMergedMeshes) job.finished.connect(self.updateOriginOfMergedMeshes)
@ -1346,20 +1286,8 @@ class CuraApplication(QtApplication):
else: else:
Logger.log("w", "Unable to reload data because we don't have a filename.") Logger.log("w", "Unable to reload data because we don't have a filename.")
## Get logging data of the backend engine
# \returns \type{string} Logging data
@pyqtSlot(result = str)
def getEngineLog(self):
log = ""
for entry in self.getBackend().getLog():
log += entry.decode()
return log
@pyqtSlot("QStringList") @pyqtSlot("QStringList")
def setExpandedCategories(self, categories): def setExpandedCategories(self, categories: List[str]) -> None:
categories = list(set(categories)) categories = list(set(categories))
categories.sort() categories.sort()
joined = ";".join(categories) joined = ";".join(categories)
@ -1370,7 +1298,7 @@ class CuraApplication(QtApplication):
expandedCategoriesChanged = pyqtSignal() expandedCategoriesChanged = pyqtSignal()
@pyqtProperty("QStringList", notify = expandedCategoriesChanged) @pyqtProperty("QStringList", notify = expandedCategoriesChanged)
def expandedCategories(self): def expandedCategories(self) -> List[str]:
return self.getPreferences().getValue("cura/categories_expanded").split(";") return self.getPreferences().getValue("cura/categories_expanded").split(";")
@pyqtSlot() @pyqtSlot()
@ -1420,13 +1348,12 @@ class CuraApplication(QtApplication):
## Updates origin position of all merged meshes ## Updates origin position of all merged meshes
# \param jobNode \type{Job} empty object which passed which is required by JobQueue def updateOriginOfMergedMeshes(self, _):
def updateOriginOfMergedMeshes(self, jobNode):
group_nodes = [] group_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator # Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
for decorator in node.getDecorators(): for decorator in node.getDecorators():
if isinstance(decorator, GroupDecorator): if isinstance(decorator, GroupDecorator):
group_nodes.append(node) group_nodes.append(node)
@ -1470,7 +1397,7 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def groupSelected(self): def groupSelected(self) -> None:
# Create a group-node # Create a group-node
group_node = CuraSceneNode() group_node = CuraSceneNode()
group_decorator = GroupDecorator() group_decorator = GroupDecorator()
@ -1486,7 +1413,8 @@ class CuraApplication(QtApplication):
# Remove nodes that are directly parented to another selected node from the selection so they remain parented # Remove nodes that are directly parented to another selected node from the selection so they remain parented
selected_nodes = Selection.getAllSelectedObjects().copy() selected_nodes = Selection.getAllSelectedObjects().copy()
for node in selected_nodes: for node in selected_nodes:
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"): parent = node.getParent()
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
Selection.remove(node) Selection.remove(node)
# Move selected nodes into the group-node # Move selected nodes into the group-node
@ -1498,7 +1426,7 @@ class CuraApplication(QtApplication):
Selection.add(group_node) Selection.add(group_node)
@pyqtSlot() @pyqtSlot()
def ungroupSelected(self): def ungroupSelected(self) -> None:
selected_objects = Selection.getAllSelectedObjects().copy() selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects: for node in selected_objects:
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
@ -1521,7 +1449,7 @@ class CuraApplication(QtApplication):
# Note: The group removes itself from the scene once all its children have left it, # Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged # see GroupDecorator._onChildrenChanged
def _createSplashScreen(self): def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
if self._is_headless: if self._is_headless:
return None return None
return CuraSplashScreen.CuraSplashScreen() return CuraSplashScreen.CuraSplashScreen()

View file

@ -1,6 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "@CURA_APP_NAME@"
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@" CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
CuraVersion = "@CURA_VERSION@" CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@" CuraBuildType = "@CURA_BUILDTYPE@"

View file

@ -1,9 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, QTimer from PyQt5.QtCore import pyqtProperty, Qt, QTimer
from cura.PrinterOutputDevice import ConnectionType from cura.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry

View file

@ -7,43 +7,36 @@ from UM.Mesh.MeshBuilder import MeshBuilder
from .LayerData import LayerData from .LayerData import LayerData
import numpy import numpy
from typing import Dict, Optional
## Builder class for constructing a LayerData object ## Builder class for constructing a LayerData object
class LayerDataBuilder(MeshBuilder): class LayerDataBuilder(MeshBuilder):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._layers = {} self._layers = {} # type: Dict[int, Layer]
self._element_counts = {} self._element_counts = {} # type: Dict[int, int]
def addLayer(self, layer): def addLayer(self, layer: int) -> None:
if layer not in self._layers: if layer not in self._layers:
self._layers[layer] = Layer(layer) self._layers[layer] = Layer(layer)
def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate): def getLayer(self, layer: int) -> Optional[Layer]:
if layer not in self._layers: return self._layers.get(layer)
self.addLayer(layer)
p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate) def getLayers(self) -> Dict[int, Layer]:
self._layers[layer].polygons.append(p)
def getLayer(self, layer):
if layer in self._layers:
return self._layers[layer]
def getLayers(self):
return self._layers return self._layers
def getElementCounts(self): def getElementCounts(self) -> Dict[int, int]:
return self._element_counts return self._element_counts
def setLayerHeight(self, layer, height): def setLayerHeight(self, layer: int, height: float) -> None:
if layer not in self._layers: if layer not in self._layers:
self.addLayer(layer) self.addLayer(layer)
self._layers[layer].setHeight(height) self._layers[layer].setHeight(height)
def setLayerThickness(self, layer, thickness): def setLayerThickness(self, layer: int, thickness: float) -> None:
if layer not in self._layers: if layer not in self._layers:
self.addLayer(layer) self.addLayer(layer)
@ -71,7 +64,7 @@ class LayerDataBuilder(MeshBuilder):
vertex_offset = 0 vertex_offset = 0
index_offset = 0 index_offset = 0
for layer, data in sorted(self._layers.items()): for layer, data in sorted(self._layers.items()):
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices) vertex_offset, index_offset = data.build(vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
self._element_counts[layer] = data.elementCount self._element_counts[layer] = data.elementCount
self.addVertices(vertices) self.addVertices(vertices)

View file

@ -1,13 +1,25 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from cura.LayerData import LayerData
## Simple decorator to indicate a scene node holds layer data. ## Simple decorator to indicate a scene node holds layer data.
class LayerDataDecorator(SceneNodeDecorator): class LayerDataDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._layer_data = None self._layer_data = None # type: Optional[LayerData]
def getLayerData(self): def getLayerData(self) -> Optional["LayerData"]:
return self._layer_data return self._layer_data
def setLayerData(self, layer_data): def setLayerData(self, layer_data: LayerData) -> None:
self._layer_data = layer_data self._layer_data = layer_data
def __deepcopy__(self, memo) -> "LayerDataDecorator":
copied_decorator = LayerDataDecorator()
copied_decorator._layer_data = self._layer_data
return copied_decorator

View file

@ -2,9 +2,11 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
from typing import Any from typing import Any, Optional
import numpy import numpy
from UM.Logger import Logger
class LayerPolygon: class LayerPolygon:
NoneType = 0 NoneType = 0
@ -18,22 +20,24 @@ class LayerPolygon:
MoveCombingType = 8 MoveCombingType = 8
MoveRetractionType = 9 MoveRetractionType = 9
SupportInterfaceType = 10 SupportInterfaceType = 10
__number_of_types = 11 PrimeTower = 11
__number_of_types = 12
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
## LayerPolygon, used in ProcessSlicedLayersJob ## LayerPolygon, used in ProcessSlicedLayersJob
# \param extruder # \param extruder The position of the extruder
# \param line_types array with line_types # \param line_types array with line_types
# \param data new_points # \param data new_points
# \param line_widths array with line widths # \param line_widths array with line widths
# \param line_thicknesses: array with type as index and thickness as value # \param line_thicknesses: array with type as index and thickness as value
# \param line_feedrates array with line feedrates # \param line_feedrates array with line feedrates
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates): def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
self._extruder = extruder self._extruder = extruder
self._types = line_types self._types = line_types
for i in range(len(self._types)): for i in range(len(self._types)):
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine. if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
Logger.log("w", "Found an unknown line type: %s", i)
self._types[i] = self.NoneType self._types[i] = self.NoneType
self._data = data self._data = data
self._line_widths = line_widths self._line_widths = line_widths
@ -53,16 +57,16 @@ class LayerPolygon:
# Buffering the colors shouldn't be necessary as it is not # Buffering the colors shouldn't be necessary as it is not
# re-used and can save alot of memory usage. # re-used and can save alot of memory usage.
self._color_map = LayerPolygon.getColorMap() self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] self._colors = self._color_map[self._types] # type: numpy.ndarray
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool) self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
self._build_cache_line_mesh_mask = None self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
def buildCache(self): def buildCache(self) -> None:
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool) self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
@ -90,10 +94,14 @@ class LayerPolygon:
# \param extruders : vertex numpy array to be filled # \param extruders : vertex numpy array to be filled
# \param line_types : vertex numpy array to be filled # \param line_types : vertex numpy array to be filled
# \param indices : index numpy array to be filled # \param indices : index numpy array to be filled
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices): def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
self.buildCache() self.buildCache()
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
Logger.log("w", "Failed to build cache for layer polygon")
return
line_mesh_mask = self._build_cache_line_mesh_mask line_mesh_mask = self._build_cache_line_mesh_mask
needed_points_list = self._build_cache_needed_points needed_points_list = self._build_cache_needed_points
@ -236,7 +244,8 @@ class LayerPolygon:
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_support_interface").getRgbF() # SupportInterfaceType theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_prime_tower").getRgbF()
]) ])
return cls.__color_map return cls.__color_map

View file

@ -219,7 +219,7 @@ class MaterialManager(QObject):
root_material_id = material_metadata["base_file"] root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"] definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"] approximate_diameter = str(material_metadata["approximate_diameter"])
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
@ -332,7 +332,6 @@ class MaterialManager(QObject):
buildplate_node = nozzle_node.getChildNode(buildplate_name) buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. buildplate-specific material # 1. buildplate-specific material
# 2. nozzle-specific material # 2. nozzle-specific material
@ -537,16 +536,40 @@ class MaterialManager(QObject):
return return
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
# list, so removeContainer() can ignore those ones.
for node in nodes_to_remove:
container_id = node.getMetaDataEntry("id", "")
results = self._container_registry.findContainers(id = container_id)
if not results:
self._container_registry.addWrongContainerId(container_id)
for node in nodes_to_remove: for node in nodes_to_remove:
self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
# #
# Methods for GUI # Methods for GUI
# #
@pyqtSlot("QVariant", result=bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
return False
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
#
# Sets the new name for the given material.
#
@pyqtSlot("QVariant", str) @pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
@ -649,10 +672,9 @@ class MaterialManager(QObject):
extruder_stack = machine_manager.activeStack extruder_stack = machine_manager.activeStack
machine_definition = self._application.getGlobalContainerStack().definition machine_definition = self._application.getGlobalContainerStack().definition
preferred_material = machine_definition.getMetaDataEntry("preferred_material") root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
approximate_diameter = str(extruder_stack.approximateMaterialDiameter) approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
root_material_id = preferred_material if preferred_material else "generic_pla"
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
material_group = self.getMaterialGroup(root_material_id) material_group = self.getMaterialGroup(root_material_id)

View file

@ -20,7 +20,6 @@ class BaseMaterialsModel(ListModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
self._application = CuraApplication.getInstance() self._application = CuraApplication.getInstance()

View file

@ -209,6 +209,7 @@ class QualityManager(QObject):
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder # Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities. # qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False has_extruder_specific_qualities = False
@ -441,7 +442,8 @@ class QualityManager(QObject):
quality_changes_group = quality_model_item["quality_changes_group"] quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None: if quality_changes_group is None:
# create global quality changes only # create global quality changes only
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name, new_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
global_stack, None) global_stack, None)
self._container_registry.addContainer(new_quality_changes) self._container_registry.addContainer(new_quality_changes)
else: else:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime from datetime import datetime
import json import json
@ -9,29 +9,29 @@ from typing import Optional
import requests import requests
from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
## Class containing several helpers to deal with the authorization flow.
# Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers: class AuthorizationHelpers:
def __init__(self, settings: "OAuth2Settings") -> None: def __init__(self, settings: "OAuth2Settings") -> None:
self._settings = settings self._settings = settings
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
@property @property
# The OAuth2 settings object. ## The OAuth2 settings object.
def settings(self) -> "OAuth2Settings": def settings(self) -> "OAuth2Settings":
return self._settings return self._settings
# Request the access token from the authorization server. ## Request the access token from the authorization server.
# \param authorization_code: The authorization code from the 1st step. # \param authorization_code: The authorization code from the 1st step.
# \param verification_code: The verification code needed for the PKCE extension. # \param verification_code: The verification code needed for the PKCE
# \return: An AuthenticationResponse object. # extension.
# \return An AuthenticationResponse object.
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -46,9 +46,9 @@ class AuthorizationHelpers:
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
# Request the access token from the authorization server using a refresh token. ## Request the access token from the authorization server using a refresh token.
# \param refresh_token: # \param refresh_token:
# \return: An AuthenticationResponse object. # \return An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -63,9 +63,9 @@ class AuthorizationHelpers:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
@staticmethod @staticmethod
# Parse the token response from the authorization server into an AuthenticationResponse object. ## Parse the token response from the authorization server into an AuthenticationResponse object.
# \param token_response: The JSON string data response from the authorization server. # \param token_response: The JSON string data response from the authorization server.
# \return: An AuthenticationResponse object. # \return An AuthenticationResponse object.
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
token_data = None token_data = None
@ -75,7 +75,7 @@ class AuthorizationHelpers:
Logger.log("w", "Could not parse token response data: %s", token_response.text) Logger.log("w", "Could not parse token response data: %s", token_response.text)
if not token_data: if not token_data:
return AuthenticationResponse(success=False, err_message="Could not read response.") return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
if token_response.status_code not in (200, 201): if token_response.status_code not in (200, 201):
return AuthenticationResponse(success = False, err_message = token_data["error_description"]) return AuthenticationResponse(success = False, err_message = token_data["error_description"])
@ -88,9 +88,9 @@ class AuthorizationHelpers:
scope=token_data["scope"], scope=token_data["scope"],
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
# Calls the authentication API endpoint to get the token data. ## Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token. # \param access_token: The encoded JWT token.
# \return: Dict containing some profile data. # \return Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]: def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
try: try:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
@ -114,15 +114,15 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
# Generate a 16-character verification code. ## Generate a 16-character verification code.
# \param code_length: How long should the code be? # \param code_length: How long should the code be?
def generateVerificationCode(code_length: int = 16) -> str: def generateVerificationCode(code_length: int = 16) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod
# Generates a base64 encoded sha512 encrypted version of a given string. ## Generates a base64 encoded sha512 encrypted version of a given string.
# \param verification_code: # \param verification_code:
# \return: The encrypted code in base64 format. # \return The encrypted code in base64 format.
def generateVerificationCodeChallenge(verification_code: str) -> str: def generateVerificationCodeChallenge(verification_code: str) -> str:
encoded = sha512(verification_code.encode()).digest() encoded = sha512(verification_code.encode()).digest()
return b64encode(encoded, altchars = b"_-").decode() return b64encode(encoded, altchars = b"_-").decode()

View file

@ -1,18 +1,20 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
from UM.i18n import i18nCatalog
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.OAuth2.Models import ResponseStatus from cura.OAuth2.Models import ResponseStatus
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
catalog = i18nCatalog("cura")
# This handler handles all HTTP requests on the local web server. ## This handler handles all HTTP requests on the local web server.
# It also requests the access token for the 2nd stage of the OAuth flow. # It also requests the access token for the 2nd stage of the OAuth flow.
class AuthorizationRequestHandler(BaseHTTPRequestHandler): class AuthorizationRequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server) -> None: def __init__(self, request, client_address, server) -> None:
@ -47,9 +49,9 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# This will cause the server to shut down, so we do it at the very end of the request handling. # This will cause the server to shut down, so we do it at the very end of the request handling.
self.authorization_callback(token_response) self.authorization_callback(token_response)
# Handler for the callback URL redirect. ## Handler for the callback URL redirect.
# \param query: Dict containing the HTTP query parameters. # \param query Dict containing the HTTP query parameters.
# \return: HTTP ResponseData containing a success page to show to the user. # \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
code = self._queryGet(query, "code") code = self._queryGet(query, "code")
if code and self.authorization_helpers is not None and self.verification_code is not None: if code and self.authorization_helpers is not None and self.verification_code is not None:
@ -61,14 +63,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog). # Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
token_response = AuthenticationResponse( token_response = AuthenticationResponse(
success = False, success = False,
err_message="Please give the required permissions when authorizing this application." err_message = catalog.i18nc("@message", "Please give the required permissions when authorizing this application.")
) )
else: else:
# We don't know what went wrong here, so instruct the user to check the logs. # We don't know what went wrong here, so instruct the user to check the logs.
token_response = AuthenticationResponse( token_response = AuthenticationResponse(
success = False, success = False,
error_message="Something unexpected happened when trying to log in, please try again." error_message = catalog.i18nc("@message", "Something unexpected happened when trying to log in, please try again.")
) )
if self.authorization_helpers is None: if self.authorization_helpers is None:
return ResponseData(), token_response return ResponseData(), token_response
@ -80,8 +82,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
), token_response ), token_response
## Handle all other non-existing server calls.
@staticmethod @staticmethod
# Handle all other non-existing server calls.
def _handleNotFound() -> ResponseData: def _handleNotFound() -> ResponseData:
return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.") return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.")
@ -95,7 +97,7 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
def _sendData(self, data: bytes) -> None: def _sendData(self, data: bytes) -> None:
self.wfile.write(data) self.wfile.write(data)
## Convenience helper for getting values from a pre-parsed query string
@staticmethod @staticmethod
# Convenience Helper for getting values from a pre-parsed query string
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]: def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]:
return query_data.get(key, [default])[0] return query_data.get(key, [default])[0]

View file

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from http.server import HTTPServer from http.server import HTTPServer
from typing import Callable, Any, TYPE_CHECKING from typing import Callable, Any, TYPE_CHECKING
@ -8,19 +9,19 @@ if TYPE_CHECKING:
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
# The authorization request callback handler server. ## The authorization request callback handler server.
# This subclass is needed to be able to pass some data to the request handler. # This subclass is needed to be able to pass some data to the request handler.
# This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after # This cannot be done on the request handler directly as the HTTPServer
# init. # creates an instance of the handler after init.
class AuthorizationRequestServer(HTTPServer): class AuthorizationRequestServer(HTTPServer):
# Set the authorization helpers instance on the request handler. ## Set the authorization helpers instance on the request handler.
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None: def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
# Set the authorization callback on the request handler. ## Set the authorization callback on the request handler.
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None: def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
# Set the verification code on the request handler. ## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None: def setVerificationCode(self, verification_code: str) -> None:
self.RequestHandlerClass.verification_code = verification_code # type: ignore self.RequestHandlerClass.verification_code = verification_code # type: ignore

View file

@ -25,12 +25,9 @@ if TYPE_CHECKING:
from UM.Preferences import Preferences from UM.Preferences import Preferences
## The authorization service is responsible for handling the login flow,
# storing user credentials and providing account information.
class AuthorizationService: class AuthorizationService:
"""
The authorization service is responsible for handling the login flow,
storing user credentials and providing account information.
"""
# Emit signal when authentication is completed. # Emit signal when authentication is completed.
onAuthStateChanged = Signal() onAuthStateChanged = Signal()
@ -60,12 +57,13 @@ class AuthorizationService:
if self._preferences: if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
# Get the user profile as obtained from the JWT (JSON Web Token). ## Get the user profile as obtained from the JWT (JSON Web Token).
# If the JWT is not yet parsed, calling this will take care of that. # If the JWT is not yet parsed, calling this will take care of that.
# \return UserProfile if a user is logged in, None otherwise. # \return UserProfile if a user is logged in, None otherwise.
# \sa _parseJWT # \sa _parseJWT
def getUserProfile(self) -> Optional["UserProfile"]: def getUserProfile(self) -> Optional["UserProfile"]:
if not self._user_profile: if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT.
try: try:
self._user_profile = self._parseJWT() self._user_profile = self._parseJWT()
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
@ -80,7 +78,7 @@ class AuthorizationService:
return self._user_profile return self._user_profile
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. ## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
# \return UserProfile if it was able to parse, None otherwise. # \return UserProfile if it was able to parse, None otherwise.
def _parseJWT(self) -> Optional["UserProfile"]: def _parseJWT(self) -> Optional["UserProfile"]:
if not self._auth_data or self._auth_data.access_token is None: if not self._auth_data or self._auth_data.access_token is None:
@ -100,7 +98,7 @@ class AuthorizationService:
return self._auth_helpers.parseJWT(self._auth_data.access_token) return self._auth_helpers.parseJWT(self._auth_data.access_token)
# Get the access token as provided by the response data. ## Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]: def getAccessToken(self) -> Optional[str]:
if self._auth_data is None: if self._auth_data is None:
Logger.log("d", "No auth data to retrieve the access_token from") Logger.log("d", "No auth data to retrieve the access_token from")
@ -116,7 +114,7 @@ class AuthorizationService:
return self._auth_data.access_token if self._auth_data else None return self._auth_data.access_token if self._auth_data else None
# Try to refresh the access token. This should be used when it has expired. ## Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None: def refreshAccessToken(self) -> None:
if self._auth_data is None or self._auth_data.refresh_token is None: if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.log("w", "Unable to refresh access token, since there is no refresh token.") Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
@ -126,15 +124,15 @@ class AuthorizationService:
self._storeAuthData(response) self._storeAuthData(response)
self.onAuthStateChanged.emit(logged_in = True) self.onAuthStateChanged.emit(logged_in = True)
else: else:
self.onAuthStateChanged(logged_in = False) self.onAuthStateChanged.emit(logged_in = False)
# Delete the authentication data that we have stored locally (eg; logout) ## Delete the authentication data that we have stored locally (eg; logout)
def deleteAuthData(self) -> None: def deleteAuthData(self) -> None:
if self._auth_data is not None: if self._auth_data is not None:
self._storeAuthData() self._storeAuthData()
self.onAuthStateChanged.emit(logged_in = False) self.onAuthStateChanged.emit(logged_in = False)
# Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login. ## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self) -> None: def startAuthorizationFlow(self) -> None:
Logger.log("d", "Starting new OAuth2 flow...") Logger.log("d", "Starting new OAuth2 flow...")
@ -161,7 +159,7 @@ class AuthorizationService:
# Start a local web server to receive the callback URL on. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code)
# Callback method for the authentication flow. ## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
if auth_response.success: if auth_response.success:
self._storeAuthData(auth_response) self._storeAuthData(auth_response)
@ -170,7 +168,7 @@ class AuthorizationService:
self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message) self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
self._server.stop() # Stop the web server at all times. self._server.stop() # Stop the web server at all times.
# Load authentication data from preferences. ## Load authentication data from preferences.
def loadAuthDataFromPreferences(self) -> None: def loadAuthDataFromPreferences(self) -> None:
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to load authentication data, since no preference has been set!") Logger.log("e", "Unable to load authentication data, since no preference has been set!")
@ -194,7 +192,7 @@ class AuthorizationService:
except ValueError: except ValueError:
Logger.logException("w", "Could not load auth data from preferences") Logger.logException("w", "Could not load auth data from preferences")
# Store authentication data in preferences. ## Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None: def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!") Logger.log("e", "Unable to save authentication data, since no preference has been set!")

View file

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import threading import threading
from typing import Optional, Callable, Any, TYPE_CHECKING from typing import Optional, Callable, Any, TYPE_CHECKING
@ -14,12 +15,15 @@ if TYPE_CHECKING:
class LocalAuthorizationServer: class LocalAuthorizationServer:
# The local LocalAuthorizationServer takes care of the oauth2 callbacks. ## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
# Once the flow is completed, this server should be closed down again by calling stop() # Once the flow is completed, this server should be closed down again by
# \param auth_helpers: An instance of the authorization helpers class. # calling stop()
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes. # \param auth_helpers An instance of the authorization helpers class.
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped # \param auth_state_changed_callback A callback function to be called when
# at shutdown. Their resources (e.g. open files) may never be released. # the authorization state changes.
# \param daemon Whether the server thread should be run in daemon mode.
# Note: Daemon threads are abruptly stopped at shutdown. Their resources
# (e.g. open files) may never be released.
def __init__(self, auth_helpers: "AuthorizationHelpers", def __init__(self, auth_helpers: "AuthorizationHelpers",
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
daemon: bool) -> None: daemon: bool) -> None:
@ -30,8 +34,8 @@ class LocalAuthorizationServer:
self._auth_state_changed_callback = auth_state_changed_callback self._auth_state_changed_callback = auth_state_changed_callback
self._daemon = daemon self._daemon = daemon
# Starts the local web server to handle the authorization callback. ## Starts the local web server to handle the authorization callback.
# \param verification_code: The verification code part of the OAuth2 client identification. # \param verification_code The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None: def start(self, verification_code: str) -> None:
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
@ -54,7 +58,7 @@ class LocalAuthorizationServer:
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
self._web_server_thread.start() self._web_server_thread.start()
# Stops the web server if it was running. It also does some cleanup. ## Stops the web server if it was running. It also does some cleanup.
def stop(self) -> None: def stop(self) -> None:
Logger.log("d", "Stopping local oauth2 web server...") Logger.log("d", "Stopping local oauth2 web server...")

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional
@ -8,7 +8,7 @@ class BaseModel:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
# OAuth OAuth2Settings data template. ## OAuth OAuth2Settings data template.
class OAuth2Settings(BaseModel): class OAuth2Settings(BaseModel):
CALLBACK_PORT = None # type: Optional[int] CALLBACK_PORT = None # type: Optional[int]
OAUTH_SERVER_URL = None # type: Optional[str] OAUTH_SERVER_URL = None # type: Optional[str]
@ -20,14 +20,14 @@ class OAuth2Settings(BaseModel):
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
# User profile data template. ## User profile data template.
class UserProfile(BaseModel): class UserProfile(BaseModel):
user_id = None # type: Optional[str] user_id = None # type: Optional[str]
username = None # type: Optional[str] username = None # type: Optional[str]
profile_image_url = None # type: Optional[str] profile_image_url = None # type: Optional[str]
# Authentication data template. ## Authentication data template.
class AuthenticationResponse(BaseModel): class AuthenticationResponse(BaseModel):
"""Data comes from the token response with success flag and error message added.""" """Data comes from the token response with success flag and error message added."""
success = True # type: bool success = True # type: bool
@ -40,21 +40,20 @@ class AuthenticationResponse(BaseModel):
received_at = None # type: Optional[str] received_at = None # type: Optional[str]
# Response status template. ## Response status template.
class ResponseStatus(BaseModel): class ResponseStatus(BaseModel):
code = 200 # type: int code = 200 # type: int
message = "" # type: str message = "" # type: str
# Response data template. ## Response data template.
class ResponseData(BaseModel): class ResponseData(BaseModel):
status = None # type: ResponseStatus status = None # type: ResponseStatus
data_stream = None # type: Optional[bytes] data_stream = None # type: Optional[bytes]
redirect_uri = None # type: Optional[str] redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str content_type = "text/html" # type: str
## Possible HTTP responses.
# Possible HTTP responses.
HTTP_STATUS = { HTTP_STATUS = {
"OK": ResponseStatus(code = 200, message = "OK"), "OK": ResponseStatus(code = 200, message = "OK"),
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),

View file

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

View file

@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
return group return group
def __repr__(self): def __repr__(self):
return "PlatformPhysicsOperation(translation = {0})".format(self._translation) return "PlatformPhysicsOp.(trans.={0})".format(self._translation)

View file

@ -69,10 +69,8 @@ class PrintInformation(QObject):
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
self._onActiveMaterialsChanged()
self._material_amounts = [] # type: List[float] self._material_amounts = [] # type: List[float]
self._onActiveMaterialsChanged()
def initializeCuraMessagePrintTimeProperties(self) -> None: def initializeCuraMessagePrintTimeProperties(self) -> None:
self._current_print_time = {} # type: Dict[int, Duration] self._current_print_time = {} # type: Dict[int, Duration]
@ -220,6 +218,7 @@ class PrintInformation(QObject):
material_guid = material.getMetaDataEntry("GUID") material_guid = material.getMetaDataEntry("GUID")
material_name = material.getName() material_name = material.getName()
if material_guid in material_preference_values: if material_guid in material_preference_values:
material_values = material_preference_values[material_guid] material_values = material_preference_values[material_guid]

View file

@ -40,7 +40,9 @@ class ConfigurationModel(QObject):
return self._extruder_configurations return self._extruder_configurations
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None: def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
if self._buildplate_configuration != buildplate_configuration:
self._buildplate_configuration = buildplate_configuration self._buildplate_configuration = buildplate_configuration
self.configurationChanged.emit()
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
def buildplateConfiguration(self) -> str: def buildplateConfiguration(self) -> str:

View file

@ -81,8 +81,8 @@ class GenericOutputController(PrinterOutputController):
self._output_device.cancelPrint() self._output_device.cancelPrint()
pass pass
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None: def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
self._output_device.sendCommand("M140 S%s" % temperature) self._output_device.sendCommand("M140 S%s" % round(temperature)) # The API doesn't allow floating point.
def _onTargetBedTemperatureChanged(self) -> None: def _onTargetBedTemperatureChanged(self) -> None:
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0: if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:

View file

@ -310,11 +310,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def _createNetworkManager(self) -> None: def _createNetworkManager(self) -> None:
Logger.log("d", "Creating network manager") Logger.log("d", "Creating network manager")
if self._manager: if self._manager:
self._manager.finished.disconnect(self.__handleOnFinished) self._manager.finished.disconnect(self._handleOnFinished)
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._manager.finished.connect(self.__handleOnFinished) self._manager.finished.connect(self._handleOnFinished)
self._last_manager_create_time = time() self._last_manager_create_time = time()
self._manager.authenticationRequired.connect(self._onAuthenticationRequired) self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
@ -325,7 +325,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if on_finished is not None: if on_finished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
def __handleOnFinished(self, reply: QNetworkReply) -> None: def _handleOnFinished(self, reply: QNetworkReply) -> None:
# Due to garbage collection, we need to cache certain bits of post operations. # Due to garbage collection, we need to cache certain bits of post operations.
# As we don't want to keep them around forever, delete them if we get a reply. # As we don't want to keep them around forever, delete them if we get a reply.
if reply.operation() == QNetworkAccessManager.PostOperation: if reply.operation() == QNetworkAccessManager.PostOperation:

View file

@ -12,6 +12,7 @@ if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
class PrintJobOutputModel(QObject): class PrintJobOutputModel(QObject):
stateChanged = pyqtSignal() stateChanged = pyqtSignal()
timeTotalChanged = pyqtSignal() timeTotalChanged = pyqtSignal()
@ -44,7 +45,7 @@ class PrintJobOutputModel(QObject):
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged) @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
def compatibleMachineFamilies(self): def compatibleMachineFamilies(self):
# Hack; Some versions of cluster will return a family more than once... # Hack; Some versions of cluster will return a family more than once...
return set(self._compatible_machine_families) return list(set(self._compatible_machine_families))
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None: def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
if self._compatible_machine_families != compatible_machine_families: if self._compatible_machine_families != compatible_machine_families:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger from UM.Logger import Logger
@ -25,10 +25,10 @@ class PrinterOutputController:
self.can_update_firmware = False self.can_update_firmware = False
self._output_device = output_device self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None: def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: float) -> None:
Logger.log("w", "Set target hotend temperature not implemented in controller") Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None: def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
Logger.log("w", "Set target bed temperature not implemented in controller") Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None: def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
@ -22,7 +22,7 @@ class PrinterOutputModel(QObject):
nameChanged = pyqtSignal() nameChanged = pyqtSignal()
headPositionChanged = pyqtSignal() headPositionChanged = pyqtSignal()
keyChanged = pyqtSignal() keyChanged = pyqtSignal()
printerTypeChanged = pyqtSignal() typeChanged = pyqtSignal()
buildplateChanged = pyqtSignal() buildplateChanged = pyqtSignal()
cameraUrlChanged = pyqtSignal() cameraUrlChanged = pyqtSignal()
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
@ -30,8 +30,8 @@ class PrinterOutputModel(QObject):
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None: def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
super().__init__(parent) super().__init__(parent)
self._bed_temperature = -1 # Use -1 for no heated bed. self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
self._target_bed_temperature = 0 self._target_bed_temperature = 0 # type: float
self._name = "" self._name = ""
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
@ -73,7 +73,7 @@ class PrinterOutputModel(QObject):
def isPreheating(self) -> bool: def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
@pyqtProperty(str, notify = printerTypeChanged) @pyqtProperty(str, notify = typeChanged)
def type(self) -> str: def type(self) -> str:
return self._printer_type return self._printer_type
@ -81,7 +81,7 @@ class PrinterOutputModel(QObject):
if self._printer_type != printer_type: if self._printer_type != printer_type:
self._printer_type = printer_type self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type self._printer_configuration.printerType = self._printer_type
self.printerTypeChanged.emit() self.typeChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify = buildplateChanged) @pyqtProperty(str, notify = buildplateChanged)
@ -179,7 +179,6 @@ class PrinterOutputModel(QObject):
return self._name return self._name
def setName(self, name: str) -> None: def setName(self, name: str) -> None:
self._setName(name)
self.updateName(name) self.updateName(name)
def updateName(self, name: str) -> None: def updateName(self, name: str) -> None:
@ -188,19 +187,19 @@ class PrinterOutputModel(QObject):
self.nameChanged.emit() self.nameChanged.emit()
## Update the bed temperature. This only changes it locally. ## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature: int) -> None: def updateBedTemperature(self, temperature: float) -> None:
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
self._bed_temperature = temperature self._bed_temperature = temperature
self.bedTemperatureChanged.emit() self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature: int) -> None: def updateTargetBedTemperature(self, temperature: float) -> None:
if self._target_bed_temperature != temperature: if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit() self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote. ## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int) @pyqtSlot(float)
def setTargetBedTemperature(self, temperature: int) -> None: def setTargetBedTemperature(self, temperature: float) -> None:
self._controller.setTargetBedTemperature(self, temperature) self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature) self.updateTargetBedTemperature(temperature)
@ -229,12 +228,12 @@ class PrinterOutputModel(QObject):
def state(self) -> str: def state(self) -> str:
return self._printer_state return self._printer_state
@pyqtProperty(int, notify=bedTemperatureChanged) @pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self) -> int: def bedTemperature(self) -> float:
return self._bed_temperature return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged) @pyqtProperty(float, notify = targetBedTemperatureChanged)
def targetBedTemperature(self) -> int: def targetBedTemperature(self) -> float:
return self._target_bed_temperature return self._target_bed_temperature
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all

View file

@ -112,21 +112,21 @@ class CuraSceneNode(SceneNode):
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
def _calculateAABB(self) -> None: def _calculateAABB(self) -> None:
self._aabb = None
if self._mesh_data: if self._mesh_data:
aabb = self._mesh_data.getExtents(self.getWorldTransformation()) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
position = self.getWorldPosition()
aabb = AxisAlignedBox(minimum = position, maximum = position)
for child in self._children: for child in self._children:
if child.callDecoration("isNonPrintingMesh"): if child.callDecoration("isNonPrintingMesh"):
# Non-printing-meshes inside a group should not affect push apart or drop to build plate # Non-printing-meshes inside a group should not affect push apart or drop to build plate
continue continue
if aabb is None: if not child._mesh_data:
aabb = child.getBoundingBox() # Nodes without mesh data should not affect bounding boxes of their parents.
continue
if self._aabb is None:
self._aabb = child.getBoundingBox()
else: else:
aabb = aabb + child.getBoundingBox() self._aabb = self._aabb + child.getBoundingBox()
self._aabb = aabb
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":

View file

@ -10,7 +10,7 @@ class GCodeListDecorator(SceneNodeDecorator):
def getGCodeList(self) -> List[str]: def getGCodeList(self) -> List[str]:
return self._gcode_list return self._gcode_list
def setGCodeList(self, list: List[str]): def setGCodeList(self, list: List[str]) -> None:
self._gcode_list = list self._gcode_list = list
def __deepcopy__(self, memo) -> "GCodeListDecorator": def __deepcopy__(self, memo) -> "GCodeListDecorator":

View file

@ -47,8 +47,10 @@ class ContainerManager(QObject):
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self ContainerManager.__instance = self
try:
super().__init__(parent = application) super().__init__(parent = application)
except TypeError:
super().__init__()
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry

View file

@ -1,11 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import re import re
import configparser import configparser
from typing import cast, Dict, Optional from typing import Any, cast, Dict, Optional
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override from UM.Decorators import override
@ -267,7 +267,7 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setMetaDataEntry("position", "0") profile.setMetaDataEntry("position", "0")
profile.setDirty(True) profile.setDirty(True)
if idx == 0: if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes # Move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys(): for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
@ -327,6 +327,23 @@ class CuraContainerRegistry(ContainerRegistry):
self._registerSingleExtrusionMachinesExtruderStacks() self._registerSingleExtrusionMachinesExtruderStacks()
self._connectUpgradedExtruderStacksToMachines() self._connectUpgradedExtruderStacksToMachines()
## Check if the metadata for a container is okay before adding it.
#
# This overrides the one from UM.Settings.ContainerRegistry because we
# also require that the setting_version is correct.
@override(ContainerRegistry)
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
if metadata is None:
return False
if "setting_version" not in metadata:
return False
try:
if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion:
return False
except ValueError: #Not parsable as int.
return False
return True
## Update an imported profile to match the current machine configuration. ## Update an imported profile to match the current machine configuration.
# #
# \param profile The profile to configure. # \param profile The profile to configure.
@ -386,30 +403,6 @@ class CuraContainerRegistry(ContainerRegistry):
result.append( (plugin_id, meta_data) ) result.append( (plugin_id, meta_data) )
return result return result
## Returns true if the current machine requires its own materials
# \return True if the current machine requires its own materials
def _machineHasOwnMaterials(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return global_container_stack.getMetaDataEntry("has_materials", False)
return False
## Gets the ID of the active material
# \return the ID of the active material or the empty string
def _activeMaterialId(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and global_container_stack.material:
return global_container_stack.material.getId()
return ""
## Returns true if the current machine requires its own quality profiles
# \return true if the current machine requires its own quality profiles
def _machineHasOwnQualities(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
return False
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
def _convertContainerStack(self, container): def _convertContainerStack(self, container):
assert type(container) == ContainerStack assert type(container) == ContainerStack
@ -521,7 +514,7 @@ class CuraContainerRegistry(ContainerRegistry):
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
if machine.userChanges: if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
# container to the extruder stack. # container to the extruder stack.
for user_setting_key in machine.userChanges.getAllKeys(): for user_setting_key in machine.userChanges.getAllKeys():
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
@ -583,7 +576,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
else: else:
# if we still cannot find a quality changes container for the extruder, create a new one # If we still cannot find a quality changes container for the extruder, create a new one
container_name = machine_quality_changes.getName() container_name = machine_quality_changes.getName()
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
extruder_quality_changes_container = InstanceContainer(container_id, parent = application) extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
@ -601,7 +594,7 @@ class CuraContainerRegistry(ContainerRegistry):
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine_quality_changes.getName(), extruder_stack.getId()) machine_quality_changes.getName(), extruder_stack.getId())
else: else:
# move all per-extruder settings to the extruder's quality changes # Move all per-extruder settings to the extruder's quality changes
for qc_setting_key in machine_quality_changes.getAllKeys(): for qc_setting_key in machine_quality_changes.getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
@ -642,7 +635,7 @@ class CuraContainerRegistry(ContainerRegistry):
if qc_name not in qc_groups: if qc_name not in qc_groups:
qc_groups[qc_name] = [] qc_groups[qc_name] = []
qc_groups[qc_name].append(qc) qc_groups[qc_name].append(qc)
# try to find from the quality changes cura directory too # Try to find from the quality changes cura directory too
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if quality_changes_container: if quality_changes_container:
qc_groups[qc_name].append(quality_changes_container) qc_groups[qc_name].append(quality_changes_container)
@ -656,7 +649,7 @@ class CuraContainerRegistry(ContainerRegistry):
else: else:
qc_dict["global"] = qc qc_dict["global"] = qc
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
# move per-extruder settings # Move per-extruder settings
for qc_setting_key in qc_dict["global"].getAllKeys(): for qc_setting_key in qc_dict["global"].getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
@ -690,17 +683,17 @@ class CuraContainerRegistry(ContainerRegistry):
try: try:
parser.read([file_path]) parser.read([file_path])
except: except:
# skip, it is not a valid stack file # Skip, it is not a valid stack file
continue continue
if not parser.has_option("general", "name"): if not parser.has_option("general", "name"):
continue continue
if parser["general"]["name"] == name: if parser["general"]["name"] == name:
# load the container # Load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "") container_id = os.path.basename(file_path).replace(".inst.cfg", "")
if self.findInstanceContainers(id = container_id): if self.findInstanceContainers(id = container_id):
# this container is already in the registry, skip it # This container is already in the registry, skip it
continue continue
instance_container = InstanceContainer(container_id) instance_container = InstanceContainer(container_id)

View file

@ -125,7 +125,12 @@ class CuraStackBuilder:
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains") extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)] extruder_definition_id = extruder_definition_dict[str(extruder_position)]
try:
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0] extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
except IndexError as e:
# It still needs to break, but we want to know what extruder ID made it break.
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
raise e
# get material container for extruders # get material container for extruders
material_container = application.empty_material_container material_container = application.empty_material_container

View file

@ -264,7 +264,9 @@ class ExtruderManager(QObject):
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))]) used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
# The platform adhesion extruder. Not used if using none. # The platform adhesion extruder. Not used if using none.
if global_stack.getProperty("adhesion_type", "value") != "none": if global_stack.getProperty("adhesion_type", "value") != "none" or (
global_stack.getProperty("prime_tower_brim_enable", "value") and
global_stack.getProperty("adhesion_type", "value") != 'raft'):
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
if extruder_str_nr == "-1": if extruder_str_nr == "-1":
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
@ -301,12 +303,7 @@ class ExtruderManager(QObject):
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
if not global_stack: if not global_stack:
return [] return []
return global_stack.extruderList
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
result_list = [item[1] for item in result_tuple_list]
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
return result_list[:machine_extruder_count]
def _globalContainerStackChanged(self) -> None: def _globalContainerStackChanged(self) -> None:
# If the global container changed, the machine changed and might have extruders that were not registered yet # If the global container changed, the machine changed and might have extruders that were not registered yet
@ -341,7 +338,7 @@ class ExtruderManager(QObject):
extruder_train.setNextStack(global_stack) extruder_train.setNextStack(global_stack)
extruders_changed = True extruders_changed = True
self._fixSingleExtrusionMachineExtruderDefinition(global_stack) self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if extruders_changed: if extruders_changed:
self.extrudersChanged.emit(global_stack_id) self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0) self.setActiveExtruderIndex(0)
@ -349,7 +346,7 @@ class ExtruderManager(QObject):
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders.get("0") extruder_stack_0 = global_stack.extruders.get("0")

View file

@ -133,6 +133,7 @@ class GlobalStack(CuraContainerStack):
return return
self._extruders[position] = extruder self._extruders[position] = extruder
self.extrudersChanged.emit()
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
## Overridden from ContainerStack ## Overridden from ContainerStack

View file

@ -1,11 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import collections
import time import time
import re import re
import unicodedata import unicodedata
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@ -64,8 +63,6 @@ class MachineManager(QObject):
self._default_extruder_position = "0" # to be updated when extruders are switched on and off self._default_extruder_position = "0" # to be updated when extruders are switched on and off
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]]
self._instance_container_timer = QTimer() # type: QTimer self._instance_container_timer = QTimer() # type: QTimer
self._instance_container_timer.setInterval(250) self._instance_container_timer.setInterval(250)
self._instance_container_timer.setSingleShot(True) self._instance_container_timer.setSingleShot(True)
@ -272,11 +269,6 @@ class MachineManager(QObject):
extruder_stack.propertyChanged.connect(self._onPropertyChanged) extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onContainersChanged) extruder_stack.containersChanged.connect(self._onContainersChanged)
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
self._application.callLater(func)
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
def _onActiveExtruderStackChanged(self) -> None: def _onActiveExtruderStackChanged(self) -> None:
@ -365,12 +357,13 @@ class MachineManager(QObject):
# Make sure that the default machine actions for this machine have been added # Make sure that the default machine actions for this machine have been added
self._application.getMachineActionManager().addDefaultMachineActions(global_stack) self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack) ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid(): if not global_stack.isValid():
# Mark global stack as invalid # Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here return # We're done here
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
self._global_container_stack = global_stack self._global_container_stack = global_stack
self._application.setGlobalContainerStack(global_stack) self._application.setGlobalContainerStack(global_stack)
ExtruderManager.getInstance()._globalContainerStackChanged() ExtruderManager.getInstance()._globalContainerStackChanged()
@ -433,12 +426,12 @@ class MachineManager(QObject):
if not self._global_container_stack: if not self._global_container_stack:
return False return False
if self._global_container_stack.getTop().findInstances(): if self._global_container_stack.getTop().getNumInstances() != 0:
return True return True
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack.getTop().findInstances(): if stack.getTop().getNumInstances() != 0:
return True return True
return False return False
@ -448,10 +441,10 @@ class MachineManager(QObject):
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
num_user_settings = 0 num_user_settings = 0
num_user_settings += len(self._global_container_stack.getTop().findInstances()) num_user_settings += self._global_container_stack.getTop().getNumInstances()
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() stacks = self._global_container_stack.extruderList
for stack in stacks: for stack in stacks:
num_user_settings += len(stack.getTop().findInstances()) num_user_settings += stack.getTop().getNumInstances()
return num_user_settings return num_user_settings
## Delete a user setting from the global stack and all extruder stacks. ## Delete a user setting from the global stack and all extruder stacks.
@ -1381,7 +1374,6 @@ class MachineManager(QObject):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.switchPrinterType(configuration.printerType) self.switchPrinterType(configuration.printerType)
used_extruder_stack_list = ExtruderManager.getInstance().getUsedExtruderStacks()
disabled_used_extruder_position_set = set() disabled_used_extruder_position_set = set()
extruders_to_disable = set() extruders_to_disable = set()
@ -1390,8 +1382,9 @@ class MachineManager(QObject):
need_to_show_message = False need_to_show_message = False
for extruder_configuration in configuration.extruderConfigurations: for extruder_configuration in configuration.extruderConfigurations:
extruder_has_hotend = extruder_configuration.hotendID != "" # We support "" or None, since the cloud uses None instead of empty strings
extruder_has_material = extruder_configuration.material.guid != "" extruder_has_hotend = extruder_configuration.hotendID and extruder_configuration.hotendID != ""
extruder_has_material = extruder_configuration.material.guid and extruder_configuration.material.guid != ""
# If the machine doesn't have a hotend or material, disable this extruder # If the machine doesn't have a hotend or material, disable this extruder
if not extruder_has_hotend or not extruder_has_material: if not extruder_has_hotend or not extruder_has_material:

View file

@ -17,6 +17,8 @@ except ImportError:
try: try:
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
if CuraCloudAPIVersion == "":
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
except ImportError: except ImportError:
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION

View file

@ -9,6 +9,7 @@ import os
import sys import sys
from UM.Platform import Platform from UM.Platform import Platform
from cura.ApplicationMetadata import CuraAppName
parser = argparse.ArgumentParser(prog = "cura", parser = argparse.ArgumentParser(prog = "cura",
add_help = False) add_help = False)
@ -22,11 +23,14 @@ known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]: if not known_args["debug"]:
def get_cura_dir_path(): def get_cura_dir_path():
if Platform.isWindows(): if Platform.isWindows():
return os.path.expanduser("~/AppData/Roaming/cura") appdata_path = os.getenv("APPDATA")
if not appdata_path: #Defensive against the environment variable missing (should never happen).
appdata_path = "."
return os.path.join(appdata_path, CuraAppName)
elif Platform.isLinux(): elif Platform.isLinux():
return os.path.expanduser("~/.local/share/cura") return os.path.expanduser("~/.local/share/" + CuraAppName)
elif Platform.isOSX(): elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura") return os.path.expanduser("~/Library/Logs/" + CuraAppName)
if hasattr(sys, "frozen"): if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path() dirpath = get_cura_dir_path()

View file

@ -26,6 +26,7 @@ from UM.Preferences import Preferences
from cura.Machines.VariantType import VariantType from cura.Machines.VariantType import VariantType
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerStack import _ContainerIndexes from cura.Settings.CuraContainerStack import _ContainerIndexes
@ -781,6 +782,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not quality_changes_info.extruder_info_dict: if not quality_changes_info.extruder_info_dict:
container_info = ContainerInfo(None, None, None) container_info = ContainerInfo(None, None, None)
quality_changes_info.extruder_info_dict["0"] = container_info quality_changes_info.extruder_info_dict["0"] = container_info
# If the global stack we're "targeting" has never been active, but was updated from Cura 3.4,
# it might not have it's extruders set properly.
if not global_stack.extruders:
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
extruder_stack = global_stack.extruders["0"] extruder_stack = global_stack.extruders["0"]
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,

View file

@ -1,3 +1,88 @@
[4.0.0]
*Updated user interface
Ultimaker Cura is a very powerful tool with many features to support users needs. In the new UI, we present these features in a better, more intuitive way based on the workflow of our users. The Marketplace and user account control have been integrated into the main interface to easily access material profiles and plugins. Three stages are shown in the header to give a clear guidance of the flow. The stage menu is populated with collapsible panels that allow users to focus on the 3D view when needed, while still showing important information at the same time, such as slicing configuration and settings. Users can now easily go to the preview stage to examine the layer view after slicing the model, which previously was less obvious or hidden. The new UI also creates more distinction between recommended and custom mode. Novice users or users who are not interested in all the settings can easily prepare a file, relying on the strength of expert-configured print profiles. Experienced users who want greater control can configure over 300 settings to their needs.
*Redesigned "Add Printer" dialog
Updated one of the first dialogs a new user is presented with. The layout is loosely modeled on the layout of the Ultimaker 3/Ultimaker S5 "Connect to Network" dialog, and adds some instructions and intention to the dialog. Contributed by fieldOfView.
*Updated custom mode panel
Based on feedback from 4.0 beta, the custom mode panel is now resizable to make more settings visible. The set position will persist between sessions.
*Monitor tab
Updated the monitor tab interface for better alignment with Cura Connect interface.
*Remote printing
Use your Ultimaker S5 printer with an Ultimaker account to send and monitor print jobs from outside your local network. Requires firmware 5.2 (coming soon).
*User ratings for plugins
With an Ultimaker account, users can now give feedback on their experience by rating their favourite plugins.
*Integrated backups
Cura backups has been integrated into Ultimaker Cura and can be found in the extensions menu. With this feature, users can use their Ultimaker account to backup their Ultimaker Cura configurations to the cloud for easy, convenient retrieval.
*Plugin versioning
Newer plug-ins can't load in older versions if they use newer features, while old plug-ins may still load in newer versions.
*LAN and cloud printer icons
Users can now quickly see if their printer is network or cloud enabled with new icons.
*Improved UI speed
This version switches faster between extruders and printers. Your mileage may vary depending on your system specifications.
*Floats precision
No settings in Ultimaker Cura require more than three digits of precision, so floats in setting input fields have been limited to three digits only. Contributed by fieldOfView.
*Minimum support area
This feature allows set minimum area size for support and support interface polygons. Polygons which area are smaller than set value will not be generated. Contributed by vgribinchuk/Desktop Metal.
*Lazy Tree Support calculation
In previous versions, 95% of Tree Supports computation time was used to calculate the collision volumes to make sure that the branches avoid collisions with the meshes. Now it calculates these volumes only when necessary, reducing the computation time. Contributed by bjude.
*CPE and CPE+ comb retractions
Changed all CPE and CPE+ profiles to travel up to 50 mm without retraction, decreasing blobs caused by combing long distances.
*Marketplace improvements
Added optimizations to show a support site instead of an email address, increased the number of lines that are shown for the description, and show a 'website' link so people can order material directly.
*Arduino drivers silent install
Previous versions stopped silent installation because the Arduino drivers packaged with Cura are not signed. Arduino drivers are now skipped when performing a silent install.
*New third-party definitions
- Wanhao. Updated printer profiles to use new travel_speed macro (Contributed by forkineye).
- JGAurora A1, A5 and Z-603S (Contributed by pinchies).
- Alfawise U20 (Contributed by pinchies).
- Cocoon Create ModelMaker (Contributed by pinchies).
- Ender-3. Updates to the printer definition (Contributed by stelgenhof).
*Bug fixes
- Fixed an issue which prevented slicing when per extruder settings were changed with a disabled extruder.
- Improved handling of non-Ultimaker network connected printers within Ultimaker Cura. Contributed by fieldOfView
- Fixed an issue where printing with the second extruder only would retract material unnecessarily.
- Fixed an issue where outdated plugins remained partially activated.
- Fixed an issue where combing was not working when tweaking Retraction minimum travel.
- Fixed an oversized print head collision zone when using print one-at-a-time mode.
- Due to inaccuracy of floats in very large prints, the position is reset again several times using "G92 E0" commands.
- Improved update checker text for better readability.
- Updated the implementation of 3MF in Ultimaker Cura for better consistency with 3MF consortium specifications.
- Removed all final and initial print temperature offsets, and increased first layer print temperature to fix under-extrusion problems.
- Holding shift and rotating a model on its axis for fine-grained rotations would sometimes pan the camera. This has now been fixed.
- Added file type associations for .gcode and .g extensions.
- Marked some more profiles as experimental.
- Fixed an issue where duplicated PLA with a different label would replace the original PLA entry.
- Updated which profile new materials are based when you create a brand new material. Contributed by fieldOfView.
- Fixed adhesion type errors on startup.
- Fixed an issue where system tray icons would remain when Ultimaker Cura is closed until mouse-over.
- Added extra tooltip to give extra information about start/end g-codes.
- Fixed an issue where clicking 'Create Account' would go to login instead of sign-up.
- Fixed an issue where the legacy profile importer would generate corrupt profiles.
- Fixed an issue where Ultimaker Cura could crash on start-up during the upgrading of your configuration to the newest version for some people.
- Fixed an issue where Ultimaker Cura would crash after downloading plugin from Marketplace.
- Ignores plugins folder when checking files for version upgrade. Start-up is now much faster if you've installed a lot of plugins or have used many versions of Ultimaker Cura.
- Fixed an issue where the firmware checker shows up when there is no internet connection.
- Fixed an issue where settings could not be made visible again after hiding all settings.
- Fixed false configuration error for CC Red 0.6 core after a version upgrade.
- Fixed an issue where a warning is issued when selecting a printer with no material loaded. The extruder will now be disabled instead.
[3.6.0] [3.6.0]
*Gyroid infill *Gyroid infill
New infill pattern with enhanced strength properties. Gyroid infill is one of the strongest infill types for a given weight, has isotropic properties, and prints relatively fast with reduced material use and a fully connected part interior. Note: Slicing time can increase up to 40 seconds or more, depending on the model. Contributed by smartavionics. New infill pattern with enhanced strength properties. Gyroid infill is one of the strongest infill types for a given weight, has isotropic properties, and prints relatively fast with reduced material use and a fully connected part interior. Note: Slicing time can increase up to 40 seconds or more, depending on the model. Contributed by smartavionics.
@ -943,7 +1028,7 @@ This release adds support for printers with elliptic buildplates. This feature h
*AppImage for Linux *AppImage for Linux
The Linux distribution is now in AppImage format, which makes Cura easier to install. The Linux distribution is now in AppImage format, which makes Cura easier to install.
*bugfixes *Bugfixes
The user is now notified when a new version of Cura is available. The user is now notified when a new version of Cura is available.
When searching in the setting visibility preferences, the category for each setting is always displayed. When searching in the setting visibility preferences, the category for each setting is always displayed.
3MF files are now saved and loaded correctly. 3MF files are now saved and loaded correctly.

View file

@ -1,9 +1,9 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
from datetime import datetime from datetime import datetime
from typing import Optional, List, Dict, Any, cast from typing import Any, cast, Dict, List, Optional
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
@ -68,7 +68,7 @@ class DrivePluginExtension(QObject, Extension):
def showDriveWindow(self) -> None: def showDriveWindow(self) -> None:
if not self._drive_window: if not self._drive_window:
plugin_dir_path = cast(str, CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")) plugin_dir_path = cast(str, CuraApplication.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())) # We know this plug-in exists because that's us, so this always returns str.
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml") path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self}) self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
self.refreshBackups() self.refreshBackups()

View file

@ -29,7 +29,7 @@ message Object
bytes normals = 3; //An array of 3 floats. bytes normals = 3; //An array of 3 floats.
bytes indices = 4; //An array of ints. bytes indices = 4; //An array of ints.
repeated Setting settings = 5; // Setting override per object, overruling the global settings. repeated Setting settings = 5; // Setting override per object, overruling the global settings.
string name = 6; string name = 6; //Mesh name
} }
message Progress message Progress
@ -58,6 +58,7 @@ message Polygon {
MoveCombingType = 8; MoveCombingType = 8;
MoveRetractionType = 9; MoveRetractionType = 9;
SupportInterfaceType = 10; SupportInterfaceType = 10;
PrimeTowerType = 11;
} }
Type type = 1; // Type of move Type type = 1; // Type of move
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used) bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
@ -108,8 +109,9 @@ message PrintTimeMaterialEstimates { // The print time for each feature and mate
float time_travel = 9; float time_travel = 9;
float time_retract = 10; float time_retract = 10;
float time_support_interface = 11; float time_support_interface = 11;
float time_prime_tower = 12;
repeated MaterialEstimates materialEstimates = 12; // materialEstimates data repeated MaterialEstimates materialEstimates = 13; // materialEstimates data
} }
message MaterialEstimates { message MaterialEstimates {

View file

@ -137,6 +137,7 @@ class ProcessSlicedLayersJob(Job):
extruder = polygon.extruder extruder = polygon.extruder
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
line_types = line_types.reshape((-1,1)) line_types = line_types.reshape((-1,1))
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array

View file

@ -326,6 +326,7 @@ class StartSliceJob(Job):
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
result["print_temperature"] = result["material_print_temperature"] result["print_temperature"] = result["material_print_temperature"]
result["travel_speed"] = result["speed_travel"]
result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
result["date"] = time.strftime("%d-%m-%Y") result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]

View file

@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
@ -13,8 +12,6 @@ from UM.Logger import Logger
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
@ -53,6 +50,7 @@ class FirmwareUpdateChecker(Extension):
def _onContainerAdded(self, container): def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added # Only take care when a new GlobalStack was added
from cura.Settings.GlobalStack import GlobalStack # otherwise circular imports
if isinstance(container, GlobalStack): if isinstance(container, GlobalStack):
self.checkFirmwareVersion(container, True) self.checkFirmwareVersion(container, True)
@ -76,7 +74,7 @@ class FirmwareUpdateChecker(Extension):
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name)) Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
return return
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, self._check_job = FirmwareUpdateCheckerJob(silent = silent,
machine_name = container_name, metadata = metadata, machine_name = container_name, metadata = metadata,
callback = self._onActionTriggered) callback = self._onActionTriggered)
self._check_job.start() self._check_job.start()

View file

@ -25,15 +25,14 @@ class FirmwareUpdateCheckerJob(Job):
ZERO_VERSION = Version(STRING_ZERO_VERSION) ZERO_VERSION = Version(STRING_ZERO_VERSION)
EPSILON_VERSION = Version(STRING_EPSILON_VERSION) EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
def __init__(self, container, silent, machine_name, metadata, callback) -> None: def __init__(self, silent, machine_name, metadata, callback) -> None:
super().__init__() super().__init__()
self._container = container
self.silent = silent self.silent = silent
self._callback = callback self._callback = callback
self._machine_name = machine_name self._machine_name = machine_name
self._metadata = metadata self._metadata = metadata
self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup] self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
self._headers = {} # type:Dict[str, str] # Don't set headers yet. self._headers = {} # type:Dict[str, str] # Don't set headers yet.
def getUrlResponse(self, url: str) -> str: def getUrlResponse(self, url: str) -> str:
@ -45,7 +44,6 @@ class FirmwareUpdateCheckerJob(Job):
result = response.read().decode("utf-8") result = response.read().decode("utf-8")
except URLError: except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url)) Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
return result return result
def parseVersionResponse(self, response: str) -> Version: def parseVersionResponse(self, response: str) -> Version:
@ -70,9 +68,6 @@ class FirmwareUpdateCheckerJob(Job):
return max_version return max_version
def run(self): def run(self):
if self._lookups is None:
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
try: try:
# Initialize a Preference that stores the last version checked for this printer. # Initialize a Preference that stores the last version checked for this printer.
Application.getInstance().getPreferences().addPreference( Application.getInstance().getPreferences().addPreference(
@ -83,13 +78,10 @@ class FirmwareUpdateCheckerJob(Job):
application_version = Application.getInstance().getVersion() application_version = Application.getInstance().getVersion()
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)} self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
# get machine name from the definition container
machine_name = self._container.definition.getName()
# If it is not None, then we compare between the checked_version and the current_version # If it is not None, then we compare between the checked_version and the current_version
machine_id = self._lookups.getMachineId() machine_id = self._lookups.getMachineId()
if machine_id is not None: if machine_id is not None:
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name)) Logger.log("i", "You have a(n) {0} in the printer list. Do firmware-check.".format(self._machine_name))
current_version = self.getCurrentVersion() current_version = self.getCurrentVersion()
@ -105,18 +97,20 @@ class FirmwareUpdateCheckerJob(Job):
# If the checked_version is "", it's because is the first time we check firmware and in this case # If the checked_version is "", it's because is the first time we check firmware and in this case
# we will not show the notification, but we will store it for the next time # we will not show the notification, but we will store it for the next time
Application.getInstance().getPreferences().setValue(setting_key_str, current_version) Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s",
self._machine_name, checked_version, current_version)
# The first time we want to store the current version, the notification will not be shown, # The first time we want to store the current version, the notification will not be shown,
# because the new version of Cura will be release before the firmware and we don't want to # because the new version of Cura will be release before the firmware and we don't want to
# notify the user when no new firmware version is available. # notify the user when no new firmware version is available.
if (checked_version != "") and (checked_version != current_version): if (checked_version != "") and (checked_version != current_version):
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE") Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl()) message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name,
self._lookups.getRedirectUserUrl())
message.actionTriggered.connect(self._callback) message.actionTriggered.connect(self._callback)
message.show() message.show()
else: else:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name)) Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name))
except Exception as e: except Exception as e:
Logger.log("w", "Failed to check for new version: %s", e) Logger.log("w", "Failed to check for new version: %s", e)

View file

@ -18,7 +18,7 @@ class FirmwareUpdateCheckerLookup:
self._machine_id = machine_json.get("id") self._machine_id = machine_json.get("id")
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json. self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
self._check_urls = [] # type:List[str] self._check_urls = [] # type:List[str]
for check_url in machine_json.get("check_urls"): for check_url in machine_json.get("check_urls", []):
self._check_urls.append(check_url) self._check_urls.append(check_url)
self._redirect_user = machine_json.get("update_url") self._redirect_user = machine_json.get("update_url")

View file

@ -0,0 +1,62 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import pytest
from unittest.mock import MagicMock
from UM.Version import Version
import FirmwareUpdateChecker
json_data = \
{
"ned":
{
"id": 1,
"name": "ned",
"check_urls": [""],
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
"version_parser": "default"
},
"olivia":
{
"id": 3,
"name": "olivia",
"check_urls": [""],
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
"version_parser": "default"
},
"emmerson":
{
"id": 5,
"name": "emmerson",
"check_urls": [""],
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
"version_parser": "default"
}
}
@pytest.mark.parametrize("name, id", [
("ned" , 1),
("olivia" , 3),
("emmerson", 5),
])
def test_FirmwareUpdateCheckerLookup(id, name):
lookup = FirmwareUpdateChecker.FirmwareUpdateCheckerLookup.FirmwareUpdateCheckerLookup(name, json_data.get(name))
assert lookup.getMachineName() == name
assert lookup.getMachineId() == id
assert len(lookup.getCheckUrls()) >= 1
assert lookup.getRedirectUserUrl() is not None
@pytest.mark.parametrize("name, version", [
("ned" , Version("5.1.2.3")),
("olivia" , Version("4.3.2.1")),
("emmerson", Version("6.7.8.1")),
])
def test_FirmwareUpdateCheckerJob_getCurrentVersion(name, version):
machine_data = json_data.get(name)
job = FirmwareUpdateChecker.FirmwareUpdateCheckerJob.FirmwareUpdateCheckerJob(False, name, machine_data, MagicMock)
job.getUrlResponse = MagicMock(return_value = str(version)) # Pretend like we got a good response from the server
assert job.getCurrentVersion() == version

View file

@ -107,6 +107,8 @@ class FlavorParser:
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2]) self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness) self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number) this_layer = self._layer_data_builder.getLayer(self._layer_number)
if not this_layer:
return False
except ValueError: except ValueError:
return False return False
count = len(path) count = len(path)

View file

@ -12,9 +12,6 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser from . import MarlinFlavorParser, RepRapFlavorParser
# Class for loading and parsing G-code files # Class for loading and parsing G-code files
class GCodeReader(MeshReader): class GCodeReader(MeshReader):
_flavor_default = "Marlin" _flavor_default = "Marlin"

View file

@ -123,7 +123,7 @@ UM.Dialog
UM.TooltipArea { UM.TooltipArea {
Layout.fillWidth:true Layout.fillWidth:true
height: childrenRect.height height: childrenRect.height
text: catalog.i18nc("@info:tooltip","By default, white pixels represent high points on the mesh and black pixels represent low points on the mesh. Change this option to reverse the behavior such that black pixels represent high points on the mesh and white pixels represent low points on the mesh.") text: catalog.i18nc("@info:tooltip","For lithophanes dark pixels should correspond to thicker locations in order to block more light coming through. For height maps lighter pixels signify higher terrain, so lighter pixels should correspond to thicker locations in the generated 3D model.")
Row { Row {
width: parent.width width: parent.width
@ -134,9 +134,9 @@ UM.Dialog
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
ComboBox { ComboBox {
id: image_color_invert id: lighter_is_higher
objectName: "Image_Color_Invert" objectName: "Lighter_Is_Higher"
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ] model: [ catalog.i18nc("@item:inlistbox","Darker is higher"), catalog.i18nc("@item:inlistbox","Lighter is higher") ]
width: 180 * screenScaleFactor width: 180 * screenScaleFactor
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) } onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
} }

View file

@ -46,9 +46,9 @@ class ImageReader(MeshReader):
def _read(self, file_name): def _read(self, file_name):
size = max(self._ui.getWidth(), self._ui.getDepth()) size = max(self._ui.getWidth(), self._ui.getDepth())
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert) return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher)
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher):
scene_node = SceneNode() scene_node = SceneNode()
mesh = MeshBuilder() mesh = MeshBuilder()
@ -104,7 +104,7 @@ class ImageReader(MeshReader):
Job.yieldThread() Job.yieldThread()
if image_color_invert: if not lighter_is_higher:
height_data = 1 - height_data height_data = 1 - height_data
for _ in range(0, blur_iterations): for _ in range(0, blur_iterations):

View file

@ -30,10 +30,10 @@ class ImageReaderUI(QObject):
self._width = self.default_width self._width = self.default_width
self._depth = self.default_depth self._depth = self.default_depth
self.base_height = 1 self.base_height = 0.4
self.peak_height = 10 self.peak_height = 2.5
self.smoothing = 1 self.smoothing = 1
self.image_color_invert = False; self.lighter_is_higher = False;
self._ui_lock = threading.Lock() self._ui_lock = threading.Lock()
self._cancelled = False self._cancelled = False
@ -143,4 +143,4 @@ class ImageReaderUI(QObject):
@pyqtSlot(int) @pyqtSlot(int)
def onImageColorInvertChanged(self, value): def onImageColorInvertChanged(self, value):
self.image_color_invert = (value == 1) self.lighter_is_higher = (value == 1)

View file

@ -3,7 +3,7 @@
import os import os
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
from UM.Application import Application from UM.Application import Application
from UM.Extension import Extension from UM.Extension import Extension
@ -30,18 +30,22 @@ class ModelChecker(QObject, Extension):
lifetime = 0, lifetime = 0,
title = catalog.i18nc("@info:title", "3D Model Assistant")) title = catalog.i18nc("@info:title", "3D Model Assistant"))
self._change_timer = QTimer()
self._change_timer.setInterval(200)
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self.onChanged)
Application.getInstance().initializationFinished.connect(self._pluginsInitialized) Application.getInstance().initializationFinished.connect(self._pluginsInitialized)
Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged) Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged)
Application.getInstance().globalContainerStackChanged.connect(self._onChanged) Application.getInstance().globalContainerStackChanged.connect(self._onChanged)
## Pass-through to allow UM.Signal to connect with a pyqtSignal.
def _onChanged(self, *args, **kwargs): def _onChanged(self, *args, **kwargs):
# Ignore camera updates. # Ignore camera updates.
if len(args) == 0: if len(args) == 0:
self.onChanged.emit() self._change_timer.start()
return return
if not isinstance(args[0], Camera): if not isinstance(args[0], Camera):
self.onChanged.emit() self._change_timer.start()
## Called when plug-ins are initialized. ## Called when plug-ins are initialized.
# #

View file

@ -112,7 +112,7 @@ class ChangeAtZ(Script):
"e1_Change_speed": "e1_Change_speed":
{ {
"label": "Change Speed", "label": "Change Speed",
"description": "Select if total speed (print and travel) has to be cahnged", "description": "Select if total speed (print and travel) has to be changed",
"type": "bool", "type": "bool",
"default_value": false "default_value": false
}, },

View file

@ -36,7 +36,7 @@ class DisplayFilenameAndLayerOnLCD(Script):
name = self.getSettingValueByKey("name") name = self.getSettingValueByKey("name")
else: else:
name = Application.getInstance().getPrintInformation().jobName name = Application.getInstance().getPrintInformation().jobName
lcd_text = "M117 " + name + " layer: " lcd_text = "M117 " + name + " layer "
i = 0 i = 0
for layer in data: for layer in data:
display_text = lcd_text + str(i) display_text = lcd_text + str(i)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from typing import Optional, Tuple from typing import Optional, Tuple
@ -45,6 +45,22 @@ class FilamentChange(Script):
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 300.0 "default_value": 300.0
},
"x_position":
{
"label": "X Position",
"description": "Extruder X position. The print head will move here for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0
},
"y_position":
{
"label": "Y Position",
"description": "Extruder Y position. The print head will move here for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0
} }
} }
}""" }"""
@ -55,6 +71,8 @@ class FilamentChange(Script):
layer_nums = self.getSettingValueByKey("layer_number") layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract") initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract") later_retract = self.getSettingValueByKey("later_retract")
x_pos = self.getSettingValueByKey("x_position")
y_pos = self.getSettingValueByKey("y_position")
color_change = "M600" color_change = "M600"
@ -64,6 +82,12 @@ class FilamentChange(Script):
if later_retract is not None and later_retract > 0.: if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract) color_change = color_change + (" L%.2f" % later_retract)
if x_pos is not None:
color_change = color_change + (" X%.2f" % x_pos)
if y_pos is not None:
color_change = color_change + (" Y%.2f" % y_pos)
color_change = color_change + " ; Generated by FilamentChange plugin" color_change = color_change + " ; Generated by FilamentChange plugin"
layer_targets = layer_nums.split(",") layer_targets = layer_nums.split(",")

View file

@ -0,0 +1,50 @@
# Created by Wayne Porter
from ..Script import Script
class InsertAtLayerChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Insert at layer change",
"key": "InsertAtLayerChange",
"metadata": {},
"version": 2,
"settings":
{
"insert_location":
{
"label": "When to insert",
"description": "Whether to insert code before or after layer change.",
"type": "enum",
"options": {"before": "Before", "after": "After"},
"default_value": "before"
},
"gcode_to_add":
{
"label": "GCODE to insert.",
"description": "GCODE to add before or after layer change.",
"type": "str",
"default_value": ""
}
}
}"""
def execute(self, data):
gcode_to_add = self.getSettingValueByKey("gcode_to_add") + "\n"
for layer in data:
# Check that a layer is being printed
lines = layer.split("\n")
for line in lines:
if ";LAYER:" in line:
index = data.index(layer)
if self.getSettingValueByKey("insert_location") == "before":
layer = gcode_to_add + layer
else:
layer = layer + gcode_to_add
data[index] = layer
break
return data

View file

@ -0,0 +1,95 @@
# Created by Wayne Porter
from ..Script import Script
class TimeLapse(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Time Lapse",
"key": "TimeLapse",
"metadata": {},
"version": 2,
"settings":
{
"trigger_command":
{
"label": "Trigger camera command",
"description": "Gcode command used to trigger camera.",
"type": "str",
"default_value": "M240"
},
"pause_length":
{
"label": "Pause length",
"description": "How long to wait (in ms) after camera was triggered.",
"type": "int",
"default_value": 700,
"minimum_value": 0,
"unit": "ms"
},
"park_print_head":
{
"label": "Park Print Head",
"description": "Park the print head out of the way. Assumes absolute positioning.",
"type": "bool",
"default_value": true
},
"head_park_x":
{
"label": "Park Print Head X",
"description": "What X location does the head move to for photo.",
"unit": "mm",
"type": "float",
"default_value": 0,
"enabled": "park_print_head"
},
"head_park_y":
{
"label": "Park Print Head Y",
"description": "What Y location does the head move to for photo.",
"unit": "mm",
"type": "float",
"default_value": 190,
"enabled": "park_print_head"
},
"park_feed_rate":
{
"label": "Park Feed Rate",
"description": "How fast does the head move to the park coordinates.",
"unit": "mm/s",
"type": "float",
"default_value": 9000,
"enabled": "park_print_head"
}
}
}"""
def execute(self, data):
feed_rate = self.getSettingValueByKey("park_feed_rate")
park_print_head = self.getSettingValueByKey("park_print_head")
x_park = self.getSettingValueByKey("head_park_x")
y_park = self.getSettingValueByKey("head_park_y")
trigger_command = self.getSettingValueByKey("trigger_command")
pause_length = self.getSettingValueByKey("pause_length")
gcode_to_append = ";TimeLapse Begin\n"
if park_print_head:
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + ";Park print head\n"
gcode_to_append += self.putValue(M = 400) + ";Wait for moves to finish\n"
gcode_to_append += trigger_command + ";Snap Photo\n"
gcode_to_append += self.putValue(G = 4, P = pause_length) + ";Wait for camera\n"
gcode_to_append += ";TimeLapse End\n"
for layer in data:
# Check that a layer is being printed
lines = layer.split("\n")
for line in lines:
if ";LAYER:" in line:
index = data.index(layer)
layer += gcode_to_append
data[index] = layer
break
return data

View file

@ -0,0 +1,46 @@
# Cura PostProcessingPlugin
# Author: Amanda de Castilho
# Date: January 5,2019
# Description: This plugin overrides probing command and inserts code to ensure
# previous probe measurements are loaded and bed leveling enabled
# (searches for G29 and replaces it with M501 & M420 S1)
# *** Assumes G29 is in the start code, will do nothing if it isn't ***
from ..Script import Script
class UsePreviousProbeMeasurements(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Use Previous Probe Measurements",
"key": "UsePreviousProbeMeasurements",
"metadata": {},
"version": 2,
"settings":
{
"use_previous_measurements":
{
"label": "Use last measurement?",
"description": "Selecting this will remove the G29 probing command and instead ensure previous measurements are loaded and enabled",
"type": "bool",
"default_value": false
}
}
}"""
def execute(self, data):
text = "M501 ;load bed level data\nM420 S1 ;enable bed leveling"
if self.getSettingValueByKey("use_previous_measurements"):
for layer in data:
layer_index = data.index(layer)
lines = layer.split("\n")
for line in lines:
if line.startswith("G29"):
line_index = lines.index(line)
lines[line_index] = text
final_lines = "\n".join(lines)
data[layer_index] = final_lines
return data

View file

@ -58,6 +58,7 @@ Item
Cura.ConfigurationMenu Cura.ConfigurationMenu
{ {
id: printerSetup
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: itemRow.width - machineSelection.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width Layout.preferredWidth: itemRow.width - machineSelection.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width

View file

@ -50,7 +50,7 @@ catalog = i18nCatalog("cura")
## View used to display g-code paths. ## View used to display g-code paths.
class SimulationView(CuraView): class SimulationView(CuraView):
# Must match SimulationView.qml # Must match SimulationViewMenuComponent.qml
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
LAYER_VIEW_TYPE_LINE_TYPE = 1 LAYER_VIEW_TYPE_LINE_TYPE = 1
LAYER_VIEW_TYPE_FEEDRATE = 2 LAYER_VIEW_TYPE_FEEDRATE = 2

View file

@ -49,12 +49,13 @@ fragment =
// discard movements // discard movements
discard; discard;
} }
// support: 4, 5, 7, 10 // support: 4, 5, 7, 10, 11 (prime tower)
if ((u_show_helpers == 0) && ( if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) || ((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) || ((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) || ((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ((v_line_type >= 10.5) && (v_line_type <= 11.5))
)) { )) {
discard; discard;
} }

View file

@ -154,7 +154,7 @@ geometry41core =
if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) {
return; return;
} }
if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) { if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10) || v_line_type[0] == 11)) {
return; return;
} }
if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) { if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) {

View file

@ -45,19 +45,23 @@ fragment =
void main() void main()
{ {
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5))
{ // actually, 8 and 9
// discard movements // discard movements
discard; discard;
} }
// support: 4, 5, 7, 10 // support: 4, 5, 7, 10, 11
if ((u_show_helpers == 0) && ( if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) || ((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) || ((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) || ((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
)) { ((v_line_type >= 10.5) && (v_line_type <= 11.5))
))
{
discard; discard;
} }
// skin: 1, 2, 3 // skin: 1, 2, 3
if ((u_show_skin == 0) && ( if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5) (v_line_type >= 0.5) && (v_line_type <= 3.5)
@ -65,7 +69,8 @@ fragment =
discard; discard;
} }
// infill: // infill:
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) { if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5))
{
// discard movements // discard movements
discard; discard;
} }
@ -117,12 +122,13 @@ fragment41core =
// discard movements // discard movements
discard; discard;
} }
// helpers: 4, 5, 7, 10 // helpers: 4, 5, 7, 10, 11
if ((u_show_helpers == 0) && ( if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) || ((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) || ((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) || ((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
((v_line_type >= 10.5) && (v_line_type <= 11.5))
)) { )) {
discard; discard;
} }

View file

@ -54,7 +54,7 @@ Item
anchors.top: tile.top anchors.top: tile.top
width: childrenRect.width width: childrenRect.width
height: childrenRect.height height: childrenRect.height
packageData: model
} }
ToolboxCompatibilityChart ToolboxCompatibilityChart

View file

@ -12,6 +12,7 @@ Column
property bool installed: toolbox.isInstalled(model.id) property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id) property bool canUpdate: toolbox.canUpdate(model.id)
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
property var packageData
width: UM.Theme.getSize("toolbox_action_button").width width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height spacing: UM.Theme.getSize("narrow_margin").height
@ -66,6 +67,27 @@ Column
} }
} }
Label
{
property var whereToBuyUrl:
{
var pg_name = "whereToBuy"
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
}
renderType: Text.NativeRendering
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>")
linkColor: UM.Theme.getColor("text_link")
visible: whereToBuyUrl != undefined
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
MouseArea
{
anchors.fill: parent
onClicked: Qt.openUrlExternally(parent.whereToBuyUrl)
}
}
ToolboxProgressButton ToolboxProgressButton
{ {
id: updateButton id: updateButton

View file

@ -81,7 +81,7 @@ Item
} }
sourceSize.height: height sourceSize.height: height
visible: installedPackages != 0 visible: installedPackages != 0
color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
source: "../images/installed_check.svg" source: "../images/installed_check.svg"
} }
} }

View file

@ -61,7 +61,7 @@ Rectangle
right: parent.right right: parent.right
} }
visible: installedPackages != 0 visible: installedPackages != 0
color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
source: "../images/installed_check.svg" source: "../images/installed_check.svg"
} }

View file

@ -42,6 +42,7 @@ ScrollView
} }
Rectangle Rectangle
{ {
id: installedPlugins
color: "transparent" color: "transparent"
width: parent.width width: parent.width
height: childrenRect.height + UM.Theme.getSize("default_margin").width height: childrenRect.height + UM.Theme.getSize("default_margin").width
@ -74,6 +75,7 @@ ScrollView
Rectangle Rectangle
{ {
id: installedMaterials
color: "transparent" color: "transparent"
width: parent.width width: parent.width
height: childrenRect.height + UM.Theme.getSize("default_margin").width height: childrenRect.height + UM.Theme.getSize("default_margin").width

View file

@ -649,6 +649,7 @@ class Toolbox(QObject, Extension):
Logger.log("w", "Received invalid JSON for %s.", response_type) Logger.log("w", "Received invalid JSON for %s.", response_type)
break break
else: else:
Logger.log("w", "Unable to connect with the server, we got a response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
self.setViewPage("errored") self.setViewPage("errored")
self.resetDownload() self.resetDownload()
elif reply.operation() == QNetworkAccessManager.PutOperation: elif reply.operation() == QNetworkAccessManager.PutOperation:

View file

@ -0,0 +1,41 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import cast
from Charon.VirtualFile import VirtualFile
from UM.Mesh.MeshReader import MeshReader
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.PluginRegistry import PluginRegistry
from cura.Scene.CuraSceneNode import CuraSceneNode
from plugins.GCodeReader.GCodeReader import GCodeReader
class UFPReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-ufp",
comment = "Ultimaker Format Package",
suffixes = ["ufp"]
)
)
self._supported_extensions = [".ufp"]
def _read(self, file_name: str) -> CuraSceneNode:
# Open the file
archive = VirtualFile()
archive.open(file_name)
# Get the gcode data from the file
gcode_data = archive.getData("/3D/model.gcode")
# Convert the bytes stream to string
gcode_stream = gcode_data["/3D/model.gcode"].decode("utf-8")
# Open the GCodeReader to parse the data
gcode_reader = cast(GCodeReader, PluginRegistry.getInstance().getPluginObject("GCodeReader"))
gcode_reader.preReadFromStream(gcode_stream)
return gcode_reader.readFromStream(gcode_stream)

View file

@ -0,0 +1,26 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import UFPReader
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"mesh_reader": [
{
"mime_type": "application/x-ufp",
"extension": "ufp",
"description": i18n_catalog.i18nc("@item:inlistbox", "Ultimaker Format Package")
}
]
}
def register(app):
app.addNonSliceableExtension(".ufp")
return {"mesh_reader": UFPReader.UFPReader()}

View file

@ -0,0 +1,8 @@
{
"name": "UFP Reader",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["6.0.0"],
"i18n-catalog": "cura"
}

View file

@ -28,7 +28,7 @@ class UFPWriter(MeshWriter):
MimeTypeDatabase.addMimeType( MimeTypeDatabase.addMimeType(
MimeType( MimeType(
name = "application/x-ufp", name = "application/x-ufp",
comment = "Cura UFP File", comment = "Ultimaker Format Package",
suffixes = ["ufp"] suffixes = ["ufp"]
) )
) )

View file

@ -210,7 +210,7 @@ Item
Label Label
{ {
text: "All jobs are printed." text: i18n.i18nc("@info", "All jobs are printed.")
color: UM.Theme.getColor("monitor_text_primary") color: UM.Theme.getColor("monitor_text_primary")
font: UM.Theme.getFont("medium") // 14pt, regular font: UM.Theme.getFont("medium") // 14pt, regular
} }

View file

@ -50,7 +50,17 @@ Component
MonitorCarousel MonitorCarousel
{ {
id: carousel id: carousel
printers: OutputDevice.receivedPrintJobs ? OutputDevice.printers : [null] printers:
{
// When printing over the cloud we don't recieve print jobs until there is one, so
// unless there's at least one print job we'll be stuck with skeleton loading
// indefinitely.
if (Cura.MachineManager.activeMachineIsUsingCloudConnection || OutputDevice.receivedPrintJobs)
{
return OutputDevice.printers
}
return [null]
}
} }
} }

View file

@ -12,8 +12,10 @@ from UM.Backend.Backend import BackendState
from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileHandler import FileHandler
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Duration import Duration, DurationFormat from UM.Qt.Duration import Duration, DurationFormat
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
@ -82,8 +84,11 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
self._account = api_client.account self._account = api_client.account
# We use the Cura Connect monitor tab to get most functionality right away. # We use the Cura Connect monitor tab to get most functionality right away.
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), if PluginRegistry.getInstance() is not None:
"../../resources/qml/MonitorStage.qml") self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
# Trigger the printersChanged signal when the private signal is triggered. # Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged) self.printersChanged.connect(self._clusterPrintersChanged)

View file

@ -11,8 +11,8 @@ I18N_CATALOG = i18nCatalog("cura")
class CloudProgressMessage(Message): class CloudProgressMessage(Message):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"), title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"),
title = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"), text = I18N_CATALOG.i18nc("@info:status", "Uploading via Ultimaker Cloud"),
progress = -1, progress = -1,
lifetime = 0, lifetime = 0,
dismissable = False, dismissable = False,

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, cast, Tuple, Union, Optional, Dict, List from typing import Any, cast, Tuple, Union, Optional, Dict, List
@ -10,13 +10,13 @@ import os
from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously. from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously.
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Qt.Duration import Duration, DurationFormat from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Duration import Duration, DurationFormat
from UM.Scene.SceneNode import SceneNode # For typing. from UM.Scene.SceneNode import SceneNode # For typing.
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
@ -65,7 +65,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._print_jobs = [] # type: List[UM3PrintJobOutputModel] self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
self._received_print_jobs = False # type: bool self._received_print_jobs = False # type: bool
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorStage.qml") if PluginRegistry.getInstance() is not None:
self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
# Trigger the printersChanged signal when the private signal is triggered # Trigger the printersChanged signal when the private signal is triggered
self.printersChanged.connect(self._clusterPrintersChanged) self.printersChanged.connect(self._clusterPrintersChanged)
@ -126,7 +130,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _spawnPrinterSelectionDialog(self): def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None: if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml") if PluginRegistry.getInstance() is not None:
path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "PrintWindow.qml"
)
self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self}) self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None: if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show() self._printer_selection_dialog.show()
@ -197,7 +205,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0,
dismissable = False, progress = -1, dismissable = False, progress = -1,
title = i18n_catalog.i18nc("@info:title", "Sending Data")) title = i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = "",
description = "") description = "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show() self._progress_message.show()
@ -263,7 +271,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens. # timeout responses if this happens.
self._last_response_time = time() self._last_response_time = time()
if self._progress_message is not None and new_progress > self._progress_message.getProgress(): if self._progress_message is not None and new_progress != self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible. self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100) self._progress_message.setProgress(bytes_sent / bytes_total * 100)
@ -275,7 +283,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
lifetime=5, dismissable=True, lifetime=5, dismissable=True,
title=i18n_catalog.i18nc("@info:title", "Data Sent")) title=i18n_catalog.i18nc("@info:title", "Data Sent"))
self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None, self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon = "",
description="") description="")
self._success_message.actionTriggered.connect(self._successMessageActionTriggered) self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
self._success_message.show() self._success_message.show()
@ -387,9 +395,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username] newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
for job in newly_finished_jobs: for job in newly_finished_jobs:
if job.assignedPrinter: if job.assignedPrinter:
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name)) job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(printer_name=job.assignedPrinter.name, job_name = job.name)
else: else:
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name)) job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.").format(job_name = job.name)
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished")) job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
job_completed_message.show() job_completed_message.show()

View file

@ -1,7 +1,5 @@
from typing import List, Optional from typing import List, Optional
from UM.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
@ -12,10 +10,13 @@ from cura.PrinterOutputDevice import ConnectionType
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Logger import Logger from UM.FileHandler.FileHandler import FileHandler
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerRegistry import ContainerRegistry
from PyQt5.QtNetwork import QNetworkRequest from PyQt5.QtNetwork import QNetworkRequest
from PyQt5.QtCore import QTimer, QUrl from PyQt5.QtCore import QTimer, QUrl
@ -76,7 +77,11 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.setIconName("print") self.setIconName("print")
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorItem.qml") if PluginRegistry.getInstance() is not None:
self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
self._output_controller = LegacyUM3PrinterOutputController(self) self._output_controller = LegacyUM3PrinterOutputController(self)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
@ -33,7 +33,7 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
data = "{\"target\": \"%s\"}" % state data = "{\"target\": \"%s\"}" % state
self._output_device.put("print_job/state", data, on_finished=None) self._output_device.put("print_job/state", data, on_finished=None)
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float):
data = str(temperature) data = str(temperature)
self._output_device.put("printer/bed/temperature/target", data, on_finished = self._onPutBedTemperatureCompleted) self._output_device.put("printer/bed/temperature/target", data, on_finished = self._onPutBedTemperatureCompleted)

View file

@ -1,14 +1,14 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import os import os
from typing import Dict, TYPE_CHECKING, Set, Optional from typing import Dict, TYPE_CHECKING, Set, Optional
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
# Absolute imports don't work in plugins # Absolute imports don't work in plugins
from .Models import ClusterMaterial, LocalMaterial from .Models import ClusterMaterial, LocalMaterial
@ -86,8 +86,8 @@ class SendMaterialJob(Job):
# #
# \param materials_to_send A set with id's of materials that must be sent. # \param materials_to_send A set with id's of materials that must be sent.
def _sendMaterials(self, materials_to_send: Set[str]) -> None: def _sendMaterials(self, materials_to_send: Set[str]) -> None:
container_registry = Application.getInstance().getContainerRegistry() container_registry = CuraApplication.getInstance().getContainerRegistry()
material_manager = Application.getInstance().getMaterialManager() material_manager = CuraApplication.getInstance().getMaterialManager()
material_group_dict = material_manager.getAllMaterialGroups() material_group_dict = material_manager.getAllMaterialGroups()
for root_material_id in material_group_dict: for root_material_id in material_group_dict:
@ -166,7 +166,7 @@ class SendMaterialJob(Job):
# \return a dictionary of LocalMaterial objects by GUID # \return a dictionary of LocalMaterial objects by GUID
def _getLocalMaterials(self) -> Dict[str, LocalMaterial]: def _getLocalMaterials(self) -> Dict[str, LocalMaterial]:
result = {} # type: Dict[str, LocalMaterial] result = {} # type: Dict[str, LocalMaterial]
material_manager = Application.getInstance().getMaterialManager() material_manager = CuraApplication.getInstance().getMaterialManager()
material_group_dict = material_manager.getAllMaterialGroups() material_group_dict = material_manager.getAllMaterialGroups()

View file

@ -14,12 +14,14 @@ from PyQt5.QtGui import QDesktopServices
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import ConnectionType from cura.PrinterOutputDevice import ConnectionType
from cura.Settings.GlobalStack import GlobalStack # typing from cura.Settings.GlobalStack import GlobalStack # typing
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.PluginRegistry import PluginRegistry
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.Version import Version from UM.Version import Version
from UM.Message import Message
from UM.i18n import i18nCatalog
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
@ -452,39 +454,20 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
def _onCloudFlowPossible(self) -> None: def _onCloudFlowPossible(self) -> None:
# Cloud flow is possible, so show the message # Cloud flow is possible, so show the message
if not self._start_cloud_flow_message: if not self._start_cloud_flow_message:
self._start_cloud_flow_message = Message( self._createCloudFlowStartMessage()
text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
lifetime = 0,
image_source = QUrl.fromLocalFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..",
"resources", "svg", "cloud-flow-start.svg")),
image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"),
option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
option_state = False
)
self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "")
self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain)
self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted)
self._start_cloud_flow_message.show() self._start_cloud_flow_message.show()
return
def _onCloudPrintingConfigured(self) -> None: def _onCloudPrintingConfigured(self) -> None:
if self._start_cloud_flow_message: # Hide the cloud flow start message if it was hanging around already
# For example: if the user already had the browser openen and made the association themselves
if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
self._start_cloud_flow_message.hide() self._start_cloud_flow_message.hide()
self._start_cloud_flow_message = None
# Show the successful pop-up # Cloud flow is complete, so show the message
if not self._start_cloud_flow_message: if not self._cloud_flow_complete_message:
self._cloud_flow_complete_message = Message( self._createCloudFlowCompleteMessage()
text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible:
lifetime = 30,
image_source = QUrl.fromLocalFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..",
"resources", "svg", "cloud-flow-completed.svg")),
image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
)
# Don't show the review connection link if we're not on the local network
if self._application.getMachineManager().activeMachineHasNetworkConnection:
self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection)
self._cloud_flow_complete_message.show() self._cloud_flow_complete_message.show()
# Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
@ -517,11 +500,40 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
return return
def _onMachineSwitched(self) -> None: def _onMachineSwitched(self) -> None:
if self._start_cloud_flow_message is not None: # Hide any left over messages
if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible:
self._start_cloud_flow_message.hide() self._start_cloud_flow_message.hide()
self._start_cloud_flow_message = None if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible:
if self._cloud_flow_complete_message is not None:
self._cloud_flow_complete_message.hide() self._cloud_flow_complete_message.hide()
self._cloud_flow_complete_message = None
# Check for cloud flow again with newly selected machine
self.checkCloudFlowIsPossible() self.checkCloudFlowIsPossible()
def _createCloudFlowStartMessage(self):
self._start_cloud_flow_message = Message(
text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."),
lifetime = 0,
image_source = QUrl.fromLocalFile(os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "svg", "cloud-flow-start.svg"
)),
image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"),
option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
option_state = False
)
self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "")
self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain)
self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted)
def _createCloudFlowCompleteMessage(self):
self._cloud_flow_complete_message = Message(
text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."),
lifetime = 30,
image_source = QUrl.fromLocalFile(os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "svg", "cloud-flow-completed.svg"
)),
image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
)
self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection)

View file

@ -0,0 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport
import Savitar #@UnusedImport

View file

@ -18,7 +18,7 @@ class AutoDetectBaudJob(Job):
def __init__(self, serial_port: int) -> None: def __init__(self, serial_port: int) -> None:
super().__init__() super().__init__()
self._serial_port = serial_port self._serial_port = serial_port
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600]
def run(self) -> None: def run(self) -> None:
Logger.log("d", "Auto detect baud rate started.") Logger.log("d", "Auto detect baud rate started.")
@ -74,7 +74,7 @@ class AutoDetectBaudJob(Job):
line = serial.readline() line = serial.readline()
if b"ok" in line and b"T:" in line: if b"ok" in line and b"T:" in line:
successful_responses += 1 successful_responses += 1
if successful_responses >= 3: if successful_responses >= 1:
self.setResult(baud_rate) self.setResult(baud_rate)
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format( Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(
serial = self._serial_port, baud_rate = baud_rate, retry = retry, time_elapsed = time() - start_timeout_time)) serial = self._serial_port, baud_rate = baud_rate, retry = retry, time_elapsed = time() - start_timeout_time))

View file

@ -1,9 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
from UM.Logger import Logger
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #To get the g-code output.
from UM.PluginRegistry import PluginRegistry #To get the g-code output.
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -15,10 +18,11 @@ from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob from .AutoDetectBaudJob import AutoDetectBaudJob
from .AvrFirmwareUpdater import AvrFirmwareUpdater from .AvrFirmwareUpdater import AvrFirmwareUpdater
from io import StringIO #To write the g-code output.
from queue import Queue
from serial import Serial, SerialException, SerialTimeoutException from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread, Event from threading import Thread, Event
from time import time from time import time
from queue import Queue
from typing import Union, Optional, List, cast from typing import Union, Optional, List, cast
import re import re
@ -49,7 +53,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._baud_rate = baud_rate self._baud_rate = baud_rate
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600]
# Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block.
self._update_thread = Thread(target = self._update, daemon = True) self._update_thread = Thread(target = self._update, daemon = True)
@ -114,28 +118,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# \param kwargs Keyword arguments. # \param kwargs Keyword arguments.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
if self._is_printing: if self._is_printing:
return # Aleady printing return # Already printing
self.writeStarted.emit(self) self.writeStarted.emit(self)
# cancel any ongoing preheat timer before starting a print # cancel any ongoing preheat timer before starting a print
self._printers[0].getController().stopPreheatTimers() self._printers[0].getController().stopPreheatTimers()
CuraApplication.getInstance().getController().setActiveStage("MonitorStage") CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
# find the G-code for the active build plate to print #Find the g-code to print.
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_textio = StringIO()
gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict") gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
gcode_list = gcode_dict[active_build_plate_id] success = gcode_writer.write(gcode_textio, None)
if not success:
return
self._printGCode(gcode_list) self._printGCode(gcode_textio.getvalue())
## Start a print based on a g-code. ## Start a print based on a g-code.
# \param gcode_list List with gcode (strings). # \param gcode The g-code to print.
def _printGCode(self, gcode_list: List[str]): def _printGCode(self, gcode: str):
self._gcode.clear() self._gcode.clear()
self._paused = False self._paused = False
for layer in gcode_list: self._gcode.extend(gcode.split("\n"))
self._gcode.extend(layer.split("\n"))
# Reset line number. If this is not done, first line is sometimes ignored # Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110") self._gcode.insert(0, "M110")
@ -243,7 +248,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._last_temperature_request = time() self._last_temperature_request = time()
if re.search(b"[B|T\d*]: ?\d+\.?\d*", line): # Temperature message. 'T:' for extruder and 'B:' for bed if re.search(b"[B|T\d*]: ?\d+\.?\d*", line): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?(\d+\.?\d*) ?\/?(\d+\.?\d*)?", line) extruder_temperature_matches = re.findall(b"T(\d*): ?(\d+\.?\d*)\s*\/?(\d+\.?\d*)?", line)
# Update all temperature values # Update all temperature values
matched_extruder_nrs = [] matched_extruder_nrs = []
for match in extruder_temperature_matches: for match in extruder_temperature_matches:
@ -265,7 +270,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if match[2]: if match[2]:
extruder.updateTargetHotendTemperature(float(match[2])) extruder.updateTargetHotendTemperature(float(match[2]))
bed_temperature_matches = re.findall(b"B: ?(\d+\.?\d*) ?\/?(\d+\.?\d*) ?", line) bed_temperature_matches = re.findall(b"B: ?(\d+\.?\d*)\s*\/?(\d+\.?\d*)?", line)
if bed_temperature_matches: if bed_temperature_matches:
match = bed_temperature_matches[0] match = bed_temperature_matches[0]
if match[0]: if match[0]:

View file

@ -63,9 +63,9 @@ _RENAMED_MATERIAL_PROFILES = {
## Upgrades configurations from the state they were in at version 3.4 to the ## Upgrades configurations from the state they were in at version 3.4 to the
# state they should be in at version 3.5. # state they should be in at version 3.5.
class VersionUpgrade34to35(VersionUpgrade): class VersionUpgrade34to35(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.3 format. ## Gets the version number from a CFG file in Uranium's 3.4 format.
# #
# Since the format may change, this is implemented for the 3.3 format only # Since the format may change, this is implemented for the 3.4 format only
# and needs to be included in the version upgrade system rather than # and needs to be included in the version upgrade system rather than
# globally in Uranium. # globally in Uranium.
# #

View file

@ -0,0 +1,86 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
import io
from typing import Dict, List, Tuple
from UM.VersionUpgrade import VersionUpgrade
_renamed_quality_profiles = {
"gmax15plus_pla_dual_normal": "gmax15plus_global_dual_normal",
"gmax15plus_pla_dual_thick": "gmax15plus_global_dual_thick",
"gmax15plus_pla_dual_thin": "gmax15plus_global_dual_thin",
"gmax15plus_pla_dual_very_thick": "gmax15plus_global_dual_very_thick",
"gmax15plus_pla_normal": "gmax15plus_global_normal",
"gmax15plus_pla_thick": "gmax15plus_global_thick",
"gmax15plus_pla_thin": "gmax15plus_global_thin",
"gmax15plus_pla_very_thick": "gmax15plus_global_very_thick"
} # type: Dict[str, str]
## Upgrades configurations from the state they were in at version 4.0 to the
# state they should be in at version 4.1.
class VersionUpgrade40to41(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 4.0 format.
#
# Since the format may change, this is implemented for the 4.0 format only
# and needs to be included in the version upgrade system rather than
# globally in Uranium.
#
# \param serialised The serialised form of a CFG file.
# \return The version number stored in the CFG file.
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades instance containers to have the new version
# number.
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "7"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades Preferences to have the new version number.
def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "6"
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = "7"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades stacks to have the new version number.
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "7"
#Update the name of the quality profile.
if parser["containers"]["4"] in _renamed_quality_profiles:
parser["containers"]["4"] = _renamed_quality_profiles[parser["containers"]["4"]]
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,59 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade40to41
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade40to41.VersionUpgrade40to41()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 6000006): ("preferences", 6000007, upgrade.upgradePreferences),
("machine_stack", 4000006): ("machine_stack", 4000007, upgrade.upgradeStack),
("extruder_train", 4000006): ("extruder_train", 4000007, upgrade.upgradeStack),
("definition_changes", 4000006): ("definition_changes", 4000007, upgrade.upgradeInstanceContainer),
("quality_changes", 4000006): ("quality_changes", 4000007, upgrade.upgradeInstanceContainer),
("quality", 4000006): ("quality", 4000007, upgrade.upgradeInstanceContainer),
("user", 4000006): ("user", 4000007, upgrade.upgradeInstanceContainer),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 4.0 to 4.1",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"api": "6.0",
"i18n-catalog": "cura"
}

View file

@ -17,6 +17,7 @@ from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Scene.ConvexHullNode import ConvexHullNode
from . import XRayPass from . import XRayPass
@ -41,6 +42,10 @@ class XRayView(View):
self._xray_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("xray").getRgb())) self._xray_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("xray").getRgb()))
for node in BreadthFirstIterator(scene.getRoot()): for node in BreadthFirstIterator(scene.getRoot()):
# We do not want to render ConvexHullNode as it conflicts with the bottom of the X-Ray (z-fighting).
if type(node) is ConvexHullNode:
continue
if not node.render(renderer): if not node.render(renderer):
if node.getMeshData() and node.isVisible(): if node.getMeshData() and node.isVisible():
renderer.queueNode(node, renderer.queueNode(node,

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy import copy
@ -6,7 +6,7 @@ import io
import json #To parse the product-to-id mapping file. import json #To parse the product-to-id mapping file.
import os.path #To find the product-to-id mapping. import os.path #To find the product-to-id mapping.
import sys import sys
from typing import Any, Dict, List, Optional, Tuple, cast from typing import Any, Dict, List, Optional, Tuple, cast, Set
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from UM.Resources import Resources from UM.Resources import Resources
@ -60,6 +60,7 @@ class XmlMaterialProfile(InstanceContainer):
def setMetaDataEntry(self, key, value, apply_to_all = True): def setMetaDataEntry(self, key, value, apply_to_all = True):
registry = ContainerRegistry.getInstance() registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()): if registry.isReadOnly(self.getId()):
Logger.log("w", "Can't change metadata {key} of material {material_id} because it's read-only.".format(key = key, material_id = self.getId()))
return return
# Prevent recursion # Prevent recursion
@ -119,7 +120,7 @@ class XmlMaterialProfile(InstanceContainer):
## Overridden from InstanceContainer ## Overridden from InstanceContainer
# base file: common settings + supported machines # base file: common settings + supported machines
# machine / variant combination: only changes for itself. # machine / variant combination: only changes for itself.
def serialize(self, ignored_metadata_keys: Optional[set] = None): def serialize(self, ignored_metadata_keys: Optional[Set[str]] = None):
registry = ContainerRegistry.getInstance() registry = ContainerRegistry.getInstance()
base_file = self.getMetaDataEntry("base_file", "") base_file = self.getMetaDataEntry("base_file", "")
@ -944,9 +945,7 @@ class XmlMaterialProfile(InstanceContainer):
for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces): for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces):
machine_compatibility = common_compatibility machine_compatibility = common_compatibility
for entry in machine.iterfind("./um:setting", cls.__namespaces): for entry in machine.iterfind("./um:setting[@key='hardware compatible']", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
if entry.text is not None: if entry.text is not None:
machine_compatibility = cls._parseCompatibleValue(entry.text) machine_compatibility = cls._parseCompatibleValue(entry.text)
@ -1019,9 +1018,7 @@ class XmlMaterialProfile(InstanceContainer):
continue continue
hotend_compatibility = machine_compatibility hotend_compatibility = machine_compatibility
for entry in hotend.iterfind("./um:setting", cls.__namespaces): for entry in hotend.iterfind("./um:setting[@key='hardware compatible']", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
if entry.text is not None: if entry.text is not None:
hotend_compatibility = cls._parseCompatibleValue(entry.text) hotend_compatibility = cls._parseCompatibleValue(entry.text)

View file

@ -1,11 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from UM.VersionUpgrade import VersionUpgrade from UM.VersionUpgrade import VersionUpgrade
from cura.CuraApplication import CuraApplication
from .XmlMaterialProfile import XmlMaterialProfile from .XmlMaterialProfile import XmlMaterialProfile

View file

@ -16,7 +16,7 @@ def getMetaData():
"mimetype": "application/x-ultimaker-material-profile" "mimetype": "application/x-ultimaker-material-profile"
}, },
"version_upgrade": { "version_upgrade": {
("materials", 1000000): ("materials", 1000006, upgrader.upgradeMaterial), ("materials", 1000000): ("materials", 1000007, upgrader.upgradeMaterial),
}, },
"sources": { "sources": {
"materials": { "materials": {

View file

@ -6,7 +6,7 @@
"display_name": "3MF Reader", "display_name": "3MF Reader",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -23,7 +23,7 @@
"display_name": "3MF Writer", "display_name": "3MF Writer",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -40,7 +40,7 @@
"display_name": "Change Log", "display_name": "Change Log",
"description": "Shows changes since latest checked version.", "description": "Shows changes since latest checked version.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -57,7 +57,7 @@
"display_name": "Cura Backups", "display_name": "Cura Backups",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"package_version": "1.2.0", "package_version": "1.2.0",
"sdk_version": 6, "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -74,7 +74,7 @@
"display_name": "CuraEngine Backend", "display_name": "CuraEngine Backend",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -91,7 +91,7 @@
"display_name": "Cura Profile Reader", "display_name": "Cura Profile Reader",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -108,7 +108,7 @@
"display_name": "Cura Profile Writer", "display_name": "Cura Profile Writer",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -125,7 +125,7 @@
"display_name": "Firmware Update Checker", "display_name": "Firmware Update Checker",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -142,7 +142,7 @@
"display_name": "Firmware Updater", "display_name": "Firmware Updater",
"description": "Provides a machine actions for updating firmware.", "description": "Provides a machine actions for updating firmware.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -159,7 +159,7 @@
"display_name": "Compressed G-code Reader", "display_name": "Compressed G-code Reader",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -176,7 +176,7 @@
"display_name": "Compressed G-code Writer", "display_name": "Compressed G-code Writer",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -193,7 +193,7 @@
"display_name": "G-Code Profile Reader", "display_name": "G-Code Profile Reader",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -210,7 +210,7 @@
"display_name": "G-Code Reader", "display_name": "G-Code Reader",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "VictorLarchenko", "author_id": "VictorLarchenko",
@ -227,7 +227,7 @@
"display_name": "G-Code Writer", "display_name": "G-Code Writer",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -244,7 +244,7 @@
"display_name": "Image Reader", "display_name": "Image Reader",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -261,7 +261,7 @@
"display_name": "Legacy Cura Profile Reader", "display_name": "Legacy Cura Profile Reader",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -278,7 +278,7 @@
"display_name": "Machine Settings Action", "display_name": "Machine Settings Action",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "fieldOfView", "author_id": "fieldOfView",
@ -295,7 +295,7 @@
"display_name": "Model Checker", "display_name": "Model Checker",
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -312,7 +312,7 @@
"display_name": "Monitor Stage", "display_name": "Monitor Stage",
"description": "Provides a monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -329,7 +329,7 @@
"display_name": "Per-Object Settings Tool", "display_name": "Per-Object Settings Tool",
"description": "Provides the per-model settings.", "description": "Provides the per-model settings.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -346,7 +346,7 @@
"display_name": "Post Processing", "display_name": "Post Processing",
"description": "Extension that allows for user created scripts for post processing.", "description": "Extension that allows for user created scripts for post processing.",
"package_version": "2.2.1", "package_version": "2.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -363,7 +363,7 @@
"display_name": "Prepare Stage", "display_name": "Prepare Stage",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -380,7 +380,7 @@
"display_name": "Preview Stage", "display_name": "Preview Stage",
"description": "Provides a preview stage in Cura.", "description": "Provides a preview stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -397,7 +397,7 @@
"display_name": "Removable Drive Output Device", "display_name": "Removable Drive Output Device",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -414,7 +414,7 @@
"display_name": "Simulation View", "display_name": "Simulation View",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -431,7 +431,7 @@
"display_name": "Slice Info", "display_name": "Slice Info",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -448,7 +448,7 @@
"display_name": "Solid View", "display_name": "Solid View",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -465,7 +465,7 @@
"display_name": "Support Eraser Tool", "display_name": "Support Eraser Tool",
"description": "Creates an eraser mesh to block the printing of support in certain places.", "description": "Creates an eraser mesh to block the printing of support in certain places.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -482,7 +482,24 @@
"display_name": "Toolbox", "display_name": "Toolbox",
"description": "Find, manage and install new Cura packages.", "description": "Find, manage and install new Cura packages.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"UFPReader": {
"package_info": {
"package_id": "UFPReader",
"package_type": "plugin",
"display_name": "UFP Reader",
"description": "Provides support for reading Ultimaker Format Packages.",
"package_version": "1.0.0",
"sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -499,7 +516,7 @@
"display_name": "UFP Writer", "display_name": "UFP Writer",
"description": "Provides support for writing Ultimaker Format Packages.", "description": "Provides support for writing Ultimaker Format Packages.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -516,7 +533,7 @@
"display_name": "Ultimaker Machine Actions", "display_name": "Ultimaker Machine Actions",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -533,7 +550,7 @@
"display_name": "UM3 Network Printing", "display_name": "UM3 Network Printing",
"description": "Manages network connections to Ultimaker 3 printers.", "description": "Manages network connections to Ultimaker 3 printers.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -550,7 +567,7 @@
"display_name": "USB Printing", "display_name": "USB Printing",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.", "description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"package_version": "1.0.2", "package_version": "1.0.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -567,7 +584,7 @@
"display_name": "User Agreement", "display_name": "User Agreement",
"description": "Ask the user once if he/she agrees with our license.", "description": "Ask the user once if he/she agrees with our license.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -584,7 +601,7 @@
"display_name": "Version Upgrade 2.1 to 2.2", "display_name": "Version Upgrade 2.1 to 2.2",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.", "description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -601,7 +618,7 @@
"display_name": "Version Upgrade 2.2 to 2.4", "display_name": "Version Upgrade 2.2 to 2.4",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.", "description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -618,7 +635,7 @@
"display_name": "Version Upgrade 2.5 to 2.6", "display_name": "Version Upgrade 2.5 to 2.6",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.", "description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -635,7 +652,7 @@
"display_name": "Version Upgrade 2.6 to 2.7", "display_name": "Version Upgrade 2.6 to 2.7",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.", "description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -652,7 +669,7 @@
"display_name": "Version Upgrade 2.7 to 3.0", "display_name": "Version Upgrade 2.7 to 3.0",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.", "description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -669,7 +686,7 @@
"display_name": "Version Upgrade 3.0 to 3.1", "display_name": "Version Upgrade 3.0 to 3.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.", "description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -686,7 +703,7 @@
"display_name": "Version Upgrade 3.2 to 3.3", "display_name": "Version Upgrade 3.2 to 3.3",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.", "description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -703,7 +720,7 @@
"display_name": "Version Upgrade 3.3 to 3.4", "display_name": "Version Upgrade 3.3 to 3.4",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.", "description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -720,7 +737,7 @@
"display_name": "Version Upgrade 3.4 to 3.5", "display_name": "Version Upgrade 3.4 to 3.5",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.", "description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -737,7 +754,24 @@
"display_name": "Version Upgrade 3.5 to 4.0", "display_name": "Version Upgrade 3.5 to 4.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.", "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"VersionUpgrade40to41": {
"package_info": {
"package_id": "VersionUpgrade40to41",
"package_type": "plugin",
"display_name": "Version Upgrade 4.0 to 4.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"package_version": "1.0.1",
"sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -754,7 +788,7 @@
"display_name": "X3D Reader", "display_name": "X3D Reader",
"description": "Provides support for reading X3D files.", "description": "Provides support for reading X3D files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "SevaAlekseyev", "author_id": "SevaAlekseyev",
@ -771,7 +805,7 @@
"display_name": "XML Material Profiles", "display_name": "XML Material Profiles",
"description": "Provides capabilities to read and write XML-based material profiles.", "description": "Provides capabilities to read and write XML-based material profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -788,7 +822,7 @@
"display_name": "X-Ray View", "display_name": "X-Ray View",
"description": "Provides the X-Ray view.", "description": "Provides the X-Ray view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -805,7 +839,7 @@
"display_name": "Generic ABS", "display_name": "Generic ABS",
"description": "The generic ABS profile which other profiles can be based upon.", "description": "The generic ABS profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -823,7 +857,7 @@
"display_name": "Generic BAM", "display_name": "Generic BAM",
"description": "The generic BAM profile which other profiles can be based upon.", "description": "The generic BAM profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -841,7 +875,7 @@
"display_name": "Generic CFF CPE", "display_name": "Generic CFF CPE",
"description": "The generic CFF CPE profile which other profiles can be based upon.", "description": "The generic CFF CPE profile which other profiles can be based upon.",
"package_version": "1.1.1", "package_version": "1.1.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -859,7 +893,7 @@
"display_name": "Generic CFF PA", "display_name": "Generic CFF PA",
"description": "The generic CFF PA profile which other profiles can be based upon.", "description": "The generic CFF PA profile which other profiles can be based upon.",
"package_version": "1.1.1", "package_version": "1.1.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -877,7 +911,7 @@
"display_name": "Generic CPE", "display_name": "Generic CPE",
"description": "The generic CPE profile which other profiles can be based upon.", "description": "The generic CPE profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -895,7 +929,7 @@
"display_name": "Generic CPE+", "display_name": "Generic CPE+",
"description": "The generic CPE+ profile which other profiles can be based upon.", "description": "The generic CPE+ profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -913,7 +947,7 @@
"display_name": "Generic GFF CPE", "display_name": "Generic GFF CPE",
"description": "The generic GFF CPE profile which other profiles can be based upon.", "description": "The generic GFF CPE profile which other profiles can be based upon.",
"package_version": "1.1.1", "package_version": "1.1.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -931,7 +965,7 @@
"display_name": "Generic GFF PA", "display_name": "Generic GFF PA",
"description": "The generic GFF PA profile which other profiles can be based upon.", "description": "The generic GFF PA profile which other profiles can be based upon.",
"package_version": "1.1.1", "package_version": "1.1.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -949,7 +983,7 @@
"display_name": "Generic HIPS", "display_name": "Generic HIPS",
"description": "The generic HIPS profile which other profiles can be based upon.", "description": "The generic HIPS profile which other profiles can be based upon.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -967,7 +1001,7 @@
"display_name": "Generic Nylon", "display_name": "Generic Nylon",
"description": "The generic Nylon profile which other profiles can be based upon.", "description": "The generic Nylon profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -985,7 +1019,7 @@
"display_name": "Generic PC", "display_name": "Generic PC",
"description": "The generic PC profile which other profiles can be based upon.", "description": "The generic PC profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1003,7 +1037,7 @@
"display_name": "Generic PETG", "display_name": "Generic PETG",
"description": "The generic PETG profile which other profiles can be based upon.", "description": "The generic PETG profile which other profiles can be based upon.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1021,7 +1055,7 @@
"display_name": "Generic PLA", "display_name": "Generic PLA",
"description": "The generic PLA profile which other profiles can be based upon.", "description": "The generic PLA profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1039,7 +1073,7 @@
"display_name": "Generic PP", "display_name": "Generic PP",
"description": "The generic PP profile which other profiles can be based upon.", "description": "The generic PP profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1057,7 +1091,7 @@
"display_name": "Generic PVA", "display_name": "Generic PVA",
"description": "The generic PVA profile which other profiles can be based upon.", "description": "The generic PVA profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1075,7 +1109,7 @@
"display_name": "Generic Tough PLA", "display_name": "Generic Tough PLA",
"description": "The generic Tough PLA profile which other profiles can be based upon.", "description": "The generic Tough PLA profile which other profiles can be based upon.",
"package_version": "1.0.2", "package_version": "1.0.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1093,7 +1127,7 @@
"display_name": "Generic TPU", "display_name": "Generic TPU",
"description": "The generic TPU profile which other profiles can be based upon.", "description": "The generic TPU profile which other profiles can be based upon.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1111,7 +1145,7 @@
"display_name": "Dagoma Chromatik PLA", "display_name": "Dagoma Chromatik PLA",
"description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.", "description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://dagoma.fr/boutique/filaments.html", "website": "https://dagoma.fr/boutique/filaments.html",
"author": { "author": {
"author_id": "Dagoma", "author_id": "Dagoma",
@ -1128,7 +1162,7 @@
"display_name": "FABtotum ABS", "display_name": "FABtotum ABS",
"description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so its compatible with all versions of the FABtotum Personal fabricator.", "description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so its compatible with all versions of the FABtotum Personal fabricator.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1145,7 +1179,7 @@
"display_name": "FABtotum Nylon", "display_name": "FABtotum Nylon",
"description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.", "description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1162,7 +1196,7 @@
"display_name": "FABtotum PLA", "display_name": "FABtotum PLA",
"description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starchs sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotums one is between 185° and 195°.", "description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starchs sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotums one is between 185° and 195°.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1179,7 +1213,7 @@
"display_name": "FABtotum TPU Shore 98A", "display_name": "FABtotum TPU Shore 98A",
"description": "", "description": "",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1196,7 +1230,7 @@
"display_name": "Fiberlogy HD PLA", "display_name": "Fiberlogy HD PLA",
"description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.", "description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/", "website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/",
"author": { "author": {
"author_id": "Fiberlogy", "author_id": "Fiberlogy",
@ -1213,7 +1247,7 @@
"display_name": "Filo3D PLA", "display_name": "Filo3D PLA",
"description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.", "description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://dagoma.fr", "website": "https://dagoma.fr",
"author": { "author": {
"author_id": "Dagoma", "author_id": "Dagoma",
@ -1230,7 +1264,7 @@
"display_name": "IMADE3D JellyBOX PETG", "display_name": "IMADE3D JellyBOX PETG",
"description": "", "description": "",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://shop.imade3d.com/filament.html", "website": "http://shop.imade3d.com/filament.html",
"author": { "author": {
"author_id": "IMADE3D", "author_id": "IMADE3D",
@ -1247,7 +1281,7 @@
"display_name": "IMADE3D JellyBOX PLA", "display_name": "IMADE3D JellyBOX PLA",
"description": "", "description": "",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://shop.imade3d.com/filament.html", "website": "http://shop.imade3d.com/filament.html",
"author": { "author": {
"author_id": "IMADE3D", "author_id": "IMADE3D",
@ -1264,7 +1298,7 @@
"display_name": "Octofiber PLA", "display_name": "Octofiber PLA",
"description": "PLA material from Octofiber.", "description": "PLA material from Octofiber.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://nl.octofiber.com/3d-printing-filament/pla.html", "website": "https://nl.octofiber.com/3d-printing-filament/pla.html",
"author": { "author": {
"author_id": "Octofiber", "author_id": "Octofiber",
@ -1281,7 +1315,7 @@
"display_name": "PolyFlex™ PLA", "display_name": "PolyFlex™ PLA",
"description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.", "description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://www.polymaker.com/shop/polyflex/", "website": "http://www.polymaker.com/shop/polyflex/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1298,7 +1332,7 @@
"display_name": "PolyMax™ PLA", "display_name": "PolyMax™ PLA",
"description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.", "description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://www.polymaker.com/shop/polymax/", "website": "http://www.polymaker.com/shop/polymax/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1315,7 +1349,7 @@
"display_name": "PolyPlus™ PLA True Colour", "display_name": "PolyPlus™ PLA True Colour",
"description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.", "description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://www.polymaker.com/shop/polyplus-true-colour/", "website": "http://www.polymaker.com/shop/polyplus-true-colour/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1332,7 +1366,7 @@
"display_name": "PolyWood™ PLA", "display_name": "PolyWood™ PLA",
"description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.", "description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "http://www.polymaker.com/shop/polywood/", "website": "http://www.polymaker.com/shop/polywood/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1349,7 +1383,7 @@
"display_name": "Ultimaker ABS", "display_name": "Ultimaker ABS",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1368,7 +1402,7 @@
"display_name": "Ultimaker Breakaway", "display_name": "Ultimaker Breakaway",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/breakaway", "website": "https://ultimaker.com/products/materials/breakaway",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1387,7 +1421,7 @@
"display_name": "Ultimaker CPE", "display_name": "Ultimaker CPE",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1406,7 +1440,7 @@
"display_name": "Ultimaker CPE+", "display_name": "Ultimaker CPE+",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/cpe", "website": "https://ultimaker.com/products/materials/cpe",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1425,7 +1459,7 @@
"display_name": "Ultimaker Nylon", "display_name": "Ultimaker Nylon",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1444,7 +1478,7 @@
"display_name": "Ultimaker PC", "display_name": "Ultimaker PC",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/pc", "website": "https://ultimaker.com/products/materials/pc",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1463,7 +1497,7 @@
"display_name": "Ultimaker PLA", "display_name": "Ultimaker PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1482,7 +1516,7 @@
"display_name": "Ultimaker PP", "display_name": "Ultimaker PP",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/pp", "website": "https://ultimaker.com/products/materials/pp",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1501,7 +1535,7 @@
"display_name": "Ultimaker PVA", "display_name": "Ultimaker PVA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.1", "package_version": "1.2.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1520,7 +1554,7 @@
"display_name": "Ultimaker TPU 95A", "display_name": "Ultimaker TPU 95A",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.2.2", "package_version": "1.2.2",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/tpu-95a", "website": "https://ultimaker.com/products/materials/tpu-95a",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1539,7 +1573,7 @@
"display_name": "Ultimaker Tough PLA", "display_name": "Ultimaker Tough PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.0.3", "package_version": "1.0.3",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://ultimaker.com/products/materials/tough-pla", "website": "https://ultimaker.com/products/materials/tough-pla",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1558,7 +1592,7 @@
"display_name": "Vertex Delta ABS", "display_name": "Vertex Delta ABS",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1575,7 +1609,7 @@
"display_name": "Vertex Delta PET", "display_name": "Vertex Delta PET",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1592,7 +1626,7 @@
"display_name": "Vertex Delta PLA", "display_name": "Vertex Delta PLA",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1609,7 +1643,7 @@
"display_name": "Vertex Delta TPU", "display_name": "Vertex Delta TPU",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "6.0", "sdk_version": "6.0.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",

View file

@ -18,7 +18,7 @@
"default_value": "Alfawise U20" "default_value": "Alfawise U20"
}, },
"machine_start_gcode": { "machine_start_gcode": {
"default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 Y60.0 Z0 E9.0 F1000.0;intro line\nG1 Y100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
}, },
"machine_end_gcode": { "machine_end_gcode": {
"default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"

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