mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-21 21:58:01 -06:00
Merge branch 'master' into feature_extruder_warning_icon
This commit is contained in:
commit
19dbd1f168
4607 changed files with 36124 additions and 13817 deletions
2
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
2
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
|
@ -64,7 +64,7 @@ body:
|
|||
You can find your log file here:
|
||||
Windows: `%APPDATA%\cura\<Cura version>\cura.log` or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
||||
MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
|
||||
Ubuntu/Linus: `$USER/.local/share/cura/<Cura version>/cura.log`
|
||||
Ubuntu/Linux: `$USER/.local/share/cura/<Cura version>/cura.log`
|
||||
|
||||
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
||||
- type: checkboxes
|
||||
|
|
|
@ -7,5 +7,5 @@ license: "LGPL-3.0"
|
|||
message: "If you use this software, please cite it using these metadata."
|
||||
repository-code: "https://github.com/ultimaker/cura/"
|
||||
title: "Ultimaker Cura"
|
||||
version: "4.10.0"
|
||||
version: "4.12.0"
|
||||
...
|
|
@ -33,7 +33,7 @@ configure_file(${CMAKE_SOURCE_DIR}/com.ultimaker.cura.desktop.in ${CMAKE_BINARY_
|
|||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||
|
||||
|
||||
# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
|
||||
# FIXME: The new FindPython3 finds the system's Python3.6 rather than the Python3.5 that we built for Cura's environment.
|
||||
# So we're using the old method here, with FindPythonInterp for now.
|
||||
find_package(PythonInterp 3 REQUIRED)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ Cura
|
|||
====
|
||||
Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
|
||||
|
||||

|
||||

|
||||
|
||||
Logging Issues
|
||||
------------
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
include(CTest)
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
|
||||
# FIXME: The new FindPython3 finds the system's Python3.6 rather than the Python3.5 that we built for Cura's environment.
|
||||
# So we're using the old method here, with FindPythonInterp for now.
|
||||
find_package(PythonInterp 3 REQUIRED)
|
||||
|
||||
|
|
BIN
cura-logo.PNG
Normal file
BIN
cura-logo.PNG
Normal file
Binary file not shown.
After Width: | Height: | Size: 520 KiB |
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, TYPE_CHECKING, Callable
|
||||
from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
|
||||
|
||||
|
@ -46,6 +46,9 @@ class Account(QObject):
|
|||
loginStateChanged = pyqtSignal(bool)
|
||||
"""Signal emitted when user logged in or out"""
|
||||
|
||||
additionalRightsChanged = pyqtSignal("QVariantMap")
|
||||
"""Signal emitted when a users additional rights change"""
|
||||
|
||||
accessTokenChanged = pyqtSignal()
|
||||
syncRequested = pyqtSignal()
|
||||
"""Sync services may connect to this signal to receive sync triggers.
|
||||
|
@ -59,7 +62,7 @@ class Account(QObject):
|
|||
updatePackagesEnabledChanged = pyqtSignal(bool)
|
||||
|
||||
CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " \
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write connect.material.write " \
|
||||
"library.project.read library.project.write cura.printjob.read cura.printjob.write " \
|
||||
"cura.mesh.read cura.mesh.write"
|
||||
|
||||
|
@ -70,6 +73,7 @@ class Account(QObject):
|
|||
|
||||
self._error_message = None # type: Optional[Message]
|
||||
self._logged_in = False
|
||||
self._additional_rights: Dict[str, Any] = {}
|
||||
self._sync_state = SyncState.IDLE
|
||||
self._manual_sync_enabled = False
|
||||
self._update_packages_enabled = False
|
||||
|
@ -301,3 +305,14 @@ class Account(QObject):
|
|||
return # Nothing to do, user isn't logged in.
|
||||
|
||||
self._authorization_service.deleteAuthData()
|
||||
|
||||
def updateAdditionalRight(self, **kwargs) -> None:
|
||||
"""Update the additional rights of the account.
|
||||
The argument(s) are the rights that need to be set"""
|
||||
self._additional_rights.update(kwargs)
|
||||
self.additionalRightsChanged.emit(self._additional_rights)
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
|
||||
def additionalRights(self) -> Dict[str, Any]:
|
||||
"""A dictionary which can be queried for additional account rights."""
|
||||
return self._additional_rights
|
||||
|
|
|
@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
|
|||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "7.6.0"
|
||||
CuraSDKVersion = "7.8.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
|
|
|
@ -110,18 +110,11 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
|
|||
return found_solution_for_all, node_items
|
||||
|
||||
|
||||
def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> bool:
|
||||
"""
|
||||
Find placement for a set of scene nodes, and move them by using a single grouped operation.
|
||||
:param nodes_to_arrange: The list of nodes that need to be moved.
|
||||
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
||||
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
||||
are placed.
|
||||
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
|
||||
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
|
||||
|
||||
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
||||
"""
|
||||
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
|
||||
build_volume: "BuildVolume",
|
||||
fixed_nodes: Optional[List["SceneNode"]] = None,
|
||||
factor = 10000,
|
||||
add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
|
||||
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)
|
||||
|
||||
|
@ -143,6 +136,27 @@ def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fi
|
|||
grouped_operation.addOperation(
|
||||
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
|
||||
not_fit_count += 1
|
||||
grouped_operation.push()
|
||||
|
||||
return found_solution_for_all
|
||||
return grouped_operation, not_fit_count
|
||||
|
||||
|
||||
def arrange(nodes_to_arrange: List["SceneNode"],
|
||||
build_volume: "BuildVolume",
|
||||
fixed_nodes: Optional[List["SceneNode"]] = None,
|
||||
factor = 10000,
|
||||
add_new_nodes_in_scene: bool = False) -> bool:
|
||||
"""
|
||||
Find placement for a set of scene nodes, and move them by using a single grouped operation.
|
||||
:param nodes_to_arrange: The list of nodes that need to be moved.
|
||||
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
||||
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
||||
are placed.
|
||||
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
|
||||
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
|
||||
|
||||
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
||||
"""
|
||||
|
||||
grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
|
||||
grouped_operation.push()
|
||||
return not_fit_count == 0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import io
|
||||
|
@ -168,7 +168,10 @@ class Backup:
|
|||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||
shutil.move(backup_preferences_file, preferences_file)
|
||||
try:
|
||||
shutil.move(backup_preferences_file, preferences_file)
|
||||
except EnvironmentError as e:
|
||||
Logger.error(f"Unable to back-up preferences file: {type(e)} - {str(e)}")
|
||||
|
||||
# Read the preferences from the newly restored configuration (or else the cached Preferences will override the restored ones)
|
||||
self._application.readPreferencesFromConfiguration()
|
||||
|
@ -203,6 +206,8 @@ class Backup:
|
|||
archive.extract(archive_filename, target_path)
|
||||
except (PermissionError, EnvironmentError):
|
||||
Logger.logException("e", f"Unable to extract the file {archive_filename} from the backup due to permission or file system errors.")
|
||||
except UnicodeEncodeError:
|
||||
Logger.error(f"Unable to extract the file {archive_filename} because of an encoding error.")
|
||||
CuraApplication.getInstance().processEvents()
|
||||
return True
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import math
|
|||
|
||||
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
||||
|
@ -289,7 +290,7 @@ class BuildVolume(SceneNode):
|
|||
# Mark the node as outside build volume if the set extruder is disabled
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
try:
|
||||
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
|
||||
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
|
||||
node.setOutsideBuildArea(True)
|
||||
continue
|
||||
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
|
||||
|
@ -1078,9 +1079,14 @@ class BuildVolume(SceneNode):
|
|||
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
|
||||
# the value is.
|
||||
adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value")
|
||||
skirt_brim_line_width = self._global_container_stack.extruderList[int(adhesion_extruder)].getProperty("skirt_brim_line_width", "value")
|
||||
try:
|
||||
adhesion_stack = self._global_container_stack.extruderList[int(adhesion_extruder)]
|
||||
except IndexError:
|
||||
Logger.warning(f"Couldn't find extruder with index '{adhesion_extruder}', defaulting to 0 instead.")
|
||||
adhesion_stack = self._global_container_stack.extruderList[0]
|
||||
skirt_brim_line_width = adhesion_stack.getProperty("skirt_brim_line_width", "value")
|
||||
|
||||
initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
|
||||
initial_layer_line_width_factor = adhesion_stack.getProperty("initial_layer_line_width_factor", "value")
|
||||
# Use brim width if brim is enabled OR the prime tower has a brim.
|
||||
if adhesion_type == "brim":
|
||||
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
||||
|
|
|
@ -129,7 +129,7 @@ class CuraApplication(QtApplication):
|
|||
# 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
|
||||
# changes of the settings.
|
||||
SettingVersion = 17
|
||||
SettingVersion = 19
|
||||
|
||||
Created = False
|
||||
|
||||
|
@ -162,6 +162,7 @@ class CuraApplication(QtApplication):
|
|||
self.default_theme = "cura-light"
|
||||
|
||||
self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features?utm_source=cura&utm_medium=software&utm_campaign=cura-update-features"
|
||||
self.beta_change_log_url = "https://ultimaker.com/ultimaker-cura-beta-features?utm_source=cura&utm_medium=software&utm_campaign=cura-update-features"
|
||||
|
||||
self._boot_loading_time = time.time()
|
||||
|
||||
|
@ -320,7 +321,7 @@ class CuraApplication(QtApplication):
|
|||
super().initialize()
|
||||
|
||||
self._preferences.addPreference("cura/single_instance", False)
|
||||
self._use_single_instance = self._preferences.getValue("cura/single_instance")
|
||||
self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance
|
||||
|
||||
self.__sendCommandToSingleInstance()
|
||||
self._initializeSettingDefinitions()
|
||||
|
@ -471,7 +472,8 @@ class CuraApplication(QtApplication):
|
|||
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
|
||||
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("setting_visibility", SettingVisibilityPresetsModel.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.SettingVisibilityPreset, "application/x-uranium-preferences"),
|
||||
("machine", 2): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer")
|
||||
("machine", 2): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer"),
|
||||
("extruder", 2): (Resources.DefinitionContainers, "application/x-uranium-definitioncontainer")
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -715,6 +717,7 @@ class CuraApplication(QtApplication):
|
|||
for extruder in global_stack.extruderList:
|
||||
extruder.userChanges.clear()
|
||||
global_stack.userChanges.clear()
|
||||
self.getMachineManager().correctExtruderSettings()
|
||||
|
||||
# if the user decided to keep settings then the user settings should be re-calculated and validated for errors
|
||||
# before slicing. To ensure that slicer uses right settings values
|
||||
|
@ -749,7 +752,9 @@ class CuraApplication(QtApplication):
|
|||
@pyqtSlot(str, result = QUrl)
|
||||
def getDefaultPath(self, key):
|
||||
default_path = self.getPreferences().getValue("local_file/%s" % key)
|
||||
return QUrl.fromLocalFile(default_path)
|
||||
if os.path.exists(default_path):
|
||||
return QUrl.fromLocalFile(default_path)
|
||||
return QUrl()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setDefaultPath(self, key, default_path):
|
||||
|
@ -1311,9 +1316,9 @@ class CuraApplication(QtApplication):
|
|||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent().callDecoration("isSliceable"):
|
||||
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 reset)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
|
@ -1331,9 +1336,9 @@ class CuraApplication(QtApplication):
|
|||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't 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)
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
nodes.append(node)
|
||||
|
@ -1360,9 +1365,9 @@ class CuraApplication(QtApplication):
|
|||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't 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)
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
nodes.append(node)
|
||||
|
@ -1389,7 +1394,7 @@ class CuraApplication(QtApplication):
|
|||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't have a mesh and is not a group.
|
||||
|
||||
parent_node = node.getParent()
|
||||
if parent_node and parent_node.callDecoration("isGroup"):
|
||||
|
@ -1417,11 +1422,11 @@ class CuraApplication(QtApplication):
|
|||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't have a mesh and is not a group.
|
||||
|
||||
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 reset)
|
||||
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
|
@ -2038,11 +2043,11 @@ class CuraApplication(QtApplication):
|
|||
if not node.isEnabled():
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
continue # Node that doesn't have a mesh and is not a group.
|
||||
if only_selectable and not node.isSelectable():
|
||||
continue # Only remove nodes that are selectable.
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not 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 reset)
|
||||
nodes.append(node)
|
||||
if nodes:
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
|
|
|
@ -12,7 +12,7 @@ from cura.CuraApplication import CuraApplication
|
|||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||
# to indicate this.
|
||||
# MainComponent works in the same way the MainComponent of a stage.
|
||||
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active stage
|
||||
# the stageMenuComponent returns an item that should be used somewhere in the stage menu. It's up to the active stage
|
||||
# to actually do something with this.
|
||||
class CuraView(View):
|
||||
def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
|
||||
|
|
|
@ -15,6 +15,7 @@ class Layer:
|
|||
self._height = 0.0
|
||||
self._thickness = 0.0
|
||||
self._polygons = [] # type: List[LayerPolygon]
|
||||
self._vertex_count = 0
|
||||
self._element_count = 0
|
||||
|
||||
@property
|
||||
|
@ -29,6 +30,10 @@ class Layer:
|
|||
def polygons(self) -> List[LayerPolygon]:
|
||||
return self._polygons
|
||||
|
||||
@property
|
||||
def vertexCount(self):
|
||||
return self._vertex_count
|
||||
|
||||
@property
|
||||
def elementCount(self):
|
||||
return self._element_count
|
||||
|
@ -43,24 +48,40 @@ class Layer:
|
|||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshVertexCount()
|
||||
|
||||
return result
|
||||
|
||||
def lineMeshElementCount(self) -> int:
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshElementCount()
|
||||
return result
|
||||
|
||||
def lineMeshCumulativeTypeChangeCount(self, path: int) -> int:
|
||||
""" The number of line-type changes in this layer up until #path.
|
||||
See also LayerPolygon::cumulativeTypeChangeCounts.
|
||||
|
||||
:param path: The path-index up until which the cumulative changes are counted.
|
||||
:return: The cumulative number of line-type changes up until this path.
|
||||
"""
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
num_counts = len(polygon.cumulativeTypeChangeCounts)
|
||||
if path < num_counts:
|
||||
return result + polygon.cumulativeTypeChangeCounts[path]
|
||||
path -= num_counts
|
||||
result += polygon.cumulativeTypeChangeCounts[num_counts - 1]
|
||||
return result
|
||||
|
||||
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
|
||||
result_vertex_offset = vertex_offset
|
||||
result_index_offset = index_offset
|
||||
self._vertex_count = 0
|
||||
self._element_count = 0
|
||||
for polygon in self._polygons:
|
||||
polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
|
||||
result_vertex_offset += polygon.lineMeshVertexCount()
|
||||
result_index_offset += polygon.lineMeshElementCount()
|
||||
self._vertex_count += polygon.vertexCount
|
||||
self._element_count += polygon.elementCount
|
||||
|
||||
return result_vertex_offset, result_index_offset
|
||||
|
|
|
@ -63,6 +63,7 @@ class LayerDataBuilder(MeshBuilder):
|
|||
feedrates = numpy.empty((vertex_count), numpy.float32)
|
||||
extruders = numpy.empty((vertex_count), numpy.float32)
|
||||
line_types = numpy.empty((vertex_count), numpy.float32)
|
||||
vertex_indices = numpy.arange(0, vertex_count, 1, dtype = numpy.float32)
|
||||
|
||||
vertex_offset = 0
|
||||
index_offset = 0
|
||||
|
@ -109,6 +110,12 @@ class LayerDataBuilder(MeshBuilder):
|
|||
"value": feedrates,
|
||||
"opengl_name": "a_feedrate",
|
||||
"opengl_type": "float"
|
||||
},
|
||||
# Can't use glDrawElements to index (due to oversight in (Py)Qt), can't use gl_PrimitiveID (due to legacy support):
|
||||
"vertex_indices": {
|
||||
"value": vertex_indices,
|
||||
"opengl_name": "a_vertex_index",
|
||||
"opengl_type": "float"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,11 +55,19 @@ class LayerPolygon:
|
|||
|
||||
self._jump_mask = self.__jump_map[self._types]
|
||||
self._jump_count = numpy.sum(self._jump_mask)
|
||||
self._cumulative_type_change_counts = numpy.zeros(len(self._types)) # See the comment on the 'cumulativeTypeChangeCounts' property below.
|
||||
last_type = self.types[0]
|
||||
current_type_count = 0
|
||||
for i in range(0, len(self._cumulative_type_change_counts)):
|
||||
if last_type != self.types[i]:
|
||||
current_type_count += 1
|
||||
last_type = self.types[i]
|
||||
self._cumulative_type_change_counts[i] = current_type_count
|
||||
self._mesh_line_count = len(self._types) - self._jump_count
|
||||
self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
|
||||
|
||||
# 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 a lot of memory usage.
|
||||
self._color_map = LayerPolygon.getColorMap()
|
||||
self._colors = self._color_map[self._types] # type: numpy.ndarray
|
||||
|
||||
|
@ -146,7 +154,7 @@ class LayerPolygon:
|
|||
# When the line type changes the index needs to be increased by 2.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
|
||||
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
||||
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||
# The -1 is to compensate for the necessarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
||||
|
||||
self._build_cache_line_mesh_mask = None
|
||||
|
@ -179,6 +187,10 @@ class LayerPolygon:
|
|||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def vertexCount(self):
|
||||
return self._vertex_end - self._vertex_begin
|
||||
|
||||
@property
|
||||
def elementCount(self):
|
||||
return (self._index_end - self._index_begin) * 2 # The range of vertices multiplied by 2 since each vertex is used twice
|
||||
|
@ -207,6 +219,17 @@ class LayerPolygon:
|
|||
def jumpCount(self):
|
||||
return self._jump_count
|
||||
|
||||
@property
|
||||
def cumulativeTypeChangeCounts(self):
|
||||
""" This polygon class stores with a vertex the type of the line to the next vertex. However, in other contexts,
|
||||
other ways of representing this might be more suited to the task (for example, when a vertex can possibly only
|
||||
have _one_ type, it's unavoidable to duplicate vertices when the type is changed). In such situations it's might
|
||||
be useful to know how many times the type has changed, in order to keep the various vertex-indices aligned.
|
||||
|
||||
:return: The total times the line-type changes from one type to another within this LayerPolygon.
|
||||
"""
|
||||
return self._cumulative_type_change_counts
|
||||
|
||||
def getNormals(self) -> numpy.ndarray:
|
||||
"""Calculate normals for the entire polygon using numpy.
|
||||
|
||||
|
|
|
@ -97,8 +97,7 @@ class MachineErrorChecker(QObject):
|
|||
|
||||
def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
|
||||
"""Start the error check for property changed
|
||||
|
||||
this is seperate from the startErrorCheck because it ignores a number property types
|
||||
this is separate from the startErrorCheck because it ignores a number property types
|
||||
|
||||
:param key:
|
||||
:param property_name:
|
||||
|
|
|
@ -59,6 +59,8 @@ class ExtrudersModel(ListModel):
|
|||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||
"""List of colours to display if there is no material or the material has no known colour. """
|
||||
|
||||
MaterialNameRole = Qt.UserRole + 13
|
||||
|
||||
def __init__(self, parent = None):
|
||||
"""Initialises the extruders model, defining the roles and listening for changes in the data.
|
||||
|
||||
|
@ -79,6 +81,7 @@ class ExtrudersModel(ListModel):
|
|||
self.addRoleName(self.MaterialBrandRole, "material_brand")
|
||||
self.addRoleName(self.ColorNameRole, "color_name")
|
||||
self.addRoleName(self.MaterialTypeRole, "material_type")
|
||||
self.addRoleName(self.MaterialNameRole, "material_name")
|
||||
self._update_extruder_timer = QTimer()
|
||||
self._update_extruder_timer.setInterval(100)
|
||||
self._update_extruder_timer.setSingleShot(True)
|
||||
|
@ -199,8 +202,8 @@ class ExtrudersModel(ListModel):
|
|||
"material_brand": material_brand,
|
||||
"color_name": color_name,
|
||||
"material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "",
|
||||
"material_name": extruder.material.getMetaDataEntry("name") if extruder.material else "",
|
||||
}
|
||||
|
||||
items.append(item)
|
||||
extruders_changed = True
|
||||
|
||||
|
@ -224,6 +227,7 @@ class ExtrudersModel(ListModel):
|
|||
"material_brand": "",
|
||||
"color_name": "",
|
||||
"material_type": "",
|
||||
"material_label": ""
|
||||
}
|
||||
items.append(item)
|
||||
if self._items != items:
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtProperty, pyqtSignal
|
||||
from typing import Optional
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -20,6 +21,7 @@ class GlobalStacksModel(ListModel):
|
|||
MetaDataRole = Qt.UserRole + 5
|
||||
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
|
||||
RemovalWarningRole = Qt.UserRole + 7
|
||||
IsOnlineRole = Qt.UserRole + 8
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
@ -31,18 +33,49 @@ class GlobalStacksModel(ListModel):
|
|||
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
||||
self.addRoleName(self.MetaDataRole, "metadata")
|
||||
self.addRoleName(self.DiscoverySourceRole, "discoverySource")
|
||||
self.addRoleName(self.IsOnlineRole, "isOnline")
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(200)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._update)
|
||||
|
||||
self._filter_connection_type = None # type: Optional[ConnectionType]
|
||||
self._filter_online_only = False
|
||||
|
||||
# Listen to changes
|
||||
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||
self._updateDelayed()
|
||||
|
||||
filterConnectionTypeChanged = pyqtSignal()
|
||||
def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
|
||||
self._filter_connection_type = new_filter
|
||||
|
||||
@pyqtProperty(int, fset = setFilterConnectionType, notify = filterConnectionTypeChanged)
|
||||
def filterConnectionType(self) -> int:
|
||||
"""
|
||||
The connection type to filter the list of printers by.
|
||||
|
||||
Only printers that match this connection type will be listed in the
|
||||
model.
|
||||
"""
|
||||
if self._filter_connection_type is None:
|
||||
return -1
|
||||
return self._filter_connection_type.value
|
||||
|
||||
filterOnlineOnlyChanged = pyqtSignal()
|
||||
def setFilterOnlineOnly(self, new_filter: bool) -> None:
|
||||
self._filter_online_only = new_filter
|
||||
|
||||
@pyqtProperty(bool, fset = setFilterOnlineOnly, notify = filterOnlineOnlyChanged)
|
||||
def filterOnlineOnly(self) -> bool:
|
||||
"""
|
||||
Whether to filter the global stacks to show only printers that are online.
|
||||
"""
|
||||
return self._filter_online_only
|
||||
|
||||
def _onContainerChanged(self, container) -> None:
|
||||
"""Handler for container added/removed events from registry"""
|
||||
|
||||
|
@ -58,6 +91,10 @@ class GlobalStacksModel(ListModel):
|
|||
|
||||
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
for container_stack in container_stacks:
|
||||
if self._filter_connection_type is not None: # We want to filter on connection types.
|
||||
if not any((connection_type == self._filter_connection_type for connection_type in container_stack.configuredConnectionTypes)):
|
||||
continue # No connection type on this printer matches the filter.
|
||||
|
||||
has_remote_connection = False
|
||||
|
||||
for connection_type in container_stack.configuredConnectionTypes:
|
||||
|
@ -67,6 +104,10 @@ class GlobalStacksModel(ListModel):
|
|||
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||
continue
|
||||
|
||||
is_online = container_stack.getMetaDataEntry("is_online", False)
|
||||
if self._filter_online_only and not is_online:
|
||||
continue
|
||||
|
||||
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
|
||||
section_name = "Connected printers" if has_remote_connection else "Preset printers"
|
||||
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||
|
@ -82,6 +123,7 @@ class GlobalStacksModel(ListModel):
|
|||
"hasRemoteConnection": has_remote_connection,
|
||||
"metadata": container_stack.getMetaData().copy(),
|
||||
"discoverySource": section_name,
|
||||
"removalWarning": removal_warning})
|
||||
"removalWarning": removal_warning,
|
||||
"isOnline": is_online})
|
||||
items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
|
||||
self.setItems(items)
|
||||
|
|
|
@ -107,7 +107,7 @@ class IntentCategoryModel(ListModel):
|
|||
qualities = IntentModel()
|
||||
qualities.setIntentCategory(category)
|
||||
result.append({
|
||||
"name": IntentCategoryModel.translation(category, "name", catalog.i18nc("@label", "Unknown")),
|
||||
"name": IntentCategoryModel.translation(category, "name", category),
|
||||
"description": IntentCategoryModel.translation(category, "description", None),
|
||||
"intent_category": category,
|
||||
"weight": list(IntentCategoryModel._get_translations().keys()).index(category),
|
||||
|
|
|
@ -2,24 +2,28 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy # To duplicate materials.
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
import uuid # To generate new GUIDs for new materials.
|
||||
import zipfile # To export all materials in a .zip archive.
|
||||
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Resources import Resources # To find QML files.
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
import cura.CuraApplication # Imported like this to prevent cirmanagecular imports.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
|
||||
from cura.UltimakerCloud.CloudMaterialSync import CloudMaterialSync
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class MaterialManagementModel(QObject):
|
||||
favoritesChanged = pyqtSignal(str)
|
||||
"""Triggered when a favorite is added or removed.
|
||||
|
@ -27,6 +31,66 @@ class MaterialManagementModel(QObject):
|
|||
:param The base file of the material is provided as parameter when this emits
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
self._material_sync = CloudMaterialSync(parent=self)
|
||||
self._checkIfNewMaterialsWereInstalled()
|
||||
|
||||
def _checkIfNewMaterialsWereInstalled(self) -> None:
|
||||
"""
|
||||
Checks whether new material packages were installed in the latest startup. If there were, then it shows
|
||||
a message prompting the user to sync the materials with their printers.
|
||||
"""
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
|
||||
if package_data["package_info"]["package_type"] == "material":
|
||||
# At least one new material was installed
|
||||
# TODO: This should be enabled again once CURA-8609 is merged
|
||||
#self._showSyncNewMaterialsMessage()
|
||||
break
|
||||
|
||||
def _showSyncNewMaterialsMessage(self) -> None:
|
||||
sync_materials_message = Message(
|
||||
text = catalog.i18nc("@action:button",
|
||||
"Please sync the material profiles with your printers before starting to print."),
|
||||
title = catalog.i18nc("@action:button", "New materials installed"),
|
||||
message_type = Message.MessageType.WARNING,
|
||||
lifetime = 0
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"sync",
|
||||
name = catalog.i18nc("@action:button", "Sync materials with printers"),
|
||||
icon = "",
|
||||
description = "Sync your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"learn_more",
|
||||
name = catalog.i18nc("@action:button", "Learn more"),
|
||||
icon = "",
|
||||
description = "Learn more about syncing your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style = Message.ActionButtonStyle.LINK
|
||||
)
|
||||
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
|
||||
|
||||
# Show the message only if there are printers that support material export
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
global_stacks = container_registry.findContainerStacks(type = "machine")
|
||||
if any([stack.supportsMaterialExport for stack in global_stacks]):
|
||||
sync_materials_message.show()
|
||||
|
||||
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
|
||||
if sync_message_action == "sync":
|
||||
QDesktopServices.openUrl(QUrl("https://example.com/openSyncAllWindow"))
|
||||
# self.openSyncAllWindow()
|
||||
sync_message.hide()
|
||||
elif sync_message_action == "learn_more":
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
|
||||
|
||||
|
||||
@pyqtSlot("QVariant", result = bool)
|
||||
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
||||
"""Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
|
||||
|
@ -261,39 +325,10 @@ class MaterialManagementModel(QObject):
|
|||
except ValueError: # Material was not in the favorites list.
|
||||
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
def getPreferredExportAllPath(self) -> QUrl:
|
||||
@pyqtSlot()
|
||||
def openSyncAllWindow(self) -> None:
|
||||
"""
|
||||
Get the preferred path to export materials to.
|
||||
Opens the window to sync all materials.
|
||||
"""
|
||||
self._material_sync.openSyncAllWindow()
|
||||
|
||||
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
|
||||
file path.
|
||||
:return: The preferred path to export all materials to.
|
||||
"""
|
||||
cura_application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
device_manager = cura_application.getOutputDeviceManager()
|
||||
devices = device_manager.getOutputDevices()
|
||||
for device in devices:
|
||||
if device.__class__.__name__ == "RemovableDriveOutputDevice":
|
||||
return QUrl.fromLocalFile(device.getId())
|
||||
else: # No removable drives? Use local path.
|
||||
return cura_application.getDefaultPath("dialog_material_path")
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def exportAll(self, file_path: QUrl) -> None:
|
||||
"""
|
||||
Export all materials to a certain file path.
|
||||
:param file_path: The path to export the materials to.
|
||||
"""
|
||||
registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
|
||||
for metadata in registry.findInstanceContainersMetadata(type = "material"):
|
||||
if metadata["base_file"] != metadata["id"]: # Only process base files.
|
||||
continue
|
||||
if metadata["id"] == "empty_material": # Don't export the empty material.
|
||||
continue
|
||||
material = registry.findContainers(id = metadata["id"])[0]
|
||||
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
|
||||
filename = metadata["id"] + "." + suffix
|
||||
archive.writestr(filename, material.serialize())
|
||||
|
|
|
@ -41,10 +41,6 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
machine_manager.activeMaterialChanged.connect(self._onChange)
|
||||
machine_manager.activeVariantChanged.connect(self._onChange)
|
||||
machine_manager.extruderChanged.connect(self._onChange)
|
||||
|
||||
extruder_manager = application.getExtruderManager()
|
||||
extruder_manager.extrudersChanged.connect(self._onChange)
|
||||
|
||||
self._layer_height_unit = "" # This is cached
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||
if basic_item is not None:
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
else:
|
||||
Logger.log("w", "Unable to find the basic visiblity preset.")
|
||||
Logger.log("w", "Unable to find the basic visibility preset.")
|
||||
basic_visibile_settings = ""
|
||||
|
||||
self._preferences = preferences
|
||||
|
|
|
@ -6,11 +6,15 @@ from typing import List
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Job import Job
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Message import Message
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.TranslateOperation import TranslateOperation
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Arranging.Nest2DArrange import arrange
|
||||
from cura.Arranging.Nest2DArrange import arrange, createGroupOperationForArrange
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -43,11 +47,11 @@ class MultiplyObjectsJob(Job):
|
|||
# Only count sliceable objects
|
||||
if node_.callDecoration("isSliceable"):
|
||||
fixed_nodes.append(node_)
|
||||
|
||||
nodes_to_add_without_arrange = []
|
||||
for node in self._objects:
|
||||
# If object is part of a group, multiply group
|
||||
current_node = node
|
||||
while current_node.getParent() and (current_node.getParent().callDecoration("isGroup") or current_node.getParent().callDecoration("isSliceable")):
|
||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||
current_node = current_node.getParent()
|
||||
|
||||
if current_node in processed_nodes:
|
||||
|
@ -56,19 +60,38 @@ class MultiplyObjectsJob(Job):
|
|||
|
||||
for _ in range(self._count):
|
||||
new_node = copy.deepcopy(node)
|
||||
|
||||
# Same build plate
|
||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||
new_node.callDecoration("setBuildPlateNumber", build_plate_number)
|
||||
for child in new_node.getChildren():
|
||||
child.callDecoration("setBuildPlateNumber", build_plate_number)
|
||||
|
||||
nodes.append(new_node)
|
||||
if not current_node.getParent().callDecoration("isSliceable"):
|
||||
nodes.append(new_node)
|
||||
else:
|
||||
# The node we're trying to place has another node that is sliceable as a parent.
|
||||
# As such, we shouldn't arrange it (but it should be added to the scene!)
|
||||
nodes_to_add_without_arrange.append(new_node)
|
||||
new_node.setParent(current_node.getParent())
|
||||
|
||||
found_solution_for_all = True
|
||||
group_operation = GroupedOperation()
|
||||
if nodes:
|
||||
found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes,
|
||||
factor = 10000, add_new_nodes_in_scene = True)
|
||||
group_operation, not_fit_count = createGroupOperationForArrange(nodes,
|
||||
Application.getInstance().getBuildVolume(),
|
||||
fixed_nodes,
|
||||
factor = 10000,
|
||||
add_new_nodes_in_scene = True)
|
||||
found_solution_for_all = not_fit_count == 0
|
||||
|
||||
if nodes_to_add_without_arrange:
|
||||
for nested_node in nodes_to_add_without_arrange:
|
||||
group_operation.addOperation(AddSceneNodeOperation(nested_node, nested_node.getParent()))
|
||||
# Move the node a tiny bit so it doesn't overlap with the existing one.
|
||||
# This doesn't fix it if someone creates more than one duplicate, but it at least shows that something
|
||||
# happened (and after moving it, it's clear that there are more underneath)
|
||||
group_operation.addOperation(TranslateOperation(nested_node, Vector(2.5, 2.5, 2.5)))
|
||||
|
||||
group_operation.push()
|
||||
status_message.hide()
|
||||
|
||||
if not found_solution_for_all:
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import random
|
||||
import secrets
|
||||
from hashlib import sha512
|
||||
from base64 import b64encode
|
||||
from typing import Optional
|
||||
|
@ -48,8 +48,8 @@ class AuthorizationHelpers:
|
|||
}
|
||||
try:
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
except requests.exceptions.ConnectionError:
|
||||
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
|
||||
except requests.exceptions.ConnectionError as connection_error:
|
||||
return AuthenticationResponse(success = False, err_message = f"Unable to connect to remote server: {connection_error}")
|
||||
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||
"""Request the access token from the authorization server using a refresh token.
|
||||
|
@ -139,11 +139,11 @@ class AuthorizationHelpers:
|
|||
def generateVerificationCode(code_length: int = 32) -> str:
|
||||
"""Generate a verification code of arbitrary length.
|
||||
|
||||
:param code_length:: How long should the code be? This should never be lower than 16, but it's probably
|
||||
:param code_length:: How long should the code be in bytes? This should never be lower than 16, but it's probably
|
||||
better to leave it at 32
|
||||
"""
|
||||
|
||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||
return secrets.token_hex(code_length)
|
||||
|
||||
@staticmethod
|
||||
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
||||
|
|
|
@ -99,7 +99,14 @@ class AuthorizationService:
|
|||
# If no auth data exists, we should always log in again.
|
||||
Logger.log("d", "There was no auth data or access token")
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
try:
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
except AttributeError:
|
||||
# THis might seem a bit double, but we get crash reports about this (CURA-2N2 in sentry)
|
||||
Logger.log("d", "There was no auth data or access token")
|
||||
return None
|
||||
|
||||
if user_data:
|
||||
# If the profile was found, we return it immediately.
|
||||
return user_data
|
||||
|
@ -120,7 +127,7 @@ class AuthorizationService:
|
|||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
def getAccessToken(self) -> Optional[str]:
|
||||
"""Get the access token as provided by the repsonse data."""
|
||||
"""Get the access token as provided by the response data."""
|
||||
|
||||
if self._auth_data is None:
|
||||
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||
|
|
|
@ -14,13 +14,18 @@ if TYPE_CHECKING:
|
|||
# Need to do some extra workarounds on windows:
|
||||
import sys
|
||||
from UM.Platform import Platform
|
||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||
import win32timezone
|
||||
if Platform.isWindows():
|
||||
if hasattr(sys, "frozen"):
|
||||
import win32timezone
|
||||
from keyring.backends.Windows import WinVaultKeyring
|
||||
keyring.set_keyring(WinVaultKeyring())
|
||||
if Platform.isOSX() and hasattr(sys, "frozen"):
|
||||
if Platform.isOSX():
|
||||
from keyring.backends.macOS import Keyring
|
||||
keyring.set_keyring(Keyring())
|
||||
if Platform.isLinux():
|
||||
# We do not support the keyring on Linux, so make sure no Keyring backend is loaded, even if there is a system one.
|
||||
from keyring.backends.fail import Keyring as NoKeyringBackend
|
||||
keyring.set_keyring(NoKeyringBackend())
|
||||
|
||||
# Even if errors happen, we don't want this stored locally:
|
||||
DONT_EVER_STORE_LOCALLY: List[str] = ["refresh_token"]
|
||||
|
|
|
@ -79,7 +79,7 @@ class PickingPass(RenderPass):
|
|||
return -1
|
||||
|
||||
distance = output.pixel(px, py) # distance in micron, from in r, g & b channels
|
||||
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm
|
||||
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and convert to mm
|
||||
return distance
|
||||
|
||||
def getPickedPosition(self, x: int, y: int) -> Vector:
|
||||
|
|
|
@ -49,7 +49,7 @@ class FirmwareUpdater(QObject):
|
|||
raise NotImplementedError("_updateFirmware needs to be implemented")
|
||||
|
||||
def _cleanupAfterUpdate(self) -> None:
|
||||
"""Cleanup after a succesful update"""
|
||||
"""Cleanup after a successful update"""
|
||||
|
||||
# Clean up for next attempt.
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||
|
@ -114,6 +114,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
return b"".join(file_data_bytes_list)
|
||||
|
||||
def _update(self) -> None:
|
||||
"""
|
||||
Update the connection state of this device.
|
||||
|
||||
This is called on regular intervals.
|
||||
"""
|
||||
if self._last_response_time:
|
||||
time_since_last_response = time() - self._last_response_time
|
||||
else:
|
||||
|
@ -127,11 +132,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
if time_since_last_response > self._timeout_time >= time_since_last_request:
|
||||
# Go (or stay) into timeout.
|
||||
if self._connection_state_before_timeout is None:
|
||||
self._connection_state_before_timeout = self._connection_state
|
||||
self._connection_state_before_timeout = self.connectionState
|
||||
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
elif self.connectionState == ConnectionState.Closed:
|
||||
# Go out of timeout.
|
||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||
self.setConnectionState(self._connection_state_before_timeout)
|
||||
|
@ -361,7 +366,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
self._last_response_time = time()
|
||||
|
||||
if self._connection_state == ConnectionState.Connecting:
|
||||
if self.connectionState == ConnectionState.Connecting:
|
||||
self.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
callback_key = reply.url().toString() + str(reply.operation())
|
||||
|
@ -414,6 +419,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def ipAddress(self) -> str:
|
||||
"""IP adress of this printer"""
|
||||
"""IP address of this printer"""
|
||||
|
||||
return self._address
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
|
@ -120,11 +122,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
callback(QMessageBox.Yes)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||
"""
|
||||
Returns whether we could theoretically send commands to this printer.
|
||||
:return: `True` if we are connected, or `False` if not.
|
||||
"""
|
||||
return self.connectionState != ConnectionState.Closed and self.connectionState != ConnectionState.Error
|
||||
|
||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||
if self._connection_state != connection_state:
|
||||
"""
|
||||
Store the connection state of the printer.
|
||||
|
||||
Causes everything that displays the connection state to update its QML models.
|
||||
:param connection_state: The new connection state to store.
|
||||
"""
|
||||
if self.connectionState != connection_state:
|
||||
self._connection_state = connection_state
|
||||
cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().setMetaDataEntry("is_online", self.isConnected())
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
|
@ -133,6 +146,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
"""
|
||||
Get the connection state of the printer, e.g. whether it is connected, still connecting, error state, etc.
|
||||
:return: The current connection state of this output device.
|
||||
"""
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
|
|
256
cura/PrinterOutput/UploadMaterialsJob.py
Normal file
256
cura/PrinterOutput/UploadMaterialsJob.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import enum
|
||||
import functools # For partial methods to use as callbacks with information pre-filled.
|
||||
import json # To serialise metadata for API calls.
|
||||
import os # To delete the archive when we're done.
|
||||
from PyQt5.QtCore import QUrl
|
||||
import tempfile # To create an archive before we upload it.
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find all printers to upload to.
|
||||
from cura.UltimakerCloud import UltimakerCloudConstants # To know where the API is.
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To know how to communicate with this server.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To call the API.
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
|
||||
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from cura.UltimakerCloud.CloudMaterialSync import CloudMaterialSync
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UploadMaterialsError(Exception):
|
||||
"""
|
||||
Class to indicate something went wrong while uploading.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UploadMaterialsJob(Job):
|
||||
"""
|
||||
Job that uploads a set of materials to the Digital Factory.
|
||||
|
||||
The job has a number of stages:
|
||||
- First, it generates an archive of all materials. This typically takes a lot of processing power during which the
|
||||
GIL remains locked.
|
||||
- Then it requests the API to upload an archive.
|
||||
- Then it uploads the archive to the URL given by the first request.
|
||||
- Then it tells the API that the archive can be distributed to the printers.
|
||||
"""
|
||||
|
||||
UPLOAD_REQUEST_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/connect/v1/materials/upload"
|
||||
UPLOAD_CONFIRM_URL = UltimakerCloudConstants.CuraCloudAPIRoot + "/connect/v1/clusters/{cluster_id}/printers/{cluster_printer_id}/action/import_material"
|
||||
|
||||
class Result(enum.IntEnum):
|
||||
SUCCESS = 0
|
||||
FAILED = 1
|
||||
|
||||
class PrinterStatus(enum.Enum):
|
||||
UPLOADING = "uploading"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
|
||||
def __init__(self, material_sync: "CloudMaterialSync"):
|
||||
super().__init__()
|
||||
self._material_sync = material_sync
|
||||
self._scope = JsonDecoratorScope(UltimakerCloudScope(cura.CuraApplication.CuraApplication.getInstance())) # type: JsonDecoratorScope
|
||||
self._archive_filename = None # type: Optional[str]
|
||||
self._archive_remote_id = None # type: Optional[str] # ID that the server gives to this archive. Used to communicate about the archive to the server.
|
||||
self._printer_sync_status = {} # type: Dict[str, str]
|
||||
self._printer_metadata = [] # type: List[Dict[str, Any]]
|
||||
self.processProgressChanged.connect(self._onProcessProgressChanged)
|
||||
|
||||
uploadCompleted = Signal() # Triggered when the job is really complete, including uploading to the cloud.
|
||||
processProgressChanged = Signal() # Triggered when we've made progress creating the archive.
|
||||
uploadProgressChanged = Signal() # Triggered when we've made progress with the complete job. This signal emits a progress fraction (0-1) as well as the status of every printer.
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Generates an archive of materials and starts uploading that archive to the cloud.
|
||||
"""
|
||||
self._printer_metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(
|
||||
type = "machine",
|
||||
connection_type = "3", # Only cloud printers.
|
||||
is_online = "True", # Only online printers. Otherwise the server gives an error.
|
||||
host_guid = "*", # Required metadata field. Otherwise we get a KeyError.
|
||||
um_cloud_cluster_id = "*" # Required metadata field. Otherwise we get a KeyError.
|
||||
)
|
||||
for printer in self._printer_metadata:
|
||||
self._printer_sync_status[printer["host_guid"]] = self.PrinterStatus.UPLOADING.value
|
||||
|
||||
try:
|
||||
archive_file = tempfile.NamedTemporaryFile("wb", delete = False)
|
||||
archive_file.close()
|
||||
self._archive_filename = archive_file.name
|
||||
self._material_sync.exportAll(QUrl.fromLocalFile(self._archive_filename), notify_progress = self.processProgressChanged)
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to create archive of materials to sync with printers: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to create archive of materials to sync with printers.")))
|
||||
return
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(self._archive_filename)
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to load the archive of materials to sync it with printers: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to load the archive of materials to sync it with printers.")))
|
||||
return
|
||||
|
||||
request_metadata = {
|
||||
"data": {
|
||||
"file_size": file_size,
|
||||
"material_profile_name": "cura.umm", # File name can be anything as long as it's .umm. It's not used by anyone.
|
||||
"content_type": "application/zip", # This endpoint won't receive files of different MIME types.
|
||||
"origin": "cura" # Some identifier against hackers intercepting this upload request, apparently.
|
||||
}
|
||||
}
|
||||
request_payload = json.dumps(request_metadata).encode("UTF-8")
|
||||
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.put(
|
||||
url = self.UPLOAD_REQUEST_URL,
|
||||
data = request_payload,
|
||||
callback = self.onUploadRequestCompleted,
|
||||
error_callback = self.onError,
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def onUploadRequestCompleted(self, reply: "QNetworkReply") -> None:
|
||||
"""
|
||||
Triggered when we successfully requested to upload a material archive.
|
||||
|
||||
We then need to start uploading the material archive to the URL that the request answered with.
|
||||
:param reply: The reply from the server to our request to upload an archive.
|
||||
"""
|
||||
response_data = HttpRequestManager.readJSON(reply)
|
||||
if response_data is None:
|
||||
Logger.error(f"Invalid response to material upload request. Could not parse JSON data.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory appears to be corrupted.")))
|
||||
return
|
||||
if "data" not in response_data:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'data' field that contains the entire response.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
if "upload_url" not in response_data["data"]:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'upload_url' field to upload archive to.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
if "material_profile_id" not in response_data["data"]:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'material_profile_id' to communicate about the materials with the server.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
|
||||
upload_url = response_data["data"]["upload_url"]
|
||||
self._archive_remote_id = response_data["data"]["material_profile_id"]
|
||||
try:
|
||||
with open(cast(str, self._archive_filename), "rb") as f:
|
||||
file_data = f.read()
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to load archive back in for sending to cloud: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to load the archive of materials to sync it with printers.")))
|
||||
return
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.put(
|
||||
url = upload_url,
|
||||
data = file_data,
|
||||
callback = self.onUploadCompleted,
|
||||
error_callback = self.onError,
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def onUploadCompleted(self, reply: "QNetworkReply") -> None:
|
||||
"""
|
||||
When we've successfully uploaded the archive to the cloud, we need to notify the API to start syncing that
|
||||
archive to every printer.
|
||||
:param reply: The reply from the cloud storage when the upload succeeded.
|
||||
"""
|
||||
for container_stack in self._printer_metadata:
|
||||
cluster_id = container_stack["um_cloud_cluster_id"]
|
||||
printer_id = container_stack["host_guid"]
|
||||
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.post(
|
||||
url = self.UPLOAD_CONFIRM_URL.format(cluster_id = cluster_id, cluster_printer_id = printer_id),
|
||||
callback = functools.partial(self.onUploadConfirmed, printer_id),
|
||||
error_callback = functools.partial(self.onUploadConfirmed, printer_id), # Let this same function handle the error too.
|
||||
scope = self._scope,
|
||||
data = json.dumps({"data": {"material_profile_id": self._archive_remote_id}}).encode("UTF-8")
|
||||
)
|
||||
|
||||
def onUploadConfirmed(self, printer_id: str, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
"""
|
||||
Triggered when we've got a confirmation that the material is synced with the printer, or that syncing failed.
|
||||
|
||||
If syncing succeeded we mark this printer as having the status "success". If it failed we mark the printer as
|
||||
"failed". If this is the last upload that needed to be completed, we complete the job with either a success
|
||||
state (every printer successfully synced) or a failed state (any printer failed).
|
||||
:param printer_id: The printer host_guid that we completed syncing with.
|
||||
:param reply: The reply that the server gave to confirm.
|
||||
:param error: If the request failed, this error gives an indication what happened.
|
||||
"""
|
||||
if error is not None:
|
||||
Logger.error(f"Failed to confirm uploading material archive to printer {printer_id}: {error}")
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.FAILED.value
|
||||
else:
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.SUCCESS.value
|
||||
|
||||
still_uploading = len([val for val in self._printer_sync_status.values() if val == self.PrinterStatus.UPLOADING.value])
|
||||
self.uploadProgressChanged.emit(0.8 + (len(self._printer_sync_status) - still_uploading) / len(self._printer_sync_status), self.getPrinterSyncStatus())
|
||||
|
||||
if still_uploading == 0: # This is the last response to be processed.
|
||||
if self.PrinterStatus.FAILED.value in self._printer_sync_status.values():
|
||||
self.setResult(self.Result.FAILED)
|
||||
self.setError(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to connect to Digital Factory to sync materials with some of the printers.")))
|
||||
else:
|
||||
self.setResult(self.Result.SUCCESS)
|
||||
self.uploadCompleted.emit(self.getResult(), self.getError())
|
||||
|
||||
def onError(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"]) -> None:
|
||||
"""
|
||||
Used as callback from HTTP requests when the request failed.
|
||||
|
||||
The given network error from the `HttpRequestManager` is logged, and the job is marked as failed.
|
||||
:param reply: The main reply of the server. This reply will most likely not be valid.
|
||||
:param error: The network error (Qt's enum) that occurred.
|
||||
"""
|
||||
Logger.error(f"Failed to upload material archive: {error}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to connect to Digital Factory.")))
|
||||
|
||||
def getPrinterSyncStatus(self) -> Dict[str, str]:
|
||||
"""
|
||||
For each printer, identified by host_guid, this gives the current status of uploading the material archive.
|
||||
|
||||
The possible states are given in the PrinterStatus enum.
|
||||
:return: A dictionary with printer host_guids as keys, and their status as values.
|
||||
"""
|
||||
return self._printer_sync_status
|
||||
|
||||
def failed(self, error: UploadMaterialsError) -> None:
|
||||
"""
|
||||
Helper function for when we have a general failure.
|
||||
|
||||
This sets the sync status for all printers to failed, sets the error on
|
||||
the job and the result of the job to FAILED.
|
||||
:param error: An error to show to the user.
|
||||
"""
|
||||
self.setResult(self.Result.FAILED)
|
||||
self.setError(error)
|
||||
for printer_id in self._printer_sync_status:
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.FAILED.value
|
||||
self.uploadProgressChanged.emit(1.0, self.getPrinterSyncStatus())
|
||||
self.uploadCompleted.emit(self.getResult(), self.getError())
|
||||
|
||||
def _onProcessProgressChanged(self, progress: float) -> None:
|
||||
"""
|
||||
When we progress in the process of uploading materials, we not only signal the new progress (float from 0 to 1)
|
||||
but we also signal the current status of every printer. These are emitted as the two parameters of the signal.
|
||||
:param progress: The progress of this job, between 0 and 1.
|
||||
"""
|
||||
self.uploadProgressChanged.emit(progress * 0.8, self.getPrinterSyncStatus()) # The processing is 80% of the progress bar.
|
|
@ -23,6 +23,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
|||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -319,7 +321,7 @@ class ContainerManager(QObject):
|
|||
stack.qualityChanges = quality_changes
|
||||
|
||||
if not quality_changes or container_registry.isReadOnly(quality_changes.getId()):
|
||||
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
||||
Logger.log("e", "Could not update quality of a nonexistent or read only quality profile in stack %s", stack.getId())
|
||||
continue
|
||||
|
||||
self._performMerge(quality_changes, stack.getTop())
|
||||
|
@ -408,7 +410,7 @@ class ContainerManager(QObject):
|
|||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
for plugin_id, container_type in container_registry.getContainerTypes():
|
||||
# Ignore default container types since those are not plugins
|
||||
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
|
||||
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer, GlobalStack, ExtruderStack):
|
||||
continue
|
||||
|
||||
serialize_type = ""
|
||||
|
|
|
@ -32,6 +32,10 @@ from cura.Machines.ContainerTree import ContainerTree
|
|||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from .DatabaseHandlers.IntentDatabaseHandler import IntentDatabaseHandler
|
||||
from .DatabaseHandlers.QualityDatabaseHandler import QualityDatabaseHandler
|
||||
from .DatabaseHandlers.VariantDatabaseHandler import VariantDatabaseHandler
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
|
@ -44,6 +48,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# is added, we check to see if an extruder stack needs to be added.
|
||||
self.containerAdded.connect(self._onContainerAdded)
|
||||
|
||||
self._database_handlers["variant"] = VariantDatabaseHandler()
|
||||
self._database_handlers["quality"] = QualityDatabaseHandler()
|
||||
self._database_handlers["intent"] = IntentDatabaseHandler()
|
||||
|
||||
@override(ContainerRegistry)
|
||||
def addContainer(self, container: ContainerInterface) -> bool:
|
||||
"""Overridden from ContainerRegistry
|
||||
|
|
|
@ -66,7 +66,7 @@ class CuraStackBuilder:
|
|||
Logger.logException("e", "Failed to create an extruder stack for position {pos}: {err}".format(pos = position, err = str(e)))
|
||||
return None
|
||||
|
||||
# If given, set the machine_extruder_count when creating the machine, or else the extruderList used bellow will
|
||||
# If given, set the machine_extruder_count when creating the machine, or else the extruderList used below will
|
||||
# not return the correct extruder list (since by default, the machine_extruder_count is 1) in machines with
|
||||
# settable number of extruders.
|
||||
if machine_extruder_count and 0 <= machine_extruder_count <= len(extruder_dict):
|
||||
|
|
25
cura/Settings/DatabaseHandlers/IntentDatabaseHandler.py
Normal file
25
cura/Settings/DatabaseHandlers/IntentDatabaseHandler.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Settings.SQLQueryFactory import SQLQueryFactory
|
||||
from UM.Settings.DatabaseContainerMetadataController import DatabaseMetadataContainerController
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
|
||||
class IntentDatabaseHandler(DatabaseMetadataContainerController):
|
||||
"""The Database handler for Intent containers"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(SQLQueryFactory(table = "intent",
|
||||
fields = {
|
||||
"id": "text",
|
||||
"name": "text",
|
||||
"quality_type": "text",
|
||||
"intent_category": "text",
|
||||
"variant": "text",
|
||||
"definition": "text",
|
||||
"material": "text",
|
||||
"version": "text",
|
||||
"setting_version": "text"
|
||||
}))
|
||||
self._container_type = InstanceContainer
|
38
cura/Settings/DatabaseHandlers/QualityDatabaseHandler.py
Normal file
38
cura/Settings/DatabaseHandlers/QualityDatabaseHandler.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Settings.SQLQueryFactory import SQLQueryFactory, metadata_type
|
||||
from UM.Settings.DatabaseContainerMetadataController import DatabaseMetadataContainerController
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
|
||||
class QualityDatabaseHandler(DatabaseMetadataContainerController):
|
||||
"""The Database handler for Quality containers"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(SQLQueryFactory(table = "quality",
|
||||
fields = {
|
||||
"id": "text",
|
||||
"name": "text",
|
||||
"quality_type": "text",
|
||||
"material": "text",
|
||||
"variant": "text",
|
||||
"global_quality": "bool",
|
||||
"definition": "text",
|
||||
"version": "text",
|
||||
"setting_version": "text"
|
||||
}))
|
||||
self._container_type = InstanceContainer
|
||||
|
||||
def groomMetadata(self, metadata: metadata_type) -> metadata_type:
|
||||
"""
|
||||
Ensures that the metadata is in the order of the field keys and has the right size.
|
||||
if the metadata doesn't contains a key which is stored in the DB it will add it as
|
||||
an empty string. Key, value pairs that are not stored in the DB are dropped.
|
||||
If the `global_quality` isn't set it well default to 'False'
|
||||
|
||||
:param metadata: The container metadata
|
||||
"""
|
||||
if "global_quality" not in metadata:
|
||||
metadata["global_quality"] = "False"
|
||||
return super().groomMetadata(metadata)
|
22
cura/Settings/DatabaseHandlers/VariantDatabaseHandler.py
Normal file
22
cura/Settings/DatabaseHandlers/VariantDatabaseHandler.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Settings.SQLQueryFactory import SQLQueryFactory
|
||||
from UM.Settings.DatabaseContainerMetadataController import DatabaseMetadataContainerController
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
|
||||
class VariantDatabaseHandler(DatabaseMetadataContainerController):
|
||||
"""The Database handler for Variant containers"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(SQLQueryFactory(table = "variant",
|
||||
fields = {
|
||||
"id": "text",
|
||||
"name": "text",
|
||||
"hardware_type": "text",
|
||||
"definition": "text",
|
||||
"version": "text",
|
||||
"setting_version": "text"
|
||||
}))
|
||||
self._container_type = InstanceContainer
|
0
cura/Settings/DatabaseHandlers/__init__.py
Normal file
0
cura/Settings/DatabaseHandlers/__init__.py
Normal file
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import time
|
||||
|
@ -627,7 +627,7 @@ class MachineManager(QObject):
|
|||
return ""
|
||||
return global_container_stack.getIntentCategory()
|
||||
|
||||
# Provies a list of extruder positions that have a different intent from the active one.
|
||||
# Provides a list of extruder positions that have a different intent from the active one.
|
||||
@pyqtProperty("QStringList", notify=activeIntentChanged)
|
||||
def extruderPositionsWithNonActiveIntent(self):
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
|
@ -855,7 +855,6 @@ class MachineManager(QObject):
|
|||
caution_message = Message(
|
||||
catalog.i18nc("@info:message Followed by a list of settings.",
|
||||
"Settings have been changed to match the current availability of extruders:") + " [{settings_list}]".format(settings_list = ", ".join(add_user_changes)),
|
||||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||
caution_message.show()
|
||||
|
||||
|
@ -1191,7 +1190,7 @@ class MachineManager(QObject):
|
|||
|
||||
self.setIntentByCategory(quality_changes_group.intent_category)
|
||||
self._reCalculateNumUserSettings()
|
||||
|
||||
self.correctExtruderSettings()
|
||||
self.activeQualityGroupChanged.emit()
|
||||
self.activeQualityChangesGroupChanged.emit()
|
||||
|
||||
|
@ -1398,6 +1397,8 @@ class MachineManager(QObject):
|
|||
# previous one).
|
||||
self._global_container_stack.setUserChanges(global_user_changes)
|
||||
for i, user_changes in enumerate(per_extruder_user_changes):
|
||||
if i >= len(self._global_container_stack.extruderList): # New printer has fewer extruders.
|
||||
break
|
||||
self._global_container_stack.extruderList[i].setUserChanges(per_extruder_user_changes[i])
|
||||
|
||||
@pyqtSlot(QObject)
|
||||
|
|
|
@ -61,6 +61,10 @@ class SettingInheritanceManager(QObject):
|
|||
result.append(key)
|
||||
return result
|
||||
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def hasOverrides(self, key: str, extruder_index: str):
|
||||
return key in self.getOverridesForExtruder(key, extruder_index)
|
||||
|
||||
@pyqtSlot(str, str, result = "QStringList")
|
||||
def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
|
||||
if self._global_container_stack is None:
|
||||
|
|
|
@ -18,6 +18,8 @@ class SingleInstance:
|
|||
|
||||
self._single_instance_server = None
|
||||
|
||||
self._application.getPreferences().addPreference("cura/single_instance_clear_before_load", True)
|
||||
|
||||
# Starts a client that checks for a single instance server and sends the files that need to opened if the server
|
||||
# exists. Returns True if the single instance server is found, otherwise False.
|
||||
def startClient(self) -> bool:
|
||||
|
@ -42,8 +44,9 @@ class SingleInstance:
|
|||
# "command" field is required and holds the name of the command to execute.
|
||||
# Other fields depend on the command.
|
||||
|
||||
payload = {"command": "clear-all"}
|
||||
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||
if self._application.getPreferences().getValue("cura/single_instance_clear_before_load"):
|
||||
payload = {"command": "clear-all"}
|
||||
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||
|
||||
payload = {"command": "focus"}
|
||||
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||
|
@ -68,7 +71,7 @@ class SingleInstance:
|
|||
Logger.log("e", "Single instance server was not created.")
|
||||
|
||||
def _onClientConnected(self) -> None:
|
||||
Logger.log("i", "New connection recevied on our single-instance server")
|
||||
Logger.log("i", "New connection received on our single-instance server")
|
||||
connection = None #type: Optional[QLocalSocket]
|
||||
if self._single_instance_server:
|
||||
connection = self._single_instance_server.nextPendingConnection()
|
||||
|
|
|
@ -56,8 +56,8 @@ class OnExitCallbackManager:
|
|||
self._application.callLater(self._application.closeApplication)
|
||||
|
||||
# This is the callback function which an on-exit callback should call when it finishes, it should provide the
|
||||
# "should_proceed" flag indicating whether this check has "passed", or in other words, whether quiting the
|
||||
# application should be blocked. If the last on-exit callback doesn't block the quiting, it will call the next
|
||||
# "should_proceed" flag indicating whether this check has "passed", or in other words, whether quitting the
|
||||
# application should be blocked. If the last on-exit callback doesn't block the quitting, it will call the next
|
||||
# registered on-exit callback if available.
|
||||
def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None:
|
||||
if not should_proceed:
|
||||
|
|
|
@ -90,7 +90,7 @@ class ObjectsModel(ListModel):
|
|||
|
||||
parent = node.getParent()
|
||||
if parent and parent.callDecoration("isGroup"):
|
||||
return False # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
return False # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||
|
||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") and node_build_plate_number != self._build_plate_number:
|
||||
|
|
|
@ -13,6 +13,8 @@ from UM.Qt.Duration import Duration
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from UM.OutputDevice.ProjectOutputDevice import ProjectOutputDevice
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -68,6 +70,7 @@ class PrintInformation(QObject):
|
|||
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
|
||||
self._application.fileLoaded.connect(self.setBaseName)
|
||||
self._application.workspaceLoaded.connect(self.setProjectName)
|
||||
self._application.getOutputDeviceManager().writeStarted.connect(self._onOutputStart)
|
||||
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
|
||||
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
|
@ -439,3 +442,11 @@ class PrintInformation(QObject):
|
|||
"""Listen to scene changes to check if we need to reset the print information"""
|
||||
|
||||
self.setToZeroPrintInformation(self._active_build_plate)
|
||||
|
||||
def _onOutputStart(self, output_device: OutputDevice) -> None:
|
||||
"""If this is the sort of output 'device' (like local or online file storage, rather than a printer),
|
||||
the user could have altered the file-name, and thus the project name should be altered as well."""
|
||||
if isinstance(output_device, ProjectOutputDevice):
|
||||
new_name = output_device.getLastOutputName()
|
||||
if new_name is not None:
|
||||
self.setJobName(os.path.splitext(os.path.basename(new_name))[0])
|
||||
|
|
217
cura/UltimakerCloud/CloudMaterialSync.py
Normal file
217
cura/UltimakerCloud/CloudMaterialSync.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
import zipfile # To export all materials in a .zip archive.
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from UM.Resources import Resources
|
||||
from cura.PrinterOutput.UploadMaterialsJob import UploadMaterialsJob, UploadMaterialsError # To export materials to the output printer.
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Signal import Signal
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class CloudMaterialSync(QObject):
|
||||
"""
|
||||
Handles the synchronisation of material profiles with cloud accounts.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QObject = None):
|
||||
super().__init__(parent)
|
||||
self.sync_all_dialog = None # type: Optional[QObject]
|
||||
self._export_upload_status = "idle"
|
||||
self._checkIfNewMaterialsWereInstalled()
|
||||
self._export_progress = 0.0
|
||||
self._printer_status = {} # type: Dict[str, str]
|
||||
|
||||
def _checkIfNewMaterialsWereInstalled(self) -> None:
|
||||
"""
|
||||
Checks whether new material packages were installed in the latest startup. If there were, then it shows
|
||||
a message prompting the user to sync the materials with their printers.
|
||||
"""
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
|
||||
if package_data["package_info"]["package_type"] == "material":
|
||||
# At least one new material was installed
|
||||
self._showSyncNewMaterialsMessage()
|
||||
break
|
||||
|
||||
def openSyncAllWindow(self):
|
||||
|
||||
self.reset()
|
||||
|
||||
if self.sync_all_dialog is None:
|
||||
qml_path = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QmlFiles, "Preferences",
|
||||
"Materials", "MaterialsSyncDialog.qml")
|
||||
self.sync_all_dialog = cura.CuraApplication.CuraApplication.getInstance().createQmlComponent(
|
||||
qml_path, {})
|
||||
if self.sync_all_dialog is None: # Failed to load QML file.
|
||||
return
|
||||
self.sync_all_dialog.setProperty("syncModel", self)
|
||||
self.sync_all_dialog.setProperty("pageIndex", 0) # Return to first page.
|
||||
self.sync_all_dialog.setProperty("hasExportedUsb", False) # If the user exported USB before, reset that page.
|
||||
self.sync_all_dialog.show()
|
||||
|
||||
def _showSyncNewMaterialsMessage(self) -> None:
|
||||
sync_materials_message = Message(
|
||||
text = catalog.i18nc("@action:button",
|
||||
"Please sync the material profiles with your printers before starting to print."),
|
||||
title = catalog.i18nc("@action:button", "New materials installed"),
|
||||
message_type = Message.MessageType.WARNING,
|
||||
lifetime = 0
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"sync",
|
||||
name = catalog.i18nc("@action:button", "Sync materials with printers"),
|
||||
icon = "",
|
||||
description = "Sync your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"learn_more",
|
||||
name = catalog.i18nc("@action:button", "Learn more"),
|
||||
icon = "",
|
||||
description = "Learn more about syncing your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style = Message.ActionButtonStyle.LINK
|
||||
)
|
||||
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
|
||||
|
||||
# Show the message only if there are printers that support material export
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
global_stacks = container_registry.findContainerStacks(type = "machine")
|
||||
if any([stack.supportsMaterialExport for stack in global_stacks]):
|
||||
sync_materials_message.show()
|
||||
|
||||
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
|
||||
if sync_message_action == "sync":
|
||||
self.openSyncAllWindow()
|
||||
sync_message.hide()
|
||||
elif sync_message_action == "learn_more":
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
def getPreferredExportAllPath(self) -> QUrl:
|
||||
"""
|
||||
Get the preferred path to export materials to.
|
||||
|
||||
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
|
||||
file path.
|
||||
:return: The preferred path to export all materials to.
|
||||
"""
|
||||
cura_application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
device_manager = cura_application.getOutputDeviceManager()
|
||||
devices = device_manager.getOutputDevices()
|
||||
for device in devices:
|
||||
if device.__class__.__name__ == "RemovableDriveOutputDevice":
|
||||
return QUrl.fromLocalFile(device.getId())
|
||||
else: # No removable drives? Use local path.
|
||||
return cura_application.getDefaultPath("dialog_material_path")
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def exportAll(self, file_path: QUrl, notify_progress: Optional["Signal"] = None) -> None:
|
||||
"""
|
||||
Export all materials to a certain file path.
|
||||
:param file_path: The path to export the materials to.
|
||||
"""
|
||||
registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
# Create empty archive.
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
|
||||
except OSError as e:
|
||||
Logger.log("e", f"Can't write to destination {file_path.toLocalFile()}: {type(e)} - {str(e)}")
|
||||
error_message = Message(
|
||||
text = catalog.i18nc("@message:text", "Could not save material archive to {}:").format(file_path.toLocalFile()) + " " + str(e),
|
||||
title = catalog.i18nc("@message:title", "Failed to save material archive"),
|
||||
message_type = Message.MessageType.ERROR
|
||||
)
|
||||
error_message.show()
|
||||
return
|
||||
|
||||
materials_metadata = registry.findInstanceContainersMetadata(type = "material")
|
||||
for index, metadata in enumerate(materials_metadata):
|
||||
if notify_progress is not None:
|
||||
progress = index / len(materials_metadata)
|
||||
notify_progress.emit(progress)
|
||||
if metadata["base_file"] != metadata["id"]: # Only process base files.
|
||||
continue
|
||||
if metadata["id"] == "empty_material": # Don't export the empty material.
|
||||
continue
|
||||
material = registry.findContainers(id = metadata["id"])[0]
|
||||
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
|
||||
filename = metadata["id"] + "." + suffix
|
||||
try:
|
||||
archive.writestr(filename, material.serialize())
|
||||
except OSError as e:
|
||||
Logger.log("e", f"An error has occurred while writing the material \'{metadata['id']}\' in the file \'{filename}\': {e}.")
|
||||
|
||||
exportUploadStatusChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = exportUploadStatusChanged)
|
||||
def exportUploadStatus(self) -> str:
|
||||
return self._export_upload_status
|
||||
|
||||
@pyqtSlot()
|
||||
def exportUpload(self) -> None:
|
||||
"""
|
||||
Export all materials and upload them to the user's account.
|
||||
"""
|
||||
self._export_upload_status = "uploading"
|
||||
self.exportUploadStatusChanged.emit()
|
||||
job = UploadMaterialsJob(self)
|
||||
job.uploadProgressChanged.connect(self._onUploadProgressChanged)
|
||||
job.uploadCompleted.connect(self.exportUploadCompleted)
|
||||
job.start()
|
||||
|
||||
def _onUploadProgressChanged(self, progress: float, printers_status: Dict[str, str]):
|
||||
self.setExportProgress(progress)
|
||||
self.setPrinterStatus(printers_status)
|
||||
|
||||
def exportUploadCompleted(self, job_result: UploadMaterialsJob.Result, job_error: Optional[Exception]):
|
||||
if not self.sync_all_dialog: # Shouldn't get triggered before the dialog is open, but better to check anyway.
|
||||
return
|
||||
if job_result == UploadMaterialsJob.Result.FAILED:
|
||||
if isinstance(job_error, UploadMaterialsError):
|
||||
self.sync_all_dialog.setProperty("syncStatusText", str(job_error))
|
||||
else: # Could be "None"
|
||||
self.sync_all_dialog.setProperty("syncStatusText", catalog.i18nc("@text", "Unknown error."))
|
||||
self._export_upload_status = "error"
|
||||
else:
|
||||
self._export_upload_status = "success"
|
||||
self.exportUploadStatusChanged.emit()
|
||||
|
||||
exportProgressChanged = pyqtSignal(float)
|
||||
|
||||
def setExportProgress(self, progress: float) -> None:
|
||||
self._export_progress = progress
|
||||
self.exportProgressChanged.emit(self._export_progress)
|
||||
|
||||
@pyqtProperty(float, fset = setExportProgress, notify = exportProgressChanged)
|
||||
def exportProgress(self) -> float:
|
||||
return self._export_progress
|
||||
|
||||
printerStatusChanged = pyqtSignal()
|
||||
|
||||
def setPrinterStatus(self, new_status: Dict[str, str]) -> None:
|
||||
self._printer_status = new_status
|
||||
self.printerStatusChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantMap", fset = setPrinterStatus, notify = printerStatusChanged)
|
||||
def printerStatus(self) -> Dict[str, str]:
|
||||
return self._printer_status
|
||||
|
||||
def reset(self) -> None:
|
||||
self.setPrinterStatus({})
|
||||
self.setExportProgress(0.0)
|
||||
self._export_upload_status = "idle"
|
||||
self.exportUploadStatusChanged.emit()
|
|
@ -1,9 +1,15 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.API.Account import Account
|
||||
|
||||
|
||||
class UltimakerCloudScope(DefaultUserAgentScope):
|
||||
|
@ -12,7 +18,7 @@ class UltimakerCloudScope(DefaultUserAgentScope):
|
|||
Also add the user agent headers (see DefaultUserAgentScope).
|
||||
"""
|
||||
|
||||
def __init__(self, application: CuraApplication):
|
||||
def __init__(self, application: "CuraApplication"):
|
||||
super().__init__(application)
|
||||
api = application.getCuraAPI()
|
||||
self._account = api.account # type: Account
|
||||
|
|
|
@ -50,7 +50,7 @@ do
|
|||
echo "Found Uranium branch [${URANIUM_BRANCH}]."
|
||||
break
|
||||
else
|
||||
echo "Could not find Uranium banch [${URANIUM_BRANCH}], try next."
|
||||
echo "Could not find Uranium branch [${URANIUM_BRANCH}], try next."
|
||||
fi
|
||||
done
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ The build volume draws a cube (for rectangular build plates) that represents the
|
|||
|
||||
The build volume also draws a grid underneath the build volume. The grid features 1cm lines which allows the user to roughly estimate how big its print is or the distance between prints. It also features a finer 1mm line pattern within that grid. The grid is drawn as a single quad. This quad is then sent to the graphical card with a specialised shader which draws the grid pattern.
|
||||
|
||||
For elliptical build plates, the volume bounds are drawn as two circles, one at the top and one at the bottom of the available height. The build plate grid is drawn as a tesselated circle, but with the same shader.
|
||||
For elliptical build plates, the volume bounds are drawn as two circles, one at the top and one at the bottom of the available height. The build plate grid is drawn as a tessellated circle, but with the same shader.
|
||||
|
||||
Disallowed areas
|
||||
----
|
||||
|
|
|
@ -428,6 +428,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
"Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"),
|
||||
message_type = Message.MessageType.WARNING)
|
||||
Logger.warning(f"Unable to slice with the current settings. The following settings have errors: {', '.join(error_labels)}")
|
||||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
|
@ -454,6 +455,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
"Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"),
|
||||
message_type = Message.MessageType.WARNING)
|
||||
Logger.warning(f"Unable to slice due to per-object settings. The following settings have errors on one or more models: {', '.join(errors.values())}")
|
||||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
|
@ -468,6 +470,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
return
|
||||
else:
|
||||
self.setState(BackendState.NotStarted)
|
||||
|
||||
|
@ -646,7 +649,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
||||
# We can asume that all nodes have a parent as we're looping through the scene (and filter out root)
|
||||
# We can assume that all nodes have a parent as we're looping through the scene (and filter out root)
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
|
||||
def markSliceAll(self) -> None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
|
@ -123,6 +123,9 @@ class StartSliceJob(Job):
|
|||
Job.yieldThread()
|
||||
|
||||
for changed_setting_key in changed_setting_keys:
|
||||
if not stack.getProperty(changed_setting_key, "enabled"):
|
||||
continue
|
||||
|
||||
validation_state = stack.getProperty(changed_setting_key, "validationState")
|
||||
|
||||
if validation_state is None:
|
||||
|
@ -195,7 +198,7 @@ class StartSliceJob(Job):
|
|||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||
# Since we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
|
||||
|
@ -353,10 +356,19 @@ class StartSliceJob(Job):
|
|||
result[key] = stack.getProperty(key, "value")
|
||||
Job.yieldThread()
|
||||
|
||||
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
||||
# Material identification in addition to non-human-readable GUID
|
||||
result["material_id"] = stack.material.getMetaDataEntry("base_file", "")
|
||||
result["material_type"] = stack.material.getMetaDataEntry("material", "")
|
||||
result["material_name"] = stack.material.getMetaDataEntry("name", "")
|
||||
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
|
||||
|
||||
# Renamed settings.
|
||||
result["print_bed_temperature"] = result["material_bed_temperature"]
|
||||
result["print_temperature"] = result["material_print_temperature"]
|
||||
result["travel_speed"] = result["speed_travel"]
|
||||
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||
|
||||
#Some extra settings.
|
||||
result["time"] = time.strftime("%H:%M:%S")
|
||||
result["date"] = time.strftime("%d-%m-%Y")
|
||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
|
||||
|
@ -455,9 +467,9 @@ class StartSliceJob(Job):
|
|||
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
|
||||
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature", "print_temperature"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) is None
|
||||
|
||||
# Replace the setting tokens in start and end g-code.
|
||||
# Use values from the first used extruder by default so we get the expected temperatures
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Ultimaker Digital Library",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"api": 7,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -200,7 +200,7 @@ Item
|
|||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
text: "Save"
|
||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1
|
||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
|
||||
|
||||
onClicked:
|
||||
{
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Message import Message
|
||||
from UM.Version import Version
|
||||
|
||||
def getBackwardsCompatibleMessage(text: str, title: str, lifetime: int, message_type_str: str) -> Message:
|
||||
|
||||
def getBackwardsCompatibleMessage(text: str, title: str, message_type_str: str, lifetime: Optional[int] = 30) -> Message:
|
||||
if CuraApplication.getInstance().getAPIVersion() < Version("7.7.0"):
|
||||
return Message(text=text, title=title, lifetime=lifetime)
|
||||
else:
|
||||
|
|
|
@ -73,7 +73,7 @@ class DFFileExportAndUploadManager:
|
|||
self._generic_success_message = getBackwardsCompatibleMessage(
|
||||
text = "Your {} uploaded to '{}'.".format("file was" if len(self._file_upload_job_metadata) <= 1 else "files were", self._library_project_name),
|
||||
title = "Upload successful",
|
||||
lifetime = 0,
|
||||
lifetime = 30,
|
||||
message_type_str = "POSITIVE"
|
||||
)
|
||||
self._generic_success_message.addAction(
|
||||
|
@ -221,8 +221,8 @@ class DFFileExportAndUploadManager:
|
|||
self._file_upload_job_metadata[filename]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
text = "Failed to export the file '{}'. The upload process is aborted.".format(filename),
|
||||
title = "Export error",
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
self._on_upload_error()
|
||||
self._onFileUploadFinished(filename)
|
||||
|
@ -244,8 +244,8 @@ class DFFileExportAndUploadManager:
|
|||
self._file_upload_job_metadata[filename_3mf]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
text = "Failed to upload the file '{}' to '{}'. {}".format(filename_3mf, self._library_project_name, human_readable_error),
|
||||
title = "File upload error",
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
self._on_upload_error()
|
||||
self._onFileUploadFinished(filename_3mf)
|
||||
|
@ -267,8 +267,8 @@ class DFFileExportAndUploadManager:
|
|||
self._file_upload_job_metadata[filename_ufp]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
title = "File upload error",
|
||||
text = "Failed to upload the file '{}' to '{}'. {}".format(filename_ufp, self._library_project_name, human_readable_error),
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
self._on_upload_error()
|
||||
self._onFileUploadFinished(filename_ufp)
|
||||
|
@ -304,8 +304,8 @@ class DFFileExportAndUploadManager:
|
|||
self._file_upload_job_metadata[filename]["file_upload_failed_message"] = getBackwardsCompatibleMessage(
|
||||
title = "File upload error",
|
||||
text = "Failed to upload the file '{}' to '{}'. {}".format(self._file_name, self._library_project_name, human_readable_error),
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
|
||||
self._on_upload_error()
|
||||
|
@ -341,14 +341,14 @@ class DFFileExportAndUploadManager:
|
|||
"file_upload_success_message": getBackwardsCompatibleMessage(
|
||||
text = "'{}' was uploaded to '{}'.".format(filename_3mf, self._library_project_name),
|
||||
title = "Upload successful",
|
||||
lifetime = 0,
|
||||
message_type_str = "POSITIVE"
|
||||
message_type_str = "POSITIVE",
|
||||
lifetime = 30
|
||||
),
|
||||
"file_upload_failed_message": getBackwardsCompatibleMessage(
|
||||
text = "Failed to upload the file '{}' to '{}'.".format(filename_3mf, self._library_project_name),
|
||||
title = "File upload error",
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
}
|
||||
job_3mf = ExportFileJob(self._file_handlers["3mf"], self._nodes, self._file_name, "3mf")
|
||||
|
@ -365,14 +365,14 @@ class DFFileExportAndUploadManager:
|
|||
"file_upload_success_message": getBackwardsCompatibleMessage(
|
||||
text = "'{}' was uploaded to '{}'.".format(filename_ufp, self._library_project_name),
|
||||
title = "Upload successful",
|
||||
lifetime = 0,
|
||||
message_type_str = "POSITIVE"
|
||||
message_type_str = "POSITIVE",
|
||||
lifetime = 30,
|
||||
),
|
||||
"file_upload_failed_message": getBackwardsCompatibleMessage(
|
||||
text = "Failed to upload the file '{}' to '{}'.".format(filename_ufp, self._library_project_name),
|
||||
title = "File upload error",
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR"
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 30
|
||||
)
|
||||
}
|
||||
job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp")
|
||||
|
|
|
@ -67,10 +67,12 @@ class DigitalFactoryApiClient:
|
|||
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
|
||||
if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and
|
||||
response.library_max_private_projects is not None):
|
||||
callback(
|
||||
response.library_max_private_projects == -1 or # Note: -1 is unlimited
|
||||
response.library_max_private_projects > 0)
|
||||
# A user has DF access when library_max_private_projects is either -1 (unlimited) or bigger then 0
|
||||
has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
|
||||
callback(has_access)
|
||||
self._library_max_private_projects = response.library_max_private_projects
|
||||
# update the account with the additional user rights
|
||||
self._account.updateAdditionalRight(df_access = has_access)
|
||||
else:
|
||||
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
||||
callback(False)
|
||||
|
|
|
@ -531,8 +531,8 @@ class DigitalFactoryController(QObject):
|
|||
getBackwardsCompatibleMessage(
|
||||
text = "Failed to write to temporary file for '{}'.".format(file_name),
|
||||
title = "File-system error",
|
||||
lifetime = 10,
|
||||
message_type_str="ERROR"
|
||||
message_type_str="ERROR",
|
||||
lifetime = 10
|
||||
).show()
|
||||
return
|
||||
|
||||
|
@ -546,8 +546,8 @@ class DigitalFactoryController(QObject):
|
|||
getBackwardsCompatibleMessage(
|
||||
text = "Failed Digital Library download for '{}'.".format(f),
|
||||
title = "Network error {}".format(error),
|
||||
lifetime = 10,
|
||||
message_type_str="ERROR"
|
||||
message_type_str="ERROR",
|
||||
lifetime = 10
|
||||
).show()
|
||||
|
||||
download_manager = HttpRequestManager.getInstance()
|
||||
|
@ -592,17 +592,19 @@ class DigitalFactoryController(QObject):
|
|||
|
||||
if filename == "":
|
||||
Logger.log("w", "The file name cannot be empty.")
|
||||
getBackwardsCompatibleMessage(text = "Cannot upload file with an empty name to the Digital Library",
|
||||
getBackwardsCompatibleMessage(
|
||||
text = "Cannot upload file with an empty name to the Digital Library",
|
||||
title = "Empty file name provided",
|
||||
lifetime = 0,
|
||||
message_type_str = "ERROR").show()
|
||||
message_type_str = "ERROR",
|
||||
lifetime = 0
|
||||
).show()
|
||||
return
|
||||
|
||||
self._saveFileToSelectedProjectHelper(filename, formats)
|
||||
|
||||
def _saveFileToSelectedProjectHelper(self, filename: str, formats: List[str]) -> None:
|
||||
# Indicate we have started sending a job.
|
||||
self.uploadStarted.emit()
|
||||
# Indicate we have started sending a job (and propagate any user file name changes back to the open project)
|
||||
self.uploadStarted.emit(filename if "3mf" in formats else None)
|
||||
|
||||
library_project_id = self._project_model.items[self._selected_project_idx]["libraryProjectId"]
|
||||
library_project_name = self._project_model.items[self._selected_project_idx]["displayName"]
|
||||
|
|
|
@ -8,6 +8,8 @@ from UM.Logger import Logger
|
|||
from UM.OutputDevice import OutputDeviceError
|
||||
from UM.OutputDevice.ProjectOutputDevice import ProjectOutputDevice
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Version import Version
|
||||
from cura import ApplicationMetadata
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from .DigitalFactoryController import DigitalFactoryController
|
||||
|
@ -105,8 +107,11 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
|
|||
self.enabled = logged_in and self._controller.userAccountHasLibraryAccess()
|
||||
self.enabledChanged.emit()
|
||||
|
||||
def _onWriteStarted(self) -> None:
|
||||
def _onWriteStarted(self, new_name: Optional[str] = None) -> None:
|
||||
self._writing = True
|
||||
if new_name and Version(ApplicationMetadata.CuraSDKVersion) >= Version("7.8.0"):
|
||||
# setLastOutputName is only supported in sdk version 7.8.0 and up
|
||||
self.setLastOutputName(new_name) # On saving, the user can change the name, this should propagate.
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
def _onWriteFinished(self) -> None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import math
|
||||
|
@ -153,7 +153,7 @@ class FlavorParser:
|
|||
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
|
||||
# Length of the extruded filament
|
||||
de = current_extrusion - previous_extrusion
|
||||
# Volumne of the extruded filament
|
||||
# Volume of the extruded filament
|
||||
dVe = de * Af
|
||||
# Length of the printed line
|
||||
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
|
||||
|
@ -198,7 +198,7 @@ class FlavorParser:
|
|||
|
||||
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
|
||||
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
|
||||
if z > self._previous_z and (z - self._previous_z < 1.5):
|
||||
if z > self._previous_z and (z - self._previous_z < 1.5) and (params.x is not None or params.y is not None):
|
||||
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
|
||||
self._previous_z = z
|
||||
elif self._previous_extrusion_value > e[self._extruder_number]:
|
||||
|
@ -428,7 +428,7 @@ class FlavorParser:
|
|||
|
||||
G = self._getInt(line, "G")
|
||||
if G is not None:
|
||||
# When find a movement, the new posistion is calculated and added to the current_path, but
|
||||
# When find a movement, the new position is calculated and added to the current_path, but
|
||||
# don't need to create a polygon until the end of the layer
|
||||
current_position = self.processGCode(G, line, current_position, current_path)
|
||||
continue
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from . import FlavorParser
|
||||
|
||||
# This parser is intented for interpret the Marlin/Sprinter Firmware flavor
|
||||
# This parser is intended to interpret the Marlin/Sprinter Firmware flavor
|
||||
class MarlinFlavorParser(FlavorParser.FlavorParser):
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -159,7 +159,7 @@ Rectangle
|
|||
{
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: Qt.openUrlExternally("https://ultimaker.com/en/resources/manuals/ultimaker-3d-printers")
|
||||
onClicked: Qt.openUrlExternally("https://ultimaker.com/in/cura/troubleshooting/network?utm_source=cura&utm_medium=software&utm_campaign=monitor-not-connected")
|
||||
onEntered: manageQueueText.font.underline = true
|
||||
onExited: manageQueueText.font.underline = false
|
||||
}
|
||||
|
|
|
@ -312,7 +312,7 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
|
||||
// Specialty provider that only watches global_inherits (we can't filter on what property changed we get events
|
||||
// so we bypass that to make a dedicated provider).
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
|
|
|
@ -96,11 +96,11 @@ UM.Dialog
|
|||
}
|
||||
showAll: toggleShowAll.checked || filterInput.text !== ""
|
||||
}
|
||||
delegate:Loader
|
||||
delegate: Loader
|
||||
{
|
||||
id: loader
|
||||
|
||||
width: parent.width
|
||||
width: listview.width
|
||||
height: model.type != undefined ? UM.Theme.getSize("section").height : 0
|
||||
|
||||
property var definition: model
|
||||
|
|
|
@ -403,7 +403,7 @@ UM.Dialog
|
|||
storeIndex: 0
|
||||
}
|
||||
|
||||
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
|
||||
// Specialty provider that only watches global_inherits (we can't filter on what property changed we get events
|
||||
// so we bypass that to make a dedicated provider).
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
|
||||
# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
|
||||
# Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
|
||||
# Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug.
|
||||
# Modified by Ghostkeeper (Ultimaker), rubend@tutanota.com, to debug.
|
||||
# Modified by Wes Hanney, https://github.com/novamxd, Retract Length + Speed, Clean up
|
||||
# Modified by Alex Jaxon, https://github.com/legend069, Added option to modify Build Volume Temperature
|
||||
|
||||
|
||||
# history / changelog:
|
||||
# V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
|
||||
|
@ -31,15 +33,19 @@
|
|||
# V4.9.93: Minor bugfixes (input settings) / documentation
|
||||
# V4.9.94: Bugfix Combobox-selection; remove logger
|
||||
# V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
|
||||
# V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
|
||||
# V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unknown variable 'speed'
|
||||
# V5.1: API Changes included for use with Cura 2.2
|
||||
# V5.2.0: Wes Hanney. Added support for changing Retract Length and Speed. Removed layer spread option. Fixed issue of cumulative ChangeZ
|
||||
# V5.2.0: Wes Hanney. Added support for changing Retract Length and Speed. Removed layer spread option. Fixed issue of cumulative ChangeAtZ
|
||||
# mods so they can now properly be stacked on top of each other. Applied code refactoring to clean up various coding styles. Added comments.
|
||||
# Broke up functions for clarity. Split up class so it can be debugged outside of Cura.
|
||||
# V5.2.1: Wes Hanney. Added support for firmware based retractions. Fixed issue of properly restoring previous values in single layer option.
|
||||
# Added support for outputting changes to LCD (untested). Added type hints to most functions and variables. Added more comments. Created GCodeCommand
|
||||
# class for better detection of G1 vs G10 or G11 commands, and accessing arguments. Moved most GCode methods to GCodeCommand class. Improved wording
|
||||
# of Single Layer vs Keep Layer to better reflect what was happening.
|
||||
# V5.3.0 Alex Jaxon, Added option to modify Build Volume Temperature keeping current format
|
||||
#
|
||||
|
||||
|
||||
|
||||
# Uses -
|
||||
# M220 S<factor in percent> - set speed factor override percentage
|
||||
|
@ -56,9 +62,9 @@ from ..Script import Script
|
|||
import re
|
||||
|
||||
|
||||
# this was broken up into a separate class so the main ChangeZ script could be debugged outside of Cura
|
||||
# this was broken up into a separate class so the main ChangeAtZ script could be debugged outside of Cura
|
||||
class ChangeAtZ(Script):
|
||||
version = "5.2.1"
|
||||
version = "5.3.0"
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
|
@ -69,7 +75,7 @@ class ChangeAtZ(Script):
|
|||
"settings": {
|
||||
"caz_enabled": {
|
||||
"label": "Enabled",
|
||||
"description": "Allows adding multiple ChangeZ mods and disabling them as needed.",
|
||||
"description": "Allows adding multiple ChangeAtZ mods and disabling them as needed.",
|
||||
"type": "bool",
|
||||
"default_value": true
|
||||
},
|
||||
|
@ -222,6 +228,23 @@ class ChangeAtZ(Script):
|
|||
"maximum_value_warning": "120",
|
||||
"enabled": "h1_Change_bedTemp"
|
||||
},
|
||||
"h1_Change_buildVolumeTemperature": {
|
||||
"label": "Change Build Volume Temperature",
|
||||
"description": "Select if Build Volume Temperature has to be changed",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
},
|
||||
"h2_buildVolumeTemperature": {
|
||||
"label": "Build Volume Temperature",
|
||||
"description": "New Build Volume Temperature",
|
||||
"unit": "C",
|
||||
"type": "float",
|
||||
"default_value": 20,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "10",
|
||||
"maximum_value_warning": "50",
|
||||
"enabled": "h1_Change_buildVolumeTemperature"
|
||||
},
|
||||
"i1_Change_extruderOne": {
|
||||
"label": "Change Extruder 1 Temp",
|
||||
"description": "Select if First Extruder Temperature has to be changed",
|
||||
|
@ -345,6 +368,7 @@ class ChangeAtZ(Script):
|
|||
self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne")
|
||||
self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo")
|
||||
self.setFloatSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp")
|
||||
self.setFloatSettingIfEnabled(caz_instance, "h1_Change_buildVolumeTemperature", "buildVolumeTemperature", "h2_buildVolumeTemperature")
|
||||
self.setFloatSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
|
||||
self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
|
||||
self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
|
||||
|
@ -657,7 +681,7 @@ class ChangeAtZProcessor:
|
|||
# Indicates if the user has opted for linear move retractions or firmware retractions
|
||||
linearRetraction = True
|
||||
|
||||
# Indicates if we're targetting by layer or height value
|
||||
# Indicates if we're targeting by layer or height value
|
||||
targetByLayer = True
|
||||
|
||||
# Indicates if we have injected our changed values for the given layer yet
|
||||
|
@ -776,6 +800,10 @@ class ChangeAtZProcessor:
|
|||
if "bedTemp" in values:
|
||||
codes.append("BedTemp: " + str(round(values["bedTemp"])))
|
||||
|
||||
# looking for wait for Build Volume Temperature
|
||||
if "buildVolumeTemperature" in values:
|
||||
codes.append("buildVolumeTemperature: " + str(round(values["buildVolumeTemperature"])))
|
||||
|
||||
# set our extruder one temp (if specified)
|
||||
if "extruderOne" in values:
|
||||
codes.append("Extruder 1 Temp: " + str(round(values["extruderOne"])))
|
||||
|
@ -858,6 +886,10 @@ class ChangeAtZProcessor:
|
|||
if "bedTemp" in values:
|
||||
codes.append("M140 S" + str(values["bedTemp"]))
|
||||
|
||||
# looking for wait for Build Volume Temperature
|
||||
if "buildVolumeTemperature" in values:
|
||||
codes.append("M141 S" + str(values["buildVolumeTemperature"]))
|
||||
|
||||
# set our extruder one temp (if specified)
|
||||
if "extruderOne" in values:
|
||||
codes.append("M104 S" + str(values["extruderOne"]) + " T0")
|
||||
|
@ -943,7 +975,7 @@ class ChangeAtZProcessor:
|
|||
# nothing to do
|
||||
return ""
|
||||
|
||||
# Returns the unmodified GCODE line from previous ChangeZ edits
|
||||
# Returns the unmodified GCODE line from previous ChangeAtZ edits
|
||||
@staticmethod
|
||||
def getOriginalLine(line: str) -> str:
|
||||
|
||||
|
@ -990,7 +1022,7 @@ class ChangeAtZProcessor:
|
|||
else:
|
||||
return self.currentZ >= self.targetZ
|
||||
|
||||
# Marks any current ChangeZ layer defaults in the layer for deletion
|
||||
# Marks any current ChangeAtZ layer defaults in the layer for deletion
|
||||
@staticmethod
|
||||
def markChangesForDeletion(layer: str):
|
||||
return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
|
||||
|
@ -1079,7 +1111,7 @@ class ChangeAtZProcessor:
|
|||
else:
|
||||
modified_gcode += line + "\n"
|
||||
|
||||
# if we're targetting by layer we want to add our values just after the layer label
|
||||
# if we're targeting by layer we want to add our values just after the layer label
|
||||
if ";LAYER:" in line:
|
||||
modified_gcode += self.getInjectCode()
|
||||
|
||||
|
@ -1288,7 +1320,7 @@ class ChangeAtZProcessor:
|
|||
# flag that we're inside our target layer
|
||||
self.insideTargetLayer = True
|
||||
|
||||
# Removes all the ChangeZ layer defaults from the given layer
|
||||
# Removes all the ChangeAtZ layer defaults from the given layer
|
||||
@staticmethod
|
||||
def removeMarkedChanges(layer: str) -> str:
|
||||
return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
|
||||
|
@ -1364,14 +1396,24 @@ class ChangeAtZProcessor:
|
|||
# move to the next command
|
||||
return
|
||||
|
||||
# handle Build Volume Temperature changes, really shouldn't want to wait for enclousure temp mid print though.
|
||||
if command.command == "M141" or command.command == "M191":
|
||||
|
||||
# get our bed temp if provided
|
||||
if "S" in command.arguments:
|
||||
self.lastValues["buildVolumeTemperature"] = command.getArgumentAsFloat("S")
|
||||
|
||||
# move to the next command
|
||||
return
|
||||
|
||||
# handle extruder temp changes
|
||||
if command.command == "M104" or command.command == "M109":
|
||||
|
||||
# get our tempurature
|
||||
tempurature = command.getArgumentAsFloat("S")
|
||||
# get our temperature
|
||||
temperature = command.getArgumentAsFloat("S")
|
||||
|
||||
# don't bother if we don't have a tempurature
|
||||
if tempurature is None:
|
||||
# don't bother if we don't have a temperature
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
# get our extruder, default to extruder one
|
||||
|
@ -1379,10 +1421,10 @@ class ChangeAtZProcessor:
|
|||
|
||||
# set our extruder temp based on the extruder
|
||||
if extruder is None or extruder == 0:
|
||||
self.lastValues["extruderOne"] = tempurature
|
||||
self.lastValues["extruderOne"] = temperature
|
||||
|
||||
if extruder is None or extruder == 1:
|
||||
self.lastValues["extruderTwo"] = tempurature
|
||||
self.lastValues["extruderTwo"] = temperature
|
||||
|
||||
# move to the next command
|
||||
return
|
||||
|
@ -1401,10 +1443,10 @@ class ChangeAtZProcessor:
|
|||
if command.command == "M221":
|
||||
|
||||
# get our flow rate
|
||||
tempurature = command.getArgumentAsFloat("S")
|
||||
temperature = command.getArgumentAsFloat("S")
|
||||
|
||||
# don't bother if we don't have a flow rate (for some reason)
|
||||
if tempurature is None:
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
# get our extruder, default to global
|
||||
|
@ -1412,11 +1454,11 @@ class ChangeAtZProcessor:
|
|||
|
||||
# set our extruder temp based on the extruder
|
||||
if extruder is None:
|
||||
self.lastValues["flowrate"] = tempurature
|
||||
self.lastValues["flowrate"] = temperature
|
||||
elif extruder == 1:
|
||||
self.lastValues["flowrateOne"] = tempurature
|
||||
self.lastValues["flowrateOne"] = temperature
|
||||
elif extruder == 1:
|
||||
self.lastValues["flowrateTwo"] = tempurature
|
||||
self.lastValues["flowrateTwo"] = temperature
|
||||
|
||||
# move to the next command
|
||||
return
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
# Description: This plugin shows custom messages about your print on the Status bar...
|
||||
# Please look at the 3 options
|
||||
# - Scolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you arent printing a small item select this option.
|
||||
# - Scrolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you aren't printing a small item select this option.
|
||||
# - Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - Type a custom name in here
|
||||
# - Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
from typing import List
|
||||
from ..Script import Script
|
||||
|
||||
from UM.Application import Application #To get the current printer's settings.
|
||||
|
||||
class FilamentChange(Script):
|
||||
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
@ -81,10 +83,51 @@ class FilamentChange(Script):
|
|||
"type": "float",
|
||||
"default_value": 0,
|
||||
"minimum_value": 0
|
||||
},
|
||||
"retract_method":
|
||||
{
|
||||
"label": "Retract method",
|
||||
"description": "The gcode variant to use for retract.",
|
||||
"type": "enum",
|
||||
"options": {"U": "Marlin (M600 U)", "L": "Reprap (M600 L)"},
|
||||
"default_value": "U",
|
||||
"value": "\\\"L\\\" if machine_gcode_flavor==\\\"RepRap (RepRap)\\\" else \\\"U\\\"",
|
||||
"enabled": "not firmware_config"
|
||||
},
|
||||
"machine_gcode_flavor":
|
||||
{
|
||||
"label": "G-code flavor",
|
||||
"description": "The type of g-code to be generated. This setting is controlled by the script and will not be visible.",
|
||||
"type": "enum",
|
||||
"options":
|
||||
{
|
||||
"RepRap (Marlin/Sprinter)": "Marlin",
|
||||
"RepRap (Volumetric)": "Marlin (Volumetric)",
|
||||
"RepRap (RepRap)": "RepRap",
|
||||
"UltiGCode": "Ultimaker 2",
|
||||
"Griffin": "Griffin",
|
||||
"Makerbot": "Makerbot",
|
||||
"BFB": "Bits from Bytes",
|
||||
"MACH3": "Mach3",
|
||||
"Repetier": "Repetier"
|
||||
},
|
||||
"default_value": "RepRap (Marlin/Sprinter)",
|
||||
"enabled": "false"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
## Copy machine name and gcode flavor from global stack so we can use their value in the script stack
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None or self._instance is None:
|
||||
return
|
||||
|
||||
for key in ["machine_gcode_flavor"]:
|
||||
self._instance.setProperty(key, "value", global_container_stack.getProperty(key, "value"))
|
||||
|
||||
def execute(self, data: List[str]):
|
||||
"""Inserts the filament change g-code at specific layer numbers.
|
||||
|
||||
|
@ -106,7 +149,10 @@ class FilamentChange(Script):
|
|||
color_change = color_change + (" E%.2f" % initial_retract)
|
||||
|
||||
if later_retract is not None and later_retract > 0.:
|
||||
color_change = color_change + (" L%.2f" % later_retract)
|
||||
# Reprap uses 'L': https://reprap.org/wiki/G-code#M600:_Filament_change_pause
|
||||
# Marlin uses 'U' https://marlinfw.org/docs/gcode/M600.html
|
||||
retract_method = self.getSettingValueByKey("retract_method")
|
||||
color_change = color_change + (" %s%.2f" % (retract_method, later_retract))
|
||||
|
||||
if x_pos is not None:
|
||||
color_change = color_change + (" X%.2f" % x_pos)
|
||||
|
|
|
@ -54,7 +54,7 @@ class PauseAtHeight(Script):
|
|||
"label": "Method",
|
||||
"description": "The method or gcode command to use for pausing.",
|
||||
"type": "enum",
|
||||
"options": {"marlin": "Marlin (M0)", "griffin": "Griffin (M0, firmware retract)", "bq": "BQ (M25)", "reprap": "RepRap (M226)", "repetier": "Repetier (@pause)"},
|
||||
"options": {"marlin": "Marlin (M0)", "griffin": "Griffin (M0, firmware retract)", "bq": "BQ (M25)", "reprap": "RepRap (M226)", "repetier": "Repetier/OctoPrint (@pause)"},
|
||||
"default_value": "marlin",
|
||||
"value": "\\\"griffin\\\" if machine_gcode_flavor==\\\"Griffin\\\" else \\\"reprap\\\" if machine_gcode_flavor==\\\"RepRap (RepRap)\\\" else \\\"repetier\\\" if machine_gcode_flavor==\\\"Repetier\\\" else \\\"bq\\\" if \\\"BQ\\\" in machine_name or \\\"Flying Bear Ghost 4S\\\" in machine_name else \\\"marlin\\\""
|
||||
},
|
||||
|
@ -69,6 +69,14 @@ class PauseAtHeight(Script):
|
|||
"maximum_value_warning": "1800",
|
||||
"unit": "s"
|
||||
},
|
||||
"head_park_enabled":
|
||||
{
|
||||
"label": "Park Print",
|
||||
"description": "Instruct the head to move to a safe location when pausing. Leave this unchecked if your printer handles parking for you.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "pause_method != \\\"griffin\\\""
|
||||
},
|
||||
"head_park_x":
|
||||
{
|
||||
"label": "Park Print Head X",
|
||||
|
@ -76,7 +84,7 @@ class PauseAtHeight(Script):
|
|||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 190,
|
||||
"enabled": "pause_method != \\\"griffin\\\""
|
||||
"enabled": "head_park_enabled and pause_method != \\\"griffin\\\""
|
||||
},
|
||||
"head_park_y":
|
||||
{
|
||||
|
@ -85,7 +93,7 @@ class PauseAtHeight(Script):
|
|||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 190,
|
||||
"enabled": "pause_method != \\\"griffin\\\""
|
||||
"enabled": "head_park_enabled and pause_method != \\\"griffin\\\""
|
||||
},
|
||||
"head_move_z":
|
||||
{
|
||||
|
@ -94,7 +102,7 @@ class PauseAtHeight(Script):
|
|||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 15.0,
|
||||
"enabled": "pause_method == \\\"repetier\\\""
|
||||
"enabled": "head_park_enabled and pause_method == \\\"repetier\\\""
|
||||
},
|
||||
"retraction_amount":
|
||||
{
|
||||
|
@ -239,6 +247,7 @@ class PauseAtHeight(Script):
|
|||
retraction_speed = self.getSettingValueByKey("retraction_speed")
|
||||
extrude_amount = self.getSettingValueByKey("extrude_amount")
|
||||
extrude_speed = self.getSettingValueByKey("extrude_speed")
|
||||
park_enabled = self.getSettingValueByKey("head_park_enabled")
|
||||
park_x = self.getSettingValueByKey("head_park_x")
|
||||
park_y = self.getSettingValueByKey("head_park_y")
|
||||
move_z = self.getSettingValueByKey("head_move_z")
|
||||
|
@ -389,11 +398,12 @@ class PauseAtHeight(Script):
|
|||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = 6000) + "\n"
|
||||
|
||||
#Move the head away
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
|
||||
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
|
||||
if current_z < move_z:
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + move_z, F = 300) + "\n"
|
||||
if park_enabled:
|
||||
#Move the head away
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
|
||||
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
|
||||
if current_z < move_z:
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + move_z, F = 300) + "\n"
|
||||
|
||||
#Disable the E steppers
|
||||
prepend_gcode += self.putValue(M = 84, E = 0) + "\n"
|
||||
|
@ -409,14 +419,15 @@ class PauseAtHeight(Script):
|
|||
else:
|
||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
|
||||
|
||||
# Move the head away
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
|
||||
if park_enabled:
|
||||
# Move the head away
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
|
||||
|
||||
# This line should be ok
|
||||
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
|
||||
# This line should be ok
|
||||
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
|
||||
|
||||
if current_z < 15:
|
||||
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + " ; too close to bed--move to at least 15mm\n"
|
||||
if current_z < 15:
|
||||
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + " ; too close to bed--move to at least 15mm\n"
|
||||
|
||||
if control_temperatures:
|
||||
# Set extruder standby temperature
|
||||
|
@ -447,7 +458,7 @@ class PauseAtHeight(Script):
|
|||
|
||||
# Optionally extrude material
|
||||
if extrude_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = 200) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = 200) + "; Extra extrude after the unpause\n"
|
||||
prepend_gcode += self.putValue("@info wait for cleaning nozzle from previous filament") + "\n"
|
||||
prepend_gcode += self.putValue("@pause remove the waste filament from parking area and press continue printing") + "\n"
|
||||
|
||||
|
@ -456,8 +467,10 @@ class PauseAtHeight(Script):
|
|||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = 6000) + "\n"
|
||||
|
||||
#Move the head back
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
|
||||
if park_enabled:
|
||||
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + "\n"
|
||||
|
||||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n"
|
||||
|
||||
|
@ -476,24 +489,25 @@ class PauseAtHeight(Script):
|
|||
# Set extruder resume temperature
|
||||
prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + " ; resume temperature\n"
|
||||
|
||||
# Push the filament back,
|
||||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
|
||||
if extrude_amount != 0: # Need to prime after the pause.
|
||||
# Push the filament back.
|
||||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
|
||||
|
||||
# Optionally extrude material
|
||||
if extrude_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n"
|
||||
# Prime the material.
|
||||
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "; Extra extrude after the unpause\n"
|
||||
|
||||
# and retract again, the properly primes the nozzle
|
||||
# when changing filament.
|
||||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
|
||||
# And retract again to make the movements back to the starting position.
|
||||
if retraction_amount != 0:
|
||||
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
|
||||
|
||||
# Move the head back
|
||||
if current_z < 15:
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + " ; move back down to resume height\n"
|
||||
if park_enabled:
|
||||
if current_z < 15:
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
|
||||
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + " ; move back down to resume height\n"
|
||||
|
||||
if retraction_amount != 0:
|
||||
if firmware_retract: #Can't set the distance directly to what the user wants. We have to choose ourselves.
|
||||
retraction_count = 1 if control_temperatures else 3 #Retract more if we don't control the temperature.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2017 Ruben Dulek
|
||||
# Copyright (c) 2017 Ghostkeeper
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import re #To perform the search and replace.
|
||||
|
|
|
@ -195,7 +195,7 @@ class Stretcher:
|
|||
i.e. it is a travel move
|
||||
"""
|
||||
if i_pos == 0:
|
||||
return True # Begining a layer always breaks filament (for simplicity)
|
||||
return True # Beginning a layer always breaks filament (for simplicity)
|
||||
step = layer_steps[i_pos]
|
||||
prev_step = layer_steps[i_pos - 1]
|
||||
if step.step_e != prev_step.step_e:
|
||||
|
|
|
@ -136,7 +136,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
self._stream.close()
|
||||
self._stream = None
|
||||
except:
|
||||
Logger.logException("w", "An execption occured while trying to write to removable drive.")
|
||||
Logger.logException("w", "An exception occurred while trying to write to removable drive.")
|
||||
message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(),str(job.getError())),
|
||||
title = catalog.i18nc("@info:title", "Error"),
|
||||
message_type = Message.MessageType.ERROR)
|
||||
|
|
|
@ -48,7 +48,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
|
|||
result = False
|
||||
|
||||
if result:
|
||||
Logger.log("i", "Succesfully ejected the device")
|
||||
Logger.log("i", "Successfully ejected the device")
|
||||
return result
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
|
|
|
@ -187,7 +187,7 @@ Item
|
|||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
|
||||
// don't allow the lower handle to be heigher than the upper handle
|
||||
// don't allow the lower handle to be higher than the upper handle
|
||||
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize)
|
||||
{
|
||||
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
|
||||
|
@ -300,7 +300,7 @@ Item
|
|||
// don't allow the upper handle to be lower than the lower handle
|
||||
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize)
|
||||
{
|
||||
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
|
||||
upperHandle.y = y - (upperHandle.height + sliderRoot.minimumRangeHandleSize)
|
||||
}
|
||||
|
||||
// update the range handle
|
||||
|
|
|
@ -142,6 +142,7 @@ class SimulationPass(RenderPass):
|
|||
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
||||
start = 0
|
||||
end = 0
|
||||
current_polygon_offset = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer in sorted(element_counts.keys()):
|
||||
# In the current layer, we show just the indicated paths
|
||||
|
@ -155,18 +156,26 @@ class SimulationPass(RenderPass):
|
|||
if index >= polygon.data.size // 3 - offset:
|
||||
index -= polygon.data.size // 3 - offset
|
||||
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
|
||||
current_polygon_offset += 1
|
||||
continue
|
||||
# The head position is calculated and translated
|
||||
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
|
||||
break
|
||||
break
|
||||
if self._layer_view._minimum_layer_num > layer:
|
||||
start += element_counts[layer]
|
||||
end += element_counts[layer]
|
||||
end += layer_data.getLayer(layer).vertexCount
|
||||
if layer < self._layer_view._minimum_layer_num:
|
||||
start = end
|
||||
|
||||
# Calculate the range of paths in the last layer
|
||||
# Calculate the range of paths in the last layer. -- The type-change count is needed to keep the
|
||||
# vertex-indices aligned between the two different ways we represent polygons here.
|
||||
# Since there is one type per line, that could give a vertex two different types, if it's a vertex
|
||||
# where a type-chage occurs. However, the shader expects vertices to have only one type. In order to
|
||||
# fix this, those vertices are duplicated. This introduces a discrepancy that we have to take into
|
||||
# account, which is done by the type-change-count.
|
||||
layer = layer_data.getLayer(self._layer_view._current_layer_num)
|
||||
type_change_count = 0 if layer is None else layer.lineMeshCumulativeTypeChangeCount(max(self._layer_view._current_path_num - 1, 0))
|
||||
current_layer_start = end
|
||||
current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice
|
||||
current_layer_end = current_layer_start + self._layer_view._current_path_num + current_polygon_offset + type_change_count
|
||||
|
||||
# This uses glDrawRangeElements internally to only draw a certain range of lines.
|
||||
# All the layers but the current selected layer are rendered first
|
||||
|
|
|
@ -59,7 +59,7 @@ UM.PointingRectangle {
|
|||
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
|
||||
// key bindings, work when label is currenctly focused (active handle in LayerSlider)
|
||||
// key bindings, work when label is currently focused (active handle in LayerSlider)
|
||||
Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
|
||||
Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
|
||||
|
||||
|
|
|
@ -190,11 +190,11 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
// Scrolls trough Z layers
|
||||
// Scrolls through Z layers
|
||||
LayerSlider
|
||||
{
|
||||
property var preferredHeight: UM.Theme.getSize("slider_layerview_size").height
|
||||
property double heightMargin: UM.Theme.getSize("default_margin").height * 3 // extra margin to accomodate layer number tooltips
|
||||
property double heightMargin: UM.Theme.getSize("default_margin").height * 3 // extra margin to accommodate layer number tooltips
|
||||
property double layerSliderSafeHeight: layerSliderSafeYMax - layerSliderSafeYMin
|
||||
|
||||
id: layerSlider
|
||||
|
|
|
@ -13,9 +13,11 @@ vertex =
|
|||
attribute highp vec4 a_vertex;
|
||||
attribute lowp vec4 a_color;
|
||||
attribute lowp vec4 a_material_color;
|
||||
attribute highp float a_vertex_index;
|
||||
|
||||
varying lowp vec4 v_color;
|
||||
varying float v_line_type;
|
||||
varying highp float v_vertex_index;
|
||||
|
||||
void main()
|
||||
{
|
||||
|
@ -28,6 +30,7 @@ vertex =
|
|||
}
|
||||
|
||||
v_line_type = a_line_type;
|
||||
v_vertex_index = a_vertex_index;
|
||||
}
|
||||
|
||||
fragment =
|
||||
|
@ -40,14 +43,21 @@ fragment =
|
|||
#endif // GL_ES
|
||||
varying lowp vec4 v_color;
|
||||
varying float v_line_type;
|
||||
varying highp float v_vertex_index;
|
||||
|
||||
uniform int u_show_travel_moves;
|
||||
uniform int u_show_helpers;
|
||||
uniform int u_show_skin;
|
||||
uniform int u_show_infill;
|
||||
|
||||
uniform highp vec2 u_drawRange;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (u_drawRange.x >= 0.0 && u_drawRange.y >= 0.0 && (v_vertex_index < u_drawRange.x || v_vertex_index > u_drawRange.y))
|
||||
{
|
||||
discard;
|
||||
}
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
|
||||
// discard movements
|
||||
discard;
|
||||
|
@ -77,77 +87,6 @@ fragment =
|
|||
gl_FragColor = v_color;
|
||||
}
|
||||
|
||||
vertex41core =
|
||||
#version 410
|
||||
uniform highp mat4 u_modelMatrix;
|
||||
uniform highp mat4 u_viewMatrix;
|
||||
uniform highp mat4 u_projectionMatrix;
|
||||
|
||||
uniform lowp float u_active_extruder;
|
||||
uniform lowp float u_shade_factor;
|
||||
uniform highp int u_layer_view_type;
|
||||
|
||||
in highp float a_extruder;
|
||||
in highp float a_line_type;
|
||||
in highp vec4 a_vertex;
|
||||
in lowp vec4 a_color;
|
||||
in lowp vec4 a_material_color;
|
||||
|
||||
out lowp vec4 v_color;
|
||||
out float v_line_type;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex;
|
||||
v_color = a_color;
|
||||
if ((a_line_type != 8) && (a_line_type != 9)) {
|
||||
v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
|
||||
}
|
||||
|
||||
v_line_type = a_line_type;
|
||||
}
|
||||
|
||||
fragment41core =
|
||||
#version 410
|
||||
in lowp vec4 v_color;
|
||||
in float v_line_type;
|
||||
out vec4 frag_color;
|
||||
|
||||
uniform int u_show_travel_moves;
|
||||
uniform int u_show_helpers;
|
||||
uniform int u_show_skin;
|
||||
uniform int u_show_infill;
|
||||
|
||||
void main()
|
||||
{
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
// helpers: 4, 5, 7, 10
|
||||
if ((u_show_helpers == 0) && (
|
||||
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
|
||||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
|
||||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
|
||||
)) {
|
||||
discard;
|
||||
}
|
||||
// skin: 1, 2, 3
|
||||
if ((u_show_skin == 0) && (
|
||||
(v_line_type >= 0.5) && (v_line_type <= 3.5)
|
||||
)) {
|
||||
discard;
|
||||
}
|
||||
// infill:
|
||||
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
|
||||
frag_color = v_color;
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_active_extruder = 0.0
|
||||
u_shade_factor = 0.60
|
||||
|
@ -159,10 +98,13 @@ u_show_helpers = 1
|
|||
u_show_skin = 1
|
||||
u_show_infill = 1
|
||||
|
||||
u_drawRange = [-1.0, -1.0]
|
||||
|
||||
[bindings]
|
||||
u_modelMatrix = model_matrix
|
||||
u_viewMatrix = view_matrix
|
||||
u_projectionMatrix = projection_matrix
|
||||
u_drawRange = draw_range
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
|
@ -170,3 +112,4 @@ a_color = color
|
|||
a_extruder = extruder
|
||||
a_line_type = line_type
|
||||
a_material_color = material_color
|
||||
a_vertex_index = vertex_index
|
||||
|
|
|
@ -27,6 +27,7 @@ vertex41core =
|
|||
in highp float a_extruder;
|
||||
in highp float a_prev_line_type;
|
||||
in highp float a_line_type;
|
||||
in highp float a_vertex_index;
|
||||
in highp float a_feedrate;
|
||||
in highp float a_thickness;
|
||||
|
||||
|
@ -37,8 +38,9 @@ vertex41core =
|
|||
out lowp vec2 v_line_dim;
|
||||
out highp int v_extruder;
|
||||
out highp mat4 v_extruder_opacity;
|
||||
out float v_prev_line_type;
|
||||
out float v_line_type;
|
||||
out highp float v_prev_line_type;
|
||||
out highp float v_line_type;
|
||||
out highp float v_index;
|
||||
|
||||
out lowp vec4 f_color;
|
||||
out highp vec3 f_vertex;
|
||||
|
@ -168,6 +170,7 @@ vertex41core =
|
|||
v_extruder = int(a_extruder);
|
||||
v_prev_line_type = a_prev_line_type;
|
||||
v_line_type = a_line_type;
|
||||
v_index = a_vertex_index;
|
||||
v_extruder_opacity = u_extruder_opacity;
|
||||
|
||||
// for testing without geometry shader
|
||||
|
@ -191,6 +194,8 @@ geometry41core =
|
|||
uniform int u_show_infill;
|
||||
uniform int u_show_starts;
|
||||
|
||||
uniform highp vec2 u_drawRange;
|
||||
|
||||
layout(lines) in;
|
||||
layout(triangle_strip, max_vertices = 40) out;
|
||||
|
||||
|
@ -202,6 +207,7 @@ geometry41core =
|
|||
in mat4 v_extruder_opacity[];
|
||||
in float v_prev_line_type[];
|
||||
in float v_line_type[];
|
||||
in float v_index[];
|
||||
|
||||
out vec4 f_color;
|
||||
out vec3 f_normal;
|
||||
|
@ -231,6 +237,10 @@ geometry41core =
|
|||
float size_x;
|
||||
float size_y;
|
||||
|
||||
if (u_drawRange[0] >= 0.0 && u_drawRange[1] >= 0.0 && (v_index[0] < u_drawRange[0] || v_index[0] >= u_drawRange[1]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
|
||||
return;
|
||||
}
|
||||
|
@ -427,12 +437,15 @@ u_max_feedrate = 1
|
|||
u_min_thickness = 0
|
||||
u_max_thickness = 1
|
||||
|
||||
u_drawRange = [-1.0, -1.0]
|
||||
|
||||
[bindings]
|
||||
u_modelMatrix = model_matrix
|
||||
u_viewMatrix = view_matrix
|
||||
u_projectionMatrix = projection_matrix
|
||||
u_normalMatrix = normal_matrix
|
||||
u_lightPosition = light_0_position
|
||||
u_drawRange = draw_range
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
|
@ -445,3 +458,4 @@ a_prev_line_type = prev_line_type
|
|||
a_line_type = line_type
|
||||
a_feedrate = feedrate
|
||||
a_thickness = thickness
|
||||
a_vertex_index = vertex_index
|
||||
|
|
|
@ -18,6 +18,7 @@ vertex41core =
|
|||
in highp vec2 a_line_dim; // line width and thickness
|
||||
in highp float a_extruder;
|
||||
in highp float a_line_type;
|
||||
in highp float a_vertex_index;
|
||||
|
||||
out lowp vec4 v_color;
|
||||
|
||||
|
@ -26,7 +27,8 @@ vertex41core =
|
|||
out lowp vec2 v_line_dim;
|
||||
out highp int v_extruder;
|
||||
out highp mat4 v_extruder_opacity;
|
||||
out float v_line_type;
|
||||
out highp float v_line_type;
|
||||
out highp float v_index;
|
||||
|
||||
out lowp vec4 f_color;
|
||||
out highp vec3 f_vertex;
|
||||
|
@ -47,6 +49,7 @@ vertex41core =
|
|||
v_line_dim = a_line_dim;
|
||||
v_extruder = int(a_extruder);
|
||||
v_line_type = a_line_type;
|
||||
v_index = a_vertex_index;
|
||||
v_extruder_opacity = u_extruder_opacity;
|
||||
|
||||
// for testing without geometry shader
|
||||
|
@ -67,6 +70,8 @@ geometry41core =
|
|||
uniform int u_show_skin;
|
||||
uniform int u_show_infill;
|
||||
|
||||
uniform highp vec2 u_drawRange;
|
||||
|
||||
layout(lines) in;
|
||||
layout(triangle_strip, max_vertices = 26) out;
|
||||
|
||||
|
@ -77,6 +82,7 @@ geometry41core =
|
|||
in int v_extruder[];
|
||||
in mat4 v_extruder_opacity[];
|
||||
in float v_line_type[];
|
||||
in float v_index[];
|
||||
|
||||
out vec4 f_color;
|
||||
out vec3 f_normal;
|
||||
|
@ -106,6 +112,10 @@ geometry41core =
|
|||
float size_x;
|
||||
float size_y;
|
||||
|
||||
if (u_drawRange[0] >= 0.0 && u_drawRange[1] >= 0.0 && (v_index[0] < u_drawRange[0] || v_index[0] >= u_drawRange[1]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
|
||||
return;
|
||||
}
|
||||
|
@ -268,12 +278,15 @@ u_show_helpers = 1
|
|||
u_show_skin = 1
|
||||
u_show_infill = 1
|
||||
|
||||
u_drawRange = [-1.0, -1.0]
|
||||
|
||||
[bindings]
|
||||
u_modelMatrix = model_matrix
|
||||
u_viewMatrix = view_matrix
|
||||
u_projectionMatrix = projection_matrix
|
||||
u_normalMatrix = normal_matrix
|
||||
u_lightPosition = light_0_position
|
||||
u_drawRange = draw_range
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
|
@ -284,3 +297,4 @@ a_line_dim = line_dim
|
|||
a_extruder = extruder
|
||||
a_material_color = material_color
|
||||
a_line_type = line_type
|
||||
a_vertex_index = vertex_index
|
||||
|
|
|
@ -13,9 +13,11 @@ vertex =
|
|||
attribute highp vec4 a_vertex;
|
||||
attribute lowp vec4 a_color;
|
||||
attribute lowp vec4 a_material_color;
|
||||
attribute highp float a_vertex_index;
|
||||
|
||||
varying lowp vec4 v_color;
|
||||
varying float v_line_type;
|
||||
varying highp float v_vertex_index;
|
||||
|
||||
void main()
|
||||
{
|
||||
|
@ -28,6 +30,7 @@ vertex =
|
|||
// }
|
||||
|
||||
v_line_type = a_line_type;
|
||||
v_vertex_index = a_vertex_index;
|
||||
}
|
||||
|
||||
fragment =
|
||||
|
@ -40,14 +43,21 @@ fragment =
|
|||
#endif // GL_ES
|
||||
varying lowp vec4 v_color;
|
||||
varying float v_line_type;
|
||||
varying highp float v_vertex_index;
|
||||
|
||||
uniform int u_show_travel_moves;
|
||||
uniform int u_show_helpers;
|
||||
uniform int u_show_skin;
|
||||
uniform int u_show_infill;
|
||||
|
||||
uniform highp vec2 u_drawRange;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (u_drawRange.x >= 0.0 && u_drawRange.y >= 0.0 && (v_vertex_index < u_drawRange.x || v_vertex_index > u_drawRange.y))
|
||||
{
|
||||
discard;
|
||||
}
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5))
|
||||
{ // actually, 8 and 9
|
||||
// discard movements
|
||||
|
@ -81,78 +91,6 @@ fragment =
|
|||
gl_FragColor = v_color;
|
||||
}
|
||||
|
||||
vertex41core =
|
||||
#version 410
|
||||
uniform highp mat4 u_modelMatrix;
|
||||
uniform highp mat4 u_viewMatrix;
|
||||
uniform highp mat4 u_projectionMatrix;
|
||||
|
||||
uniform lowp float u_active_extruder;
|
||||
uniform lowp float u_shade_factor;
|
||||
uniform highp int u_layer_view_type;
|
||||
|
||||
in highp float a_extruder;
|
||||
in highp float a_line_type;
|
||||
in highp vec4 a_vertex;
|
||||
in lowp vec4 a_color;
|
||||
in lowp vec4 a_material_color;
|
||||
|
||||
out lowp vec4 v_color;
|
||||
out float v_line_type;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex;
|
||||
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer
|
||||
// if ((a_line_type != 8) && (a_line_type != 9)) {
|
||||
// v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
|
||||
// }
|
||||
|
||||
v_line_type = a_line_type;
|
||||
}
|
||||
|
||||
fragment41core =
|
||||
#version 410
|
||||
in lowp vec4 v_color;
|
||||
in float v_line_type;
|
||||
out vec4 frag_color;
|
||||
|
||||
uniform int u_show_travel_moves;
|
||||
uniform int u_show_helpers;
|
||||
uniform int u_show_skin;
|
||||
uniform int u_show_infill;
|
||||
|
||||
void main()
|
||||
{
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
// helpers: 4, 5, 7, 10, 11
|
||||
if ((u_show_helpers == 0) && (
|
||||
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
|
||||
((v_line_type >= 6.5) && (v_line_type <= 7.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;
|
||||
}
|
||||
// skin: 1, 2, 3
|
||||
if ((u_show_skin == 0) && (
|
||||
(v_line_type >= 0.5) && (v_line_type <= 3.5)
|
||||
)) {
|
||||
discard;
|
||||
}
|
||||
// infill:
|
||||
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
|
||||
frag_color = v_color;
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_active_extruder = 0.0
|
||||
u_shade_factor = 0.60
|
||||
|
@ -164,10 +102,13 @@ u_show_helpers = 1
|
|||
u_show_skin = 1
|
||||
u_show_infill = 1
|
||||
|
||||
u_drawRange = [-1.0, -1.0]
|
||||
|
||||
[bindings]
|
||||
u_modelMatrix = model_matrix
|
||||
u_viewMatrix = view_matrix
|
||||
u_projectionMatrix = projection_matrix
|
||||
u_drawRange = draw_range
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
|
@ -175,3 +116,4 @@ a_color = color
|
|||
a_extruder = extruder
|
||||
a_line_type = line_type
|
||||
a_material_color = material_color
|
||||
a_vertex_index = vertex_index
|
||||
|
|
|
@ -130,6 +130,7 @@ class SliceInfo(QObject, Extension):
|
|||
data["cura_version"] = self._application.getVersion()
|
||||
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||
org_id = user_profile.get("organization_id", None) if user_profile else None
|
||||
data["is_logged_in"] = self._application.getCuraAPI().account.isLoggedIn
|
||||
data["organization_id"] = org_id if org_id else None
|
||||
data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else []
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<b>Intent Profile:</b> Default<br/>
|
||||
<b>Quality Profile:</b> Fast<br/>
|
||||
<b>Using Custom Settings:</b> No<br/>
|
||||
<b>Is Logged In:</b> Yes<br/>
|
||||
<b>Organization ID (if any):</b> ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=<br/>
|
||||
<b>Subscriptions (if any):</b>
|
||||
<ul>
|
||||
|
|
|
@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QApplication
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Operations.TranslateOperation import TranslateOperation
|
||||
from UM.Tool import Tool
|
||||
from UM.Event import Event, MouseEvent
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
@ -120,8 +121,8 @@ class SupportEraser(Tool):
|
|||
# First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent
|
||||
op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
|
||||
op.addOperation(SetParentOperation(node, parent))
|
||||
op.addOperation(TranslateOperation(node, position, set_position = True))
|
||||
op.push()
|
||||
node.setPosition(position, CuraSceneNode.TransformSpace.World)
|
||||
|
||||
CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import UM 1.1 as UM
|
|||
|
||||
Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("secondary")
|
||||
color: UM.Theme.getColor("toolbox_premium_packages_background")
|
||||
height: childrenRect.height
|
||||
width: parent.width
|
||||
Column
|
||||
|
|
|
@ -71,6 +71,7 @@ ScrollView
|
|||
padding: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@info", "No plugin has been installed.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("lining")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +124,7 @@ ScrollView
|
|||
visible: toolbox.materialsInstalledModel.count < 1
|
||||
padding: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@info", "No material has been installed.")
|
||||
color: UM.Theme.getColor("lining")
|
||||
font: UM.Theme.getFont("medium")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class CloudPackageChecker(QObject):
|
|||
self._i18n_catalog = i18nCatalog("cura")
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
self._last_notified_packages = set() # type: Set[str]
|
||||
"""Packages for which a notification has been shown. No need to bother the user twice fo equal content"""
|
||||
"""Packages for which a notification has been shown. No need to bother the user twice for equal content"""
|
||||
|
||||
# This is a plugin, so most of the components required are not ready when
|
||||
# this is initialized. Therefore, we wait until the application is ready.
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import Dict, List, Any
|
|||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM import i18n_catalog
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
@ -15,6 +15,8 @@ from cura.CuraApplication import CuraApplication
|
|||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class DownloadPresenter:
|
||||
"""Downloads a set of packages from the Ultimaker Cloud Marketplace
|
||||
|
@ -90,7 +92,7 @@ class DownloadPresenter:
|
|||
lifetime = 0,
|
||||
use_inactivity_timer = False,
|
||||
progress = 0.0,
|
||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account"))
|
||||
|
||||
def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
|
||||
self._progress[package_id]["received"] = self._progress[package_id]["total"]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, Optional, List, Any
|
||||
|
@ -95,7 +98,11 @@ class LicensePresenter(QObject):
|
|||
|
||||
for package_id, item in packages.items():
|
||||
item["package_id"] = package_id
|
||||
item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
|
||||
try:
|
||||
item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
|
||||
except EnvironmentError as e:
|
||||
Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
|
||||
continue # Skip this package.
|
||||
if item["licence_content"] is None:
|
||||
# Implicitly accept when there is no license
|
||||
item["accepted"] = True
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
|
@ -542,7 +542,7 @@ class Toolbox(QObject, Extension):
|
|||
# Make API Calls
|
||||
# --------------------------------------------------------------------------
|
||||
def _makeRequestByType(self, request_type: str) -> None:
|
||||
Logger.log("d", "Requesting [%s] metadata from server.", request_type)
|
||||
Logger.debug(f"Requesting {request_type} metadata from server.")
|
||||
url = self._request_urls[request_type]
|
||||
|
||||
callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r)
|
||||
|
@ -554,7 +554,7 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
@pyqtSlot(str)
|
||||
def startDownload(self, url: str) -> None:
|
||||
Logger.log("i", "Attempting to download & install package from %s.", url)
|
||||
Logger.info(f"Attempting to download & install package from {url}.")
|
||||
|
||||
callback = lambda r: self._onDownloadFinished(r)
|
||||
error_callback = lambda r, e: self._onDownloadFailed(r, e)
|
||||
|
@ -572,7 +572,7 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
@pyqtSlot()
|
||||
def cancelDownload(self) -> None:
|
||||
Logger.log("i", "User cancelled the download of a package. request %s", self._download_request_data)
|
||||
Logger.info(f"User cancelled the download of a package. request {self._download_request_data}")
|
||||
if self._download_request_data is not None:
|
||||
self._application.getHttpRequestManager().abortRequest(self._download_request_data)
|
||||
self._download_request_data = None
|
||||
|
@ -585,7 +585,7 @@ class Toolbox(QObject, Extension):
|
|||
# Handlers for Network Events
|
||||
# --------------------------------------------------------------------------
|
||||
def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
||||
Logger.log("e", "Request [%s] failed due to error [%s]: %s", request_type, error, reply.errorString())
|
||||
Logger.error(f"Request {request_type} failed due to error {error}: {reply.errorString()}")
|
||||
self.setViewPage("errored")
|
||||
|
||||
def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None:
|
||||
|
@ -682,9 +682,13 @@ class Toolbox(QObject, Extension):
|
|||
if not package_info:
|
||||
Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
|
||||
return
|
||||
|
||||
license_content = self._package_manager.getPackageLicense(file_path)
|
||||
package_id = package_info["package_id"]
|
||||
|
||||
try:
|
||||
license_content = self._package_manager.getPackageLicense(file_path)
|
||||
except EnvironmentError as e:
|
||||
Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
|
||||
return
|
||||
if license_content is not None:
|
||||
# get the icon url for package_id, make sure the result is a string, never None
|
||||
icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or ""
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading Ultimaker Format Packages.",
|
||||
"supported_sdk_versions": ["7.6.0"],
|
||||
"supported_sdk_versions": ["7.8.0"],
|
||||
"i18n-catalog": "cura"
|
||||
}
|
|
@ -51,7 +51,7 @@ Item
|
|||
anchors.centerIn: parent
|
||||
color: UM.Theme.getColor("monitor_icon_primary")
|
||||
height: UM.Theme.getSize("medium_button_icon").width
|
||||
source: "../svg/icons/Buildplate.svg"
|
||||
source: UM.Theme.getIcon("Buildplate")
|
||||
width: height
|
||||
visible: buildplate
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ Item
|
|||
{
|
||||
id: base
|
||||
|
||||
// The print job which all other information is dervied from
|
||||
// The print job which all other information is derived from
|
||||
property var printJob: null
|
||||
|
||||
width: childrenRect.width
|
||||
|
@ -97,7 +97,7 @@ Item
|
|||
case "queued":
|
||||
return catalog.i18nc("@label:status", "Action required");
|
||||
default:
|
||||
return catalog.i18nc("@label:status", "Finishes %1 at %2".arg(OutputDevice.getDateCompleted(printJob.timeRemaining)).arg(OutputDevice.getTimeCompleted(printJob.timeRemaining)));
|
||||
return catalog.i18nc("@label:status", "Finishes %1 at %2").arg(OutputDevice.getDateCompleted(printJob.timeRemaining)).arg(OutputDevice.getTimeCompleted(printJob.timeRemaining));
|
||||
}
|
||||
}
|
||||
width: contentWidth
|
||||
|
|
|
@ -172,7 +172,7 @@ Item
|
|||
MouseArea
|
||||
{
|
||||
anchors.fill: managePrinterLink
|
||||
onClicked: OutputDevice.openPrintJobControlPanel()
|
||||
onClicked: OutputDevice.openPrinterControlPanel()
|
||||
onEntered:
|
||||
{
|
||||
manageQueueText.font.underline = true
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.2
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Controls 2.15 as NewControls
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
UM.Dialog {
|
||||
|
@ -82,8 +84,9 @@ UM.Dialog {
|
|||
renderType: Text.NativeRendering;
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
NewControls.ComboBox {
|
||||
id: printerComboBox;
|
||||
currentIndex: 0;
|
||||
Behavior on height { NumberAnimation { duration: 100 } }
|
||||
height: 40 * screenScaleFactor;
|
||||
model: ListModel {
|
||||
|
|
353
plugins/UM3NetworkPrinting/resources/svg/CloudPlatform.svg
Normal file
353
plugins/UM3NetworkPrinting/resources/svg/CloudPlatform.svg
Normal file
|
@ -0,0 +1,353 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 274.75 126.24"
|
||||
version="1.1"
|
||||
id="svg425"
|
||||
sodipodi:docname="CloudPlatform.svg"
|
||||
width="274.75"
|
||||
height="126.24"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata429">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1200"
|
||||
id="namedview427"
|
||||
showgrid="false"
|
||||
fit-margin-left="1"
|
||||
fit-margin-bottom="1"
|
||||
fit-margin-top="1"
|
||||
fit-margin-right="1"
|
||||
inkscape:zoom="2.593819"
|
||||
inkscape:cx="115.77157"
|
||||
inkscape:cy="14.444977"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg425" />
|
||||
<defs
|
||||
id="defs332">
|
||||
<style
|
||||
id="style330">.cls-1{fill:#f3f8fe;}.cls-2{fill:none;stroke:#061884;stroke-miterlimit:10;}.cls-3{fill:#061884;}.cls-4,.cls-6{fill:#fff;}.cls-4{fill-rule:evenodd;}.cls-5{fill:#dde9fd;}.cls-7{fill:#c5dbfb;}</style>
|
||||
</defs>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
transform="translate(-28.84,-11.189998)">
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M 71.93,79.82 H 49.62 a 4.12,4.12 0 0 0 -4.13,4.11 v 47.55 a 4.13,4.13 0 0 0 4.13,4.12 h 22.31 a 4.13,4.13 0 0 0 4.13,-4.12 V 83.93 a 4.12,4.12 0 0 0 -4.13,-4.11 z m 2.18,51 a 2.82,2.82 0 0 1 -2.82,2.82 h -21 a 2.83,2.83 0 0 1 -2.82,-2.82 V 84.58 a 2.84,2.84 0 0 1 2.82,-2.83 h 5.92 a 1.45,1.45 0 0 0 1.45,1.46 h 6.3 a 1.46,1.46 0 0 0 1.46,-1.46 h 5.91 a 2.83,2.83 0 0 1 2.82,2.83 z"
|
||||
id="path334"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 71.93,79.82 H 49.62 a 4.12,4.12 0 0 0 -4.13,4.11 v 47.55 a 4.13,4.13 0 0 0 4.13,4.12 h 22.31 a 4.13,4.13 0 0 0 4.13,-4.12 V 83.93 a 4.12,4.12 0 0 0 -4.13,-4.11 z"
|
||||
id="path336"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#061884;stroke-miterlimit:10" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 63.2,81 h -4.85 a 0.5,0.5 0 1 0 0,1 h 4.85 a 0.5,0.5 0 0 0 0,-1 z"
|
||||
id="path338"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="m 74.11,84.58 v 46.26 a 2.82,2.82 0 0 1 -2.82,2.82 h -21 a 2.83,2.83 0 0 1 -2.82,-2.82 V 84.58 a 2.84,2.84 0 0 1 2.82,-2.83 h 5.92 a 1.45,1.45 0 0 0 1.45,1.46 h 6.3 a 1.46,1.46 0 0 0 1.46,-1.46 h 5.91 a 2.83,2.83 0 0 1 2.78,2.83 z"
|
||||
id="path340"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-rule:evenodd" />
|
||||
<rect
|
||||
class="cls-5"
|
||||
x="50.32"
|
||||
y="125.88"
|
||||
width="19.91"
|
||||
height="4.7399998"
|
||||
id="rect342"
|
||||
style="fill:#dde9fd" />
|
||||
<rect
|
||||
class="cls-5"
|
||||
x="50.32"
|
||||
y="85.959999"
|
||||
width="19.91"
|
||||
height="1.9"
|
||||
rx="0.94999999"
|
||||
id="rect344"
|
||||
style="fill:#dde9fd" />
|
||||
<rect
|
||||
class="cls-5"
|
||||
x="50.32"
|
||||
y="114.4"
|
||||
width="10.43"
|
||||
height="1.9"
|
||||
rx="0.94999999"
|
||||
id="rect346"
|
||||
style="fill:#dde9fd" />
|
||||
<rect
|
||||
class="cls-5"
|
||||
x="50.32"
|
||||
y="117.25"
|
||||
width="10.43"
|
||||
height="1.9"
|
||||
rx="0.94999999"
|
||||
id="rect348"
|
||||
style="fill:#dde9fd" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 291.5,135.38 a 5.12,5.12 0 0 0 5.11,-5.11 v -0.38 a 0.38,0.38 0 0 0 -0.37,-0.37 h -103.9 a 0.37,0.37 0 0 0 -0.36,0.37 v 0.38 a 5.11,5.11 0 0 0 5.1,5.11 z"
|
||||
id="path350"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 296.24,129.89 h -103.9 v 0.38 0 a 4.74,4.74 0 0 0 4.74,4.74 h 94.42 a 4.74,4.74 0 0 0 4.74,-4.74 v -0.38 m 0,-0.73 a 0.73,0.73 0 0 1 0.73,0.73 v 0.38 a 5.47,5.47 0 0 1 -5.47,5.47 h -94.42 a 5.47,5.47 0 0 1 -5.47,-5.47 v -0.38 a 0.73,0.73 0 0 1 0.73,-0.73 z"
|
||||
id="path352"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 235.51,129.16 a 2.93,2.93 0 0 0 2.93,2.93 h 11.71 a 2.93,2.93 0 0 0 2.92,-2.93 z"
|
||||
id="path354"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M 287.83,129.52 V 71.36 a 2.56,2.56 0 0 0 -2.56,-2.56 h -81.95 a 2.56,2.56 0 0 0 -2.56,2.56 v 58.16 z"
|
||||
id="path356"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 287.83,129.52 V 71.36 a 2.56,2.56 0 0 0 -2.56,-2.56 h -81.95 a 2.56,2.56 0 0 0 -2.56,2.56 v 58.16"
|
||||
id="path358"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#061884;stroke-miterlimit:10" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 284.17,128.79 v -56 a 0.36,0.36 0 0 0 -0.37,-0.36 h -79 a 0.36,0.36 0 0 0 -0.36,0.36 v 56 z"
|
||||
id="path360"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 283.8,72.82 h -79 v 55.61 h 79 V 72.82 m 0.74,0 v 56.34 H 204.05 V 72.82 a 0.73,0.73 0 0 1 0.73,-0.73 h 79 a 0.74,0.74 0 0 1 0.76,0.73 z"
|
||||
id="path362"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 204.11,129.57 V 73.86 a 1.64,1.64 0 0 1 1.64,-1.64 H 283 a 1.64,1.64 0 0 1 1.64,1.64 v 55.71"
|
||||
id="path364"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#061884;stroke-miterlimit:10" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="m 291.5,135.38 a 5.12,5.12 0 0 0 5.11,-5.11 v -0.38 a 0.38,0.38 0 0 0 -0.37,-0.37 h -103.9 a 0.37,0.37 0 0 0 -0.36,0.37 v 0.38 a 5.11,5.11 0 0 0 5.1,5.11 z"
|
||||
id="path366"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#061884;stroke-miterlimit:10" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 131.73,12.19 c -3.87,0 -8.7,5.75 -14.75,17.5 -4.63,9 -8.26,18.32 -8.3,18.41 a 0.86443623,0.86443623 0 0 0 1.61,0.63 c 5.46,-14.09 16.24,-36 21.88,-34.77 5.64,1.23 5.35,21.35 3.87,33.76 a 0.86142324,0.86142324 0 1 0 1.71,0.21 c 0.41,-3.45 3.75,-33.71 -5.22,-35.65 a 3.57,3.57 0 0 0 -0.8,-0.09 z"
|
||||
id="path368"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 143.87,17.34 a 3.56,3.56 0 0 0 -0.8,0.08 c -9,1.94 -5.63,32.2 -5.22,35.65 a 0.86142324,0.86142324 0 1 0 1.71,-0.21 c -1.48,-12.41 -1.74,-32.55 3.87,-33.76 5.61,-1.21 16.42,20.68 21.88,34.77 a 0.86443623,0.86443623 0 1 0 1.61,-0.63 c 0,-0.09 -3.67,-9.42 -8.3,-18.41 -6.05,-11.75 -10.88,-17.49 -14.75,-17.49 z"
|
||||
id="path370"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 178,135.58 a 2.25,2.25 0 0 0 2.24,-2.24 v -84 A 2.3,2.3 0 0 0 178,47 H 94.81 a 2.29,2.29 0 0 0 -2.24,2.29 v 84 a 2.24,2.24 0 0 0 2.24,2.24 h 6 l 0.69,-0.38 c 3.56,-2 3.94,-2.2 8.66,-2.2 h 51.59 c 4.72,0 5.09,0.21 8.66,2.2 l 0.69,0.38 z"
|
||||
id="path372"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M 178,47.45 H 94.81 A 1.85,1.85 0 0 0 93,49.31 v 84 0 a 1.81,1.81 0 0 0 1.81,1.81 h 5.93 c 4.15,-2.3 4.37,-2.58 9.46,-2.58 h 51.59 c 5.08,0 5.31,0.28 9.46,2.58 H 178 a 1.81,1.81 0 0 0 1.81,-1.81 v -84 A 1.85,1.85 0 0 0 178,47.45 m 2.67,1.86 v 84 A 2.68,2.68 0 0 1 178,136 h -7 l -0.19,-0.11 -0.59,-0.33 c -3.55,-2 -3.84,-2.14 -8.45,-2.14 H 110.2 c -4.61,0 -4.9,0.16 -8.45,2.14 l -0.59,0.33 -0.2,0.11 h -6.15 a 2.66,2.66 0 0 1 -2.67,-2.67 v -84 a 2.69,2.69 0 0 1 2.67,-2.72 H 178 a 2.7,2.7 0 0 1 2.71,2.7 z"
|
||||
id="path374"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<rect
|
||||
class="cls-3"
|
||||
x="111.92"
|
||||
y="126.55"
|
||||
width="3.4400001"
|
||||
height="0.86000001"
|
||||
id="rect376"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="102.46"
|
||||
cy="50.029999"
|
||||
r="0.86000001"
|
||||
id="circle378"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="124.81"
|
||||
cy="50.029999"
|
||||
r="0.86000001"
|
||||
id="circle380"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="147.17"
|
||||
cy="50.029999"
|
||||
r="0.86000001"
|
||||
id="circle382"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="169.53"
|
||||
cy="50.029999"
|
||||
r="0.86000001"
|
||||
id="circle384"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="102.46"
|
||||
cy="126.55"
|
||||
r="0.86000001"
|
||||
id="circle386"
|
||||
style="fill:#061884" />
|
||||
<circle
|
||||
class="cls-3"
|
||||
cx="169.53"
|
||||
cy="126.55"
|
||||
r="0.86000001"
|
||||
id="circle388"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 168.52,121.82 a 6.6,6.6 0 0 0 6.6,-6.59 V 60.42 A 3.1,3.1 0 0 0 172,57.34 h -71.19 a 3.08,3.08 0 0 0 -3.08,3.08 v 54.81 a 6.59,6.59 0 0 0 6.6,6.59 z"
|
||||
id="path390"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 172,57.77 h -71.19 a 2.65,2.65 0 0 0 -2.65,2.65 v 54.81 a 6.16,6.16 0 0 0 6.17,6.16 h 64.19 a 6.18,6.18 0 0 0 6.17,-6.16 V 60.42 A 2.66,2.66 0 0 0 172,57.77 m 3.52,2.65 v 54.81 0 a 7,7 0 0 1 -7,7 h -64.19 a 7,7 0 0 1 -7,-7 V 60.42 a 3.51,3.51 0 0 1 3.51,-3.51 H 172 a 3.52,3.52 0 0 1 3.55,3.51 z"
|
||||
id="path392"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f3f8fe" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 172,56.91 h -71.19 a 3.51,3.51 0 0 0 -3.51,3.51 v 54.81 a 7,7 0 0 0 7,7 h 64.19 a 7,7 0 0 0 7,-7 V 60.42 A 3.52,3.52 0 0 0 172,56.91 m 4.38,3.51 v 54.81 a 7.9,7.9 0 0 1 -7.89,7.88 h -64.16 a 7.88,7.88 0 0 1 -7.89,-7.88 V 60.42 a 4.37,4.37 0 0 1 4.37,-4.37 H 172 a 4.38,4.38 0 0 1 4.41,4.37 z"
|
||||
id="path394"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-7"
|
||||
d="m 146.31,118 h -20.64 v 10.32 h 20.64 V 118 m 0,-0.85 v 0 a 0.83,0.83 0 0 1 0.84,0.83 v 10.32 0 a 0.83,0.83 0 0 1 -0.84,0.83 h -20.66 a 0.84,0.84 0 0 1 -0.84,-0.83 v -10.37 a 0.84,0.84 0 0 1 0.84,-0.83 z"
|
||||
id="path396"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#c5dbfb" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 142.1,65.93 a 1.35,1.35 0 0 0 1.29,-1 L 145,58.77 v -2.29 h -18 v 2.29 l 1.6,6.23 a 1.34,1.34 0 0 0 1.28,1 z"
|
||||
id="path398"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 144.59,56.91 h -17.2 v 1.8 l 1.61,6.14 a 0.9,0.9 0 0 0 0.87,0.65 h 12.23 a 0.89,0.89 0 0 0 0.87,-0.65 l 1.62,-6.14 v -1.8 m 0.86,-0.86 v 2.78 0.1 l -1.62,6.16 a 1.77,1.77 0 0 1 -1.7,1.27 h -12.25 a 1.78,1.78 0 0 1 -1.7,-1.29 l -1.62,-6.14 v -0.1 -2.78 z"
|
||||
id="path400"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 140.19,67.65 h 0.15 a 1.34,1.34 0 0 0 1.17,-1.48 l -0.84,-7.11 h -9.36 l -0.84,7.11 v 0.15 a 1.32,1.32 0 0 0 1.33,1.33 z"
|
||||
id="path402"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 140.29,59.49 h -8.6 l -0.79,6.73 v 0.1 a 0.9,0.9 0 0 0 0.9,0.9 h 8.49 a 0.9,0.9 0 0 0 0.79,-1 l -0.79,-6.73 m 0.77,-0.86 0.09,0.76 0.79,6.74 a 1.21,1.21 0 0 1 0,0.19 1.76,1.76 0 0 1 -1.76,1.76 h -8.58 a 1.76,1.76 0 0 1 -1.55,-1.95 l 0.79,-6.73 0.09,-0.76 z"
|
||||
id="path404"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 147,59.06 a 2.59,2.59 0 0 0 0,-5.16 h -22 a 2.59,2.59 0 0 0 0,5.16 z"
|
||||
id="path406"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 147,54.33 h -22 a 2,2 0 0 0 -1.92,2.13 2.07,2.07 0 0 0 1.92,2.17 h 22 a 2.07,2.07 0 0 0 1.92,-2.17 2,2 0 0 0 -1.92,-2.13 m 2.78,2.13 a 2.92,2.92 0 0 1 -2.78,3 h -22 a 3,3 0 0 1 0,-6 h 22 a 2.9,2.9 0 0 1 2.75,3 z"
|
||||
id="path408"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<rect
|
||||
class="cls-3"
|
||||
x="135.56"
|
||||
y="54.330002"
|
||||
width="0.86000001"
|
||||
height="4.3699999"
|
||||
id="rect410"
|
||||
style="fill:#061884" />
|
||||
<line
|
||||
class="cls-2"
|
||||
x1="29.84"
|
||||
y1="135.92999"
|
||||
x2="302.59"
|
||||
y2="135.92999"
|
||||
id="line412"
|
||||
style="fill:none;stroke:#061884;stroke-miterlimit:10" />
|
||||
<polygon
|
||||
class="cls-5"
|
||||
points="112.35,101.51 124.06,121.81 147.5,121.81 159.22,101.51 147.5,81.22 124.06,81.22 "
|
||||
id="polygon414"
|
||||
style="fill:#dde9fd" />
|
||||
<polygon
|
||||
class="cls-5"
|
||||
points="224.57,103.51 234.68,121.01 254.89,121.01 264.99,103.51 254.89,86.01 234.68,86.01 "
|
||||
id="polygon416"
|
||||
style="fill:#dde9fd" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="m 125.65,117.53 a 0.41,0.41 0 0 0 -0.41,0.4 v 10.37 0 a 0.41,0.41 0 0 0 0.41,0.4 h 20.68 a 0.4,0.4 0 0 0 0.41,-0.4 v -10.37 0 a 0.4,0.4 0 0 0 -0.41,-0.4 z"
|
||||
id="path418"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 146.33,117.1 h -20.68 a 0.84,0.84 0 0 0 -0.84,0.83 v 10.37 a 0.84,0.84 0 0 0 0.84,0.83 h 20.68 a 0.83,0.83 0 0 0 0.84,-0.83 v -10.37 0 a 0.83,0.83 0 0 0 -0.84,-0.83 m 1.7,0.83 v 10.37 a 1.7,1.7 0 0 1 -1.7,1.69 h -20.68 a 1.7,1.7 0 0 1 -1.7,-1.69 v -10.37 a 1.7,1.7 0 0 1 1.7,-1.69 h 20.68 a 1.7,1.7 0 0 1 1.67,1.69 z"
|
||||
id="path420"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#061884" />
|
||||
<polygon
|
||||
class="cls-5"
|
||||
points="50.22,101.67 55.22,110.33 65.22,110.33 70.22,101.67 65.22,93.01 55.22,93.01 "
|
||||
id="polygon422"
|
||||
style="fill:#dde9fd" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 14 KiB |
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="291px" height="209px" viewBox="0 0 291 209" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 2</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="CC--Cloud-connection-succes" transform="translate(-570.000000, -132.000000)">
|
||||
<g id="Group-2" transform="translate(570.000000, 132.000000)">
|
||||
<g id="Icon/-group-printer/-connected" fill="#08073F" fill-rule="nonzero">
|
||||
<g id="printer-group">
|
||||
<g id="Group-Copy" transform="translate(0.000000, 0.218255)">
|
||||
<g id="UM3">
|
||||
<path d="M151.645765,136.481718 C149.925833,142.069331 149,148.004918 149,154.156745 C149,157.125641 149.215633,160.044173 149.632091,162.897534 L94.5646946,162.897534 C92.6599327,163.073639 90.7551708,163.778061 89.3698894,165.010799 L88.3309283,166.067432 C87.2919673,167.828487 85.2140452,168.180697 83.1361231,168.180697 L73.7854738,168.180697 C73.2659933,168.180697 72.7465127,167.652381 72.7465127,167.124065 L72.7465127,152.918227 L19.3939394,152.918227 C17.7008177,153.074764 16.007696,153.700917 14.7763348,154.796684 L13.8528138,155.735914 C12.9292929,157.301296 11.0822511,157.614372 9.23520921,157.614372 L0.923520928,157.614372 C0.461760462,157.614372 1.42108547e-13,157.144757 1.42108547e-13,156.675142 L1.42108547e-13,9.0596475 C1.42108547e-13,8.59003299 0.461760462,8.12041848 0.923520928,8.12041848 L72.7465127,8.12041848 L72.7465127,1.05663265 C72.7465127,0.528316328 73.2659933,-2.84217094e-14 73.7854738,-2.84217094e-14 L216.815777,-2.84217094e-14 C217.335257,-2.84217094e-14 217.854738,0.528316328 217.854738,1.05663265 L217.854738,8.12041848 L289.677732,8.12041848 C290.139493,8.12041848 290.601253,8.59003299 290.601253,9.0596475 L290.601253,156.675142 C290.601253,157.144757 290.139493,157.614372 289.677732,157.614372 L281.366043,157.614372 C279.672922,157.457833 277.9798,156.831681 276.748439,155.735914 L275.824918,154.796684 C274.901397,153.231303 273.054355,152.918227 271.207313,152.918227 L268.987471,152.918227 C268.818229,144.560013 266.939851,136.621364 263.687558,129.437501 L268.590671,129.437501 C272.900435,129.437501 276.440598,125.837123 276.440598,121.454054 L276.594519,121.454054 L276.594519,24.5569264 C276.594519,22.8350065 275.209237,21.5827012 273.670035,21.5827012 L217.854738,21.5827012 L217.854738,94.8055789 C214.965144,94.3781586 212.008429,94.1567452 209,94.1567452 C206.665515,94.1567452 204.362169,94.2900687 202.097162,94.5495169 L202.097162,21.5827012 L202.097162,18.4910714 C202.097162,16.5539116 200.538721,15.145068 198.807119,15.145068 L161.616164,15.145068 L128.985089,15.145068 L91.6209717,15.145068 C89.7162098,15.145068 88.3309283,16.730017 88.3309283,18.4910714 L88.3309283,21.5827012 L88.3309283,127.50034 C88.3309283,128.164624 88.4032089,128.812928 88.5401496,129.437501 C89.4197136,133.449106 92.9667866,136.481718 97.1620971,136.481718 L128.985089,136.481718 L151.645765,136.481718 Z M72.7465127,129.437501 L72.7465127,21.5827012 L16.7772968,21.5827012 C15.0841751,21.5827012 13.8528138,22.9915447 13.8528138,24.5569264 L13.8528138,121.454054 C13.8528138,125.837123 17.3929774,129.437501 21.7027417,129.437501 L72.7465127,129.437501 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-cloud" transform="translate(156.000000, 103.000000)">
|
||||
<g id="Group" transform="translate(0.394933, 0.724044)">
|
||||
<circle id="Oval-Copy" fill="#3282FF" cx="52.6050671" cy="52.601776" r="52.1311475"></circle>
|
||||
<path d="M74.8002981,45.4035747 C74.1684054,39.8133538 69.4292101,35.538479 63.7421759,35.538479 C62.1624441,35.538479 60.8986587,35.8673156 59.6348733,36.5249886 C56.7913562,31.9212773 51.7362146,28.9617486 46.3651267,28.9617486 C37.5186289,28.9617486 30.5678092,36.1961521 30.5678092,45.4035747 C30.5678092,45.4035747 30.5678092,45.4035747 30.5678092,45.7324112 C25.1967213,46.3900842 21.0894188,51.322632 21.0894188,56.9128529 C21.0894188,63.1607468 26.1445604,68.4221311 32.147541,68.4221311 C36.8867362,68.4221311 67.533532,68.4221311 73.2205663,68.4221311 C79.2235469,68.4221311 84.2786885,63.1607468 84.2786885,56.9128529 C84.2786885,50.9937956 80.171386,46.3900842 74.8002981,45.4035747 Z" id="Path" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.6 KiB |
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="186px" height="57px" viewBox="0 0 186 57" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Cloud_connection-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Cloud_connection-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M38.6261428,52.7109865 L7.48755878,52.7109865 C6.85100215,52.7676651 6.21444551,52.994379 5.75149524,53.3911284 L5.40428251,53.7311992 C5.05706981,54.2979841 4.36264439,54.4113411 3.66821896,54.4113411 L0.543304569,54.4113411 C0.369698215,54.4113411 0.196091859,54.2413055 0.196091859,54.0712703 L0.196091859,0.623463283 C0.196091859,0.453427843 0.369698215,0.283392401 0.543304569,0.283392401 L48.3429212,0.283392401 C48.5165273,0.283392401 48.6901338,0.453427843 48.6901338,0.623463283 L48.6901338,26.0155943 C48.4613867,26.0052354 48.2313048,26 48,26 C46.4042274,26 44.8666558,26.2491876 43.4240742,26.7107738 L43.4240742,6.23463283 C43.4240742,5.61116956 42.9032553,5.15774169 42.3245675,5.15774169 L6.50378945,5.15774169 C5.86723281,5.15774169 5.40428251,5.66784803 5.40428251,6.23463283 L5.40428251,41.3186122 C5.40428251,42.9056095 6.73526457,44.2092147 8.35559054,44.2092147 L33.3440862,44.2092147 C34.087979,47.6221969 35.9937272,50.6011835 38.6261428,52.7109865 Z" id="Combined-Shape" fill="#08073F" fill-rule="nonzero"></path>
|
||||
<path d="M158.961954,52.7109865 L131.487559,52.7109865 C130.851002,52.7676651 130.214446,52.994379 129.751495,53.3911284 L129.404283,53.7311992 C129.05707,54.2979841 128.362644,54.4113411 127.668219,54.4113411 L124.543305,54.4113411 C124.369698,54.4113411 124.196092,54.2413055 124.196092,54.0712703 L124.196092,0.623463283 C124.196092,0.453427843 124.369698,0.283392401 124.543305,0.283392401 L172.342921,0.283392401 C172.516527,0.283392401 172.690134,0.453427843 172.690134,0.623463283 L172.690134,27.0854877 C172.13468,27.0289729 171.570805,27 171,27 C169.770934,27 168.574002,27.1343278 167.424074,27.3886981 L167.424074,6.23463283 C167.424074,5.61116956 166.903255,5.15774169 166.324567,5.15774169 L130.503789,5.15774169 C129.867233,5.15774169 129.404283,5.66784803 129.404283,6.23463283 L129.404283,41.3186122 C129.404283,42.9056095 130.735265,44.2092147 132.355591,44.2092147 L155.096113,44.2092147 C155.462794,47.4493334 156.859805,50.3873861 158.961954,52.7109865 Z" id="Combined-Shape" fill="#08073F" fill-rule="nonzero"></path>
|
||||
<path d="M171,56 C163.26057,56 157,49.9481159 157,42.5 C157,35.0518841 163.26057,29 171,29 C178.73943,29 185,35.0518841 185,42.5 C185,49.9481159 178.73943,56 171,56 Z M177.416667,40.7546296 C177.233333,39.1569444 175.858333,37.9351852 174.208333,37.9351852 C173.75,37.9351852 173.383333,38.0291667 173.016667,38.2171296 C172.191667,36.9013889 170.725,36.0555556 169.166667,36.0555556 C166.6,36.0555556 164.583333,38.1231482 164.583333,40.7546296 C164.583333,40.7546296 164.583333,40.7546296 164.583333,40.8486111 C163.025,41.0365741 161.833333,42.4462963 161.833333,44.0439815 C161.833333,45.8296296 163.3,47.3333333 165.041667,47.3333333 C166.416667,47.3333333 175.308333,47.3333333 176.958333,47.3333333 C178.7,47.3333333 180.166667,45.8296296 180.166667,44.0439815 C180.166667,42.3523148 178.975,41.0365741 177.416667,40.7546296 Z" id="Combined-Shape" fill="#3282FF" fill-rule="nonzero"></path>
|
||||
<path d="M48,54 C40.8202983,54 35,48.1797017 35,41 C35,33.8202983 40.8202983,28 48,28 C55.1797017,28 61,33.8202983 61,41 C61,48.1797017 55.1797017,54 48,54 Z M46.862511,41.4631428 L43.8629783,38.6111022 L41.1067187,41.5099007 L47.0308248,47.1427085 L55.8527121,37.698579 L52.9296286,34.9680877 L46.862511,41.4631428 Z" id="Combined-Shape" fill="#3282FF" fill-rule="nonzero"></path>
|
||||
<path d="M54.5,25 C53.6715729,25 53,24.3284271 53,23.5 C53,22.6715729 53.6715729,22 54.5,22 C55.3284271,22 56,22.6715729 56,23.5 C56,24.3284271 55.3284271,25 54.5,25 Z M78.5,25 C77.6715729,25 77,24.3284271 77,23.5 C77,22.6715729 77.6715729,22 78.5,22 C79.3284271,22 80,22.6715729 80,23.5 C80,24.3284271 79.3284271,25 78.5,25 Z M102.5,25 C101.671573,25 101,24.3284271 101,23.5 C101,22.6715729 101.671573,22 102.5,22 C103.328427,22 104,22.6715729 104,23.5 C104,24.3284271 103.328427,25 102.5,25 Z M62.5,25 C61.6715729,25 61,24.3284271 61,23.5 C61,22.6715729 61.6715729,22 62.5,22 C63.3284271,22 64,22.6715729 64,23.5 C64,24.3284271 63.3284271,25 62.5,25 Z M86.5,25 C85.6715729,25 85,24.3284271 85,23.5 C85,22.6715729 85.6715729,22 86.5,22 C87.3284271,22 88,22.6715729 88,23.5 C88,24.3284271 87.3284271,25 86.5,25 Z M110.5,25 C109.671573,25 109,24.3284271 109,23.5 C109,22.6715729 109.671573,22 110.5,22 C111.328427,22 112,22.6715729 112,23.5 C112,24.3284271 111.328427,25 110.5,25 Z M70.5,25 C69.6715729,25 69,24.3284271 69,23.5 C69,22.6715729 69.6715729,22 70.5,22 C71.3284271,22 72,22.6715729 72,23.5 C72,24.3284271 71.3284271,25 70.5,25 Z M94.5,25 C93.6715729,25 93,24.3284271 93,23.5 C93,22.6715729 93.6715729,22 94.5,22 C95.3284271,22 96,22.6715729 96,23.5 C96,24.3284271 95.3284271,25 94.5,25 Z M118.5,25 C117.671573,25 117,24.3284271 117,23.5 C117,22.6715729 117.671573,22 118.5,22 C119.328427,22 120,22.6715729 120,23.5 C120,24.3284271 119.328427,25 118.5,25 Z" id="Combined-Shape" fill="#3282FF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.3 KiB |
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
|
@ -16,6 +16,7 @@ from UM.Util import parseBool
|
|||
from cura.API import Account
|
||||
from cura.API.Account import SyncState
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To update printer metadata with information received about cloud printers.
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT
|
||||
|
@ -129,6 +130,8 @@ class CloudOutputDeviceManager:
|
|||
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
||||
self._onDevicesDiscovered(new_clusters)
|
||||
|
||||
self._updateOnlinePrinters(all_clusters)
|
||||
|
||||
# Hide the current removed_printers_message, if there is any
|
||||
if self._removed_printers_message:
|
||||
self._removed_printers_message.actionTriggered.disconnect(self._onRemovedPrintersMessageActionTriggered)
|
||||
|
@ -154,6 +157,8 @@ class CloudOutputDeviceManager:
|
|||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
||||
|
||||
Logger.debug("Synced cloud printers with account.")
|
||||
|
||||
def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||
|
@ -216,11 +221,6 @@ class CloudOutputDeviceManager:
|
|||
online_cluster_names = {c.friendly_name.lower() for c in clusters if c.is_online and not c.friendly_name is None}
|
||||
new_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower()))
|
||||
|
||||
image_path = os.path.join(
|
||||
CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
|
||||
"resources", "svg", "cloud-flow-completed.svg"
|
||||
)
|
||||
|
||||
message = Message(
|
||||
title = self.i18n_catalog.i18ncp(
|
||||
"info:status",
|
||||
|
@ -230,7 +230,6 @@ class CloudOutputDeviceManager:
|
|||
),
|
||||
progress = 0,
|
||||
lifetime = 0,
|
||||
image_source = image_path,
|
||||
message_type = Message.MessageType.POSITIVE
|
||||
)
|
||||
message.show()
|
||||
|
@ -261,6 +260,16 @@ class CloudOutputDeviceManager:
|
|||
message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>"
|
||||
message.setText(message_text)
|
||||
|
||||
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
|
||||
"""
|
||||
Update the metadata of the printers to store whether they are online or not.
|
||||
:param printer_responses: The responses received from the API about the printer statuses.
|
||||
"""
|
||||
for container_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "machine"):
|
||||
cluster_id = container_stack.getMetaDataEntry("um_cloud_cluster_id", "")
|
||||
if cluster_id in printer_responses:
|
||||
container_stack.setMetaDataEntry("is_online", printer_responses[cluster_id].is_online)
|
||||
|
||||
def _updateOutdatedMachine(self, outdated_machine: GlobalStack, new_cloud_output_device: CloudOutputDevice) -> None:
|
||||
"""
|
||||
Update the cloud metadata of a pre-existing machine that is rediscovered (e.g. if the printer was removed and
|
||||
|
|
|
@ -15,27 +15,26 @@ I18N_CATALOG = i18nCatalog("cura")
|
|||
|
||||
class CloudFlowMessage(Message):
|
||||
|
||||
def __init__(self, address: str) -> None:
|
||||
|
||||
def __init__(self, printer_name: str) -> None:
|
||||
image_path = os.path.join(
|
||||
CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
|
||||
"resources", "svg", "cloud-flow-start.svg"
|
||||
"resources", "svg", "CloudPlatform.svg"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
text=I18N_CATALOG.i18nc("@info:status",
|
||||
"Send and monitor print jobs from anywhere using your Ultimaker account."),
|
||||
lifetime=0,
|
||||
dismissable=True,
|
||||
option_state=False,
|
||||
image_source=QUrl.fromLocalFile(image_path),
|
||||
image_caption=I18N_CATALOG.i18nc("@info:status Ultimaker Cloud should not be translated.",
|
||||
"Connect to Ultimaker Digital Factory"),
|
||||
text = I18N_CATALOG.i18nc("@info:status",
|
||||
f"Your printer <b>{printer_name}</b> could be connected via cloud.\n Manage your print queue and monitor your prints from anywhere connecting your printer to Digital Factory"),
|
||||
title = I18N_CATALOG.i18nc("@info:title", "Are you ready for cloud printing?"),
|
||||
image_source = QUrl.fromLocalFile(image_path)
|
||||
)
|
||||
self._address = address
|
||||
self.addAction("", I18N_CATALOG.i18nc("@action", "Get started"), "", "")
|
||||
self._printer_name = printer_name
|
||||
self.addAction("get_started", I18N_CATALOG.i18nc("@action", "Get started"), "", "")
|
||||
self.addAction("learn_more", I18N_CATALOG.i18nc("@action", "Learn more"), "", "", button_style = Message.ActionButtonStyle.LINK, button_align = Message.ActionButtonAlignment.ALIGN_LEFT)
|
||||
|
||||
self.actionTriggered.connect(self._onCloudFlowStarted)
|
||||
|
||||
def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
|
||||
QDesktopServices.openUrl(QUrl("http://{}/cloud_connect".format(self._address)))
|
||||
self.hide()
|
||||
def _onCloudFlowStarted(self, message_id: str, action_id: str) -> None:
|
||||
if action_id == "get_started":
|
||||
QDesktopServices.openUrl(QUrl("https://digitalfactory.ultimaker.com/app/printers?add_printer=true&utm_source=cura&utm_medium=software&utm_campaign=message-networkprinter-added"))
|
||||
self.hide()
|
||||
else:
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360012019239?utm_source=cura&utm_medium=software&utm_campaign=add-cloud-printer"))
|
||||
|
|
|
@ -52,7 +52,6 @@ class LocalClusterOutputDeviceManager:
|
|||
|
||||
def start(self) -> None:
|
||||
"""Start the network discovery."""
|
||||
|
||||
self._zero_conf_client.start()
|
||||
for address in self._getStoredManualAddresses():
|
||||
self.addManualDevice(address)
|
||||
|
@ -292,4 +291,4 @@ class LocalClusterOutputDeviceManager:
|
|||
if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
|
||||
# Do not show the message if the user is not signed in.
|
||||
return
|
||||
CloudFlowMessage(device.ipAddress).show()
|
||||
CloudFlowMessage(device.name).show()
|
||||
|
|
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