This commit is contained in:
root 2021-08-01 19:55:05 +02:00
commit 22156a5421
59 changed files with 998 additions and 125 deletions

View File

@ -10,9 +10,9 @@ For crashes and similar issues, please attach the following information:
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output) * (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
* The Cura GUI log file, located at * The Cura GUI log file, located at
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log` * `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$USER/Library/Application Support/cura/<Cura version>/cura.log` (OSX) * `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$USER/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux) * `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder

View File

@ -109,7 +109,6 @@ class Account(QObject):
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged) self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
self._authorization_service.loadAuthDataFromPreferences() self._authorization_service.loadAuthDataFromPreferences()
@pyqtProperty(int, notify=syncStateChanged) @pyqtProperty(int, notify=syncStateChanged)
def syncState(self): def syncState(self):
return self._sync_state return self._sync_state
@ -178,6 +177,7 @@ class Account(QObject):
if error_message: if error_message:
if self._error_message: if self._error_message:
self._error_message.hide() self._error_message.hide()
Logger.log("w", "Failed to login: %s", error_message)
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed")) self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
self._error_message.show() self._error_message.show()
self._logged_in = False self._logged_in = False

View File

@ -2,9 +2,10 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials. import copy # To duplicate materials.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
from typing import Any, Dict, Optional, TYPE_CHECKING from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials. import uuid # To generate new GUIDs for new materials.
import zipfile # To export all materials in a .zip archive.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
@ -20,11 +21,6 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class MaterialManagementModel(QObject): class MaterialManagementModel(QObject):
"""Proxy class to the materials page in the preferences.
This class handles the actions in that page, such as creating new materials, renaming them, etc.
"""
favoritesChanged = pyqtSignal(str) favoritesChanged = pyqtSignal(str)
"""Triggered when a favorite is added or removed. """Triggered when a favorite is added or removed.
@ -264,3 +260,40 @@ class MaterialManagementModel(QObject):
self.favoritesChanged.emit(material_base_file) self.favoritesChanged.emit(material_base_file)
except ValueError: # Material was not in the favorites list. except ValueError: # Material was not in the favorites list.
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file)) Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
@pyqtSlot(result = QUrl)
def getPreferredExportAllPath(self) -> QUrl:
"""
Get the preferred path to export materials to.
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
file path.
:return: The preferred path to export all materials to.
"""
cura_application = cura.CuraApplication.CuraApplication.getInstance()
device_manager = cura_application.getOutputDeviceManager()
devices = device_manager.getOutputDevices()
for device in devices:
if device.__class__.__name__ == "RemovableDriveOutputDevice":
return QUrl.fromLocalFile(device.getId())
else: # No removable drives? Use local path.
return cura_application.getDefaultPath("dialog_material_path")
@pyqtSlot(QUrl)
def exportAll(self, file_path: QUrl) -> None:
"""
Export all materials to a certain file path.
:param file_path: The path to export the materials to.
"""
registry = CuraContainerRegistry.getInstance()
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
for metadata in registry.findInstanceContainersMetadata(type = "material"):
if metadata["base_file"] != metadata["id"]: # Only process base files.
continue
if metadata["id"] == "empty_material": # Don't export the empty material.
continue
material = registry.findContainers(id = metadata["id"])[0]
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
filename = metadata["id"] + "." + suffix
archive.writestr(filename, material.serialize())

View File

@ -54,6 +54,7 @@ class LocalAuthorizationServer:
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.
Logger.log("d", "Auth web server was already running. Updating the verification code")
self._web_server.setVerificationCode(verification_code) self._web_server.setVerificationCode(verification_code)
return return
@ -85,6 +86,7 @@ class LocalAuthorizationServer:
except OSError: except OSError:
# OS error can happen if the socket was already closed. We really don't care about that case. # OS error can happen if the socket was already closed. We really don't care about that case.
pass pass
Logger.log("d", "Local oauth2 web server was shut down")
self._web_server = None self._web_server = None
self._web_server_thread = None self._web_server_thread = None
@ -96,12 +98,13 @@ class LocalAuthorizationServer:
:return: None :return: None
""" """
Logger.log("d", "Local web server for authorization has started")
if self._web_server: if self._web_server:
if sys.platform == "win32": if sys.platform == "win32":
try: try:
self._web_server.serve_forever() self._web_server.serve_forever()
except OSError as e: except OSError:
Logger.warning(str(e)) Logger.logException("w", "An exception happened while serving the auth server")
else: else:
# Leave the default behavior in non-windows platforms # Leave the default behavior in non-windows platforms
self._web_server.serve_forever() self._web_server.serve_forever()

View File

@ -86,6 +86,14 @@ class GlobalStack(CuraContainerStack):
def supportsNetworkConnection(self): def supportsNetworkConnection(self):
return self.getMetaDataEntry("supports_network_connection", False) return self.getMetaDataEntry("supports_network_connection", False)
@pyqtProperty(bool, constant = True)
def supportsMaterialExport(self):
"""
Whether the printer supports Cura's export format of material profiles.
:return: ``True`` if it supports it, or ``False`` if not.
"""
return self.getMetaDataEntry("supports_material_export", False)
@classmethod @classmethod
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 2 return 2

View File

@ -4,12 +4,12 @@
import argparse #To run the engine in debug mode if the front-end is in debug mode. import argparse #To run the engine in debug mode if the front-end is in debug mode.
from collections import defaultdict from collections import defaultdict
import os import os
from PyQt5.QtCore import QObject, QTimer, pyqtSlot from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot
import sys import sys
from time import time from time import time
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
from PyQt5.QtGui import QImage from PyQt5.QtGui import QDesktopServices, QImage
from UM.Backend.Backend import Backend, BackendState from UM.Backend.Backend import Backend, BackendState
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -157,6 +157,18 @@ class CuraEngineBackend(QObject, Backend):
self.determineAutoSlicing() self.determineAutoSlicing()
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._slicing_error_message = Message(
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
title = catalog.i18nc("@message:title", "Slicing failed")
)
self._slicing_error_message.addAction(
action_id = "report_bug",
name = catalog.i18nc("@message:button", "Report a bug"),
description = catalog.i18nc("@message:description", "Report a bug on Ultimaker Cura's issue tracker."),
icon = "[no_icon]"
)
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._snapshot = None #type: Optional[QImage] self._snapshot = None #type: Optional[QImage]
application.initializationFinished.connect(self.initialize) application.initializationFinished.connect(self.initialize)
@ -922,9 +934,22 @@ class CuraEngineBackend(QObject, Backend):
if not self._restart: if not self._restart:
if self._process: # type: ignore if self._process: # type: ignore
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore return_code = self._process.wait()
if return_code != 0:
Logger.log("e", f"Backend exited abnormally with return code {return_code}!")
self._slicing_error_message.show()
self.setState(BackendState.Error)
self.stopSlicing()
else:
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
self._process = None # type: ignore self._process = None # type: ignore
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
"""
Triggered when the user wants to report an error in the back-end.
"""
QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose"))
def _onGlobalStackChanged(self) -> None: def _onGlobalStackChanged(self) -> None:
"""Called when the global container stack changes""" """Called when the global container stack changes"""

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 378.13 348.13" version="1.1">
<defs
id="defs7">
<style
id="style2">
.cls-2,.cls-6{fill:#c5dbfb;}
.cls-6,.cls-7{stroke-width:2px;}
.cls-7{fill:#f3f8fe;}
.cls-6,.cls-7{stroke:#061884;}
</style>
</defs>
<path class="cls-2" d="M43,17V3H83a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 3 1 C 1.8954305 1 1 1.8954305 1 3 L 1 67 C 1 68.104569 1.8954305 69 3 69 L 72.152344 69 A 100 100 0 0 1 89 49.873047 L 89 19 C 89 17.895431 88.104569 17 87 17 L 56 17 L 40 1 L 3 1 z " />
<path fill="#c5dbfb" d="M 3 0 C 1.3549904 0 0 1.3549904 0 3 L 0 67 C 0 68.64501 1.3549904 70 3 70 L 71.484375 70 A 100 100 0 0 1 72.835938 68 L 3 68 C 2.4358706 68 2 67.564129 2 67 L 2 3 C 2 2.4358706 2.4358706 2 3 2 L 39.585938 2 L 55.585938 18 L 87 18 C 87.564129 18 88 18.435871 88 19 L 88 50.765625 A 100 100 0 0 1 90 49.007812 L 90 19 C 90 17.35499 88.64501 16 87 16 L 56.414062 16 L 40.414062 0 L 3 0 z " />
<path class="cls-2" d="M153,17V3h40a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 113 1 C 111.89543 1 111 1.8954305 111 3 L 111 35.201172 A 100 100 0 0 1 155 25 A 100 100 0 0 1 199 35.201172 L 199 19 C 199 17.895431 198.10457 17 197 17 L 166 17 L 150 1 L 113 1 z " />
<path fill="#c5dbfb" d="M 113 0 C 111.35499 0 110 1.3549904 110 3 L 110 35.699219 A 100 100 0 0 1 112 34.716797 L 112 3 C 112 2.4358706 112.43587 2 113 2 L 149.58594 2 L 165.58594 18 L 197 18 C 197.56413 18 198 18.435871 198 19 L 198 34.716797 A 100 100 0 0 1 200 35.699219 L 200 19 C 200 17.35499 198.64501 16 197 16 L 166.41406 16 L 150.41406 0 L 113 0 z " />
<path class="cls-2" d="M263,17V3h40a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 223 1 C 221.89543 1 221 1.8954305 221 3 L 221 49.875 A 100 100 0 0 1 237.84961 69 L 307 69 C 308.10457 69 309 68.104569 309 67 L 309 19 C 309 17.895431 308.10457 17 307 17 L 276 17 L 260 1 L 223 1 z " />
<path fill="#c5dbfb" d="M 223 0 C 221.35499 0 220 1.3549904 220 3 L 220 49.005859 A 100 100 0 0 1 222 50.765625 L 222 3 C 222 2.4358706 222.43587 2 223 2 L 259.58594 2 L 275.58594 18 L 307 18 C 307.56413 18 308 18.435871 308 19 L 308 67 C 308 67.564129 307.56413 68 307 68 L 237.16406 68 A 100 100 0 0 1 238.51562 70 L 307 70 C 308.64501 70 310 68.64501 310 67 L 310 19 C 310 17.35499 308.64501 16 307 16 L 276.41406 16 L 260.41406 0 L 223 0 z " />
<path fill="#c5dbfb" d="M 43 93 L 43 107 L 56.634766 107 A 100 100 0 0 1 60.259766 93 L 43 93 z " />
<path fill="white" d="M 3 91 C 1.8954305 91 1 91.895431 1 93 L 1 157 C 1 158.10457 1.8954305 159 3 159 L 60.958984 159 A 100 100 0 0 1 55 125 A 100 100 0 0 1 56.634766 107 L 56 107 L 40 91 L 3 91 z " />
<path fill="#c5dbfb" d="M 3 90 C 1.3549904 90 0 91.35499 0 93 L 0 157 C 0 158.64501 1.3549904 160 3 160 L 61.324219 160 A 100 100 0 0 1 60.603516 158 L 3 158 C 2.4358706 158 2 157.56413 2 157 L 2 93 C 2 92.435871 2.4358706 92 3 92 L 39.585938 92 L 55.585938 108 L 56.455078 108 A 100 100 0 0 1 56.822266 106 L 56.414062 106 L 40.414062 90 L 3 90 z " />
<path class="cls-2" d="M263,107V93h40a2,2,0,0,1,2,2v12Z" />
<path fill="white" d="M 249.04102 91 A 100 100 0 0 1 255 125 A 100 100 0 0 1 249.04102 159 L 307 159 C 308.10457 159 309 158.10457 309 157 L 309 109 C 309 107.89543 308.10457 107 307 107 L 276 107 L 260 91 L 249.04102 91 z " />
<path fill="#c5dbfb" d="M 248.67578 90 A 100 100 0 0 1 249.39648 92 L 259.58594 92 L 275.58594 108 L 307 108 C 307.56413 108 308 108.43587 308 109 L 308 157 C 308 157.56413 307.56413 158 307 158 L 249.39844 158 A 100 100 0 0 1 248.67383 160 L 307 160 C 308.64501 160 310 158.64501 310 157 L 310 109 C 310 107.35499 308.64501 106 307 106 L 276.41406 106 L 260.41406 90 L 248.67578 90 z " />
<path fill="#c5dbfb" d="M 43 183 L 43 197 L 85 197 L 85 196.41406 A 100 100 0 0 1 73.539062 183 L 43 183 z " />
<path fill="white" d="M 3 181 C 1.8954305 181 1 181.89543 1 183 L 1 247 C 1 248.10457 1.8954305 249 3 249 L 87 249 C 88.104569 249 89 248.10457 89 247 L 89 200.125 A 100 100 0 0 1 85.603516 197 L 56 197 L 40 181 L 3 181 z " />
<path fill="#c5dbfb" d="M 3 180 C 1.3549904 180 0 181.35499 0 183 L 0 247 C 0 248.64501 1.3549904 250 3 250 L 87 250 C 88.64501 250 90 248.64501 90 247 L 90 200.99414 A 100 100 0 0 1 88 199.23438 L 88 247 C 88 247.56413 87.564129 248 87 248 L 3 248 C 2.4358706 248 2 247.56413 2 247 L 2 183 C 2 182.43587 2.4358706 182 3 182 L 39.585938 182 L 55.585938 198 L 86.65625 198 A 100 100 0 0 1 84.580078 196 L 56.414062 196 L 40.414062 180 L 3 180 z " />
<path fill="white" d="M 111 214.79883 L 111 247 C 111 248.10457 111.89543 249 113 249 L 197 249 C 198.10457 249 199 248.10457 199 247 L 199 214.79883 A 100 100 0 0 1 155 225 A 100 100 0 0 1 111 214.79883 z " />
<path fill="#c5dbfb" d="M 110 214.30078 L 110 247 C 110 248.64501 111.35499 250 113 250 L 197 250 C 198.64501 250 200 248.64501 200 247 L 200 214.30078 A 100 100 0 0 1 198 215.2832 L 198 247 C 198 247.56413 197.56413 248 197 248 L 113 248 C 112.43587 248 112 247.56413 112 247 L 112 215.2832 A 100 100 0 0 1 110 214.30078 z " />
<path class="cls-2" d="M263,197V183h40a2,2,0,0,1,2,2v12Z" />
<path fill="white" d="M 237.84766 181 A 100 100 0 0 1 221 200.12695 L 221 247 C 221 248.10457 221.89543 249 223 249 L 307 249 C 308.10457 249 309 248.10457 309 247 L 309 199 C 309 197.89543 308.10457 197 307 197 L 276 197 L 260 181 L 237.84766 181 z " />
<path fill="#c5dbfb" d="M 238.51562 180 A 100 100 0 0 1 237.16406 182 L 259.58594 182 L 275.58594 198 L 307 198 C 307.56413 198 308 198.43587 308 199 L 308 247 C 308 247.56413 307.56413 248 307 248 L 223 248 C 222.43587 248 222 247.56413 222 247 L 222 199.23438 A 100 100 0 0 1 220 200.99219 L 220 247 C 220 248.64501 221.35499 250 223 250 L 307 250 C 308.64501 250 310 248.64501 310 247 L 310 199 C 310 197.35499 308.64501 196 307 196 L 276.41406 196 L 260.41406 180 L 238.51562 180 z " />
<path class="cls-6" d="M351.12,322.62h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7A10,10,0,0,1,351.12,322.62Z" transform="translate(850.61 309.91) rotate(135)" />
<rect class="cls-7" x="293.75" y="225.25" width="40" height="117" transform="translate(-108.74 304.96) rotate(-45)" />
<polyline class="cls-7" points="213.69 199.25 252.58 238.14 267.43 223.29 228.54 184.4" />
<path fill="white" stroke="#061884" stroke-width="2px" d="M 154.94141 30 A 95 95 0 0 0 60 125 A 95 95 0 0 0 155 220 A 95 95 0 0 0 250 125 A 95 95 0 0 0 155 30 A 95 95 0 0 0 154.94141 30 z M 154.82812 40 A 85 85 0 0 1 155 40 A 85 85 0 0 1 240 125 A 85 85 0 0 1 155 210 A 85 85 0 0 1 70 125 A 85 85 0 0 1 154.82812 40 z " />
<path class="cls-6" d="M256.37,227.87h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7a10,10,0,0,1,10-10Z" transform="translate(-89.12 257.58) rotate(-45)" />
<path fill="white" d="M 154.94141 45 A 80 80 0 0 0 111 58.185547 L 111 67 C 111 68.104569 111.89543 69 113 69 L 197 69 C 198.10457 69 199 68.104569 199 67 L 199 58.1875 A 80 80 0 0 0 155 45 A 80 80 0 0 0 154.94141 45 z " />
<path fill="#061884" d="M 112 57.539062 A 80 80 0 0 0 110 58.857422 L 110 67 C 110 68.64501 111.35499 70 113 70 L 197 70 C 198.64501 70 200 68.64501 200 67 L 200 58.857422 A 80 80 0 0 0 198 57.541016 L 198 67 C 198 67.564129 197.56413 68 197 68 L 113 68 C 112.43587 68 112 67.564129 112 67 L 112 57.539062 z " />
<path fill="#196ef0" d="M 81.679688 93 A 80 80 0 0 0 77.050781 107 L 85 107 L 85 95 A 2 2 0 0 0 83 93 L 81.679688 93 z " />
<path fill="white" d="M 77.050781 107 A 80 80 0 0 0 75 125 A 80 80 0 0 0 82.585938 159 L 87 159 C 88.104569 159 89 158.10457 89 157 L 89 109 C 89 107.89543 88.104569 107 87 107 L 77.050781 107 z " />
<path fill="#061884" d="M 77.289062 106 A 80 80 0 0 0 76.828125 108 L 87 108 C 87.564129 108 88 108.43587 88 109 L 88 157 C 88 157.56413 87.564129 158 87 158 L 82.125 158 A 80 80 0 0 0 83.0625 160 L 87 160 C 88.64501 160 90 158.64501 90 157 L 90 109 C 90 107.35499 88.64501 106 87 106 L 77.289062 106 z " />
<path fill="white" d="M 223 91 C 221.89543 91 221 91.895431 221 93 L 221 157 C 221 158.10457 221.89543 159 223 159 L 227.41406 159 A 80 80 0 0 0 235 125 A 80 80 0 0 0 227.41406 91 L 223 91 z " />
<path fill="#061884" d="M 223 90 C 221.35499 90 220 91.35499 220 93 L 220 157 C 220 158.64501 221.35499 160 223 160 L 226.9375 160 A 80 80 0 0 0 227.87695 158 L 223 158 C 222.43587 158 222 157.56413 222 157 L 222 93 C 222 92.435871 222.43587 92 223 92 L 227.875 92 A 80 80 0 0 0 226.9375 90 L 223 90 z " />
<path fill="#196ef0" d="M 153 183 L 153 197 L 189.86914 197 A 80 80 0 0 0 195 194.28125 L 195 185 A 2 2 0 0 0 193 183 L 153 183 z "/>
<path fill="white" d="M 113 181 C 111.89543 181 111 181.89543 111 183 L 111 191.8125 A 80 80 0 0 0 155 205 A 80 80 0 0 0 189.86914 197 L 166 197 L 150 181 L 113 181 z " />
<path fill="#061884" d="M 113 180 C 111.35499 180 110 181.35499 110 183 L 110 191.14258 A 80 80 0 0 0 112 192.45898 L 112 183 C 112 182.43587 112.43587 182 113 182 L 149.58594 182 L 165.58594 198 L 187.72461 198 A 80 80 0 0 0 191.86328 196 L 166.41406 196 L 150.41406 180 L 113 180 z " />
<path fill="#061884" d="m 149.18,133.69 v -3.48 a 14.36,14.36 0 0 1 1.74,-7.25 20.17,20.17 0 0 1 6.4,-6.17 25.87,25.87 0 0 0 5.68,-4.79 7,7 0 0 0 1.48,-4.34 4.13,4.13 0 0 0 -1.93,-3.62 9,9 0 0 0 -5.14,-1.3 24.94,24.94 0 0 0 -7.34,1.16 45.2,45.2 0 0 0 -7.78,3.31 l -5.37,-10.64 a 48.41,48.41 0 0 1 9.89,-4.21 40.25,40.25 0 0 1 11.67,-1.61 q 9.57,0 14.9,4.43 a 14.16,14.16 0 0 1 5.32,11.41 15.41,15.41 0 0 1 -2.55,9 30.38,30.38 0 0 1 -7.92,7.34 32.11,32.11 0 0 0 -5.23,4.37 5.91,5.91 0 0 0 -1.34,4 v 2.41 z m -1.61,15.12 q 0,-4.38 2.46,-6.12 a 10,10 0 0 1 5.95,-1.75 9.69,9.69 0 0 1 5.77,1.75 q 2.46,1.74 2.46,6.12 0,4.22 -2.46,6 a 9.42,9.42 0 0 1 -5.77,1.84 9.69,9.69 0 0 1 -5.95,-1.84 q -2.46,-1.81 -2.46,-6 z" />
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -93,7 +93,7 @@ Popup
} }
validator: RegExpValidator validator: RegExpValidator
{ {
regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/ regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/
} }
text: PrintInformation.jobName text: PrintInformation.jobName
@ -148,7 +148,7 @@ Popup
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
text: "Create" text: "Create"
enabled: newProjectNameTextField.text != "" && !busy enabled: newProjectNameTextField.text.length >= 2 && !busy
onClicked: onClicked:
{ {

View File

@ -63,7 +63,7 @@ Item
anchors.topMargin: UM.Theme.getSize("thin_margin").height anchors.topMargin: UM.Theme.getSize("thin_margin").height
validator: RegExpValidator validator: RegExpValidator
{ {
regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/ regExp: /^[\w\-\. ()]{0,255}$/
} }
text: PrintInformation.jobName text: PrintInformation.jobName
@ -200,7 +200,7 @@ Item
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
text: "Save" text: "Save"
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text != "" enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1
onClicked: onClicked:
{ {

View File

@ -1,10 +1,12 @@
// Copyright (C) 2021 Ultimaker B.V. // Copyright (C) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM import UM 1.2 as UM
import Cura 1.6 as Cura import Cura 1.6 as Cura
@ -18,7 +20,7 @@ Item
width: parent.width width: parent.width
height: parent.height height: parent.height
property alias createNewProjectButtonVisible: createNewProjectButton.visible property bool createNewProjectButtonVisible: true
anchors anchors
{ {
@ -29,25 +31,37 @@ Item
margins: UM.Theme.getSize("default_margin").width margins: UM.Theme.getSize("default_margin").width
} }
Label RowLayout
{ {
id: selectProjectLabel id: headerRow
text: "Select Project" anchors
font: UM.Theme.getFont("medium") {
color: UM.Theme.getColor("small_button_text") top: parent.top
anchors.top: parent.top left: parent.left
anchors.left: parent.left right: parent.right
visible: projectListContainer.visible }
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
Cura.TextField
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
leftIcon: UM.Theme.getIcon("Magnifier")
placeholderText: "Search"
} }
Cura.SecondaryButton Cura.SecondaryButton
{ {
id: createNewProjectButton id: createNewProjectButton
anchors.verticalCenter: selectProjectLabel.verticalCenter
anchors.right: parent.right
text: "New Library project" text: "New Library project"
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
onClicked: onClicked:
{ {
@ -56,6 +70,21 @@ Item
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
} }
Cura.SecondaryButton
{
id: upgradePlanButton
text: "Upgrade plan"
iconSource: UM.Theme.getIcon("LinkExternal")
visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects."
tooltipWidth: parent.width * 0.5
onClicked: Qt.openUrlExternally("https://ultimaker.com/software/ultimaker-essentials/sign-up-cura?utm_source=cura&utm_medium=software&utm_campaign=lib-max")
}
}
Item Item
{ {
id: noLibraryProjectsContainer id: noLibraryProjectsContainer
@ -76,19 +105,18 @@ Item
{ {
id: digitalFactoryImage id: digitalFactoryImage
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: "../images/digital_factory.svg" source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
sourceSize.width: width
sourceSize.height: height
} }
Label Label
{ {
id: noLibraryProjectsLabel id: noLibraryProjectsLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: "It appears that you don't have any projects in the Library yet." text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
} }
Cura.TertiaryButton Cura.TertiaryButton
@ -97,6 +125,7 @@ Item
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: "Visit Digital Library" text: "Visit Digital Library"
onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library") onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library")
visible: searchBar.text === "" //Show the link to Digital Library when there are no projects in the user's Library.
} }
} }
} }
@ -106,7 +135,7 @@ Item
id: projectListContainer id: projectListContainer
anchors anchors
{ {
top: selectProjectLabel.bottom top: headerRow.bottom
topMargin: UM.Theme.getSize("default_margin").height topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom bottom: parent.bottom
left: parent.left left: parent.left

View File

@ -22,6 +22,7 @@ from .DFFileUploader import DFFileUploader
from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest
from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse
from .DFPrintJobUploadRequest import DFPrintJobUploadRequest from .DFPrintJobUploadRequest import DFPrintJobUploadRequest
from .DigitalFactoryFeatureBudgetResponse import DigitalFactoryFeatureBudgetResponse
from .DigitalFactoryFileResponse import DigitalFactoryFileResponse from .DigitalFactoryFileResponse import DigitalFactoryFileResponse
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
from .PaginationLinks import PaginationLinks from .PaginationLinks import PaginationLinks
@ -54,9 +55,67 @@ class DigitalFactoryApiClient:
self._http = HttpRequestManager.getInstance() self._http = HttpRequestManager.getInstance()
self._on_error = on_error self._on_error = on_error
self._file_uploader = None # type: Optional[DFFileUploader] self._file_uploader = None # type: Optional[DFFileUploader]
self._library_max_private_projects: Optional[int] = None
self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]
def checkUserHasAccess(self, callback: Callable) -> None:
"""Checks if the user has any sort of access to the digital library.
A user is considered to have access if the max-# of private projects is greater then 0 (or -1 for unlimited).
"""
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and
response.library_max_private_projects is not None):
callback(
response.library_max_private_projects == -1 or # Note: -1 is unlimited
response.library_max_private_projects > 0)
self._library_max_private_projects = response.library_max_private_projects
else:
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
callback(False)
self._http.get(f"{self.CURA_API_ROOT}/feature_budgets",
scope = self._scope,
callback = self._parseCallback(callbackWrap, DigitalFactoryFeatureBudgetResponse, callbackWrap),
error_callback = callbackWrap,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None:
"""
Checks if the user is allowed to create new library projects.
A user is allowed to create new library projects if the haven't reached their maximum allowed private projects.
"""
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if response is not None:
if isinstance(response, DigitalFactoryProjectResponse): # The user has only one private project
callback(True)
elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response):
callback(len(response) < cast(int, self._library_max_private_projects))
else:
Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}")
callback(False)
else:
Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}")
callback(False)
if self._library_max_private_projects is not None and self._library_max_private_projects > 0:
# The user has a limit in the number of private projects they can create. Check whether they have already
# reached that limit.
# Note: Set the pagination manager to None when doing this get request, or else the next/previous links
# of the pagination will become corrupted
url = f"{self.CURA_API_ROOT}/projects?shared=false&limit={self._library_max_private_projects}"
self._http.get(url,
scope = self._scope,
callback = self._parseCallback(callbackWrap, DigitalFactoryProjectResponse, callbackWrap, pagination_manager = None),
error_callback = callbackWrap,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
else:
# If the limit is -1, then the user is allowed unlimited projects. If its 0 then they are not allowed to
# create any projects
callback(self._library_max_private_projects == -1)
def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None: def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
""" """
Retrieves a digital factory project by its library project id. Retrieves a digital factory project by its library project id.
@ -73,7 +132,7 @@ class DigitalFactoryApiClient:
error_callback = failed, error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT) timeout = self.DEFAULT_REQUEST_TIMEOUT)
def getProjectsFirstPage(self, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None: def getProjectsFirstPage(self, search_filter: str, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None:
""" """
Retrieves digital factory projects for the user that is currently logged in. Retrieves digital factory projects for the user that is currently logged in.
@ -81,13 +140,18 @@ class DigitalFactoryApiClient:
according to the limit set in the pagination manager. If there is no projects pagination manager, this function according to the limit set in the pagination manager. If there is no projects pagination manager, this function
leaves the project limit to the default set on the server side (999999). leaves the project limit to the default set on the server side (999999).
:param search_filter: Text to filter the search results. If given an empty string, results are not filtered.
:param on_finished: The function to be called after the result is parsed. :param on_finished: The function to be called after the result is parsed.
:param failed: The function to be called if the request fails. :param failed: The function to be called if the request fails.
""" """
url = "{}/projects".format(self.CURA_API_ROOT) url = f"{self.CURA_API_ROOT}/projects"
query_character = "?"
if self._projects_pagination_mgr: if self._projects_pagination_mgr:
self._projects_pagination_mgr.reset() # reset to clear all the links and response metadata self._projects_pagination_mgr.reset() # reset to clear all the links and response metadata
url += "?limit={}".format(self._projects_pagination_mgr.limit) url += f"{query_character}limit={self._projects_pagination_mgr.limit}"
query_character = "&"
if search_filter != "":
url += f"{query_character}search={search_filter}"
self._http.get(url, self._http.get(url,
scope = self._scope, scope = self._scope,
@ -301,12 +365,10 @@ class DigitalFactoryApiClient:
:param on_finished: The function to be called after the result is parsed. :param on_finished: The function to be called after the result is parsed.
:param on_error: The function to be called if anything goes wrong. :param on_error: The function to be called if anything goes wrong.
""" """
Logger.log("i", "Attempt to create new DF project '{}'.".format(project_name))
display_name = re.sub(r"[^a-zA-Z0-9- ./™®ö+']", " ", project_name)
Logger.log("i", "Attempt to create new DF project '{}'.".format(display_name))
url = "{}/projects".format(self.CURA_API_ROOT) url = "{}/projects".format(self.CURA_API_ROOT)
data = json.dumps({"data": {"display_name": display_name}}).encode() data = json.dumps({"data": {"display_name": project_name}}).encode()
self._http.put(url, self._http.put(url,
scope = self._scope, scope = self._scope,
data = data, data = data,

View File

@ -1,4 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json import json
import math import math
import os import os
@ -8,7 +10,7 @@ from enum import IntEnum
from pathlib import Path from pathlib import Path
from typing import Optional, List, Dict, Any, cast from typing import Optional, List, Dict, Any, cast
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QUrl from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
@ -89,6 +91,12 @@ class DigitalFactoryController(QObject):
uploadFileError = Signal() uploadFileError = Signal()
uploadFileFinished = Signal() uploadFileFinished = Signal()
"""Signal to inform about the state of user access."""
userAccessStateChanged = pyqtSignal(bool)
"""Signal to inform whether the user is allowed to create more Library projects."""
userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
def __init__(self, application: CuraApplication) -> None: def __init__(self, application: CuraApplication) -> None:
super().__init__(parent = None) super().__init__(parent = None)
@ -106,12 +114,18 @@ class DigitalFactoryController(QObject):
self._has_more_projects_to_load = False self._has_more_projects_to_load = False
self._account = self._application.getInstance().getCuraAPI().account # type: Account self._account = self._application.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
# Initialize the project model # Initialize the project model
self._project_model = DigitalFactoryProjectModel() self._project_model = DigitalFactoryProjectModel()
self._selected_project_idx = -1 self._selected_project_idx = -1
self._project_creation_error_text = "Something went wrong while creating a new project. Please try again." self._project_creation_error_text = "Something went wrong while creating a new project. Please try again."
self._project_filter = ""
self._project_filter_change_timer = QTimer()
self._project_filter_change_timer.setInterval(200)
self._project_filter_change_timer.setSingleShot(True)
self._project_filter_change_timer.timeout.connect(self._applyProjectFilter)
# Initialize the file model # Initialize the file model
self._file_model = DigitalFactoryFileModel() self._file_model = DigitalFactoryFileModel()
@ -131,6 +145,9 @@ class DigitalFactoryController(QObject):
self._application.engineCreatedSignal.connect(self._onEngineCreated) self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._application.initializationFinished.connect(self._applicationInitializationFinished) self._application.initializationFinished.connect(self._applicationInitializationFinished)
self._user_has_access = False
self._user_account_can_create_new_project = False
def clear(self) -> None: def clear(self) -> None:
self._project_model.clearProjects() self._project_model.clearProjects()
self._api.clear() self._api.clear()
@ -143,16 +160,22 @@ class DigitalFactoryController(QObject):
self.setSelectedProjectIndex(-1) self.setSelectedProjectIndex(-1)
def _onLoginStateChanged(self, logged_in: bool) -> None:
def callback(has_access, **kwargs):
self._user_has_access = has_access
self.userAccessStateChanged.emit(logged_in)
self._api.checkUserHasAccess(callback)
def userAccountHasLibraryAccess(self) -> bool: def userAccountHasLibraryAccess(self) -> bool:
""" """
Checks whether the currently logged in user account has access to the Digital Library Checks whether the currently logged in user account has access to the Digital Library
:return: True if the user account has Digital Library access, else False :return: True if the user account has Digital Library access, else False
""" """
subscriptions = [] # type: List[Dict[str, Any]] if self._user_has_access:
if self._account.userProfile: self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
subscriptions = self._account.userProfile.get("subscriptions", []) return self._user_has_access
return len(subscriptions) > 0
def initialize(self, preselected_project_id: Optional[str] = None) -> None: def initialize(self, preselected_project_id: Optional[str] = None) -> None:
self.clear() self.clear()
@ -162,7 +185,7 @@ class DigitalFactoryController(QObject):
if preselected_project_id: if preselected_project_id:
self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed) self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
else: else:
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None: def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None:
""" """
@ -288,6 +311,38 @@ class DigitalFactoryController(QObject):
self._selected_file_indices = file_indices self._selected_file_indices = file_indices
self.selectedFileIndicesChanged.emit(file_indices) self.selectedFileIndicesChanged.emit(file_indices)
def setProjectFilter(self, new_filter: str) -> None:
"""
Called when the user wants to change the search filter for projects.
The filter is not immediately applied. There is some delay to allow the user to finish typing.
:param new_filter: The new filter that the user wants to apply.
"""
self._project_filter = new_filter
self._project_filter_change_timer.start()
"""
Signal to notify Qt that the applied filter has changed.
"""
projectFilterChanged = pyqtSignal()
@pyqtProperty(str, notify = projectFilterChanged, fset = setProjectFilter)
def projectFilter(self) -> str:
"""
The current search filter being applied to the project list.
:return: The current search filter being applied to the project list.
"""
return self._project_filter
def _applyProjectFilter(self) -> None:
"""
Actually apply the current filter to search for projects with the user-defined search string.
:return:
"""
self.clear()
self.projectFilterChanged.emit()
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel": def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel":
return self._project_model return self._project_model
@ -502,7 +557,8 @@ class DigitalFactoryController(QObject):
# false, we also need to clean it from the projects model # false, we also need to clean it from the projects model
self._project_model.clearProjects() self._project_model.clearProjects()
self.setSelectedProjectIndex(-1) self.setSelectedProjectIndex(-1)
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress) self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
self._has_preselected_project = new_has_preselected_project self._has_preselected_project = new_has_preselected_project
self.preselectedProjectChanged.emit() self.preselectedProjectChanged.emit()
@ -511,6 +567,14 @@ class DigitalFactoryController(QObject):
def hasPreselectedProject(self) -> bool: def hasPreselectedProject(self) -> bool:
return self._has_preselected_project return self._has_preselected_project
def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None:
self._user_account_can_create_new_project = can_create_new_library_project
self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project)
@pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged)
def userAccountCanCreateNewLibraryProject(self) -> bool:
return self._user_account_can_create_new_project
@pyqtSlot(str, "QStringList") @pyqtSlot(str, "QStringList")
def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None: def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
""" """

View File

@ -0,0 +1,43 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .BaseModel import BaseModel
from typing import Optional
class DigitalFactoryFeatureBudgetResponse(BaseModel):
"""Class representing the capabilities of a user account for Digital Library.
NOTE: For each max_..._projects fields, '-1' means unlimited!
"""
def __init__(self,
library_can_use_business_value: Optional[bool] = False,
library_can_use_comments: Optional[bool] = False,
library_can_use_status: Optional[bool] = False,
library_can_use_tags: Optional[bool] = False,
library_can_use_technical_requirements: Optional[bool] = False,
library_max_organization_shared_projects: Optional[int] = None, # -1 means unlimited
library_max_private_projects: Optional[int] = None, # -1 means unlimited
library_max_team_shared_projects: Optional[int] = None, # -1 means unlimited
**kwargs) -> None:
self.library_can_use_business_value = library_can_use_business_value
self.library_can_use_comments = library_can_use_comments
self.library_can_use_status = library_can_use_status
self.library_can_use_tags = library_can_use_tags
self.library_can_use_technical_requirements = library_can_use_technical_requirements
self.library_max_organization_shared_projects = library_max_organization_shared_projects # -1 means unlimited
self.library_max_private_projects = library_max_private_projects # -1 means unlimited
self.library_max_team_shared_projects = library_max_team_shared_projects # -1 means unlimited
super().__init__(**kwargs)
def __repr__(self) -> str:
return "max private: {}, max org: {}, max team: {}".format(
self.library_max_private_projects,
self.library_max_organization_shared_projects,
self.library_max_team_shared_projects)
# Validates the model, raising an exception if the model is invalid.
def validate(self) -> None:
super().validate()
# No validation for now, as the response can be "data: []", which should be interpreted as all False and 0's

View File

@ -22,7 +22,7 @@ class DigitalFactoryFileProvider(FileProvider):
self._dialog = None self._dialog = None
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged) self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self.priority = 10 self.priority = 10
@ -53,7 +53,7 @@ class DigitalFactoryFileProvider(FileProvider):
if not self._dialog: if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Open dialog.") Logger.log("e", "Unable to create the Digital Library Open dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None: def _onUserAccessStateChanged(self, logged_in: bool) -> None:
""" """
Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status
:param logged_in: The new login status :param logged_in: The new login status

View File

@ -45,7 +45,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
self._writing = False self._writing = False
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged) self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
@ -97,7 +97,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
if not self._dialog: if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Save dialog.") Logger.log("e", "Unable to create the Digital Library Save dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None: def _onUserAccessStateChanged(self, logged_in: bool) -> None:
""" """
Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status
:param logged_in: The new login status :param logged_in: The new login status

View File

@ -1,3 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@ -37,7 +40,7 @@ def test_getProjectsFirstPage(api_client):
failed_callback = MagicMock() failed_callback = MagicMock()
# Call # Call
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback) api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
# Asserts # Asserts
pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects
@ -45,16 +48,16 @@ def test_getProjectsFirstPage(api_client):
args = http_manager.get.call_args_list[0] args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit # Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20" assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20&search=filter"
# Change the limit & try again # Change the limit & try again
http_manager.get.reset_mock() http_manager.get.reset_mock()
pagination_manager.limit = 80 pagination_manager.limit = 80
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback) api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
args = http_manager.get.call_args_list[0] args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit # Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80" assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80&search=filter"
def test_getMoreProjects_noNewProjects(api_client): def test_getMoreProjects_noNewProjects(api_client):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger from UM.Logger import Logger
@ -103,20 +103,27 @@ class PerObjectSettingsTool(Tool):
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance) settings.addInstance(new_instance)
for property_key in ["top_bottom_thickness", "wall_thickness", "wall_line_count"]: # Override some settings to ensure that the infill mesh by default adds no skin or walls. Or remove them if not an infill mesh.
specialized_settings = {
"top_bottom_thickness": 0,
"top_thickness": "=top_bottom_thickness",
"bottom_thickness": "=top_bottom_thickness",
"top_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))",
"bottom_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))",
"wall_thickness": 0,
"wall_line_count": "=max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0"
}
for property_key in specialized_settings:
if mesh_type == "infill_mesh": if mesh_type == "infill_mesh":
if settings.getInstance(property_key) is None: if settings.getInstance(property_key) is None:
definition = stack.getSettingDefinition(property_key) definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings) new_instance = SettingInstance(definition, settings)
# We just want the wall_line count to be there in case it was overriden in the global stack. new_instance.setProperty("value", specialized_settings[property_key])
# as such, we don't need to set a value.
if property_key != "wall_line_count":
new_instance.setProperty("value", 0)
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance) settings.addInstance(new_instance)
settings_visibility_changed = True settings_visibility_changed = True
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and (settings.getProperty(property_key, "value") == 0 or property_key == "wall_line_count"): elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and property_key in specialized_settings:
settings.removeInstance(property_key) settings.removeInstance(property_key)
settings_visibility_changed = True settings_visibility_changed = True

View File

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.9
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
@ -13,6 +13,8 @@ Item
{ {
id: prepareMenu id: prepareMenu
property var fileProviderModel: CuraApplication.getFileProviderModel()
UM.I18nCatalog UM.I18nCatalog
{ {
id: catalog id: catalog
@ -36,9 +38,9 @@ Item
{ {
id: itemRow id: itemRow
anchors.left: openFileButton.right anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.leftMargin: UM.Theme.getSize("default_margin").width + openFileButton.width + openFileMenu.width
property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3) property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3)
height: parent.height height: parent.height
@ -74,22 +76,116 @@ Item
} }
} }
//Pop-up shown when there are multiple items to select from.
Cura.ExpandablePopup
{
id: openFileMenu
visible: prepareMenu.fileProviderModel.count > 1
contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft
headerCornerSide: Cura.RoundedRectangle.Direction.All
headerPadding: Math.round((parent.height - UM.Theme.getSize("button_icon").height) / 2)
contentPadding: UM.Theme.getSize("default_lining").width
enabled: visible
height: parent.height
width: visible ? (headerPadding * 3 + UM.Theme.getSize("button_icon").height + iconSize) : 0
headerItem: UM.RecolorImage
{
id: menuIcon
source: UM.Theme.getIcon("Folder", "medium")
color: UM.Theme.getColor("icon")
sourceSize.height: height
}
contentItem: Item
{
id: popup
Column
{
id: openProviderColumn
//The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size.
onChildrenRectChanged:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
onPositioningComplete:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
Repeater
{
model: prepareMenu.fileProviderModel
delegate: Button
{
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("default_margin").width
width: contentItem.width + leftPadding + rightPadding
height: UM.Theme.getSize("action_button").height
hoverEnabled: true
contentItem: Label
{
text: model.displayText
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
width: contentWidth
height: parent.height
}
onClicked:
{
if(model.index == 0) //The 0th element is the "From Disk" option, which should activate the open local file dialog.
{
Cura.Actions.open.trigger();
}
else
{
prepareMenu.fileProviderModel.trigger(model.name);
}
}
background: Rectangle
{
color: parent.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent"
radius: UM.Theme.getSize("action_button_radius").width
width: popup.width
}
}
}
}
}
}
//If there is just a single item, show a button instead that directly chooses the one option.
Button Button
{ {
id: openFileButton id: openFileButton
height: UM.Theme.getSize("stage_menu").height visible: prepareMenu.fileProviderModel.count <= 1
width: UM.Theme.getSize("stage_menu").height
height: parent.height
width: visible ? height : 0 //Square button (and don't take up space if invisible).
onClicked: Cura.Actions.open.trigger() onClicked: Cura.Actions.open.trigger()
enabled: visible && prepareMenu.fileProviderModel.count > 0
hoverEnabled: true hoverEnabled: true
contentItem: Item contentItem: Item
{ {
anchors.fill: parent
UM.RecolorImage UM.RecolorImage
{ {
id: buttonIcon id: buttonIcon
anchors.centerIn: parent
source: UM.Theme.getIcon("Folder", "medium") source: UM.Theme.getIcon("Folder", "medium")
anchors.centerIn: parent
width: UM.Theme.getSize("button_icon").width width: UM.Theme.getSize("button_icon").width
height: UM.Theme.getSize("button_icon").height height: UM.Theme.getSize("button_icon").height
color: UM.Theme.getColor("icon") color: UM.Theme.getColor("icon")
@ -101,8 +197,8 @@ Item
background: Rectangle background: Rectangle
{ {
id: background id: background
height: UM.Theme.getSize("stage_menu").height height: parent.height
width: UM.Theme.getSize("stage_menu").height width: parent.width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width

View File

@ -203,16 +203,16 @@ Cura.ExpandableComponent
style: UM.Theme.styles.checkbox style: UM.Theme.styles.checkbox
Rectangle
UM.RecolorImage
{ {
id: swatch id: swatch
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height height: UM.Theme.getSize("layerview_legend_size").height
source: UM.Theme.getIcon("Extruder", "medium")
color: model.color color: model.color
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
} }
Label Label

View File

@ -256,7 +256,16 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
""" """
self._uploaded_print_job = self._pre_upload_print_job self._uploaded_print_job = self._pre_upload_print_job
self._progress.hide() self._progress.hide()
PrintJobUploadSuccessMessage().show() message = PrintJobUploadSuccessMessage()
message.addAction("monitor print",
name=I18N_CATALOG.i18nc("@action:button", "Monitor print"),
icon="",
description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"),
button_align=message.ActionButtonAlignment.ALIGN_RIGHT)
df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=monitor-button"
message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide()))
message.show()
self.writeFinished.emit() self.writeFinished.emit()
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"):

View File

@ -13,6 +13,5 @@ class PrintJobUploadSuccessMessage(Message):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
title = I18N_CATALOG.i18nc("@info:title", "Data Sent"), title = I18N_CATALOG.i18nc("@info:title", "Data Sent")
lifetime = 5
) )

View File

@ -115,6 +115,7 @@
"skirt_gap": { "value": 5.0 }, "skirt_gap": { "value": 5.0 },
"skirt_line_count": { "value": 4 }, "skirt_line_count": { "value": 4 },
"meshfix_maximum_deviation": { "value": 0.05 },
"support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" }, "support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" },
"support_pattern": { "value": "'zigzag'" }, "support_pattern": { "value": "'zigzag'" },

View File

@ -5,6 +5,7 @@
"metadata": { "metadata": {
"visible": true, "visible": true,
"platform": "kossel_pro_build_platform.3mf", "platform": "kossel_pro_build_platform.3mf",
"platform_offset": [0, -0.25, 0],
"machine_extruder_trains": { "machine_extruder_trains": {
"0": "anycubic_kossel_extruder_0" "0": "anycubic_kossel_extruder_0"
} }

View File

@ -0,0 +1,35 @@
{
"name": "Atom 2",
"version": 2,
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Victor (Yu Chieh) Lin",
"manufacturer": "Layer One",
"file_formats": "text/x-gcode",
"platform_offset": [0,0,0],
"machine_extruder_trains": { "0": "atom2_extruder_0"
}
},
"overrides": {
"machine_name": { "default_value": "Atom 2" },
"machine_shape": { "default_value": "elliptic" },
"machine_width": { "default_value": 210 },
"machine_depth": { "default_value": 210 },
"machine_height": { "default_value": 320 },
"machine_extruder_count": { "default_value": 1 },
"machine_heated_bed": { "default_value": false },
"machine_center_is_zero": { "default_value": true },
"machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG92 E0\nG1 F200 E3\nG92 E0" },
"machine_end_gcode": { "default_value": "M104 S0\nG28\nG91\nG1 E-6 F300\nM84\nG90" },
"layer_height": { "default_value": 0.2 },
"default_material_print_temperature": { "default_value": 210 },
"speed_print": { "default_value": 32 },
"optimize_wall_printing_order": { "value": "True" },
"infill_sparse_density": { "default_value": 10 },
"brim_width": { "default_value": 4 }
}
}

View File

@ -85,8 +85,8 @@
"material_flow": { "value": 100 }, "material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" }, "travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'sharpest_corner'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_inner'" },
"infill_line_width": { "value": "line_width * 1.2" }, "infill_line_width": { "value": "line_width * 1.2" },
"infill_sparse_density": { "value": "20" }, "infill_sparse_density": { "value": "20" },
@ -156,7 +156,7 @@
"support_interface_enable": { "value": true }, "support_interface_enable": { "value": true },
"support_interface_height": { "value": "layer_height * 4" }, "support_interface_height": { "value": "layer_height * 4" },
"support_interface_density": { "value": 33.333 }, "support_interface_density": { "value": 75 },
"support_interface_pattern": { "value": "'grid'" }, "support_interface_pattern": { "value": "'grid'" },
"support_interface_skip_height": { "value": 0.2 }, "support_interface_skip_height": { "value": 0.2 },
"minimum_support_area": { "value": 2 }, "minimum_support_area": { "value": 2 },

View File

@ -0,0 +1,51 @@
{
"name": "Biqu BX",
"version": 2,
"inherits": "biqu_base",
"metadata": {
"quality_definition": "biqu_base",
"visible": true,
"has_machine_materials": true,
"platform": "BIQU_BX_PLATE.stl",
"platform_offset": [
7,
-7.4,
-3
]
},
"overrides": {
"coasting_enable": { "value": false },
"retraction_amount": { "value": 1 },
"retraction_speed": { "value": 40 },
"retraction_extrusion_window": { "value": 1 },
"retract_at_layer_change": { "value": true },
"support_enable": { "value": false },
"support_structure": { "value": "'normal'" },
"support_type": { "value": "'buildplate'" },
"support_angle": { "value": 45 },
"support_infill_rate": { "value": 15 },
"infill_overlap": { "value": 15.0 },
"skin_overlap": { "value": 20.0 },
"fill_outline_gaps": { "value": true },
"filter_out_tiny_gaps": { "value": true },
"roofing_layer_count": { "value": 2 },
"xy_offset_layer_0": { "value": -0.1 },
"speed_print": { "value": 50 },
"machine_name": { "default_value": "Biqu BX" },
"machine_width": { "value": 250 },
"machine_depth": { "value": 250 },
"machine_height": { "value": 250 },
"machine_head_with_fans_polygon": { "default_value": [
[-33, 35],
[-33, -23],
[33, -23],
[33, 35]
]
},
"machine_start_gcode": {
"default_value": "; BIQU BX Start G-code\r\n; For inforation on how to tune this profile and get the\r\n; most out of your BX visit: https:\/\/github.com\/looxonline\/Marlin\r\n; For the official github site visit: https:\/\/github.com\/bigtreetech\/BIQU-BX\r\n\r\nM117 Initial homing sequence. ; Home so that the probe is positioned to heat\r\nG28\r\n\r\nM117 Probe heating position\r\nG0 X65 Y5 Z1 ; Move the probe to the heating position.\r\n\r\nM117 Getting the heaters up to temp!\r\nM104 S140 ; Set Extruder temperature, no wait\r\nM140 S60 ; Set Heat Bed temperature\r\nM190 S60 ; Wait for Heat Bed temperature\r\n\r\nM117 Waiting for probe to warm! ; Wait another 90s for the probe to absorb heat.\r\nG4 S90 \r\n\r\nM117 Post warming re-home\r\nG28 ; Home all axes again after warming\r\n\r\nM117 Z-Dance of my people\r\nG34\r\n\r\nM117 ABL Probing\r\nG29\r\n\r\nM900 K0 L0 T0 ;Edit the K and L values if you have calibrated a k factor for your filament\r\nM900 T0 S0\r\n\r\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\r\nG1 X4.1 Y10 Z0.3 F5000.0 ; Move to start position\r\n\r\nM117 Getting the extruder up to temp\r\nM140 S{material_bed_temperature_layer_0} ; Set Heat Bed temperature\r\nM104 S{material_print_temperature_layer_0} ; Set Extruder temperature\r\nM109 S{material_print_temperature_layer_0} ; Wait for Extruder temperature\r\nM190 S{material_bed_temperature_layer_0} ; Wait for Heat Bed temperature\r\n\r\nG92 E0 ; Reset Extruder\r\nM117 Purging\r\nG1 X4.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\r\nG1 X4.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\r\nG1 X4.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\r\nG92 E0 ; Reset Extruder\r\nM117 Lets make\r\nG1 X8 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish"
},
"gantry_height": { "value": 27.5 }
}
}

View File

@ -0,0 +1,76 @@
{
"version": 2,
"name": "FlSun SuperRacer",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Thushan Fernando",
"manufacturer": "Flsun",
"platform": "flsun_sr.3mf",
"file_formats": "text/x-gcode",
"has_materials": true,
"has_machine_quality": false,
"preferred_quality_type": "fast",
"machine_extruder_trains": {
"0": "flsun_sr_extruder_0"
}
},
"overrides": {
"machine_extruder_count": {
"default_value": 1
},
"retraction_enable": {
"default_value": true
},
"machine_heated_bed": {
"default_value": true
},
"machine_width": {
"default_value": 260
},
"machine_depth": {
"default_value": 260
},
"machine_height": {
"default_value": 330
},
"machine_center_is_zero": {
"default_value": true
},
"machine_head_with_fans_polygon": {
"default_value": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
]
},
"z_seam_type": {
"value": "'back'"
},
"gantry_height": {
"value": "0"
},
"machine_shape": {
"default_value": "elliptic"
},
"machine_nozzle_size": {
"default_value": 0.4
},
"material_diameter": {
"default_value": 1.75
},
"infill_sparse_density": {
"default_value": 15
},
"machine_start_gcode": {
"default_value": "G21\nG90\nM82\nM107 T0\nM140 S{material_bed_temperature}\nM104 S{material_print_temperature} T0\nM190 S{material_bed_temperature}\nM109 S{material_print_temperature} T0\nG28\nG92 E0\n"
},
"machine_end_gcode": {
"default_value": "M107 T0\nM104 S0\nM104 S0 T1\nM140 S0\nG92 E0\nG91\nG1 E-1 F300 \nG1 Z+0.5 E-5 F6000\nG28 X0 Y0\nG90 ;absolute positioning\n"
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
}
}
}

View File

@ -4,7 +4,7 @@
"inherits": "fdmprinter", "inherits": "fdmprinter",
"metadata": { "metadata": {
"author": "William & Cataldo URSO", "author": "William & Cataldo URSO",
"manufacturer": "Shenzhen Geeetech Technology", "manufacturer": "Geeetech",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"visible": true, "visible": true,
"has_materials": true, "has_materials": true,

View File

@ -3,7 +3,7 @@
"name": "MP Mini Delta", "name": "MP Mini Delta",
"inherits": "fdmprinter", "inherits": "fdmprinter",
"metadata": { "metadata": {
"author": "MPMD Facebook Group", "author": "MPMD V1 Facebook Group",
"manufacturer": "Monoprice", "manufacturer": "Monoprice",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "mp_mini_delta_platform.3mf", "platform": "mp_mini_delta_platform.3mf",
@ -25,7 +25,7 @@
"overrides": { "overrides": {
"machine_start_gcode": "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 " "default_value": ";MPMD V1 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": "machine_end_gcode":
{ {
@ -47,9 +47,9 @@
"default_value": 0.21 "default_value": 0.21
}, },
"material_bed_temperature": { "value": 40 }, "material_bed_temperature": { "value": 40 },
"line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" }, "line_width": { "value": "round(machine_nozzle_size, 2)" },
"material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, "material_print_temperature_layer_0": { "value": "material_print_temperature" },
"material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" }, "material_bed_temperature_layer_0": { "value": "material_bed_temperature" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_max_feedrate_x": { "default_value": 150 }, "machine_max_feedrate_x": { "default_value": 150 },
"machine_max_feedrate_y": { "default_value": 150 }, "machine_max_feedrate_y": { "default_value": 150 },

View File

@ -0,0 +1,51 @@
{
"version": 2,
"name": "MP Mini Delta V2",
"inherits": "fdmprinter",
"metadata": {
"author": "mpminidelta subreddit",
"manufacturer": "Monoprice",
"file_formats": "text/x-gcode",
"platform": "mp_mini_delta_platform.3mf",
"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_v2_extruder_0"
}
},
"overrides": {
"machine_start_gcode":
{
"default_value": ";(**** start.gcode for MP Mini Delta V2****)\nG21\nG90\nM82\nM107\nM104 S170\nG28 X0 Y0\nG28 Z0\nG29 Z0.4\nG1 Z15 F300\nM109 S{material_print_temperature_layer_0}\nG92 E0\nG1 F200 E3\nG92 E0\nG1 F2000\n"
},
"machine_end_gcode": {
"default_value": ";(**** end.gcode for MP Mini Delta V2****)\nG28;(Stick out the part)\nM190 S0;(Turn off heat bed, don't wait.)\nG92 E10;(Set extruder to 10)\nG1 E7 F200;(retract 3mm)\nM104 S0;(Turn off nozzle, don't wait)\nG4 S300;(Delay 5 minutes)\nM107;(Turn off part fan)\nM84;(Turn off stepper motors.)"
},
"material_print_temp_prepend":{"default_value":false},
"material_bed_temperature": { "value": 40 },
"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
},
"line_width": { "value": "round(machine_nozzle_size, 2)" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"retraction_amount": { "default_value": 5 },
"retraction_speed": { "default_value": 28 },
"retraction_hop_enabled": { "default_value": false },
"retract_at_layer_change": { "default_value": true }
}
}

View File

@ -22,7 +22,8 @@
"0": "ultimaker2_plus_connect_extruder_0" "0": "ultimaker2_plus_connect_extruder_0"
}, },
"supports_usb_connection": false, "supports_usb_connection": false,
"supports_network_connection": true "supports_network_connection": true,
"supports_material_export": true
}, },
"overrides": { "overrides": {

View File

@ -27,6 +27,7 @@
"first_start_actions": [ "DiscoverUM3Action" ], "first_start_actions": [ "DiscoverUM3Action" ],
"supported_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false, "supports_usb_connection": false,
"supports_material_export": true,
"weight": -1, "weight": -1,
"firmware_update_info": { "firmware_update_info": {
"id": 213482, "id": 213482,

View File

@ -28,6 +28,7 @@
"supported_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false, "supports_usb_connection": false,
"supports_network_connection": true, "supports_network_connection": true,
"supports_material_export": true,
"weight": -2, "weight": -2,
"firmware_update_info": { "firmware_update_info": {
"id": 9051, "id": 9051,

View File

@ -0,0 +1,15 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "atom2",
"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,20 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "flsun_sr",
"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 0",
"inherits": "fdmextruder",
"metadata": {
"machine": "mp_mini_delta_v2",
"position": "0"
},
"overrides": {
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View File

@ -1177,7 +1177,7 @@ msgstr "Hinten links"
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "z_seam_position option back" msgctxt "z_seam_position option back"
msgid "Back" msgid "Back"
msgstr "Zurück" msgstr "Hinten"
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "z_seam_position option backright" msgctxt "z_seam_position option backright"

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,7 @@ Button
property alias textFont: buttonText.font property alias textFont: buttonText.font
property alias cornerRadius: backgroundRect.radius property alias cornerRadius: backgroundRect.radius
property alias tooltip: tooltip.tooltipText property alias tooltip: tooltip.tooltipText
property alias tooltipWidth: tooltip.width
property color color: UM.Theme.getColor("primary") property color color: UM.Theme.getColor("primary")
property color hoverColor: UM.Theme.getColor("primary_hover") property color hoverColor: UM.Theme.getColor("primary_hover")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
pragma Singleton pragma Singleton
@ -122,7 +122,15 @@ Item
Action Action
{ {
id: quitAction id: quitAction
text: catalog.i18nc("@action:inmenu menubar:file","&Quit")
//On MacOS, don't translate the "Quit" word.
//Qt moves the "quit" entry to a different place, and if it got renamed can't find it again when it attempts to
//delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "&Quit" : catalog.i18nc("@action:inmenu menubar:file", "&Quit")
iconName: "application-exit" iconName: "application-exit"
shortcut: StandardKey.Quit shortcut: StandardKey.Quit
} }
@ -172,7 +180,14 @@ Item
Action Action
{ {
id: preferencesAction id: preferencesAction
text: catalog.i18nc("@action:inmenu", "Configure Cura...") //On MacOS, don't translate the "Configure" word.
//Qt moves the "configure" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "Configure Cura..." : catalog.i18nc("@action:inmenu", "Configure Cura...")
iconName: "configure" iconName: "configure"
} }
@ -263,7 +278,15 @@ Item
Action Action
{ {
id: aboutAction; id: aboutAction;
text: catalog.i18nc("@action:inmenu menubar:help", "About...");
//On MacOS, don't translate the "About" word.
//Qt moves the "about" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "About..." : catalog.i18nc("@action:inmenu menubar:help", "About...");
iconName: "help-about"; iconName: "help-about";
} }

View File

@ -417,6 +417,7 @@ UM.MainWindow
Cura.PrimaryButton Cura.PrimaryButton
{ {
text: model.name text: model.name
iconSource: UM.Theme.getIcon(model.icon)
height: UM.Theme.getSize("message_action_button").height height: UM.Theme.getSize("message_action_button").height
} }
} }
@ -426,6 +427,7 @@ UM.MainWindow
Cura.SecondaryButton Cura.SecondaryButton
{ {
text: model.name text: model.name
iconSource: UM.Theme.getIcon(model.icon)
height: UM.Theme.getSize("message_action_button").height height: UM.Theme.getSize("message_action_button").height
} }
} }
@ -434,6 +436,14 @@ UM.MainWindow
Cura.TertiaryButton Cura.TertiaryButton
{ {
text: model.name text: model.name
iconSource:
{
if (model.icon == null || model.icon == "")
{
return UM.Theme.getIcon("LinkExternal")
}
return UM.Theme.getIcon(model.icon)
}
height: UM.Theme.getSize("message_action_button").height height: UM.Theme.getSize("message_action_button").height
} }
} }

View File

@ -167,7 +167,7 @@ Item
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
margins: background.padding margins: background.padding
} }
source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft") source: UM.Theme.getIcon("ChevronSingleDown")
visible: source != "" visible: source != ""
width: UM.Theme.getSize("standard_arrow").width width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height height: UM.Theme.getSize("standard_arrow").height

View File

@ -180,7 +180,7 @@ Item
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
margins: background.padding margins: background.padding
} }
source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft") source: UM.Theme.getIcon("ChevronSingleDown")
visible: source != "" visible: source != ""
width: UM.Theme.getSize("standard_arrow").width width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height height: UM.Theme.getSize("standard_arrow").height

View File

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
@ -48,7 +48,17 @@ Item
ViewMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&View") } ViewMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&View") }
SettingsMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&Settings") } SettingsMenu
{
//On MacOS, don't translate the "Settings" word.
//Qt moves the "settings" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
title: (Qt.platform.os == "osx") ? "&Settings" : catalog.i18nc("@title:menu menubar:toplevel", "&Settings")
}
Menu Menu
{ {
@ -91,7 +101,15 @@ Item
Menu Menu
{ {
id: preferencesMenu id: preferencesMenu
title: catalog.i18nc("@title:menu menubar:toplevel", "P&references")
//On MacOS, don't translate the "Preferences" word.
//Qt moves the "preferences" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
title: (Qt.platform.os == "osx") ? "&Preferences" : catalog.i18nc("@title:menu menubar:toplevel", "P&references")
MenuItem { action: Cura.Actions.preferences } MenuItem { action: Cura.Actions.preferences }
} }

View File

@ -1,5 +1,5 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
@ -191,6 +191,21 @@ Item
} }
enabled: base.hasCurrentItem enabled: base.hasCurrentItem
} }
//Sync button.
Button
{
id: syncMaterialsButton
text: catalog.i18nc("@action:button Sending materials to printers", "Sync with Printers")
iconName: "sync-synchronizing"
onClicked:
{
forceActiveFocus();
exportAllMaterialsDialog.folder = base.materialManagementModel.getPreferredExportAllPath();
exportAllMaterialsDialog.open();
}
visible: Cura.MachineManager.activeMachine.supportsMaterialExport
}
} }
Item { Item {
@ -368,6 +383,19 @@ Item
} }
} }
FileDialog
{
id: exportAllMaterialsDialog
title: catalog.i18nc("@title:window", "Export All Materials")
selectExisting: false
nameFilters: ["Material archives (*.umm)", "All files (*)"]
onAccepted:
{
base.materialManagementModel.exportAll(fileUrl);
CuraApplication.setDefaultPath("dialog_material_path", folder);
}
}
MessageDialog MessageDialog
{ {
id: messageDialog id: messageDialog

View File

@ -95,7 +95,7 @@ UM.PreferencesPage
placeholderText: catalog.i18nc("@label:textbox", "Filter...") placeholderText: catalog.i18nc("@label:textbox", "Filter...")
onTextChanged: definitionsModel.filter = {"i18n_label": "*" + text} onTextChanged: definitionsModel.filter = {"i18n_label|i18n_description": "*" + text}
} }
NewControls.ComboBox NewControls.ComboBox

View File

@ -54,7 +54,7 @@ Item
{ {
id: networkPrinterScrollView id: networkPrinterScrollView
maxItemCountAtOnce: 10 // show at max 10 items at once, otherwise you need to scroll. maxItemCountAtOnce: 9 // show at max 9 items at once, otherwise you need to scroll.
onRefreshButtonClicked: onRefreshButtonClicked:
{ {

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
@ -214,16 +214,16 @@ Item
id: troubleshootingButton id: troubleshootingButton
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("thin_margin").width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
height: troubleshootingLinkIcon.height height: troubleshootingLinkIcon.height
width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("default_margin").width width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("thin_margin").width
UM.RecolorImage UM.RecolorImage
{ {
id: troubleshootingLinkIcon id: troubleshootingLinkIcon
anchors.right: troubleshootingLabel.left anchors.right: troubleshootingLabel.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("thin_margin").width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
height: troubleshootingLabel.height height: troubleshootingLabel.height
width: height width: height

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
@ -15,6 +15,8 @@ TextField
{ {
id: textField id: textField
property alias leftIcon: iconLeft.source
UM.I18nCatalog { id: catalog; name: "cura" } UM.I18nCatalog { id: catalog; name: "cura" }
hoverEnabled: true hoverEnabled: true
@ -22,6 +24,7 @@ TextField
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
renderType: Text.NativeRendering renderType: Text.NativeRendering
leftPadding: iconLeft.visible ? iconLeft.width + UM.Theme.getSize("default_margin").width * 2 : UM.Theme.getSize("thin_margin").width
states: [ states: [
State State
@ -52,7 +55,6 @@ TextField
color: UM.Theme.getColor("main_background") color: UM.Theme.getColor("main_background")
anchors.margins: Math.round(UM.Theme.getSize("default_lining").width)
radius: UM.Theme.getSize("setting_control_radius").width radius: UM.Theme.getSize("setting_control_radius").width
border.color: border.color:
@ -67,5 +69,23 @@ TextField
} }
return UM.Theme.getColor("setting_control_border") return UM.Theme.getColor("setting_control_border")
} }
//Optional icon added on the left hand side.
UM.RecolorImage
{
id: iconLeft
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
visible: source != ""
height: UM.Theme.getSize("small_button_icon").height
width: visible ? height : 0
color: textField.color
}
} }
} }

View File

@ -16,7 +16,7 @@
"primary_text": [255, 255, 255, 204], "primary_text": [255, 255, 255, 204],
"secondary": [95, 95, 95, 255], "secondary": [95, 95, 95, 255],
"secondary_button": [0, 0, 0, 0], "secondary_button": [39, 44, 48, 255],
"secondary_button_hover": [85, 85, 87, 255], "secondary_button_hover": [85, 85, 87, 255],
"secondary_button_text": [255, 255, 255, 255], "secondary_button_text": [255, 255, 255, 255],

View File

@ -6,8 +6,8 @@
.st0{fill:#231F20;} .st0{fill:#231F20;}
</style> </style>
<g id="Layer_1_1_"> <g id="Layer_1_1_">
<path class="st0" d="M15,26C8.9,26,4,21.1,4,15S8.9,4,15,4s11,4.9,11,11S21.1,26,15,26z M15,7c-4.4,0-8,3.6-8,8s3.6,8,8,8 <path class="st0" d="M15,26C8.9,26,4,21.1,4,15S8.9,4,15,4s11,4.9,11,11S21.1,26,15,26z M15,8c-3.8,0-7,3.2-7,7s3.2,7,7,7
s8-3.6,8-8S19.4,7,15,7z"/> s7-3.1,7-7S18.9,8,15,8z"/>
</g> </g>
<g id="Comments"> <g id="Comments">
</g> </g>

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

View File

@ -193,7 +193,7 @@
"primary_button_hover": [16, 70, 156, 255], "primary_button_hover": [16, 70, 156, 255],
"primary_button_text": [255, 255, 255, 255], "primary_button_text": [255, 255, 255, 255],
"secondary_button": [255, 255, 255, 0], "secondary_button": [255, 255, 255, 255],
"secondary_button_shadow": [216, 216, 216, 255], "secondary_button_shadow": [216, 216, 216, 255],
"secondary_button_hover": [232, 240, 253, 255], "secondary_button_hover": [232, 240, 253, 255],
"secondary_button_text": [25, 110, 240, 255], "secondary_button_text": [25, 110, 240, 255],

View File

@ -0,0 +1,12 @@
[general]
name = 0.2mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.2

View File

@ -0,0 +1,12 @@
[general]
name = 0.3mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.3

View File

@ -0,0 +1,12 @@
[general]
name = 0.4mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.4

View File

@ -0,0 +1,12 @@
[general]
name = 0.5mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.5

View File

@ -0,0 +1,12 @@
[general]
name = 0.6mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.6

View File

@ -0,0 +1,12 @@
[general]
name = 0.8mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.8