Merge branch 'master' into feature_amf_reader

This commit is contained in:
Aldo Hoeben 2019-04-26 11:51:55 +02:00 committed by GitHub
commit 066b90bdea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
336 changed files with 57051 additions and 9240 deletions

View file

@ -259,7 +259,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
quality_name = ""
custom_quality_name = ""
num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes
num_settings_overridden_by_quality_changes = 0 # How many settings are changed by the quality changes
num_user_settings = 0
quality_changes_conflict = False
@ -297,7 +297,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
custom_quality_name = parser["general"]["name"]
values = parser["values"] if parser.has_section("values") else dict()
num_settings_overriden_by_quality_changes += len(values)
num_settings_overridden_by_quality_changes += len(values)
# Check if quality changes already exists.
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
type = "quality_changes")
@ -515,7 +515,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setNumVisibleSettings(num_visible_settings)
self._dialog.setQualityName(quality_name)
self._dialog.setQualityType(quality_type)
self._dialog.setNumSettingsOverridenByQualityChanges(num_settings_overriden_by_quality_changes)
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
self._dialog.setNumUserSettings(num_user_settings)
self._dialog.setActiveMode(active_mode)
self._dialog.setMachineName(machine_name)
@ -820,6 +820,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
global_stack, extruder_stack)
container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container)
for key, value in container_info.parser["values"].items():
container_info.container.setProperty(key, "value", value)

View file

@ -41,7 +41,7 @@ class WorkspaceDialog(QObject):
self._num_user_settings = 0
self._active_mode = ""
self._quality_name = ""
self._num_settings_overriden_by_quality_changes = 0
self._num_settings_overridden_by_quality_changes = 0
self._quality_type = ""
self._machine_name = ""
self._machine_type = ""
@ -151,10 +151,10 @@ class WorkspaceDialog(QObject):
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
def numSettingsOverridenByQualityChanges(self):
return self._num_settings_overriden_by_quality_changes
return self._num_settings_overridden_by_quality_changes
def setNumSettingsOverridenByQualityChanges(self, num_settings_overriden_by_quality_changes):
self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes
def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes):
self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes
self.numSettingsOverridenByQualityChangesChanged.emit()
@pyqtProperty(str, notify=qualityNameChanged)

View file

@ -1,109 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from UM.Extension import Extension
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Version import Version
from PyQt5.QtCore import pyqtSlot, QObject
import os.path
import collections
catalog = i18nCatalog("cura")
class ChangeLog(Extension, QObject,):
def __init__(self, parent = None):
QObject.__init__(self, parent)
Extension.__init__(self)
self._changelog_window = None
self._changelog_context = None
version_string = Application.getInstance().getVersion()
if version_string is not "master":
self._current_app_version = Version(version_string)
else:
self._current_app_version = None
self._change_logs = None
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog"))
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
def getChangeLogs(self):
if not self._change_logs:
self.loadChangeLogs()
return self._change_logs
@pyqtSlot(result = str)
def getChangeLogString(self):
logs = self.getChangeLogs()
result = ""
for version in logs:
result += "<h1>" + str(version) + "</h1><br>"
result += ""
for change in logs[version]:
if str(change) != "":
result += "<b>" + str(change) + "</b><br>"
for line in logs[version][change]:
result += str(line) + "<br>"
result += "<br>"
pass
return result
def loadChangeLogs(self):
self._change_logs = collections.OrderedDict()
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
open_version = None
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
for line in f:
line = line.replace("\n","")
if "[" in line and "]" in line:
line = line.replace("[","")
line = line.replace("]","")
open_version = Version(line)
open_header = ""
self._change_logs[open_version] = collections.OrderedDict()
elif line.startswith("*"):
open_header = line.replace("*","")
self._change_logs[open_version][open_header] = []
elif line != "":
if open_header not in self._change_logs[open_version]:
self._change_logs[open_version][open_header] = []
self._change_logs[open_version][open_header].append(line)
def _onEngineCreated(self):
if not self._current_app_version:
return #We're on dev branch.
if Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown") == "master":
latest_version_shown = Version("0.0.0")
else:
latest_version_shown = Version(Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown"))
Application.getInstance().getPreferences().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
# Do not show the changelog when there is no global container stack
# This implies we are running Cura for the first time.
if not Application.getInstance().getGlobalContainerStack():
return
if self._current_app_version > latest_version_shown:
self.showChangelog()
def showChangelog(self):
if not self._changelog_window:
self.createChangelogWindow()
self._changelog_window.show()
def hideChangelog(self):
if self._changelog_window:
self._changelog_window.hide()
def createChangelogWindow(self):
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})

View file

@ -1,41 +0,0 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.1 as UM
UM.Dialog
{
id: base
minimumWidth: (UM.Theme.getSize("modal_window_minimum").width * 0.75) | 0
minimumHeight: (UM.Theme.getSize("modal_window_minimum").height * 0.75) | 0
width: minimumWidth
height: minimumHeight
title: catalog.i18nc("@label", "Changelog")
TextArea
{
anchors.fill: parent
text: manager.getChangeLogString()
readOnly: true;
textFormat: TextEdit.RichText
}
rightButtons: [
Button
{
UM.I18nCatalog
{
id: catalog
name: "cura"
}
text: catalog.i18nc("@action:button", "Close")
onClicked: base.hide()
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import ChangeLog
def getMetaData():
return {}
def register(app):
return {"extension": ChangeLog.ChangeLog()}

View file

@ -1,8 +0,0 @@
{
"name": "Changelog",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Shows changes since latest checked version.",
"api": "6.0",
"i18n-catalog": "cura"
}

View file

@ -45,7 +45,7 @@ class DriveApiService:
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.log("w", "Unable to connect with the server.")
Logger.logException("w", "Unable to connect with the server.")
return []
# HTTP status 300s mean redirection. 400s and 500s are errors.
@ -98,7 +98,12 @@ class DriveApiService:
# If there is no download URL, we can't restore the backup.
return self._emitRestoreError()
download_package = requests.get(download_url, stream = True)
try:
download_package = requests.get(download_url, stream = True)
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return self._emitRestoreError()
if download_package.status_code >= 300:
# Something went wrong when attempting to download the backup.
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
@ -142,9 +147,14 @@ class DriveApiService:
Logger.log("w", "Could not get access token.")
return False
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
try:
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return False
if delete_backup.status_code >= 300:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
@ -159,15 +169,19 @@ class DriveApiService:
if not access_token:
Logger.log("w", "Could not get access token.")
return None
backup_upload_request = requests.put(self.BACKUP_URL, json = {
"data": {
"backup_size": backup_size,
"metadata": backup_metadata
}
}, headers = {
"Authorization": "Bearer {}".format(access_token)
})
try:
backup_upload_request = requests.put(
self.BACKUP_URL,
json = {"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
},
headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return None
# Any status code of 300 or above indicates an error.
if backup_upload_request.status_code >= 300:

View file

@ -10,20 +10,17 @@ from time import time
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
from UM.Backend.Backend import Backend, BackendState
from UM.Scene.Camera import Camera
from UM.Scene.SceneNode import SceneNode
from UM.Signal import Signal
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Qt.Duration import DurationFormat
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.SettingInstance import SettingInstance #For typing.
from UM.Tool import Tool #For typing.
from UM.Mesh.MeshData import MeshData #For typing.
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager

View file

@ -24,7 +24,7 @@ from cura import LayerPolygon
import numpy
from time import time
from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
catalog = i18nCatalog("cura")

View file

@ -196,10 +196,7 @@ class StartSliceJob(Job):
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
# Find a reason not to add the node
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:

View file

@ -1,31 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import math
import re
from typing import Dict, List, NamedTuple, Optional, Union
import numpy
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.Vector import Vector
from UM.Message import Message
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.CuraApplication import CuraApplication
from cura.LayerDataBuilder import LayerDataBuilder
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from typing import Dict, List, NamedTuple, Optional, Union
catalog = i18nCatalog("cura")
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
## This parser is intended to interpret the common firmware codes among all the
# different flavors
class FlavorParser:
@ -33,7 +35,7 @@ class FlavorParser:
def __init__(self) -> None:
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._message = None # type: Optional[Message]
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
@ -425,7 +427,8 @@ class FlavorParser:
if line.startswith("M"):
M = self._getInt(line, "M")
self.processMCode(M, line, current_position, current_path)
if M is not None:
self.processMCode(M, line, current_position, current_path)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
@ -463,7 +466,7 @@ class FlavorParser:
Logger.log("w", "File doesn't contain any valid layers")
settings = CuraApplication.getInstance().getGlobalContainerStack()
if not settings.getProperty("machine_center_is_zero", "value"):
if settings is not None and not settings.getProperty("machine_center_is_zero", "value"):
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))

View file

@ -12,9 +12,6 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
_flavor_default = "Marlin"

View file

@ -123,7 +123,7 @@ UM.Dialog
UM.TooltipArea {
Layout.fillWidth:true
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","By default, white pixels represent high points on the mesh and black pixels represent low points on the mesh. Change this option to reverse the behavior such that black pixels represent high points on the mesh and white pixels represent low points on the mesh.")
text: catalog.i18nc("@info:tooltip","For lithophanes dark pixels should correspond to thicker locations in order to block more light coming through. For height maps lighter pixels signify higher terrain, so lighter pixels should correspond to thicker locations in the generated 3D model.")
Row {
width: parent.width
@ -134,9 +134,9 @@ UM.Dialog
anchors.verticalCenter: parent.verticalCenter
}
ComboBox {
id: image_color_invert
objectName: "Image_Color_Invert"
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ]
id: lighter_is_higher
objectName: "Lighter_Is_Higher"
model: [ catalog.i18nc("@item:inlistbox","Darker is higher"), catalog.i18nc("@item:inlistbox","Lighter is higher") ]
width: 180 * screenScaleFactor
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
}

View file

@ -46,9 +46,9 @@ class ImageReader(MeshReader):
def _read(self, file_name):
size = max(self._ui.getWidth(), self._ui.getDepth())
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert)
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher)
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert):
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher):
scene_node = SceneNode()
mesh = MeshBuilder()
@ -104,7 +104,7 @@ class ImageReader(MeshReader):
Job.yieldThread()
if image_color_invert:
if not lighter_is_higher:
height_data = 1 - height_data
for _ in range(0, blur_iterations):

View file

@ -30,10 +30,10 @@ class ImageReaderUI(QObject):
self._width = self.default_width
self._depth = self.default_depth
self.base_height = 1
self.peak_height = 10
self.base_height = 0.4
self.peak_height = 2.5
self.smoothing = 1
self.image_color_invert = False;
self.lighter_is_higher = False;
self._ui_lock = threading.Lock()
self._cancelled = False
@ -143,4 +143,4 @@ class ImageReaderUI(QObject):
@pyqtSlot(int)
def onImageColorInvertChanged(self, value):
self.image_color_invert = (value == 1)
self.lighter_is_higher = (value == 1)

View file

@ -1,16 +1,21 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty
import UM.i18n
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.cura_empty_instance_containers import isEmptyContainer
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
catalog = UM.i18n.i18nCatalog("cura")
@ -18,139 +23,102 @@ catalog = UM.i18n.i18nCatalog("cura")
## This action allows for certain settings that are "machine only") to be modified.
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
class MachineSettingsAction(MachineAction):
def __init__(self, parent = None):
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
self._application = Application.getInstance()
self._global_container_stack = None
from cura.CuraApplication import CuraApplication
self._application = CuraApplication.getInstance()
from cura.Settings.CuraContainerStack import _ContainerIndexes
self._container_index = _ContainerIndexes.DefinitionChanges
self._store_container_index = _ContainerIndexes.DefinitionChanges
self._container_registry = ContainerRegistry.getInstance()
self._container_registry.containerAdded.connect(self._onContainerAdded)
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
# The machine settings dialog blocks auto-slicing when it's shown, and re-enables it when it's finished.
self._backend = self._application.getBackend()
self.onFinished.connect(self._onFinished)
self._empty_definition_container_id_list = []
def _isEmptyDefinitionChanges(self, container_id: str):
if not self._empty_definition_container_id_list:
self._empty_definition_container_id_list = [self._application.empty_container.getId(),
self._application.empty_definition_changes_container.getId()]
return container_id in self._empty_definition_container_id_list
# Which container index in a stack to store machine setting changes.
@pyqtProperty(int, constant = True)
def storeContainerIndex(self) -> int:
return self._store_container_index
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onContainerRemoved(self, container):
# Remove definition_changes containers when a stack is removed
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
definition_changes_id = container.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
return
def _reset(self):
if not self._global_container_stack:
global_stack = self._application.getMachineManager().activeMachine
if not global_stack:
return
# Make sure there is a definition_changes container to store the machine settings
definition_changes_id = self._global_container_stack.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
self._global_container_stack.getName() + "_settings")
# Notify the UI in which container to store the machine settings data
from cura.Settings.CuraContainerStack import _ContainerIndexes
container_index = _ContainerIndexes.DefinitionChanges
if container_index != self._container_index:
self._container_index = container_index
self.containerIndexChanged.emit()
definition_changes_id = global_stack.definitionChanges.getId()
if isEmptyContainer(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(global_stack,
global_stack.getName() + "_settings")
# Disable auto-slicing while the MachineAction is showing
if self._backend: # This sometimes triggers before backend is loaded.
self._backend.disableTimer()
@pyqtSlot()
def onFinishAction(self):
# Restore autoslicing when the machineaction is dismissed
def _onFinished(self):
# Restore auto-slicing when the machine action is dismissed
if self._backend and self._backend.determineAutoSlicing():
self._backend.enableTimer()
self._backend.tickle()
containerIndexChanged = pyqtSignal()
@pyqtProperty(int, notify = containerIndexChanged)
def containerIndex(self):
return self._container_index
def _onGlobalContainerChanged(self):
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
# This additional emit is needed because we cannot connect a UM.Signal directly to a pyqtSignal
self.globalContainerChanged.emit()
globalContainerChanged = pyqtSignal()
@pyqtProperty(int, notify = globalContainerChanged)
def definedExtruderCount(self):
if not self._global_container_stack:
return 0
return len(self._global_container_stack.getMetaDataEntry("machine_extruder_trains"))
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count):
def setMachineExtruderCount(self, extruder_count: int) -> None:
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
@pyqtSlot()
def forceUpdate(self):
def forceUpdate(self) -> None:
# Force rebuilding the build volume by reloading the global container stack.
# This is a bit of a hack, but it seems quick enough.
self._application.globalContainerStackChanged.emit()
self._application.getMachineManager().globalContainerChanged.emit()
@pyqtSlot()
def updateHasMaterialsMetadata(self):
def updateHasMaterialsMetadata(self) -> None:
global_stack = self._application.getMachineManager().activeMachine
# Updates the has_materials metadata flag after switching gcode flavor
if not self._global_container_stack:
if not global_stack:
return
definition = self._global_container_stack.getBottom()
definition = global_stack.getDefinition()
if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
extruder_positions = list(self._global_container_stack.extruders.keys())
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
extruder_positions = list(global_stack.extruders.keys())
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_node = None
if has_materials:
self._global_container_stack.setMetaDataEntry("has_materials", True)
global_stack.setMetaDataEntry("has_materials", True)
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials")
if "has_materials" in global_stack.getMetaData():
global_stack.removeMetaDataEntry("has_materials")
# set materials
for position in extruder_positions:
if has_materials:
material_node = material_manager.getDefaultMaterial(self._global_container_stack, position, None)
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
machine_manager.setMaterial(position, material_node)
self._application.globalContainerStackChanged.emit()
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int):
def updateMaterialForDiameter(self, extruder_position: int) -> None:
# Updates the material container to a material that matches the material diameter set for the printer
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.3 as UM
import Cura 1.1 as Cura
//
// This component contains the content for the "Welcome" page of the welcome on-boarding process.
//
Item
{
id: base
UM.I18nCatalog { id: catalog; name: "cura" }
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
property int labelWidth: 210 * screenScaleFactor
property int controlWidth: (UM.Theme.getSize("setting_control").width * 3 / 4) | 0
property var labelFont: UM.Theme.getFont("medium")
property int columnWidth: ((parent.width - 2 * UM.Theme.getSize("default_margin").width) / 2) | 0
property int columnSpacing: 3 * screenScaleFactor
property int propertyStoreIndex: manager.storeContainerIndex // definition_changes
property string extruderStackId: ""
property int extruderPosition: 0
property var forceUpdateFunction: manager.forceUpdate
function updateMaterialDiameter()
{
manager.updateMaterialForDiameter(extruderPosition)
}
Item
{
id: upperBlock
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
height: childrenRect.height
// =======================================
// Left-side column "Nozzle Settings"
// =======================================
Column
{
anchors.top: parent.top
anchors.left: parent.left
width: parent.width * 2 / 3
spacing: base.columnSpacing
Label // Title Label
{
text: catalog.i18nc("@title:label", "Nozzle Settings")
font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
}
Cura.NumericTextFieldWithUnit // "Nozzle size"
{
id: extruderNozzleSizeField
visible: !Cura.MachineManager.hasVariants
containerStackId: base.extruderStackId
settingKey: "machine_nozzle_size"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Nozzle size")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Compatible material diameter"
{
id: extruderCompatibleMaterialDiameterField
containerStackId: base.extruderStackId
settingKey: "material_diameter"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Compatible material diameter")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
// Other modules won't automatically respond after the user changes the value, so we need to force it.
afterOnEditingFinishedFunction: updateMaterialDiameter
}
Cura.NumericTextFieldWithUnit // "Nozzle offset X"
{
id: extruderNozzleOffsetXField
containerStackId: base.extruderStackId
settingKey: "machine_nozzle_offset_x"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Nozzle offset X")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Nozzle offset Y"
{
id: extruderNozzleOffsetYField
containerStackId: base.extruderStackId
settingKey: "machine_nozzle_offset_y"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Nozzle offset Y")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Cooling Fan Number"
{
id: extruderNozzleCoolingFanNumberField
containerStackId: base.extruderStackId
settingKey: "machine_extruder_cooling_fan_number"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Cooling Fan Number")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: ""
forceUpdateOnChangeFunction: forceUpdateFunction
}
}
}
Item // Extruder Start and End G-code
{
id: lowerBlock
anchors.top: upperBlock.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
Cura.GcodeTextArea // "Extruder Start G-code"
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "Extruder Start G-code")
containerStackId: base.extruderStackId
settingKey: "machine_extruder_start_code"
settingStoreIndex: propertyStoreIndex
}
Cura.GcodeTextArea // "Extruder End G-code"
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.right: parent.right
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "Extruder End G-code")
containerStackId: base.extruderStackId
settingKey: "machine_extruder_end_code"
settingStoreIndex: propertyStoreIndex
}
}
}

View file

@ -0,0 +1,341 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.3 as UM
import Cura 1.1 as Cura
//
// This the content in the "Printer" tab in the Machine Settings dialog.
//
Item
{
id: base
UM.I18nCatalog { id: catalog; name: "cura" }
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
property int labelWidth: 120 * screenScaleFactor
property int controlWidth: (UM.Theme.getSize("setting_control").width * 3 / 4) | 0
property var labelFont: UM.Theme.getFont("default")
property int columnWidth: ((parent.width - 2 * UM.Theme.getSize("default_margin").width) / 2) | 0
property int columnSpacing: 3 * screenScaleFactor
property int propertyStoreIndex: manager.storeContainerIndex // definition_changes
property string machineStackId: Cura.MachineManager.activeMachineId
property var forceUpdateFunction: manager.forceUpdate
Item
{
id: upperBlock
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
height: childrenRect.height
// =======================================
// Left-side column for "Printer Settings"
// =======================================
Column
{
anchors.top: parent.top
anchors.left: parent.left
width: base.columnWidth
spacing: base.columnSpacing
Label // Title Label
{
text: catalog.i18nc("@title:label", "Printer Settings")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Cura.NumericTextFieldWithUnit // "X (Width)"
{
id: machineXWidthField
containerStackId: machineStackId
settingKey: "machine_width"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "X (Width)")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Y (Depth)"
{
id: machineYDepthField
containerStackId: machineStackId
settingKey: "machine_depth"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Y (Depth)")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Z (Height)"
{
id: machineZHeightField
containerStackId: machineStackId
settingKey: "machine_height"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Z (Height)")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.ComboBoxWithOptions // "Build plate shape"
{
id: buildPlateShapeComboBox
containerStackId: machineStackId
settingKey: "machine_shape"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Build plate shape")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.SimpleCheckBox // "Origin at center"
{
id: originAtCenterCheckBox
containerStackId: machineStackId
settingKey: "machine_center_is_zero"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Origin at center")
labelFont: base.labelFont
labelWidth: base.labelWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.SimpleCheckBox // "Heated bed"
{
id: heatedBedCheckBox
containerStackId: machineStackId
settingKey: "machine_heated_bed"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Heated bed")
labelFont: base.labelFont
labelWidth: base.labelWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.ComboBoxWithOptions // "G-code flavor"
{
id: gcodeFlavorComboBox
containerStackId: machineStackId
settingKey: "machine_gcode_flavor"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "G-code flavor")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
forceUpdateOnChangeFunction: forceUpdateFunction
// FIXME(Lipu): better document this.
// This has something to do with UM2 and UM2+ regarding "has_material" and the gcode flavor settings.
// I don't remember exactly what.
afterOnEditingFinishedFunction: manager.updateHasMaterialsMetadata
}
}
// =======================================
// Right-side column for "Printhead Settings"
// =======================================
Column
{
anchors.top: parent.top
anchors.right: parent.right
width: base.columnWidth
spacing: base.columnSpacing
Label // Title Label
{
text: catalog.i18nc("@title:label", "Printhead Settings")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Cura.PrintHeadMinMaxTextField // "X min"
{
id: machineXMinField
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "X min")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
axisName: "x"
axisMinOrMax: "min"
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.PrintHeadMinMaxTextField // "Y min"
{
id: machineYMinField
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Y min")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
axisName: "y"
axisMinOrMax: "min"
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.PrintHeadMinMaxTextField // "X max"
{
id: machineXMaxField
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "X max")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
axisName: "x"
axisMinOrMax: "max"
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.PrintHeadMinMaxTextField // "Y max"
{
id: machineYMaxField
containerStackId: machineStackId
settingKey: "machine_head_with_fans_polygon"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Y max")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
axisName: "y"
axisMinOrMax: "max"
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.NumericTextFieldWithUnit // "Gantry Height"
{
id: machineGantryHeightField
containerStackId: machineStackId
settingKey: "gantry_height"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Gantry Height")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm")
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.ComboBoxWithOptions // "Number of Extruders"
{
id: numberOfExtrudersComboBox
containerStackId: machineStackId
settingKey: "machine_extruder_count"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Number of Extruders")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
forceUpdateOnChangeFunction: forceUpdateFunction
// FIXME(Lipu): better document this.
// This has something to do with UM2 and UM2+ regarding "has_material" and the gcode flavor settings.
// I don't remember exactly what.
afterOnEditingFinishedFunction: manager.updateHasMaterialsMetadata
setValueFunction: manager.setMachineExtruderCount
optionModel: ListModel
{
id: extruderCountModel
Component.onCompleted:
{
extruderCountModel.clear()
for (var i = 1; i <= Cura.MachineManager.activeMachine.maxExtruderCount; i++)
{
// Use String as value. JavaScript only has Number. PropertyProvider.setPropertyValue()
// takes a QVariant as value, and Number gets translated into a float. This will cause problem
// for integer settings such as "Number of Extruders".
extruderCountModel.append({ text: String(i), value: String(i) })
}
}
}
}
}
}
Item // Start and End G-code
{
id: lowerBlock
anchors.top: upperBlock.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
Cura.GcodeTextArea // "Start G-code"
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "Start G-code")
containerStackId: machineStackId
settingKey: "machine_start_gcode"
settingStoreIndex: propertyStoreIndex
}
Cura.GcodeTextArea // "End G-code"
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.right: parent.right
width: base.columnWidth - UM.Theme.getSize("default_margin").width
labelText: catalog.i18nc("@title:label", "End G-code")
containerStackId: machineStackId
settingKey: "machine_end_gcode"
settingStoreIndex: propertyStoreIndex
}
}
}

View file

@ -2,8 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from cura.Stages.CuraStage import CuraStage

View file

@ -162,7 +162,7 @@ class PostProcessingPlugin(QObject, Extension):
loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
continue
spec.loader.exec_module(loaded_script)
spec.loader.exec_module(loaded_script) # type: ignore
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
loaded_class = getattr(loaded_script, script_name)

View file

@ -97,7 +97,7 @@ class FilamentChange(Script):
if layer_num <= len(data):
index, layer_data = self._searchLayerData(data, layer_num - 1)
if layer_data is None:
Logger.log("e", "Could not found the layer")
Logger.log("e", "Could not find the layer {layer_num}".format(layer_num = layer_num))
continue
lines = layer_data.split("\n")
lines.insert(2, color_change)

View file

@ -20,11 +20,19 @@ Item
name: "cura"
}
anchors
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
}
// Item to ensure that all of the buttons are nicely centered.
Item
{
anchors.horizontalCenter: parent.horizontalCenter
width: openFileButton.width + itemRow.width + UM.Theme.getSize("default_margin").width
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
RowLayout
@ -32,9 +40,9 @@ Item
id: itemRow
anchors.left: openFileButton.right
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
width: Math.round(0.9 * prepareMenu.width)
height: parent.height
spacing: 0

View file

@ -20,15 +20,21 @@ Item
name: "cura"
}
anchors
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
}
Row
{
id: stageMenuRow
anchors.centerIn: parent
height: parent.height
width: childrenRect.width
// We want this row to have a preferred with equals to the 85% of the parent
property int preferredWidth: Math.round(0.85 * previewMenu.width)
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
Cura.ViewsSelector
{
@ -49,12 +55,12 @@ Item
color: UM.Theme.getColor("lining")
}
// This component will grow freely up to complete the preferredWidth of the row.
// This component will grow freely up to complete the width of the row.
Loader
{
id: viewPanel
height: parent.height
width: source != "" ? (stageMenuRow.preferredWidth - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width) : 0
width: source != "" ? (previewMenu.width - viewsSelector.width - printSetupSelectorItem.width - 2 * (UM.Theme.getSize("wide_margin").width + UM.Theme.getSize("default_lining").width)) : 0
source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : ""
}

View file

@ -15,6 +15,8 @@ Cura.ExpandableComponent
{
id: base
dragPreferencesNamePrefix: "view/colorscheme"
contentHeaderTitle: catalog.i18nc("@label", "Color scheme")
Connections
@ -177,7 +179,6 @@ Cura.ExpandableComponent
height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
width: parent.width
visible: !UM.SimulationView.compatibilityMode
enabled: index < 4
onClicked:
{

View file

@ -1,150 +1,154 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
import Cura 1.1 as Cura
UM.Dialog
Window
{
UM.I18nCatalog { id: catalog; name: "cura" }
id: baseDialog
title: catalog.i18nc("@title:window", "More information on anonymous data collection")
visible: false
modality: Qt.ApplicationModal
minimumWidth: 500 * screenScaleFactor
minimumHeight: 400 * screenScaleFactor
width: minimumWidth
height: minimumHeight
property bool allowSendData: true // for saving the user's choice
color: UM.Theme.getColor("main_background")
onAccepted: manager.setSendSliceInfo(allowSendData)
property bool allowSendData: true // for saving the user's choice
onVisibilityChanged:
{
if (visible)
{
baseDialog.allowSendData = UM.Preferences.getValue("info/send_slice_info");
baseDialog.allowSendData = UM.Preferences.getValue("info/send_slice_info")
if (baseDialog.allowSendData)
{
allowSendButton.checked = true;
allowSendButton.checked = true
}
else
{
dontSendButton.checked = true;
dontSendButton.checked = true
}
}
}
// Main content area
Item
{
id: textRow
anchors
{
top: parent.top
bottom: radioButtonsRow.top
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
Label
Item // Text part
{
id: headerText
id: textRow
anchors
{
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@text:window", "Cura sends anonymous data to Ultimaker in order to improve the print quality and user experience. Below is an example of all the data that is sent.")
wrapMode: Text.WordWrap
}
TextArea
{
id: exampleData
anchors
{
top: headerText.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottom: radioButtonsRow.top
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
text: manager.getExampleData()
readOnly: true
textFormat: TextEdit.PlainText
}
}
Column
{
id: radioButtonsRow
width: parent.width
anchors.bottom: buttonRow.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
ExclusiveGroup { id: group }
RadioButton
{
id: dontSendButton
text: catalog.i18nc("@text:window", "I don't want to send this data")
exclusiveGroup: group
onClicked:
Label
{
baseDialog.allowSendData = !checked;
id: headerText
anchors
{
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@text:window", "Cura sends anonymous data to Ultimaker in order to improve the print quality and user experience. Below is an example of all the data that is sent.")
wrapMode: Text.WordWrap
renderType: Text.NativeRendering
}
}
RadioButton
{
id: allowSendButton
text: catalog.i18nc("@text:window", "Allow sending this data to Ultimaker and help us improve Cura")
exclusiveGroup: group
onClicked:
Cura.ScrollableTextArea
{
baseDialog.allowSendData = checked;
}
}
}
anchors
{
top: headerText.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
Item
{
id: buttonRow
anchors.bottom: parent.bottom
width: parent.width
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
UM.I18nCatalog { id: catalog; name: "cura" }
Button
{
anchors.right: parent.right
text: catalog.i18nc("@action:button", "OK")
onClicked:
{
baseDialog.accepted()
baseDialog.hide()
textArea.text: manager.getExampleData()
textArea.readOnly: true
}
}
Button
Column // Radio buttons for agree and disagree
{
id: radioButtonsRow
anchors.left: parent.left
text: catalog.i18nc("@action:button", "Cancel")
onClicked:
anchors.right: parent.right
anchors.bottom: buttonRow.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
Cura.RadioButton
{
baseDialog.rejected()
baseDialog.hide()
id: dontSendButton
text: catalog.i18nc("@text:window", "I don't want to send this data")
onClicked:
{
baseDialog.allowSendData = !checked
}
}
Cura.RadioButton
{
id: allowSendButton
text: catalog.i18nc("@text:window", "Allow sending this data to Ultimaker and help us improve Cura")
onClicked:
{
baseDialog.allowSendData = checked
}
}
}
Item // Bottom buttons
{
id: buttonRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: childrenRect.height
Cura.PrimaryButton
{
anchors.right: parent.right
text: catalog.i18nc("@action:button", "OK")
onClicked:
{
manager.setSendSliceInfo(allowSendData)
baseDialog.hide()
}
}
Cura.SecondaryButton
{
anchors.left: parent.left
text: catalog.i18nc("@action:button", "Cancel")
onClicked:
{
baseDialog.hide()
}
}
}
}

View file

@ -48,20 +48,6 @@ class SliceInfo(QObject, Extension):
def _onAppInitialized(self):
# DO NOT read any preferences values in the constructor because at the time plugins are created, no version
# upgrade has been performed yet because version upgrades are plugins too!
if not self._application.getPreferences().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
lifetime = 0,
dismissable = False,
title = catalog.i18nc("@info:title", "Collecting Data"))
self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None,
description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK)
self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
if self._more_info_dialog is None:
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
@ -76,7 +62,7 @@ class SliceInfo(QObject, Extension):
def showMoreInfoDialog(self):
if self._more_info_dialog is None:
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
self._more_info_dialog.open()
self._more_info_dialog.show()
def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name)
@ -195,6 +181,8 @@ class SliceInfo(QObject, Extension):
model = dict()
model["hash"] = node.getMeshData().getHash()
bounding_box = node.getBoundingBox()
if not bounding_box:
continue
model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
"y": bounding_box.minimum.y,
"z": bounding_box.minimum.z},

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.View.View import View
@ -7,7 +7,6 @@ from UM.Scene.Selection import Selection
from UM.Resources import Resources
from UM.Application import Application
from UM.View.RenderBatch import RenderBatch
from UM.Settings.Validator import ValidatorState
from UM.Math.Color import Color
from UM.View.GL.OpenGL import OpenGL
@ -20,9 +19,9 @@ import math
class SolidView(View):
def __init__(self):
super().__init__()
Application.getInstance().getPreferences().addPreference("view/show_overhang", True)
application = Application.getInstance()
application.getPreferences().addPreference("view/show_overhang", True)
application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
@ -30,6 +29,38 @@ class SolidView(View):
self._extruders_model = None
self._theme = None
self._support_angle = 90
self._global_stack = None
Application.getInstance().engineCreatedSignal.connect(self._onGlobalContainerChanged)
def _onGlobalContainerChanged(self) -> None:
if self._global_stack:
try:
self._global_stack.propertyChanged.disconnect(self._onPropertyChanged)
except TypeError:
pass
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._onPropertyChanged)
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
self._onPropertyChanged("support_angle", "value") # Force an re-evaluation
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if key != "support_angle" or property_name != "value":
return
# As the rendering is called a *lot* we really, dont want to re-evaluate the property every time. So we store em!
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = global_container_stack.extruders.get(str(support_extruder_nr))
if support_angle_stack:
self._support_angle = support_angle_stack.getProperty("support_angle", "value")
def beginRendering(self):
scene = self.getController().getScene()
@ -63,14 +94,10 @@ class SolidView(View):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if support_angle_stack is not None and Application.getInstance().getPreferences().getValue("view/show_overhang"):
angle = support_angle_stack.getProperty("support_angle", "value")
if Application.getInstance().getPreferences().getValue("view/show_overhang"):
# Make sure the overhang angle is valid before passing it to the shader
if angle is not None and angle >= 0 and angle <= 90:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle)))
if self._support_angle is not None and self._support_angle >= 0 and self._support_angle <= 90:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle)))
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang.
else:

View file

