Merge remote-tracking branch 'upstream/master' into dagoma_discoultimate

This commit is contained in:
Orel 2020-02-04 10:07:56 +01:00
commit 173a05ddb5
71 changed files with 1912 additions and 299 deletions

View File

@ -56,6 +56,13 @@ function(cura_add_test)
endif() endif()
endfunction() endfunction()
#Add test for import statements which are not compatible with all builds
add_test(
NAME "invalid-imports"
COMMAND ${Python3_EXECUTABLE} scripts/check_invalid_imports.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}") cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
file(GLOB_RECURSE _plugins plugins/*/__init__.py) file(GLOB_RECURSE _plugins plugins/*/__init__.py)

View File

@ -28,9 +28,10 @@ class CuraAPI(QObject):
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication. # The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
# Since the API is intended to be used by plugins, the cura application should have already created this. # Since the API is intended to be used by plugins, the cura application should have already created this.
def __new__(cls, application: Optional["CuraApplication"] = None): def __new__(cls, application: Optional["CuraApplication"] = None):
if cls.__instance is None: if cls.__instance is not None:
raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__))
if application is None: if application is None:
raise Exception("Upon first time creation, the application must be set.") raise RuntimeError("Upon first time creation, the application must be set.")
cls.__instance = super(CuraAPI, cls).__new__(cls) cls.__instance = super(CuraAPI, cls).__new__(cls)
cls._application = application cls._application = application
return cls.__instance return cls.__instance

View File

@ -145,6 +145,14 @@ class Backup:
# \return Whether we had success or not. # \return Whether we had success or not.
@staticmethod @staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool: def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
# Implement security recommendations: Sanity check on zip files will make it harder to spoof.
from cura.CuraApplication import CuraApplication
config_filename = CuraApplication.getInstance().getApplicationName() + ".cfg" # Should be there if valid.
if config_filename not in [file.filename for file in archive.filelist]:
Logger.logException("e", "Unable to extract the backup due to corruption of compressed file(s).")
return False
Logger.log("d", "Removing current data in location: %s", target_path) Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
@ -191,8 +191,6 @@ class CuraApplication(QtApplication):
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None
self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager] self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager]
self.empty_container = None # type: EmptyInstanceContainer self.empty_container = None # type: EmptyInstanceContainer
@ -632,6 +630,12 @@ class CuraApplication(QtApplication):
def showPreferences(self) -> None: def showPreferences(self) -> None:
self.showPreferencesWindow.emit() self.showPreferencesWindow.emit()
# This is called by drag-and-dropping curapackage files.
@pyqtSlot(QUrl)
def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]:
filename = QUrl(file_url).toLocalFile()
return self._package_manager.installPackage(filename)
@override(Application) @override(Application)
def getGlobalContainerStack(self) -> Optional["GlobalStack"]: def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
return self._global_container_stack return self._global_container_stack
@ -1827,11 +1831,17 @@ class CuraApplication(QtApplication):
def _onContextMenuRequested(self, x: float, y: float) -> None: def _onContextMenuRequested(self, x: float, y: float) -> None:
# Ensure we select the object if we request a context menu over an object without having a selection. # Ensure we select the object if we request a context menu over an object without having a selection.
if not Selection.hasSelection(): if Selection.hasSelection():
node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y)) return
if node: selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
print("--------------ding! Got the crash.")
return
node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
if not node:
return
parent = node.getParent() parent = node.getParent()
while(parent and parent.callDecoration("isGroup")): while parent and parent.callDecoration("isGroup"):
node = parent node = parent
parent = node.getParent() parent = node.getParent()

View File

@ -115,9 +115,10 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
## Generate a 16-character verification code. ## Generate a verification code of arbitrary length.
# \param code_length: How long should the code be? # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
def generateVerificationCode(code_length: int = 16) -> str: # leave it at 32
def generateVerificationCode(code_length: int = 32) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod

View File

@ -25,6 +25,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str] self.verification_code = None # type: Optional[str]
self.state = None # type: Optional[str]
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback. # CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
def do_HEAD(self) -> None: def do_HEAD(self) -> None:
self.do_GET() self.do_GET()
@ -58,7 +60,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# \return HTTP ResponseData containing a success page to show to the user. # \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
code = self._queryGet(query, "code") code = self._queryGet(query, "code")
if code and self.authorization_helpers is not None and self.verification_code is not None: state = self._queryGet(query, "state")
if state != self.state:
token_response = AuthenticationResponse(
success = False,
err_message=catalog.i18nc("@message",
"The provided state is not correct.")
)
elif code and self.authorization_helpers is not None and self.verification_code is not None:
# If the code was returned we get the access token. # If the code was returned we get the access token.
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
code, self.verification_code) code, self.verification_code)

View File

@ -25,3 +25,6 @@ class AuthorizationRequestServer(HTTPServer):
## Set the verification code on the request handler. ## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None: def setVerificationCode(self, verification_code: str) -> None:
self.RequestHandlerClass.verification_code = verification_code # type: ignore self.RequestHandlerClass.verification_code = verification_code # type: ignore
def setState(self, state: str) -> None:
self.RequestHandlerClass.state = state # type: ignore

View File

@ -153,13 +153,15 @@ class AuthorizationService:
verification_code = self._auth_helpers.generateVerificationCode() verification_code = self._auth_helpers.generateVerificationCode()
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code) challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
state = AuthorizationHelpers.generateVerificationCode()
# Create the query string needed for the OAuth2 flow. # Create the query string needed for the OAuth2 flow.
query_string = urlencode({ query_string = urlencode({
"client_id": self._settings.CLIENT_ID, "client_id": self._settings.CLIENT_ID,
"redirect_uri": self._settings.CALLBACK_URL, "redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES, "scope": self._settings.CLIENT_SCOPES,
"response_type": "code", "response_type": "code",
"state": "(.Y.)", "state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
"code_challenge": challenge_code, "code_challenge": challenge_code,
"code_challenge_method": "S512" "code_challenge_method": "S512"
}) })
@ -168,7 +170,7 @@ class AuthorizationService:
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
# Start a local web server to receive the callback URL on. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code, state)
## Callback method for the authentication flow. ## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:

View File

@ -36,7 +36,8 @@ class LocalAuthorizationServer:
## Starts the local web server to handle the authorization callback. ## Starts the local web server to handle the authorization callback.
# \param verification_code The verification code part of the OAuth2 client identification. # \param verification_code The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None: # \param state The unique state code (to ensure that the request we get back is really from the server.
def start(self, verification_code: str, state: str) -> None:
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though. # We still inject the new verification code though.
@ -53,6 +54,7 @@ class LocalAuthorizationServer:
self._web_server.setAuthorizationHelpers(self._auth_helpers) self._web_server.setAuthorizationHelpers(self._auth_helpers)
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
self._web_server.setVerificationCode(verification_code) self._web_server.setVerificationCode(verification_code)
self._web_server.setState(state)
# Start the server on a new thread. # Start the server on a new thread.
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)

View File

@ -747,6 +747,11 @@ class MachineManager(QObject):
result = [] # type: List[str] result = [] # type: List[str]
for setting_instance in container.findInstances(): for setting_instance in container.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
if setting_key == "print_sequence":
old_value = container.getProperty(setting_key, "value")
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
result.append(setting_key)
continue
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue continue

View File

@ -29,9 +29,13 @@ parser.add_argument("--debug",
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
if with_sentry_sdk: if with_sentry_sdk:
sentry_env = "production" sentry_env = "unknown" # Start off with a "IDK"
if ApplicationMetadata.CuraVersion == "master": if hasattr(sys, "frozen"):
sentry_env = "production" # A frozen build is a "real" distribution.
elif ApplicationMetadata.CuraVersion == "master":
sentry_env = "development" sentry_env = "development"
elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
sentry_env = "beta"
try: try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99": if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
sentry_env = "nightly" sentry_env = "nightly"

View File

@ -13,6 +13,8 @@ export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
cd "${PROJECT_DIR}" cd "${PROJECT_DIR}"
# #
# Clone Uranium and set PYTHONPATH first # Clone Uranium and set PYTHONPATH first
# #

View File

@ -4,7 +4,8 @@
from configparser import ConfigParser from configparser import ConfigParser
import zipfile import zipfile
import os import os
from typing import cast, Dict, List, Optional, Tuple import json
from typing import cast, Dict, List, Optional, Tuple, Any
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes
return nodes, self._loadMetadata(file_name)
@staticmethod
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
archive = zipfile.ZipFile(file_name, "r")
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
result = dict()
for metadata_file in metadata_files:
try:
plugin_id = metadata_file.split("/")[0]
result[plugin_id] = json.loads(archive.open("%s/plugin_metadata.json" % plugin_id).read().decode("utf-8"))
except Exception:
Logger.logException("w", "Unable to retrieve metadata for %s", metadata_file)
return result
def _processQualityChanges(self, global_stack): def _processQualityChanges(self, global_stack):
if self._machine_info.quality_changes_info is None: if self._machine_info.quality_changes_info is None:

View File

@ -73,11 +73,25 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
version_config_parser.write(version_file_string) version_config_parser.write(version_file_string)
archive.writestr(version_file, version_file_string.getvalue()) archive.writestr(version_file, version_file_string.getvalue())
self._writePluginMetadataToArchive(archive)
# Close the archive & reset states. # Close the archive & reset states.
archive.close() archive.close()
mesh_writer.setStoreArchive(False) mesh_writer.setStoreArchive(False)
return True return True
@staticmethod
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
file_name_template = "%s/plugin_metadata.json"
for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
file_name = file_name_template % plugin_id
file_in_archive = zipfile.ZipInfo(file_name)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
import json
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
# \param container That follows the \type{ContainerInterface} to archive. # \param container That follows the \type{ContainerInterface} to archive.
# \param archive The archive to write to. # \param archive The archive to write to.

View File

@ -1,5 +1,6 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Uranium is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Mesh.MeshWriter import MeshWriter from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter):
} }
self._unit_matrix_string = self._convertMatrixToString(Matrix()) self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None self._archive = None # type: Optional[zipfile.ZipFile]
self._store_archive = False self._store_archive = False
def _convertMatrixToString(self, matrix): def _convertMatrixToString(self, matrix):

View File

@ -72,16 +72,23 @@ class DisplayFilenameAndLayerOnLCD(Script):
lcd_text = "M117 Printing " + name + " - Layer " lcd_text = "M117 Printing " + name + " - Layer "
i = self.getSettingValueByKey("startNum") i = self.getSettingValueByKey("startNum")
for layer in data: for layer in data:
display_text = lcd_text + str(i) + " " + name display_text = lcd_text + str(i)
layer_index = data.index(layer) layer_index = data.index(layer)
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if line.startswith(";LAYER_COUNT:"): if line.startswith(";LAYER_COUNT:"):
max_layer = line max_layer = line
max_layer = max_layer.split(":")[1] max_layer = max_layer.split(":")[1]
if self.getSettingValueByKey("startNum") == 0:
max_layer = str(int(max_layer) - 1)
if line.startswith(";LAYER:"): if line.startswith(";LAYER:"):
if self.getSettingValueByKey("maxlayer"): if self.getSettingValueByKey("maxlayer"):
display_text = display_text + " of " + max_layer display_text = display_text + " of " + max_layer
if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name
else:
if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name + "!"
else: else:
display_text = display_text + "!" display_text = display_text + "!"
line_index = lines.index(line) line_index = lines.index(line)

View File

@ -2,6 +2,7 @@
from ..Script import Script from ..Script import Script
class TimeLapse(Script): class TimeLapse(Script):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -75,21 +76,29 @@ class TimeLapse(Script):
trigger_command = self.getSettingValueByKey("trigger_command") trigger_command = self.getSettingValueByKey("trigger_command")
pause_length = self.getSettingValueByKey("pause_length") pause_length = self.getSettingValueByKey("pause_length")
gcode_to_append = ";TimeLapse Begin\n" gcode_to_append = ";TimeLapse Begin\n"
last_x = 0
last_y = 0
if park_print_head: if park_print_head:
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + " ;Park print head\n" gcode_to_append += self.putValue(G=1, F=feed_rate,
X=x_park, Y=y_park) + " ;Park print head\n"
gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n" gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
gcode_to_append += trigger_command + " ;Snap Photo\n" gcode_to_append += trigger_command + " ;Snap Photo\n"
gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n" gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
gcode_to_append += ";TimeLapse End\n"
for layer in data: for idx, layer in enumerate(data):
for line in layer.split("\n"):
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
last_x = self.getValue(line, "X", last_x)
last_y = self.getValue(line, "Y", last_y)
# Check that a layer is being printed # Check that a layer is being printed
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if ";LAYER:" in line: if ";LAYER:" in line:
index = data.index(layer)
layer += gcode_to_append layer += gcode_to_append
data[index] = layer layer += "G0 X%s Y%s\n" % (last_x, last_y)
data[idx] = layer
break break
return data return data

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys import sys
@ -116,8 +116,9 @@ class SimulationView(CuraView):
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode() self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled."),
title = catalog.i18nc("@info:title", "Simulation View")) title = catalog.i18nc("@info:title", "Simulation View"))
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."), title = catalog.i18nc("@info:title", "No layer data"))
QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
@ -149,6 +150,7 @@ class SimulationView(CuraView):
if self._activity == activity: if self._activity == activity:
return return
self._activity = activity self._activity = activity
self._updateSliceWarningVisibility()
self.activityChanged.emit() self.activityChanged.emit()
def getSimulationPass(self) -> SimulationPass: def getSimulationPass(self) -> SimulationPass:
@ -543,11 +545,13 @@ class SimulationView(CuraView):
self._composite_pass.getLayerBindings().append("simulationview") self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader() self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._simulationview_composite_shader) self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
self._updateSliceWarningVisibility()
elif event.type == Event.ViewDeactivateEvent: elif event.type == Event.ViewDeactivateEvent:
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
self._slice_first_warning_message.hide()
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
@ -661,6 +665,12 @@ class SimulationView(CuraView):
self._updateWithPreferences() self._updateWithPreferences()
def _updateSliceWarningVisibility(self):
if not self.getActivity():
self._slice_first_warning_message.show()
else:
self._slice_first_warning_message.hide()
class _CreateTopLayersJob(Job): class _CreateTopLayersJob(Job):
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None: def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:

View File

@ -72,6 +72,7 @@ Window
right: parent.right right: parent.right
} }
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:") text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }

View File

@ -20,6 +20,8 @@ UM.Dialog{
maximumHeight: minimumHeight maximumHeight: minimumHeight
margin: 0 margin: 0
property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next")
Rectangle Rectangle
{ {
id: root id: root
@ -90,7 +92,7 @@ UM.Dialog{
Label Label
{ {
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages can not be installed because of incompatible Cura version:") text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:")
visible: subscribedPackagesModel.hasIncompatiblePackages visible: subscribedPackagesModel.hasIncompatiblePackages
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height height: contentHeight + UM.Theme.getSize("default_margin").height
@ -125,26 +127,6 @@ UM.Dialog{
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
UM.TooltipArea
{
width: childrenRect.width;
height: childrenRect.height;
text: catalog.i18nc("@info:tooltip", "Dismisses the package and won't be shown in this dialog anymore")
anchors.right: parent.right
anchors.verticalCenter: packageIcon.verticalCenter
Label
{
text: "(Dismiss)"
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text")
MouseArea
{
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: handler.dismissIncompatiblePackage(subscribedPackagesModel, model.package_id)
}
}
}
} }
} }
} }
@ -152,14 +134,16 @@ UM.Dialog{
} // End of ScrollView } // End of ScrollView
Cura.ActionButton Cura.PrimaryButton
{ {
id: nextButton id: nextButton
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height anchors.margins: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@button", "Next") text: actionButtonText
onClicked: accept() onClicked: accept()
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
} }
} }
} }

View File

