mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-12 09:17:50 -06:00
Merge remote-tracking branch 'origin/master' into CL-1266_cloud_association_for_manual_ips
This commit is contained in:
commit
49cb3de562
1047 changed files with 52357 additions and 5122 deletions
|
@ -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
19
contributing.md
Normal 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`.
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,12 +675,12 @@ 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)
|
||||||
|
|
||||||
if Platform.isLinux():
|
if Platform.isLinux():
|
||||||
lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions.
|
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
|
||||||
else:
|
else:
|
||||||
lib_suffixes = {""}
|
lib_suffixes = {""}
|
||||||
for suffix in lib_suffixes:
|
for suffix in lib_suffixes:
|
||||||
|
@ -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,62 +1195,75 @@ 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
|
||||||
continue
|
continue
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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@"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,10 +75,10 @@ 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"])
|
||||||
|
|
||||||
return AuthenticationResponse(success=True,
|
return AuthenticationResponse(success=True,
|
||||||
token_type=token_data["token_type"],
|
token_type=token_data["token_type"],
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -60,30 +62,30 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||||
elif self._queryGet(query, "error_code") == "user_denied":
|
elif self._queryGet(query, "error_code") == "user_denied":
|
||||||
# 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
|
||||||
|
|
||||||
return ResponseData(
|
return ResponseData(
|
||||||
status=HTTP_STATUS["REDIRECT"],
|
status = HTTP_STATUS["REDIRECT"],
|
||||||
data_stream=b"Redirecting...",
|
data_stream = b"Redirecting...",
|
||||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
redirect_uri = self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||||
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.")
|
||||||
|
|
||||||
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
||||||
self.send_response(status.code, status.message)
|
self.send_response(status.code, status.message)
|
||||||
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
|
@ -124,17 +122,17 @@ class AuthorizationService:
|
||||||
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||||
if response.success:
|
if response.success:
|
||||||
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,16 +159,16 @@ 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)
|
||||||
self.onAuthStateChanged.emit(logged_in=True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
else:
|
else:
|
||||||
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!")
|
||||||
|
@ -182,7 +180,7 @@ class AuthorizationService:
|
||||||
# Also check if we can actually get the user profile information.
|
# Also check if we can actually get the user profile information.
|
||||||
user_profile = self.getUserProfile()
|
user_profile = self.getUserProfile()
|
||||||
if user_profile is not None:
|
if user_profile is not None:
|
||||||
self.onAuthStateChanged.emit(logged_in=True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
else:
|
else:
|
||||||
if self._unable_to_get_data_message is not None:
|
if self._unable_to_get_data_message is not None:
|
||||||
self._unable_to_get_data_message.hide()
|
self._unable_to_get_data_message.hide()
|
||||||
|
@ -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!")
|
||||||
|
|
|
@ -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...")
|
||||||
|
|
||||||
|
|
|
@ -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,23 +40,22 @@ 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"),
|
||||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -96,14 +96,14 @@ class GenericOutputController(PrinterOutputController):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return # Got invalid values, can't pre-heat.
|
return # Got invalid values, can't pre-heat.
|
||||||
|
|
||||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
self.setTargetBedTemperature(printer, temperature = temperature)
|
||||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||||
self._preheat_bed_timer.start()
|
self._preheat_bed_timer.start()
|
||||||
self._preheat_printer = printer
|
self._preheat_printer = printer
|
||||||
printer.updateIsPreheating(True)
|
printer.updateIsPreheating(True)
|
||||||
|
|
||||||
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||||
self.setTargetBedTemperature(printer, temperature=0)
|
self.setTargetBedTemperature(printer, temperature = 0)
|
||||||
self._preheat_bed_timer.stop()
|
self._preheat_bed_timer.stop()
|
||||||
printer.updateIsPreheating(False)
|
printer.updateIsPreheating(False)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -225,55 +224,55 @@ class PrinterOutputModel(QObject):
|
||||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||||
return self._active_print_job
|
return self._active_print_job
|
||||||
|
|
||||||
@pyqtProperty(str, notify=stateChanged)
|
@pyqtProperty(str, notify = stateChanged)
|
||||||
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
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPreHeatBed(self) -> bool:
|
def canPreHeatBed(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pre_heat_bed
|
return self._controller.can_pre_heat_bed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
# Does the printer support pre-heating the bed at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPreHeatHotends(self) -> bool:
|
def canPreHeatHotends(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pre_heat_hotends
|
return self._controller.can_pre_heat_hotends
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support sending raw G-code at all
|
# Does the printer support sending raw G-code at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canSendRawGcode(self) -> bool:
|
def canSendRawGcode(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_send_raw_gcode
|
return self._controller.can_send_raw_gcode
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support pause at all
|
# Does the printer support pause at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPause(self) -> bool:
|
def canPause(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pause
|
return self._controller.can_pause
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support abort at all
|
# Does the printer support abort at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canAbort(self) -> bool:
|
def canAbort(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_abort
|
return self._controller.can_abort
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support manual control at all
|
# Does the printer support manual control at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canControlManually(self) -> bool:
|
def canControlManually(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_control_manually
|
return self._controller.can_control_manually
|
||||||
|
|
|
@ -242,7 +242,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
_, idx = numpy.unique(vertex_byte_view, return_index = True)
|
||||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||||
|
|
||||||
hull = Polygon(vertex_data)
|
hull = Polygon(vertex_data)
|
||||||
|
|
|
@ -54,7 +54,7 @@ class ConvexHullNode(SceneNode):
|
||||||
|
|
||||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||||
self._hull.getPoints()[::-1], # bottom layer is reversed
|
self._hull.getPoints()[::-1], # bottom layer is reversed
|
||||||
self._mesh_height-thickness, self._mesh_height, color=self._color):
|
self._mesh_height - thickness, self._mesh_height, color = self._color):
|
||||||
|
|
||||||
hull_mesh = hull_mesh_builder.build()
|
hull_mesh = hull_mesh_builder.build()
|
||||||
self.setMeshData(hull_mesh)
|
self.setMeshData(hull_mesh)
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -303,8 +303,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
profile.setMetaDataEntry("position", extruder_position)
|
profile.setMetaDataEntry("position", extruder_position)
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
else: #More extruders in the imported file than in the machine.
|
else: # More extruders in the imported file than in the machine.
|
||||||
continue #Delete the additional profiles.
|
continue # Delete the additional profiles.
|
||||||
|
|
||||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
@ -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)
|
||||||
|
@ -734,7 +727,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
||||||
|
|
||||||
#Override just for the type.
|
# Override just for the type.
|
||||||
@classmethod
|
@classmethod
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
10
cura_app.py
10
cura_app.py
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 Support’s 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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
0
plugins/FirmwareUpdateChecker/tests/__init__.py
Normal file
0
plugins/FirmwareUpdateChecker/tests/__init__.py
Normal 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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(",")
|
||||||
|
|
50
plugins/PostProcessingPlugin/scripts/InsertAtLayerChange.py
Normal file
50
plugins/PostProcessingPlugin/scripts/InsertAtLayerChange.py
Normal 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
|
95
plugins/PostProcessingPlugin/scripts/TimeLapse.py
Normal file
95
plugins/PostProcessingPlugin/scripts/TimeLapse.py
Normal 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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
41
plugins/UFPReader/UFPReader.py
Normal file
41
plugins/UFPReader/UFPReader.py
Normal 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)
|
26
plugins/UFPReader/__init__.py
Normal file
26
plugins/UFPReader/__init__.py
Normal 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()}
|
||||||
|
|
8
plugins/UFPReader/plugin.json
Normal file
8
plugins/UFPReader/plugin.json
Normal 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"
|
||||||
|
}
|
|
@ -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"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,9 +33,9 @@ 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)
|
||||||
|
|
||||||
def _onPutBedTemperatureCompleted(self, reply):
|
def _onPutBedTemperatureCompleted(self, reply):
|
||||||
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
|
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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.")
|
||||||
|
@ -72,9 +72,9 @@ class AutoDetectBaudJob(Job):
|
||||||
|
|
||||||
while timeout_time > time():
|
while timeout_time > time():
|
||||||
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))
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "Version Upgrade 3.4 to 3.5",
|
"name": "Version Upgrade 3.4 to 3.5",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|
|
@ -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()]
|
59
plugins/VersionUpgrade/VersionUpgrade40to41/__init__.py
Normal file
59
plugins/VersionUpgrade/VersionUpgrade40to41/__init__.py
Normal 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 }
|
8
plugins/VersionUpgrade/VersionUpgrade40to41/plugin.json
Normal file
8
plugins/VersionUpgrade/VersionUpgrade40to41/plugin.json
Normal 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"
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue