Merge branch 'Ultimaker:master' into vivedino_add_printers

This commit is contained in:
Jim Edwards 2021-12-20 09:25:40 -07:00 committed by GitHub
commit a19f3d2b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1151 changed files with 114606 additions and 89734 deletions

View File

@ -64,7 +64,7 @@ body:
You can find your log file here: 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` 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` 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 If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
- type: checkboxes - type: checkboxes

5
.gitignore vendored
View File

@ -56,6 +56,11 @@ plugins/SettingsGuide
plugins/SettingsGuide2 plugins/SettingsGuide2
plugins/SVGToolpathReader plugins/SVGToolpathReader
plugins/X3GWriter plugins/X3GWriter
plugins/CuraFlatPack
plugins/CuraRemoteSupport
plugins/ModelCutter
plugins/PrintProfileCreator
plugins/MultiPrintPlugin
#Build stuff #Build stuff
CMakeCache.txt CMakeCache.txt

View File

@ -7,5 +7,5 @@ license: "LGPL-3.0"
message: "If you use this software, please cite it using these metadata." message: "If you use this software, please cite it using these metadata."
repository-code: "https://github.com/ultimaker/cura/" repository-code: "https://github.com/ultimaker/cura/"
title: "Ultimaker Cura" title: "Ultimaker Cura"
version: "4.10.0" version: "4.12.0"
... ...

View File

@ -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. 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.
![Screenshot](screenshot.png) ![Screenshot](cura-logo.PNG)
Logging Issues Logging Issues
------------ ------------

BIN
cura-logo.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

View File

@ -1,15 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime
from typing import Optional, Dict, TYPE_CHECKING, Callable
from datetime import datetime
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings from cura.OAuth2.Models import OAuth2Settings, UserProfile
from cura.UltimakerCloud import UltimakerCloudConstants from cura.UltimakerCloud import UltimakerCloudConstants
if TYPE_CHECKING: if TYPE_CHECKING:
@ -46,6 +46,12 @@ class Account(QObject):
loginStateChanged = pyqtSignal(bool) loginStateChanged = pyqtSignal(bool)
"""Signal emitted when user logged in or out""" """Signal emitted when user logged in or out"""
userProfileChanged = pyqtSignal()
"""Signal emitted when new account information is available."""
additionalRightsChanged = pyqtSignal("QVariantMap")
"""Signal emitted when a users additional rights change"""
accessTokenChanged = pyqtSignal() accessTokenChanged = pyqtSignal()
syncRequested = pyqtSignal() syncRequested = pyqtSignal()
"""Sync services may connect to this signal to receive sync triggers. """Sync services may connect to this signal to receive sync triggers.
@ -59,7 +65,7 @@ class Account(QObject):
updatePackagesEnabledChanged = pyqtSignal(bool) updatePackagesEnabledChanged = pyqtSignal(bool)
CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \ 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 " \ "library.project.read library.project.write cura.printjob.read cura.printjob.write " \
"cura.mesh.read cura.mesh.write" "cura.mesh.read cura.mesh.write"
@ -68,12 +74,14 @@ class Account(QObject):
self._application = application self._application = application
self._new_cloud_printers_detected = False self._new_cloud_printers_detected = False
self._error_message = None # type: Optional[Message] self._error_message: Optional[Message] = None
self._logged_in = False self._logged_in = False
self._user_profile: Optional[UserProfile] = None
self._additional_rights: Dict[str, Any] = {}
self._sync_state = SyncState.IDLE self._sync_state = SyncState.IDLE
self._manual_sync_enabled = False self._manual_sync_enabled = False
self._update_packages_enabled = False self._update_packages_enabled = False
self._update_packages_action = None # type: Optional[Callable] self._update_packages_action: Optional[Callable] = None
self._last_sync_str = "-" self._last_sync_str = "-"
self._callback_port = 32118 self._callback_port = 32118
@ -99,7 +107,7 @@ class Account(QObject):
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self.sync) self._update_timer.timeout.connect(self.sync)
self._sync_services = {} # type: Dict[str, int] self._sync_services: Dict[str, int] = {}
"""contains entries "service_name" : SyncState""" """contains entries "service_name" : SyncState"""
def initialize(self) -> None: def initialize(self) -> None:
@ -192,12 +200,17 @@ class Account(QObject):
self._logged_in = logged_in self._logged_in = logged_in
self.loginStateChanged.emit(logged_in) self.loginStateChanged.emit(logged_in)
if logged_in: if logged_in:
self._authorization_service.getUserProfile(self._onProfileChanged)
self._setManualSyncEnabled(False) self._setManualSyncEnabled(False)
self._sync() self._sync()
else: else:
if self._update_timer.isActive(): if self._update_timer.isActive():
self._update_timer.stop() self._update_timer.stop()
def _onProfileChanged(self, profile: Optional[UserProfile]) -> None:
self._user_profile = profile
self.userProfileChanged.emit()
def _sync(self) -> None: def _sync(self) -> None:
"""Signals all sync services to start syncing """Signals all sync services to start syncing
@ -239,32 +252,28 @@ class Account(QObject):
return return
self._authorization_service.startAuthorizationFlow(force_logout_before_login) self._authorization_service.startAuthorizationFlow(force_logout_before_login)
@pyqtProperty(str, notify=loginStateChanged) @pyqtProperty(str, notify = userProfileChanged)
def userName(self): def userName(self):
user_profile = self._authorization_service.getUserProfile() if not self._user_profile:
if not user_profile: return ""
return None return self._user_profile.username
return user_profile.username
@pyqtProperty(str, notify = loginStateChanged) @pyqtProperty(str, notify = userProfileChanged)
def profileImageUrl(self): def profileImageUrl(self):
user_profile = self._authorization_service.getUserProfile() if not self._user_profile:
if not user_profile: return ""
return None return self._user_profile.profile_image_url
return user_profile.profile_image_url
@pyqtProperty(str, notify=accessTokenChanged) @pyqtProperty(str, notify=accessTokenChanged)
def accessToken(self) -> Optional[str]: def accessToken(self) -> Optional[str]:
return self._authorization_service.getAccessToken() return self._authorization_service.getAccessToken()
@pyqtProperty("QVariantMap", notify = loginStateChanged) @pyqtProperty("QVariantMap", notify = userProfileChanged)
def userProfile(self) -> Optional[Dict[str, Optional[str]]]: def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
"""None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """ """None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """
if not self._user_profile:
user_profile = self._authorization_service.getUserProfile()
if not user_profile:
return None return None
return user_profile.__dict__ return self._user_profile.__dict__
@pyqtProperty(str, notify=lastSyncDateTimeChanged) @pyqtProperty(str, notify=lastSyncDateTimeChanged)
def lastSyncDateTime(self) -> str: def lastSyncDateTime(self) -> str:
@ -301,3 +310,14 @@ class Account(QObject):
return # Nothing to do, user isn't logged in. return # Nothing to do, user isn't logged in.
self._authorization_service.deleteAuthData() 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

View 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. # Cura is released under the terms of the LGPLv3 or higher.
# --------- # ---------
@ -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 # 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 # 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. # CuraVersion.py.in template.
CuraSDKVersion = "7.8.0" CuraSDKVersion = "7.9.0"
try: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore
@ -46,6 +46,10 @@ except ImportError:
# Various convenience flags indicating what kind of Cura build it is. # Various convenience flags indicating what kind of Cura build it is.
__ENTERPRISE_VERSION_TYPE = "enterprise" __ENTERPRISE_VERSION_TYPE = "enterprise"
IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE
IsAlternateVersion = CuraBuildType.lower() not in [DEFAULT_CURA_BUILD_TYPE, __ENTERPRISE_VERSION_TYPE]
# NOTE: IsAlternateVersion is to make it possibile to have 'non-numbered' versions, at least as presented to the user.
# (Internally, it'll still have some sort of version-number, but the user is never meant to see it in the GUI).
# Warning: This will also change (some of) the icons/splash-screen to the 'work in progress' alternatives!
try: try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore from cura.CuraVersion import CuraAppDisplayName # type: ignore

View File

@ -91,7 +91,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in hull_polygon.getPoints(): for point in hull_polygon.getPoints():
converted_points.append(Point(point[0] * factor, point[1] * factor)) converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
item = Item(converted_points) item = Item(converted_points)
item.markAsFixedInBin(0) item.markAsFixedInBin(0)
node_items.append(item) node_items.append(item)
@ -110,18 +110,11 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
return found_solution_for_all, node_items 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: def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
""" build_volume: "BuildVolume",
Find placement for a set of scene nodes, and move them by using a single grouped operation. fixed_nodes: Optional[List["SceneNode"]] = None,
:param nodes_to_arrange: The list of nodes that need to be moved. factor = 10000,
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
: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
"""
scene_root = Application.getInstance().getController().getScene().getRoot() scene_root = Application.getInstance().getController().getScene().getRoot()
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) 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( grouped_operation.addOperation(
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
not_fit_count += 1 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

View File

@ -181,8 +181,7 @@ class Backup:
return extracted return extracted
@staticmethod def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool:
def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
"""Extract the whole archive to the given target path. """Extract the whole archive to the given target path.
:param archive: The archive as ZipFile. :param archive: The archive as ZipFile.
@ -201,7 +200,11 @@ class Backup:
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)
name_list = archive.namelist() name_list = archive.namelist()
ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
for archive_filename in name_list: for archive_filename in name_list:
if ignore_string.search(archive_filename):
Logger.warning(f"File ({archive_filename}) in archive that doesn't fit current backup policy; ignored.")
continue
try: try:
archive.extract(archive_filename, target_path) archive.extract(archive_filename, target_path)
except (PermissionError, EnvironmentError): except (PermissionError, EnvironmentError):

View File

@ -6,6 +6,7 @@ import math
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict 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.MeshData import MeshData
from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshBuilder import MeshBuilder
@ -65,6 +66,7 @@ class BuildVolume(SceneNode):
self._height = 0 # type: float self._height = 0 # type: float
self._depth = 0 # type: float self._depth = 0 # type: float
self._shape = "" # type: str self._shape = "" # type: str
self._scale_vector = Vector(1.0, 1.0, 1.0)
self._shader = None self._shader = None
@ -289,7 +291,7 @@ class BuildVolume(SceneNode):
# Mark the node as outside build volume if the set extruder is disabled # Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
try: 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) node.setOutsideBuildArea(True)
continue continue
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet. except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
@ -512,6 +514,13 @@ class BuildVolume(SceneNode):
self._disallowed_area_size = max(size, self._disallowed_area_size) self._disallowed_area_size = max(size, self._disallowed_area_size)
return mb.build() return mb.build()
def _updateScaleFactor(self) -> None:
if not self._global_container_stack:
return
scale_xy = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
scale_z = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_z" , "value"))
self._scale_vector = Vector(scale_xy, scale_xy, scale_z)
def rebuild(self) -> None: def rebuild(self) -> None:
"""Recalculates the build volume & disallowed areas.""" """Recalculates the build volume & disallowed areas."""
@ -553,9 +562,12 @@ class BuildVolume(SceneNode):
self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height) self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
self._updateScaleFactor()
self._volume_aabb = AxisAlignedBox( self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d), minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector),
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)) maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d).scale(self._scale_vector)
)
bed_adhesion_size = self.getEdgeDisallowedSize() bed_adhesion_size = self.getEdgeDisallowedSize()
@ -563,15 +575,15 @@ class BuildVolume(SceneNode):
# This is probably wrong in all other cases. TODO! # This is probably wrong in all other cases. TODO!
# The +1 and -1 is added as there is always a bit of extra room required to work properly. # The +1 and -1 is added as there is always a bit of extra room required to work properly.
scale_to_max_bounds = AxisAlignedBox( scale_to_max_bounds = AxisAlignedBox(
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1), minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1).scale(self._scale_vector),
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1) maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1).scale(self._scale_vector)
) )
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
self.updateNodeBoundaryCheck() self.updateNodeBoundaryCheck()
def getBoundingBox(self): def getBoundingBox(self) -> Optional[AxisAlignedBox]:
return self._volume_aabb return self._volume_aabb
def getRaftThickness(self) -> float: def getRaftThickness(self) -> float:
@ -632,18 +644,18 @@ class BuildVolume(SceneNode):
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingPropertyChanged) extruder.propertyChanged.connect(self._onSettingPropertyChanged)
self._width = self._global_container_stack.getProperty("machine_width", "value") self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
machine_height = self._global_container_stack.getProperty("machine_height", "value") machine_height = self._global_container_stack.getProperty("machine_height", "value")
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height) self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
if self._height < machine_height: if self._height < (machine_height * self._scale_vector.z):
self._build_volume_message.show() self._build_volume_message.show()
else: else:
self._build_volume_message.hide() self._build_volume_message.hide()
else: else:
self._height = self._global_container_stack.getProperty("machine_height", "value") self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide() self._build_volume_message.hide()
self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value")
self._updateDisallowedAreas() self._updateDisallowedAreas()
@ -677,18 +689,18 @@ class BuildVolume(SceneNode):
if setting_key == "print_sequence": if setting_key == "print_sequence":
machine_height = self._global_container_stack.getProperty("machine_height", "value") machine_height = self._global_container_stack.getProperty("machine_height", "value")
if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height) self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
if self._height < machine_height: if self._height < (machine_height * self._scale_vector.z):
self._build_volume_message.show() self._build_volume_message.show()
else: else:
self._build_volume_message.hide() self._build_volume_message.hide()
else: else:
self._height = self._global_container_stack.getProperty("machine_height", "value") self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
self._build_volume_message.hide() self._build_volume_message.hide()
update_disallowed_areas = True update_disallowed_areas = True
# sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
if setting_key in self._machine_settings: if setting_key in self._machine_settings or setting_key in self._material_size_settings:
self._updateMachineSizeProperties() self._updateMachineSizeProperties()
update_extra_z_clearance = True update_extra_z_clearance = True
update_disallowed_areas = True update_disallowed_areas = True
@ -737,9 +749,10 @@ class BuildVolume(SceneNode):
def _updateMachineSizeProperties(self) -> None: def _updateMachineSizeProperties(self) -> None:
if not self._global_container_stack: if not self._global_container_stack:
return return
self._height = self._global_container_stack.getProperty("machine_height", "value") self._updateScaleFactor()
self._width = self._global_container_stack.getProperty("machine_width", "value") self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value")
def _updateDisallowedAreasAndRebuild(self): def _updateDisallowedAreasAndRebuild(self):
@ -756,6 +769,14 @@ class BuildVolume(SceneNode):
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks()) self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
self.rebuild() self.rebuild()
def _scaleAreas(self, result_areas: List[Polygon]) -> None:
if self._global_container_stack is None:
return
for i, polygon in enumerate(result_areas):
result_areas[i] = polygon.scale(
100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
)
def _updateDisallowedAreas(self) -> None: def _updateDisallowedAreas(self) -> None:
if not self._global_container_stack: if not self._global_container_stack:
return return
@ -811,9 +832,11 @@ class BuildVolume(SceneNode):
self._disallowed_areas = [] self._disallowed_areas = []
for extruder_id in result_areas: for extruder_id in result_areas:
self._scaleAreas(result_areas[extruder_id])
self._disallowed_areas.extend(result_areas[extruder_id]) self._disallowed_areas.extend(result_areas[extruder_id])
self._disallowed_areas_no_brim = [] self._disallowed_areas_no_brim = []
for extruder_id in result_areas_no_brim: for extruder_id in result_areas_no_brim:
self._scaleAreas(result_areas_no_brim[extruder_id])
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
def _computeDisallowedAreasPrinted(self, used_extruders): def _computeDisallowedAreasPrinted(self, used_extruders):
@ -1078,9 +1101,14 @@ class BuildVolume(SceneNode):
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what # setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
# the value is. # the value is.
adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value") 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. # Use brim width if brim is enabled OR the prime tower has a brim.
if adhesion_type == "brim": if adhesion_type == "brim":
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value") brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
@ -1194,4 +1222,5 @@ class BuildVolume(SceneNode):
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"] _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings _material_size_settings = ["material_shrinkage_percentage", "material_shrinkage_percentage_xy", "material_shrinkage_percentage_z"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings + _material_size_settings

View File

@ -152,16 +152,17 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(name = ApplicationMetadata.CuraAppName, super().__init__(name = ApplicationMetadata.CuraAppName,
app_display_name = ApplicationMetadata.CuraAppDisplayName, app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion, version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType,
api_version = ApplicationMetadata.CuraSDKVersion, api_version = ApplicationMetadata.CuraSDKVersion,
build_type = ApplicationMetadata.CuraBuildType, build_type = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode, is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png", tray_icon_name = "cura-icon-32.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon-32_wip.png",
**kwargs) **kwargs)
self.default_theme = "cura-light" 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.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() self._boot_loading_time = time.time()
@ -483,7 +484,7 @@ class CuraApplication(QtApplication):
if not self.getIsHeadLess(): if not self.getIsHeadLess():
try: try:
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon_wip.png")))
except FileNotFoundError: except FileNotFoundError:
Logger.log("w", "Unable to find the window icon.") Logger.log("w", "Unable to find the window icon.")
@ -716,6 +717,7 @@ class CuraApplication(QtApplication):
for extruder in global_stack.extruderList: for extruder in global_stack.extruderList:
extruder.userChanges.clear() extruder.userChanges.clear()
global_stack.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 # 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 # before slicing. To ensure that slicer uses right settings values
@ -750,7 +752,9 @@ class CuraApplication(QtApplication):
@pyqtSlot(str, result = QUrl) @pyqtSlot(str, result = QUrl)
def getDefaultPath(self, key): def getDefaultPath(self, key):
default_path = self.getPreferences().getValue("local_file/%s" % 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) @pyqtSlot(str, str)
def setDefaultPath(self, key, default_path): def setDefaultPath(self, key, default_path):

View File

@ -59,6 +59,8 @@ class ExtrudersModel(ListModel):
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
"""List of colours to display if there is no material or the material has no known colour. """ """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): def __init__(self, parent = None):
"""Initialises the extruders model, defining the roles and listening for changes in the data. """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.MaterialBrandRole, "material_brand")
self.addRoleName(self.ColorNameRole, "color_name") self.addRoleName(self.ColorNameRole, "color_name")
self.addRoleName(self.MaterialTypeRole, "material_type") self.addRoleName(self.MaterialTypeRole, "material_type")
self.addRoleName(self.MaterialNameRole, "material_name")
self._update_extruder_timer = QTimer() self._update_extruder_timer = QTimer()
self._update_extruder_timer.setInterval(100) self._update_extruder_timer.setInterval(100)
self._update_extruder_timer.setSingleShot(True) self._update_extruder_timer.setSingleShot(True)
@ -199,8 +202,8 @@ class ExtrudersModel(ListModel):
"material_brand": material_brand, "material_brand": material_brand,
"color_name": color_name, "color_name": color_name,
"material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "", "material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "",
"material_name": extruder.material.getMetaDataEntry("name") if extruder.material else "",
} }
items.append(item) items.append(item)
extruders_changed = True extruders_changed = True
@ -224,6 +227,7 @@ class ExtrudersModel(ListModel):
"material_brand": "", "material_brand": "",
"color_name": "", "color_name": "",
"material_type": "", "material_type": "",
"material_label": ""
} }
items.append(item) items.append(item)
if self._items != items: if self._items != items:

View File

@ -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. # 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 List, Optional
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -10,6 +11,7 @@ from UM.Util import parseBool
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES # To filter on the printer's capabilities.
class GlobalStacksModel(ListModel): class GlobalStacksModel(ListModel):
@ -20,6 +22,7 @@ class GlobalStacksModel(ListModel):
MetaDataRole = Qt.UserRole + 5 MetaDataRole = Qt.UserRole + 5
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
RemovalWarningRole = Qt.UserRole + 7 RemovalWarningRole = Qt.UserRole + 7
IsOnlineRole = Qt.UserRole + 8
def __init__(self, parent = None) -> None: def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -31,18 +34,70 @@ class GlobalStacksModel(ListModel):
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection") self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
self.addRoleName(self.MetaDataRole, "metadata") self.addRoleName(self.MetaDataRole, "metadata")
self.addRoleName(self.DiscoverySourceRole, "discoverySource") self.addRoleName(self.DiscoverySourceRole, "discoverySource")
self.addRoleName(self.IsOnlineRole, "isOnline")
self._change_timer = QTimer() self._change_timer = QTimer()
self._change_timer.setInterval(200) self._change_timer.setInterval(200)
self._change_timer.setSingleShot(True) self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._update) self._change_timer.timeout.connect(self._update)
self._filter_connection_type = None # type: Optional[ConnectionType]
self._filter_online_only = False
self._filter_capabilities: List[str] = [] # Required capabilities that all listed printers must have.
# Listen to changes # Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged) CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged) CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._updateDelayed() self._updateDelayed()
filterConnectionTypeChanged = pyqtSignal()
filterCapabilitiesChanged = pyqtSignal()
filterOnlineOnlyChanged = pyqtSignal()
def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
if self._filter_connection_type != new_filter:
self._filter_connection_type = new_filter
self.filterConnectionTypeChanged.emit()
@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
def setFilterOnlineOnly(self, new_filter: bool) -> None:
if self._filter_online_only != new_filter:
self._filter_online_only = new_filter
self.filterOnlineOnlyChanged.emit()
@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 setFilterCapabilities(self, new_filter: List[str]) -> None:
if self._filter_capabilities != new_filter:
self._filter_capabilities = new_filter
self.filterCapabilitiesChanged.emit()
@pyqtProperty("QStringList", fset = setFilterCapabilities, notify = filterCapabilitiesChanged)
def filterCapabilities(self) -> List[str]:
"""
Capabilities to require on the list of printers.
Only printers that have all of these capabilities will be shown in this model.
"""
return self._filter_capabilities
def _onContainerChanged(self, container) -> None: def _onContainerChanged(self, container) -> None:
"""Handler for container added/removed events from registry""" """Handler for container added/removed events from registry"""
@ -58,6 +113,10 @@ class GlobalStacksModel(ListModel):
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine") container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
for container_stack in container_stacks: 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 has_remote_connection = False
for connection_type in container_stack.configuredConnectionTypes: for connection_type in container_stack.configuredConnectionTypes:
@ -67,6 +126,14 @@ class GlobalStacksModel(ListModel):
if parseBool(container_stack.getMetaDataEntry("hidden", False)): if parseBool(container_stack.getMetaDataEntry("hidden", False)):
continue continue
is_online = container_stack.getMetaDataEntry("is_online", False)
if self._filter_online_only and not is_online:
continue
capabilities = set(container_stack.getMetaDataEntry(META_CAPABILITIES, "").split(","))
if set(self._filter_capabilities) - capabilities: # Not all required capabilities are met.
continue
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName()) device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
section_name = "Connected printers" if has_remote_connection else "Preset printers" section_name = "Connected printers" if has_remote_connection else "Preset printers"
section_name = self._catalog.i18nc("@info:title", section_name) section_name = self._catalog.i18nc("@info:title", section_name)
@ -82,6 +149,7 @@ class GlobalStacksModel(ListModel):
"hasRemoteConnection": has_remote_connection, "hasRemoteConnection": has_remote_connection,
"metadata": container_stack.getMetaData().copy(), "metadata": container_stack.getMetaData().copy(),
"discoverySource": section_name, "discoverySource": section_name,
"removalWarning": removal_warning}) "removalWarning": removal_warning,
"isOnline": is_online})
items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"])) items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
self.setItems(items) self.setItems(items)

View File

@ -106,11 +106,15 @@ class IntentCategoryModel(ListModel):
for category in available_categories: for category in available_categories:
qualities = IntentModel() qualities = IntentModel()
qualities.setIntentCategory(category) qualities.setIntentCategory(category)
try:
weight = list(IntentCategoryModel._get_translations().keys()).index(category)
except ValueError:
weight = 99
result.append({ result.append({
"name": IntentCategoryModel.translation(category, "name", catalog.i18nc("@label", "Unknown")), "name": IntentCategoryModel.translation(category, "name", category),
"description": IntentCategoryModel.translation(category, "description", None), "description": IntentCategoryModel.translation(category, "description", None),
"intent_category": category, "intent_category": category,
"weight": list(IntentCategoryModel._get_translations().keys()).index(category), "weight": weight,
"qualities": qualities "qualities": qualities
}) })
result.sort(key = lambda k: k["weight"]) result.sort(key = lambda k: k["weight"])

View File

@ -2,25 +2,28 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials. 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 from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials. 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.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Resources import Resources # To find QML files.
from UM.Signal import postponeSignals, CompressTechnique 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.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.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: if TYPE_CHECKING:
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class MaterialManagementModel(QObject): class MaterialManagementModel(QObject):
favoritesChanged = pyqtSignal(str) favoritesChanged = pyqtSignal(str)
"""Triggered when a favorite is added or removed. """Triggered when a favorite is added or removed.
@ -28,6 +31,66 @@ class MaterialManagementModel(QObject):
:param The base file of the material is provided as parameter when this emits :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) @pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> 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? """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
@ -262,52 +325,10 @@ class MaterialManagementModel(QObject):
except ValueError: # Material was not in the favorites list. 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)) Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
@pyqtSlot(result = QUrl) @pyqtSlot()
def getPreferredExportAllPath(self) -> QUrl: 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()
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
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
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}.")

View File

@ -361,8 +361,15 @@ class QualityManagementModel(ListModel):
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))), "section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))),
}) })
# Sort by quality_type for each intent category # Sort by quality_type for each intent category
intent_translations_list = list(intent_translations)
result = sorted(result, key = lambda x: (list(intent_translations).index(x["intent_category"]), x["quality_type"])) def getIntentWeight(intent_category):
try:
return intent_translations_list.index(intent_category)
except ValueError:
return 99
result = sorted(result, key = lambda x: (getIntentWeight(x["intent_category"]), x["quality_type"]))
item_list += result item_list += result
# Create quality_changes group items # Create quality_changes group items

View File

@ -41,10 +41,6 @@ class QualityProfilesDropDownMenuModel(ListModel):
machine_manager.activeQualityGroupChanged.connect(self._onChange) machine_manager.activeQualityGroupChanged.connect(self._onChange)
machine_manager.activeMaterialChanged.connect(self._onChange) machine_manager.activeMaterialChanged.connect(self._onChange)
machine_manager.activeVariantChanged.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 self._layer_height_unit = "" # This is cached

View File

@ -6,11 +6,15 @@ from typing import List
from UM.Application import Application from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Math.Vector import Vector
from UM.Message import Message 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.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Arranging.Nest2DArrange import arrange from cura.Arranging.Nest2DArrange import arrange, createGroupOperationForArrange
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -43,11 +47,11 @@ class MultiplyObjectsJob(Job):
# Only count sliceable objects # Only count sliceable objects
if node_.callDecoration("isSliceable"): if node_.callDecoration("isSliceable"):
fixed_nodes.append(node_) fixed_nodes.append(node_)
nodes_to_add_without_arrange = []
for node in self._objects: for node in self._objects:
# If object is part of a group, multiply group # If object is part of a group, multiply group
current_node = node 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() current_node = current_node.getParent()
if current_node in processed_nodes: if current_node in processed_nodes:
@ -56,19 +60,38 @@ class MultiplyObjectsJob(Job):
for _ in range(self._count): for _ in range(self._count):
new_node = copy.deepcopy(node) new_node = copy.deepcopy(node)
# Same build plate # Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber") build_plate_number = current_node.callDecoration("getBuildPlateNumber")
new_node.callDecoration("setBuildPlateNumber", build_plate_number) new_node.callDecoration("setBuildPlateNumber", build_plate_number)
for child in new_node.getChildren(): for child in new_node.getChildren():
child.callDecoration("setBuildPlateNumber", build_plate_number) child.callDecoration("setBuildPlateNumber", build_plate_number)
if not current_node.getParent().callDecoration("isSliceable"):
nodes.append(new_node) 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 found_solution_for_all = True
group_operation = GroupedOperation()
if nodes: if nodes:
found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, group_operation, not_fit_count = createGroupOperationForArrange(nodes,
factor = 10000, add_new_nodes_in_scene = True) 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() status_message.hide()
if not found_solution_for_all: if not found_solution_for_all:

View File

@ -1,18 +1,19 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime
import json
import secrets
from hashlib import sha512
from base64 import b64encode from base64 import b64encode
from typing import Optional from datetime import datetime
import requests from hashlib import sha512
from PyQt5.QtNetwork import QNetworkReply
from UM.i18n import i18nCatalog import secrets
from UM.Logger import Logger from typing import Callable, Optional
import urllib.parse
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
@ -30,14 +31,13 @@ class AuthorizationHelpers:
return self._settings return self._settings
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str, callback: Callable[[AuthenticationResponse], None]) -> None:
"""Request the access token from the authorization server. """
Request the access token from the authorization server.
:param authorization_code: The authorization code from the 1st step. :param authorization_code: The authorization code from the 1st step.
:param verification_code: The verification code needed for the PKCE extension. :param verification_code: The verification code needed for the PKCE extension.
:return: An AuthenticationResponse object. :param callback: Once the token has been obtained, this function will be called with the response.
""" """
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
@ -46,18 +46,21 @@ class AuthorizationHelpers:
"code_verifier": verification_code, "code_verifier": verification_code,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "", "scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
} }
try: headers = {"Content-type": "application/x-www-form-urlencoded"}
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore HttpRequestManager.getInstance().post(
except requests.exceptions.ConnectionError as connection_error: self._token_url,
return AuthenticationResponse(success = False, err_message = f"Unable to connect to remote server: {connection_error}") data = urllib.parse.urlencode(data).encode("UTF-8"),
headers_dict = headers,
callback = lambda response: self.parseTokenResponse(response, callback),
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
)
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
"""Request the access token from the authorization server using a refresh token. """
Request the access token from the authorization server using a refresh token.
:param refresh_token: :param refresh_token: A long-lived token used to refresh the authentication token.
:return: An AuthenticationResponse object. :param callback: Once the token has been obtained, this function will be called with the response.
""" """
Logger.log("d", "Refreshing the access token for [%s]", self._settings.OAUTH_SERVER_URL) Logger.log("d", "Refreshing the access token for [%s]", self._settings.OAUTH_SERVER_URL)
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -66,75 +69,99 @@ class AuthorizationHelpers:
"refresh_token": refresh_token, "refresh_token": refresh_token,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "", "scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
} }
try: headers = {"Content-type": "application/x-www-form-urlencoded"}
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore HttpRequestManager.getInstance().post(
except requests.exceptions.ConnectionError: self._token_url,
return AuthenticationResponse(success = False, err_message = "Unable to connect to remote server") data = urllib.parse.urlencode(data).encode("UTF-8"),
except OSError as e: headers_dict = headers,
return AuthenticationResponse(success = False, err_message = "Operating system is unable to set up a secure connection: {err}".format(err = str(e))) callback = lambda response: self.parseTokenResponse(response, callback),
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
)
@staticmethod def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
"""Parse the token response from the authorization server into an AuthenticationResponse object. """Parse the token response from the authorization server into an AuthenticationResponse object.
:param token_response: The JSON string data response from the authorization server. :param token_response: The JSON string data response from the authorization server.
:return: An AuthenticationResponse object. :return: An AuthenticationResponse object.
""" """
token_data = HttpRequestManager.readJSON(token_response)
token_data = None
try:
token_data = json.loads(token_response.text)
except ValueError:
Logger.log("w", "Could not parse token response data: %s", token_response.text)
if not token_data: if not token_data:
return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response.")) callback(AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response.")))
return
if token_response.status_code not in (200, 201): if token_response.error() != QNetworkReply.NetworkError.NoError:
return AuthenticationResponse(success = False, err_message = token_data["error_description"]) callback(AuthenticationResponse(success = False, err_message = token_data["error_description"]))
return
return AuthenticationResponse(success=True, callback(AuthenticationResponse(success = True,
token_type=token_data["token_type"], token_type = token_data["token_type"],
access_token=token_data["access_token"], access_token = token_data["access_token"],
refresh_token=token_data["refresh_token"], refresh_token = token_data["refresh_token"],
expires_in=token_data["expires_in"], expires_in = token_data["expires_in"],
scope=token_data["scope"], scope = token_data["scope"],
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)))
return
def parseJWT(self, access_token: str) -> Optional["UserProfile"]: def checkToken(self, access_token: str, success_callback: Optional[Callable[[UserProfile], None]] = None, failed_callback: Optional[Callable[[], None]] = None) -> None:
"""Calls the authentication API endpoint to get the token data. """Calls the authentication API endpoint to get the token data.
The API is called asynchronously. When a response is given, the callback is called with the user's profile.
:param access_token: The encoded JWT token. :param access_token: The encoded JWT token.
:return: Dict containing some profile data. :param success_callback: When a response is given, this function will be called with a user profile. If None,
there will not be a callback.
:param failed_callback: When the request failed or the response didn't parse, this function will be called.
""" """
check_token_url = "{}/check-token".format(self._settings.OAUTH_SERVER_URL)
try: Logger.log("d", "Checking the access token for [%s]", check_token_url)
check_token_url = "{}/check-token".format(self._settings.OAUTH_SERVER_URL) headers = {
Logger.log("d", "Checking the access token for [%s]", check_token_url) "Authorization": f"Bearer {access_token}"
token_request = requests.get(check_token_url, headers = { }
"Authorization": "Bearer {}".format(access_token) HttpRequestManager.getInstance().get(
}) check_token_url,
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): headers_dict = headers,
# Connection was suddenly dropped. Nothing we can do about that. callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
Logger.logException("w", "Something failed while attempting to parse the JWT token") error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None
return None
if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None
user_data = token_request.json().get("data")
if not user_data or not isinstance(user_data, dict):
Logger.log("w", "Could not parse user data from token: %s", user_data)
return None
return UserProfile(
user_id = user_data["user_id"],
username = user_data["username"],
profile_image_url = user_data.get("profile_image_url", ""),
organization_id = user_data.get("organization", {}).get("organization_id"),
subscriptions = user_data.get("subscriptions", [])
) )
def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:
"""
Parses the user profile from a reply to /check-token.
If the response is valid, the callback will be called to return the user profile to the caller.
:param reply: A network reply to a request to the /check-token URL.
:param success_callback: A function to call once a user profile was successfully obtained.
:param failed_callback: A function to call if parsing the profile failed.
"""
if reply.error() != QNetworkReply.NetworkError.NoError:
Logger.warning(f"Could not access account information. QNetworkError {reply.errorString()}")
if failed_callback is not None:
failed_callback()
return
profile_data = HttpRequestManager.getInstance().readJSON(reply)
if profile_data is None or "data" not in profile_data:
Logger.warning("Could not parse user data from token.")
if failed_callback is not None:
failed_callback()
return
profile_data = profile_data["data"]
required_fields = {"user_id", "username"}
if "user_id" not in profile_data or "username" not in profile_data:
Logger.warning(f"User data missing required field(s): {required_fields - set(profile_data.keys())}")
if failed_callback is not None:
failed_callback()
return
if success_callback is not None:
success_callback(UserProfile(
user_id = profile_data["user_id"],
username = profile_data["username"],
profile_image_url = profile_data.get("profile_image_url", ""),
organization_id = profile_data.get("organization", {}).get("organization_id"),
subscriptions = profile_data.get("subscriptions", [])
))
@staticmethod @staticmethod
def generateVerificationCode(code_length: int = 32) -> str: def generateVerificationCode(code_length: int = 32) -> str:
"""Generate a verification code of arbitrary length. """Generate a verification code of arbitrary length.

View File

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from threading import Lock # To turn an asynchronous call synchronous.
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
@ -14,6 +15,7 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class AuthorizationRequestHandler(BaseHTTPRequestHandler): class AuthorizationRequestHandler(BaseHTTPRequestHandler):
"""This handler handles all HTTP requests on the local web server. """This handler handles all HTTP requests on the local web server.
@ -24,11 +26,11 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
super().__init__(request, client_address, server) super().__init__(request, client_address, server)
# These values will be injected by the HTTPServer that this handler belongs to. # These values will be injected by the HTTPServer that this handler belongs to.
self.authorization_helpers = None # type: Optional[AuthorizationHelpers] self.authorization_helpers: Optional[AuthorizationHelpers] = None
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.authorization_callback: Optional[Callable[[AuthenticationResponse], None]] = None
self.verification_code = None # type: Optional[str] self.verification_code: Optional[str] = None
self.state = None # type: Optional[str] self.state: Optional[str] = None
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback. # CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
def do_HEAD(self) -> None: def do_HEAD(self) -> None:
@ -70,13 +72,23 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
if state != self.state: if state != self.state:
token_response = AuthenticationResponse( token_response = AuthenticationResponse(
success = False, success = False,
err_message=catalog.i18nc("@message", err_message = catalog.i18nc("@message", "The provided state is not correct.")
"The provided state is not correct.")
) )
elif code and self.authorization_helpers is not None and self.verification_code is not None: elif code and self.authorization_helpers is not None and self.verification_code is not None:
token_response = AuthenticationResponse(
success = False,
err_message = catalog.i18nc("@message", "Timeout when authenticating with the account server.")
)
# If the code was returned we get the access token. # If the code was returned we get the access token.
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( lock = Lock()
code, self.verification_code) lock.acquire()
def callback(response: AuthenticationResponse) -> None:
nonlocal token_response
token_response = response
lock.release()
self.authorization_helpers.getAccessTokenUsingAuthorizationCode(code, self.verification_code, callback)
lock.acquire(timeout = 60) # Block thread until request is completed (which releases the lock). If not acquired, the timeout message stays.
elif self._queryGet(query, "error_code") == "user_denied": elif self._queryGet(query, "error_code") == "user_denied":
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog). # Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).

View File

@ -3,10 +3,9 @@
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING, Dict from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
from urllib.parse import urlencode, quote_plus from urllib.parse import urlencode, quote_plus
import requests.exceptions
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
@ -16,7 +15,7 @@ from UM.Signal import Signal
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from cura.OAuth2.Models import AuthenticationResponse from cura.OAuth2.Models import AuthenticationResponse, BaseModel
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -26,6 +25,7 @@ if TYPE_CHECKING:
MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers" MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
class AuthorizationService: class AuthorizationService:
"""The authorization service is responsible for handling the login flow, storing user credentials and providing """The authorization service is responsible for handling the login flow, storing user credentials and providing
account information. account information.
@ -43,12 +43,13 @@ class AuthorizationService:
self._settings = settings self._settings = settings
self._auth_helpers = AuthorizationHelpers(settings) self._auth_helpers = AuthorizationHelpers(settings)
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL) self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
self._auth_data = None # type: Optional[AuthenticationResponse] self._auth_data: Optional[AuthenticationResponse] = None
self._user_profile = None # type: Optional["UserProfile"] self._user_profile: Optional["UserProfile"] = None
self._preferences = preferences self._preferences = preferences
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
self._currently_refreshing_token = False # Whether we are currently in the process of refreshing auth. Don't make new requests while busy.
self._unable_to_get_data_message = None # type: Optional[Message] self._unable_to_get_data_message: Optional[Message] = None
self.onAuthStateChanged.connect(self._authChanged) self.onAuthStateChanged.connect(self._authChanged)
@ -62,69 +63,80 @@ class AuthorizationService:
if self._preferences: if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
def getUserProfile(self) -> Optional["UserProfile"]: def getUserProfile(self, callback: Optional[Callable[[Optional["UserProfile"]], None]] = None) -> None:
"""Get the user profile as obtained from the JWT (JSON Web Token). """
Get the user profile as obtained from the JWT (JSON Web Token).
If the JWT is not yet parsed, calling this will take care of that. If the JWT is not yet checked and parsed, calling this will take care of that.
:param callback: Once the user profile is obtained, this function will be called with the given user profile. If
:return: UserProfile if a user is logged in, None otherwise. the profile fails to be obtained, this function will be called with None.
See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT` See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT`
""" """
if self._user_profile:
# We already obtained the profile. No need to make another request for it.
if callback is not None:
callback(self._user_profile)
return
if not self._user_profile: # If no user profile was stored locally, we try to get it from JWT.
# If no user profile was stored locally, we try to get it from JWT. def store_profile(profile: Optional["UserProfile"]) -> None:
try: if profile is not None:
self._user_profile = self._parseJWT() self._user_profile = profile
except requests.exceptions.ConnectionError: if callback is not None:
# Unable to get connection, can't login. callback(profile)
Logger.logException("w", "Unable to validate user data with the remote server.") elif self._auth_data:
return None # If there is no user profile from the JWT, we have to log in again.
Logger.warning("The user profile could not be loaded. The user must log in again!")
self.deleteAuthData()
if callback is not None:
callback(None)
else:
if callback is not None:
callback(None)
if not self._user_profile and self._auth_data: self._parseJWT(callback = store_profile)
# If there is still no user profile from the JWT, we have to log in again.
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
self.deleteAuthData()
return None
return self._user_profile def _parseJWT(self, callback: Callable[[Optional["UserProfile"]], None]) -> None:
"""
def _parseJWT(self) -> Optional["UserProfile"]: Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
"""Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. :param callback: A function to call asynchronously once the user profile has been obtained. It will be called
with `None` if it failed to obtain a user profile.
:return: UserProfile if it was able to parse, None otherwise.
""" """
if not self._auth_data or self._auth_data.access_token is None: if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again. # If no auth data exists, we should always log in again.
Logger.log("d", "There was no auth data or access token") Logger.debug("There was no auth data or access token")
return None callback(None)
return
try: # When we checked the token we may get a user profile. This callback checks if that is a valid one and tries to refresh the token if it's not.
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token) def check_user_profile(user_profile: Optional["UserProfile"]) -> None:
except AttributeError: if user_profile:
# THis might seem a bit double, but we get crash reports about this (CURA-2N2 in sentry) # If the profile was found, we call it back immediately.
Logger.log("d", "There was no auth data or access token") callback(user_profile)
return None return
# The JWT was expired or invalid and we should request a new one.
if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.warning("There was no refresh token in the auth data.")
callback(None)
return
if user_data: def process_auth_data(auth_data: AuthenticationResponse) -> None:
# If the profile was found, we return it immediately. if auth_data.access_token is None:
return user_data Logger.warning("Unable to use the refresh token to get a new access token.")
# The JWT was expired or invalid and we should request a new one. callback(None)
if self._auth_data.refresh_token is None: return
Logger.log("w", "There was no refresh token in the auth data.") # Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been
return None # deleted from the server already. Do not store the auth_data if we could not get new auth_data (e.g.
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token) # due to a network error), since this would cause an infinite loop trying to get new auth-data.
if not self._auth_data or self._auth_data.access_token is None: if auth_data.success:
Logger.log("w", "Unable to use the refresh token to get a new access token.") self._storeAuthData(auth_data)
# The token could not be refreshed using the refresh token. We should login again. self._auth_helpers.checkToken(auth_data.access_token, callback, lambda: callback(None))
return None
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
# from the server already. Do not store the auth_data if we could not get new auth_data (eg due to a
# network error), since this would cause an infinite loop trying to get new auth-data self._auth_helpers.checkToken(self._auth_data.access_token, check_user_profile, lambda: check_user_profile(None))
if self._auth_data.success:
self._storeAuthData(self._auth_data)
return self._auth_helpers.parseJWT(self._auth_data.access_token)
def getAccessToken(self) -> Optional[str]: def getAccessToken(self) -> Optional[str]:
"""Get the access token as provided by the response data.""" """Get the access token as provided by the response data."""
@ -149,13 +161,20 @@ class AuthorizationService:
if self._auth_data is None or self._auth_data.refresh_token is None: if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.log("w", "Unable to refresh access token, since there is no refresh token.") Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
return return
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
if response.success: def process_auth_data(response: AuthenticationResponse) -> None:
self._storeAuthData(response) if response.success:
self.onAuthStateChanged.emit(logged_in = True) self._storeAuthData(response)
else: self.onAuthStateChanged.emit(logged_in = True)
Logger.log("w", "Failed to get a new access token from the server.") else:
self.onAuthStateChanged.emit(logged_in = False) Logger.warning("Failed to get a new access token from the server.")
self.onAuthStateChanged.emit(logged_in = False)
if self._currently_refreshing_token:
Logger.debug("Was already busy refreshing token. Do not start a new request.")
return
self._currently_refreshing_token = True
self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
def deleteAuthData(self) -> None: def deleteAuthData(self) -> None:
"""Delete the authentication data that we have stored locally (eg; logout)""" """Delete the authentication data that we have stored locally (eg; logout)"""
@ -244,21 +263,23 @@ class AuthorizationService:
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY)) preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
if preferences_data: if preferences_data:
self._auth_data = AuthenticationResponse(**preferences_data) self._auth_data = AuthenticationResponse(**preferences_data)
# Also check if we can actually get the user profile information.
user_profile = self.getUserProfile()
if user_profile is not None:
self.onAuthStateChanged.emit(logged_in = True)
Logger.log("d", "Auth data was successfully loaded")
else:
if self._unable_to_get_data_message is not None:
self._unable_to_get_data_message.hide()
self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", # Also check if we can actually get the user profile information.
"Unable to reach the Ultimaker account server."), def callback(profile: Optional["UserProfile"]) -> None:
title = i18n_catalog.i18nc("@info:title", "Warning"), if profile is not None:
message_type = Message.MessageType.ERROR) self.onAuthStateChanged.emit(logged_in = True)
Logger.log("w", "Unable to load auth data from preferences") Logger.debug("Auth data was successfully loaded")
self._unable_to_get_data_message.show() else:
if self._unable_to_get_data_message is not None:
self._unable_to_get_data_message.show()
else:
self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info",
"Unable to reach the Ultimaker account server."),
title = i18n_catalog.i18nc("@info:title", "Log-in failed"),
message_type = Message.MessageType.ERROR)
Logger.warning("Unable to get user profile using auth data from preferences.")
self._unable_to_get_data_message.show()
self.getUserProfile(callback)
except (ValueError, TypeError): except (ValueError, TypeError):
Logger.logException("w", "Could not load auth data from preferences") Logger.logException("w", "Could not load auth data from preferences")
@ -271,8 +292,9 @@ class AuthorizationService:
return return
self._auth_data = auth_data self._auth_data = auth_data
self._currently_refreshing_token = False
if auth_data: if auth_data:
self._user_profile = self.getUserProfile() self.getUserProfile()
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(auth_data.dump())) self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(auth_data.dump()))
else: else:
Logger.log("d", "Clearing the user profile") Logger.log("d", "Clearing the user profile")

View File

@ -2,9 +2,10 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Type, TYPE_CHECKING, Optional, List from typing import Type, TYPE_CHECKING, Optional, List
from io import BlockingIOError
import keyring import keyring
from keyring.backend import KeyringBackend from keyring.backend import KeyringBackend
from keyring.errors import NoKeyringError, PasswordSetError, KeyringLocked from keyring.errors import NoKeyringError, PasswordSetError, KeyringLocked, KeyringError
from UM.Logger import Logger from UM.Logger import Logger
@ -44,7 +45,7 @@ class KeyringAttribute:
self._store_secure = False self._store_secure = False
Logger.logException("w", "No keyring backend present") Logger.logException("w", "No keyring backend present")
return getattr(instance, self._name) return getattr(instance, self._name)
except KeyringLocked: except (KeyringLocked, BlockingIOError):
self._store_secure = False self._store_secure = False
Logger.log("i", "Access to the keyring was denied.") Logger.log("i", "Access to the keyring was denied.")
return getattr(instance, self._name) return getattr(instance, self._name)
@ -52,6 +53,10 @@ class KeyringAttribute:
self._store_secure = False self._store_secure = False
Logger.log("w", "The password retrieved from the keyring cannot be used because it contains characters that cannot be decoded.") Logger.log("w", "The password retrieved from the keyring cannot be used because it contains characters that cannot be decoded.")
return getattr(instance, self._name) return getattr(instance, self._name)
except KeyringError:
self._store_secure = False
Logger.logException("w", "Unknown keyring error.")
return getattr(instance, self._name)
else: else:
return getattr(instance, self._name) return getattr(instance, self._name)

View File

@ -72,8 +72,8 @@ class PickingPass(RenderPass):
window_size = self._renderer.getWindowSize() window_size = self._renderer.getWindowSize()
px = (0.5 + x / 2.0) * window_size[0] px = int((0.5 + x / 2.0) * window_size[0])
py = (0.5 + y / 2.0) * window_size[1] py = int((0.5 + y / 2.0) * window_size[1])
if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1): if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1):
return -1 return -1

View File

@ -42,7 +42,7 @@ class PrintJobOutputModel(QObject):
self._preview_image = None # type: Optional[QImage] self._preview_image = None # type: Optional[QImage]
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged) @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
def compatibleMachineFamilies(self): def compatibleMachineFamilies(self) -> List[str]:
# Hack; Some versions of cluster will return a family more than once... # Hack; Some versions of cluster will return a family more than once...
return list(set(self._compatible_machine_families)) return list(set(self._compatible_machine_families))
@ -77,11 +77,11 @@ class PrintJobOutputModel(QObject):
self._configuration = configuration self._configuration = configuration
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify=ownerChanged) @pyqtProperty(str, notify = ownerChanged)
def owner(self): def owner(self) -> str:
return self._owner return self._owner
def updateOwner(self, owner): def updateOwner(self, owner: str) -> None:
if self._owner != owner: if self._owner != owner:
self._owner = owner self._owner = owner
self.ownerChanged.emit() self.ownerChanged.emit()
@ -132,7 +132,7 @@ class PrintJobOutputModel(QObject):
@pyqtProperty(float, notify = timeElapsedChanged) @pyqtProperty(float, notify = timeElapsedChanged)
def progress(self) -> float: def progress(self) -> float:
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception. result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
return min(result, 1.0) # Never get a progress past 1.0 return min(result, 1.0) # Never get a progress past 1.0
@pyqtProperty(str, notify=stateChanged) @pyqtProperty(str, notify=stateChanged)
@ -151,12 +151,12 @@ class PrintJobOutputModel(QObject):
return False return False
return True return True
def updateTimeTotal(self, new_time_total): def updateTimeTotal(self, new_time_total: int) -> None:
if self._time_total != new_time_total: if self._time_total != new_time_total:
self._time_total = new_time_total self._time_total = new_time_total
self.timeTotalChanged.emit() self.timeTotalChanged.emit()
def updateTimeElapsed(self, new_time_elapsed): def updateTimeElapsed(self, new_time_elapsed: int) -> None:
if self._time_elapsed != new_time_elapsed: if self._time_elapsed != new_time_elapsed:
self._time_elapsed = new_time_elapsed self._time_elapsed = new_time_elapsed
self.timeElapsedChanged.emit() self.timeElapsedChanged.emit()

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from UM.FileHandler.FileHandler import FileHandler #For typing. from UM.FileHandler.FileHandler import FileHandler #For typing.
@ -114,6 +114,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
return b"".join(file_data_bytes_list) return b"".join(file_data_bytes_list)
def _update(self) -> None: def _update(self) -> None:
"""
Update the connection state of this device.
This is called on regular intervals.
"""
if self._last_response_time: if self._last_response_time:
time_since_last_response = time() - self._last_response_time time_since_last_response = time() - self._last_response_time
else: else:
@ -127,11 +132,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if time_since_last_response > self._timeout_time >= time_since_last_request: if time_since_last_response > self._timeout_time >= time_since_last_request:
# Go (or stay) into timeout. # Go (or stay) into timeout.
if self._connection_state_before_timeout is None: 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) self.setConnectionState(ConnectionState.Closed)
elif self._connection_state == ConnectionState.Closed: elif self.connectionState == ConnectionState.Closed:
# Go out of timeout. # Go out of timeout.
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here 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) self.setConnectionState(self._connection_state_before_timeout)
@ -361,7 +366,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_response_time = time() self._last_response_time = time()
if self._connection_state == ConnectionState.Connecting: if self.connectionState == ConnectionState.Connecting:
self.setConnectionState(ConnectionState.Connected) self.setConnectionState(ConnectionState.Connected)
callback_key = reply.url().toString() + str(reply.operation()) callback_key = reply.url().toString() + str(reply.operation())

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from enum import IntEnum from enum import IntEnum
from typing import Callable, List, Optional, Union from typing import Callable, List, Optional, Union
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
import cura.CuraApplication # Imported like this to prevent circular imports.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication from UM.Qt.QtApplication import QtApplication
@ -120,11 +122,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
callback(QMessageBox.Yes) callback(QMessageBox.Yes)
def isConnected(self) -> bool: 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: 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 self._connection_state = connection_state
cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().setMetaDataEntry("is_online", self.isConnected())
self.connectionStateChanged.emit(self._id) self.connectionStateChanged.emit(self._id)
@pyqtProperty(int, constant = True) @pyqtProperty(int, constant = True)
@ -133,6 +146,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtProperty(int, notify = connectionStateChanged) @pyqtProperty(int, notify = connectionStateChanged)
def connectionState(self) -> "ConnectionState": 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 return self._connection_state
def _update(self) -> None: def _update(self) -> None:

View File

@ -0,0 +1,264 @@
# 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.
)
# Filter out any printer not capable of the 'import_material' capability. Needs FW 7.0.1-RC at the least!
self._printer_metadata = [ printer_data for printer_data in self._printer_metadata if (
UltimakerCloudConstants.META_CAPABILITIES in printer_data and
"import_material" in printer_data[UltimakerCloudConstants.META_CAPABILITIES]
)
]
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.

View File

@ -12,6 +12,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from cura.Machines.ContainerTree import ContainerTree
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
@ -403,6 +404,32 @@ class ExtruderManager(QObject):
raise IndexError(msg) raise IndexError(msg)
extruder_stack_0.definition = extruder_definition extruder_stack_0.definition = extruder_definition
@pyqtSlot("QVariant", result = bool)
def getExtruderHasQualityForMaterial(self, extruder_stack: "ExtruderStack") -> bool:
"""Checks if quality nodes exist for the variant/material combination."""
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if not global_stack or not extruder_stack:
return False
if not global_stack.getMetaDataEntry("has_materials"):
return True
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
active_variant_name = extruder_stack.variant.getMetaDataEntry("name")
if active_variant_name not in machine_node.variants:
Logger.log("w", "Could not find the variant %s", active_variant_name)
return True
active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials[extruder_stack.material.getMetaDataEntry("base_file")]
active_material_node_qualities = active_material_node.qualities
if not active_material_node_qualities:
return False
return list(active_material_node_qualities.keys())[0] != "empty_quality"
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key: str) -> List: def getInstanceExtruderValues(self, key: str) -> List:
"""Get all extruder values for a certain setting. """Get all extruder values for a certain setting.

View File

@ -855,7 +855,6 @@ class MachineManager(QObject):
caution_message = Message( caution_message = Message(
catalog.i18nc("@info:message Followed by a list of settings.", 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)), "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")) title = catalog.i18nc("@info:title", "Settings updated"))
caution_message.show() caution_message.show()
@ -1191,7 +1190,7 @@ class MachineManager(QObject):
self.setIntentByCategory(quality_changes_group.intent_category) self.setIntentByCategory(quality_changes_group.intent_category)
self._reCalculateNumUserSettings() self._reCalculateNumUserSettings()
self.correctExtruderSettings()
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
@ -1536,7 +1535,7 @@ class MachineManager(QObject):
machine_node = ContainerTree.getInstance().machines.get(machine_definition_id) machine_node = ContainerTree.getInstance().machines.get(machine_definition_id)
variant_node = machine_node.variants.get(variant_name) variant_node = machine_node.variants.get(variant_name)
if variant_node is None: if variant_node is None:
Logger.error("There is no variant with the name {variant_name}.") Logger.error(f"There is no variant with the name {variant_name}.")
return return
self.setVariant(position, variant_node) self.setVariant(position, variant_node)

View File

@ -61,6 +61,10 @@ class SettingInheritanceManager(QObject):
result.append(key) result.append(key)
return result 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") @pyqtSlot(str, str, result = "QStringList")
def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]: def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
if self._global_container_stack is None: if self._global_container_stack is None:

View File

@ -3,6 +3,7 @@
import numpy import numpy
from PyQt5 import QtCore from PyQt5 import QtCore
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
from cura.PreviewPass import PreviewPass from cura.PreviewPass import PreviewPass
@ -46,6 +47,7 @@ class Snapshot:
render_width, render_height = (width, height) if active_camera is None else active_camera.getWindowSize() render_width, render_height = (width, height) if active_camera is None else active_camera.getWindowSize()
render_width = int(render_width) render_width = int(render_width)
render_height = int(render_height) render_height = int(render_height)
QCoreApplication.processEvents() # This ensures that the opengl context is correctly available
preview_pass = PreviewPass(render_width, render_height) preview_pass = PreviewPass(render_width, render_height)
root = scene.getRoot() root = scene.getRoot()

View File

@ -17,7 +17,9 @@ class CuraSplashScreen(QSplashScreen):
self._scale = 0.7 self._scale = 0.7
self._version_y_offset = 0 # when extra visual elements are in the background image, move version text down self._version_y_offset = 0 # when extra visual elements are in the background image, move version text down
if ApplicationMetadata.IsEnterpriseVersion: if ApplicationMetadata.IsAlternateVersion:
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura_wip.png"))
elif ApplicationMetadata.IsEnterpriseVersion:
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura_enterprise.png")) splash_image = QPixmap(Resources.getPath(Resources.Images, "cura_enterprise.png"))
self._version_y_offset = 26 self._version_y_offset = 26
else: else:
@ -70,7 +72,7 @@ class CuraSplashScreen(QSplashScreen):
font = QFont() # Using system-default font here font = QFont() # Using system-default font here
font.setPixelSize(18) font.setPixelSize(18)
painter.setFont(font) painter.setFont(font)
painter.drawText(60, 70 + self._version_y_offset, round(330 * self._scale), round(230 * self._scale), Qt.AlignLeft | Qt.AlignTop, version[0]) painter.drawText(60, 70 + self._version_y_offset, round(330 * self._scale), round(230 * self._scale), Qt.AlignLeft | Qt.AlignTop, version[0] if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType)
if len(version) > 1: if len(version) > 1:
font.setPixelSize(16) font.setPixelSize(16)
painter.setFont(font) painter.setFont(font)

View File

@ -46,7 +46,9 @@ class TextManager(QObject):
line = line.replace("[", "") line = line.replace("[", "")
line = line.replace("]", "") line = line.replace("]", "")
open_version = Version(line) open_version = Version(line)
if open_version > Version([14, 99, 99]): # Bit of a hack: We released the 15.x.x versions before 2.x if open_version < Version([0, 0, 1]): # Something went wrong with parsing, assume non-numerical alternate version that should be on top.
open_version = Version([99, 99, 99])
if Version([14, 99, 99]) < open_version < Version([16, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()]) open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()])
open_header = "" open_header = ""
change_logs_dict[open_version] = collections.OrderedDict() change_logs_dict[open_version] = collections.OrderedDict()
@ -66,7 +68,9 @@ class TextManager(QObject):
text_version = version text_version = version
if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()]) text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()])
content += "<h1>" + str(text_version) + "</h1><br>" if version > Version([99, 0, 0]): # Leave it out altogether if it was originally a non-numbered version.
text_version = ""
content += ("<h1>" + str(text_version) + "</h1><br>") if text_version else ""
content += "" content += ""
for change in change_logs_dict[version]: for change in change_logs_dict[version]:
if str(change) != "": if str(change) != "":

View File

@ -1,7 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import deque
import os import os
from collections import deque
from typing import TYPE_CHECKING, Optional, List, Dict, Any from typing import TYPE_CHECKING, Optional, List, Dict, Any
from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal
@ -16,24 +18,23 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
#
# This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in the
# welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
#
# - id : A unique page_id which can be used in function goToPage(page_id)
# - page_url : The QUrl to the QML file that contains the content of this page
# - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is not
# provided, it will go to the page with the current index + 1
# - next_page_button_text: (OPTIONAL) The text to show for the "next" button, by default it's the translated text of
# "Next". Note that each step QML can decide whether to use this text or not, so it's not
# mandatory.
# - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
# shown. By default all pages should be shown. If a function returns False, that page will
# be skipped and its next page will be shown.
#
# Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
#
class WelcomePagesModel(ListModel): class WelcomePagesModel(ListModel):
"""
This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in
the welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
- id : A unique page_id which can be used in function goToPage(page_id)
- page_url : The QUrl to the QML file that contains the content of this page
- next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is
not provided, it will go to the page with the current index + 1
- next_page_button_text : (OPTIONAL) The text to show for the "next" button, by default it's the translated text of
"Next". Note that each step QML can decide whether to use this text or not, so it's not
mandatory.
- should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
shown. By default all pages should be shown. If a function returns False, that page will
be skipped and its next page will be shown.
Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
"""
IdRole = Qt.UserRole + 1 # Page ID IdRole = Qt.UserRole + 1 # Page ID
PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file
@ -55,11 +56,11 @@ class WelcomePagesModel(ListModel):
self._default_next_button_text = self._catalog.i18nc("@action:button", "Next") self._default_next_button_text = self._catalog.i18nc("@action:button", "Next")
self._pages = [] # type: List[Dict[str, Any]] self._pages: List[Dict[str, Any]] = []
self._current_page_index = 0 self._current_page_index = 0
# Store all the previous page indices so it can go back. # Store all the previous page indices so it can go back.
self._previous_page_indices_stack = deque() # type: deque self._previous_page_indices_stack: deque = deque()
# If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the # If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the
# specific case. See initialize() for how this variable is set. # specific case. See initialize() for how this variable is set.
@ -72,17 +73,21 @@ class WelcomePagesModel(ListModel):
def currentPageIndex(self) -> int: def currentPageIndex(self) -> int:
return self._current_page_index return self._current_page_index
# Returns a float number in [0, 1] which indicates the current progress.
@pyqtProperty(float, notify = currentPageIndexChanged) @pyqtProperty(float, notify = currentPageIndexChanged)
def currentProgress(self) -> float: def currentProgress(self) -> float:
"""
Returns a float number in [0, 1] which indicates the current progress.
"""
if len(self._items) == 0: if len(self._items) == 0:
return 0 return 0
else: else:
return self._current_page_index / len(self._items) return self._current_page_index / len(self._items)
# Indicates if the current page is the last page.
@pyqtProperty(bool, notify = currentPageIndexChanged) @pyqtProperty(bool, notify = currentPageIndexChanged)
def isCurrentPageLast(self) -> bool: def isCurrentPageLast(self) -> bool:
"""
Indicates if the current page is the last page.
"""
return self._current_page_index == len(self._items) - 1 return self._current_page_index == len(self._items) - 1
def _setCurrentPageIndex(self, page_index: int) -> None: def _setCurrentPageIndex(self, page_index: int) -> None:
@ -91,17 +96,22 @@ class WelcomePagesModel(ListModel):
self._current_page_index = page_index self._current_page_index = page_index
self.currentPageIndexChanged.emit() self.currentPageIndexChanged.emit()
# Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
@pyqtSlot() @pyqtSlot()
def atEnd(self) -> None: def atEnd(self) -> None:
"""
Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
"""
self.allFinished.emit() self.allFinished.emit()
self.resetState() self.resetState()
# Goes to the next page.
# If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
# the "self._current_page_index".
@pyqtSlot() @pyqtSlot()
def goToNextPage(self, from_index: Optional[int] = None) -> None: def goToNextPage(self, from_index: Optional[int] = None) -> None:
"""
Goes to the next page.
If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
the "self._current_page_index".
"""
# Look for the next page that should be shown # Look for the next page that should be shown
current_index = self._current_page_index if from_index is None else from_index current_index = self._current_page_index if from_index is None else from_index
while True: while True:
@ -137,9 +147,11 @@ class WelcomePagesModel(ListModel):
# Move to the next page # Move to the next page
self._setCurrentPageIndex(next_page_index) self._setCurrentPageIndex(next_page_index)
# Goes to the previous page. If there's no previous page, do nothing.
@pyqtSlot() @pyqtSlot()
def goToPreviousPage(self) -> None: def goToPreviousPage(self) -> None:
"""
Goes to the previous page. If there's no previous page, do nothing.
"""
if len(self._previous_page_indices_stack) == 0: if len(self._previous_page_indices_stack) == 0:
Logger.log("i", "No previous page, do nothing") Logger.log("i", "No previous page, do nothing")
return return
@ -148,9 +160,9 @@ class WelcomePagesModel(ListModel):
self._current_page_index = previous_page_index self._current_page_index = previous_page_index
self.currentPageIndexChanged.emit() self.currentPageIndexChanged.emit()
# Sets the current page to the given page ID. If the page ID is not found, do nothing.
@pyqtSlot(str) @pyqtSlot(str)
def goToPage(self, page_id: str) -> None: def goToPage(self, page_id: str) -> None:
"""Sets the current page to the given page ID. If the page ID is not found, do nothing."""
page_index = self.getPageIndexById(page_id) page_index = self.getPageIndexById(page_id)
if page_index is None: if page_index is None:
# FIXME: If we cannot find the next page, we cannot do anything here. # FIXME: If we cannot find the next page, we cannot do anything here.
@ -165,18 +177,22 @@ class WelcomePagesModel(ListModel):
# Find the next page to show starting from the "page_index" # Find the next page to show starting from the "page_index"
self.goToNextPage(from_index = page_index) self.goToNextPage(from_index = page_index)
# Checks if the page with the given index should be shown by calling the "should_show_function" associated with it.
# If the function is not present, returns True (show page by default).
def _shouldPageBeShown(self, page_index: int) -> bool: def _shouldPageBeShown(self, page_index: int) -> bool:
"""
Checks if the page with the given index should be shown by calling the "should_show_function" associated with
it. If the function is not present, returns True (show page by default).
"""
next_page_item = self.getItem(page_index) next_page_item = self.getItem(page_index)
should_show_function = next_page_item.get("should_show_function", lambda: True) should_show_function = next_page_item.get("should_show_function", lambda: True)
return should_show_function() return should_show_function()
# Resets the state of the WelcomePagesModel. This functions does the following:
# - Resets current_page_index to 0
# - Clears the previous page indices stack
@pyqtSlot() @pyqtSlot()
def resetState(self) -> None: def resetState(self) -> None:
"""
Resets the state of the WelcomePagesModel. This functions does the following:
- Resets current_page_index to 0
- Clears the previous page indices stack
"""
self._current_page_index = 0 self._current_page_index = 0
self._previous_page_indices_stack.clear() self._previous_page_indices_stack.clear()
@ -188,8 +204,8 @@ class WelcomePagesModel(ListModel):
def shouldShowWelcomeFlow(self) -> bool: def shouldShowWelcomeFlow(self) -> bool:
return self._should_show_welcome_flow return self._should_show_welcome_flow
# Gets the page index with the given page ID. If the page ID doesn't exist, returns None.
def getPageIndexById(self, page_id: str) -> Optional[int]: def getPageIndexById(self, page_id: str) -> Optional[int]:
"""Gets the page index with the given page ID. If the page ID doesn't exist, returns None."""
page_idx = None page_idx = None
for idx, page_item in enumerate(self._items): for idx, page_item in enumerate(self._items):
if page_item["id"] == page_id: if page_item["id"] == page_id:
@ -197,8 +213,9 @@ class WelcomePagesModel(ListModel):
break break
return page_idx return page_idx
# Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages". @staticmethod
def _getBuiltinWelcomePagePath(self, page_filename: str) -> "QUrl": def _getBuiltinWelcomePagePath(page_filename: str) -> QUrl:
"""Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages"."""
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
os.path.join("WelcomePages", page_filename))) os.path.join("WelcomePages", page_filename)))
@ -213,21 +230,22 @@ class WelcomePagesModel(ListModel):
self._initialize() self._initialize()
def _initialize(self, update_should_show_flag: bool = True) -> None: def _initialize(self, update_should_show_flag: bool = True) -> None:
show_whatsnew_only = False show_whats_new_only = False
if update_should_show_flag: if update_should_show_flag:
has_active_machine = self._application.getMachineManager().activeMachine is not None has_active_machine = self._application.getMachineManager().activeMachine is not None
has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion() has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion()
# Only show the what's new dialog if there's no machine and we have just upgraded # Only show the what's new dialog if there's no machine and we have just upgraded
show_complete_flow = not has_active_machine show_complete_flow = not has_active_machine
show_whatsnew_only = has_active_machine and has_app_just_upgraded show_whats_new_only = has_active_machine and has_app_just_upgraded
# FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and # FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and
# possibly some others, setting the initial active machine is not done when the MachineManager gets initialized. # possibly some others, setting the initial active machine is not done when the MachineManager gets
# So at this point, we don't know if there will be an active machine or not. It could be that the active machine # initialized. So at this point, we don't know if there will be an active machine or not. It could be that
# files are corrupted so we cannot rely on Preferences either. This makes sure that once the active machine # the active machine files are corrupted so we cannot rely on Preferences either. This makes sure that once
# gets changed, this model updates the flags, so it can decide whether to show the welcome flow or not. # the active machine gets changed, this model updates the flags, so it can decide whether to show the
should_show_welcome_flow = show_complete_flow or show_whatsnew_only # welcome flow or not.
should_show_welcome_flow = show_complete_flow or show_whats_new_only
if should_show_welcome_flow != self._should_show_welcome_flow: if should_show_welcome_flow != self._should_show_welcome_flow:
self._should_show_welcome_flow = should_show_welcome_flow self._should_show_welcome_flow = should_show_welcome_flow
self.shouldShowWelcomeFlowChanged.emit() self.shouldShowWelcomeFlowChanged.emit()
@ -274,23 +292,25 @@ class WelcomePagesModel(ListModel):
] ]
pages_to_show = all_pages_list pages_to_show = all_pages_list
if show_whatsnew_only: if show_whats_new_only:
pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list)) pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list))
self._pages = pages_to_show self._pages = pages_to_show
self.setItems(self._pages) self.setItems(self._pages)
# For convenience, inject the default "next" button text to each item if it's not present.
def setItems(self, items: List[Dict[str, Any]]) -> None: def setItems(self, items: List[Dict[str, Any]]) -> None:
# For convenience, inject the default "next" button text to each item if it's not present.
for item in items: for item in items:
if "next_page_button_text" not in item: if "next_page_button_text" not in item:
item["next_page_button_text"] = self._default_next_button_text item["next_page_button_text"] = self._default_next_button_text
super().setItems(items) super().setItems(items)
# Indicates if the machine action panel should be shown by checking if there's any first start machine actions
# available.
def shouldShowMachineActions(self) -> bool: def shouldShowMachineActions(self) -> bool:
"""
Indicates if the machine action panel should be shown by checking if there's any first start machine actions
available.
"""
global_stack = self._application.getMachineManager().activeMachine global_stack = self._application.getMachineManager().activeMachine
if global_stack is None: if global_stack is None:
return False return False
@ -312,6 +332,3 @@ class WelcomePagesModel(ListModel):
def addPage(self) -> None: def addPage(self) -> None:
pass pass
__all__ = ["WelcomePagesModel"]

View File

@ -1,27 +1,39 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from .WelcomePagesModel import WelcomePagesModel
import os import os
from typing import Optional, Dict, List, Tuple from typing import Optional, Dict, List, Tuple, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty, pyqtSlot from PyQt5.QtCore import pyqtProperty, pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
from UM.Resources import Resources from UM.Resources import Resources
# from cura.UI.WelcomePagesModel import WelcomePagesModel
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for showing the
# "what's new" page. This is also used in the "Help" menu to show the changes log. if TYPE_CHECKING:
# from PyQt5.QtCore import QObject
from cura.CuraApplication import CuraApplication
class WhatsNewPagesModel(WelcomePagesModel): class WhatsNewPagesModel(WelcomePagesModel):
"""
This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for showing the
"what's new" page. This is also used in the "Help" menu to show the changes log.
"""
image_formats = [".png", ".jpg", ".jpeg", ".gif", ".svg"] image_formats = [".png", ".jpg", ".jpeg", ".gif", ".svg"]
text_formats = [".txt", ".htm", ".html"] text_formats = [".txt", ".htm", ".html"]
image_key = "image" image_key = "image"
text_key = "text" text_key = "text"
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent)
self._subpages: List[Dict[str, Optional[str]]] = []
@staticmethod @staticmethod
def _collectOrdinalFiles(resource_type: int, include: List[str]) -> Tuple[Dict[int, str], int]: def _collectOrdinalFiles(resource_type: int, include: List[str]) -> Tuple[Dict[int, str], int]:
result = {} #type: Dict[int, str] result = {} # type: Dict[int, str]
highest = -1 highest = -1
try: try:
folder_path = Resources.getPath(resource_type, "whats_new") folder_path = Resources.getPath(resource_type, "whats_new")
@ -65,7 +77,7 @@ class WhatsNewPagesModel(WelcomePagesModel):
texts, max_text = WhatsNewPagesModel._collectOrdinalFiles(Resources.Texts, WhatsNewPagesModel.text_formats) texts, max_text = WhatsNewPagesModel._collectOrdinalFiles(Resources.Texts, WhatsNewPagesModel.text_formats)
highest = max(max_image, max_text) highest = max(max_image, max_text)
self._subpages = [] #type: List[Dict[str, Optional[str]]] self._subpages = []
for n in range(0, highest + 1): for n in range(0, highest + 1):
self._subpages.append({ self._subpages.append({
WhatsNewPagesModel.image_key: None if n not in images else images[n], WhatsNewPagesModel.image_key: None if n not in images else images[n],
@ -93,5 +105,3 @@ class WhatsNewPagesModel(WelcomePagesModel):
def getSubpageText(self, page: int) -> str: def getSubpageText(self, page: int) -> str:
result = self._getSubpageItem(page, WhatsNewPagesModel.text_key) result = self._getSubpageItem(page, WhatsNewPagesModel.text_key)
return result if result else "* * *" return result if result else "* * *"
__all__ = ["WhatsNewPagesModel"]

View File

@ -0,0 +1,218 @@
# 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.setProperty("syncStatusText", "") # Reset any previous error messages.
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()

View File

@ -13,6 +13,9 @@ DEFAULT_DIGITAL_FACTORY_URL = "https://digitalfactory.ultimaker.com" # type: st
META_UM_LINKED_TO_ACCOUNT = "um_linked_to_account" META_UM_LINKED_TO_ACCOUNT = "um_linked_to_account"
"""(bool) Whether a cloud printer is linked to an Ultimaker account""" """(bool) Whether a cloud printer is linked to an Ultimaker account"""
META_CAPABILITIES = "capabilities"
"""(list[str]) a list of capabilities this printer supports"""
try: try:
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
if CuraCloudAPIRoot == "": if CuraCloudAPIRoot == "":

View File

@ -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 PyQt5.QtNetwork import QNetworkRequest
from UM.Logger import Logger from UM.Logger import Logger
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope 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): class UltimakerCloudScope(DefaultUserAgentScope):
@ -12,7 +18,7 @@ class UltimakerCloudScope(DefaultUserAgentScope):
Also add the user agent headers (see DefaultUserAgentScope). Also add the user agent headers (see DefaultUserAgentScope).
""" """
def __init__(self, application: CuraApplication): def __init__(self, application: "CuraApplication"):
super().__init__(application) super().__init__(application)
api = application.getCuraAPI() api = application.getCuraAPI()
self._account = api.account # type: Account self._account = api.account # type: Account

View File

@ -48,6 +48,8 @@ if with_sentry_sdk:
sentry_env = "development" # Master is always a development version. sentry_env = "development" # Master is always a development version.
elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion: elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
sentry_env = "beta" sentry_env = "beta"
elif "alpha" in ApplicationMetadata.CuraVersion or "ALPHA" in ApplicationMetadata.CuraVersion:
sentry_env = "alpha"
try: try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99": if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
sentry_env = "nightly" sentry_env = "nightly"

View File

@ -49,7 +49,9 @@ _ignored_machine_network_metadata = {
"removal_warning", "removal_warning",
"group_name", "group_name",
"group_size", "group_size",
"connection_type" "connection_type",
"capabilities",
"octoprint_api_key",
} # type: Set[str] } # type: Set[str]
@ -377,7 +379,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# - the global stack DOESN'T exist but some/all of the extruder stacks exist # - the global stack DOESN'T exist but some/all of the extruder stacks exist
# To simplify this, only check if the global stack exists or not # To simplify this, only check if the global stack exists or not
global_stack_id = self._stripFileToId(global_stack_file) global_stack_id = self._stripFileToId(global_stack_file)
serialized = archive.open(global_stack_file).read().decode("utf-8") serialized = archive.open(global_stack_file).read().decode("utf-8")
serialized = GlobalStack._updateSerialized(serialized, global_stack_file) serialized = GlobalStack._updateSerialized(serialized, global_stack_file)
machine_name = self._getMachineNameFromSerializedStack(serialized) machine_name = self._getMachineNameFromSerializedStack(serialized)
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized) self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)

View File

@ -32,6 +32,12 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
Logger.error("3MF Writer class is unavailable. Can't write workspace.") Logger.error("3MF Writer class is unavailable. Can't write workspace.")
return False return False
global_stack = machine_manager.activeMachine
if global_stack is None:
self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
return False
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
mesh_writer.setStoreArchive(True) mesh_writer.setStoreArchive(True)
mesh_writer.write(stream, nodes, mode) mesh_writer.write(stream, nodes, mode)
@ -40,7 +46,6 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
if archive is None: # This happens if there was no mesh data to write. if archive is None: # This happens if there was no mesh data to write.
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
global_stack = machine_manager.activeMachine
try: try:
# Add global container stack data to the archive. # Add global container stack data to the archive.
@ -149,7 +154,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
"group_name", "group_name",
"group_size", "group_size",
"connection_type", "connection_type",
"octoprint_api_key" "capabilities",
"octoprint_api_key",
} }
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys) serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)

View File

@ -10,6 +10,10 @@ from UM.Application import Application
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot
from PyQt5.QtCore import QBuffer
import Savitar import Savitar
@ -149,6 +153,22 @@ class ThreeMFWriter(MeshWriter):
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
# Attempt to add a thumbnail
snapshot = self._createSnapshot()
if snapshot:
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG")
thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
archive.writestr(thumbnail_file, thumbnail_buffer.data())
# Add PNG to content types file
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
# Add thumbnail relation to _rels/.rels file
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
savitar_scene = Savitar.Scene() savitar_scene = Savitar.Scene()
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData() metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
@ -212,3 +232,17 @@ class ThreeMFWriter(MeshWriter):
self._archive = archive self._archive = archive
return True return True
@call_on_qt_thread # must be called from the main thread because of OpenGL
def _createSnapshot(self):
Logger.log("d", "Creating thumbnail image...")
if not CuraApplication.getInstance().isVisible:
Logger.log("w", "Can't create snapshot when renderer not initialized.")
return None
try:
snapshot = Snapshot.snapshot(width = 300, height = 300)
except:
Logger.logException("w", "Failed to create snapshot image")
return None
return snapshot

View File

@ -123,6 +123,9 @@ class StartSliceJob(Job):
Job.yieldThread() Job.yieldThread()
for changed_setting_key in changed_setting_keys: 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") validation_state = stack.getProperty(changed_setting_key, "validationState")
if validation_state is None: if validation_state is None:

View File

@ -67,10 +67,12 @@ class DigitalFactoryApiClient:
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and
response.library_max_private_projects is not None): response.library_max_private_projects is not None):
callback( # A user has DF access when library_max_private_projects is either -1 (unlimited) or bigger then 0
response.library_max_private_projects == -1 or # Note: -1 is unlimited has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
response.library_max_private_projects > 0) callback(has_access)
self._library_max_private_projects = response.library_max_private_projects 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: else:
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
callback(False) callback(False)

View File

@ -261,7 +261,10 @@ class DigitalFactoryController(QObject):
""" """
Error function, called whenever the retrieval of the files in a library project fails. Error function, called whenever the retrieval of the files in a library project fails.
""" """
Logger.log("w", "Failed to retrieve the list of files in project '{}' from the Digital Library".format(self._project_model._projects[self._selected_project_idx])) try:
Logger.warning(f"Failed to retrieve the list of files in project '{self._project_model._projects[self._selected_project_idx]}' from the Digital Library")
except IndexError:
Logger.warning(f"Failed to retrieve the list of files in a project from the Digital Library. And failed to get the project too.")
self.setRetrievingFilesStatus(RetrievalStatus.Failed) self.setRetrievingFilesStatus(RetrievalStatus.Failed)
@pyqtSlot() @pyqtSlot()

View File

@ -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 # 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. # 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._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z self._previous_z = z
elif self._previous_extrusion_value > e[self._extruder_number]: elif self._previous_extrusion_value > e[self._extruder_number]:

View File

@ -96,11 +96,11 @@ UM.Dialog
} }
showAll: toggleShowAll.checked || filterInput.text !== "" showAll: toggleShowAll.checked || filterInput.text !== ""
} }
delegate:Loader delegate: Loader
{ {
id: loader id: loader
width: parent.width width: listview.width
height: model.type != undefined ? UM.Theme.getSize("section").height : 0 height: model.type != undefined ? UM.Theme.getSize("section").height : 0
property var definition: model property var definition: model

View File

@ -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 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 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 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 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: # history / changelog:
# V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment) # V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
@ -33,13 +35,17 @@
# 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: 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 unknown 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.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. # 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. # 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. # 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 # 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 # 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. # 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 - # Uses -
# M220 S<factor in percent> - set speed factor override percentage # M220 S<factor in percent> - set speed factor override percentage
@ -56,9 +62,9 @@ from ..Script import Script
import re 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): class ChangeAtZ(Script):
version = "5.2.1" version = "5.3.0"
def getSettingDataString(self): def getSettingDataString(self):
return """{ return """{
@ -69,10 +75,10 @@ class ChangeAtZ(Script):
"settings": { "settings": {
"caz_enabled": { "caz_enabled": {
"label": "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", "type": "bool",
"default_value": true "default_value": true
}, },
"a_trigger": { "a_trigger": {
"label": "Trigger", "label": "Trigger",
"description": "Trigger at height or at layer no.", "description": "Trigger at height or at layer no.",
@ -119,7 +125,7 @@ class ChangeAtZ(Script):
"description": "Displays the current changes to the LCD", "description": "Displays the current changes to the LCD",
"type": "bool", "type": "bool",
"default_value": false "default_value": false
}, },
"e1_Change_speed": { "e1_Change_speed": {
"label": "Change Speed", "label": "Change Speed",
"description": "Select if total speed (print and travel) has to be changed", "description": "Select if total speed (print and travel) has to be changed",
@ -222,6 +228,23 @@ class ChangeAtZ(Script):
"maximum_value_warning": "120", "maximum_value_warning": "120",
"enabled": "h1_Change_bedTemp" "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": { "i1_Change_extruderOne": {
"label": "Change Extruder 1 Temp", "label": "Change Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be changed", "description": "Select if First Extruder Temperature has to be changed",
@ -278,25 +301,25 @@ class ChangeAtZ(Script):
"description": "Indicates you would like to modify retraction properties.", "description": "Indicates you would like to modify retraction properties.",
"type": "bool", "type": "bool",
"default_value": false "default_value": false
}, },
"caz_retractstyle": { "caz_retractstyle": {
"label": "Retract Style", "label": "Retract Style",
"description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.", "description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.",
"type": "enum", "type": "enum",
"options": { "options": {
"linear": "Linear Move", "linear": "Linear Move",
"firmware": "Firmware" "firmware": "Firmware"
}, },
"default_value": "linear", "default_value": "linear",
"enabled": "caz_change_retract" "enabled": "caz_change_retract"
}, },
"caz_change_retractfeedrate": { "caz_change_retractfeedrate": {
"label": "Change Retract Feed Rate", "label": "Change Retract Feed Rate",
"description": "Changes the retraction feed rate during print", "description": "Changes the retraction feed rate during print",
"type": "bool", "type": "bool",
"default_value": false, "default_value": false,
"enabled": "caz_change_retract" "enabled": "caz_change_retract"
}, },
"caz_retractfeedrate": { "caz_retractfeedrate": {
"label": "Retract Feed Rate", "label": "Retract Feed Rate",
"description": "New Retract Feed Rate (mm/s)", "description": "New Retract Feed Rate (mm/s)",
@ -325,7 +348,7 @@ class ChangeAtZ(Script):
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "20", "maximum_value_warning": "20",
"enabled": "caz_change_retractlength" "enabled": "caz_change_retractlength"
} }
} }
}""" }"""
@ -345,6 +368,7 @@ class ChangeAtZ(Script):
self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne") self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne")
self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo") 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_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, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo") self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed") self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
@ -776,6 +800,10 @@ class ChangeAtZProcessor:
if "bedTemp" in values: if "bedTemp" in values:
codes.append("BedTemp: " + str(round(values["bedTemp"]))) 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) # set our extruder one temp (if specified)
if "extruderOne" in values: if "extruderOne" in values:
codes.append("Extruder 1 Temp: " + str(round(values["extruderOne"]))) codes.append("Extruder 1 Temp: " + str(round(values["extruderOne"])))
@ -858,6 +886,10 @@ class ChangeAtZProcessor:
if "bedTemp" in values: if "bedTemp" in values:
codes.append("M140 S" + str(values["bedTemp"])) 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) # set our extruder one temp (if specified)
if "extruderOne" in values: if "extruderOne" in values:
codes.append("M104 S" + str(values["extruderOne"]) + " T0") codes.append("M104 S" + str(values["extruderOne"]) + " T0")
@ -943,7 +975,7 @@ class ChangeAtZProcessor:
# nothing to do # nothing to do
return "" return ""
# Returns the unmodified GCODE line from previous ChangeZ edits # Returns the unmodified GCODE line from previous ChangeAtZ edits
@staticmethod @staticmethod
def getOriginalLine(line: str) -> str: def getOriginalLine(line: str) -> str:
@ -990,7 +1022,7 @@ class ChangeAtZProcessor:
else: else:
return self.currentZ >= self.targetZ 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 @staticmethod
def markChangesForDeletion(layer: str): def markChangesForDeletion(layer: str):
return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer) return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
@ -1288,7 +1320,7 @@ class ChangeAtZProcessor:
# flag that we're inside our target layer # flag that we're inside our target layer
self.insideTargetLayer = True self.insideTargetLayer = True
# Removes all the ChangeZ layer defaults from the given layer # Removes all the ChangeAtZ layer defaults from the given layer
@staticmethod @staticmethod
def removeMarkedChanges(layer: str) -> str: def removeMarkedChanges(layer: str) -> str:
return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer) return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
@ -1364,6 +1396,16 @@ class ChangeAtZProcessor:
# move to the next command # move to the next command
return 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 # handle extruder temp changes
if command.command == "M104" or command.command == "M109": if command.command == "M104" or command.command == "M109":

View File

@ -458,7 +458,7 @@ class PauseAtHeight(Script):
# Optionally extrude material # Optionally extrude material
if extrude_amount != 0: 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("@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" prepend_gcode += self.putValue("@pause remove the waste filament from parking area and press continue printing") + "\n"
@ -489,18 +489,17 @@ class PauseAtHeight(Script):
# Set extruder resume temperature # Set extruder resume temperature
prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + " ; resume temperature\n" prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + " ; resume temperature\n"
# Push the filament back, if extrude_amount != 0: # Need to prime after the pause.
if retraction_amount != 0: # Push the filament back.
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n" if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
# Optionally extrude material # Prime the material.
if extrude_amount != 0: prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "; Extra extrude after the unpause\n"
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n"
# and retract again, the properly primes the nozzle # And retract again to make the movements back to the starting position.
# when changing filament. if retraction_amount != 0:
if retraction_amount != 0: prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head back # Move the head back
if park_enabled: if park_enabled:

View File

@ -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. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import re #To perform the search and replace. import re #To perform the search and replace.

View 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. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Math.Color import Color from UM.Math.Color import Color

View File

@ -56,12 +56,12 @@ vertex41core =
value = (abs_value - min_value) / (max_value - min_value); value = (abs_value - min_value) / (max_value - min_value);
} }
float red = value; float red = value;
float green = 1-abs(1-4*value); float green = 1.0 - abs(1.0 - 4.0 * value);
if (value > 0.375) if (value > 0.375)
{ {
green = 0.5; green = 0.5;
} }
float blue = max(1-4*value, 0); float blue = max(1.0 - 4.0 * value, 0.0);
return vec4(red, green, blue, 1.0); return vec4(red, green, blue, 1.0);
} }
@ -76,7 +76,7 @@ vertex41core =
{ {
value = (abs_value - min_value) / (max_value - min_value); value = (abs_value - min_value) / (max_value - min_value);
} }
float red = min(max(4*value-2, 0), 1); float red = min(max(4.0 * value - 2.0, 0.0), 1.0);
float green = min(1.5*value, 0.75); float green = min(1.5*value, 0.75);
if (value > 0.75) if (value > 0.75)
{ {
@ -98,18 +98,18 @@ vertex41core =
value = (abs_value - min_value) / (max_value - min_value); value = (abs_value - min_value) / (max_value - min_value);
} }
float red = value; float red = value;
float green = 1 - abs(1 - 4 * value); float green = 1.0 - abs(1.0 - 4.0 * value);
if(value > 0.375) if(value > 0.375)
{ {
green = 0.5; green = 0.5;
} }
float blue = max(1 - 4 * value, 0); float blue = max(1.0 - 4.0 * value, 0.0);
return vec4(red, green, blue, 1.0); return vec4(red, green, blue, 1.0);
} }
float clamp(float v) float clamp(float v)
{ {
float t = v < 0 ? 0 : v; float t = v < 0.0 ? 0.0 : v;
return t > 1.0 ? 1.0 : t; return t > 1.0 ? 1.0 : t;
} }

View File

@ -2,7 +2,7 @@
"name": "Simulation View", "name": "Simulation View",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the Simulation view.", "description": "Provides the preview of sliced layerdata.",
"api": 7, "api": 7,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -130,6 +130,7 @@ class SliceInfo(QObject, Extension):
data["cura_version"] = self._application.getVersion() data["cura_version"] = self._application.getVersion()
data["cura_build_type"] = ApplicationMetadata.CuraBuildType data["cura_build_type"] = ApplicationMetadata.CuraBuildType
org_id = user_profile.get("organization_id", None) if user_profile else None 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["organization_id"] = org_id if org_id else None
data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else [] data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else []

View File

@ -7,6 +7,7 @@
<b>Intent Profile:</b> Default<br/> <b>Intent Profile:</b> Default<br/>
<b>Quality Profile:</b> Fast<br/> <b>Quality Profile:</b> Fast<br/>
<b>Using Custom Settings:</b> No<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>Organization ID (if any):</b> ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=<br/>
<b>Subscriptions (if any):</b> <b>Subscriptions (if any):</b>
<ul> <ul>

View File

@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QApplication
from UM.Application import Application from UM.Application import Application
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Tool import Tool from UM.Tool import Tool
from UM.Event import Event, MouseEvent from UM.Event import Event, MouseEvent
from UM.Mesh.MeshBuilder import MeshBuilder 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 # 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(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
op.addOperation(SetParentOperation(node, parent)) op.addOperation(SetParentOperation(node, parent))
op.addOperation(TranslateOperation(node, position, set_position = True))
op.push() op.push()
node.setPosition(position, CuraSceneNode.TransformSpace.World)
CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)

View File

@ -66,7 +66,7 @@ class CloudPackageChecker(QObject):
self._application.getHttpRequestManager().get(url, self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished, callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished, error_callback = self._onUserPackagesRequestFinished,
timeout=10, timeout = 10,
scope = self._scope) scope = self._scope)
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:

View File

@ -1,3 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os import os
from collections import OrderedDict from collections import OrderedDict
from typing import Dict, Optional, List, Any from typing import Dict, Optional, List, Any
@ -95,7 +98,11 @@ class LicensePresenter(QObject):
for package_id, item in packages.items(): for package_id, item in packages.items():
item["package_id"] = package_id 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: if item["licence_content"] is None:
# Implicitly accept when there is no license # Implicitly accept when there is no license
item["accepted"] = True item["accepted"] = True

View File

@ -682,9 +682,13 @@ class Toolbox(QObject, Extension):
if not package_info: if not package_info:
Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path) Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
return return
license_content = self._package_manager.getPackageLicense(file_path)
package_id = package_info["package_id"] 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: if license_content is not None:
# get the icon url for package_id, make sure the result is a string, never 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 "" icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or ""

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.", "description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["7.8.0"], "supported_sdk_versions": ["7.9.0"],
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -73,6 +73,8 @@ Item
switch (printJob.state) switch (printJob.state)
{ {
case "wait_cleanup": case "wait_cleanup":
// This hack was removed previously. Then we found out that we don't get back 'aborted_wait_cleanup'
// for the UM2+C it seems. Will communicate this to other teams, in the mean time, put this back.
if (printJob.timeTotal > printJob.timeElapsed) if (printJob.timeTotal > printJob.timeElapsed)
{ {
return catalog.i18nc("@label:status", "Aborted"); return catalog.i18nc("@label:status", "Aborted");
@ -88,6 +90,20 @@ Item
return catalog.i18nc("@label:status", "Aborting..."); return catalog.i18nc("@label:status", "Aborting...");
case "aborted": // NOTE: Unused, see above case "aborted": // NOTE: Unused, see above
return catalog.i18nc("@label:status", "Aborted"); return catalog.i18nc("@label:status", "Aborted");
case "aborted_post_print":
return catalog.i18nc("@label:status", "Aborted");
case "aborted_wait_user_action":
return catalog.i18nc("@label:status", "Aborted");
case "aborted_wait_cleanup":
return catalog.i18nc("@label:status", "Aborted");
case "failed":
return catalog.i18nc("@label:status", "Failed");
case "failed_post_print":
return catalog.i18nc("@label:status", "Failed");
case "failed_wait_user_action":
return catalog.i18nc("@label:status", "Failed");
case "failed_wait_cleanup":
return catalog.i18nc("@label:status", "Failed");
case "pausing": case "pausing":
return catalog.i18nc("@label:status", "Pausing..."); return catalog.i18nc("@label:status", "Pausing...");
case "paused": case "paused":
@ -97,7 +113,7 @@ Item
case "queued": case "queued":
return catalog.i18nc("@label:status", "Action required"); return catalog.i18nc("@label:status", "Action required");
default: 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 width: contentWidth

View 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

View File

@ -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

View 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. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
@ -16,9 +16,10 @@ from UM.Util import parseBool
from cura.API import Account from cura.API import Account
from cura.API.Account import SyncState from cura.API.Account import SyncState
from cura.CuraApplication import CuraApplication 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.CuraStackBuilder import CuraStackBuilder
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT
from .CloudApiClient import CloudApiClient from .CloudApiClient import CloudApiClient
from .CloudOutputDevice import CloudOutputDevice from .CloudOutputDevice import CloudOutputDevice
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
@ -127,8 +128,12 @@ class CloudOutputDeviceManager:
# to the current account # to the current account
if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
if not self._um_cloud_printers[device_id].getMetaDataEntry(META_CAPABILITIES, None):
self._um_cloud_printers[device_id].setMetaDataEntry(META_CAPABILITIES, ",".join(cluster_data.capabilities))
self._onDevicesDiscovered(new_clusters) self._onDevicesDiscovered(new_clusters)
self._updateOnlinePrinters(all_clusters)
# Hide the current removed_printers_message, if there is any # Hide the current removed_printers_message, if there is any
if self._removed_printers_message: if self._removed_printers_message:
self._removed_printers_message.actionTriggered.disconnect(self._onRemovedPrintersMessageActionTriggered) self._removed_printers_message.actionTriggered.disconnect(self._onRemovedPrintersMessageActionTriggered)
@ -154,6 +159,8 @@ class CloudOutputDeviceManager:
self._syncing = False self._syncing = False
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) 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: def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
self._syncing = False self._syncing = False
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
@ -255,6 +262,16 @@ class CloudOutputDeviceManager:
message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>" message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>"
message.setText(message_text) 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: 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 Update the cloud metadata of a pre-existing machine that is rediscovered (e.g. if the printer was removed and

View File

@ -15,27 +15,26 @@ I18N_CATALOG = i18nCatalog("cura")
class CloudFlowMessage(Message): class CloudFlowMessage(Message):
def __init__(self, address: str) -> None: def __init__(self, printer_name: str) -> None:
image_path = os.path.join( image_path = os.path.join(
CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "", CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
"resources", "svg", "cloud-flow-start.svg" "resources", "svg", "CloudPlatform.svg"
) )
super().__init__( super().__init__(
text=I18N_CATALOG.i18nc("@info:status", text = I18N_CATALOG.i18nc("@info:status",
"Send and monitor print jobs from anywhere using your Ultimaker account."), 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"),
lifetime=0, title = I18N_CATALOG.i18nc("@info:title", "Are you ready for cloud printing?"),
dismissable=True, image_source = QUrl.fromLocalFile(image_path)
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"),
) )
self._address = address self._printer_name = printer_name
self.addAction("", I18N_CATALOG.i18nc("@action", "Get started"), "", "") 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) self.actionTriggered.connect(self._onCloudFlowStarted)
def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: def _onCloudFlowStarted(self, message_id: str, action_id: str) -> None:
QDesktopServices.openUrl(QUrl("http://{}/cloud_connect".format(self._address))) if action_id == "get_started":
self.hide() 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"))

View File

@ -37,7 +37,7 @@ class CloudClusterResponse(BaseModel):
self.friendly_name = friendly_name self.friendly_name = friendly_name
self.printer_type = printer_type self.printer_type = printer_type
self.printer_count = printer_count self.printer_count = printer_count
self.capabilities = capabilities self.capabilities = capabilities if capabilities is not None else []
super().__init__(**kwargs) super().__init__(**kwargs)
# Validates the model, raising an exception if the model is invalid. # Validates the model, raising an exception if the model is invalid.
@ -45,3 +45,10 @@ class CloudClusterResponse(BaseModel):
super().validate() super().validate()
if not self.cluster_id: if not self.cluster_id:
raise ValueError("cluster_id is required on CloudCluster") raise ValueError("cluster_id is required on CloudCluster")
def __repr__(self) -> str:
"""
Convenience function for printing when debugging.
:return: A human-readable representation of the data in this object.
"""
return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}})

View File

@ -40,7 +40,7 @@ class ClusterPrintJobStatus(BaseModel):
configuration_changes_required: List[ configuration_changes_required: List[
Union[Dict[str, Any], ClusterPrintJobConfigurationChange]] = None, Union[Dict[str, Any], ClusterPrintJobConfigurationChange]] = None,
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
compatible_machine_families: List[str] = None, compatible_machine_families: Optional[List[str]] = None,
impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None, impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None,
preview_url: Optional[str] = None, preview_url: Optional[str] = None,
**kwargs) -> None: **kwargs) -> None:
@ -97,7 +97,7 @@ class ClusterPrintJobStatus(BaseModel):
configuration_changes_required) \ configuration_changes_required) \
if configuration_changes_required else [] if configuration_changes_required else []
self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None
self.compatible_machine_families = compatible_machine_families if compatible_machine_families else [] self.compatible_machine_families = compatible_machine_families if compatible_machine_families is not None else []
self.impediments_to_printing = self.parseModels(ClusterPrintJobImpediment, impediments_to_printing) \ self.impediments_to_printing = self.parseModels(ClusterPrintJobImpediment, impediments_to_printing) \
if impediments_to_printing else [] if impediments_to_printing else []
@ -130,8 +130,10 @@ class ClusterPrintJobStatus(BaseModel):
model.updateConfiguration(self._createConfigurationModel()) model.updateConfiguration(self._createConfigurationModel())
model.updateTimeTotal(self.time_total) model.updateTimeTotal(self.time_total)
model.updateTimeElapsed(self.time_elapsed) if self.time_elapsed is not None:
model.updateOwner(self.owner) model.updateTimeElapsed(self.time_elapsed)
if self.owner is not None:
model.updateOwner(self.owner)
model.updateState(self.status) model.updateState(self.status)
model.setCompatibleMachineFamilies(self.compatible_machine_families) model.setCompatibleMachineFamilies(self.compatible_machine_families)

View File

@ -52,7 +52,6 @@ class LocalClusterOutputDeviceManager:
def start(self) -> None: def start(self) -> None:
"""Start the network discovery.""" """Start the network discovery."""
self._zero_conf_client.start() self._zero_conf_client.start()
for address in self._getStoredManualAddresses(): for address in self._getStoredManualAddresses():
self.addManualDevice(address) self.addManualDevice(address)
@ -292,4 +291,4 @@ class LocalClusterOutputDeviceManager:
if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
# Do not show the message if the user is not signed in. # Do not show the message if the user is not signed in.
return return
CloudFlowMessage(device.ipAddress).show() CloudFlowMessage(device.name).show()

View File

@ -3,6 +3,7 @@
import configparser import configparser
import io import io
import json
import os.path import os.path
from typing import List, Tuple from typing import List, Tuple
@ -49,6 +50,28 @@ class VersionUpgrade411to412(VersionUpgrade):
# Update version number. # Update version number.
parser["metadata"]["setting_version"] = "19" parser["metadata"]["setting_version"] = "19"
# If the account scope in 4.11 is outdated, delete it so that the user is enforced to log in again and get the
# correct permissions.
new_scopes = {"account.user.read",
"drive.backup.read",
"drive.backup.write",
"packages.download",
"packages.rating.read",
"packages.rating.write",
"connect.cluster.read",
"connect.cluster.write",
"library.project.read",
"library.project.write",
"cura.printjob.read",
"cura.printjob.write",
"cura.mesh.read",
"cura.mesh.write",
"cura.material.write"}
if "ultimaker_auth_data" in parser["general"]:
ultimaker_auth_data = json.loads(parser["general"]["ultimaker_auth_data"])
if new_scopes - set(ultimaker_auth_data["scope"].split(" ")):
parser["general"]["ultimaker_auth_data"] = "{}"
result = io.StringIO() result = io.StringIO()
parser.write(result) parser.write(result)
return [filename], [result.getvalue()] return [filename], [result.getvalue()]

View File

