Merge branch 'master' into feature_unify_pause_at_height

This commit is contained in:
Aldo Hoeben 2020-03-30 17:58:25 +02:00 committed by GitHub
commit 7ea3891da0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
168 changed files with 15794 additions and 491 deletions

1
.gitignore vendored
View file

@ -53,6 +53,7 @@ plugins/GodMode
plugins/OctoPrintPlugin
plugins/ProfileFlattener
plugins/SettingsGuide
plugins/SVGToolpathReader
plugins/X3GWriter
#Build stuff

View file

@ -4,12 +4,11 @@ from typing import Optional, Dict, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message
from cura import UltimakerCloudAuthentication
from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
from cura.UltimakerCloud import UltimakerCloudAuthentication
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication

View file

@ -10,18 +10,23 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The BackupsManager is responsible for managing the creating and restoring of
# back-ups.
#
# Back-ups themselves are represented in a different class.
class BackupsManager:
"""
The BackupsManager is responsible for managing the creating and restoring of
back-ups.
Back-ups themselves are represented in a different class.
"""
def __init__(self, application: "CuraApplication") -> None:
self._application = application
## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version).
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
"""
Get a back-up of the current configuration.
:return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version).
"""
self._disableAutoSave()
backup = Backup(self._application)
backup.makeFromCurrent()
@ -29,11 +34,13 @@ class BackupsManager:
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
return backup.zip_file, backup.meta_data
## Restore a back-up from a given ZipFile.
# \param zip_file A bytes object containing the actual back-up.
# \param meta_data A dict containing some metadata that is needed to
# restore the back-up correctly.
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
"""
Restore a back-up from a given ZipFile.
:param zip_file: A bytes object containing the actual back-up.
:param meta_data: A dict containing some metadata that is needed to restore the back-up correctly.
"""
if not meta_data.get("cura_release", None):
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
@ -48,9 +55,10 @@ class BackupsManager:
# We don't want to store the data at this point as that would override the just-restored backup.
self._application.windowClosed(save_data = False)
## Here we try to disable the auto-save plug-in as it might interfere with
# restoring a back-up.
def _disableAutoSave(self) -> None:
"""Here we (try to) disable the saving as it might interfere with restoring a back-up."""
self._application.enableSave(False)
auto_save = self._application.getAutoSave()
# The auto save is only not created if the application has not yet started.
if auto_save:
@ -58,8 +66,10 @@ class BackupsManager:
else:
Logger.log("e", "Unable to disable the autosave as application init has not been completed")
## Re-enable auto-save after we're done.
def _enableAutoSave(self) -> None:
"""Re-enable auto-save and other saving after we're done."""
self._application.enableSave(True)
auto_save = self._application.getAutoSave()
# The auto save is only not created if the application has not yet started.
if auto_save:

View file

@ -7,71 +7,52 @@ import time
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any
import numpy
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog
import UM.Util
import cura.Settings.cura_empty_instance_containers
from UM.Application import Application
from UM.Decorators import override
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Resources import Resources
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector
from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Message import Message
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
from UM.Resources import Resources
from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import Validator
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ShapeArray import ShapeArray
from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene import ZOffsetDecorator
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
@ -80,6 +61,8 @@ from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.Machines.Models.IntentModel import IntentModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
@ -89,51 +72,47 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel
from cura.Machines.Models.IntentModel import IntentModel
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.Operations.SetParentOperation import SetParentOperation
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
import cura.Settings.cura_empty_instance_containers
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.Scene import ZOffsetDecorator
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel
from cura.UI.TextManager import TextManager
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.RecommendedMode import RecommendedMode
from cura.UI.TextManager import TextManager
from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
from cura.UltimakerCloud import UltimakerCloudAuthentication
from cura.Utils.NetworkingUtil import NetworkingUtil
from .SingleInstance import SingleInstance
from .AutoSave import AutoSave
from . import PlatformPhysics
from . import BuildVolume
from . import CameraAnimation
from . import CuraActions
from . import PlatformPhysics
from . import PrintJobPreviewImageProvider
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.GlobalStack import GlobalStack
from .AutoSave import AutoSave
from .SingleInstance import SingleInstance
if TYPE_CHECKING:
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
@ -263,6 +242,7 @@ class CuraApplication(QtApplication):
# Backups
self._auto_save = None # type: Optional[AutoSave]
self._enable_save = True
self._container_registry_class = CuraContainerRegistry
# Redefined here in order to please the typing.
@ -706,15 +686,20 @@ class CuraApplication(QtApplication):
self._message_box_callback = None
self._message_box_callback_arguments = []
def enableSave(self, enable: bool):
self._enable_save = enable
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self) -> None:
if not self.started:
if not self.started or not self._enable_save:
# Do not do saving during application start or when data should not be saved on quit.
return
ContainerRegistry.getInstance().saveDirtyContainers()
self.savePreferences()
def saveStack(self, stack):
if not self._enable_save:
return
ContainerRegistry.getInstance().saveContainer(stack)
@pyqtSlot(str, result = QUrl)

View file

@ -1,9 +1,11 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QTimer
from shapely.errors import TopologicalError # To capture errors if Shapely messes up.
from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Math.Vector import Vector
@ -136,7 +138,11 @@ class PlatformPhysics:
own_convex_hull = node.callDecoration("getConvexHull")
other_convex_hull = other_node.callDecoration("getConvexHull")
if own_convex_hull and other_convex_hull:
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
try:
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
except TopologicalError as e: # Can happen if the convex hull is degenerate?
Logger.warning("Got a topological error when calculating convex hull intersection: {err}".format(err = str(e)))
overlap = False
if overlap: # Moving ensured that overlap was still there. Try anew!
temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z = move_vector.z + overlap[1] * self._move_factor)

View file

@ -239,6 +239,8 @@ class ContainerManager(QObject):
container_type = container_registry.getContainerForMimeType(mime_type)
if not container_type:
return {"status": "error", "message": "Could not find a container to handle the specified file."}
if not issubclass(container_type, InstanceContainer):
return {"status": "error", "message": "This is not a material container, but another type of file."}
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
container_id = container_registry.uniqueName(container_id)

View file

@ -684,7 +684,10 @@ class MachineManager(QObject):
if other_machine_stacks:
self.setActiveMachine(other_machine_stacks[0]["id"])
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
metadatas = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)
if not metadatas:
return # machine_id doesn't exist. Nothing to remove.
metadata = metadatas[0]
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
for container in containers:

View file