@ -65,6 +65,7 @@ Item
{
id: description
text: details.description || ""
font: UM.Theme.getFont("default")
anchors
{
top: title.bottom

View file

@ -26,7 +26,7 @@ UM.Dialog
minimumWidth: 450 * screenScaleFactor
minimumHeight: 150 * screenScaleFactor
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal
modality: Qt.WindowModal
Column
{

View file

@ -10,7 +10,7 @@ import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id)
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
property var packageData
@ -112,11 +112,9 @@ Column
{
target: toolbox
onInstallChanged: installed = toolbox.isInstalled(model.id)
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
onFilterChanged:
{
installed = toolbox.isInstalled(model.id)
canUpdate = toolbox.canUpdate(model.id)
}
}
}

View file

@ -76,7 +76,7 @@ Item
height: (parent.height * 0.4) | 0
anchors
{
bottom: parent.bottomcommi
bottom: parent.bottom
right: parent.right
}
sourceSize.height: height

View file

@ -14,7 +14,7 @@ Rectangle
Column
{
height: childrenRect.height + 2 * padding
spacing: UM.Theme.getSize("toolbox_showcase_spacing").width
spacing: UM.Theme.getSize("default_margin").width
width: parent.width
padding: UM.Theme.getSize("wide_margin").height
Label

View file

@ -1,9 +1,11 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Controls 1.4
import UM 1.1 as UM
import UM 1.4 as UM
import Cura 1.0 as Cura
Item
{
@ -50,6 +52,7 @@ Item
}
}
}
ToolboxTabButton
{
id: installedTabButton
@ -62,7 +65,25 @@ Item
rightMargin: UM.Theme.getSize("default_margin").width
}
onClicked: toolbox.viewCategory = "installed"
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
}
Cura.NotificationIcon
{
id: marketplaceNotificationIcon
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
anchors.right: installedTabButton.right
anchors.verticalCenter: installedTabButton.verticalCenter
labelText:
{
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
return itemCount > 9 ? "9+" : itemCount
}
}
ToolboxShadow
{
anchors.top: bar.bottom

View file

@ -10,7 +10,7 @@ import Cura 1.1 as Cura
Column
{
property bool canUpdate: false
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool canDowngrade: false
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
@ -83,7 +83,6 @@ Column
target: toolbox
onMetadataChanged:
{
canUpdate = toolbox.canUpdate(model.id)
canDowngrade = toolbox.canDowngrade(model.id)
}
}

View file

@ -9,14 +9,17 @@ Button
{
id: control
property bool active: false
hoverEnabled: true
implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
background: Item
{
implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
id: backgroundItem
Rectangle
{
id: highlight
visible: control.active
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
@ -24,28 +27,42 @@ Button
height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
contentItem: Label
{
id: label
text: control.text
color:
{
if(control.hovered)
{
return UM.Theme.getColor("toolbox_header_button_text_hovered");
}
if(control.active)
{
return UM.Theme.getColor("toolbox_header_button_text_active");
}
else
{
return UM.Theme.getColor("toolbox_header_button_text_inactive");
}
}
font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
color: UM.Theme.getColor("toolbox_header_button_text_inactive")
font: UM.Theme.getFont("medium")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
}
states:
[
State
{
name: "disabled"
when: !control.enabled
PropertyChanges
{
target: label
font: UM.Theme.getFont("default_italic")
}
},
State
{
name: "active"
when: control.active
PropertyChanges
{
target: label
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("action_button_text")
}
}
]
}

View file

@ -53,7 +53,7 @@ class AuthorsModel(ListModel):
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "package_types":
if key == "package_types":
key_filter = lambda item, value = value: value in item["package_types"] # type: ignore
elif "*" in value:
key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore

View file

@ -112,7 +112,7 @@ class PackagesModel(ListModel):
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "tags":
if key == "tags":
key_filter = lambda item, v = value: v in item["tags"]
elif "*" in value:
key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v)

View file

@ -50,7 +50,6 @@ class Toolbox(QObject, Extension):
self._request_headers = [] # type: List[Tuple[bytes, bytes]]
self._updateRequestHeader()
self._request_urls = {} # type: Dict[str, QUrl]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = set() # type: Set[str]
@ -106,6 +105,7 @@ class Toolbox(QObject, Extension):
self._application.initializationFinished.connect(self._onAppInitialized)
self._application.getCuraAPI().account.loginStateChanged.connect(self._updateRequestHeader)
self._application.getCuraAPI().account.accessTokenChanged.connect(self._updateRequestHeader)
# Signals:
# --------------------------------------------------------------------------
@ -190,8 +190,10 @@ class Toolbox(QObject, Extension):
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
}
@pyqtSlot()
def browsePackages(self) -> None:
# Request the latest and greatest!
self._fetchPackageData()
def _fetchPackageData(self):
# Create the network manager:
# This was formerly its own function but really had no reason to be as
# it was never called more than once ever.
@ -209,6 +211,10 @@ class Toolbox(QObject, Extension):
# Gather installed packages:
self._updateInstalledModels()
@pyqtSlot()
def browsePackages(self) -> None:
self._fetchPackageData()
if not self._dialog:
self._dialog = self._createDialog("Toolbox.qml")
@ -455,36 +461,6 @@ class Toolbox(QObject, Extension):
break
return remote_package
# Checks
# --------------------------------------------------------------------------
@pyqtSlot(str, result = bool)
def canUpdate(self, package_id: str) -> bool:
local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None:
local_package = self.getOldPluginPackageMetadata(package_id)
if local_package is None:
return False
remote_package = self.getRemotePackage(package_id)
if remote_package is None:
return False
local_version = Version(local_package["package_version"])
remote_version = Version(remote_package["package_version"])
can_upgrade = False
if remote_version > local_version:
can_upgrade = True
# A package with the same version can be built to have different SDK versions. So, for a package with the same
# version, we also need to check if the current one has a lower SDK version. If so, this package should also
# be upgradable.
elif remote_version == local_version:
# First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system).
remote_sdk_version = Version(remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0)))
local_sdk_version = Version(local_package.get("sdk_version_semver", local_package.get("sdk_version", 0)))
can_upgrade = local_sdk_version < remote_sdk_version
return can_upgrade
@pyqtSlot(str, result = bool)
def canDowngrade(self, package_id: str) -> bool:
# If the currently installed version is higher than the bundled version (if present), the we can downgrade
@ -584,9 +560,15 @@ class Toolbox(QObject, Extension):
if self._download_reply:
try:
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
except TypeError: # Raised when the method is not connected to the signal yet.
except (TypeError, RuntimeError): # Raised when the method is not connected to the signal yet.
pass # Don't need to disconnect.
self._download_reply.abort()
try:
self._download_reply.abort()
except RuntimeError:
# In some cases the garbage collector is a bit to agressive, which causes the dowload_reply
# to be deleted (especially if the machine has been put to sleep). As we don't know what exactly causes
# this (The issue probably lives in the bowels of (py)Qt somewhere), we can only catch and ignore it.
pass
self._download_reply = None
self._download_request = None
self.setDownloadProgress(0)
@ -632,11 +614,12 @@ class Toolbox(QObject, Extension):
self._server_response_data[response_type] = json_data["data"]
self._models[response_type].setMetadata(self._server_response_data[response_type])
if response_type is "packages":
if response_type == "packages":
self._models[response_type].setFilter({"type": "plugin"})
self.reBuildMaterialsModels()
self.reBuildPluginsModels()
elif response_type is "authors":
self._notifyPackageManager()
elif response_type == "authors":
self._models[response_type].setFilter({"package_types": "material"})
self._models[response_type].setFilter({"tags": "generic"})
@ -656,6 +639,11 @@ class Toolbox(QObject, Extension):
# Ignore any operation that is not a get operation
pass
# This function goes through all known remote versions of a package and notifies the package manager of this change
def _notifyPackageManager(self):
for package in self._server_response_data["packages"]:
self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))
def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100

View file

@ -0,0 +1,42 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from Charon.VirtualFile import VirtualFile
from UM.Mesh.MeshReader import MeshReader
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.PluginRegistry import PluginRegistry
if TYPE_CHECKING:
from cura.Scene.CuraSceneNode import CuraSceneNode
class UFPReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-ufp",
comment = "Ultimaker Format Package",
suffixes = ["ufp"]
)
)
self._supported_extensions = [".ufp"]
def _read(self, file_name: str) -> "CuraSceneNode":
# Open the file
archive = VirtualFile()
archive.open(file_name)
# Get the gcode data from the file
gcode_data = archive.getData("/3D/model.gcode")
# Convert the bytes stream to string
gcode_stream = gcode_data["/3D/model.gcode"].decode("utf-8")
# Open the GCodeReader to parse the data
gcode_reader = PluginRegistry.getInstance().getPluginObject("GCodeReader") # type: ignore
gcode_reader.preReadFromStream(gcode_stream) # type: ignore
return gcode_reader.readFromStream(gcode_stream) # type: ignore

View file

@ -0,0 +1,26 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import UFPReader
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"mesh_reader": [
{
"mime_type": "application/x-ufp",
"extension": "ufp",
"description": i18n_catalog.i18nc("@item:inlistbox", "Ultimaker Format Package")
}
]
}
def register(app):
app.addNonSliceableExtension(".ufp")
return {"mesh_reader": UFPReader.UFPReader()}

View file

@ -0,0 +1,8 @@
{
"name": "UFP Reader",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["6.0.0"],
"i18n-catalog": "cura"
}

View file

@ -28,7 +28,7 @@ class UFPWriter(MeshWriter):
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-ufp",
comment = "Cura UFP File",
comment = "Ultimaker Format Package",
suffixes = ["ufp"]
)
)

View file

@ -371,7 +371,7 @@ Cura.MachineAction
Label
{
text: catalog.i18nc("@alabel", "Enter the IP address or hostname of your printer on the network.")
text: catalog.i18nc("@label", "Enter the IP address or hostname of your printer on the network.")
width: parent.width
wrapMode: Text.WordWrap
renderType: Text.NativeRendering

View file

@ -158,14 +158,9 @@ Item
spacing: 6 // TODO: Theme!
visible: printJob
Repeater
MonitorPrinterPill
{
id: compatiblePills
delegate: MonitorPrinterPill
{
text: modelData
}
model: printJob ? printJob.compatibleMachineFamilies : []
text: printJob.configuration.printerType
}
}
}

View file

@ -22,7 +22,7 @@ Item
width: childrenRect.width
height: 18 * screenScaleFactor // TODO: Theme!
ProgressBar
UM.ProgressBar
{
id: progressBar
anchors
@ -30,22 +30,6 @@ Item
verticalCenter: parent.verticalCenter
}
value: printJob ? printJob.progress : 0
style: ProgressBarStyle
{
background: Rectangle
{
color: UM.Theme.getColor("monitor_progress_bar_empty")
implicitHeight: visible ? 12 * screenScaleFactor : 0 // TODO: Theme!
implicitWidth: 180 * screenScaleFactor // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
}
progress: Rectangle
{
id: progressItem;
color: printJob && printJob.isActive ? UM.Theme.getColor("monitor_progress_bar_fill") : UM.Theme.getColor("monitor_progress_bar_deactive")
radius: 2 * screenScaleFactor // TODO: Theme!
}
}
}
Label
{

View file

@ -140,7 +140,7 @@ Item
{
id: printerConfiguration
anchors.verticalCenter: parent.verticalCenter
buildplate: printer ? "Glass" : null // 'Glass' as a default
buildplate: printer ? catalog.i18nc("@label", "Glass") : null // 'Glass' as a default
configurations:
{
var configs = []

View file

@ -210,7 +210,7 @@ Item
Label
{
text: "All jobs are printed."
text: i18n.i18nc("@info", "All jobs are printed.")
color: UM.Theme.getColor("monitor_text_primary")
font: UM.Theme.getFont("medium") // 14pt, regular
}

View file

@ -18,8 +18,8 @@ from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutputDevice import ConnectionType
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .CloudOutputController import CloudOutputController
from ..MeshFormatHandler import MeshFormatHandler
@ -58,6 +58,14 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
# Therefore we create a private signal used to trigger the printersChanged signal.
_clusterPrintersChanged = pyqtSignal()
# Map of Cura Connect machine_variant field to Cura machine types.
# Needed for printer discovery stack creation.
_host_machine_variant_to_machine_type_map = {
"Ultimaker 3": "ultimaker3",
"Ultimaker 3 Extended": "ultimaker3_extended",
"Ultimaker S5": "ultimaker_s5"
}
## Creates a new cloud output device
# \param api_client: The client that will run the API calls
# \param cluster: The device response received from the cloud API.
@ -68,10 +76,10 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
# Because the cloud connection does not off all of these, we manually construct this version here.
# An example of why this is needed is the selection of the compatible file type when exporting the tool path.
properties = {
b"address": b"",
b"name": cluster.host_name.encode() if cluster.host_name else b"",
b"address": cluster.host_internal_ip.encode() if cluster.host_internal_ip else b"",
b"name": cluster.friendly_name.encode() if cluster.friendly_name else b"",
b"firmware_version": cluster.host_version.encode() if cluster.host_version else b"",
b"printer_type": b""
b"cluster_size": b"1" # cloud devices are always clusters of at least one
}
super().__init__(device_id = cluster.cluster_id, address = "",
@ -85,16 +93,18 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
# We use the Cura Connect monitor tab to get most functionality right away.
if PluginRegistry.getInstance() is not None:
self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
if plugin_path is None:
Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")
# Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged)
# We keep track of which printer is visible in the monitor page.
self._active_printer = None # type: Optional[PrinterOutputModel]
self._host_machine_type = ""
# Properties to populate later on with received cloud data.
self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
@ -145,9 +155,17 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
## Checks whether the given network key is found in the cloud's host name
def matchesNetworkKey(self, network_key: str) -> bool:
# A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
# the host name should then be "ultimakersystem-aabbccdd0011"
return network_key.startswith(self.clusterData.host_name)
if network_key.startswith(self.clusterData.host_name):
return True
# However, for manually added printers, the local IP address is used in lieu of a proper
# network key, so check for that as well
if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip):
return True
return False
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None:
@ -227,6 +245,10 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinterStatus]
if len(printers) > 0:
# We need the machine type of the host (1st list entry) to allow discovery to work.
self._host_machine_type = printers[0].machine_variant
removed_printers, added_printers, updated_printers = findChanges(previous, received)
for removed_printer in removed_printers:
@ -350,6 +372,19 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
).show()
self.writeFinished.emit()
## Gets the printer type of the cluster host. Falls back to the printer type in the device properties.
@pyqtProperty(str, notify=_clusterPrintersChanged)
def printerType(self) -> str:
if self._printers and self._host_machine_type in self._host_machine_variant_to_machine_type_map:
return self._host_machine_variant_to_machine_type_map[self._host_machine_type]
return super().printerType
## Gets the number of printers in the cluster.
# We use a minimum of 1 because cloud devices are always a cluster and printer discovery needs it.
@pyqtProperty(int, notify = _clusterPrintersChanged)
def clusterSize(self) -> int:
return max(1, len(self._printers))
## Gets the remote printers.
@pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
def printers(self) -> List[PrinterOutputModel]:
@ -367,10 +402,6 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
self._active_printer = printer
self.activePrinterChanged.emit()
@pyqtProperty(int, notify = _clusterPrintersChanged)
def clusterSize(self) -> int:
return len(self._printers)
## Get remote print jobs.
@pyqtProperty("QVariantList", notify = printJobsChanged)
def printJobs(self) -> List[UM3PrintJobOutputModel]:

View file

@ -7,7 +7,7 @@ from PyQt5.QtCore import QTimer
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal, signalemitter
from UM.Signal import Signal
from cura.API import Account
from cura.CuraApplication import CuraApplication
from cura.Settings.GlobalStack import GlobalStack
@ -81,25 +81,61 @@ class CloudOutputDeviceManager:
Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates))
# Remove output devices that are gone
for removed_cluster in removed_devices:
if removed_cluster.isConnected():
removed_cluster.disconnect()
removed_cluster.close()
self._output_device_manager.removeOutputDevice(removed_cluster.key)
self.removedCloudCluster.emit()
del self._remote_clusters[removed_cluster.key]
for device in removed_devices:
if device.isConnected():
device.disconnect()
device.close()
self._output_device_manager.removeOutputDevice(device.key)
self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key)
self.removedCloudCluster.emit(device)
del self._remote_clusters[device.key]
# Add an output device for each new remote cluster.
# We only add when is_online as we don't want the option in the drop down if the cluster is not online.
for added_cluster in added_clusters:
device = CloudOutputDevice(self._api, added_cluster)
self._remote_clusters[added_cluster.cluster_id] = device
self.addedCloudCluster.emit()
for cluster in added_clusters:
device = CloudOutputDevice(self._api, cluster)
self._remote_clusters[cluster.cluster_id] = device
self._application.getDiscoveredPrintersModel().addDiscoveredPrinter(
cluster.cluster_id,
device.key,
cluster.friendly_name,
self._createMachineFromDiscoveredPrinter,
device.printerType,
device
)
self.addedCloudCluster.emit(cluster)
# Update the output devices
for device, cluster in updates:
device.clusterData = cluster
self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(
cluster.cluster_id,
cluster.friendly_name,
device.printerType,
)
self._connectToActiveMachine()
def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
device = self._remote_clusters[key] # type: CloudOutputDevice
if not device:
Logger.log("e", "Could not find discovered device with key [%s]", key)
return
group_name = device.clusterData.friendly_name
machine_type_id = device.printerType
Logger.log("i", "Creating machine from cloud device with key = [%s], group name = [%s], printer type = [%s]",
key, group_name, machine_type_id)
# The newly added machine is automatically activated.
self._application.getMachineManager().addMachine(machine_type_id, group_name)
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine:
return
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
self._connectToOutputDevice(device, active_machine)
## Callback for when the active machine was changed by the user or a new remote cluster was found.
def _connectToActiveMachine(self) -> None:

View file

@ -11,8 +11,8 @@ I18N_CATALOG = i18nCatalog("cura")
class CloudProgressMessage(Message):
def __init__(self):
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
title = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"),
text = I18N_CATALOG.i18nc("@info:status", "Uploading via Ultimaker Cloud"),
progress = -1,
lifetime = 0,
dismissable = False,

View file

@ -2,8 +2,8 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Union, Dict, Optional, Any
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial
from .BaseCloudModel import BaseCloudModel

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional, Union, Dict, Any
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from ...UM3PrintJobOutputModel import UM3PrintJobOutputModel
from ...ConfigurationChangeModel import ConfigurationChangeModel
from ..CloudOutputController import CloudOutputController
@ -95,9 +95,9 @@ class CloudClusterPrintJobStatus(BaseCloudModel):
return model
## Creates a new configuration model
def _createConfigurationModel(self) -> ConfigurationModel:
def _createConfigurationModel(self) -> PrinterConfigurationModel:
extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
configuration = ConfigurationModel()
configuration = PrinterConfigurationModel()
configuration.setExtruderConfigurations(extruders)
return configuration

View file

@ -2,7 +2,7 @@ from typing import Optional
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from .BaseCloudModel import BaseCloudModel

View file

@ -3,7 +3,7 @@
from typing import List, Union, Dict, Optional, Any
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from .CloudClusterBuildPlate import CloudClusterBuildPlate
from .CloudClusterPrintCoreConfiguration import CloudClusterPrintCoreConfiguration
from .BaseCloudModel import BaseCloudModel

View file

@ -16,13 +16,16 @@ class CloudClusterResponse(BaseCloudModel):
# \param status: The status of the cluster authentication (active or inactive).
# \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
host_version: Optional[str] = None, **kwargs) -> None:
host_internal_ip: Optional[str] = None, host_version: Optional[str] = None,
friendly_name: Optional[str] = None, **kwargs) -> None:
self.cluster_id = cluster_id
self.host_guid = host_guid
self.host_name = host_name
self.status = status
self.is_online = is_online
self.host_version = host_version
self.host_internal_ip = host_internal_ip
self.friendly_name = friendly_name
super().__init__(**kwargs)
# Validates the model, raising an exception if the model is invalid.

View file

@ -19,12 +19,12 @@ from UM.Scene.SceneNode import SceneNode # For typing.
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutputDevice import ConnectionType
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .Cloud.Utils import formatTimeCompleted, formatDateCompleted
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
@ -66,10 +66,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._received_print_jobs = False # type: bool
if PluginRegistry.getInstance() is not None:
self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
if plugin_path is None:
Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")
# Trigger the printersChanged signal when the private signal is triggered
self.printersChanged.connect(self._clusterPrintersChanged)
@ -395,9 +396,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
for job in newly_finished_jobs:
if job.assignedPrinter:
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(printer_name=job.assignedPrinter.name, job_name = job.name)
else:
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.").format(job_name = job.name)
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
job_completed_message.show()
@ -522,7 +523,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
key=data["uuid"], name= data["name"])
configuration = ConfigurationModel()
configuration = PrinterConfigurationModel()
extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
for index in range(0, self._number_of_extruders):
try:
@ -534,6 +535,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))
configuration.setExtruderConfigurations(extruders)
configuration.setPrinterType(data.get("machine_variant", ""))
print_job.updateConfiguration(configuration)
print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
print_job.stateChanged.connect(self._printJobStateChanged)
@ -633,6 +635,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printer.updateKey(data["uuid"])
printer.updateType(data["machine_variant"])
if data["status"] != "unreachable":
self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(data["ip_address"],
name = data["friendly_name"],
machine_type = data["machine_variant"])
# Do not store the build plate information that comes from connect if the current printer has not build plate information
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
printer.updateBuildplate(data["build_plate"]["type"])

View file

@ -5,7 +5,7 @@ from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
class ClusterUM3PrinterOutputController(PrinterOutputController):
def __init__(self, output_device):

View file

@ -18,7 +18,7 @@ from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from .UM3OutputDevicePlugin import UM3OutputDevicePlugin
if TYPE_CHECKING:
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
catalog = i18nCatalog("cura")
@ -41,6 +41,11 @@ class DiscoverUM3Action(MachineAction):
# Time to wait after a zero-conf service change before allowing a zeroconf reset
self._zero_conf_change_grace_period = 0.25 #type: float
# Overrides the one in MachineAction.
# This requires not attention from the user (any more), so we don't need to show any 'upgrade screens'.
def needsUserInteraction(self) -> bool:
return False
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
@ -110,7 +115,7 @@ class DiscoverUM3Action(MachineAction):
previous_connect_group_name = meta_data["group_name"]
global_container_stack.setMetaDataEntry("group_name", group_name)
# Find all the places where there is the same group name and change it accordingly
CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "group_name", value = previous_connect_group_name, new_value = group_name)
self._replaceContainersMetadata(key = "group_name", value = previous_connect_group_name, new_value = group_name)
else:
global_container_stack.setMetaDataEntry("group_name", group_name)
# Set the default value for "hidden", which is used when you have a group with multiple types of printers
@ -118,49 +123,21 @@ class DiscoverUM3Action(MachineAction):
if self._network_plugin:
# Ensure that the connection states are refreshed.
self._network_plugin.reCheckConnections()
self._network_plugin.refreshConnections()
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
def _replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None:
machines = CuraContainerRegistry.getInstance().findContainerStacks(type="machine")
for machine in machines:
if machine.getMetaDataEntry(key) == value:
machine.setMetaDataEntry(key, new_value)
# Associates the currently active machine with the given printer device. The network connection information will be
# stored into the metadata of the currently active machine.
@pyqtSlot(QObject)
def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
if not printer_device:
return
Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key)
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data: # Global stack already had a connection, but it's changed.
old_network_key = meta_data["um_network_key"]
# Since we might have a bunch of hidden stacks, we also need to change it there.
metadata_filter = {"um_network_key": old_network_key}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine", **metadata_filter)
for container in containers:
container.setMetaDataEntry("um_network_key", printer_device.key)
# Delete old authentication data.
Logger.log("d", "Removing old authentication id %s for device %s",
global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key)
container.removeMetaDataEntry("network_authentication_id")
container.removeMetaDataEntry("network_authentication_key")
# Ensure that these containers do know that they are configured for network connection
container.addConfiguredConnectionType(printer_device.connectionType.value)
else: # Global stack didn't have a connection yet, configure it.
global_container_stack.setMetaDataEntry("um_network_key", printer_device.key)
global_container_stack.addConfiguredConnectionType(printer_device.connectionType.value)
if self._network_plugin:
# Ensure that the connection states are refreshed.
self._network_plugin.reCheckConnections()
self._network_plugin.associateActiveMachineWithPrinterDevice(printer_device)
@pyqtSlot(result = str)
def getStoredKey(self) -> str:
@ -180,7 +157,9 @@ class DiscoverUM3Action(MachineAction):
@pyqtSlot(str, result = bool)
def existsKey(self, key: str) -> bool:
return CuraApplication.getInstance().getMachineManager().existNetworkInstances(network_key = key)
metadata_filter = {"um_network_key": key}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine", **metadata_filter)
return bool(containers)
@pyqtSlot()
def loadConfigurationFromPrinter(self) -> None:

View file