@ -0,0 +1,118 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
from typing import Tuple, List
import io
from UM.VersionUpgrade import VersionUpgrade
_removed_settings = {
"travel_compensate_overlapping_walls_enabled",
"travel_compensate_overlapping_walls_0_enabled",
"travel_compensate_overlapping_walls_x_enabled",
"fill_perimeter_gaps",
"wall_min_flow",
"wall_min_flow_retract",
"speed_equalize_flow_enabled",
"speed_equalize_flow_min"
}
class VersionUpgrade49to50(VersionUpgrade):
def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades preferences to remove from the visibility list the settings that were removed in this version.
It also changes the preferences to have the new version number.
This removes any settings that were removed in the new Cura version.
:param serialized: The original contents of the preferences file.
:param filename: The file name of the preferences file.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = "18"
# Remove deleted settings from the visible settings list.
if "general" in parser and "visible_settings" in parser["general"]:
visible_settings = set(parser["general"]["visible_settings"].split(";"))
for removed in _removed_settings:
if removed in visible_settings:
visible_settings.remove(removed)
# Replace Outer Before Inner Walls with equivalent.
if "outer_inset_first" in visible_settings:
visible_settings.remove("outer_inset_first")
visible_settings.add("inset_direction")
parser["general"]["visible_settings"] = ";".join(visible_settings)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades instance containers to remove the settings that were removed in this version.
It also changes the instance containers to have the new version number.
This removes any settings that were removed in the new Cura version and updates settings that need to be updated
with a new value.
:param serialized: The original contents of the instance container.
:param filename: The original file name of the instance container.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized)
# Update version number.
parser["metadata"]["setting_version"] = "18"
if "values" in parser:
# Remove deleted settings from the instance containers.
for removed in _removed_settings:
if removed in parser["values"]:
del parser["values"][removed]
# Replace Outer Before Inner Walls with equivalent setting.
if "outer_inset_first" in parser["values"]:
old_value = parser["values"]["outer_inset_first"]
if old_value.startswith("="): # Was already a formula.
old_value = old_value[1:]
parser["values"]["inset_direction"] = f"='outside_in' if ({old_value}) else 'inside_out'" # Makes it work both with plain setting values and formulas.
# Disable Fuzzy Skin as it doesn't work with with the libArachne walls
if "magic_fuzzy_skin_enabled" in parser["values"]:
parser["values"]["magic_fuzzy_skin_enabled"] = "False"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""
Upgrades stacks to have the new version number.
:param serialized: The original contents of the stack.
:param filename: The original file name of the stack.
:return: A list of new file names, and a list of the new contents for
those files.
"""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["general"]["version"] = "5"
parser["metadata"]["setting_version"] = "18"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View File

@ -0,0 +1,61 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade49to50
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade49to50.VersionUpgrade49to50()
def getMetaData() -> Dict[str, Any]:
return { # Since there is no VersionUpgrade from 48 to 49 yet, upgrade the 48 profiles to 50.
"version_upgrade": {
# From To Upgrade function
("preferences", 6000016): ("preferences", 6000018, upgrade.upgradePreferences),
("machine_stack", 5000016): ("machine_stack", 5000018, upgrade.upgradeStack),
("extruder_train", 5000016): ("extruder_train", 5000018, upgrade.upgradeStack),
("machine_stack", 4000018): ("machine_stack", 5000018, upgrade.upgradeStack), # We made a mistake in the arachne beta 1
("extruder_train", 4000018): ("extruder_train", 5000018, upgrade.upgradeStack), # We made a mistake in the arachne beta 1
("definition_changes", 4000016): ("definition_changes", 4000018, upgrade.upgradeInstanceContainer),
("quality_changes", 4000016): ("quality_changes", 4000018, upgrade.upgradeInstanceContainer),
("quality", 4000016): ("quality", 4000018, upgrade.upgradeInstanceContainer),
("user", 4000016): ("user", 4000018, upgrade.upgradeInstanceContainer),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View File

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 4.9 to 5.0",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.9 to Cura 5.0.",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -29,7 +29,7 @@ pywin32==301
requests==2.22.0 requests==2.22.0
scipy==1.6.2 scipy==1.6.2
sentry-sdk==0.13.5 sentry-sdk==0.13.5
shapely[vectorized]==1.7.1 shapely[vectorized]==1.8.0
six==1.12.0 six==1.12.0
trimesh==3.2.33 trimesh==3.2.33
twisted==21.2.0 twisted==21.2.0

View File

@ -6,7 +6,7 @@
"display_name": "3MF Reader", "display_name": "3MF Reader",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -23,7 +23,7 @@
"display_name": "3MF Writer", "display_name": "3MF Writer",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -40,7 +40,7 @@
"display_name": "AMF Reader", "display_name": "AMF Reader",
"description": "Provides support for reading AMF files.", "description": "Provides support for reading AMF files.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "fieldOfView", "author_id": "fieldOfView",
@ -57,7 +57,7 @@
"display_name": "Cura Backups", "display_name": "Cura Backups",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"package_version": "1.2.0", "package_version": "1.2.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -74,7 +74,7 @@
"display_name": "CuraEngine Backend", "display_name": "CuraEngine Backend",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -91,7 +91,7 @@
"display_name": "Cura Profile Reader", "display_name": "Cura Profile Reader",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -108,7 +108,7 @@
"display_name": "Cura Profile Writer", "display_name": "Cura Profile Writer",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -125,7 +125,7 @@
"display_name": "Ultimaker Digital Library", "display_name": "Ultimaker Digital Library",
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.", "description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
"package_version": "1.1.0", "package_version": "1.1.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -142,7 +142,7 @@
"display_name": "Firmware Update Checker", "display_name": "Firmware Update Checker",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -159,7 +159,7 @@
"display_name": "Firmware Updater", "display_name": "Firmware Updater",
"description": "Provides a machine actions for updating firmware.", "description": "Provides a machine actions for updating firmware.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -176,7 +176,7 @@
"display_name": "Compressed G-code Reader", "display_name": "Compressed G-code Reader",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -193,7 +193,7 @@
"display_name": "Compressed G-code Writer", "display_name": "Compressed G-code Writer",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -210,7 +210,7 @@
"display_name": "G-Code Profile Reader", "display_name": "G-Code Profile Reader",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -227,7 +227,7 @@
"display_name": "G-Code Reader", "display_name": "G-Code Reader",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "VictorLarchenko", "author_id": "VictorLarchenko",
@ -244,7 +244,7 @@
"display_name": "G-Code Writer", "display_name": "G-Code Writer",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -261,7 +261,7 @@
"display_name": "Image Reader", "display_name": "Image Reader",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -278,7 +278,7 @@
"display_name": "Legacy Cura Profile Reader", "display_name": "Legacy Cura Profile Reader",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -295,7 +295,7 @@
"display_name": "Machine Settings Action", "display_name": "Machine Settings Action",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "fieldOfView", "author_id": "fieldOfView",
@ -312,7 +312,7 @@
"display_name": "Model Checker", "display_name": "Model Checker",
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -329,7 +329,7 @@
"display_name": "Monitor Stage", "display_name": "Monitor Stage",
"description": "Provides a monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -346,7 +346,7 @@
"display_name": "Per-Object Settings Tool", "display_name": "Per-Object Settings Tool",
"description": "Provides the per-model settings.", "description": "Provides the per-model settings.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -363,7 +363,7 @@
"display_name": "Post Processing", "display_name": "Post Processing",
"description": "Extension that allows for user created scripts for post processing.", "description": "Extension that allows for user created scripts for post processing.",
"package_version": "2.2.1", "package_version": "2.2.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -380,7 +380,7 @@
"display_name": "Prepare Stage", "display_name": "Prepare Stage",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -397,7 +397,7 @@
"display_name": "Preview Stage", "display_name": "Preview Stage",
"description": "Provides a preview stage in Cura.", "description": "Provides a preview stage in Cura.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -414,7 +414,7 @@
"display_name": "Removable Drive Output Device", "display_name": "Removable Drive Output Device",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -431,7 +431,7 @@
"display_name": "Sentry Logger", "display_name": "Sentry Logger",
"description": "Logs certain events so that they can be used by the crash reporter", "description": "Logs certain events so that they can be used by the crash reporter",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -448,7 +448,7 @@
"display_name": "Simulation View", "display_name": "Simulation View",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -465,7 +465,7 @@
"display_name": "Slice Info", "display_name": "Slice Info",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -482,7 +482,7 @@
"display_name": "Solid View", "display_name": "Solid View",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -499,7 +499,7 @@
"display_name": "Support Eraser Tool", "display_name": "Support Eraser Tool",
"description": "Creates an eraser mesh to block the printing of support in certain places.", "description": "Creates an eraser mesh to block the printing of support in certain places.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -516,7 +516,7 @@
"display_name": "Trimesh Reader", "display_name": "Trimesh Reader",
"description": "Provides support for reading model files.", "description": "Provides support for reading model files.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -533,7 +533,7 @@
"display_name": "Toolbox", "display_name": "Toolbox",
"description": "Find, manage and install new Cura packages.", "description": "Find, manage and install new Cura packages.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -550,7 +550,7 @@
"display_name": "UFP Reader", "display_name": "UFP Reader",
"description": "Provides support for reading Ultimaker Format Packages.", "description": "Provides support for reading Ultimaker Format Packages.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -567,7 +567,7 @@
"display_name": "UFP Writer", "display_name": "UFP Writer",
"description": "Provides support for writing Ultimaker Format Packages.", "description": "Provides support for writing Ultimaker Format Packages.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -584,7 +584,7 @@
"display_name": "Ultimaker Machine Actions", "display_name": "Ultimaker Machine Actions",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -601,7 +601,7 @@
"display_name": "UM3 Network Printing", "display_name": "UM3 Network Printing",
"description": "Manages network connections to Ultimaker 3 printers.", "description": "Manages network connections to Ultimaker 3 printers.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -618,7 +618,7 @@
"display_name": "USB Printing", "display_name": "USB Printing",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.", "description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"package_version": "1.0.2", "package_version": "1.0.2",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -635,7 +635,7 @@
"display_name": "Version Upgrade 2.1 to 2.2", "display_name": "Version Upgrade 2.1 to 2.2",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.", "description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -652,7 +652,7 @@
"display_name": "Version Upgrade 2.2 to 2.4", "display_name": "Version Upgrade 2.2 to 2.4",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.", "description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -669,7 +669,7 @@
"display_name": "Version Upgrade 2.5 to 2.6", "display_name": "Version Upgrade 2.5 to 2.6",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.", "description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -686,7 +686,7 @@
"display_name": "Version Upgrade 2.6 to 2.7", "display_name": "Version Upgrade 2.6 to 2.7",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.", "description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -703,7 +703,7 @@
"display_name": "Version Upgrade 2.7 to 3.0", "display_name": "Version Upgrade 2.7 to 3.0",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.", "description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -720,7 +720,7 @@
"display_name": "Version Upgrade 3.0 to 3.1", "display_name": "Version Upgrade 3.0 to 3.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.", "description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -737,7 +737,7 @@
"display_name": "Version Upgrade 3.2 to 3.3", "display_name": "Version Upgrade 3.2 to 3.3",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.", "description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -754,7 +754,7 @@
"display_name": "Version Upgrade 3.3 to 3.4", "display_name": "Version Upgrade 3.3 to 3.4",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.", "description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -771,7 +771,7 @@
"display_name": "Version Upgrade 3.4 to 3.5", "display_name": "Version Upgrade 3.4 to 3.5",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.", "description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -788,7 +788,7 @@
"display_name": "Version Upgrade 3.5 to 4.0", "display_name": "Version Upgrade 3.5 to 4.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.", "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -805,7 +805,7 @@
"display_name": "Version Upgrade 4.0 to 4.1", "display_name": "Version Upgrade 4.0 to 4.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.", "description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -822,7 +822,7 @@
"display_name": "Version Upgrade 4.1 to 4.2", "display_name": "Version Upgrade 4.1 to 4.2",
"description": "Upgrades configurations from Cura 4.1 to Cura 4.2.", "description": "Upgrades configurations from Cura 4.1 to Cura 4.2.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -839,7 +839,7 @@
"display_name": "Version Upgrade 4.2 to 4.3", "display_name": "Version Upgrade 4.2 to 4.3",
"description": "Upgrades configurations from Cura 4.2 to Cura 4.3.", "description": "Upgrades configurations from Cura 4.2 to Cura 4.3.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -856,7 +856,7 @@
"display_name": "Version Upgrade 4.3 to 4.4", "display_name": "Version Upgrade 4.3 to 4.4",
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.", "description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -873,7 +873,7 @@
"display_name": "Version Upgrade 4.4 to 4.5", "display_name": "Version Upgrade 4.4 to 4.5",
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.", "description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -890,7 +890,7 @@
"display_name": "Version Upgrade 4.5 to 4.6", "display_name": "Version Upgrade 4.5 to 4.6",
"description": "Upgrades configurations from Cura 4.5 to Cura 4.6.", "description": "Upgrades configurations from Cura 4.5 to Cura 4.6.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -907,7 +907,7 @@
"display_name": "Version Upgrade 4.6.0 to 4.6.2", "display_name": "Version Upgrade 4.6.0 to 4.6.2",
"description": "Upgrades configurations from Cura 4.6.0 to Cura 4.6.2.", "description": "Upgrades configurations from Cura 4.6.0 to Cura 4.6.2.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -924,7 +924,7 @@
"display_name": "Version Upgrade 4.6.2 to 4.7", "display_name": "Version Upgrade 4.6.2 to 4.7",
"description": "Upgrades configurations from Cura 4.6.2 to Cura 4.7.", "description": "Upgrades configurations from Cura 4.6.2 to Cura 4.7.",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -941,7 +941,7 @@
"display_name": "Version Upgrade 4.7.0 to 4.8.0", "display_name": "Version Upgrade 4.7.0 to 4.8.0",
"description": "Upgrades configurations from Cura 4.7.0 to Cura 4.8.0", "description": "Upgrades configurations from Cura 4.7.0 to Cura 4.8.0",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -958,7 +958,7 @@
"display_name": "Version Upgrade 4.8.0 to 4.9.0", "display_name": "Version Upgrade 4.8.0 to 4.9.0",
"description": "Upgrades configurations from Cura 4.8.0 to Cura 4.9.0", "description": "Upgrades configurations from Cura 4.8.0 to Cura 4.9.0",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -976,7 +976,7 @@
"description": "Upgrades configurations from Cura 4.9 to Cura 4.10", "description": "Upgrades configurations from Cura 4.9 to Cura 4.10",
"package_version": "1.0.0", "package_version": "1.0.0",
"sdk_version": 7, "sdk_version": 7,
"sdk_version_semver": "7.8.0", "sdk_version_semver": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1011,7 +1011,7 @@
"display_name": "X3D Reader", "display_name": "X3D Reader",
"description": "Provides support for reading X3D files.", "description": "Provides support for reading X3D files.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "SevaAlekseyev", "author_id": "SevaAlekseyev",
@ -1028,7 +1028,7 @@
"display_name": "XML Material Profiles", "display_name": "XML Material Profiles",
"description": "Provides capabilities to read and write XML-based material profiles.", "description": "Provides capabilities to read and write XML-based material profiles.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1045,7 +1045,7 @@
"display_name": "X-Ray View", "display_name": "X-Ray View",
"description": "Provides the X-Ray view.", "description": "Provides the X-Ray view.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com", "website": "https://ultimaker.com",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1062,7 +1062,7 @@
"display_name": "Generic ABS", "display_name": "Generic ABS",
"description": "The generic ABS profile which other profiles can be based upon.", "description": "The generic ABS profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1080,7 +1080,7 @@
"display_name": "Generic BAM", "display_name": "Generic BAM",
"description": "The generic BAM profile which other profiles can be based upon.", "description": "The generic BAM profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1098,7 +1098,7 @@
"display_name": "Generic CFF CPE", "display_name": "Generic CFF CPE",
"description": "The generic CFF CPE profile which other profiles can be based upon.", "description": "The generic CFF CPE profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1116,7 +1116,7 @@
"display_name": "Generic CFF PA", "display_name": "Generic CFF PA",
"description": "The generic CFF PA profile which other profiles can be based upon.", "description": "The generic CFF PA profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1134,7 +1134,7 @@
"display_name": "Generic CPE", "display_name": "Generic CPE",
"description": "The generic CPE profile which other profiles can be based upon.", "description": "The generic CPE profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1152,7 +1152,7 @@
"display_name": "Generic CPE+", "display_name": "Generic CPE+",
"description": "The generic CPE+ profile which other profiles can be based upon.", "description": "The generic CPE+ profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1170,7 +1170,7 @@
"display_name": "Generic GFF CPE", "display_name": "Generic GFF CPE",
"description": "The generic GFF CPE profile which other profiles can be based upon.", "description": "The generic GFF CPE profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1188,7 +1188,7 @@
"display_name": "Generic GFF PA", "display_name": "Generic GFF PA",
"description": "The generic GFF PA profile which other profiles can be based upon.", "description": "The generic GFF PA profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1206,7 +1206,7 @@
"display_name": "Generic HIPS", "display_name": "Generic HIPS",
"description": "The generic HIPS profile which other profiles can be based upon.", "description": "The generic HIPS profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1224,7 +1224,7 @@
"display_name": "Generic Nylon", "display_name": "Generic Nylon",
"description": "The generic Nylon profile which other profiles can be based upon.", "description": "The generic Nylon profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1242,7 +1242,7 @@
"display_name": "Generic PC", "display_name": "Generic PC",
"description": "The generic PC profile which other profiles can be based upon.", "description": "The generic PC profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1260,7 +1260,7 @@
"display_name": "Generic PETG", "display_name": "Generic PETG",
"description": "The generic PETG profile which other profiles can be based upon.", "description": "The generic PETG profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1278,7 +1278,7 @@
"display_name": "Generic PLA", "display_name": "Generic PLA",
"description": "The generic PLA profile which other profiles can be based upon.", "description": "The generic PLA profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1296,7 +1296,7 @@
"display_name": "Generic PP", "display_name": "Generic PP",
"description": "The generic PP profile which other profiles can be based upon.", "description": "The generic PP profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1314,7 +1314,7 @@
"display_name": "Generic PVA", "display_name": "Generic PVA",
"description": "The generic PVA profile which other profiles can be based upon.", "description": "The generic PVA profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1332,7 +1332,7 @@
"display_name": "Generic Tough PLA", "display_name": "Generic Tough PLA",
"description": "The generic Tough PLA profile which other profiles can be based upon.", "description": "The generic Tough PLA profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1350,7 +1350,7 @@
"display_name": "Generic TPU", "display_name": "Generic TPU",
"description": "The generic TPU profile which other profiles can be based upon.", "description": "The generic TPU profile which other profiles can be based upon.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://github.com/Ultimaker/fdm_materials", "website": "https://github.com/Ultimaker/fdm_materials",
"author": { "author": {
"author_id": "Generic", "author_id": "Generic",
@ -1368,7 +1368,7 @@
"display_name": "Dagoma Chromatik PLA", "display_name": "Dagoma Chromatik PLA",
"description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.", "description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://dagoma.fr/boutique/filaments.html", "website": "https://dagoma.fr/boutique/filaments.html",
"author": { "author": {
"author_id": "Dagoma", "author_id": "Dagoma",
@ -1385,7 +1385,7 @@
"display_name": "FABtotum ABS", "display_name": "FABtotum ABS",
"description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so its compatible with all versions of the FABtotum Personal fabricator.", "description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so its compatible with all versions of the FABtotum Personal fabricator.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1402,7 +1402,7 @@
"display_name": "FABtotum Nylon", "display_name": "FABtotum Nylon",
"description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.", "description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1419,7 +1419,7 @@
"display_name": "FABtotum PLA", "display_name": "FABtotum PLA",
"description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starchs sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotums one is between 185° and 195°.", "description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starchs sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotums one is between 185° and 195°.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1436,7 +1436,7 @@
"display_name": "FABtotum TPU Shore 98A", "display_name": "FABtotum TPU Shore 98A",
"description": "", "description": "",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66", "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66",
"author": { "author": {
"author_id": "FABtotum", "author_id": "FABtotum",
@ -1453,7 +1453,7 @@
"display_name": "Fiberlogy HD PLA", "display_name": "Fiberlogy HD PLA",
"description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.", "description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/", "website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/",
"author": { "author": {
"author_id": "Fiberlogy", "author_id": "Fiberlogy",
@ -1470,7 +1470,7 @@
"display_name": "Filo3D PLA", "display_name": "Filo3D PLA",
"description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.", "description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://dagoma.fr", "website": "https://dagoma.fr",
"author": { "author": {
"author_id": "Dagoma", "author_id": "Dagoma",
@ -1487,7 +1487,7 @@
"display_name": "IMADE3D JellyBOX PETG", "display_name": "IMADE3D JellyBOX PETG",
"description": "", "description": "",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://shop.imade3d.com/filament.html", "website": "http://shop.imade3d.com/filament.html",
"author": { "author": {
"author_id": "IMADE3D", "author_id": "IMADE3D",
@ -1504,7 +1504,7 @@
"display_name": "IMADE3D JellyBOX PLA", "display_name": "IMADE3D JellyBOX PLA",
"description": "", "description": "",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://shop.imade3d.com/filament.html", "website": "http://shop.imade3d.com/filament.html",
"author": { "author": {
"author_id": "IMADE3D", "author_id": "IMADE3D",
@ -1521,7 +1521,7 @@
"display_name": "Octofiber PLA", "display_name": "Octofiber PLA",
"description": "PLA material from Octofiber.", "description": "PLA material from Octofiber.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://nl.octofiber.com/3d-printing-filament/pla.html", "website": "https://nl.octofiber.com/3d-printing-filament/pla.html",
"author": { "author": {
"author_id": "Octofiber", "author_id": "Octofiber",
@ -1538,7 +1538,7 @@
"display_name": "PolyFlex™ PLA", "display_name": "PolyFlex™ PLA",
"description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.", "description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://www.polymaker.com/shop/polyflex/", "website": "http://www.polymaker.com/shop/polyflex/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1555,7 +1555,7 @@
"display_name": "PolyMax™ PLA", "display_name": "PolyMax™ PLA",
"description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.", "description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://www.polymaker.com/shop/polymax/", "website": "http://www.polymaker.com/shop/polymax/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1572,7 +1572,7 @@
"display_name": "PolyPlus™ PLA True Colour", "display_name": "PolyPlus™ PLA True Colour",
"description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.", "description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://www.polymaker.com/shop/polyplus-true-colour/", "website": "http://www.polymaker.com/shop/polyplus-true-colour/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1589,7 +1589,7 @@
"display_name": "PolyWood™ PLA", "display_name": "PolyWood™ PLA",
"description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.", "description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.",
"package_version": "1.0.1", "package_version": "1.0.1",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "http://www.polymaker.com/shop/polywood/", "website": "http://www.polymaker.com/shop/polywood/",
"author": { "author": {
"author_id": "Polymaker", "author_id": "Polymaker",
@ -1606,7 +1606,7 @@
"display_name": "Ultimaker ABS", "display_name": "Ultimaker ABS",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1625,7 +1625,7 @@
"display_name": "Ultimaker Breakaway", "display_name": "Ultimaker Breakaway",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/breakaway", "website": "https://ultimaker.com/products/materials/breakaway",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1644,7 +1644,7 @@
"display_name": "Ultimaker CPE", "display_name": "Ultimaker CPE",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1663,7 +1663,7 @@
"display_name": "Ultimaker CPE+", "display_name": "Ultimaker CPE+",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/cpe", "website": "https://ultimaker.com/products/materials/cpe",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1682,7 +1682,7 @@
"display_name": "Ultimaker Nylon", "display_name": "Ultimaker Nylon",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1701,7 +1701,7 @@
"display_name": "Ultimaker PC", "display_name": "Ultimaker PC",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/pc", "website": "https://ultimaker.com/products/materials/pc",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1720,7 +1720,7 @@
"display_name": "Ultimaker PLA", "display_name": "Ultimaker PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1739,7 +1739,7 @@
"display_name": "Ultimaker PP", "display_name": "Ultimaker PP",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/pp", "website": "https://ultimaker.com/products/materials/pp",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1758,7 +1758,7 @@
"display_name": "Ultimaker PVA", "display_name": "Ultimaker PVA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/abs", "website": "https://ultimaker.com/products/materials/abs",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1777,7 +1777,7 @@
"display_name": "Ultimaker TPU 95A", "display_name": "Ultimaker TPU 95A",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/tpu-95a", "website": "https://ultimaker.com/products/materials/tpu-95a",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1796,7 +1796,7 @@
"display_name": "Ultimaker Tough PLA", "display_name": "Ultimaker Tough PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.", "description": "Example package for material and quality profiles for Ultimaker materials.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://ultimaker.com/products/materials/tough-pla", "website": "https://ultimaker.com/products/materials/tough-pla",
"author": { "author": {
"author_id": "UltimakerPackages", "author_id": "UltimakerPackages",
@ -1815,7 +1815,7 @@
"display_name": "Vertex Delta ABS", "display_name": "Vertex Delta ABS",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1832,7 +1832,7 @@
"display_name": "Vertex Delta PET", "display_name": "Vertex Delta PET",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1849,7 +1849,7 @@
"display_name": "Vertex Delta PLA", "display_name": "Vertex Delta PLA",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",
@ -1866,7 +1866,7 @@
"display_name": "Vertex Delta TPU", "display_name": "Vertex Delta TPU",
"description": "ABS material and quality files for the Delta Vertex K8800.", "description": "ABS material and quality files for the Delta Vertex K8800.",
"package_version": "1.4.0", "package_version": "1.4.0",
"sdk_version": "7.8.0", "sdk_version": "7.9.0",
"website": "https://vertex3dprinter.eu", "website": "https://vertex3dprinter.eu",
"author": { "author": {
"author_id": "Velleman", "author_id": "Velleman",

View File

@ -0,0 +1,49 @@
{
"version": 2,
"name": "3DI Base Printer",
"inherits": "fdmprinter",
"metadata": {
"visible": false,
"author": "Vaibhav Jain",
"manufacturer": "3Deometry Innovations",
"file_formats": "text/x-gcode",
"machine_extruder_trains":
{
"0": "3di_base_extruder_0"
}
},
"overrides": {
"machine_name":{
"default_value": "3DI Base Printer"
},
"machine_heated_bed": {
"default_value": true
},
"machine_width": {
"default_value": 220
},
"machine_height": {
"default_value": 220
},
"machine_depth": {
"default_value": 220
},
"machine_center_is_zero": {
"default_value": true
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 ;Home all axes (max endstops)\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..."
},
"machine_end_gcode": {
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG28 ;Home all axes (max endstops)\nM84 ;steppers off\nG90 ;absolute positioning"
},
"machine_shape": {
"default_value": "elliptic"
}
}
}

View File

@ -0,0 +1,26 @@
{
"version": 2,
"name": "3DI D300",
"inherits": "3di_base",
"metadata": {
"visible": true,
"platform": "3di_d300_platform.STL",
"platform_offset": [-200, -5, 173.205]
},
"overrides": {
"machine_name": {
"default_value": "3DI D300"
},
"machine_width": {
"default_value": 300
},
"machine_height": {
"default_value": 300
},
"machine_depth": {
"default_value": 300
}
}
}

View File

@ -74,7 +74,6 @@
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
@ -87,7 +86,6 @@
"infill_wipe_dist": { "value": 0 }, "infill_wipe_dist": { "value": 0 },
"wall_0_wipe_dist": { "value": 0.2 }, "wall_0_wipe_dist": { "value": 0.2 },
"fill_perimeter_gaps": { "value": "'everywhere'" },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": true }, "filter_out_tiny_gaps": { "value": true },

View File

@ -78,7 +78,6 @@
"material_initial_print_temperature": { "value": "material_print_temperature + 10" }, "material_initial_print_temperature": { "value": "material_print_temperature + 10" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"infill_sparse_density": { "value": 25 }, "infill_sparse_density": { "value": 25 },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
@ -91,21 +90,21 @@
"retraction_hop_enabled": { "value": true }, "retraction_hop_enabled": { "value": true },
"retraction_hop": { "value": 0.075 }, "retraction_hop": { "value": 0.075 },
"retraction_hop_only_when_collides": { "value": true }, "retraction_hop_only_when_collides": { "value": true },
"retraction_combing": { "value": "'off'" }, "retraction_combing": { "value": "'off'" },
"retraction_combing_max_distance": { "value": 30 }, "retraction_combing_max_distance": { "value": 30 },
"travel_avoid_other_parts": { "value": true }, "travel_avoid_other_parts": { "value": true },
"travel_avoid_supports": { "value": true }, "travel_avoid_supports": { "value": true },
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_amount": { "value": 6 }, "retraction_amount": { "value": 6 },
"retraction_enable": { "value": true }, "retraction_enable": { "value": true },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_fan_speed": { "value": 70 }, "cool_fan_speed": { "value": 70 },
"cool_fan_speed_0": { "value": 30 }, "cool_fan_speed_0": { "value": 30 },
"cool_fan_enabled": { "value": true }, "cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
@ -116,7 +115,6 @@
"skirt_line_count": { "value": 4 }, "skirt_line_count": { "value": 4 },
"meshfix_maximum_deviation": { "value": 0.05 }, "meshfix_maximum_deviation": { "value": 0.05 },
"support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" }, "support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" },
"support_pattern": { "value": "'zigzag'" }, "support_pattern": { "value": "'zigzag'" },
"support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" }, "support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" },
@ -124,14 +122,15 @@
"support_xy_distance": { "value": "wall_line_width_0 * 2" }, "support_xy_distance": { "value": "wall_line_width_0 * 2" },
"support_xy_distance_overhang": { "value": "wall_line_width_0" }, "support_xy_distance_overhang": { "value": "wall_line_width_0" },
"support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height * 2" }, "support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height * 2" },
"support_top_distance": { "value": "extruderValue(support_roof_extruder_nr if support_roof_enable else support_infill_extruder_nr, 'support_z_distance') + (layer_height if support_structure == 'tree' else 0)"},
"support_xy_overrides_z": { "value": "'xy_overrides_z'" }, "support_xy_overrides_z": { "value": "'xy_overrides_z'" },
"support_wall_count": { "value": 1 }, "support_wall_count": { "value": 1 },
"support_brim_enable": { "value": true }, "support_brim_enable": { "value": true },
"support_brim_width": { "value": 4 }, "support_brim_width": { "value": 4 },
"support_interface_enable": { "value": true }, "support_interface_enable": { "value": true },
"support_structure": { "value": "'tree'" }, "support_structure": { "value": "'tree'" },
"support_type": { "value": "'buildplate' if support_structure == 'tree' else 'everywhere'" }, "support_type": { "value": "'buildplate' if support_structure == 'tree' else 'everywhere'" },
"support_interface_height": { "value": "layer_height * 4" }, "support_interface_height": { "value": "layer_height * 4" },
"support_interface_density": { "value": 33.333 }, "support_interface_density": { "value": 33.333 },
"support_interface_pattern": { "value": "'grid'" }, "support_interface_pattern": { "value": "'grid'" },

View File

@ -0,0 +1,52 @@
{
"version": 2,
"name": "Arjun Pro 300",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Venkat Kamesh",
"manufacturer": "Sri Vignan Technologies",
"weight": 3,
"file_formats": "text/x-gcode",
"platform": "arjunpro300_platform.STL",
"platform_offset": [-155, -6, 190],
"has_material": true,
"has_variants": true,
"preferred_variant_name": "0.4 mm Nozzle",
"machine_extruder_trains":
{
"0": "arjunpro_extruder_0",
"1": "arjunpro_extruder_1"
}
},
"overrides": {
"machine_name": { "default_value": "Arjun Pro 300" },
"machine_width": { "default_value": 300 },
"machine_height": { "default_value": 293 },
"machine_depth": { "default_value": 300 },
"machine_center_is_zero": {"default_value": false},
"machine_heated_bed": { "default_value": true },
"machine_nozzle_size": {"default_value": 0.4},
"machine_show_variants": {"default_value": true},
"machine_acceleration": {"default_value": 2000},
"machine_max_feedrate_x": { "value": 300 },
"machine_max_feedrate_y": { "value": 300 },
"machine_max_feedrate_z": { "value": 15 },
"machine_max_feedrate_e": { "value": 150 },
"machine_use_extruder_offset_to_offset_coords": {"default_value": false},
"line_width": {"value": "machine_nozzle_size"},
"speed_travel": {"maximum_value": "300", "value": "200"},
"optimize_wall_printing_order": { "value": "True" },
"material_diameter": { "default_value": 1.75},
"retraction_amount": {"default_value": 6.5},
"retraction_speed": { "default_value": 30},
"adhesion_type": { "default_value": "skirt" },
"machine_gcode_flavor": { "default_value": "Marlin"},
"ironing_enabled":{"default_value": true},
"machine_start_gcode": { "default_value": "M605 S0\nG21\nG90\nM82\nM107\nT1\nG28 \nG29 \nG1 X0 Y5 F2000\nT1\nG92 E0\nG1 E45 F210\nG92 E0\nT0\nG92 E0\nG1 E45 F210\nG92 E0\nM117\n"},
"machine_end_gcode": { "default_value": "G91\nG1 Z+0.5 E-16 Y+10 F9000\nG90\nM107\nM104 S0 T1\nM104 S0 T0\nM140 S0\nM117\nG28 X0 Y0\nT0\nM84"},
"machine_extruder_count": { "default_value": 2 }
}
}

View File

@ -0,0 +1,49 @@
{
"version": 2,
"name": "Arjun Pro 300 Duplication",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Venkat Kamesh",
"manufacturer": "Sri Vignan Technologies",
"weight": 3,
"file_formats": "text/x-gcode",
"has_material": true,
"has_variants": true,
"preferred_variant_name": "0.4 mm Nozzle",
"machine_extruder_trains":
{
"0": "arjunpro_dm_extruder"
}
},
"overrides": {
"machine_name": { "default_value": "Arjunpro 300 Duplication" },
"machine_width": { "default_value": 120 },
"machine_height": { "default_value": 293 },
"machine_depth": { "default_value": 300 },
"machine_center_is_zero": {"default_value": false},
"machine_heated_bed": { "default_value": true },
"machine_nozzle_size": {"default_value": 0.4},
"machine_show_variants": {"default_value": true},
"machine_acceleration": {"default_value": 2000},
"machine_max_feedrate_x": { "value": 300 },
"machine_max_feedrate_y": { "value": 300 },
"machine_max_feedrate_z": { "value": 15 },
"machine_max_feedrate_e": { "value": 150 },
"machine_use_extruder_offset_to_offset_coords": {"default_value": false},
"line_width": {"value": "machine_nozzle_size"},
"speed_travel": {"maximum_value": "300", "value": "200"},
"optimize_wall_printing_order": { "value": "True" },
"material_diameter": { "default_value": 1.75},
"retraction_amount": {"default_value": 6.5},
"retraction_speed": { "default_value": 30},
"adhesion_type": { "default_value": "skirt" },
"machine_gcode_flavor": { "default_value": "Marlin"},
"ironing_enabled":{"default_value": true},
"machine_start_gcode": {"default_value": "M605 S2 R0 X125\nG21\nG90\nM82\nM107\nM104 S{material_print_temperature}\nM105\nM109 S{material_print_temperature}\nG28 \nG29 \nG1 Z15 F150\nG28 Y5\nG1 Y20 F6000\nG28 X0\nG1 X80 F6000\nT0\nG92 E0\nG1 E35 F250\nG1 E45 F120\nG92 E0\nG1 X100 Z0 F5000\nG1 X125 F6000\nM117\n"},
"machine_end_gcode": {"default_value": "G91\nG1 Z+0.5 E-16 Y+10 F9000\nG90\nM107\nM107 P1\nM104 S0\nM140 S0\nM117\nM605 S0\nG28 X0 Y0\nM84"},
"machine_extruder_count": { "default_value": 1 }
}
}

View File

@ -0,0 +1,49 @@
{
"version": 2,
"name": "Arjun Pro 300 Mirror",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Venkat Kamesh",
"manufacturer": "Sri Vignan Technologies",
"weight": 3,
"file_formats": "text/x-gcode",
"has_material": true,
"has_variants": true,
"preferred_variant_name": "0.4 mm Nozzle",
"machine_extruder_trains":
{
"0": "arjunpro_mm_extruder"
}
},
"overrides": {
"machine_name": { "default_value": "Arjunpro 300 Mirror" },
"machine_width": { "default_value": 120 },
"machine_height": { "default_value": 293 },
"machine_depth": { "default_value": 300 },
"machine_center_is_zero": {"default_value": false},
"machine_heated_bed": { "default_value": true },
"machine_nozzle_size": {"default_value": 0.4},
"machine_show_variants": {"default_value": true},
"machine_acceleration": {"default_value": 2000},
"machine_max_feedrate_x": { "value": 300 },
"machine_max_feedrate_y": { "value": 300 },
"machine_max_feedrate_z": { "value": 15 },
"machine_max_feedrate_e": { "value": 150 },
"machine_use_extruder_offset_to_offset_coords": {"default_value": false},
"line_width": {"value": "machine_nozzle_size"},
"speed_travel": {"maximum_value": "300", "value": "200"},
"optimize_wall_printing_order": { "value": "True" },
"material_diameter": { "default_value": 1.75},
"retraction_amount": {"default_value": 6.5},
"retraction_speed": { "default_value": 30},
"adhesion_type": { "default_value": "skirt" },
"machine_gcode_flavor": { "default_value": "Marlin"},
"ironing_enabled":{"default_value": true},
"machine_start_gcode": {"default_value": "M605 S2 R0 X125\nM605 S3 X125\nG21\nG90\nM82\nM107\nM104 S{material_print_temperature}\nM105\nM109 S{material_print_temperature}\nG28 \nG29 \nG1 Z15 F150\nG28 Y5\nG1 Y20 F6000\nG28 X0\nG1 X80 F6000\nT0\nG92 E0\nG1 E35 F250\nG1 E45 F120\nG92 E0\nG1 X100 Z0 F5000\nG1 X125 F6000\nM117\n"},
"machine_end_gcode": {"default_value": "G91\nG1 Z+0.5 E-16 Y+10 F9000\nG90\nM107\nM107 P1\nM104 S0\nM140 S0\nM117\nM605 S0\nG28 X0 Y0\nM84"},
"machine_extruder_count": { "default_value": 1 }
}
}

View File

@ -179,7 +179,6 @@
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_none'" }, "z_seam_corner": { "value": "'z_seam_corner_none'" },
@ -192,7 +191,6 @@
"infill_wipe_dist": { "value": 0.0 }, "infill_wipe_dist": { "value": 0.0 },
"wall_0_wipe_dist": { "value": 0.0 }, "wall_0_wipe_dist": { "value": 0.0 },
"fill_perimeter_gaps": { "value": "'everywhere'" },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false }, "filter_out_tiny_gaps": { "value": false },

View File

@ -258,7 +258,6 @@
"layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" }, "layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" },
"line_width": { "value": "machine_nozzle_size * 1.125" }, "line_width": { "value": "machine_nozzle_size * 1.125" },
"wall_line_width": { "value": "machine_nozzle_size" }, "wall_line_width": { "value": "machine_nozzle_size" },
"fill_perimeter_gaps": { "default_value": "everywhere" },
"fill_outline_gaps": { "value": "True" }, "fill_outline_gaps": { "value": "True" },
"meshfix_maximum_resolution": { "value": "0.01" }, "meshfix_maximum_resolution": { "value": "0.01" },
"meshfix_maximum_deviation": { "value": "layer_height / 2" }, "meshfix_maximum_deviation": { "value": "layer_height / 2" },

View File

@ -22,8 +22,8 @@
"machine_heated_bed": { "default_value": false }, "machine_heated_bed": { "default_value": false },
"machine_center_is_zero": { "default_value": true }, "machine_center_is_zero": { "default_value": true },
"machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG92 E0\nG1 F200 E3\nG92 E0" }, "machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG1 Y-110 Z15\nG0 Z{layer_height_0}\nG92 E0\nG1 F200 Y-100 E6\nG92 E0" },
"machine_end_gcode": { "default_value": "M104 S0\nG28\nG91\nG1 E-6 F300\nM84\nG90" }, "machine_end_gcode": { "default_value": "G28\nG91\nG1 E-6 F300\nM104 S0\nG1 E-1000 F5000\nM84\nG90" },
"layer_height": { "default_value": 0.2 }, "layer_height": { "default_value": 0.2 },
"default_material_print_temperature": { "default_value": 210 }, "default_material_print_temperature": { "default_value": 210 },

View File

@ -4,7 +4,6 @@
"inherits": "fdmprinter", "inherits": "fdmprinter",
"metadata": { "metadata": {
"visible": true, "visible": true,
"author": "Ultimaker",
"manufacturer": "BFB", "manufacturer": "BFB",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform_offset": [ 0, 0, 0], "platform_offset": [ 0, 0, 0],

View File

@ -83,7 +83,6 @@
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'sharpest_corner'" }, "z_seam_type": { "value": "'sharpest_corner'" },
"z_seam_corner": { "value": "'z_seam_corner_inner'" }, "z_seam_corner": { "value": "'z_seam_corner_inner'" },
@ -97,7 +96,6 @@
"infill_wipe_dist": { "value": 0.0 }, "infill_wipe_dist": { "value": 0.0 },
"wall_0_wipe_dist": { "value": 0.0 }, "wall_0_wipe_dist": { "value": 0.0 },
"fill_perimeter_gaps": { "value": "'everywhere'" },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false }, "filter_out_tiny_gaps": { "value": false },

View File

@ -45,9 +45,6 @@
"machine_gcode_flavor": { "machine_gcode_flavor": {
"default_value": "RepRap (RepRap)" "default_value": "RepRap (RepRap)"
}, },
"fill_perimeter_gaps": {
"value": "'everywhere'"
},
"fill_outline_gaps": { "fill_outline_gaps": {
"value": true "value": true
}, },

View File

@ -1,5 +1,5 @@
{ {
"version": 2, "version": 2,
"name": "Crazy3DPrint CZ-300", "name": "Crazy3DPrint CZ-300",
"inherits": "crazy3dprint_base", "inherits": "crazy3dprint_base",
"metadata": { "metadata": {
@ -52,9 +52,9 @@
"skirt_line_count": { "default_value" : 5 }, "skirt_line_count": { "default_value" : 5 },
"initial_layer_line_width_factor": { "default_value" : 140 }, "initial_layer_line_width_factor": { "default_value" : 140 },
"top_bottom_pattern": { "default_value" : "concentric" }, "top_bottom_pattern": { "default_value" : "concentric" },
"outer_inset_first": { "default_value": true },
"fill_outline_gaps": { "default_value": true }, "fill_outline_gaps": { "default_value": true },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"inset_direction": {"value": "'outside_in'" },
"machine_start_gcode": { "machine_start_gcode": {
"default_value": "G28 ; home all axes\nG1 Z15 F5000 ; lift nozzle\nG92 E0\nG1 F200 E3\n" "default_value": "G28 ; home all axes\nG1 Z15 F5000 ; lift nozzle\nG92 E0\nG1 F200 E3\n"
}, },

View File

@ -183,7 +183,6 @@
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
@ -196,7 +195,6 @@
"infill_wipe_dist": { "value": 0.0 }, "infill_wipe_dist": { "value": 0.0 },
"wall_0_wipe_dist": { "value": 0.0 }, "wall_0_wipe_dist": { "value": 0.0 },
"fill_perimeter_gaps": { "value": "'everywhere'" },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false }, "filter_out_tiny_gaps": { "value": false },
@ -247,7 +245,8 @@
"support_use_towers": { "value": false }, "support_use_towers": { "value": false },
"support_xy_distance": { "value": "wall_line_width_0 * 2" }, "support_xy_distance": { "value": "wall_line_width_0 * 2" },
"support_xy_distance_overhang": { "value": "wall_line_width_0" }, "support_xy_distance_overhang": { "value": "wall_line_width_0" },
"support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height*2" }, "support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height * 2" },
"support_top_distance": { "value": "extruderValue(support_roof_extruder_nr if support_roof_enable else support_infill_extruder_nr, 'support_z_distance') + (layer_height if support_structure == 'tree' else 0)"},
"support_xy_overrides_z": { "value": "'xy_overrides_z'" }, "support_xy_overrides_z": { "value": "'xy_overrides_z'" },
"support_wall_count": { "value": 1 }, "support_wall_count": { "value": 1 },
"support_brim_enable": { "value": true }, "support_brim_enable": { "value": true },

View File

@ -5,7 +5,7 @@
"overrides": { "overrides": {
"machine_name": { "default_value": "Creality Ender-6" }, "machine_name": { "default_value": "Creality Ender-6" },
"machine_start_gcode": { "default_value": "\nG28 ;Home\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n"}, "machine_start_gcode": { "default_value": "\nG28 ;Home\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n"},
"machine_end_gcode": { "default_value": "G91 ;Relative positionning\nG1 E-2 F2700 ;Retract a bit\nG1 E-2 Z0.2 F2400 ;Retract and raise Z\nG1 X5 Y5 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positionning\n\nG28 X Y ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\n\nM84 X Y E ;Disable all steppers but Z\n" }, "machine_end_gcode": { "default_value": "G91 ;Relative positioning\nG1 E-2 F2700 ;Retract a bit\nG1 E-2 Z0.2 F2400 ;Retract and raise Z\nG1 X5 Y5 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positioning\n\nG28 X Y ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\n\nM84 X Y E ;Disable all steppers but Z\n" },
"machine_width": { "default_value": 260 }, "machine_width": { "default_value": 260 },
"machine_depth": { "default_value": 260 }, "machine_depth": { "default_value": 260 },
"machine_height": { "default_value": 400 }, "machine_height": { "default_value": 400 },

View File

@ -0,0 +1,25 @@
{
"name": "Creality Sermoon D1",
"version": 2,
"inherits": "creality_base",
"overrides": {
"machine_name": { "default_value": "Creality Sermoon D1" },
"machine_width": { "default_value": 280 },
"machine_depth": { "default_value": 260 },
"machine_height": { "default_value": 310 },
"machine_head_with_fans_polygon": { "default_value": [
[-26, 34],
[-26, -32],
[32, -32],
[32, 34]
]
},
"gantry_height": { "value": 25 }
},
"metadata": {
"quality_definition": "creality_base",
"visible": true
}
}

View File

@ -0,0 +1,32 @@
{
"version": 2,
"name": "Creasee CS50s Pro",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"manufacturer": "Creasee",
"machine_extruder_trains":
{
"0": "creasee_cs50spro_extruder"
}
},
"overrides": {
"machine_name": { "default_value": "Creasee CS50s Pro" },
"machine_width": {
"default_value": 500
},
"machine_depth": {
"default_value": 500
},
"machine_height": {
"default_value": 600
},
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View File

@ -0,0 +1,32 @@
{
"version": 2,
"name": "Creasee Phoenix",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"manufacturer": "Creasee",
"machine_extruder_trains":
{
"0": "creasee_phoenix_extruder"
}
},
"overrides": {
"machine_name": { "default_value": "Creasee Phoenix" },
"machine_width": {
"default_value": 350
},
"machine_depth": {
"default_value": 350
},
"machine_height": {
"default_value": 350
},
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View File

@ -0,0 +1,32 @@
{
"version": 2,
"name": "Creasee Skywalker",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"manufacturer": "Creasee",
"machine_extruder_trains":
{
"0": "creasee_skywalker_extruder"
}
},
"overrides": {
"machine_name": { "default_value": "Creasee Skywalker" },
"machine_width": {
"default_value": 300
},
"machine_depth": {
"default_value": 300
},
"machine_height": {
"default_value": 400
},
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View File

@ -0,0 +1,58 @@
{
"version": 2,
"name": "cremaker common",
"inherits": "fdmprinter",
"metadata": {
"visible": false,
"author": "Joyplace",
"manufacturer": "JOYPLACE CO., LTD.",
"file_formats": "text/x-gcode",
"icon": "icon_ultimaker2",
"has_materials": true,
"machine_extruder_trains": {
"0": "cremaker_extruder_0"
}
},
"overrides": {
"machine_heated_bed": { "default_value": true },
"material_diameter": { "default_value": 1.75 },
"machine_nozzle_size": { "default_value": 0.4 },
"layer_height": { "value": 0.2 },
"layer_height_0": { "value": 0.3 },
"optimize_wall_printing_order": { "value": true },
"xy_offset": { "value": 0.1 },
"xy_offset_layer_0": { "value": -0.1 },
"hole_xy_offset": { "value": 0.15 },
"material_print_temperature": { "value": 200 },
"speed_travel": { "value": 100 },
"speed_layer_0": { "value": 25 },
"acceleration_enabled": { "value": true },
"acceleration_print": { "value": 1250 },
"acceleration_infill": { "value": 1250 },
"acceleration_wall": { "value": 800 },
"acceleration_wall_0": { "value": 800 },
"acceleration_wall_x": { "value": 800 },
"acceleration_travel": { "value": 1250 },
"acceleration_layer_0": { "value": 1000 },
"acceleration_print_layer_0": { "value": 1000 },
"acceleration_travel_layer_0": { "value": 1000 },
"retraction_amount": { "value": 1.2 },
"retraction_speed": { "value": 40 },
"retraction_combing": { "value": "'infill'" },
"retraction_hop_enabled": { "value": true },
"retraction_hop_only_when_collides": { "value": true },
"retraction_hop": { "value": 0.3 },
"adhesion_type": { "value": "'skirt'" },
"relative_extrusion": { "value": true },
"gantry_height": { "value": 28 },
"machine_max_feedrate_z": { "value": 12 },
"machine_max_feedrate_e": { "value": 120 },
"machine_max_acceleration_z": { "value": 10 },
"machine_acceleration": { "value": 1250 },
"machine_max_jerk_xy": { "value": 10 },
"machine_max_jerk_z": { "value": 0.3 },
"machine_max_jerk_e": { "value": 5.0 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }
}
}

View File

@ -0,0 +1,40 @@
{
"version": 2,
"name": "Cremaker M V1",
"inherits": "cremaker_common",
"metadata": {
"visible": true,
"platform": "cremaker_platform_200.obj"
},
"overrides": {
"machine_name": { "default_value": "Cremaker M V1" },
"machine_width": { "default_value": 200 },
"machine_depth": { "default_value": 200 },
"machine_height": { "default_value": 260 },
"initial_layer_line_width_factor": { "default_value": 110.0 },
"machine_head_with_fans_polygon": {
"default_value": [
[ -35, 48 ],
[ 54, 48 ],
[ 54, -67 ],
[ -35, -67 ]
]
},
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": "G28\nG1 Z5.0 F6000\nG1 X2 Y5 F3000\nG1 Z0.3\nG92 E0\nG1 Y100 E10 F600\nG92 E0"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E1\nG1 E-1 F300\nG28 X0 Y180\nM84"
},
"jerk_enabled": { "value": true },
"jerk_print": { "value": 8 },
"jerk_infill": { "value": 8 },
"jerk_wall": { "value": 8 },
"jerk_wall_0": { "value": 8 },
"jerk_wall_x": { "value": 8 },
"jerk_travel": { "value": 10 },
"jerk_layer_0": { "value": 8 }
}
}

View File

@ -0,0 +1,38 @@
{
"version": 2,
"name": "Cremaker M V2",
"inherits": "cremaker_common",
"metadata": {
"visible": true,
"platform": "cremaker_platform_220.obj"
},
"overrides": {
"machine_name": { "default_value": "Cremaker M V2" },
"machine_width": { "default_value": 220 },
"machine_depth": { "default_value": 220 },
"machine_height": { "default_value": 260 },
"initial_layer_line_width_factor": { "default_value": 100.0 },
"machine_head_with_fans_polygon": {
"default_value": [
[ -35, 48 ],
[ 54, 48 ],
[ 54, -67 ],
[ -35, -67 ]
]
},
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": "G28\nG29\nG1 Z5.0 F6000\nG1 X2 Y5 Z0.3 F3000\nG92 E0\nG1 Y100 E10 F1500\nG0 X2.3 F3000\nG1 Y20 E8.5 F1500\nG92 E0\nG1 F2400 E-2"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E1\nG1 E-1 F300\nG28 X0 Y200\nM84"
},
"cool_fan_speed": { "value": 50 },
"coasting_enable": { "value": true },
"coasting_volume": { "value": 0.05 },
"coasting_min_volume": { "value": 1.0 },
"jerk_enabled": { "value": false }
}
}

View File

@ -0,0 +1,32 @@
{
"version": 2,
"name": "Cremaker S V1",
"inherits": "cremaker_common",
"metadata": {
"visible": true,
"platform": "cremaker_platform_200.obj"
},
"overrides": {
"machine_name": { "default_value": "Cremaker S V1" },
"machine_width": { "default_value": 200 },
"machine_depth": { "default_value": 200 },
"machine_height": { "default_value": 160 },
"initial_layer_line_width_factor": { "default_value": 110.0 },
"machine_head_with_fans_polygon": {
"default_value": [
[ -39, 45 ],
[ 23, 45 ],
[ 23, -33 ],
[ -39, -33 ]
]
},
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": "G28\nG1 Z5.0 F6000\nG1 X2 Y5 F3000\nG1 Z0.3\nG92 E0\nG1 Y100 E10 F600\nG92 E0"
},
"machine_end_gcode": {
"default_value": "M104 S0 ; turn off extruder\nM140 S0 ; turn off heatbed\nG92 E1\nG1 E-1 F300\nG28 X0 Y180\nM84"
}
}
}

View File

@ -20,9 +20,6 @@
"machine_heated_bed": { "machine_heated_bed": {
"default_value": true "default_value": true
}, },
"travel_compensate_overlapping_walls_enabled": {
"default_value": false
},
"layer_height": { "layer_height": {
"default_value": 0.2 "default_value": 0.2
}, },
@ -33,7 +30,6 @@
"default_value": "raft" "default_value": "raft"
}, },
"top_bottom_pattern": { "default_value": "lines" }, "top_bottom_pattern": { "default_value": "lines" },
"fill_perimeter_gaps": { "default_value": "everywhere" },
"infill_sparse_density": { "default_value": 20 }, "infill_sparse_density": { "default_value": 20 },
"infill_before_walls": { "default_value": false }, "infill_before_walls": { "default_value": false },
"top_bottom_thickness": { "top_bottom_thickness": {

View File

@ -0,0 +1,126 @@
{
"version": 2,
"name": "Eazao Zero",
"inherits": "fdmprinter",
"metadata":
{
"visible": true,
"author": "Eazao",
"manufacturer": "Eazao",
"file_formats": "text/x-gcode",
"has_materials": true,
"has_machine_quality": false,
"preferred_quality_type": "normal",
"preferred_material": "generic_pla",
"machine_extruder_trains":
{
"0": "eazao_zero_extruder_0"
}
},
"overrides":
{
"machine_name":
{
"default_value": "EAZAO Zero"
},
"machine_heated_bed":
{
"default_value": false
},
"machine_width":
{
"default_value": 150
},
"machine_depth":
{
"default_value": 150
},
"machine_height":
{
"default_value": 240
},
"machine_center_is_zero":
{
"default_value": false
},
"machine_gcode_flavor":
{
"default_value": "Marlin (Marlin/Sprinter)"
},
"machine_start_gcode":
{
"default_value": "G21 \nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG28 ;Home \nG1 Z15.0 F1500 ;move the platform down 15mm\nG92 E0 \nG1 F300 E10\nG92 E0\nM302\nM163 S0 P0.9; Set Mix Factor\nM163 S1 P0.1; Set Mix Factor\nM164 S0\n"
},
"machine_end_gcode":
{
"default_value": "G92 E10\nG1 E-10 F300\nG28 X0 Y0 ;move X Y to min endstops\nM82\nM84 ;steppers off\n"
},
"machine_max_feedrate_x": { "value": 100 },
"machine_max_feedrate_y": { "value": 100 },
"machine_max_feedrate_z": { "value": 5 },
"machine_max_feedrate_e": { "value": 25 },
"machine_max_acceleration_x": { "value": 500 },
"machine_max_acceleration_y": { "value": 500 },
"machine_max_acceleration_z": { "value": 50 },
"machine_max_acceleration_e": { "value": 500 },
"machine_acceleration": { "value": 300 },
"acceleration_print": { "value": 300 },
"acceleration_travel": { "value": 300 },
"acceleration_enabled": { "value": false },
"machine_max_jerk_xy": { "value": 10 },
"machine_max_jerk_z": { "value": 0.3 },
"machine_max_jerk_e": { "value": 5 },
"jerk_print": { "value": 10 },
"jerk_travel": { "value": "jerk_print" },
"jerk_travel_layer_0": { "value": "jerk_travel" },
"jerk_enabled": { "value": false },
"layer_height": {"value":1.0},
"layer_height_0": {"value":1.0},
"line_width": {"value":3.0},
"wall_thickness": {"value":3.0},
"optimize_wall_printing_order": { "value": "True" },
"top_bottom_thickness": {"value":0},
"bottom_layers":{"value":2},
"initial_bottom_layers":{"value":2},
"infill_sparse_density": {"value":0},
"material_print_temperature": { "value": "0" },
"material_print_temperature_layer_0": { "value": "0" },
"material_initial_print_temperature": { "value": "0" },
"material_final_print_temperature": { "value": "0" },
"speed_print": { "value": 20.0 },
"speed_wall": { "value": 20.0 },
"speed_wall_0": { "value": 20.0 },
"speed_wall_x": { "value": 20.0 },
"speed_travel": { "value": 20.0 },
"speed_z_hop": { "value": "machine_max_feedrate_z" },
"retraction_hop_enabled": { "value": false },
"retraction_hop": { "value": 0.2 },
"retraction_combing": { "value": "'noskin'" },
"retraction_combing_max_distance": { "value": 0 },
"travel_avoid_other_parts": { "value": true },
"travel_avoid_supports": { "value": false },
"travel_retract_before_outer_wall": { "value": false },
"retraction_enable": { "value": false },
"retraction_speed": { "value": 25 },
"retraction_amount": { "value": 7 },
"retraction_count_max": { "value": 100 },
"retraction_extrusion_window": { "value": 10 },
"cool_fan_enabled": { "value": false },
"adhesion_type": { "default_value": "none" }
}
}

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