@ -4,12 +4,12 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Dialogs 1.1 import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 1.4 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.6 as Cura
UM.Dialog UM.Dialog
{ {
@ -19,50 +19,90 @@ UM.Dialog
minimumHeight: UM.Theme.getSize("license_window_minimum").height minimumHeight: UM.Theme.getSize("license_window_minimum").height
width: minimumWidth width: minimumWidth
height: minimumHeight height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
margin: screenScaleFactor * 10
Item ColumnLayout
{ {
anchors.fill: parent anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
UM.I18nCatalog{id: catalog; name: "cura"} UM.I18nCatalog{id: catalog; name: "cura"}
Label Label
{ {
id: licenseHeader id: licenseHeader
anchors.top: parent.top Layout.fillWidth: true
anchors.left: parent.left text: catalog.i18nc("@label", "You need to accept the license to install the package")
anchors.right: parent.right color: UM.Theme.getColor("text")
text: licenseModel.headerText
wrapMode: Text.Wrap wrapMode: Text.Wrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
TextArea
{ Row {
id: licenseText id: packageRow
anchors.top: licenseHeader.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height height: childrenRect.height
readOnly: true spacing: UM.Theme.getSize("default_margin").width
text: licenseModel.licenseText leftPadding: UM.Theme.getSize("narrow_margin").width
Image
{
id: icon
width: 30 * screenScaleFactor
height: width
fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || "../../images/logobot.svg"
mipmap: true
} }
Label
{
id: packageName
text: licenseModel.packageName
color: UM.Theme.getColor("text")
font.bold: true
anchors.verticalCenter: icon.verticalCenter
height: contentHeight
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseModel.licenseText
textArea.readOnly: true
}
} }
rightButtons: rightButtons:
[ [
Button Cura.PrimaryButton
{ {
id: acceptButton leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
anchors.margins: UM.Theme.getSize("default_margin").width rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
text: catalog.i18nc("@action:button", "Accept")
text: licenseModel.acceptButtonText
onClicked: { handler.onLicenseAccepted() } onClicked: { handler.onLicenseAccepted() }
}, }
Button ]
leftButtons:
[
Cura.SecondaryButton
{ {
id: declineButton id: declineButton
anchors.margins: UM.Theme.getSize("default_margin").width text: licenseModel.declineButtonText
text: catalog.i18nc("@action:button", "Decline")
onClicked: { handler.onLicenseDeclined() } onClicked: { handler.onLicenseDeclined() }
} }
] ]

View File

@ -18,3 +18,11 @@ class CloudApiModel:
cloud_api_root=cloud_api_root, cloud_api_root=cloud_api_root,
cloud_api_version=cloud_api_version, cloud_api_version=cloud_api_version,
) )
## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}
@classmethod
def userPackageUrl(cls, package_id: str) -> str:
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
package_id=package_id
)

View File

@ -8,11 +8,12 @@ from UM import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication, ApplicationMetadata
from ..CloudApiModel import CloudApiModel from ..CloudApiModel import CloudApiModel
from .SubscribedPackagesModel import SubscribedPackagesModel from .SubscribedPackagesModel import SubscribedPackagesModel
from ..UltimakerCloudScope import UltimakerCloudScope from ..UltimakerCloudScope import UltimakerCloudScope
from typing import List, Dict, Any
class CloudPackageChecker(QObject): class CloudPackageChecker(QObject):
def __init__(self, application: CuraApplication) -> None: def __init__(self, application: CuraApplication) -> None:
@ -25,12 +26,12 @@ class CloudPackageChecker(QObject):
self._application.initializationFinished.connect(self._onAppInitialized) self._application.initializationFinished.connect(self._onAppInitialized)
self._i18n_catalog = i18nCatalog("cura") self._i18n_catalog = i18nCatalog("cura")
self._sdk_version = ApplicationMetadata.CuraSDKVersion
# This is a plugin, so most of the components required are not ready when # This is a plugin, so most of the components required are not ready when
# this is initialized. Therefore, we wait until the application is ready. # this is initialized. Therefore, we wait until the application is ready.
def _onAppInitialized(self) -> None: def _onAppInitialized(self) -> None:
self._package_manager = self._application.getPackageManager() self._package_manager = self._application.getPackageManager()
# initial check # initial check
self._fetchUserSubscribedPackages() self._fetchUserSubscribedPackages()
# check again whenever the login state changes # check again whenever the login state changes
@ -38,25 +39,51 @@ class CloudPackageChecker(QObject):
def _fetchUserSubscribedPackages(self) -> None: def _fetchUserSubscribedPackages(self) -> None:
if self._application.getCuraAPI().account.isLoggedIn: if self._application.getCuraAPI().account.isLoggedIn:
self._getUserPackages() self._getUserSubscribedPackages()
def _handleCompatibilityData(self, json_data) -> None: def _getUserSubscribedPackages(self) -> None:
user_subscribed_packages = [plugin["package_id"] for plugin in json_data] Logger.debug("Requesting subscribed packages metadata from server.")
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
scope = self._scope)
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w",
"Requesting user packages failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
return
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Check for errors:
if "errors" in json_data:
for error in json_data["errors"]:
Logger.log("e", "%s", error["title"])
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
user_subscribed_packages = [plugin["package_id"] for plugin in subscribed_packages_payload]
user_installed_packages = self._package_manager.getUserInstalledPackages() user_installed_packages = self._package_manager.getUserInstalledPackages()
# We need to re-evaluate the dismissed packages
# (i.e. some package might got updated to the correct SDK version in the meantime,
# hence remove them from the Dismissed Incompatible list)
self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version)
user_dismissed_packages = self._package_manager.getDismissedPackages() user_dismissed_packages = self._package_manager.getDismissedPackages()
if user_dismissed_packages: if user_dismissed_packages:
user_installed_packages += user_dismissed_packages user_installed_packages += user_dismissed_packages
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages)) package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
self._model.setMetadata(json_data)
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize()
if not self._model.hasCompatiblePackages:
return None
if package_discrepancy: if package_discrepancy:
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize(subscribed_packages_payload)
self._handlePackageDiscrepancies() self._handlePackageDiscrepancies()
def _handlePackageDiscrepancies(self) -> None: def _handlePackageDiscrepancies(self) -> None:
@ -64,7 +91,6 @@ class CloudPackageChecker(QObject):
sync_message = Message(self._i18n_catalog.i18nc( sync_message = Message(self._i18n_catalog.i18nc(
"@info:generic", "@info:generic",
"\nDo you want to sync material and software packages with your account?"), "\nDo you want to sync material and software packages with your account?"),
lifetime=0,
title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync", sync_message.addAction("sync",
name=self._i18n_catalog.i18nc("@action:button", "Sync"), name=self._i18n_catalog.i18nc("@action:button", "Sync"),
@ -77,34 +103,3 @@ class CloudPackageChecker(QObject):
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
sync_message.hide() sync_message.hide()
self.discrepancies.emit(self._model) self.discrepancies.emit(self._model)
def _getUserPackages(self) -> None:
Logger.log("d", "Requesting subscribed packages metadata from server.")
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
scope = self._scope)
def _onUserPackagesRequestFinished(self,
reply: "QNetworkReply",
error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w",
"Requesting user packages failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
return
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Check for errors:
if "errors" in json_data:
for error in json_data["errors"]:
Logger.log("e", "%s", error["title"])
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user packages")

View File

@ -1,18 +1,51 @@
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from ..CloudApiModel import CloudApiModel from ..CloudApiModel import CloudApiModel
from ..UltimakerCloudScope import UltimakerCloudScope from ..UltimakerCloudScope import UltimakerCloudScope
## Manages Cloud subscriptions. When a package is added to a user's account, the user is 'subscribed' to that package
# Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins
class CloudPackageManager: class CloudPackageManager:
def __init__(self, app: CuraApplication) -> None: """Manages Cloud subscriptions
self._request_manager = app.getHttpRequestManager()
self._scope = UltimakerCloudScope(app)
def subscribe(self, package_id: str) -> None: When a package is added to a user's account, the user is 'subscribed' to that package.
Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins
Singleton: use CloudPackageManager.getInstance() instead of CloudPackageManager()
"""
__instance = None
@classmethod
def getInstance(cls, app: CuraApplication):
if not cls.__instance:
cls.__instance = CloudPackageManager(app)
return cls.__instance
def __init__(self, app: CuraApplication) -> None:
if self.__instance is not None:
raise RuntimeError("This is a Singleton. use getInstance()")
self._scope = UltimakerCloudScope(app) # type: UltimakerCloudScope
app.getPackageManager().packageInstalled.connect(self._onPackageInstalled)
def unsubscribe(self, package_id: str) -> None:
url = CloudApiModel.userPackageUrl(package_id)
HttpRequestManager.getInstance().delete(url = url, scope = self._scope)
def _subscribe(self, package_id: str) -> None:
"""You probably don't want to use this directly. All installed packages will be automatically subscribed."""
Logger.debug("Subscribing to {}", package_id)
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version) data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
self._request_manager.put(url=CloudApiModel.api_url_user_packages, HttpRequestManager.getInstance().put(
url = CloudApiModel.api_url_user_packages,
data = data.encode(), data = data.encode(),
scope = self._scope scope = self._scope
) )
def _onPackageInstalled(self, package_id: str):
if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
# We might already be subscribed, but checking would take one extra request. Instead, simply subscribe
self._subscribe(package_id)

View File

@ -28,13 +28,12 @@ class DiscrepanciesPresenter(QObject):
assert self._dialog assert self._dialog
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
@pyqtSlot("QVariant", str)
def dismissIncompatiblePackage(self, model: SubscribedPackagesModel, package_id: str) -> None:
model.dismissPackage(package_id) # update the model to update the view
self._package_manager.dismissPackage(package_id) # adds this package_id as dismissed in the user config file
def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None: def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
# If there are incompatible packages - automatically dismiss them
if model.getIncompatiblePackages():
self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages())
# For now, all compatible packages presented to the user should be installed. # For now, all compatible packages presented to the user should be installed.
# Later, we might remove items for which the user unselected the package # Later, we might remove items for which the user unselected the package
if model.getCompatiblePackages():
model.setItems(model.getCompatiblePackages()) model.setItems(model.getCompatiblePackages())
self.packageMutations.emit(model) self.packageMutations.emit(model)

View File

@ -62,7 +62,8 @@ class DownloadPresenter:
"received": 0, "received": 0,
"total": 1, # make sure this is not considered done yet. Also divByZero-safe "total": 1, # make sure this is not considered done yet. Also divByZero-safe
"file_written": None, "file_written": None,
"request_data": request_data "request_data": request_data,
"package_model": item
} }
self._started = True self._started = True
@ -128,7 +129,14 @@ class DownloadPresenter:
if not item["file_written"]: if not item["file_written"]:
return False return False
success_items = {package_id : value["file_written"] for package_id, value in self._progress.items()} success_items = {
package_id:
{
"package_path": value["file_written"],
"icon_url": value["package_model"]["icon_url"]
}
for package_id, value in self._progress.items()
}
error_items = [package_id for package_id in self._error] error_items = [package_id for package_id in self._error]
self._progress_message.hide() self._progress_message.hide()

View File

@ -6,31 +6,52 @@ catalog = i18nCatalog("cura")
# Model for the ToolboxLicenseDialog # Model for the ToolboxLicenseDialog
class LicenseModel(QObject): class LicenseModel(QObject):
dialogTitleChanged = pyqtSignal() DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
headerChanged = pyqtSignal() ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
licenseTextChanged = pyqtSignal()
def __init__(self) -> None: dialogTitleChanged = pyqtSignal()
packageNameChanged = pyqtSignal()
licenseTextChanged = pyqtSignal()
iconChanged = pyqtSignal()
def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None:
super().__init__() super().__init__()
self._current_page_idx = 0 self._current_page_idx = 0
self._page_count = 1 self._page_count = 1
self._dialogTitle = "" self._dialogTitle = ""
self._header_text = ""
self._license_text = "" self._license_text = ""
self._package_name = "" self._package_name = ""
self._icon_url = ""
self._decline_button_text = decline_button_text
@pyqtProperty(str, constant = True)
def acceptButtonText(self):
return self.ACCEPT_BUTTON_TEXT
@pyqtProperty(str, constant = True)
def declineButtonText(self):
return self._decline_button_text
@pyqtProperty(str, notify=dialogTitleChanged) @pyqtProperty(str, notify=dialogTitleChanged)
def dialogTitle(self) -> str: def dialogTitle(self) -> str:
return self._dialogTitle return self._dialogTitle
@pyqtProperty(str, notify=headerChanged) @pyqtProperty(str, notify=packageNameChanged)
def headerText(self) -> str: def packageName(self) -> str:
return self._header_text return self._package_name
def setPackageName(self, name: str) -> None: def setPackageName(self, name: str) -> None:
self._header_text = name + ": " + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?") self._package_name = name
self.headerChanged.emit() self.packageNameChanged.emit()
@pyqtProperty(str, notify=iconChanged)
def iconUrl(self) -> str:
return self._icon_url
def setIconUrl(self, url: str):
self._icon_url = url
self.iconChanged.emit()
@pyqtProperty(str, notify=licenseTextChanged) @pyqtProperty(str, notify=licenseTextChanged)
def licenseText(self) -> str: def licenseText(self) -> str:
@ -50,6 +71,7 @@ class LicenseModel(QObject):
self._updateDialogTitle() self._updateDialogTitle()
def _updateDialogTitle(self): def _updateDialogTitle(self):
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement ({}/{})" self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement")
.format(self._current_page_idx + 1, self._page_count)) if self._page_count > 1:
self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count)
self.dialogTitleChanged.emit() self.dialogTitleChanged.emit()

View File

@ -1,5 +1,6 @@
import os import os
from typing import Dict, Optional, List from collections import OrderedDict
from typing import Dict, Optional, List, Any
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
@ -17,6 +18,7 @@ class LicensePresenter(QObject):
def __init__(self, app: CuraApplication) -> None: def __init__(self, app: CuraApplication) -> None:
super().__init__() super().__init__()
self._catalog = i18nCatalog("cura")
self._dialog = None # type: Optional[QObject] self._dialog = None # type: Optional[QObject]
self._package_manager = app.getPackageManager() # type: PackageManager self._package_manager = app.getPackageManager() # type: PackageManager
# Emits List[Dict[str, [Any]] containing for example # Emits List[Dict[str, [Any]] containing for example
@ -25,7 +27,9 @@ class LicensePresenter(QObject):
self._current_package_idx = 0 self._current_package_idx = 0
self._package_models = [] # type: List[Dict] self._package_models = [] # type: List[Dict]
self._license_model = LicenseModel() # type: LicenseModel decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account")
self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel
self._page_count = 0
self._app = app self._app = app
@ -34,7 +38,7 @@ class LicensePresenter(QObject):
## Show a license dialog for multiple packages where users can read a license and accept or decline them ## Show a license dialog for multiple packages where users can read a license and accept or decline them
# \param plugin_path: Root directory of the Toolbox plugin # \param plugin_path: Root directory of the Toolbox plugin
# \param packages: Dict[package id, file path] # \param packages: Dict[package id, file path]
def present(self, plugin_path: str, packages: Dict[str, str]) -> None: def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
path = os.path.join(plugin_path, self._compatibility_dialog_path) path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._initState(packages) self._initState(packages)
@ -42,12 +46,11 @@ class LicensePresenter(QObject):
if self._dialog is None: if self._dialog is None:
context_properties = { context_properties = {
"catalog": i18nCatalog("cura"), "catalog": self._catalog,
"licenseModel": self._license_model, "licenseModel": self._license_model,
"handler": self "handler": self
} }
self._dialog = self._app.createQmlComponent(path, context_properties) self._dialog = self._app.createQmlComponent(path, context_properties)
self._license_model.setPageCount(len(self._package_models))
self._presentCurrentPackage() self._presentCurrentPackage()
@pyqtSlot() @pyqtSlot()
@ -60,32 +63,41 @@ class LicensePresenter(QObject):
self._package_models[self._current_package_idx]["accepted"] = False self._package_models[self._current_package_idx]["accepted"] = False
self._checkNextPage() self._checkNextPage()
def _initState(self, packages: Dict[str, str]) -> None: def _initState(self, packages: Dict[str, Dict[str, str]]) -> None:
self._package_models = [
{ implicitly_accepted_count = 0
"package_id" : package_id,
"package_path" : package_path, for package_id, item in packages.items():
"accepted" : None #: None: no answer yet item["package_id"] = package_id
} item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
for package_id, package_path in packages.items() if item["licence_content"] is None:
] # Implicitly accept when there is no license
item["accepted"] = True
implicitly_accepted_count = implicitly_accepted_count + 1
self._package_models.append(item)
else:
item["accepted"] = None #: None: no answer yet
# When presenting the packages, we want to show packages which have a license first.
# In fact, we don't want to show the others at all because they are implicitly accepted
self._package_models.insert(0, item)
CuraApplication.getInstance().processEvents()
self._page_count = len(self._package_models) - implicitly_accepted_count
self._license_model.setPageCount(self._page_count)
def _presentCurrentPackage(self) -> None: def _presentCurrentPackage(self) -> None:
package_model = self._package_models[self._current_package_idx] package_model = self._package_models[self._current_package_idx]
license_content = self._package_manager.getPackageLicense(package_model["package_path"]) package_info = self._package_manager.getPackageInfo(package_model["package_path"])
if license_content is None:
# Implicitly accept when there is no license
self.onLicenseAccepted()
return
self._license_model.setCurrentPageIdx(self._current_package_idx) self._license_model.setCurrentPageIdx(self._current_package_idx)
self._license_model.setPackageName(package_model["package_id"]) self._license_model.setPackageName(package_info["display_name"])
self._license_model.setLicenseText(license_content) self._license_model.setIconUrl(package_model["icon_url"])
self._license_model.setLicenseText(package_model["licence_content"])
if self._dialog: if self._dialog:
self._dialog.open() # Does nothing if already open self._dialog.open() # Does nothing if already open
def _checkNextPage(self) -> None: def _checkNextPage(self) -> None:
if self._current_package_idx + 1 < len(self._package_models): if self._current_package_idx + 1 < self._page_count:
self._current_package_idx += 1 self._current_package_idx += 1
self._presentCurrentPackage() self._presentCurrentPackage()
else: else:

View File

@ -37,27 +37,18 @@ class SubscribedPackagesModel(ListModel):
return True return True
return False return False
# Sets the "is_compatible" to True for the given package, in memory
@pyqtSlot()
def dismissPackage(self, package_id: str) -> None:
package = self.find(key="package_id", value=package_id)
if package != -1:
self.setProperty(package, property="is_dismissed", value=True)
Logger.debug("Package {} has been dismissed".format(package_id))
def setMetadata(self, data: List[Dict[str, List[Any]]]) -> None:
self._metadata = data
def addDiscrepancies(self, discrepancy: List[str]) -> None: def addDiscrepancies(self, discrepancy: List[str]) -> None:
self._discrepancies = discrepancy self._discrepancies = discrepancy
def getCompatiblePackages(self): def getCompatiblePackages(self) -> List[Dict[str, Any]]:
return [x for x in self._items if x["is_compatible"]] return [package for package in self._items if package["is_compatible"]]
def initialize(self) -> None: def getIncompatiblePackages(self) -> List[str]:
return [package["package_id"] for package in self._items if not package["is_compatible"]]
def initialize(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
self._items.clear() self._items.clear()
for item in self._metadata: for item in subscribed_packages_payload:
if item["package_id"] not in self._discrepancies: if item["package_id"] not in self._discrepancies:
continue continue
package = { package = {

View File

@ -1,8 +1,10 @@
import os import os
from typing import List, Dict, Any, cast from typing import List, Dict, Any, cast
from UM import i18n_catalog
from UM.Extension import Extension from UM.Extension import Extension
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .CloudPackageChecker import CloudPackageChecker from .CloudPackageChecker import CloudPackageChecker
@ -36,7 +38,8 @@ class SyncOrchestrator(Extension):
self._name = "SyncOrchestrator" self._name = "SyncOrchestrator"
self._package_manager = app.getPackageManager() self._package_manager = app.getPackageManager()
self._cloud_package_manager = CloudPackageManager(app) # Keep a reference to the CloudPackageManager. it watches for installed packages and subscribes to them
self._cloud_package_manager = CloudPackageManager.getInstance(app) # type: CloudPackageManager
self._checker = CloudPackageChecker(app) # type: CloudPackageChecker self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
self._checker.discrepancies.connect(self._onDiscrepancies) self._checker.discrepancies.connect(self._onDiscrepancies)
@ -61,32 +64,37 @@ class SyncOrchestrator(Extension):
self._download_presenter.download(mutations) self._download_presenter.download(mutations)
## Called when a set of packages have finished downloading ## Called when a set of packages have finished downloading
# \param success_items: Dict[package_id, file_path] # \param success_items: Dict[package_id, Dict[str, str]]
# \param error_items: List[package_id] # \param error_items: List[package_id]
def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]) -> None: def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
# todo handle error items if error_items:
message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
self._showErrorMessage(message)
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
self._license_presenter.present(plugin_path, success_items) self._license_presenter.present(plugin_path, success_items)
# Called when user has accepted / declined all licenses for the downloaded packages # Called when user has accepted / declined all licenses for the downloaded packages
def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
Logger.debug("Got license answers: {}", answers)
has_changes = False # True when at least one package is installed has_changes = False # True when at least one package is installed
for item in answers: for item in answers:
if item["accepted"]: if item["accepted"]:
# install and subscribe packages # install and subscribe packages
if not self._package_manager.installPackage(item["package_path"]): if not self._package_manager.installPackage(item["package_path"]):
Logger.error("could not install {}".format(item["package_id"])) message = "Could not install {}".format(item["package_id"])
self._showErrorMessage(message)
continue continue
self._cloud_package_manager.subscribe(item["package_id"])
has_changes = True has_changes = True
else: else:
# todo unsubscribe declined packages self._cloud_package_manager.unsubscribe(item["package_id"])
pass
# delete temp file # delete temp file
os.remove(item["package_path"]) os.remove(item["package_path"])
if has_changes: if has_changes:
self._restart_presenter.present() self._restart_presenter.present()
## Logs an error and shows it to the user
def _showErrorMessage(self, text: str):
Logger.error(text)
Message(text, lifetime=0).show()

View File

@ -21,7 +21,6 @@ from cura.Machines.ContainerTree import ContainerTree
from .CloudApiModel import CloudApiModel from .CloudApiModel import CloudApiModel
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .CloudSync.CloudPackageManager import CloudPackageManager
from .CloudSync.LicenseModel import LicenseModel from .CloudSync.LicenseModel import LicenseModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
from .UltimakerCloudScope import UltimakerCloudScope from .UltimakerCloudScope import UltimakerCloudScope
@ -44,7 +43,6 @@ class Toolbox(QObject, Extension):
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
# Network: # Network:
self._cloud_package_manager = CloudPackageManager(application) # type: CloudPackageManager
self._download_request_data = None # type: Optional[HttpRequestData] self._download_request_data = None # type: Optional[HttpRequestData]
self._download_progress = 0 # type: float self._download_progress = 0 # type: float
self._is_downloading = False # type: bool self._is_downloading = False # type: bool
@ -147,17 +145,14 @@ class Toolbox(QObject, Extension):
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope) self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope)
@pyqtSlot(str)
def subscribe(self, package_id: str) -> None:
self._cloud_package_manager.subscribe(package_id)
def getLicenseDialogPluginFileLocation(self) -> str: def getLicenseDialogPluginFileLocation(self) -> str:
return self._license_dialog_plugin_file_location return self._license_dialog_plugin_file_location
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None: def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None:
# Set page 1/1 when opening the dialog for a single package # Set page 1/1 when opening the dialog for a single package
self._license_model.setCurrentPageIdx(0) self._license_model.setCurrentPageIdx(0)
self._license_model.setPageCount(1) self._license_model.setPageCount(1)
self._license_model.setIconUrl(icon_url)
self._license_model.setPackageName(plugin_name) self._license_model.setPackageName(plugin_name)
self._license_model.setLicenseText(license_content) self._license_model.setLicenseText(license_content)
@ -376,7 +371,6 @@ class Toolbox(QObject, Extension):
def onLicenseAccepted(self): def onLicenseAccepted(self):
self.closeLicenseDialog.emit() self.closeLicenseDialog.emit()
package_id = self.install(self.getLicenseDialogPluginFileLocation()) package_id = self.install(self.getLicenseDialogPluginFileLocation())
self.subscribe(package_id)
@pyqtSlot() @pyqtSlot()
@ -670,14 +664,16 @@ class Toolbox(QObject, Extension):
return return
license_content = self._package_manager.getPackageLicense(file_path) license_content = self._package_manager.getPackageLicense(file_path)
package_id = package_info["package_id"]
if license_content is not None: if license_content is not None:
self.openLicenseDialog(package_info["package_id"], license_content, file_path) # get the icon url for package_id, make sure the result is a string, never None
icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or ""
self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url)
return return
package_id = self.install(file_path) installed_id = self.install(file_path)
if package_id != package_info["package_id"]: if installed_id != package_id:
Logger.error("Installed package {} does not match {}".format(package_id, package_info["package_id"])) Logger.error("Installed package {} does not match {}".format(installed_id, package_id))
self.subscribe(package_id)
# Getter & Setters for Properties: # Getter & Setters for Properties:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@ -699,14 +695,14 @@ class Toolbox(QObject, Extension):
def isDownloading(self) -> bool: def isDownloading(self) -> bool:
return self._is_downloading return self._is_downloading
def setActivePackage(self, package: Dict[str, Any]) -> None: def setActivePackage(self, package: QObject) -> None:
if self._active_package != package: if self._active_package != package:
self._active_package = package self._active_package = package
self.activePackageChanged.emit() self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded ## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
def activePackage(self) -> Optional[Dict[str, Any]]: def activePackage(self) -> Optional[QObject]:
return self._active_package return self._active_package
def setViewCategory(self, category: str = "plugin") -> None: def setViewCategory(self, category: str = "plugin") -> None:

View File

@ -6,6 +6,9 @@ from cura.API import Account
from cura.CuraApplication import CuraApplication 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): class UltimakerCloudScope(DefaultUserAgentScope):
def __init__(self, application: CuraApplication): def __init__(self, application: CuraApplication):
super().__init__(application) super().__init__(application)

View File

@ -848,6 +848,23 @@
"website": "https://ultimaker.com" "website": "https://ultimaker.com"
} }
} }
},
"VersionUpgrade44to45": {
"package_info": {
"package_id": "VersionUpgrade44to45",
"package_type": "plugin",
"display_name": "Version Upgrade 4.4 to 4.5",
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
"package_version": "1.0.0",
"sdk_version": "7.0.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
}, },
"X3DReader": { "X3DReader": {
"package_info": { "package_info": {

View File

@ -2122,6 +2122,7 @@
"default_value": 210, "default_value": 210,
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "285", "maximum_value_warning": "285",
"maximum_value": "365",
"enabled": false, "enabled": false,
"settable_per_extruder": true, "settable_per_extruder": true,
"settable_per_mesh": false, "settable_per_mesh": false,
@ -2153,6 +2154,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "285", "maximum_value_warning": "285",
"maximum_value": "365",
"enabled": "machine_nozzle_temp_enabled and not (material_flow_dependent_temperature)", "enabled": "machine_nozzle_temp_enabled and not (material_flow_dependent_temperature)",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -2168,6 +2170,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "285", "maximum_value_warning": "285",
"maximum_value": "365",
"enabled": "machine_nozzle_temp_enabled", "enabled": "machine_nozzle_temp_enabled",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -2183,6 +2186,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "material_standby_temperature", "minimum_value_warning": "material_standby_temperature",
"maximum_value_warning": "material_print_temperature", "maximum_value_warning": "material_print_temperature",
"maximum_value": "365",
"enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater", "enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -2198,6 +2202,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "material_standby_temperature", "minimum_value_warning": "material_standby_temperature",
"maximum_value_warning": "material_print_temperature", "maximum_value_warning": "material_print_temperature",
"maximum_value": "365",
"enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater", "enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -2227,6 +2232,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "build_volume_temperature", "minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130", "maximum_value_warning": "130",
"maximum_value": "200",
"enabled": false, "enabled": false,
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
@ -2244,6 +2250,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "build_volume_temperature", "minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130", "maximum_value_warning": "130",
"maximum_value": "200",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
@ -2261,6 +2268,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "max(build_volume_temperature, max(extruderValues('material_bed_temperature')))", "minimum_value_warning": "max(build_volume_temperature, max(extruderValues('material_bed_temperature')))",
"maximum_value_warning": "130", "maximum_value_warning": "130",
"maximum_value": "200",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
@ -2377,6 +2385,7 @@
"enabled": false, "enabled": false,
"minimum_value": "-273.15", "minimum_value": "-273.15",
"maximum_value_warning": "300", "maximum_value_warning": "300",
"maximum_value": "365",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
}, },
@ -2415,6 +2424,7 @@
"default_value": 50, "default_value": 50,
"enabled": false, "enabled": false,
"minimum_value": "-273.15", "minimum_value": "-273.15",
"maximum_value": "365",
"maximum_value_warning": "300", "maximum_value_warning": "300",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -2691,6 +2701,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "260", "maximum_value_warning": "260",
"maximum_value": "365",
"enabled": "extruders_enabled_count > 1 and machine_nozzle_temp_enabled", "enabled": "extruders_enabled_count > 1 and machine_nozzle_temp_enabled",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -5688,7 +5699,7 @@
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "resolveOrValue('prime_tower_enable')",
"default_value": 200, "default_value": 200,
"value": "machine_width - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - 1", "value": "machine_width - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - max(map(abs, extruderValues('machine_nozzle_offset_x'))) - 1",
"maximum_value": "machine_width / 2 if machine_center_is_zero else machine_width", "maximum_value": "machine_width / 2 if machine_center_is_zero else machine_width",
"minimum_value": "resolveOrValue('prime_tower_size') - machine_width / 2 if machine_center_is_zero else resolveOrValue('prime_tower_size')", "minimum_value": "resolveOrValue('prime_tower_size') - machine_width / 2 if machine_center_is_zero else resolveOrValue('prime_tower_size')",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -5702,7 +5713,7 @@
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "resolveOrValue('prime_tower_enable')",
"default_value": 200, "default_value": 200,
"value": "machine_depth - prime_tower_size - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - 1", "value": "machine_depth - prime_tower_size - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - max(map(abs, extruderValues('machine_nozzle_offset_y'))) - 1",
"maximum_value": "machine_depth / 2 - resolveOrValue('prime_tower_size') if machine_center_is_zero else machine_depth - resolveOrValue('prime_tower_size')", "maximum_value": "machine_depth / 2 - resolveOrValue('prime_tower_size') if machine_center_is_zero else machine_depth - resolveOrValue('prime_tower_size')",
"minimum_value": "machine_depth / -2 if machine_center_is_zero else 0", "minimum_value": "machine_depth / -2 if machine_center_is_zero else 0",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -5984,7 +5995,7 @@
"print_sequence": "print_sequence":
{ {
"label": "Print Sequence", "label": "Print Sequence",
"description": "Whether to print all models one layer at a time or to wait for one model to finish, before moving on to the next. One at a time mode is only possible if all models are separated in such a way that the whole print head can move in between and all models are lower than the distance between the nozzle and the X/Y axes.", "description": "Whether to print all models one layer at a time or to wait for one model to finish, before moving on to the next. One at a time mode is possible if a) only one extruder is enabled and b) all models are separated in such a way that the whole print head can move in between and all models are lower than the distance between the nozzle and the X/Y axes. ",
"type": "enum", "type": "enum",
"options": "options":
{ {
@ -6666,6 +6677,16 @@
"limit_to_extruder": "wall_0_extruder_nr", "limit_to_extruder": "wall_0_extruder_nr",
"settable_per_mesh": true "settable_per_mesh": true
}, },
"magic_fuzzy_skin_outside_only":
{
"label": "Fuzzy Skin Outside Only",
"description": "Jitter only the parts' outlines and not the parts' holes.",
"type": "bool",
"default_value": false,
"enabled": "magic_fuzzy_skin_enabled",
"limit_to_extruder": "wall_0_extruder_nr",
"settable_per_mesh": true
},
"magic_fuzzy_skin_thickness": "magic_fuzzy_skin_thickness":
{ {
"label": "Fuzzy Skin Thickness", "label": "Fuzzy Skin Thickness",

View File

@ -0,0 +1,57 @@
{
"version": 2,
"name": "Geeetech A10",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A10_1"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A10" },
"machine_width": {
"default_value": 220
},
"machine_height": {
"default_value": 220
},
"machine_depth": {
"default_value": 260
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28"
},
"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"
},
"machine_extruder_count": {
"default_value": 1
}
}
}

View File

@ -0,0 +1,58 @@
{
"version": 2,
"name": "Geeetech A10M",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A10M_1",
"1": "geeetech_A10M_2"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A10M" },
"machine_width": {
"default_value": 220
},
"machine_height": {
"default_value": 220
},
"machine_depth": {
"default_value": 260
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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"
},
"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"
},
"machine_extruder_count": {
"default_value": 2
}
}
}

View File

@ -0,0 +1,59 @@
{
"version": 2,
"name": "Geeetech A10T",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A10T_1",
"1": "geeetech_A10T_2",
"2": "geeetech_A10T_3"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A10T" },
"machine_width": {
"default_value": 220
},
"machine_height": {
"default_value": 220
},
"machine_depth": {
"default_value": 260
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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.33\nM163 S1 P0.33\nM163 S2 P0.33\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28"
},
"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"
},
"machine_extruder_count": {
"default_value": 3
}
}
}

View File

@ -0,0 +1,57 @@
{
"version": 2,
"name": "Geeetech A20",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A20_1"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A20" },
"machine_width": {
"default_value": 250
},
"machine_height": {
"default_value": 250
},
"machine_depth": {
"default_value": 250
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28"
},
"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"
},
"machine_extruder_count": {
"default_value": 1
}
}
}

View File

@ -0,0 +1,58 @@
{
"version": 2,
"name": "Geeetech A20M",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A20M_1",
"1": "geeetech_A20M_2"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A20M" },
"machine_width": {
"default_value": 250
},
"machine_height": {
"default_value": 250
},
"machine_depth": {
"default_value": 250
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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"
},
"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"
},
"machine_extruder_count": {
"default_value": 2
}
}
}

View File

@ -0,0 +1,59 @@
{
"version": 2,
"name": "Geeetech A20T",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Amit L",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"has_materials": true,
"machine_extruder_trains":
{
"0": "geeetech_A20T_1",
"1": "geeetech_A20T_2",
"2": "geeetech_A20T_3"
}
},
"overrides": {
"machine_name": { "default_value": "Geeetech A20T" },
"machine_width": {
"default_value": 250
},
"machine_height": {
"default_value": 250
},
"machine_depth": {
"default_value": 250
}, "machine_center_is_zero": {
"default_value": false
},
"layer_height": { "default_value": 0.1 },
"layer_height_0": { "default_value": 0.15 },
"retraction_amount": { "default_value": 0.8 },
"retraction_speed": { "default_value": 35 },
"adhesion_type": { "default_value": "skirt" },
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
"gantry_height": { "value": "28" },
"machine_max_feedrate_z": { "default_value": 12 },
"machine_max_feedrate_e": { "default_value": 120 },
"machine_max_acceleration_z": { "default_value": 500 },
"machine_acceleration": { "default_value": 1000 },
"machine_max_jerk_xy": { "default_value": 10 },
"machine_max_jerk_z": { "default_value": 0.2 },
"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.33\nM163 S1 P0.33\nM163 S2 P0.33\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28"
},
"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"
},
"machine_extruder_count": {
"default_value": 3
}
}
}

View File

@ -0,0 +1,96 @@
{
"version": 2,
"name": "MAKEiT Pro-MX",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "unknown",
"manufacturer": "MAKEiT 3D",
"file_formats": "text/x-gcode",
"has_materials": false,
"machine_extruder_trains":
{
"0": "makeit_mx_dual_1st",
"1": "makeit_mx_dual_2nd"
}
},
"overrides": {
"machine_name": { "default_value": "MAKEiT Pro-MX" },
"machine_width": {
"default_value": 200
},
"machine_height": {
"default_value": 330
},
"machine_depth": {
"default_value": 240
},
"machine_center_is_zero": {
"default_value": false
},
"machine_head_with_fans_polygon":
{
"default_value": [
[ -200, 240 ],
[ -200, -32 ],
[ 200, 240 ],
[ 200, -32 ]
]
},
"gantry_height": {
"value": "200"
},
"machine_use_extruder_offset_to_offset_coords": {
"default_value": true
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG92 E0 ;zero the extruded length\nG28 ;home\nG1 F200 E30 ;extrude 30 mm of feed stock\nG92 E0 ;zero the extruded length\nG1 E-5 ;retract 5 mm\nG28 SC ;Do homeing, clean nozzles and let printer to know that printing started\nG92 X-6 ;Sets Curas checker board to match printers heated bed coordinates\nG1 F{speed_travel}\nM117 Printing..."
},
"machine_end_gcode": {
"default_value": "M104 T0 S0 ;1st extruder heater off\nM104 T1 S0 ;2nd extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-5 F9000 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+5 X+20 Y+20 F9000 ;move Z up a bit\nM117 MAKEiT Pro@Done\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM81"
},
"machine_extruder_count": {
"default_value": 2
},
"print_sequence": {
"enabled": false
},
"prime_tower_position_x": {
"value": "185"
},
"prime_tower_position_y": {
"value": "160"
},
"layer_height": {
"default_value": 0.2
},
"retraction_speed": {
"default_value": 180
},
"infill_sparse_density": {
"default_value": 20
},
"retraction_amount": {
"default_value": 6
},
"speed_print": {
"default_value": 60
},
"wall_thickness": {
"default_value": 1.2
},
"cool_min_layer_time_fan_speed_max": {
"default_value": 5
},
"adhesion_type": {
"default_value": "skirt"
},
"machine_heated_bed": {
"default_value": true
}
}
}

View File

@ -0,0 +1,76 @@
{
"version": 2,
"name": "MP Mini Delta",
"inherits": "fdmprinter",
"metadata": {
"author": "MPMD Facebook Group",
"manufacturer": "Monoprice",
"category": "Other",
"file_formats": "text/x-gcode",
"platform": "mp_mini_delta_platform.stl",
"supports_usb_connection": true,
"has_machine_quality": false,
"visible": true,
"platform_offset": [0, 0, 0],
"has_materials": true,
"has_variants": false,
"has_machine_materials": false,
"has_variant_materials": false,
"preferred_quality_type": "normal",
"machine_extruder_trains":
{
"0": "mp_mini_delta_extruder_0"
}
},
"overrides": {
"machine_start_gcode":
{
"default_value": ";MPMD Basic Calibration Tutorial: \n; https://www.thingiverse.com/thing:3892011 \n; \n; If you want to put calibration values in your \n; Start Gcode, put them here. \n; \n;If on stock firmware, at minimum, consider adding \n;M665 R here since there is a firmware bug. \n; \n; Calibration part ends here \n; \nG90 ; switch to absolute positioning \nG92 E0 ; reset extrusion distance \nG1 E20 F200 ; purge 20mm of filament to prime nozzle. \nG92 E0 ; reset extrusion distance \nG4 S5 ; Pause for 5 seconds to allow time for removing extruded filament \nG28 ; start from home position \nG1 E-6 F900 ; retract 6mm of filament before starting the bed leveling process \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for removing extruded filament \nG29 P2 Z0.28 ; Auto-level ; ADJUST Z higher or lower to set first layer height. Start with 0.02 adjustments. \nG1 Z30 ; raise Z 30mm to prepare for priming the nozzle \nG1 E5 F200 ; extrude 5mm of filament to help prime the nozzle just prior to the start of the print \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for cleaning the nozzle and build plate if needed "
},
"machine_end_gcode":
{
"default_value": "M107; \nM104 S0; turn off hotend heater \nM140 S0; turn off bed heater \nG91; Switch to use Relative Coordinates \nG1 E-2 F300; retract the filament a bit before lifting the nozzle to release some of the pressure \nG1 Z5 E-5 F4800; move nozzle up a bit and retract filament even more \nG28 X0; return to home positions so the nozzle is out of the way \nM84; turn off stepper motors \nG90; switch to absolute positioning \nM82; absolute extrusion mode"
},
"machine_width": { "default_value": 110 },
"machine_depth": { "default_value": 110 },
"machine_height": { "default_value": 120 },
"machine_heated_bed": { "default_value": true },
"machine_shape": { "default_value": "elliptic" },
"machine_center_is_zero": { "default_value": true },
"machine_nozzle_size": {
"default_value": 0.4,
"minimum_value": 0.10,
"maximum_value": 0.80
},
"layer_height": {
"default_value": 0.14,
"minimum_value": 0.04
},
"layer_height_0": {
"default_value": 0.21,
"minimum_value": 0.07
},
"line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" },
"material_print_temperature_layer_0": { "value": "material_print_temperature + 5" },
"material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_max_feedrate_x": { "default_value": 150 },
"machine_max_feedrate_y": { "default_value": 150 },
"machine_max_feedrate_z": { "default_value": 150 },
"machine_max_feedrate_e": { "default_value": 50 },
"machine_max_acceleration_x": { "default_value": 800 },
"machine_max_acceleration_y": { "default_value": 800 },
"machine_max_acceleration_z": { "default_value": 800 },
"machine_max_acceleration_e": { "default_value": 10000 },
"machine_acceleration": { "default_value": 3000 },
"machine_max_jerk_xy": { "default_value": 20 },
"machine_max_jerk_z": { "default_value": 20 },
"machine_max_jerk_e": { "default_value": 5},
"retraction_amount": { "default_value": 4 },
"retraction_speed": { "default_value": 50 },
"retraction_hop_enabled": { "default_value": false },
"retract_at_layer_change": { "default_value": true },
"coasting_enable": { "default_value": true }
}
}

View File

@ -18,10 +18,10 @@
"default_value": "skirt" "default_value": "skirt"
}, },
"bottom_thickness": { "bottom_thickness": {
"value": "0.5" "value": "0.6"
}, },
"brim_width": { "brim_width": {
"value": "2.0" "value": "3.0"
}, },
"cool_fan_enabled": { "cool_fan_enabled": {
"value": "True" "value": "True"
@ -39,19 +39,28 @@
"value": "True" "value": "True"
}, },
"cool_min_layer_time": { "cool_min_layer_time": {
"value": "5.0" "value": "1.0"
}, },
"cool_min_speed": { "cool_min_speed": {
"value": "10.0" "value": "5.0"
}, },
"infill_before_walls": { "infill_before_walls": {
"value": "True" "value": "True"
}, },
"infill_line_width": {
"value": "0.6"
},
"infill_overlap": { "infill_overlap": {
"value": "15.0" "value": "15.0"
}, },
"infill_sparse_density": {
"value": "26.0"
},
"ironing_enabled": {
"value": "True"
},
"layer_0_z_overlap": { "layer_0_z_overlap": {
"value": "0.22" "value": "0.11"
}, },
"layer_height_0": { "layer_height_0": {
"value": "0.3" "value": "0.3"
@ -60,11 +69,23 @@
"value": "100" "value": "100"
}, },
"machine_end_gcode": { "machine_end_gcode": {
"default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 E-5 X-20 Y-20 ;retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nG0 Z{machine_height} ;move the platform all the way down\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done" "default_value": ";End GCode\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-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done"
}, },
"machine_gcode_flavor": { "machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)" "default_value": "RepRap (Marlin/Sprinter)"
}, },
"machine_head_with_fans_polygon":
{
"default_value": [
[-26, -27],
[38, -27],
[38, 55],
[-26, 55]
]
},
"gantry_height": {
"value": "8"
},
"machine_height": { "machine_height": {
"value": "100" "value": "100"
}, },
@ -72,7 +93,7 @@
"default_value": "Renkforce RF100" "default_value": "Renkforce RF100"
}, },
"machine_start_gcode": { "machine_start_gcode": {
"default_value": ";Start GCode\nG21 ;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 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\n;Put printing message on LCD screen\nM117 Printing..." "default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..."
}, },
"machine_width": { "machine_width": {
"value": "100" "value": "100"
@ -90,7 +111,7 @@
"value": "True" "value": "True"
}, },
"raft_airgap": { "raft_airgap": {
"value": "0.22" "value": "0.33"
}, },
"raft_base_line_spacing": { "raft_base_line_spacing": {
"value": "3.0" "value": "3.0"
@ -111,22 +132,25 @@
"value": "0.27" "value": "0.27"
}, },
"raft_margin": { "raft_margin": {
"value": "5.0" "value": "6.0"
},
"raft_speed": {
"value": "20.0"
}, },
"raft_surface_layers": { "raft_surface_layers": {
"value": "2.0" "value": "2"
}, },
"raft_surface_line_spacing": { "raft_surface_line_spacing": {
"value": "3.0" "value": "0.4"
}, },
"raft_surface_line_width": { "raft_surface_line_width": {
"value": "0.4" "value": "0.4"
}, },
"raft_surface_thickness": { "raft_surface_thickness": {
"value": "0.27" "value": "0.1"
}, },
"retraction_amount": { "retraction_amount": {
"value": "2.0" "value": "5.0"
}, },
"retraction_combing": { "retraction_combing": {
"default_value": "all" "default_value": "all"
@ -134,15 +158,9 @@
"retraction_enable": { "retraction_enable": {
"value": "True" "value": "True"
}, },
"retraction_hop_enabled": {
"value": "1.0"
},
"retraction_min_travel": { "retraction_min_travel": {
"value": "1.5" "value": "1.5"
}, },
"retraction_speed": {
"value": "40.0"
},
"skin_overlap": { "skin_overlap": {
"value": "15.0" "value": "15.0"
}, },
@ -185,6 +203,9 @@
"support_infill_rate": { "support_infill_rate": {
"value": "15 if support_enable else 0 if support_tree_enable else 15" "value": "15 if support_enable else 0 if support_tree_enable else 15"
}, },
"support_line_width": {
"value": "0.6"
},
"support_pattern": { "support_pattern": {
"default_value": "lines" "default_value": "lines"
}, },
@ -192,13 +213,13 @@
"default_value": "everywhere" "default_value": "everywhere"
}, },
"support_xy_distance": { "support_xy_distance": {
"value": "0.5" "value": "0.7"
}, },
"support_z_distance": { "support_z_distance": {
"value": "0.1" "value": "0.35"
}, },
"top_thickness": { "top_bottom_thickness": {
"value": "0.5" "value": "0.8"
}, },
"wall_thickness": { "wall_thickness": {
"value": "0.8" "value": "0.8"

View File

@ -0,0 +1,228 @@
{
"version": 2,
"name": "Renkforce RF100 V2",
"inherits": "fdmprinter",
"metadata": {
"author": "Simon Peter (based on RF100.ini by Conrad Electronic SE)",
"file_formats": "text/x-gcode",
"manufacturer": "Renkforce",
"visible": true,
"machine_extruder_trains":
{
"0": "renkforce_rf100_extruder_0"
}
},
"overrides": {
"adhesion_type": {
"default_value": "skirt"
},
"bottom_thickness": {
"value": "0.6"
},
"brim_width": {
"value": "3.0"
},
"cool_fan_enabled": {
"value": "True"
},
"cool_fan_full_at_height": {
"value": "0.5"
},
"cool_fan_speed_max": {
"value": "100.0"
},
"cool_fan_speed_min": {
"value": "100.0"
},
"cool_lift_head": {
"value": "True"
},
"cool_min_layer_time": {
"value": "1.0"
},
"cool_min_speed": {
"value": "5.0"
},
"infill_before_walls": {
"value": "True"
},
"infill_line_width": {
"value": "0.6"
},
"infill_overlap": {
"value": "15.0"
},
"infill_sparse_density": {
"value": "26.0"
},
"ironing_enabled": {
"value": "True"
},
"layer_0_z_overlap": {
"value": "0.11"
},
"layer_height_0": {
"value": "0.3"
},
"machine_depth": {
"value": "120"
},
"machine_end_gcode": {
"default_value": ";End GCode\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-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done"
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_head_with_fans_polygon":
{
"default_value": [
[-26, -27],
[38, -27],
[38, 55],
[-26, 55]
]
},
"gantry_height": {
"value": "8"
},
"machine_height": {
"value": "120"
},
"machine_name": {
"default_value": "Renkforce RF100 V2"
},
"machine_start_gcode": {
"default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..."
},
"machine_width": {
"value": "120"
},
"material_bed_temperature": {
"enabled": false
},
"material_flow": {
"value": "110"
},
"material_print_temperature": {
"value": "210.0"
},
"ooze_shield_enabled": {
"value": "True"
},
"raft_airgap": {
"value": "0.33"
},
"raft_base_line_spacing": {
"value": "3.0"
},
"raft_base_line_width": {
"value": "1.0"
},
"raft_base_thickness": {
"value": "0.3"
},
"raft_interface_line_spacing": {
"value": "3.0"
},
"raft_interface_line_width": {
"value": "0.4"
},
"raft_interface_thickness": {
"value": "0.27"
},
"raft_margin": {
"value": "6.0"
},
"raft_speed": {
"value": "20.0"
},
"raft_surface_layers": {
"value": "2"
},
"raft_surface_line_spacing": {
"value": "0.4"
},
"raft_surface_line_width": {
"value": "0.4"
},
"raft_surface_thickness": {
"value": "0.1"
},
"retraction_amount": {
"value": "5.0"
},
"retraction_combing": {
"default_value": "all"
},
"retraction_enable": {
"value": "True"
},
"retraction_min_travel": {
"value": "1.5"
},
"skin_overlap": {
"value": "15.0"
},
"skirt_brim_minimal_length": {
"value": "150.0"
},
"skirt_gap": {
"value": "3.0"
},
"skirt_line_count": {
"value": "3"
},
"speed_infill": {
"value": "50.0"
},
"speed_layer_0": {
"value": "15.0"
},
"speed_print": {
"value": "50.0"
},
"speed_topbottom": {
"value": "30.0"
},
"speed_travel": {
"value": "50.0"
},
"speed_wall_0": {
"value": "25.0"
},
"speed_wall_x": {
"value": "35.0"
},
"support_angle": {
"value": "60.0"
},
"support_enable": {
"value": "False"
},
"support_infill_rate": {
"value": "15 if support_enable else 0 if support_tree_enable else 15"
},
"support_line_width": {
"value": "0.6"
},
"support_pattern": {
"default_value": "lines"
},
"support_type": {
"default_value": "everywhere"
},
"support_xy_distance": {
"value": "0.7"
},
"support_z_distance": {
"value": "0.35"
},
"top_bottom_thickness": {
"value": "0.8"
},
"wall_thickness": {
"value": "0.8"
}
}
}

View File

@ -0,0 +1,216 @@
{
"version": 2,
"name": "Renkforce RF100 XL",
"inherits": "fdmprinter",
"metadata": {
"author": "Simon Peter (based on RF100.ini by Conrad Electronic SE)",
"file_formats": "text/x-gcode",
"manufacturer": "Renkforce",
"visible": true,
"machine_extruder_trains":
{
"0": "renkforce_rf100_xl_extruder_0"
}
},
"overrides": {
"adhesion_type": {
"default_value": "skirt"
},
"bottom_thickness": {
"value": "0.6"
},
"brim_width": {
"value": "3.0"
},
"cool_fan_enabled": {
"value": "True"
},
"cool_fan_full_at_height": {
"value": "0.5"
},
"cool_fan_speed_max": {
"value": "100.0"
},
"cool_fan_speed_min": {
"value": "100.0"
},
"cool_lift_head": {
"value": "True"
},
"cool_min_layer_time": {
"value": "1.0"
},
"cool_min_speed": {
"value": "5.0"
},
"infill_before_walls": {
"value": "True"
},
"infill_line_width": {
"value": "0.6"
},
"infill_overlap": {
"value": "15.0"
},
"infill_sparse_density": {
"value": "26.0"
},
"ironing_enabled": {
"value": "True"
},
"layer_0_z_overlap": {
"value": "0.11"
},
"layer_height_0": {
"value": "0.3"
},
"machine_depth": {
"value": "200"
},
"machine_end_gcode": {
"default_value": ";End GCode\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-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done"
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_heated_bed": {
"default_value": "true"
},
"machine_height": {
"value": "200"
},
"machine_name": {
"default_value": "Renkforce RF100 XL"
},
"machine_start_gcode": {
"default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..."
},
"machine_width": {
"value": "200"
},
"material_bed_temperature": {
"value": "70"
},
"material_print_temperature": {
"value": "210.0"
},
"ooze_shield_enabled": {
"value": "True"
},
"raft_airgap": {
"value": "0.33"
},
"raft_base_line_spacing": {
"value": "3.0"
},
"raft_base_line_width": {
"value": "1.0"
},
"raft_base_thickness": {
"value": "0.3"
},
"raft_interface_line_spacing": {
"value": "3.0"
},
"raft_interface_line_width": {
"value": "0.4"
},
"raft_interface_thickness": {
"value": "0.27"
},
"raft_margin": {
"value": "6.0"
},
"raft_speed": {
"value": "20.0"
},
"raft_surface_layers": {
"value": "2"
},
"raft_surface_line_spacing": {
"value": "0.4"
},
"raft_surface_line_width": {
"value": "0.4"
},
"raft_surface_thickness": {
"value": "0.1"
},
"retraction_amount": {
"value": "5.0"
},
"retraction_combing": {
"default_value": "all"
},
"retraction_enable": {
"value": "True"
},
"retraction_min_travel": {
"value": "1.5"
},
"skin_overlap": {
"value": "15.0"
},
"skirt_brim_minimal_length": {
"value": "150.0"
},
"skirt_gap": {
"value": "3.0"
},
"skirt_line_count": {
"value": "3"
},
"speed_infill": {
"value": "50.0"
},
"speed_layer_0": {
"value": "15.0"
},
"speed_print": {
"value": "50.0"
},
"speed_topbottom": {
"value": "30.0"
},
"speed_travel": {
"value": "50.0"
},
"speed_wall_0": {
"value": "25.0"
},
"speed_wall_x": {
"value": "35.0"
},
"support_angle": {
"value": "60.0"
},
"support_enable": {
"value": "False"
},
"support_infill_rate": {
"value": "15 if support_enable else 0 if support_tree_enable else 15"
},
"support_line_width": {
"value": "0.6"
},
"support_pattern": {
"default_value": "lines"
},
"support_type": {
"default_value": "everywhere"
},
"support_xy_distance": {
"value": "0.7"
},
"support_z_distance": {
"value": "0.35"
},
"top_bottom_thickness": {
"value": "0.8"
},
"wall_thickness": {
"value": "0.8"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
{
"version": 2,
"name": "1st Extruder",
"inherits": "fdmextruder",
"metadata": {
"machine": "makeit_pro_mx",
"position": "0"
},
"overrides": {
"extruder_nr": {
"default_value": 0,
"maximum_value": "1"
},
"machine_nozzle_offset_x": { "default_value": 0.0 },
"machine_nozzle_offset_y": { "default_value": 0.0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 },
"machine_extruder_start_pos_abs": { "default_value": true },
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
"machine_extruder_end_pos_abs": { "default_value": true },
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
}
}

View File

@ -0,0 +1,27 @@
{
"version": 2,
"name": "2nd Extruder",
"inherits": "fdmextruder",
"metadata": {
"machine": "makeit_pro_mx",
"position": "1"
},
"overrides": {
"extruder_nr": {
"default_value": 1,
"maximum_value": "1"
},
"machine_nozzle_offset_x": { "default_value": 0.0 },
"machine_nozzle_offset_y": { "default_value": 0.0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 },
"machine_extruder_start_pos_abs": { "default_value": true },
"machine_extruder_start_pos_x": { "value": "prime_tower_position_x" },
"machine_extruder_start_pos_y": { "value": "prime_tower_position_y" },
"machine_extruder_end_pos_abs": { "default_value": true },
"machine_extruder_end_pos_x": { "value": "prime_tower_position_x" },
"machine_extruder_end_pos_y": { "value": "prime_tower_position_y" }
}
}

View File

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

View File

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

Binary file not shown.

View File

@ -238,7 +238,7 @@ UM.MainWindow
if (filename.toLowerCase().endsWith(".curapackage")) if (filename.toLowerCase().endsWith(".curapackage"))
{ {
// Try to install plugin & close. // Try to install plugin & close.
CuraApplication.getPackageManager().installPackageViaDragAndDrop(filename); CuraApplication.installPackageViaDragAndDrop(filename);
packageInstallDialog.text = catalog.i18nc("@label", "This package will be installed after restarting."); packageInstallDialog.text = catalog.i18nc("@label", "This package will be installed after restarting.");
packageInstallDialog.icon = StandardIcon.Information; packageInstallDialog.icon = StandardIcon.Information;
packageInstallDialog.open(); packageInstallDialog.open();

View File

@ -127,8 +127,8 @@ Item
icon: StandardIcon.Question icon: StandardIcon.Question
onYes: onYes:
{ {
CuraApplication.deleteAll(); CuraApplication.resetWorkspace()
Cura.Actions.resetProfile.trigger(); Cura.Actions.resetProfile.trigger()
UM.Controller.setActiveStage("PrepareStage") UM.Controller.setActiveStage("PrepareStage")
} }
} }

View File

@ -125,6 +125,7 @@ Item
id: createMenuButton id: createMenuButton
text: catalog.i18nc("@action:button", "Create") text: catalog.i18nc("@action:button", "Create")
iconName: "list-add" iconName: "list-add"
enabled: Cura.MachineManager.activeMachine.hasMaterials
onClicked: onClicked:
{ {
forceActiveFocus(); forceActiveFocus();
@ -174,7 +175,7 @@ Item
forceActiveFocus(); forceActiveFocus();
importMaterialDialog.open(); importMaterialDialog.open();
} }
visible: true enabled: Cura.MachineManager.activeMachine.hasMaterials
} }
// Export button // Export button

View File

@ -202,8 +202,9 @@ Item
// dragging a tool handle. // dragging a tool handle.
Rectangle Rectangle
{ {
x: -base.x + base.mouseX + UM.Theme.getSize("default_margin").width id: toolInfo
y: -base.y + base.mouseY + UM.Theme.getSize("default_margin").height x: visible ? -base.x + base.mouseX + UM.Theme.getSize("default_margin").width: 0
y: visible ? -base.y + base.mouseY + UM.Theme.getSize("default_margin").height: 0
width: toolHint.width + UM.Theme.getSize("default_margin").width width: toolHint.width + UM.Theme.getSize("default_margin").width
height: toolHint.height; height: toolHint.height;

View File

@ -20,7 +20,7 @@ ScrollView
background: Rectangle // Border background: Rectangle // Border
{ {
color: UM.Theme.getColor("main_background") color: UM.Theme.getColor("main_background")
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("thick_lining")
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
} }

View File

@ -388,6 +388,7 @@ support_conical_enabled
support_conical_angle support_conical_angle
support_conical_min_width support_conical_min_width
magic_fuzzy_skin_enabled magic_fuzzy_skin_enabled
magic_fuzzy_skin_outside_only
magic_fuzzy_skin_thickness magic_fuzzy_skin_thickness
magic_fuzzy_skin_point_density magic_fuzzy_skin_point_density
magic_fuzzy_skin_point_dist magic_fuzzy_skin_point_dist

View File

@ -8,7 +8,7 @@
"main_background": [39, 44, 48, 255], "main_background": [39, 44, 48, 255],
"message_background": [39, 44, 48, 255], "message_background": [39, 44, 48, 255],
"wide_lining": [31, 36, 39, 255], "wide_lining": [31, 36, 39, 255],
"thick_lining": [255, 255, 255, 30], "thick_lining": [255, 255, 255, 60],
"lining": [64, 69, 72, 255], "lining": [64, 69, 72, 255],
"viewport_overlay": [30, 36, 39, 255], "viewport_overlay": [30, 36, 39, 255],

View File

@ -149,7 +149,7 @@
"main_background": [255, 255, 255, 255], "main_background": [255, 255, 255, 255],
"wide_lining": [245, 245, 245, 255], "wide_lining": [245, 245, 245, 255],
"thick_lining": [127, 127, 127, 255], "thick_lining": [180, 180, 180, 255],
"lining": [192, 193, 194, 255], "lining": [192, 193, 194, 255],
"viewport_overlay": [246, 246, 246, 255], "viewport_overlay": [246, 246, 246, 255],
@ -520,6 +520,7 @@
"action_button": [15.0, 2.5], "action_button": [15.0, 2.5],
"action_button_icon": [1.0, 1.0], "action_button_icon": [1.0, 1.0],
"action_button_radius": [0.15, 0.15], "action_button_radius": [0.15, 0.15],
"dialog_primary_button_padding": [3.0, 0],
"radio_button": [1.3, 1.3], "radio_button": [1.3, 1.3],

View File

@ -0,0 +1,65 @@
import os
import re
import sys
from pathlib import Path
"""
Run this file with the Cura project root as the working directory
Checks for invalid imports. When importing from plugins, there will be no problems when running from source,
but for some build types the plugins dir is not on the path, so relative imports should be used instead. eg:
from ..UltimakerCloudScope import UltimakerCloudScope <-- OK
import plugins.Toolbox.src ... <-- NOT OK
"""
class InvalidImportsChecker:
# compile regex
REGEX = re.compile(r"^\s*(from plugins|import plugins)")
def check(self):
""" Checks for invalid imports
:return: True if checks passed, False when the test fails
"""
cwd = os.getcwd()
cura_result = checker.check_dir(os.path.join(cwd, "cura"))
plugins_result = checker.check_dir(os.path.join(cwd, "plugins"))
result = cura_result and plugins_result
if not result:
print("error: sources contain invalid imports. Use relative imports when referencing plugin source files")
return result
def check_dir(self, root_dir: str) -> bool:
""" Checks a directory for invalid imports
:return: True if checks passed, False when the test fails
"""
passed = True
for path_like in Path(root_dir).rglob('*.py'):
if not self.check_file(str(path_like)):
passed = False
return passed
def check_file(self, file_path):
""" Checks a file for invalid imports
:return: True if checks passed, False when the test fails
"""
passed = True
with open(file_path, 'r', encoding = "utf-8") as inputFile:
# loop through each line in file
for line_i, line in enumerate(inputFile, 1):
# check if we have a regex match
match = self.REGEX.search(line)
if match:
path = os.path.relpath(file_path)
print("{path}:{line_i}:{match}".format(path=path, line_i=line_i, match=match.group(1)))
passed = False
return passed
if __name__ == "__main__":
checker = InvalidImportsChecker()
sys.exit(0 if checker.check() else 1)