@ -2,10 +2,10 @@ from typing import List, Optional
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutputDevice import ConnectionType
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.ExtruderManager import ExtruderManager
@ -77,13 +77,15 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.setIconName("print")
if PluginRegistry.getInstance() is not None:
self._output_controller = LegacyUM3PrinterOutputController(self)
def _createMonitorViewFromQML(self) -> None:
if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None:
self._monitor_view_qml_path = os.path.join(
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "qml", "MonitorStage.qml"
)
self._output_controller = LegacyUM3PrinterOutputController(self)
super()._createMonitorViewFromQML()
def _onAuthenticationStateChanged(self):
# We only accept commands if we are authenticated.

View file

@ -7,8 +7,8 @@ from UM.Version import Version
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
class LegacyUM3PrinterOutputController(PrinterOutputController):

View file

@ -1,23 +1,25 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import os
from queue import Queue
from threading import Event, Thread
from time import time
import os
from typing import Optional, TYPE_CHECKING, Dict
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import ConnectionType
from cura.Settings.GlobalStack import GlobalStack # typing
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.PluginRegistry import PluginRegistry
from UM.Signal import Signal, signalemitter
@ -25,18 +27,25 @@ from UM.Version import Version
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
from .Cloud.CloudOutputDevice import CloudOutputDevice # typing
if TYPE_CHECKING:
from PyQt5.QtNetwork import QNetworkReply
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.Settings.GlobalStack import GlobalStack
from typing import Optional
i18n_catalog = i18nCatalog("cura")
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
# Zero-Conf is used to detect printers, which are saved in a dict.
# If we discover a printer that has the same key as the active machine instance a connection is made.
@signalemitter
class UM3OutputDevicePlugin(OutputDevicePlugin):
addDeviceSignal = Signal()
removeDeviceSignal = Signal()
addDeviceSignal = Signal() # Called '...Signal' to avoid confusion with function-names.
removeDeviceSignal = Signal() # Ditto ^^^.
discoveredDevicesChanged = Signal()
cloudFlowIsPossible = Signal()
@ -55,7 +64,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self.addDeviceSignal.connect(self._onAddDevice)
self.removeDeviceSignal.connect(self._onRemoveDevice)
self._application.globalContainerStackChanged.connect(self.reCheckConnections)
self._application.globalContainerStackChanged.connect(self.refreshConnections)
self._discovered_devices = {}
@ -142,7 +151,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self.addManualDevice(address)
self.resetLastManualDevice()
def reCheckConnections(self):
def refreshConnections(self):
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine:
return
@ -173,9 +182,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
if key == um_network_key:
self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
self.checkCloudFlowIsPossible()
self.checkCloudFlowIsPossible(None)
else:
self.getOutputDeviceManager().removeOutputDevice(key)
if key.startswith("manual:"):
self.removeManualDeviceSignal.emit(self.getPluginId(), key, self._discovered_devices[key].address)
def stop(self):
if self._zero_conf is not None:
@ -183,6 +194,10 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._zero_conf.close()
self._cloud_output_device_manager.stop()
def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt:
# This plugin should always be the fallback option (at least try it):
return ManualDeviceAdditionAttempt.POSSIBLE
def removeManualDevice(self, key, address = None):
if key in self._discovered_devices:
if not address:
@ -194,6 +209,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._manual_instances.remove(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
self.removeManualDeviceSignal.emit(self.getPluginId(), key, address)
def addManualDevice(self, address):
if address not in self._manual_instances:
self._manual_instances.append(address)
@ -215,7 +232,62 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._checkManualDevice(address)
def _checkManualDevice(self, address):
def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
discovered_device = self._discovered_devices.get(key)
if discovered_device is None:
Logger.log("e", "Could not find discovered device with key [%s]", key)
return
group_name = discovered_device.getProperty("name")
machine_type_id = discovered_device.getProperty("printer_type")
Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]",
key, group_name, machine_type_id)
self._application.getMachineManager().addMachine(machine_type_id, group_name)
# connect the new machine to that network printer
self.associateActiveMachineWithPrinterDevice(discovered_device)
# ensure that the connection states are refreshed.
self.refreshConnections()
def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
if not printer_device:
return
Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key)
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data: # Global stack already had a connection, but it's changed.
old_network_key = meta_data["um_network_key"]
# Since we might have a bunch of hidden stacks, we also need to change it there.
metadata_filter = {"um_network_key": old_network_key}
containers = self._application.getContainerRegistry().findContainerStacks(type = "machine", **metadata_filter)
for container in containers:
container.setMetaDataEntry("um_network_key", printer_device.key)
# Delete old authentication data.
Logger.log("d", "Removing old authentication id %s for device %s",
global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key)
container.removeMetaDataEntry("network_authentication_id")
container.removeMetaDataEntry("network_authentication_key")
# Ensure that these containers do know that they are configured for network connection
container.addConfiguredConnectionType(printer_device.connectionType.value)
else: # Global stack didn't have a connection yet, configure it.
global_container_stack.setMetaDataEntry("um_network_key", printer_device.key)
global_container_stack.addConfiguredConnectionType(printer_device.connectionType.value)
self.refreshConnections()
def _checkManualDevice(self, address: str) -> None:
# Check if a UM3 family device exists at this address.
# If a printer responds, it will replace the preliminary printer created above
# origin=manual is for tracking back the origin of the call
@ -223,21 +295,30 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
name_request = QNetworkRequest(url)
self._network_manager.get(name_request)
def _onNetworkRequestFinished(self, reply):
def _onNetworkRequestFinished(self, reply: "QNetworkReply") -> None:
reply_url = reply.url().toString()
if "system" in reply_url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
# Something went wrong with checking the firmware version!
return
address = reply.url().host()
device = None
properties = {} # type: Dict[bytes, bytes]
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
# Either:
# - Something went wrong with checking the firmware version!
# - Something went wrong with checking the amount of printers the cluster has!
# - Couldn't find printer at the address when trying to add it manually.
if address in self._manual_instances:
key = "manual:" + address
self.removeManualDevice(key, address)
return
if "system" in reply_url:
try:
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
except:
Logger.log("e", "Something went wrong converting the JSON.")
return
address = reply.url().host()
has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
instance_name = "manual:%s" % address
properties = {
@ -265,27 +346,27 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._network_manager.get(cluster_request)
elif "printers" in reply_url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
# Something went wrong with checking the amount of printers the cluster has!
return
# So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
try:
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
except:
Logger.log("e", "Something went wrong converting the JSON.")
return
address = reply.url().host()
instance_name = "manual:%s" % address
if instance_name in self._discovered_devices:
device = self._discovered_devices[instance_name]
properties = device.getProperties().copy()
if b"incomplete" in properties:
del properties[b"incomplete"]
properties[b"cluster_size"] = len(cluster_printers_list)
properties[b"cluster_size"] = str(len(cluster_printers_list)).encode("utf-8")
self._onRemoveDevice(instance_name)
self._onAddDevice(instance_name, address, properties)
def _onRemoveDevice(self, device_id):
if device and address in self._manual_instances:
self.getOutputDeviceManager().addOutputDevice(device)
self.addManualDeviceSignal.emit(self.getPluginId(), device.getId(), address, properties)
def _onRemoveDevice(self, device_id: str) -> None:
device = self._discovered_devices.pop(device_id, None)
if device:
if device.isConnected():
@ -295,7 +376,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
except TypeError:
# Disconnect already happened.
pass
self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
self.discoveredDevicesChanged.emit()
def _onAddDevice(self, name, address, properties):
@ -320,7 +401,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
else:
device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)
self._application.getDiscoveredPrintersModel().addDiscoveredPrinter(address, device.getId(), name, self._createMachineFromDiscoveredPrinter, properties[b"printer_type"].decode("utf-8"), device)
self._discovered_devices[device.getId()] = device
self.discoveredDevicesChanged.emit()
@ -408,13 +489,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
return True
## Check if the prerequsites are in place to start the cloud flow
def checkCloudFlowIsPossible(self) -> None:
def checkCloudFlowIsPossible(self, cluster: Optional[CloudOutputDevice]) -> None:
Logger.log("d", "Checking if cloud connection is possible...")
# Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack]
if active_machine:
# Check 1A: Printer isn't already configured for cloud
if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
Logger.log("d", "Active machine was already configured for cloud.")
@ -458,7 +538,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
self._start_cloud_flow_message.show()
def _onCloudPrintingConfigured(self) -> None:
def _onCloudPrintingConfigured(self, device) -> None:
# Hide the cloud flow start message if it was hanging around already
# For example: if the user already had the browser openen and made the association themselves
if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
@ -473,11 +553,20 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
# Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
active_machine = self._application.getMachineManager().activeMachine
if active_machine:
active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
# The active machine _might_ not be the machine that was in the added cloud cluster and
# then this will hide the cloud message for the wrong machine. So we only set it if the
# host names match between the active machine and the newly added cluster
saved_host_name = active_machine.getMetaDataEntry("um_network_key", "").split('.')[0]
added_host_name = device.toDict()["host_name"]
if added_host_name == saved_host_name:
active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
return
def _onDontAskMeAgain(self, checked: bool) -> None:
active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack]
if active_machine:
active_machine.setMetaDataEntry("do_not_show_cloud_message", checked)
if checked:
@ -507,7 +596,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._cloud_flow_complete_message.hide()
# Check for cloud flow again with newly selected machine
self.checkCloudFlowIsPossible()
self.checkCloudFlowIsPossible(None)
def _createCloudFlowStartMessage(self):
self._start_cloud_flow_message = Message(
@ -517,7 +606,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
"resources", "svg", "cloud-flow-start.svg"
)),
image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"),
image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"),
option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
option_state = False
)

View file

@ -5,7 +5,7 @@ from typing import List
from PyQt5.QtCore import pyqtProperty, pyqtSignal
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from .ConfigurationChangeModel import ConfigurationChangeModel

View file

@ -6,7 +6,7 @@ from unittest.mock import patch, MagicMock
from UM.Scene.SceneNode import SceneNode
from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from ...src.Cloud import CloudApiClient
from ...src.Cloud.CloudOutputDevice import CloudOutputDevice
from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
@ -22,6 +22,7 @@ class TestCloudOutputDevice(TestCase):
HOST_NAME = "ultimakersystem-ccbdd30044ec"
HOST_GUID = "e90ae0ac-1257-4403-91ee-a44c9b7e8050"
HOST_VERSION = "5.2.0"
FRIENDLY_NAME = "My Friendly Printer"
STATUS_URL = "{}/connect/v1/clusters/{}/status".format(CuraCloudAPIRoot, CLUSTER_ID)
PRINT_URL = "{}/connect/v1/clusters/{}/print/{}".format(CuraCloudAPIRoot, CLUSTER_ID, JOB_ID)
@ -37,7 +38,8 @@ class TestCloudOutputDevice(TestCase):
patched_method.start()
self.cluster = CloudClusterResponse(self.CLUSTER_ID, self.HOST_GUID, self.HOST_NAME, is_online=True,
status="active", host_version=self.HOST_VERSION)
status="active", host_version=self.HOST_VERSION,
friendly_name=self.FRIENDLY_NAME)
self.network = NetworkManagerMock()
self.account = MagicMock(isLoggedIn=True, accessToken="TestAccessToken")
@ -60,7 +62,7 @@ class TestCloudOutputDevice(TestCase):
# We test for these in order to make sure the correct file type is selected depending on the firmware version.
def test_properties(self):
self.assertEqual(self.device.firmwareVersion, self.HOST_VERSION)
self.assertEqual(self.device.name, self.HOST_NAME)
self.assertEqual(self.device.name, self.FRIENDLY_NAME)
def test_status(self):
self.device._update()

View file