@ -6,17 +6,20 @@ from cura.API import Account
from cura.CuraApplication import CuraApplication
## Add a Authorization header to the request for Ultimaker Cloud Api requests.
# When the user is not logged in or a token is not available, a warning will be logged
# Also add the user agent headers (see DefaultUserAgentScope)
class UltimakerCloudScope(DefaultUserAgentScope):
"""Add an Authorization header to the request for Ultimaker Cloud Api requests.
When the user is not logged in or a token is not available, a warning will be logged
Also add the user agent headers (see DefaultUserAgentScope)
"""
def __init__(self, application: CuraApplication):
super().__init__(application)
api = application.getCuraAPI()
self._account = api.account # type: Account
def request_hook(self, request: QNetworkRequest):
super().request_hook(request)
def requestHook(self, request: QNetworkRequest):
super().requestHook(request)
token = self._account.accessToken
if not self._account.isLoggedIn or token is None:
Logger.warning("Cannot add authorization to Cloud Api request")
@ -25,4 +28,4 @@ class UltimakerCloudScope(DefaultUserAgentScope):
header_dict = {
"Authorization": "Bearer {}".format(token)
}
self.add_headers(request, header_dict)
self.addHeaders(request, header_dict)

View file

View file

@ -23,6 +23,8 @@ import os
import Arcus # @UnusedImport
import Savitar # @UnusedImport
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
from UM.Platform import Platform
from cura import ApplicationMetadata
from cura.ApplicationMetadata import CuraAppName
@ -220,5 +222,10 @@ if Platform.isLinux() and getattr(sys, "frozen", False):
import trimesh.exchange.load
os.environ["LD_LIBRARY_PATH"] = old_env
if ApplicationMetadata.CuraDebugMode:
ssl_conf = QSslConfiguration.defaultConfiguration()
ssl_conf.setPeerVerifyMode(QSslSocket.VerifyNone)
QSslConfiguration.setDefaultConfiguration(ssl_conf)
app = CuraApplication()
app.run()

View file

