mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'master' into feature_unify_pause_at_height
This commit is contained in:
commit
7ea3891da0
168 changed files with 15794 additions and 491 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -53,6 +53,7 @@ plugins/GodMode
|
|||
plugins/OctoPrintPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/SettingsGuide
|
||||
plugins/SVGToolpathReader
|
||||
plugins/X3GWriter
|
||||
|
||||
#Build stuff
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
0
cura/UltimakerCloud/__init__.py
Normal file
0
cura/UltimakerCloud/__init__.py
Normal 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()
|
||||
|
|
|
@ -88,6 +88,9 @@ class ThreeMFReader(MeshReader):
|
|||
# \returns Scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
self._object_count += 1
|
||||
|
||||
node_name = savitar_node.getName()
|
||||
if node_name == "":
|
||||
node_name = "Object %s" % self._object_count
|
||||
|
||||
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
|
|
|
@ -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,6 +42,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
try:
|
||||
# Add global container stack data to the archive.
|
||||
self._writeContainerToArchive(global_stack, archive)
|
||||
|
||||
|
@ -49,6 +55,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
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.
|
||||
|
@ -59,6 +69,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
preferences_string = StringIO()
|
||||
temp_preferences.writeToFile(preferences_string)
|
||||
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
|
||||
try:
|
||||
archive.writestr(preferences_file, preferences_string.getvalue())
|
||||
|
||||
# Save Cura version
|
||||
|
@ -77,6 +88,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
|
||||
# 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
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
return
|
||||
|
||||
savitar_node = Savitar.SceneNode()
|
||||
savitar_node.setName(um_node.getName())
|
||||
|
||||
node_matrix = um_node.getLocalTransformation()
|
||||
|
||||
|
|
134
plugins/CuraDrive/src/CreateBackupJob.py
Normal file
134
plugins/CuraDrive/src/CreateBackupJob.py
Normal 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()
|
|
@ -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 = backup_list_request.json()
|
||||
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))
|
||||
return []
|
||||
Logger.log("w", "Could not get backups from remote, actual response body was: %s",
|
||||
str(backup_list_response))
|
||||
changed([]) # empty list of backups
|
||||
return
|
||||
|
||||
return backup_list_response["data"]
|
||||
changed(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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
# 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)
|
||||
|
||||
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()
|
||||
|
||||
# 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 _emitRestoreError(self) -> None:
|
||||
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
|
||||
|
||||
restore_backup_job = RestoreBackupJob(backup)
|
||||
restore_backup_job.finished.connect(self._onRestoreFinished)
|
||||
restore_backup_job.start()
|
||||
|
||||
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)
|
||||
|
||||
def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool], None]):
|
||||
|
||||
def finishedCallback(reply: QNetworkReply, ca: Callable[[bool], None] = finished_callable) -> None:
|
||||
self._onDeleteRequestCompleted(reply, ca)
|
||||
|
||||
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca: Callable[[bool], None] = finished_callable) -> None:
|
||||
self._onDeleteRequestCompleted(reply, ca, error)
|
||||
|
||||
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))
|
||||
|
|
|
@ -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._drive_api_service.deleteBackup(backup_id, self._backupDeletedCallback)
|
||||
|
||||
def _backupDeletedCallback(self, success: bool):
|
||||
if success:
|
||||
self.refreshBackups()
|
||||
|
|
92
plugins/CuraDrive/src/RestoreBackupJob.py
Normal file
92
plugins/CuraDrive/src/RestoreBackupJob.py
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -58,6 +58,17 @@ class PauseAtHeight(Script):
|
|||
"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":
|
||||
{
|
||||
"label": "Park Print Head 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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
0
plugins/PostProcessingPlugin/tests/__init__.py
Normal file
0
plugins/PostProcessingPlugin/tests/__init__.py
Normal 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
|
||||
|
|
3
plugins/Toolbox/resources/images/placeholder.svg
Normal file
3
plugins/Toolbox/resources/images/placeholder.svg
Normal 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 |
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Union
|
||||
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
from cura import ApplicationMetadata
|
||||
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
||||
|
||||
|
||||
class CloudApiModel:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 40
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1.2
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 0.6
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"visible": false,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Custom",
|
||||
"file_formats": "text/x-gcode",
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1
|
||||
"value": "1"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1
|
||||
|
|
58
resources/definitions/fabxpro.def.json
Normal file
58
resources/definitions/fabxpro.def.json
Normal 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'"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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":
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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'" },
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"default_value": 0.15
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 0.8
|
||||
"value": "0.8"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 1.2
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"default_value": 0.12
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 40
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"default_value": 0.12
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 35
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"default_value": 0.12
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 40
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 60
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"speed_print": {
|
||||
"default_value": 60
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
"value": "machine_nozzle_size / 3"
|
||||
},
|
||||
"wall_thickness": {
|
||||
"value": 0.5
|
||||
"value": "0.5"
|
||||
},
|
||||
"infill_sparse_density": {
|
||||
"value": 70
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 0.8
|
||||
"value": "0.8"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 0.3
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"default_value": 0.2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 0.8
|
||||
"value": "0.8"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"default_value": 0.3
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -322,7 +322,7 @@
|
|||
"default_value": 2
|
||||
},
|
||||
"wall_thickness": {
|
||||
"default_value": 1.2
|
||||
"value": "1.2"
|
||||
},
|
||||
"support_infill_sparse_thickness": {
|
||||
"value": "resolveOrValue('layer_height')"
|
||||
|
|
|
@ -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'" },
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"value": 0.35
|
||||
},
|
||||
"wall_thickness": {
|
||||
"value": 0.7
|
||||
"value": "0.7"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"value": 0.6
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"value": 0.35
|
||||
},
|
||||
"wall_thickness": {
|
||||
"value": 0.7
|
||||
"value": "0.7"
|
||||
},
|
||||
"top_bottom_thickness": {
|
||||
"value": 0.6
|
||||
|
|
15
resources/extruders/fabxpro_extruder_0.def.json
Normal file
15
resources/extruders/fabxpro_extruder_0.def.json
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
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
Loading…
Add table
Add a link
Reference in a new issue