@ -7,6 +7,7 @@ from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager
from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
from ...src.Cloud import CloudApiClient
from ...src.Cloud import CloudOutputDeviceManager
from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
from .Fixtures import parseFixture, readFixture
from .NetworkManagerMock import NetworkManagerMock, FakeSignal
@ -55,7 +56,9 @@ class TestCloudOutputDeviceManager(TestCase):
devices = self.device_manager.getOutputDevices()
# TODO: Check active device
response_clusters = self.clusters_response.get("data", [])
response_clusters = []
for cluster in self.clusters_response.get("data", []):
response_clusters.append(CloudClusterResponse(**cluster).toDict())
manager_clusters = sorted([device.clusterData.toDict() for device in self.manager._remote_clusters.values()],
key=lambda cluster: cluster['cluster_id'], reverse=True)
self.assertEqual(response_clusters, manager_clusters)
@ -97,7 +100,7 @@ class TestCloudOutputDeviceManager(TestCase):
self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected())
self.assertIsNone(self.device_manager.getOutputDevice(cluster2["cluster_id"]))
self.assertEquals([], active_machine_mock.setMetaDataEntry.mock_calls)
self.assertEqual([], active_machine_mock.setMetaDataEntry.mock_calls)
def test_device_connects_by_network_key(self):
active_machine_mock = self.app.getGlobalContainerStack.return_value

View file

@ -208,7 +208,7 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(1, device_mock.createFormPart.call_count)
self.assertEqual(1, device_mock.postFormWithParts.call_count)
self.assertEquals(
self.assertEqual(
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
device_mock.method_calls)
@ -238,7 +238,7 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(1, device_mock.createFormPart.call_count)
self.assertEqual(1, device_mock.postFormWithParts.call_count)
self.assertEquals(
self.assertEqual(
[call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", "<xml></xml>"),
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
device_mock.method_calls)

View file

@ -13,7 +13,7 @@ from time import sleep
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
class AvrFirmwareUpdater(FirmwareUpdater):

View file

@ -10,9 +10,9 @@ from UM.PluginRegistry import PluginRegistry #To get the g-code output.
from UM.Qt.Duration import DurationFormat
from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob
@ -226,6 +226,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except SerialTimeoutException:
Logger.log("w", "Timeout when sending command to printer via USB.")
self._command_received.set()
except SerialException:
Logger.logException("w", "An unexpected exception occurred while writing to the serial.")
self.setConnectionState(ConnectionState.Error)
def _update(self):
while self._connection_state == ConnectionState.Connected and self._serial is not None:
@ -371,10 +374,17 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
progress = (self._gcode_position / len(self._gcode))
print_job = self._printers[0].activePrintJob
try:
progress = self._gcode_position / len(self._gcode)
except ZeroDivisionError:
# There is nothing to send!
if print_job is not None:
print_job.updateState("error")
return
elapsed_time = int(time() - self._print_start_time)
print_job = self._printers[0].activePrintJob
if print_job is None:
controller = GenericOutputController(self)
controller.setCanUpdateFirmware(True)

View file

@ -5,15 +5,13 @@ import threading
import time
import serial.tools.list_ports
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from PyQt5.QtCore import QObject, pyqtSignal
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.i18n import i18nCatalog
from cura.PrinterOutputDevice import ConnectionState
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.PrinterOutputDevice import ConnectionState
from . import USBPrinterOutputDevice

View file

@ -4,7 +4,7 @@
from typing import List
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from UM.FlameProfiler import pyqtSlot

View file

@ -1,24 +1,27 @@
// Copyright (c) 2016 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.2 as UM
import Cura 1.0 as Cura
import UM 1.3 as UM
import Cura 1.1 as Cura
Cura.MachineAction
{
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name: "cura"; }
anchors.fill: parent
Item
{
id: bedLevelMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name: "cura"; }
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height * 3
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width * 3 / 4
Label
{
@ -26,17 +29,24 @@ Cura.MachineAction
width: parent.width
text: catalog.i18nc("@title", "Build Plate Leveling")
wrapMode: Text.WordWrap
font.pointSize: 18;
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.topMargin: UM.Theme.getSize("default_margin").height * 3
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "To make sure your prints will come out great, you can now adjust your buildplate. When you click 'Move to Next Position' the nozzle will move to the different positions that can be adjusted.")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
id: bedlevelingText
@ -45,37 +55,40 @@ Cura.MachineAction
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "For every position; insert a piece of paper under the nozzle and adjust the print build plate height. The print build plate height is right when the paper is slightly gripped by the tip of the nozzle.")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Row
{
id: bedlevelingWrapper
anchors.top: bedlevelingText.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.topMargin: UM.Theme.getSize("default_margin").height * 3
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Button
Cura.ActionButton
{
id: startBedLevelingButton
text: catalog.i18nc("@action:button","Start Build Plate Leveling")
text: catalog.i18nc("@action:button", "Start Build Plate Leveling")
onClicked:
{
startBedLevelingButton.visible = false;
bedlevelingButton.visible = true;
manager.startBedLeveling();
startBedLevelingButton.visible = false
bedlevelingButton.visible = true
manager.startBedLeveling()
}
}
Button
Cura.ActionButton
{
id: bedlevelingButton
text: catalog.i18nc("@action:button","Move to Next Position")
text: catalog.i18nc("@action:button", "Move to Next Position")
visible: false
onClicked:
{
manager.moveToNextLevelPosition();
manager.moveToNextLevelPosition()
}
}
}

View file

@ -1,13 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.MachineAction import MachineAction
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from PyQt5.QtCore import pyqtSignal, pyqtProperty
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Application import Application
from UM.Util import parseBool
from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura")

View file

@ -1,49 +1,46 @@
// Copyright (c) 2016 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import Cura 1.0 as Cura
import UM 1.3 as UM
import Cura 1.1 as Cura
Cura.MachineAction
{
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name: "cura"; }
anchors.fill: parent
Item
{
id: upgradeSelectionMachineAction
anchors.fill: parent
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Select Printer Upgrades")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
anchors.topMargin: UM.Theme.getSize("default_margin").width * 5
anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker 2.");
text: catalog.i18nc("@label", "Please select any upgrades made to this Ultimaker 2.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
CheckBox
Cura.CheckBox
{
id: olssonBlockCheckBox
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
height: UM.Theme.getSize("setting_control").height
text: catalog.i18nc("@label", "Olsson Block")
checked: manager.hasVariants
onClicked: manager.hasVariants = checked
@ -54,7 +51,5 @@ Cura.MachineAction
onHasVariantsChanged: olssonBlockCheckBox.checked = manager.hasVariants
}
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
}
}

View file

@ -1,193 +0,0 @@
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
from UM.Application import Application
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Action to check up if the self-built UMO was done correctly.
class UMOCheckupMachineAction(MachineAction):
def __init__(self):
super().__init__("UMOCheckup", catalog.i18nc("@action", "Checkup"))
self._qml_url = "UMOCheckupMachineAction.qml"
self._hotend_target_temp = 180
self._bed_target_temp = 60
self._output_device = None
self._bed_test_completed = False
self._hotend_test_completed = False
# Endstop tests
self._x_min_endstop_test_completed = False
self._y_min_endstop_test_completed = False
self._z_min_endstop_test_completed = False
self._check_started = False
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
onBedTestCompleted = pyqtSignal()
onHotendTestCompleted = pyqtSignal()
onXMinEndstopTestCompleted = pyqtSignal()
onYMinEndstopTestCompleted = pyqtSignal()
onZMinEndstopTestCompleted = pyqtSignal()
bedTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal()
def _onOutputDevicesChanged(self):
# Check if this action was started, but no output device was found the first time.
# If so, re-try now that an output device has been added/removed.
if self._output_device is None and self._check_started:
self.startCheck()
def _getPrinterOutputDevices(self):
return [printer_output_device for printer_output_device in
Application.getInstance().getOutputDeviceManager().getOutputDevices() if
isinstance(printer_output_device, PrinterOutputDevice)]
def _reset(self):
if self._output_device:
self._output_device.bedTemperatureChanged.disconnect(self.bedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.disconnect(self.hotendTemperatureChanged)
self._output_device.bedTemperatureChanged.disconnect(self._onBedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.disconnect(self._onHotendTemperatureChanged)
self._output_device.endstopStateChanged.disconnect(self._onEndstopStateChanged)
try:
self._output_device.stopPollEndstop()
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while stopping end stop polling: %s" % str(e))
self._output_device = None
self._check_started = False
self.checkStartedChanged.emit()
# Ensure everything is reset (and right signals are emitted again)
self._bed_test_completed = False
self.onBedTestCompleted.emit()
self._hotend_test_completed = False
self.onHotendTestCompleted.emit()
self._x_min_endstop_test_completed = False
self.onXMinEndstopTestCompleted.emit()
self._y_min_endstop_test_completed = False
self.onYMinEndstopTestCompleted.emit()
self._z_min_endstop_test_completed = False
self.onZMinEndstopTestCompleted.emit()
self.heatedBedChanged.emit()
@pyqtProperty(bool, notify = onBedTestCompleted)
def bedTestCompleted(self):
return self._bed_test_completed
@pyqtProperty(bool, notify = onHotendTestCompleted)
def hotendTestCompleted(self):
return self._hotend_test_completed
@pyqtProperty(bool, notify = onXMinEndstopTestCompleted)
def xMinEndstopTestCompleted(self):
return self._x_min_endstop_test_completed
@pyqtProperty(bool, notify=onYMinEndstopTestCompleted)
def yMinEndstopTestCompleted(self):
return self._y_min_endstop_test_completed
@pyqtProperty(bool, notify=onZMinEndstopTestCompleted)
def zMinEndstopTestCompleted(self):
return self._z_min_endstop_test_completed
@pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self):
if not self._output_device:
return 0
return self._output_device.bedTemperature
@pyqtProperty(float, notify=hotendTemperatureChanged)
def hotendTemperature(self):
if not self._output_device:
return 0
return self._output_device.hotendTemperatures[0]
def _onHotendTemperatureChanged(self):
if not self._output_device:
return
if not self._hotend_test_completed:
if self._output_device.hotendTemperatures[0] + 10 > self._hotend_target_temp and self._output_device.hotendTemperatures[0] - 10 < self._hotend_target_temp:
self._hotend_test_completed = True
self.onHotendTestCompleted.emit()
def _onBedTemperatureChanged(self):
if not self._output_device:
return
if not self._bed_test_completed:
if self._output_device.bedTemperature + 5 > self._bed_target_temp and self._output_device.bedTemperature - 5 < self._bed_target_temp:
self._bed_test_completed = True
self.onBedTestCompleted.emit()
def _onEndstopStateChanged(self, switch_type, state):
if state:
if switch_type == "x_min":
self._x_min_endstop_test_completed = True
self.onXMinEndstopTestCompleted.emit()
elif switch_type == "y_min":
self._y_min_endstop_test_completed = True
self.onYMinEndstopTestCompleted.emit()
elif switch_type == "z_min":
self._z_min_endstop_test_completed = True
self.onZMinEndstopTestCompleted.emit()
checkStartedChanged = pyqtSignal()
@pyqtProperty(bool, notify = checkStartedChanged)
def checkStarted(self):
return self._check_started
@pyqtSlot()
def startCheck(self):
self._check_started = True
self.checkStartedChanged.emit()
output_devices = self._getPrinterOutputDevices()
if output_devices:
self._output_device = output_devices[0]
try:
self._output_device.sendCommand("M18") # Turn off all motors so the user can move the axes
self._output_device.startPollEndstop()
self._output_device.bedTemperatureChanged.connect(self.bedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.connect(self.hotendTemperatureChanged)
self._output_device.bedTemperatureChanged.connect(self._onBedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.connect(self._onHotendTemperatureChanged)
self._output_device.endstopStateChanged.connect(self._onEndstopStateChanged)
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while starting end stop polling: %s" % str(e))
@pyqtSlot()
def cooldownHotend(self):
if self._output_device is not None:
self._output_device.setTargetHotendTemperature(0, 0)
@pyqtSlot()
def cooldownBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(0)
@pyqtSlot()
def heatupHotend(self):
if self._output_device is not None:
self._output_device.setTargetHotendTemperature(0, self._hotend_target_temp)
@pyqtSlot()
def heatupBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(self._bed_target_temp)
heatedBedChanged = pyqtSignal()
@pyqtProperty(bool, notify = heatedBedChanged)
def hasHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
return global_container_stack.getProperty("machine_heated_bed", "value")

View file

@ -1,288 +0,0 @@
import UM 1.2 as UM
import Cura 1.0 as Cura
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: checkupMachineAction
anchors.fill: parent;
property int leftRow: (checkupMachineAction.width * 0.40) | 0
property int rightRow: (checkupMachineAction.width * 0.60) | 0
property bool heatupHotendStarted: false
property bool heatupBedStarted: false
property bool printerConnected: Cura.MachineManager.printerConnected
UM.I18nCatalog { id: catalog; name: "cura"}
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Check Printer")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "It's a good idea to do a few sanity checks on your Ultimaker. You can skip this step if you know your machine is functional");
}
Row
{
id: startStopButtons
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Button
{
id: startCheckButton
text: catalog.i18nc("@action:button","Start Printer Check");
onClicked:
{
checkupMachineAction.heatupHotendStarted = false;
checkupMachineAction.heatupBedStarted = false;
manager.startCheck();
startCheckButton.visible = false;
}
}
}
Item
{
id: checkupContent
anchors.top: startStopButtons.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: manager.checkStarted
width: parent.width
height: 250
//////////////////////////////////////////////////////////
Label
{
id: connectionLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: parent.top
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Connection: ")
}
Label
{
id: connectionStatus
width: checkupMachineAction.rightRow
anchors.left: connectionLabel.right
anchors.top: parent.top
wrapMode: Text.WordWrap
text: checkupMachineAction.printerConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected")
}
//////////////////////////////////////////////////////////
Label
{
id: endstopXLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop X: ")
visible: checkupMachineAction.printerConnected
}
Label
{
id: endstopXStatus
width: checkupMachineAction.rightRow
anchors.left: endstopXLabel.right
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.printerConnected
}
//////////////////////////////////////////////////////////////
Label
{
id: endstopYLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Y: ")
visible: checkupMachineAction.printerConnected
}
Label
{
id: endstopYStatus
width: checkupMachineAction.rightRow
anchors.left: endstopYLabel.right
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.printerConnected
}
/////////////////////////////////////////////////////////////////////
Label
{
id: endstopZLabel
width: checkupMachineAction.leftRow
anchors.left: parent.left
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Z: ")
visible: checkupMachineAction.printerConnected
}
Label
{
id: endstopZStatus
width: checkupMachineAction.rightRow
anchors.left: endstopZLabel.right
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.printerConnected
}
////////////////////////////////////////////////////////////
Label
{
id: nozzleTempLabel
width: checkupMachineAction.leftRow
height: nozzleTempButton.height
anchors.left: parent.left
anchors.top: endstopZLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Nozzle temperature check: ")
visible: checkupMachineAction.printerConnected
}
Label
{
id: nozzleTempStatus
width: (checkupMachineAction.rightRow * 0.4) | 0
anchors.top: nozzleTempLabel.top
anchors.left: nozzleTempLabel.right
wrapMode: Text.WordWrap
text: catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.printerConnected
}
Item
{
id: nozzleTempButton
width: (checkupMachineAction.rightRow * 0.3) | 0
height: childrenRect.height
anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.printerConnected
Button
{
text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
onClicked:
{
if (checkupMachineAction.heatupHotendStarted)
{
manager.cooldownHotend()
checkupMachineAction.heatupHotendStarted = false
} else
{
manager.heatupHotend()
checkupMachineAction.heatupHotendStarted = true
}
}
}
}
Label
{
id: nozzleTemp
anchors.top: nozzleTempLabel.top
anchors.left: nozzleTempButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
width: (checkupMachineAction.rightRow * 0.2) | 0
wrapMode: Text.WordWrap
text: manager.hotendTemperature + "°C"
font.bold: true
visible: checkupMachineAction.printerConnected
}
/////////////////////////////////////////////////////////////////////////////
Label
{
id: bedTempLabel
width: checkupMachineAction.leftRow
height: bedTempButton.height
anchors.left: parent.left
anchors.top: nozzleTempLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Build plate temperature check:")
visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
}
Label
{
id: bedTempStatus
width: (checkupMachineAction.rightRow * 0.4) | 0
anchors.top: bedTempLabel.top
anchors.left: bedTempLabel.right
wrapMode: Text.WordWrap
text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
}
Item
{
id: bedTempButton
width: (checkupMachineAction.rightRow * 0.3) | 0
height: childrenRect.height
anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
Button
{
text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
onClicked:
{
if (checkupMachineAction.heatupBedStarted)
{
manager.cooldownBed()
checkupMachineAction.heatupBedStarted = false
} else
{
manager.heatupBed()
checkupMachineAction.heatupBedStarted = true
}
}
}
}
Label
{
id: bedTemp
width: (checkupMachineAction.rightRow * 0.2) | 0
anchors.top: bedTempLabel.top
anchors.left: bedTempButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
wrapMode: Text.WordWrap
text: manager.bedTemperature + "°C"
font.bold: true
visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
}
Label
{
id: resultText
visible: false
anchors.top: bedTemp.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Everything is in order! You're done with your CheckUp.")
}
}
}
}

View file

@ -1,43 +1,39 @@
// Copyright (c) 2016 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import Cura 1.0 as Cura
import UM 1.3 as UM
import Cura 1.1 as Cura
Cura.MachineAction
{
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name: "cura"; }
anchors.fill: parent
Item
{
id: upgradeSelectionMachineAction
anchors.fill: parent
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Select Printer Upgrades")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
anchors.topMargin: UM.Theme.getSize("default_margin").width * 5
anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original");
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
CheckBox
Cura.CheckBox
{
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
@ -46,7 +42,5 @@ Cura.MachineAction
checked: manager.hasHeatedBed
onClicked: manager.setHeatedBed(checked)
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
}
}

View file

@ -1,46 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Extension import Extension
from UM.Logger import Logger
class UserAgreement(QObject, Extension):
def __init__(self, application):
super(UserAgreement, self).__init__()
self._application = application
self._user_agreement_window = None
self._user_agreement_context = None
self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._application.getPreferences().addPreference("general/accepted_user_agreement", False)
def _onEngineCreated(self):
if not self._application.getPreferences().getValue("general/accepted_user_agreement"):
self.showUserAgreement()
def showUserAgreement(self):
if not self._user_agreement_window:
self.createUserAgreementWindow()
self._user_agreement_window.show()
@pyqtSlot(bool)
def didAgree(self, user_choice):
if user_choice:
Logger.log("i", "User agreed to the user agreement")
self._application.getPreferences().setValue("general/accepted_user_agreement", True)
self._user_agreement_window.hide()
else:
Logger.log("i", "User did NOT agree to the user agreement")
self._application.getPreferences().setValue("general/accepted_user_agreement", False)
self._application.quit()
self._application.setNeedToShowUserAgreement(False)
def createUserAgreementWindow(self):
path = os.path.join(self._application.getPluginRegistry().getPluginPath(self.getPluginId()), "UserAgreement.qml")
self._user_agreement_window = self._application.createQmlComponent(path, {"manager": self})

View file

@ -1,63 +0,0 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.3 as UM
UM.Dialog
{
id: baseDialog
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width * 0.75)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height * 0.5)
width: minimumWidth
height: minimumHeight
title: catalog.i18nc("@title:window", "User Agreement")
TextArea
{
anchors.top: parent.top
width: parent.width
anchors.bottom: buttonRow.top
text: ' <center><h3>DISCLAIMER BY ULTIMAKER</h3></center>
<p>PLEASE READ THIS DISCLAIMER CAREFULLY.</p>
<p>EXCEPT WHEN OTHERWISE STATED IN WRITING, ULTIMAKER PROVIDES ANY ULTIMAKER SOFTWARE OR THIRD PARTY SOFTWARE AS IS WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF ULTIMAKER SOFTWARE IS WITH YOU.</p>
<p>UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, IN NO EVENT WILL ULTIMAKER BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE ANY ULTIMAKER SOFTWARE OR THIRD PARTY SOFTWARE.</p>
'
readOnly: true;
textFormat: TextEdit.RichText
}
Item
{
id: buttonRow
anchors.bottom: parent.bottom
width: parent.width
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
UM.I18nCatalog { id: catalog; name: "cura" }
Button
{
anchors.right: parent.right
text: catalog.i18nc("@action:button", "I understand and agree")
onClicked: {
baseDialog.accepted()
}
}
Button
{
anchors.left: parent.left
text: catalog.i18nc("@action:button", "I don't agree")
onClicked: {
baseDialog.rejected()
}
}
}
onAccepted: manager.didAgree(true)
onRejected: manager.didAgree(false)
onClosing: manager.didAgree(false)
}