@ -88,7 +88,10 @@ class ThreeMFReader(MeshReader):
# \returns Scene node.
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
self._object_count += 1
node_name = "Object %s" % self._object_count
node_name = savitar_node.getName()
if node_name == "":
node_name = "Object %s" % self._object_count
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
@ -6,9 +6,12 @@ from io import StringIO
import zipfile
from UM.Application import Application
from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.Utils.Threading import call_on_qt_thread
@ -25,6 +28,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
Logger.error("3MF Writer class is unavailable. Can't write workspace.")
return False
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
@ -37,19 +42,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
global_stack = machine_manager.activeMachine
# Add global container stack data to the archive.
self._writeContainerToArchive(global_stack, archive)
try:
# Add global container stack data to the archive.
self._writeContainerToArchive(global_stack, archive)
# Also write all containers in the stack to the file
for container in global_stack.getContainers():
self._writeContainerToArchive(container, archive)
# Check if the machine has extruders and save all that data as well.
for extruder_stack in global_stack.extruders.values():
self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers():
# Also write all containers in the stack to the file
for container in global_stack.getContainers():
self._writeContainerToArchive(container, archive)
# Check if the machine has extruders and save all that data as well.
for extruder_stack in global_stack.extruders.values():
self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers():
self._writeContainerToArchive(container, archive)
except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.")
return False
# Write preferences to archive
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
temp_preferences = Preferences()
@ -59,24 +69,29 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
preferences_string = StringIO()
temp_preferences.writeToFile(preferences_string)
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
archive.writestr(preferences_file, preferences_string.getvalue())
try:
archive.writestr(preferences_file, preferences_string.getvalue())
# Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", application.getVersion())
version_config_parser.set("versions", "build_type", application.getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
# Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", application.getVersion())
version_config_parser.set("versions", "build_type", application.getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
version_file_string = StringIO()
version_config_parser.write(version_file_string)
archive.writestr(version_file, version_file_string.getvalue())
version_file_string = StringIO()
version_config_parser.write(version_file_string)
archive.writestr(version_file, version_file_string.getvalue())
self._writePluginMetadataToArchive(archive)
self._writePluginMetadataToArchive(archive)
# Close the archive & reset states.
archive.close()
# Close the archive & reset states.
archive.close()
except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.")
return False
mesh_writer.setStoreArchive(False)
return True

View file

@ -77,6 +77,7 @@ class ThreeMFWriter(MeshWriter):
return
savitar_node = Savitar.SceneNode()
savitar_node.setName(um_node.getName())
node_matrix = um_node.getLocalTransformation()

View file

@ -0,0 +1,134 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import threading
from datetime import datetime
from typing import Any, Dict, Optional
import sentry_sdk
from PyQt5.QtNetwork import QNetworkReply
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
catalog = i18nCatalog("cura")
class CreateBackupJob(Job):
"""Creates backup zip, requests upload url and uploads the backup file to cloud storage."""
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
DEFAULT_UPLOAD_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error while uploading your backup.")
def __init__(self, api_backup_url: str) -> None:
""" Create a new backup Job. start the job by calling start()
:param api_backup_url: The url of the 'backups' endpoint of the Cura Drive Api
"""
super().__init__()
self._api_backup_url = api_backup_url
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._backup_zip = None # type: Optional[bytes]
self._job_done = threading.Event()
"""Set when the job completes. Does not indicate success."""
self.backup_upload_error_message = ""
"""After the job completes, an empty string indicates success. Othrerwise, the value is a translated message."""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
CuraApplication.getInstance().processEvents()
cura_api = CuraApplication.getInstance().getCuraAPI()
self._backup_zip, backup_meta_data = cura_api.backups.createBackup()
if not self._backup_zip or not backup_meta_data:
self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.")
upload_message.hide()
return
upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup..."))
CuraApplication.getInstance().processEvents()
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
self._requestUploadSlot(backup_meta_data, len(self._backup_zip))
self._job_done.wait()
if self.backup_upload_error_message == "":
upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."))
upload_message.setProgress(None) # Hide progress bar
else:
# some error occurred. This error is presented to the user by DrivePluginExtension
upload_message.hide()
def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
"""Request a backup upload slot from the API.
:param backup_metadata: A dict containing some meta data about the backup.
:param backup_size: The size of the backup file in bytes.
"""
payload = json.dumps({"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
}).encode()
HttpRequestManager.getInstance().put(
self._api_backup_url,
data = payload,
callback = self._onUploadSlotCompleted,
error_callback = self._onUploadSlotCompleted,
scope = self._json_cloud_scope)
def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if HttpRequestManager.safeHttpStatus(reply) >= 300:
replyText = HttpRequestManager.readText(reply)
Logger.warning("Could not request backup upload: %s", replyText)
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
if HttpRequestManager.safeHttpStatus(reply) == 400:
errors = json.loads(replyText)["errors"]
if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
if self._backup_zip is None: # will never happen; keep mypy happy
zip_error = "backup is None."
else:
zip_error = "{} exceeds max size.".format(str(len(self._backup_zip)))
sentry_sdk.capture_message("backup failed: {}".format(zip_error), level ="warning")
self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")
from sentry_sdk import capture_message
self._job_done.set()
return
if error is not None:
Logger.warning("Could not request backup upload: %s", HttpRequestManager.qt_network_error_name(error))
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
self._job_done.set()
return
backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]
# Upload the backup to storage.
HttpRequestManager.getInstance().put(
backup_upload_url,
data=self._backup_zip,
callback=self._uploadFinishedCallback,
error_callback=self._uploadFinishedCallback
)
def _uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
Logger.log("w", "Could not upload backup file: %s", HttpRequestManager.readText(reply))
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
self._job_done.set()

View file

@ -1,90 +1,70 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import base64
import hashlib
from datetime import datetime
from tempfile import NamedTemporaryFile
from typing import Any, Optional, List, Dict
from typing import Any, Optional, List, Dict, Callable
import requests
from PyQt5.QtNetwork import QNetworkReply
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal, signalemitter
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from .UploadBackupJob import UploadBackupJob
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .CreateBackupJob import CreateBackupJob
from .RestoreBackupJob import RestoreBackupJob
from .Settings import Settings
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
@signalemitter
class DriveApiService:
"""The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling."""
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
# Emit signal when restoring backup started or finished.
restoringStateChanged = Signal()
"""Emits signal when restoring backup started or finished."""
# Emit signal when creating backup started or finished.
creatingStateChanged = Signal()
"""Emits signal when creating backup started or finished."""
def __init__(self) -> None:
self._cura_api = CuraApplication.getInstance().getCuraAPI()
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
def getBackups(self) -> List[Dict[str, Any]]:
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return []
try:
backup_list_request = requests.get(self.BACKUP_URL, headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("w", "Unable to connect with the server.")
return []
def getBackups(self, changed: Callable[[List[Dict[str, Any]]], None]) -> None:
def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None:
Logger.log("w", "Could not get backups: " + str(error))
changed([])
return
# HTTP status 300s mean redirection. 400s and 500s are errors.
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
if backup_list_request.status_code >= 300:
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
return []
backup_list_response = HttpRequestManager.readJSON(reply)
if "data" not in backup_list_response:
Logger.log("w", "Could not get backups from remote, actual response body was: %s",
str(backup_list_response))
changed([]) # empty list of backups
return
backup_list_response = backup_list_request.json()
if "data" not in backup_list_response:
Logger.log("w", "Could not get backups from remote, actual response body was: %s", str(backup_list_response))
return []
changed(backup_list_response["data"])
return backup_list_response["data"]
HttpRequestManager.getInstance().get(
self.BACKUP_URL,
callback= callback,
error_callback = callback,
scope=self._json_cloud_scope
)
def createBackup(self) -> None:
self.creatingStateChanged.emit(is_creating = True)
# Create the backup.
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
if not backup_zip_file or not backup_meta_data:
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
return
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
if not backup_upload_url:
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
return
# Upload the backup to storage.
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
upload_backup_job = CreateBackupJob(self.BACKUP_URL)
upload_backup_job.finished.connect(self._onUploadFinished)
upload_backup_job.start()
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
def _onUploadFinished(self, job: "CreateBackupJob") -> None:
if job.backup_upload_error_message != "":
# If the job contains an error message we pass it along so the UI can display it.
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
@ -96,96 +76,38 @@ class DriveApiService:
download_url = backup.get("download_url")
if not download_url:
# If there is no download URL, we can't restore the backup.
return self._emitRestoreError()
Logger.warning("backup download_url is missing. Aborting backup.")
self.restoringStateChanged.emit(is_restoring = False,
error_message = catalog.i18nc("@info:backup_status",
"There was an error trying to restore your backup."))
return
try:
download_package = requests.get(download_url, stream = True)
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return self._emitRestoreError()
restore_backup_job = RestoreBackupJob(backup)
restore_backup_job.finished.connect(self._onRestoreFinished)
restore_backup_job.start()
if download_package.status_code >= 300:
# Something went wrong when attempting to download the backup.
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
return self._emitRestoreError()
def _onRestoreFinished(self, job: "RestoreBackupJob") -> None:
if job.restore_backup_error_message != "":
# If the job contains an error message we pass it along so the UI can display it.
self.restoringStateChanged.emit(is_restoring=False)
else:
self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message)
# We store the file in a temporary path fist to ensure integrity.
temporary_backup_file = NamedTemporaryFile(delete = False)
with open(temporary_backup_file.name, "wb") as write_backup:
for chunk in download_package:
write_backup.write(chunk)
def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool], None]):
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
# Don't restore the backup if the MD5 hashes do not match.
# This can happen if the download was interrupted.
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
return self._emitRestoreError()
def finishedCallback(reply: QNetworkReply, ca: Callable[[bool], None] = finished_callable) -> None:
self._onDeleteRequestCompleted(reply, ca)
# Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup:
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
self.restoringStateChanged.emit(is_restoring = False)
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca: Callable[[bool], None] = finished_callable) -> None:
self._onDeleteRequestCompleted(reply, ca, error)
def _emitRestoreError(self) -> None:
self.restoringStateChanged.emit(is_restoring = False,
error_message = catalog.i18nc("@info:backup_status",
"There was an error trying to restore your backup."))
HttpRequestManager.getInstance().delete(
url = "{}/{}".format(self.BACKUP_URL, backup_id),
callback = finishedCallback,
error_callback = errorCallback,
scope= self._json_cloud_scope
)
# Verify the MD5 hash of a file.
# \param file_path Full path to the file.
# \param known_hash The known MD5 hash of the file.
# \return: Success or not.
@staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
with open(file_path, "rb") as read_backup:
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
return known_hash == local_md5_hash
def deleteBackup(self, backup_id: str) -> bool:
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return False
try:
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return False
if delete_backup.status_code >= 300:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
return True
# Request a backup upload slot from the API.
# \param backup_metadata: A dict containing some meta data about the backup.
# \param backup_size The size of the backup file in bytes.
# \return: The upload URL for the actual backup file if successful, otherwise None.
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return None
try:
backup_upload_request = requests.put(
self.BACKUP_URL,
json = {"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
},
headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return None
# Any status code of 300 or above indicates an error.
if backup_upload_request.status_code >= 300:
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
return None
return backup_upload_request.json()["data"]["upload_url"]
def _onDeleteRequestCompleted(reply: QNetworkReply, callable: Callable[[bool], None], error: Optional["QNetworkReply.NetworkError"] = None) -> None:
callable(HttpRequestManager.replyIndicatesSuccess(reply, error))

View file

@ -133,7 +133,10 @@ class DrivePluginExtension(QObject, Extension):
@pyqtSlot(name = "refreshBackups")
def refreshBackups(self) -> None:
self._backups = self._drive_api_service.getBackups()
self._drive_api_service.getBackups(self._backupsChangedCallback)
def _backupsChangedCallback(self, backups: List[Dict[str, Any]]) -> None:
self._backups = backups
self.backupsChanged.emit()
@pyqtProperty(bool, notify = restoringStateChanged)
@ -158,5 +161,8 @@ class DrivePluginExtension(QObject, Extension):
@pyqtSlot(str, name = "deleteBackup")
def deleteBackup(self, backup_id: str) -> None:
self._drive_api_service.deleteBackup(backup_id)
self.refreshBackups()
self._drive_api_service.deleteBackup(backup_id, self._backupDeletedCallback)
def _backupDeletedCallback(self, success: bool):
if success:
self.refreshBackups()

View file

@ -0,0 +1,92 @@
import base64
import hashlib
import threading
from tempfile import NamedTemporaryFile
from typing import Optional, Any, Dict
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
from UM.PackageManager import catalog
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
class RestoreBackupJob(Job):
"""Downloads a backup and overwrites local configuration with the backup.
When `Job.finished` emits, `restore_backup_error_message` will either be `""` (no error) or an error message
"""
DISK_WRITE_BUFFER_SIZE = 512 * 1024
DEFAULT_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.")
def __init__(self, backup: Dict[str, Any]) -> None:
""" Create a new restore Job. start the job by calling start()
:param backup: A dict containing a backup spec
"""
super().__init__()
self._job_done = threading.Event()
self._backup = backup
self.restore_backup_error_message = ""
def run(self) -> None:
url = self._backup.get("download_url")
assert url is not None
HttpRequestManager.getInstance().get(
url = url,
callback = self._onRestoreRequestCompleted,
error_callback = self._onRestoreRequestCompleted
)
self._job_done.wait() # A job is considered finished when the run function completes
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
self._job_done.set()
return
# We store the file in a temporary path fist to ensure integrity.
temporary_backup_file = NamedTemporaryFile(delete = False)
with open(temporary_backup_file.name, "wb") as write_backup:
app = CuraApplication.getInstance()
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
write_backup.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
app.processEvents()
if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")):
# Don't restore the backup if the MD5 hashes do not match.
# This can happen if the download was interrupted.
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
# Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup:
cura_api = CuraApplication.getInstance().getCuraAPI()
cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {}))
self._job_done.set()
@staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
"""Verify the MD5 hash of a file.
:param file_path: Full path to the file.
:param known_hash: The known MD5 hash of the file.
:return: Success or not.
"""
with open(file_path, "rb") as read_backup:
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
return known_hash == local_md5_hash

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura import UltimakerCloudAuthentication
from cura.UltimakerCloud import UltimakerCloudAuthentication
class Settings:

View file

@ -1,41 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import requests
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UploadBackupJob(Job):
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
# This job is responsible for uploading the backup file to cloud storage.
# As it can take longer than some other tasks, we schedule this using a Cura Job.
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
super().__init__()
self._signed_upload_url = signed_upload_url
self._backup_zip = backup_zip
self._upload_success = False
self.backup_upload_error_message = ""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
upload_message.hide()
if backup_upload.status_code >= 300:
self.backup_upload_error_message = backup_upload.text
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
else:
self._upload_success = True
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
self.finished.emit(self)

View file

@ -1,23 +1,24 @@
# Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
import configparser # The script lists are stored in metadata as serialised config files.
import importlib.util
import io # To allow configparser to write to a string.
import os.path
import pkgutil
import sys
import importlib.util
from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Trust import Trust
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura")
@ -161,7 +162,13 @@ class PostProcessingPlugin(QObject, Extension):
# Iterate over all scripts.
if script_name not in sys.modules:
try:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
file_path = os.path.join(path, script_name + ".py")
if not self._isScriptAllowed(file_path):
Logger.warning("Skipped loading post-processing script {}: not trusted".format(file_path))
continue
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name,
file_path)
loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
continue
@ -334,4 +341,26 @@ class PostProcessingPlugin(QObject, Extension):
if global_container_stack is not None:
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
@staticmethod
def _isScriptAllowed(file_path: str) -> bool:
"""Checks whether the given file is allowed to be loaded"""
if not ApplicationMetadata.IsEnterpriseVersion:
# No signature needed
return True
dir_path = os.path.split(file_path)[0] # type: str
plugin_path = PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")
assert plugin_path is not None # appease mypy
bundled_path = os.path.join(plugin_path, "scripts")
if dir_path == bundled_path:
# Bundled scripts are trusted.
return True
trust_instance = Trust.getInstanceOrNone()
if trust_instance is not None and Trust.signatureFileExistsFor(file_path):
if trust_instance.signedFileCheck(file_path):
return True
return False # Default verdict should be False, being the most secure fallback

View file

@ -57,6 +57,17 @@ class PauseAtHeight(Script):
"options": {"marlin": "Marlin (M0)", "griffin": "Griffin (M0, firmware retract)", "bq": "BQ (M25)", "reprap": "RepRap (M226)", "repetier": "Repetier (@pause)"},
"default_value": "marlin",
"value": "\\\"griffin\\\" if machine_gcode_flavor==\\\"Griffin\\\" else \\\"reprap\\\" if machine_gcode_flavor==\\\"RepRap (RepRap)\\\" else \\\"repetier\\\" if machine_gcode_flavor==\\\"Repetier\\\" else \\\"bq\\\" if \\\"BQ\\\" in machine_name else \\\"marlin\\\""
},
"disarm_timeout":
{
"label": "Disarm timeout",
"description": "After this time steppers are going to disarm (meaning that they can easily lose their positions). Set this to 0 if you don't want to set any duration.",
"type": "int",
"value": "0",
"minimum_value": "0",
"minimum_value_warning": "0",
"maximum_value_warning": "1800",
"unit": "s"
},
"head_park_x":
{
@ -206,6 +217,7 @@ class PauseAtHeight(Script):
pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer")
disarm_timeout = self.getSettingValueByKey("disarm_timeout")
retraction_amount = self.getSettingValueByKey("retraction_amount")
retraction_speed = self.getSettingValueByKey("retraction_speed")
extrude_amount = self.getSettingValueByKey("extrude_amount")
@ -393,6 +405,10 @@ class PauseAtHeight(Script):
if display_text:
prepend_gcode += "M117 " + display_text + "\n"
# Set the disarm timeout
if disarm_timeout > 0:
prepend_gcode += self.putValue(M = 18, S = disarm_timeout) + " ; Set the disarm timeout\n"
# Wait till the user continues printing
prepend_gcode += pause_command + " ; Do the actual pause\n"

View file

@ -0,0 +1,61 @@
import os
import sys
from unittest.mock import patch, MagicMock
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Trust import Trust
from ..PostProcessingPlugin import PostProcessingPlugin
# not sure if needed
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
""" In this file, commnunity refers to regular Cura for makers."""
mock_plugin_registry = MagicMock()
mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path")
# noinspection PyProtectedMember
@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False)
def test_community_user_script_allowed():
assert PostProcessingPlugin._isScriptAllowed("blaat.py")
# noinspection PyProtectedMember
@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False)
def test_community_bundled_script_allowed():
assert PostProcessingPlugin._isScriptAllowed(_bundled_file_path())
# noinspection PyProtectedMember
@patch("cura.ApplicationMetadata.IsEnterpriseVersion", True)
@patch.object(PluginRegistry, "getInstance", return_value=mock_plugin_registry)
def test_enterprise_unsigned_user_script_not_allowed(plugin_registry):
assert not PostProcessingPlugin._isScriptAllowed("blaat.py")
# noinspection PyProtectedMember
@patch("cura.ApplicationMetadata.IsEnterpriseVersion", True)
@patch.object(PluginRegistry, "getInstance", return_value=mock_plugin_registry)
def test_enterprise_signed_user_script_allowed(plugin_registry):
mocked_trust = MagicMock()
mocked_trust.signedFileCheck = MagicMock(return_value=True)
plugin_registry.getPluginPath = MagicMock(return_value="mocked_plugin_path")
with patch.object(Trust, "signatureFileExistsFor", return_value = True):
with patch("UM.Trust.Trust.getInstanceOrNone", return_value=mocked_trust):
assert PostProcessingPlugin._isScriptAllowed("mocked_plugin_path/scripts/blaat.py")
# noinspection PyProtectedMember
@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False)
def test_enterprise_bundled_script_allowed():
assert PostProcessingPlugin._isScriptAllowed(_bundled_file_path())
def _bundled_file_path():
return os.path.join(
Resources.getStoragePath(Resources.Resources) + "scripts/blaat.py"
)

View file

@ -1,4 +1,4 @@
//Copyright (c) 2019 Ultimaker B.V.
//Copyright (c) 2020 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path d="M24,44,7,33.4V14.6L24,4,41,14.6V33.4ZM9,32.3l15,9.3,15-9.3V15.7L24,6.4,9,15.7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View file

@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 1.4
import UM 1.1 as UM
import UM 1.5 as UM
Item
{
@ -203,7 +203,7 @@ Item
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
renderType: Text.NativeRendering
}
}

View file

@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import UM 1.5 as UM
import Cura 1.1 as Cura
Column
@ -85,7 +85,7 @@ Column
MouseArea
{
anchors.fill: parent
onClicked: Qt.openUrlExternally(parent.whereToBuyUrl)
onClicked: UM.UrlUtil.openUrl(parent.whereToBuyUrl, ["https", "http"])
}
}

View file

@ -66,8 +66,10 @@ Item
anchors.centerIn: parent
width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width
height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: model.icon_url || "../../images/logobot.svg"
source: model.icon_url || "../../images/placeholder.svg"
mipmap: true
}
UM.RecolorImage

View file

@ -22,8 +22,10 @@ Rectangle
id: thumbnail
height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
sourceSize.height: height
sourceSize.width: width
fillMode: Image.PreserveAspectFit
source: model.icon_url || "../../images/logobot.svg"
source: model.icon_url || "../../images/placeholder.svg"
mipmap: true
anchors
{

View file

@ -68,9 +68,11 @@ UM.Dialog{
Image
{
id: packageIcon
source: model.icon_url || "../../images/logobot.svg"
source: model.icon_url || "../../images/placeholder.svg"
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}
@ -111,9 +113,11 @@ UM.Dialog{
Image
{
id: packageIcon
source: model.icon_url || "../../images/logobot.svg"
source: model.icon_url || "../../images/placeholder.svg"
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}

View file

@ -53,8 +53,10 @@ UM.Dialog
id: icon
width: 30 * screenScaleFactor
height: width
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || "../../images/logobot.svg"
source: licenseModel.iconUrl || "../../images/placeholder.svg"
mipmap: true
}

View file

@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import UM 1.5 as UM
import "../components"
@ -33,7 +33,7 @@ Item
width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height
fillMode: Image.PreserveAspectFit
source: details.icon_url || "../../images/logobot.svg"
source: details.icon_url || "../../images/placeholder.svg"
mipmap: true
anchors
{
@ -132,7 +132,7 @@ Item
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"])
renderType: Text.NativeRendering
}

View file

@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import UM 1.5 as UM
import Cura 1.1 as Cura
@ -46,8 +46,12 @@ Item
{
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: details === null ? "" : (details.icon_url || "../../images/logobot.svg")
source: details === null ? "" : (details.icon_url || "../../images/placeholder.svg")
mipmap: true
height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
sourceSize.height: height
sourceSize.width: width
}
}
@ -217,7 +221,7 @@ Item
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
renderType: Text.NativeRendering
}
Label

View file

@ -1,6 +1,7 @@
from typing import Union
from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura import ApplicationMetadata
from cura.UltimakerCloud import UltimakerCloudAuthentication
class CloudApiModel:

View file

@ -1,8 +1,9 @@
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from ..CloudApiModel import CloudApiModel
from ..UltimakerCloudScope import UltimakerCloudScope
class CloudApiClient:
@ -26,7 +27,7 @@ class CloudApiClient:
if self.__instance is not None:
raise RuntimeError("This is a Singleton. use getInstance()")
self._scope = UltimakerCloudScope(app) # type: UltimakerCloudScope
self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope
app.getPackageManager().packageInstalled.connect(self._onPackageInstalled)

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import List, Dict, Any
from typing import Optional
from PyQt5.QtCore import QObject
@ -11,12 +12,12 @@ from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.CuraApplication import CuraApplication, ApplicationMetadata
from ..CloudApiModel import CloudApiModel
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
from ..UltimakerCloudScope import UltimakerCloudScope
from ..CloudApiModel import CloudApiModel
from typing import List, Dict, Any
class CloudPackageChecker(QObject):
def __init__(self, application: CuraApplication) -> None:
@ -24,7 +25,7 @@ class CloudPackageChecker(QObject):
self.discrepancies = Signal() # Emits SubscribedPackagesModel
self._application = application # type: CuraApplication
self._scope = UltimakerCloudScope(application)
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
self._model = SubscribedPackagesModel()
self._message = None # type: Optional[Message]
@ -111,4 +112,4 @@ class CloudPackageChecker(QObject):
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
sync_message.hide()
self.discrepancies.emit(self._model)
self.discrepancies.emit(self._model)

View file

@ -12,8 +12,8 @@ from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
from ..UltimakerCloudScope import UltimakerCloudScope
## Downloads a set of packages from the Ultimaker Cloud Marketplace

View file

@ -9,22 +9,20 @@ from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, U
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Extension import Extension
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.Version import Version
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from .CloudApiModel import CloudApiModel
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .AuthorsModel import AuthorsModel
from .CloudApiModel import CloudApiModel
from .CloudSync.LicenseModel import LicenseModel
from .PackagesModel import PackagesModel
from .UltimakerCloudScope import UltimakerCloudScope
if TYPE_CHECKING:
from UM.TaskManagement.HttpRequestData import HttpRequestData
@ -54,7 +52,8 @@ class Toolbox(QObject, Extension):
self._download_request_data = None # type: Optional[HttpRequestData]
self._download_progress = 0 # type: float
self._is_downloading = False # type: bool
self._scope = UltimakerCloudScope(application) # type: UltimakerCloudScope
self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope
self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope
self._request_urls = {} # type: Dict[str, str]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
@ -151,7 +150,7 @@ class Toolbox(QObject, Extension):
url = "{base_url}/packages/{package_id}/ratings".format(base_url = CloudApiModel.api_url, package_id = package_id)
data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating)
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope)
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._json_scope)
def getLicenseDialogPluginFileLocation(self) -> str:
return self._license_dialog_plugin_file_location
@ -541,7 +540,7 @@ class Toolbox(QObject, Extension):
self._application.getHttpRequestManager().get(url,
callback = callback,
error_callback = error_callback,
scope=self._scope)
scope=self._json_scope)
@pyqtSlot(str)
def startDownload(self, url: str) -> None:
@ -554,7 +553,7 @@ class Toolbox(QObject, Extension):
callback = callback,
error_callback = error_callback,
download_progress_callback = download_progress_callback,
scope=self._scope
scope=self._cloud_scope
)
self._download_request_data = request_data

View file

@ -9,18 +9,16 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from UM.Logger import Logger
from cura import UltimakerCloudAuthentication
from cura.API import Account
from cura.UltimakerCloud import UltimakerCloudAuthentication
from .ToolPathUploader import ToolPathUploader
from ..Models.BaseModel import BaseModel
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Models.Http.CloudError import CloudError
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
from ..Models.Http.CloudError import CloudError
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
from ..Models.Http.CloudPrintResponse import CloudPrintResponse
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
## The generic type variable used to document the methods below.
CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel)

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -51,7 +51,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 40

View file

@ -23,7 +23,7 @@
},
"material_diameter": { "default_value": 1.75 },
"layer_height_0": { "default_value": 0.2 },
"wall_thickness": { "default_value": 1.2 },
"wall_thickness": { "value": "1.2" },
"speed_print": { "default_value": 40 },
"support_enable": { "default_value": true },
"retraction_enable": { "default_value": true },

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -21,11 +21,11 @@
"machine_height": {
"default_value": 300
},
"machine_start_gcode": {
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -21,11 +21,11 @@
"machine_height": {
"default_value": 400
},
"machine_start_gcode": {
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -21,11 +21,11 @@
"machine_height": {
"default_value": 400
},
"machine_start_gcode": {
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -21,11 +21,11 @@
"machine_height": {
"default_value": 250
},
"machine_start_gcode": {
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -21,11 +21,11 @@
"machine_height": {
"default_value": 400
},
"machine_start_gcode": {
"machine_start_gcode": {
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -25,7 +25,7 @@
"default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform"
},
"machine_end_gcode": {
"default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84"
}
"default_value": "M104 S0\nM140 S0\nG92 E0\nG1 E-10 F2000\nG28 X0 Y0\nM84"
}
}
}

View file

@ -49,7 +49,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -26,7 +26,7 @@
"machine_center_is_zero": { "default_value": false },
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.2 },
"wall_thickness": { "default_value": 1.2 },
"wall_thickness": { "value": "1.2" },
"top_bottom_thickness": { "default_value": 1.2 },
"infill_sparse_density": { "default_value": 20 },
"speed_print": { "default_value": 60 },

View file

@ -48,7 +48,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -49,7 +49,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -48,7 +48,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"top_bottom_thickness": {
"default_value": 1.2

View file

@ -54,7 +54,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"top_bottom_thickness": {
"default_value": 0.6

View file

@ -41,7 +41,7 @@
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": { "default_value": "G21 ;metric values\nG28 ;home all\nG90 ;absolute positioning\nM107 ;start with the fan off\nG1 F2400 Z15.0 ;raise the nozzle 15mm\nM109 S{material_print_temperature} ;Set Extruder Temperature and Wait\nM190 S{material_bed_temperature}; Wait for bed temperature to reach target temp\nT0 ;Switch to Extruder 1\nG1 F3000 X5 Y10 Z0.2 ;move to prime start position\nG92 E0 ;reset extrusion distance\nG1 F600 X160 E15 ;prime nozzle in a line\nG1 F5000 X180 ;quick wipe\nG92 E0 ;reset extrusion distance" },
"machine_end_gcode": { "default_value": "M104 S0 ;hotend off\nM140 S0 ;bed off\nG92 E0\nG1 F2000 E-100 ;retract filament 100mm\nG92 E0\nG1 F3000 X0 Y270 ;move bed for easy part removal\nM84 ;disable steppers" },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 }
}
}

View file

@ -46,7 +46,7 @@
"max_skin_angle_for_expansion": { "default_value": 90 },
"skin_angles": { "default_value": "[135,45]" },
"coasting_volume": { "default_value": 0.032 },
"wall_thickness": { "default_value": 1.2 },
"wall_thickness": { "value": "1.2" },
"cool_min_layer_time_fan_speed_max": { "default_value": 15 },
"cool_min_layer_time": { "default_value": 15 },
"support_interface_pattern": { "default_value": "zigzag" },

View file

@ -3,7 +3,7 @@
"version": 2,
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"visible": false,
"author": "Ultimaker",
"manufacturer": "Custom",
"file_formats": "text/x-gcode",

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1
"value": "1"
},
"top_bottom_thickness": {
"default_value": 1

View file

@ -0,0 +1,58 @@
{
"version": 2,
"name": "FabX Pro",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "FabX",
"manufacturer": "FabX",
"file_formats": "text/x-gcode",
"platform": "fabxpro_platform.STL",
"has_materials": true,
"has_machine_quality": true,
"machine_extruder_trains":
{
"0": "fabxpro_extruder_0"
}
},
"overrides": {
"machine_name": { "default_value": "FabX Pro" },
"machine_width": {
"default_value": 170
},
"machine_height": {
"default_value": 170
},
"machine_depth": {
"default_value": 170
},
"machine_center_is_zero": {
"default_value": false
},
"machine_heated_bed": {
"default_value": true
},
"machine_head_with_fans_polygon":
{
"default_value": [
[ -75, 35 ],
[ -75, -18 ],
[ 18, 35 ],
[ 18, -18 ]
]
},
"gantry_height": {
"value": "55"
},
"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 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3 mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..."
},
"machine_end_gcode": {
"value": "'M104 S0 ;extruder heater off' + ('\\nM140 S0 ;heated bed heater off' if machine_heated_bed else '') + '\\nG91 ;relative positioning\\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\\nM84 ;steppers off\\nG90 ;absolute positioning'"
}
}
}

View file

@ -75,7 +75,7 @@
"material_guid":
{
"label": "Material GUID",
"description": "GUID of the material. This is set automatically. ",
"description": "GUID of the material. This is set automatically.",
"default_value": "",
"type": "str",
"enabled": false
@ -1032,6 +1032,7 @@
"description": "The thickness of the walls in the horizontal direction. This value divided by the wall line width defines the number of walls.",
"unit": "mm",
"default_value": 0.8,
"value": "wall_line_width_0 if magic_spiralize else 0.8",
"minimum_value": "0",
"minimum_value_warning": "line_width",
"maximum_value_warning": "10 * line_width",
@ -3642,7 +3643,7 @@
"retraction_enable":
{
"label": "Enable Retraction",
"description": "Retract the filament when the nozzle is moving over a non-printed area. ",
"description": "Retract the filament when the nozzle is moving over a non-printed area.",
"type": "bool",
"default_value": true,
"settable_per_mesh": false,
@ -4438,7 +4439,7 @@
"support_xy_distance_overhang":
{
"label": "Minimum Support X/Y Distance",
"description": "Distance of the support structure from the overhang in the X/Y directions. ",
"description": "Distance of the support structure from the overhang in the X/Y directions.",
"unit": "mm",
"type": "float",
"minimum_value": "0",
@ -5882,7 +5883,7 @@
"label": "Mesh Fixes",
"type": "category",
"icon": "category_fixes",
"description": "category_fixes",
"description": "Make the meshes more suited for 3D printing.",
"children":
{
"meshfix_union_all":
@ -6008,7 +6009,7 @@
"label": "Special Modes",
"type": "category",
"icon": "category_blackmagic",
"description": "category_blackmagic",
"description": "Non-traditional ways to print your models.",
"children":
{
"print_sequence":
@ -6181,7 +6182,7 @@
"label": "Experimental",
"type": "category",
"icon": "category_experimental",
"description": "experimental!",
"description": "Features that haven't completely been fleshed out yet.",
"children":
{
"support_tree_enable":

View file

@ -26,7 +26,7 @@
"layer_height_0": { "default_value": 0.2 },
"infill_sparse_density": { "default_value": 20 },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 },
"machine_width": { "default_value": 240 },

View file

@ -25,7 +25,7 @@
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.3 },
"infill_sparse_density": { "default_value": 20 },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 },
"infill_pattern": { "value": "'tetrahedral'" },

View file

@ -45,7 +45,7 @@
"machine_max_jerk_e": { "default_value": 2.5 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.50\nM163 S1 P0.50\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28"
"default_value": ";GeeeTech A10M start script\nG28 ;home\nG90 ;absolute positioning\nG1 X0 Y0 Z15 E0 F300 ;go to wait position\nM140 S{material_bed_temperature_layer_0} ;set bed temp\nM190 S{material_print_temperature_layer_0} ;set extruder temp and wait\nG1 Z0.8 F200 ;set extruder height\nG1 X220 Y0 E80 F1000 ;purge line\n;end of start script"
},
"machine_end_gcode": {
"default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84"

View file

@ -45,7 +45,7 @@
"machine_max_jerk_e": { "default_value": 2.5 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.50\nM163 S1 P0.50\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28"
"default_value": ";GeeeTech A20M start script\nG28 ;home\nG90 ;absolute positioning\nG1 X0 Y0 Z15 E0 F300 ;go to wait position\nM140 S{material_bed_temperature_layer_0} ;set bed temp\nM190 S{material_print_temperature_layer_0} ;set extruder temp and wait\nG1 Z0.8 F200 ;set extruder height\nG1 X220 Y0 E80 F1000 ;purge line\n;end of start script"
},
"machine_end_gcode": {
"default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84"

View file

@ -48,7 +48,7 @@
"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 X/Y/Z\nM104 S{material_print_temperature} ; Preheat\nM109 S{material_print_temperature} ; Preheat\nG91 ;relative positioning\nG90 ;absolute positioning\nG1 Z25.0 F9000 ;raise nozzle 25mm\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\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 }
}
}

View file

@ -46,7 +46,7 @@
"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 X/Y/Z\nM104 S{material_print_temperature} T0 ; Preheat Left Extruder\nM104 S{material_print_temperature} T1 ; Preheat Right Extruder\nM109 S{material_print_temperature} T0 ; Preheat Left Extruder\nM109 S{material_print_temperature} T1 ; Preheat Right Extruder\nG91 ;relative positioning\nG90 ;absolute positioning\nM218 T1 X34.3 Y0; Set 2nd extruder offset. This can be changed later if needed\nG1 Z25.0 F9000 ;raise nozzle 25mm\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 T0;Left extruder off\nM104 S0 T1; Right extruder 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\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 }
}
}

View file

@ -28,7 +28,7 @@
"machine_center_is_zero": { "default_value": false },
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.2 },
"wall_thickness": { "default_value": 1.2 },
"wall_thickness": { "value": "1.2" },
"top_bottom_thickness": { "default_value": 1.2 },
"infill_sparse_density": { "default_value": 20 },
"speed_print": { "default_value": 60 },

View file

@ -69,7 +69,7 @@
"material_bed_temp_wait": {"default_value": false },
"machine_max_feedrate_z": {"default_value": 10 },
"machine_acceleration": {"default_value": 180 },
"machine_start_gcode": {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM140 S{material_bed_temperature_layer_0}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-30 Y100 F12000\n\nM190 S{material_bed_temperature_layer_0}\nM117 HMS434 Printing ...\n\n" },
"machine_start_gcode": {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM140 S{material_bed_temperature_layer_0}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-30 Y100 F12000\n\nM190 S{material_bed_temperature_layer_0}\nM117 HMS434 Printing ...\n\nM42 P10 S255 ; chamberfans on" },
"machine_end_gcode": {"default_value": "" },
"retraction_extra_prime_amount": {"minimum_value_warning": "-2.0" },
@ -95,12 +95,11 @@
"alternate_extra_perimeter": {"value": false },
"filter_out_tiny_gaps": {"value": true },
"fill_outline_gaps": {"value": true },
"z_seam_type": {"value": "'back'"},
"z_seam_type": {"value": "'shortest'"},
"z_seam_x": {"value": "300"},
"z_seam_y": {"value": "325"},
"z_seam_corner": {"value": "'z_seam_corner_inner'"},
"skin_outline_count": {"value": "0"},
"ironing_enabled": {"value": true },
"ironing_line_spacing": {"value": "line_width / 4 * 3"},
"ironing_flow": {"value": "0"},
"ironing_inset": {"value": "ironing_line_spacing"},
@ -108,9 +107,6 @@
"infill_sparse_density": {"value": 30},
"infill_pattern": {"value": "'lines'"},
"infill_overlap": {"value": 5},
"skin_overlap": {"value": 5},
"infill_wipe_dist": {"value": 0.0},
"infill_before_walls": {"value": false},
"material_print_temperature_layer_0": {"value": "material_print_temperature"},
@ -118,15 +114,10 @@
"maximum_value_warning": "material_print_temperature + 15"},
"material_final_print_temperature": {"value": "material_print_temperature"},
"material_bed_temperature_layer_0": {"value": "material_bed_temperature"},
"material_flow": {"value": "100"},
"material_flow_layer_0": {"value": "material_flow"},
"retraction_enable": {"value": true },
"retract_at_layer_change": {"value": true },
"retraction_amount": {"value": "1"},
"retraction_speed": {"value": "20"},
"retraction_prime_speed": {"value": "8"},
"retraction_min_travel": {"value": "(round(line_width * 10))"},
"switch_extruder_retraction_amount": {"value": 2},
"switch_extruder_retraction_speeds": {"value": "(retraction_speed)"},
"switch_extruder_prime_speed": {"value": "(retraction_prime_speed)"},
@ -136,7 +127,7 @@
"speed_wall_x": {"value": "speed_wall"},
"speed_layer_0": {"value": "(speed_print/5*4) if speed_print > 45 else speed_print"},
"speed_topbottom": {"value": "speed_layer_0"},
"speed_travel": {"value": "150"},
"speed_travel": {"value": "250"},
"speed_travel_layer_0": {"value": "speed_travel"},
"speed_support_interface": {"value": "speed_topbottom"},
"speed_z_hop": {"value": 10},
@ -157,7 +148,8 @@
"cool_min_speed": {"value": "5"},
"cool_lift_head": {"value": false},
"support_infill_rate": {"value": 25},
"support_pattern": {"value": "'grid'"},
"support_infill_rate": {"value": 30},
"support_z_distance": {"value": 0},
"support_xy_distance": {"value": 0.4},
"support_join_distance": {"value": 10},
@ -167,7 +159,7 @@
"support_interface_height": {"value": "layer_height * 3"},
"support_bottom_height": {"value": "layer_height"},
"adhesion_type": {"value": "'skirt'"},
"adhesion_type": {"value": "'brim'"},
"skirt_gap": {"value": 1},
"skirt_brim_minimal_length": {"value": 50},
@ -182,7 +174,7 @@
"meshfix_maximum_deviation": {"value": 0.01 },
"minimum_polygon_circumference": {"value": 0.05 },
"coasting_enable": {"value": true},
"coasting_enable": {"value": false},
"coasting_volume": {"value": 0.1},
"coasting_min_volume": {"value": 0.17},
"coasting_speed": {"value": 90},

View file

@ -56,7 +56,7 @@
"default_value": 0.15
},
"wall_thickness": {
"default_value": 0.8
"value": "0.8"
},
"top_bottom_thickness": {
"default_value": 1.2

View file

@ -51,7 +51,7 @@
"default_value": 0.12
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 40

View file

@ -51,7 +51,7 @@
"default_value": 0.12
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 35

View file

@ -53,7 +53,7 @@
"default_value": 0.12
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 40

View file

@ -51,7 +51,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 60

View file

@ -51,7 +51,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"speed_print": {
"default_value": 60

View file

@ -81,7 +81,7 @@
"default_value": 60
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"cool_min_layer_time_fan_speed_max": {
"default_value": 5

View file

@ -81,7 +81,7 @@
"default_value": 60
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"cool_min_layer_time_fan_speed_max": {
"default_value": 5

View file

@ -81,7 +81,7 @@
"default_value": 60
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"cool_min_layer_time_fan_speed_max": {
"default_value": 5

View file

@ -39,7 +39,7 @@
},
"layer_height": { "default_value": 0.2 },
"wall_thickness": { "default_value": 0.8 },
"wall_thickness": { "value": "0.8" },
"top_bottom_thickness": { "default_value": 0.3 },
"retraction_enable": { "default_value": true },
"retraction_speed": { "default_value": 50 },

View file

@ -189,7 +189,7 @@
"value": "machine_nozzle_size / 3"
},
"wall_thickness": {
"value": 0.5
"value": "0.5"
},
"infill_sparse_density": {
"value": 70

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 0.8
"value": "0.8"
},
"top_bottom_thickness": {
"default_value": 0.3

View file

@ -44,7 +44,7 @@
"default_value": 0.2
},
"wall_thickness": {
"default_value": 0.8
"value": "0.8"
},
"top_bottom_thickness": {
"default_value": 0.3

View file

@ -31,7 +31,7 @@
"layer_height_0": { "default_value": 0.15 },
"material_flow": { "default_value": 100 },
"infill_sparse_density": { "default_value": 10 },
"wall_thickness": { "default_value": 1.2 },
"wall_thickness": { "value": "1.2" },
"retraction_amount": { "default_value": 3 },
"layer_height": { "default_value": 0.2 },
"speed_print": { "default_value": 40 },

View file

@ -322,7 +322,7 @@
"default_value": 2
},
"wall_thickness": {
"default_value": 1.2
"value": "1.2"
},
"support_infill_sparse_thickness": {
"value": "resolveOrValue('layer_height')"

View file

@ -21,7 +21,7 @@
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.3 },
"infill_sparse_density": { "default_value": 5 },
"wall_thickness": { "default_value": 1 },
"wall_thickness": { "value": "1" },
"top_bottom_thickness": { "default_value": 1 },
"infill_pattern": { "value": "'tetrahedral'" },

View file

@ -49,7 +49,7 @@
"value": 0.35
},
"wall_thickness": {
"value": 0.7
"value": "0.7"
},
"top_bottom_thickness": {
"value": 0.6

View file

@ -47,7 +47,7 @@
"value": 0.35
},
"wall_thickness": {
"value": 0.7
"value": "0.7"
},
"top_bottom_thickness": {
"value": 0.6

View file

@ -0,0 +1,15 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "fabxpro",
"position": "0"
},
"overrides": {
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -16,7 +16,7 @@
"machine_nozzle_offset_y": { "default_value": 0.0 },
"material_diameter": { "default_value": 1.75 },
"machine_extruder_start_code": {
"default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 Y120 F3000\nG1 X10 F12000\nG1 E-{switch_extruder_retraction_amount} F2400\n\n"
"default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y120 F3000\nG1 X10 F12000\n\n"
},
"machine_extruder_end_code": {
"default_value": "\nG1 X10 Y120 F12000\nG1 X-40 F12000\nM109 T0 R{material_standby_temperature}\nG1 Y100 F3000\n; ending tool1\n\n"

View file

@ -16,7 +16,7 @@
"machine_nozzle_offset_y": { "default_value": 0.0 },
"material_diameter": { "default_value": 1.75 },
"machine_extruder_start_code": {
"default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 E{switch_extruder_extra_prime_amount} F360\nG1 Y120 F3000\nG1 X10 F12000\nG1 E-{switch_extruder_retraction_amount} F2400\n\n"
"default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nG1 E{switch_retraction_prime_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y120 F3000\nG1 X10 F12000\n\n"
},
"machine_extruder_end_code": {
"default_value": "\nG1 X10 Y120 F12000\nG1 X-40 F12000\nM109 T1 R{material_standby_temperature}\nG1 Y100 F3000\n; ending tool2\n\n"

5674
resources/i18n/hu_HU/cura.po Normal file

File diff suppressed because it is too large Load diff

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