View file

@ -1,10 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import UserAgreement
def getMetaData():
return {}
def register(app):
return {"extension": UserAgreement.UserAgreement(app)}

View file

@ -1,8 +0,0 @@
{
"name": "UserAgreement",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Ask the user once if he/she agrees with our license.",
"api": "6.0",
"i18n-catalog": "cura"
}

View file

@ -3,7 +3,7 @@ from typing import Tuple, List, Set, Dict
import io
from UM.VersionUpgrade import VersionUpgrade
from cura.PrinterOutputDevice import ConnectionType
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
deleted_settings = {"bridge_wall_max_overhang"} # type: Set[str]
renamed_configurations = {"connect_group_name": "group_name"} # type: Dict[str, str]

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
@ -49,6 +49,15 @@ class VersionUpgrade40to41(VersionUpgrade):
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "7"
# Limit Maximum Deviation instead of Maximum Resolution. This should have approximately the same effect as before the algorithm change, only more consistent.
if "meshfix_maximum_resolution" in parser["values"]:
resolution = parser["values"]["meshfix_maximum_resolution"]
if resolution.startswith("="):
resolution = resolution[1:]
deviation = "=(" + resolution + ") / 2"
parser["values"]["meshfix_maximum_deviation"] = deviation
del parser["values"]["meshfix_maximum_resolution"]
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
@ -62,6 +71,11 @@ class VersionUpgrade40to41(VersionUpgrade):
parser["general"]["version"] = "6"
if "metadata" not in parser:
parser["metadata"] = {}
# Remove changelog plugin
if "latest_version_changelog_shown" in parser["general"]:
del parser["general"]["latest_version_changelog_shown"]
parser["metadata"]["setting_version"] = "7"
result = io.StringIO()

View file

@ -10,21 +10,21 @@ from UM.Math.Color import Color
from UM.PluginRegistry import PluginRegistry
from UM.Platform import Platform
from UM.Event import Event
from UM.View.View import View
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.CuraApplication import CuraApplication
from cura.CuraView import CuraView
from cura.Scene.ConvexHullNode import ConvexHullNode
from . import XRayPass
## View used to display a see-through version of objects with errors highlighted.
class XRayView(View):
class XRayView(CuraView):
def __init__(self):
super().__init__()
super().__init__(parent = None, use_empty_menu_placeholder = True)
self._xray_shader = None
self._xray_pass = None

View file

@ -144,7 +144,7 @@ class XmlMaterialProfile(InstanceContainer):
# setting_version is derived from the "version" tag in the schema, so don't serialize it into a file
if ignored_metadata_keys is None:
ignored_metadata_keys = set()
ignored_metadata_keys |= {"setting_version", "definition", "status", "variant", "type", "base_file", "approximate_diameter", "id", "container_type", "name"}
ignored_metadata_keys |= {"setting_version", "definition", "status", "variant", "type", "base_file", "approximate_diameter", "id", "container_type", "name", "compatible"}
# remove the keys that we want to ignore in the metadata
for key in ignored_metadata_keys:
if key in metadata: