Merge branch 'master' into fix_layer_number_width

This commit is contained in:
Diego Prado Gesto 2018-08-31 10:56:23 +02:00
commit a8cb1e25b4
294 changed files with 5983 additions and 2588 deletions

2
.gitignore vendored
View file

@ -15,6 +15,7 @@ LC_MESSAGES
.cache .cache
*.qmlc *.qmlc
.mypy_cache .mypy_cache
.pytest_cache
#MacOS #MacOS
.DS_Store .DS_Store
@ -25,6 +26,7 @@ LC_MESSAGES
*.lprof *.lprof
*~ *~
*.qm *.qm
.directory
.idea .idea
cura.desktop cura.desktop

View file

@ -13,6 +13,7 @@ from cura.Backups.BackupsManager import BackupsManager
# api = CuraAPI() # api = CuraAPI()
# api.backups.createBackup() # api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` # api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: class Backups:
manager = BackupsManager() # Re-used instance of the backups manager. manager = BackupsManager() # Re-used instance of the backups manager.

View file

@ -0,0 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.CuraApplication import CuraApplication
## The Interface.Settings API provides a version-proof bridge between Cura's
# (currently) sidebar UI and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.getContextMenuItems()
# data = {
# "name": "My Plugin Action",
# "iconName": "my-plugin-icon",
# "actions": my_menu_actions,
# "menu_item": MyPluginAction(self)
# }
# api.interface.settings.addContextMenuItem(data)``
class Settings:
# Re-used instance of Cura:
application = CuraApplication.getInstance() # type: CuraApplication
## Add items to the sidebar context menu.
# \param menu_item dict containing the menu item to add.
def addContextMenuItem(self, menu_item: dict) -> None:
self.application.addSidebarCustomMenuItem(menu_item)
## Get all custom items currently added to the sidebar context menu.
# \return List containing all custom context menu items.
def getContextMenuItems(self) -> list:
return self.application.getSidebarCustomMenuItems()

View file

@ -0,0 +1,24 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.PluginRegistry import PluginRegistry
from cura.API.Interface.Settings import Settings
## The Interface class serves as a common root for the specific API
# methods for each interface element.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.addContextMenuItem()
# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
# # etc.``
class Interface:
# For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion
# API methods specific to the settings portion of the UI
settings = Settings()

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups from cura.API.Backups import Backups
from cura.API.Interface import Interface
## The official Cura API that plug-ins can use to interact with Cura. ## The official Cura API that plug-ins can use to interact with Cura.
# #
@ -9,10 +10,14 @@ from cura.API.Backups import Backups
# this API provides a version-safe interface with proper deprecation warnings # this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause # etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable. # plug-ins to be unstable.
class CuraAPI: class CuraAPI:
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
# Backups API. # Backups API
backups = Backups() backups = Backups()
# Interface API
interface = Interface()

View file

@ -3,6 +3,7 @@
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Application import Application #To modify the maximum zoom level.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Scene.Platform import Platform from UM.Scene.Platform import Platform
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
@ -170,6 +171,12 @@ class BuildVolume(SceneNode):
if shape: if shape:
self._shape = shape self._shape = shape
## Get the length of the 3D diagonal through the build volume.
#
# This gives a sense of the scale of the build volume in general.
def getDiagonalSize(self) -> float:
return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
def getDisallowedAreas(self) -> List[Polygon]: def getDisallowedAreas(self) -> List[Polygon]:
return self._disallowed_areas return self._disallowed_areas
@ -235,6 +242,8 @@ class BuildVolume(SceneNode):
# Mark the node as outside build volume if the set extruder is disabled # Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if extruder_position not in self._global_container_stack.extruders:
continue
if not self._global_container_stack.extruders[extruder_position].isEnabled: if not self._global_container_stack.extruders[extruder_position].isEnabled:
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
continue continue
@ -552,6 +561,12 @@ class BuildVolume(SceneNode):
if self._engine_ready: if self._engine_ready:
self.rebuild() self.rebuild()
camera = Application.getInstance().getController().getCameraTool()
if camera:
diagonal = self.getDiagonalSize()
if diagonal > 1:
camera.setZoomRange(min = 0.1, max = diagonal * 5) #You can zoom out up to 5 times the diagonal. This gives some space around the volume.
def _onEngineCreated(self): def _onEngineCreated(self):
self._engine_ready = True self._engine_ready = True
self.rebuild() self.rebuild()

View file

@ -4,15 +4,20 @@ from PyQt5.QtCore import QSize
from UM.Application import Application from UM.Application import Application
class CameraImageProvider(QQuickImageProvider): class CameraImageProvider(QQuickImageProvider):
def __init__(self): def __init__(self):
QQuickImageProvider.__init__(self, QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)
## Request a new image. ## Request a new image.
def requestImage(self, id, size): def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try: try:
return output_device.activePrinter.camera.getImage(), QSize(15, 15) image = output_device.activePrinter.camera.getImage()
if image.isNull():
image = QImage()
return image, QSize(15, 15)
except AttributeError: except AttributeError:
pass pass
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View file

@ -50,7 +50,8 @@ class CuraActions(QObject):
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
camera = scene.getActiveCamera() camera = scene.getActiveCamera()
if camera: if camera:
camera.setPosition(Vector(-80, 250, 700)) diagonal_size = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getDiagonalSize()
camera.setPosition(Vector(-80, 250, 700) * diagonal_size / 375)
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))

View file

@ -1,11 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING, Optional from typing import cast, TYPE_CHECKING
import numpy import numpy
@ -68,9 +67,9 @@ from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.MachineManagementModel import MachineManagementModel from cura.Machines.Models.MachineManagementModel import MachineManagementModel
@ -104,6 +103,8 @@ from cura.Settings.UserChangesModel import UserChangesModel
from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
import cura.Settings.cura_empty_instance_containers
from cura.ObjectsModel import ObjectsModel from cura.ObjectsModel import ObjectsModel
@ -117,11 +118,12 @@ if TYPE_CHECKING:
numpy.seterr(all = "ignore") numpy.seterr(all = "ignore")
try: try:
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
except ImportError: except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value] CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = "" CuraBuildType = ""
CuraDebugMode = False CuraDebugMode = False
CuraSDKVersion = ""
class CuraApplication(QtApplication): class CuraApplication(QtApplication):
@ -213,7 +215,6 @@ class CuraApplication(QtApplication):
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
self._preferred_mimetype = ""
self._i18n_catalog = None self._i18n_catalog = None
self._currently_loading_files = [] self._currently_loading_files = []
@ -226,6 +227,10 @@ class CuraApplication(QtApplication):
self._need_to_show_user_agreement = True self._need_to_show_user_agreement = True
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
self._plugins_loaded = False
# Backups # Backups
self._auto_save = None self._auto_save = None
self._save_data_enabled = True self._save_data_enabled = True
@ -362,42 +367,23 @@ class CuraApplication(QtApplication):
# Add empty variant, material and quality containers. # Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # We need them to simplify the switching between materials.
empty_container = self._container_registry.getEmptyInstanceContainer() self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
self.empty_container = empty_container
empty_definition_changes_container = copy.deepcopy(empty_container) self._container_registry.addContainer(
empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes") cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes") self.empty_definition_changes_container = cura.Settings.cura_empty_instance_containers.empty_definition_changes_container
self._container_registry.addContainer(empty_definition_changes_container)
self.empty_definition_changes_container = empty_definition_changes_container
empty_variant_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
empty_variant_container.setMetaDataEntry("id", "empty_variant") self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
empty_variant_container.setMetaDataEntry("type", "variant")
self._container_registry.addContainer(empty_variant_container)
self.empty_variant_container = empty_variant_container
empty_material_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
empty_material_container.setMetaDataEntry("id", "empty_material") self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
empty_material_container.setMetaDataEntry("type", "material")
self._container_registry.addContainer(empty_material_container)
self.empty_material_container = empty_material_container
empty_quality_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_container)
empty_quality_container.setMetaDataEntry("id", "empty_quality") self.empty_quality_container = cura.Settings.cura_empty_instance_containers.empty_quality_container
empty_quality_container.setName("Not Supported")
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_container.setMetaDataEntry("type", "quality")
empty_quality_container.setMetaDataEntry("supported", False)
self._container_registry.addContainer(empty_quality_container)
self.empty_quality_container = empty_quality_container
empty_quality_changes_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes") self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
self._container_registry.addContainer(empty_quality_changes_container)
self.empty_quality_changes_container = empty_quality_changes_container
# Initializes the version upgrade manager with by providing the paths for each resource type and the latest # Initializes the version upgrade manager with by providing the paths for each resource type and the latest
# versions. # versions.
@ -494,7 +480,9 @@ class CuraApplication(QtApplication):
preferences.addPreference("view/filter_current_build_plate", False) preferences.addPreference("view/filter_current_build_plate", False)
preferences.addPreference("cura/sidebar_collapsed", False) preferences.addPreference("cura/sidebar_collapsed", False)
self._need_to_show_user_agreement = not self.getPreferences().getValue("general/accepted_user_agreement") preferences.addPreference("cura/favorite_materials", ";".join([]))
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
for key in [ for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
@ -508,9 +496,6 @@ class CuraApplication(QtApplication):
self.applicationShuttingDown.connect(self.saveSettings) self.applicationShuttingDown.connect(self.saveSettings)
self.engineCreatedSignal.connect(self._onEngineCreated) self.engineCreatedSignal.connect(self._onEngineCreated)
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._onGlobalContainerChanged()
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
CuraApplication.Created = True CuraApplication.Created = True
@ -625,7 +610,7 @@ class CuraApplication(QtApplication):
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self): def saveSettings(self):
if not self.started or not self._save_data_enabled: if not self.started or not self._save_data_enabled:
# Do not do saving during application start or when data should not be safed on quit. # Do not do saving during application start or when data should not be saved on quit.
return return
ContainerRegistry.getInstance().saveDirtyContainers() ContainerRegistry.getInstance().saveDirtyContainers()
self.savePreferences() self.savePreferences()
@ -774,7 +759,10 @@ class CuraApplication(QtApplication):
# Initialize camera # Initialize camera
root = controller.getScene().getRoot() root = controller.getScene().getRoot()
camera = Camera("3d", root) camera = Camera("3d", root)
camera.setPosition(Vector(-80, 250, 700)) diagonal = self.getBuildVolume().getDiagonalSize()
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
diagonal = 375
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))
controller.getScene().setActiveCamera("3d") controller.getScene().setActiveCamera("3d")
@ -908,6 +896,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
@ -924,9 +913,9 @@ class CuraApplication(QtApplication):
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel") qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel") qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
@ -942,6 +931,7 @@ class CuraApplication(QtApplication):
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel") qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@ -989,30 +979,14 @@ class CuraApplication(QtApplication):
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition()) self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
self._camera_animation.start() self._camera_animation.start()
def _onGlobalContainerChanged(self):
if self._global_container_stack is not None:
machine_file_formats = [file_type.strip() for file_type in self._global_container_stack.getMetaDataEntry("file_formats").split(";")]
new_preferred_mimetype = ""
if machine_file_formats:
new_preferred_mimetype = machine_file_formats[0]
if new_preferred_mimetype != self._preferred_mimetype:
self._preferred_mimetype = new_preferred_mimetype
self.preferredOutputMimetypeChanged.emit()
requestAddPrinter = pyqtSignal() requestAddPrinter = pyqtSignal()
activityChanged = pyqtSignal() activityChanged = pyqtSignal()
sceneBoundingBoxChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal()
preferredOutputMimetypeChanged = pyqtSignal()
@pyqtProperty(bool, notify = activityChanged) @pyqtProperty(bool, notify = activityChanged)
def platformActivity(self): def platformActivity(self):
return self._platform_activity return self._platform_activity
@pyqtProperty(str, notify=preferredOutputMimetypeChanged)
def preferredOutputMimetype(self):
return self._preferred_mimetype
@pyqtProperty(str, notify = sceneBoundingBoxChanged) @pyqtProperty(str, notify = sceneBoundingBoxChanged)
def getSceneBoundingBoxString(self): def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
@ -1599,12 +1573,13 @@ class CuraApplication(QtApplication):
def _readMeshFinished(self, job): def _readMeshFinished(self, job):
nodes = job.getResult() nodes = job.getResult()
filename = job.getFileName() file_name = job.getFileName()
self._currently_loading_files.remove(filename) file_name_lower = file_name.lower()
file_extension = file_name_lower.split(".")[-1]
self._currently_loading_files.remove(file_name)
self.fileLoaded.emit(filename) self.fileLoaded.emit(file_name)
arrange_objects_on_load = not self.getPreferences().getValue("cura/use_multi_build_plate") target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
root = self.getController().getScene().getRoot() root = self.getController().getScene().getRoot()
fixed_nodes = [] fixed_nodes = []
@ -1635,15 +1610,11 @@ class CuraApplication(QtApplication):
node.scale(original_node.getScale()) node.scale(original_node.getScale())
node.setSelectable(True) node.setSelectable(True)
node.setName(os.path.basename(filename)) node.setName(os.path.basename(file_name))
self.getBuildVolume().checkBoundsAndUpdate(node) self.getBuildVolume().checkBoundsAndUpdate(node)
is_non_sliceable = False is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
filename_lower = filename.lower()
for extension in self._non_sliceable_extensions:
if filename_lower.endswith(extension):
is_non_sliceable = True
break
if is_non_sliceable: if is_non_sliceable:
self.callLater(lambda: self.getController().setActiveView("SimulationView")) self.callLater(lambda: self.getController().setActiveView("SimulationView"))
@ -1662,7 +1633,7 @@ class CuraApplication(QtApplication):
if not child.getDecorator(ConvexHullDecorator): if not child.getDecorator(ConvexHullDecorator):
child.addDecorator(ConvexHullDecorator()) child.addDecorator(ConvexHullDecorator())
if arrange_objects_on_load: if file_extension != "3mf":
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
# Only check position if it's not already blatantly obvious that it won't fit. # Only check position if it's not already blatantly obvious that it won't fit.
if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
@ -1696,7 +1667,7 @@ class CuraApplication(QtApplication):
if select_models_on_load: if select_models_on_load:
Selection.add(node) Selection.add(node)
self.fileCompleted.emit(filename) self.fileCompleted.emit(file_name)
def addNonSliceableExtension(self, extension): def addNonSliceableExtension(self, extension):
self._non_sliceable_extensions.append(extension) self._non_sliceable_extensions.append(extension)
@ -1732,3 +1703,10 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def showMoreInformationDialogForAnonymousDataCollection(self): def showMoreInformationDialogForAnonymousDataCollection(self):
cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog() cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog()
def addSidebarCustomMenuItem(self, menu_item: dict) -> None:
self._sidebar_custom_menu_items.append(menu_item)
def getSidebarCustomMenuItems(self) -> list:
return self._sidebar_custom_menu_items

View file

@ -21,7 +21,7 @@ class LayerPolygon:
__number_of_types = 11 __number_of_types = 11
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
## LayerPolygon, used in ProcessSlicedLayersJob ## LayerPolygon, used in ProcessSlicedLayersJob
# \param extruder # \param extruder
# \param line_types array with line_types # \param line_types array with line_types

View file

@ -4,8 +4,8 @@
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import copy import copy
import uuid import uuid
from typing import Dict, cast import json
from typing import Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -18,6 +18,7 @@ from UM.Util import parseBool
from .MaterialNode import MaterialNode from .MaterialNode import MaterialNode
from .MaterialGroup import MaterialGroup from .MaterialGroup import MaterialGroup
from .VariantType import VariantType
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
@ -38,7 +39,8 @@ if TYPE_CHECKING:
# #
class MaterialManager(QObject): class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
def __init__(self, container_registry, parent = None): def __init__(self, container_registry, parent = None):
super().__init__(parent) super().__init__(parent)
@ -47,7 +49,7 @@ class MaterialManager(QObject):
self._fallback_materials_map = dict() # material_type -> generic material metadata self._fallback_materials_map = dict() # material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup self._material_group_map = dict() # root_material_id -> MaterialGroup
self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode) self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# We're using these two maps to convert between the specific diameter material id and the generic material id # We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
@ -75,6 +77,8 @@ class MaterialManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
self._favorites = set()
def initialize(self): def initialize(self):
# Find all materials and put them in a matrix for quick search. # Find all materials and put them in a matrix for quick search.
material_metadatas = {metadata["id"]: metadata for metadata in material_metadatas = {metadata["id"]: metadata for metadata in
@ -187,52 +191,83 @@ class MaterialManager(QObject):
self._diameter_material_map[root_material_id] = default_root_material_id self._diameter_material_map[root_material_id] = default_root_material_id
# Map #4 # Map #4
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict()
self._diameter_machine_variant_material_map = dict()
for material_metadata in material_metadatas.values(): for material_metadata in material_metadatas.values():
# We don't store empty material in the lookup tables self.__addMaterialMetadataIntoLookupTree(material_metadata)
if material_metadata["id"] == "empty_material":
continue
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"]
if approximate_diameter not in self._diameter_machine_variant_material_map:
self._diameter_machine_variant_material_map[approximate_diameter] = {}
machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
if definition not in machine_variant_material_map:
machine_variant_material_map[definition] = MaterialNode()
machine_node = machine_variant_material_map[definition]
variant_name = material_metadata.get("variant_name")
if not variant_name:
# if there is no variant, this material is for the machine, so put its metadata in the machine node.
machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# this material is variant-specific, so we save it in a variant-specific node under the
# machine-specific node
# Check first if the variant exist in the manager
existing_variant = self._application.getVariantManager().getVariantNode(definition, variant_name)
if existing_variant is not None:
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = MaterialNode()
variant_node = machine_node.children_map[variant_name]
if root_material_id in variant_node.material_map: # We shouldn't have duplicated variant-specific materials for the same machine.
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
continue
variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# Add this container id to the wrong containers list in the registry
Logger.log("w", "Not adding {id} to the material manager because the variant does not exist.".format(id = material_metadata["id"]))
self._container_registry.addWrongContainerId(material_metadata["id"])
self.materialsUpdated.emit() self.materialsUpdated.emit()
favorites = self._application.getPreferences().getValue("cura/favorite_materials")
for item in favorites.split(";"):
self._favorites.add(item)
self.favoritesUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None:
material_id = material_metadata["id"]
# We don't store empty material in the lookup tables
if material_id == "empty_material":
return
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"]
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
approximate_diameter]
if definition not in machine_nozzle_buildplate_material_map:
machine_nozzle_buildplate_material_map[definition] = MaterialNode()
# This is a list of information regarding the intermediate nodes:
# nozzle -> buildplate
nozzle_name = material_metadata.get("variant_name")
buildplate_name = material_metadata.get("buildplate_name")
intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
(buildplate_name, VariantType.BUILD_PLATE),
]
variant_manager = self._application.getVariantManager()
machine_node = machine_nozzle_buildplate_material_map[definition]
current_node = machine_node
current_intermediate_node_info_idx = 0
error_message = None # type: Optional[str]
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
if variant_name is not None:
# The new material has a specific variant, so it needs to be added to that specific branch in the tree.
variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
if variant is None:
error_message = "Material {id} contains a variant {name} that does not exist.".format(
id = material_metadata["id"], name = variant_name)
break
# Update the current node to advance to a more specific branch
if variant_name not in current_node.children_map:
current_node.children_map[variant_name] = MaterialNode()
current_node = current_node.children_map[variant_name]
current_intermediate_node_info_idx += 1
if error_message is not None:
Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
self._container_registry.addWrongContainerId(material_metadata["id"])
return
# Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
# Sanity check: Make sure that there is no duplicated materials.
if root_material_id in current_node.material_map:
Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
material_id, root_material_id)
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
return
current_node.material_map[root_material_id] = MaterialNode(material_metadata)
def _updateMaps(self): def _updateMaps(self):
Logger.log("i", "Updating material lookup data ...") Logger.log("i", "Updating material lookup data ...")
self.initialize() self.initialize()
@ -263,45 +298,52 @@ class MaterialManager(QObject):
# #
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
# #
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str], def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
diameter: float) -> Dict[str, MaterialNode]: buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
# round the diameter to get the approximate diameter # round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter)) rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_variant_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
return dict() return dict()
machine_definition_id = machine_definition.getId() machine_definition_id = machine_definition.getId()
# If there are variant materials, get the variant material # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
machine_node = machine_variant_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
variant_node = None nozzle_node = None
if extruder_variant_name is not None and machine_node is not None: buildplate_node = None
variant_node = machine_node.getChildNode(extruder_variant_name) if nozzle_name is not None and machine_node is not None:
nozzle_node = machine_node.getChildNode(nozzle_name)
# Get buildplate node if possible
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [variant_node, machine_node, default_machine_node] nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. variant-specific material # 1. buildplate-specific material
# 2. machine-specific material # 2. nozzle-specific material
# 3. generic material (for fdmprinter) # 3. machine-specific material
# 4. generic material (for fdmprinter)
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
for node in nodes_to_check: for current_node in nodes_to_check:
if node is not None: if current_node is None:
# Only exclude the materials that are explicitly specified in the "exclude_materials" field. continue
# Do not exclude other materials that are of the same type.
for material_id, node in node.material_map.items():
if material_id in machine_exclude_materials:
Logger.log("d", "Exclude material [%s] for machine [%s]",
material_id, machine_definition.getId())
continue
if material_id not in material_id_metadata_dict: # Only exclude the materials that are explicitly specified in the "exclude_materials" field.
material_id_metadata_dict[material_id] = node # Do not exclude other materials that are of the same type.
for material_id, node in current_node.material_map.items():
if material_id in machine_exclude_materials:
Logger.log("d", "Exclude material [%s] for machine [%s]",
material_id, machine_definition.getId())
continue
if material_id not in material_id_metadata_dict:
material_id_metadata_dict[material_id] = node
return material_id_metadata_dict return material_id_metadata_dict
@ -310,13 +352,14 @@ class MaterialManager(QObject):
# #
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[dict]: extruder_stack: "ExtruderStack") -> Optional[dict]:
variant_name = None buildplate_name = machine.getBuildplateName()
nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant": if extruder_stack.variant.getId() != "empty_variant":
variant_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
diameter = extruder_stack.approximateMaterialDiameter diameter = extruder_stack.approximateMaterialDiameter
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup. # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine.definition, variant_name, diameter) return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
# #
# Gets MaterialNode for the given extruder and machine with the given material name. # Gets MaterialNode for the given extruder and machine with the given material name.
@ -324,32 +367,36 @@ class MaterialManager(QObject):
# 1. the given machine doesn't have materials; # 1. the given machine doesn't have materials;
# 2. cannot find any material InstanceContainers with the given settings. # 2. cannot find any material InstanceContainers with the given settings.
# #
def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str], def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
# round the diameter to get the approximate diameter # round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter)) rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_variant_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
diameter, rounded_diameter, root_material_id) diameter, rounded_diameter, root_material_id)
return None return None
# If there are variant materials, get the variant material # If there are nozzle materials, get the nozzle-specific material
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
machine_node = machine_variant_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
variant_node = None nozzle_node = None
buildplate_node = None
# Fallback for "fdmprinter" if the machine-specific materials cannot be found # Fallback for "fdmprinter" if the machine-specific materials cannot be found
if machine_node is None: if machine_node is None:
machine_node = machine_variant_material_map.get(self._default_machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
if machine_node is not None and extruder_variant_name is not None: if machine_node is not None and nozzle_name is not None:
variant_node = machine_node.getChildNode(extruder_variant_name) nozzle_node = machine_node.getChildNode(nozzle_name)
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. variant-specific material # 1. buildplate-specific material
# 2. machine-specific material # 2. nozzle-specific material
# 3. generic material (for fdmprinter) # 3. machine-specific material
nodes_to_check = [variant_node, machine_node, # 4. generic material (for fdmprinter)
machine_variant_material_map.get(self._default_machine_definition_id)] nodes_to_check = [buildplate_node, nozzle_node, machine_node,
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
material_node = None material_node = None
for node in nodes_to_check: for node in nodes_to_check:
@ -366,7 +413,8 @@ class MaterialManager(QObject):
# 1. the given machine doesn't have materials; # 1. the given machine doesn't have materials;
# 2. cannot find any material InstanceContainers with the given settings. # 2. cannot find any material InstanceContainers with the given settings.
# #
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]: def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
node = None node = None
machine_definition = global_stack.definition machine_definition = global_stack.definition
extruder_definition = global_stack.extruders[position].definition extruder_definition = global_stack.extruders[position].definition
@ -385,7 +433,7 @@ class MaterialManager(QObject):
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
return None return None
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id) material_diameter, root_material_id)
return node return node
@ -413,13 +461,17 @@ class MaterialManager(QObject):
else: else:
return None return None
## Get default material for given global stack, extruder position and extruder variant name ## Get default material for given global stack, extruder position and extruder nozzle name
# you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, extruder_variant_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
node = None node = None
buildplate_name = global_stack.getBuildplateName()
machine_definition = global_stack.definition machine_definition = global_stack.definition
if extruder_definition is None: if extruder_definition is None:
extruder_definition = global_stack.extruders[position].definition extruder_definition = global_stack.extruders[position].definition
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
# At this point the extruder_definition is not None # At this point the extruder_definition is not None
material_diameter = extruder_definition.getProperty("material_diameter", "value") material_diameter = extruder_definition.getProperty("material_diameter", "value")
@ -428,7 +480,7 @@ class MaterialManager(QObject):
approximate_material_diameter = str(round(material_diameter)) approximate_material_diameter = str(round(material_diameter))
root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = machine_definition.getMetaDataEntry("preferred_material")
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id) material_diameter, root_material_id)
return node return node
@ -515,8 +567,8 @@ class MaterialManager(QObject):
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition") new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant_name"): if container_to_copy.getMetaDataEntry("variant_name"):
variant_name = container_to_copy.getMetaDataEntry("variant_name") nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
new_id += "_" + variant_name.replace(" ", "_") new_id += "_" + nozzle_name.replace(" ", "_")
new_container = copy.deepcopy(container_to_copy) new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id new_container.getMetaData()["id"] = new_id
@ -565,3 +617,25 @@ class MaterialManager(QObject):
new_base_id = new_id, new_base_id = new_id,
new_metadata = new_metadata) new_metadata = new_metadata)
return new_id return new_id
@pyqtSlot(str)
def addFavorite(self, root_material_id: str):
self._favorites.add(root_material_id)
self.favoritesUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str):
self._favorites.remove(root_material_id)
self.favoritesUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot()
def getFavorites(self):
return self._favorites

View file

@ -2,45 +2,63 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Application import Application
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
# ## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
# This is the base model class for GenericMaterialsModel and BrandMaterialsModel # Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. # The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top # bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
#
class BaseMaterialsModel(ListModel): class BaseMaterialsModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
IdRole = Qt.UserRole + 2
NameRole = Qt.UserRole + 3
BrandRole = Qt.UserRole + 4
MaterialRole = Qt.UserRole + 5
ColorRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
extruderPositionChanged = pyqtSignal() extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self.addRoleName(self.RootMaterialIdRole, "root_material_id") from cura.CuraApplication import CuraApplication
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name") self._application = CuraApplication.getInstance()
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialRole, "material") # Make these managers available to all material models
self.addRoleName(self.ColorRole, "color_name") self._container_registry = self._application.getInstance().getContainerRegistry()
self.addRoleName(self.ContainerNodeRole, "container_node") self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
# Update this model when switching machines
self._machine_manager.activeStackChanged.connect(self._update)
# Update this model when list of materials changes
self._material_manager.materialsUpdated.connect(self._update)
# Update this model when list of favorites changes
self._material_manager.favoritesUpdated.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id")
self.addRoleName(Qt.UserRole + 3, "GUID")
self.addRoleName(Qt.UserRole + 4, "name")
self.addRoleName(Qt.UserRole + 5, "brand")
self.addRoleName(Qt.UserRole + 6, "description")
self.addRoleName(Qt.UserRole + 7, "material")
self.addRoleName(Qt.UserRole + 8, "color_name")
self.addRoleName(Qt.UserRole + 9, "color_code")
self.addRoleName(Qt.UserRole + 10, "density")
self.addRoleName(Qt.UserRole + 11, "diameter")
self.addRoleName(Qt.UserRole + 12, "approximate_diameter")
self.addRoleName(Qt.UserRole + 13, "adhesion_info")
self.addRoleName(Qt.UserRole + 14, "is_read_only")
self.addRoleName(Qt.UserRole + 15, "container_node")
self.addRoleName(Qt.UserRole + 16, "is_favorite")
self._extruder_position = 0 self._extruder_position = 0
self._extruder_stack = None self._extruder_stack = None
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) self._available_materials = None
self._favorite_ids = None
def _updateExtruderStack(self): def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
@ -65,8 +83,55 @@ class BaseMaterialsModel(ListModel):
def extruderPosition(self) -> int: def extruderPosition(self) -> int:
return self._extruder_position return self._extruder_position
# ## This is an abstract method that needs to be implemented by the specific
# This is an abstract method that needs to be implemented by # models themselves.
#
def _update(self): def _update(self):
pass pass
## This method is used by all material models in the beginning of the
# _update() method in order to prevent errors. It's the same in all models
# so it's placed here for easy access.
def _canUpdate(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
return False
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
return False
extruder_stack = global_stack.extruders[extruder_position]
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None:
return False
return True
## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node):
metadata = container_node.metadata
item = {
"root_material_id": root_material_id,
"id": metadata["id"],
"container_id": metadata["id"], # TODO: Remove duplicate in material manager qml
"GUID": metadata["GUID"],
"name": metadata["name"],
"brand": metadata["brand"],
"description": metadata["description"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"color_code": metadata.get("color_code", ""),
"density": metadata.get("properties", {}).get("density", ""),
"diameter": metadata.get("properties", {}).get("diameter", ""),
"approximate_diameter": metadata["approximate_diameter"],
"adhesion_info": metadata["adhesion_info"],
"is_read_only": self._container_registry.isReadOnly(metadata["id"]),
"container_node": container_node,
"is_favorite": root_material_id in self._favorite_ids
}
return item

View file

@ -1,157 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
#
# This is an intermediate model to group materials with different colours for a same brand and type.
#
class MaterialsModelGroupedByType(ListModel):
NameRole = Qt.UserRole + 1
ColorsRole = Qt.UserRole + 2
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ColorsRole, "colors")
#
# This model is used to show branded materials in the material drop down menu.
# The structure of the menu looks like this:
# Brand -> Material Type -> list of materials
#
# To illustrate, a branded material menu may look like this:
# Ultimaker -> PLA -> Yellow PLA
# -> Black PLA
# -> ...
# -> ABS -> White ABS
# ...
#
class BrandMaterialsModel(ListModel):
NameRole = Qt.UserRole + 1
MaterialsRole = Qt.UserRole + 2
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.MaterialsRole, "materials")
self._extruder_position = 0
self._extruder_stack = None
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
self._update()
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
return
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.connect(self._update)
# Force update the model when the extruder stack changes
self._update()
def setExtruderPosition(self, position: int):
if self._extruder_stack is None or self._extruder_position != position:
self._extruder_position = position
self._updateExtruderStack()
self.extruderPositionChanged.emit()
@pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged)
def extruderPosition(self) -> int:
return self._extruder_position
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[str(self._extruder_position)]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
extruder_stack)
if available_material_dict is None:
self.setItems([])
return
brand_item_list = []
brand_group_dict = {}
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
brand = metadata["brand"]
# Only add results for generic materials
if brand.lower() == "generic":
continue
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
material_type = metadata["material"]
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
item = {"root_material_id": root_material_id,
"id": metadata["id"],
"name": metadata["name"],
"brand": metadata["brand"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"container_node": container_node
}
brand_group_dict[brand][material_type].append(item)
for brand, material_dict in brand_group_dict.items():
brand_item = {"name": brand,
"materials": MaterialsModelGroupedByType(self)}
material_type_item_list = []
for material_type, material_list in material_dict.items():
material_type_item = {"name": material_type,
"colors": BaseMaterialsModel(self)}
material_type_item["colors"].clear()
# Sort materials by name
material_list = sorted(material_list, key = lambda x: x["name"].upper())
material_type_item["colors"].setItems(material_list)
material_type_item_list.append(material_type_item)
# Sort material type by name
material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"].upper())
brand_item["materials"].setItems(material_type_item_list)
brand_item_list.append(brand_item)
# Sort brand by name
brand_item_list = sorted(brand_item_list, key = lambda x: x["name"].upper())
self.setItems(brand_item_list)

View file

@ -8,7 +8,7 @@ from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.VariantManager import VariantType from cura.Machines.VariantType import VariantType
class BuildPlateModel(ListModel): class BuildPlateModel(ListModel):

View file

@ -0,0 +1,42 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None):
super().__init__(parent)
self._update()
def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate():
self.setItems([])
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
continue
# Only add results for favorite materials
if root_material_id not in self._favorite_ids:
continue
item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item)
# Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["brand"].upper())
self.setItems(item_list)

View file

@ -4,63 +4,39 @@
from UM.Logger import Logger from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class GenericMaterialsModel(BaseMaterialsModel): class GenericMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
self._update() self._update()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine # Perform standard check and reset if the check fails
if global_stack is None: if not self._canUpdate():
self.setItems([]) self.setItems([])
return return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[extruder_position]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, # Get updated list of favorites
extruder_stack) self._favorite_ids = self._material_manager.getFavorites()
if available_material_dict is None:
self.setItems([])
return
item_list = [] item_list = []
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
# Only add results for generic materials for root_material_id, container_node in self._available_materials.items():
if metadata["brand"].lower() != "generic": metadata = container_node.metadata
continue
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)): if bool(metadata.get("removed", False)):
continue continue
item = {"root_material_id": root_material_id, # Only add results for generic materials
"id": metadata["id"], if metadata["brand"].lower() != "generic":
"name": metadata["name"], continue
"brand": metadata["brand"],
"material": metadata["material"], item = self._createMaterialItem(root_material_id, container_node)
"color_name": metadata["color_name"],
"container_node": container_node
}
item_list.append(item) item_list.append(item)
# Sort the item list by material name alphabetically # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["name"].upper()) item_list = sorted(item_list, key = lambda d: d["name"].upper())
self.setItems(item_list) self.setItems(item_list)

View file

@ -0,0 +1,107 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class MaterialTypesModel(ListModel):
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "colors")
class MaterialBrandsModel(BaseMaterialsModel):
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "material_types")
self._update()
def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate():
self.setItems([])
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
brand_item_list = []
brand_group_dict = {}
# Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
continue
# Add brands we haven't seen yet to the dict, skipping generics
brand = metadata["brand"]
if brand.lower() == "generic":
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict
material_type = metadata["material"]
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
# Now handle the individual materials
item = self._createMaterialItem(root_material_id, container_node)
brand_group_dict[brand][material_type].append(item)
# Part 2: Organize the tree into models
#
# Normally, the structure of the menu looks like this:
# Brand -> Material Type -> Specific Material
#
# To illustrate, a branded material menu may look like this:
# Ultimaker ┳ PLA ┳ Yellow PLA
# ┃ ┣ Black PLA
# ┃ ┗ ...
# ┃
# ┗ ABS ┳ White ABS
# ┗ ...
for brand, material_dict in brand_group_dict.items():
material_type_item_list = []
brand_item = {
"name": brand,
"material_types": MaterialTypesModel(self)
}
for material_type, material_list in material_dict.items():
material_type_item = {
"name": material_type,
"colors": BaseMaterialsModel(self)
}
material_type_item["colors"].clear()
# Sort materials by name
material_list = sorted(material_list, key = lambda x: x["name"].upper())
material_type_item["colors"].setItems(material_list)
material_type_item_list.append(material_type_item)
# Sort material type by name
material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"].upper())
brand_item["material_types"].setItems(material_type_item_list)
brand_item_list.append(brand_item)
# Sort brand by name
brand_item_list = sorted(brand_item_list, key = lambda x: x["name"].upper())
self.setItems(brand_item_list)

View file

@ -1,104 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
#
# This model is for the Material management page.
#
class MaterialManagementModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
DisplayNameRole = Qt.UserRole + 2
BrandRole = Qt.UserRole + 3
MaterialTypeRole = Qt.UserRole + 4
ColorNameRole = Qt.UserRole + 5
ColorCodeRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
ContainerIdRole = Qt.UserRole + 8
DescriptionRole = Qt.UserRole + 9
AdhesionInfoRole = Qt.UserRole + 10
ApproximateDiameterRole = Qt.UserRole + 11
GuidRole = Qt.UserRole + 12
DensityRole = Qt.UserRole + 13
DiameterRole = Qt.UserRole + 14
IsReadOnlyRole = Qt.UserRole + 15
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
self.addRoleName(self.DisplayNameRole, "name")
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialTypeRole, "material")
self.addRoleName(self.ColorNameRole, "color_name")
self.addRoleName(self.ColorCodeRole, "color_code")
self.addRoleName(self.ContainerNodeRole, "container_node")
self.addRoleName(self.ContainerIdRole, "container_id")
self.addRoleName(self.DescriptionRole, "description")
self.addRoleName(self.AdhesionInfoRole, "adhesion_info")
self.addRoleName(self.ApproximateDiameterRole, "approximate_diameter")
self.addRoleName(self.GuidRole, "guid")
self.addRoleName(self.DensityRole, "density")
self.addRoleName(self.DiameterRole, "diameter")
self.addRoleName(self.IsReadOnlyRole, "is_read_only")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._extruder_manager.activeExtruderChanged.connect(self._update)
self._material_manager.materialsUpdated.connect(self._update)
self._update()
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
active_extruder_stack = self._machine_manager.activeStack
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
active_extruder_stack)
if available_material_dict is None:
self.setItems([])
return
material_list = []
for root_material_id, container_node in available_material_dict.items():
keys_to_fetch = ("name",
"brand",
"material",
"color_name",
"color_code",
"description",
"adhesion_info",
"approximate_diameter",)
item = {"root_material_id": container_node.metadata["base_file"],
"container_node": container_node,
"guid": container_node.metadata["GUID"],
"container_id": container_node.metadata["id"],
"density": container_node.metadata.get("properties", {}).get("density", ""),
"diameter": container_node.metadata.get("properties", {}).get("diameter", ""),
"is_read_only": self._container_registry.isReadOnly(container_node.metadata["id"]),
}
for key in keys_to_fetch:
item[key] = container_node.metadata.get(key, "")
material_list.append(item)
material_list = sorted(material_list, key = lambda k: (k["brand"].upper(), k["name"].upper()))
self.setItems(material_list)

View file

@ -8,6 +8,8 @@ from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.VariantType import VariantType
class NozzleModel(ListModel): class NozzleModel(ListModel):
IdRole = Qt.UserRole + 1 IdRole = Qt.UserRole + 1
@ -43,7 +45,6 @@ class NozzleModel(ListModel):
self.setItems([]) self.setItems([])
return return
from cura.Machines.VariantManager import VariantType
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE) variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
if not variant_node_dict: if not variant_node_dict:
self.setItems([]) self.setItems([])

View file

@ -45,7 +45,7 @@ class QualityManager(QObject):
self._empty_quality_container = self._application.empty_quality_container self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
self._default_machine_definition_id = "fdmprinter" self._default_machine_definition_id = "fdmprinter"
@ -64,10 +64,10 @@ class QualityManager(QObject):
def initialize(self): def initialize(self):
# Initialize the lookup tree for quality profiles with following structure: # Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <variant> -> <material> # <machine> -> <nozzle> -> <buildplate> -> <material>
# -> <material> # <machine> -> <material>
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality") quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
@ -79,53 +79,41 @@ class QualityManager(QObject):
quality_type = metadata["quality_type"] quality_type = metadata["quality_type"]
root_material_id = metadata.get("material") root_material_id = metadata.get("material")
variant_name = metadata.get("variant") nozzle_name = metadata.get("variant")
buildplate_name = metadata.get("buildplate")
is_global_quality = metadata.get("global_quality", False) is_global_quality = metadata.get("global_quality", False)
is_global_quality = is_global_quality or (root_material_id is None and variant_name is None) is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None)
# Sanity check: material+variant and is_global_quality cannot be present at the same time # Sanity check: material+variant and is_global_quality cannot be present at the same time
if is_global_quality and (root_material_id or variant_name): if is_global_quality and (root_material_id or nozzle_name):
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"]) ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
continue continue
if definition_id not in self._machine_variant_material_quality_type_to_quality_dict: if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode() self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
machine_node = cast(QualityNode, self._machine_variant_material_quality_type_to_quality_dict[definition_id]) machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
if is_global_quality: if is_global_quality:
# For global qualities, save data in the machine node # For global qualities, save data in the machine node
machine_node.addQualityMetadata(quality_type, metadata) machine_node.addQualityMetadata(quality_type, metadata)
continue continue
if variant_name is not None: current_node = machine_node
# If variant_name is specified in the quality/quality_changes profile, check if material is specified, intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
# too. current_intermediate_node_info_idx = 0
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = QualityNode()
variant_node = cast(QualityNode, machine_node.children_map[variant_name])
if root_material_id is None: while current_intermediate_node_info_idx < len(intermediate_node_info_list):
# If only variant_name is specified but material is not, add the quality/quality_changes metadata node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
# into the current variant node. if node_name is not None:
variant_node.addQualityMetadata(quality_type, metadata) # There is specific information, update the current node to go deeper so we can add this quality
else: # at the most specific branch in the lookup tree.
# If only variant_name and material are both specified, go one level deeper: create a material node if node_name not in current_node.children_map:
# under the current variant node, and then add the quality/quality_changes metadata into the current_node.children_map[node_name] = QualityNode()
# material node. current_node = cast(QualityNode, current_node.children_map[node_name])
if root_material_id not in variant_node.children_map:
variant_node.children_map[root_material_id] = QualityNode()
material_node = cast(QualityNode, variant_node.children_map[root_material_id])
material_node.addQualityMetadata(quality_type, metadata) current_intermediate_node_info_idx += 1
else: current_node.addQualityMetadata(quality_type, metadata)
# If variant_name is not specified, check if material is specified.
if root_material_id is not None:
if root_material_id not in machine_node.children_map:
machine_node.children_map[root_material_id] = QualityNode()
material_node = cast(QualityNode, machine_node.children_map[root_material_id])
material_node.addQualityMetadata(quality_type, metadata)
# Initialize the lookup tree for quality_changes profiles with following structure: # Initialize the lookup tree for quality_changes profiles with following structure:
# <machine> -> <quality_type> -> <name> # <machine> -> <quality_type> -> <name>
@ -217,8 +205,8 @@ class QualityManager(QObject):
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node] nodes_to_check = [machine_node, default_machine_node]
# Iterate over all quality_types in the machine node # Iterate over all quality_types in the machine node
@ -238,16 +226,19 @@ class QualityManager(QObject):
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
buildplate_name = machine.getBuildplateName()
# Iterate over all extruders to find quality containers for each extruder # Iterate over all extruders to find quality containers for each extruder
for position, extruder in machine.extruders.items(): for position, extruder in machine.extruders.items():
variant_name = None nozzle_name = None
if extruder.variant.getId() != "empty_variant": if extruder.variant.getId() != "empty_variant":
variant_name = extruder.variant.getName() nozzle_name = extruder.variant.getName()
# This is a list of root material IDs to use for searching for suitable quality profiles. # This is a list of root material IDs to use for searching for suitable quality profiles.
# The root material IDs in this list are in prioritized order. # The root material IDs in this list are in prioritized order.
root_material_id_list = [] root_material_id_list = []
has_material = False # flag indicating whether this extruder has a material assigned has_material = False # flag indicating whether this extruder has a material assigned
root_material_id = None
if extruder.material.getId() != "empty_material": if extruder.material.getId() != "empty_material":
has_material = True has_material = True
root_material_id = extruder.material.getMetaDataEntry("base_file") root_material_id = extruder.material.getMetaDataEntry("base_file")
@ -264,36 +255,51 @@ class QualityManager(QObject):
# Here we construct a list of nodes we want to look for qualities with the highest priority first. # Here we construct a list of nodes we want to look for qualities with the highest priority first.
# The use case is that, when we look for qualities for a machine, we first want to search in the following # The use case is that, when we look for qualities for a machine, we first want to search in the following
# order: # order:
# 1. machine-variant-and-material-specific qualities if exist # 1. machine-nozzle-buildplate-and-material-specific qualities if exist
# 2. machine-variant-specific qualities if exist # 2. machine-nozzle-and-material-specific qualities if exist
# 3. machine-material-specific qualities if exist # 3. machine-nozzle-specific qualities if exist
# 4. machine-specific qualities if exist # 4. machine-material-specific qualities if exist
# 5. generic qualities if exist # 5. machine-specific global qualities if exist, otherwise generic global qualities
# NOTE: We DO NOT fail back to generic global qualities if machine-specific global qualities exist.
# This is because when a machine defines its own global qualities such as Normal, Fine, etc.,
# it is intended to maintain those specific qualities ONLY. If we still fail back to the generic
# global qualities, there can be unimplemented quality types e.g. "coarse", and this is not
# correct.
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there. # qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id]
nodes_to_check = [] nodes_to_check = []
if variant_name: # This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# In this case, we have both a specific variant and a specific material # the search list in the order described above. So, by iterating over that search node list, we first look
variant_node = machine_node.getChildNode(variant_name) # in the more specific branches and then the less specific (generic) ones.
if variant_node and has_material: def addNodesToCheck(node, nodes_to_check_list, node_info_list, node_info_idx):
for root_material_id in root_material_id_list: if node_info_idx < len(node_info_list):
material_node = variant_node.getChildNode(root_material_id) node_name = node_info_list[node_info_idx]
if node_name is not None:
current_node = node.getChildNode(node_name)
if current_node is not None and has_material:
addNodesToCheck(current_node, nodes_to_check_list, node_info_list, node_info_idx + 1)
if has_material:
for rmid in root_material_id_list:
material_node = node.getChildNode(rmid)
if material_node: if material_node:
nodes_to_check.append(material_node) nodes_to_check_list.append(material_node)
break break
nodes_to_check.append(variant_node)
# In this case, we only have a specific material but NOT a variant nodes_to_check_list.append(node)
if has_material:
for root_material_id in root_material_id_list: addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
material_node = machine_node.getChildNode(root_material_id)
if material_node: # The last fall back will be the global qualities (either from the machine-specific node or the generic
nodes_to_check.append(material_node) # node), but we only use one. For details see the overview comments above.
break if machine_node.quality_type_map:
nodes_to_check += [machine_node]
else:
nodes_to_check += [default_machine_node]
nodes_to_check += [machine_node, default_machine_node]
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
if has_variant_materials: if has_variant_materials:
@ -309,8 +315,8 @@ class QualityManager(QObject):
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type] quality_group = quality_group_dict[quality_type]
quality_group.nodes_for_extruders[position] = quality_node if position not in quality_group.nodes_for_extruders:
break quality_group.nodes_for_extruders[position] = quality_node
# Update availabilities for each quality group # Update availabilities for each quality group
self._updateQualityGroupsAvailability(machine, quality_group_dict.values()) self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
@ -323,8 +329,8 @@ class QualityManager(QObject):
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get( default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
self._default_machine_definition_id) self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node] nodes_to_check = [machine_node, default_machine_node]

View file

@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from enum import Enum
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
@ -11,20 +10,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
class VariantType(Enum):
BUILD_PLATE = "buildplate"
NOZZLE = "nozzle"
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
# #
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following # VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
# structure: # structure:

View file

@ -0,0 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from enum import Enum
class VariantType(Enum):
BUILD_PLATE = "buildplate"
NOZZLE = "nozzle"
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
__all__ = ["VariantType", "ALL_VARIANT_TYPES"]

View file

@ -1,112 +1,149 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.Iterator import Iterator import sys
from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key from shapely import affinity
from UM.Application import Application from shapely.geometry import Polygon
from UM.Scene.Iterator.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode
# Iterator that determines the object print order when one-at a time mode is enabled.
#
# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can
# print, we need to print from the corner that's closest to the extruder that's being used. Here is an illustration:
#
# +--------------------------------+
# | |
# | |
# | | - Rectangle represents the complete print head including fans, etc.
# | X X | y - X's are the nozzles
# | (1) (2) | ^
# | | |
# +--------------------------------+ +--> x
#
# In this case, the nozzles are symmetric, nozzle (1) is closer to the bottom left corner while (2) is closer to the
# bottom right. If we use nozzle (1) to print, then we better off printing from the bottom left corner so the print
# head will not collide into an object on its top-right side, which is a very large unused area. Following the same
# logic, if we are printing with nozzle (2), then it's better to print from the bottom-right side.
#
# This iterator determines the print order following the rules above.
#
class OneAtATimeIterator(Iterator):
## Iterator that returns a list of nodes in the order that they need to be printed
# If there is no solution an empty list is returned.
# Take note that the list of nodes can have children (that may or may not contain mesh data)
class OneAtATimeIterator(Iterator.Iterator):
def __init__(self, scene_node): def __init__(self, scene_node):
super().__init__(scene_node) # Call super to make multiple inheritence work. from cura.CuraApplication import CuraApplication
self._hit_map = [[]] self._global_stack = CuraApplication.getInstance().getGlobalContainerStack()
self._original_node_list = [] self._original_node_list = []
super().__init__(scene_node) # Call super to make multiple inheritance work.
def getMachineNearestCornerToExtruder(self, global_stack):
head_and_fans_coordinates = global_stack.getHeadAndFansCoordinates()
used_extruder = None
for extruder in global_stack.extruders.values():
if extruder.isEnabled:
used_extruder = extruder
break
extruder_offsets = [used_extruder.getProperty("machine_nozzle_offset_x", "value"),
used_extruder.getProperty("machine_nozzle_offset_y", "value")]
# find the corner that's closest to the origin
min_distance2 = sys.maxsize
min_coord = None
for coord in head_and_fans_coordinates:
x = coord[0] - extruder_offsets[0]
y = coord[1] - extruder_offsets[1]
distance2 = x**2 + y**2
if distance2 <= min_distance2:
min_distance2 = distance2
min_coord = coord
return min_coord
def _checkForCollisions(self) -> bool:
all_nodes = []
for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode):
continue
convex_hull = node.callDecoration("getConvexHullHead")
if not convex_hull:
continue
bounding_box = node.getBoundingBox()
if not bounding_box:
continue
from UM.Math.Polygon import Polygon
bounding_box_polygon = Polygon([[bounding_box.left, bounding_box.front],
[bounding_box.left, bounding_box.back],
[bounding_box.right, bounding_box.back],
[bounding_box.right, bounding_box.front]])
all_nodes.append({"node": node,
"bounding_box": bounding_box_polygon,
"convex_hull": convex_hull})
has_collisions = False
for i, node_dict in enumerate(all_nodes):
for j, other_node_dict in enumerate(all_nodes):
if i == j:
continue
if node_dict["bounding_box"].intersectsPolygon(other_node_dict["convex_hull"]):
has_collisions = True
break
if has_collisions:
break
return has_collisions
def _fillStack(self): def _fillStack(self):
min_coord = self.getMachineNearestCornerToExtruder(self._global_stack)
transform_x = -int(round(min_coord[0] / abs(min_coord[0])))
transform_y = -int(round(min_coord[1] / abs(min_coord[1])))
machine_size = [self._global_stack.getProperty("machine_width", "value"),
self._global_stack.getProperty("machine_depth", "value")]
def flip_x(polygon):
tm2 = [-1, 0, 0, 1, 0, 0]
return affinity.affine_transform(affinity.translate(polygon, xoff = -machine_size[0]), tm2)
def flip_y(polygon):
tm2 = [1, 0, 0, -1, 0, 0]
return affinity.affine_transform(affinity.translate(polygon, yoff = -machine_size[1]), tm2)
if self._checkForCollisions():
self._node_stack = []
return
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode): if not issubclass(type(node), SceneNode):
continue continue
if node.callDecoration("getConvexHull"): convex_hull = node.callDecoration("getConvexHull")
node_list.append(node) if convex_hull:
xmin = min(x for x, _ in convex_hull._points)
xmax = max(x for x, _ in convex_hull._points)
ymin = min(y for _, y in convex_hull._points)
ymax = max(y for _, y in convex_hull._points)
convex_hull_polygon = Polygon.from_bounds(xmin, ymin, xmax, ymax)
if transform_x < 0:
convex_hull_polygon = flip_x(convex_hull_polygon)
if transform_y < 0:
convex_hull_polygon = flip_y(convex_hull_polygon)
if len(node_list) < 2: node_list.append({"node": node,
self._node_stack = node_list[:] "min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]],
return })
# Copy the list node_list = sorted(node_list, key = lambda d: d["min_coord"])
self._original_node_list = node_list[:]
## Initialise the hit map (pre-compute all hits between all objects)
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
# Check if we have to files that block eachother. If this is the case, there is no solution!
for a in range(0,len(node_list)):
for b in range(0,len(node_list)):
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
return
# Sort the original list so that items that block the most other objects are at the beginning.
# This does not decrease the worst case running time, but should improve it in most cases.
sorted(node_list, key = cmp_to_key(self._calculateScore))
todo_node_list = [_ObjectOrder([], node_list)]
while len(todo_node_list) > 0:
current = todo_node_list.pop()
for node in current.todo:
# Check if the object can be placed with what we have and still allows for a solution in the future
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
# We found a possible result. Create new todo & order list.
new_todo_list = current.todo[:]
new_todo_list.remove(node)
new_order = current.order[:] + [node]
if len(new_todo_list) == 0:
# We have no more nodes to check, so quit looking.
todo_node_list = None
self._node_stack = new_order
return
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
self._node_stack = [] #No result found!
# Check if first object can be printed before the provided list (using the hit map)
def _checkHitMultiple(self, node, other_nodes):
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[node_index][other_node_index]:
return True
return False
def _checkBlockMultiple(self, node, other_nodes):
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
return True
return False
## Calculate score simply sums the number of other objects it 'blocks'
def _calculateScore(self, a, b):
score_a = sum(self._hit_map[self._original_node_list.index(a)])
score_b = sum(self._hit_map[self._original_node_list.index(b)])
return score_a - score_b
# Checks if A can be printed before B
def _checkHit(self, a, b):
if a == b:
return False
overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull"))
if overlap:
return True
else:
return False
## Internal object used to keep track of a possible order in which to print objects.
class _ObjectOrder():
def __init__(self, order, todo):
"""
:param order: List of indexes in which to print objects, ordered by printing order.
:param todo: List of indexes which are not yet inserted into the order list.
"""
self.order = order
self.todo = todo
self._node_stack = [d["node"] for d in node_list]

View file

@ -267,6 +267,7 @@ class PrintInformation(QObject):
new_active_build_plate = self._multi_build_plate_model.activeBuildPlate new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
if new_active_build_plate != self._active_build_plate: if new_active_build_plate != self._active_build_plate:
self._active_build_plate = new_active_build_plate self._active_build_plate = new_active_build_plate
self._updateJobName()
self._initVariablesWithBuildPlate(self._active_build_plate) self._initVariablesWithBuildPlate(self._active_build_plate)
@ -299,7 +300,7 @@ class PrintInformation(QObject):
def _updateJobName(self): def _updateJobName(self):
if self._base_name == "": if self._base_name == "":
self._job_name = "unnamed" self._job_name = "Untitled"
self._is_user_specified_job_name = False self._is_user_specified_job_name = False
self.jobNameChanged.emit() self.jobNameChanged.emit()
return return
@ -320,6 +321,15 @@ class PrintInformation(QObject):
else: else:
self._job_name = base_name self._job_name = base_name
# In case there are several buildplates, a suffix is attached
if self._multi_build_plate_model.maxBuildPlate > 0:
connector = "_#"
suffix = connector + str(self._active_build_plate + 1)
if connector in self._job_name:
self._job_name = self._job_name.split(connector)[0] # get the real name
if self._active_build_plate != 0:
self._job_name += suffix
self.jobNameChanged.emit() self.jobNameChanged.emit()
@pyqtSlot(str) @pyqtSlot(str)
@ -369,8 +379,9 @@ class PrintInformation(QObject):
def baseName(self): def baseName(self):
return self._base_name return self._base_name
## Created an acronymn-like abbreviated machine name from the currently active machine name ## Created an acronym-like abbreviated machine name from the currently
# Called each time the global stack is switched # active machine name.
# Called each time the global stack is switched.
def _setAbbreviatedMachineName(self): def _setAbbreviatedMachineName(self):
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack: if not global_container_stack:

View file

@ -120,7 +120,7 @@ class PrinterOutputModel(QObject):
@pyqtProperty(QVariant, notify = headPositionChanged) @pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self): def headPosition(self):
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z} return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
def updateHeadPosition(self, x, y, z): def updateHeadPosition(self, x, y, z):
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:

View file

@ -229,7 +229,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
return offset_hull return offset_hull
def _getHeadAndFans(self): def _getHeadAndFans(self):
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32)) return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
def _compute2DConvexHeadFull(self): def _compute2DConvexHeadFull(self):
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, cast, List, Optional, Union from typing import Any, cast, List, Optional
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
from UM.Application import Application from UM.Application import Application
@ -13,6 +13,7 @@ from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
from cura.Settings import cura_empty_instance_containers
from . import Exceptions from . import Exceptions
@ -39,14 +40,12 @@ class CuraContainerStack(ContainerStack):
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
self._container_registry = ContainerRegistry.getInstance() #type: ContainerRegistry self._empty_instance_container = cura_empty_instance_containers.empty_container #type: InstanceContainer
self._empty_instance_container = self._container_registry.getEmptyInstanceContainer() #type: InstanceContainer self._empty_quality_changes = cura_empty_instance_containers.empty_quality_changes_container #type: InstanceContainer
self._empty_quality = cura_empty_instance_containers.empty_quality_container #type: InstanceContainer
self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] #type: InstanceContainer self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] #type: InstanceContainer self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] #type: InstanceContainer
self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0] #type: InstanceContainer
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface] self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface]
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes

View file

@ -8,7 +8,7 @@ from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.VariantManager import VariantType from cura.Machines.VariantType import VariantType
from .GlobalStack import GlobalStack from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack from .ExtruderStack import ExtruderStack
@ -108,16 +108,27 @@ class CuraStackBuilder:
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type") preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(new_global_stack) quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
quality_group = quality_group_dict.get(preferred_quality_type) if not quality_group_dict:
# There is no available quality group, set all quality containers to empty.
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items(): for extruder_stack in new_global_stack.extruders.values():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container extruder_stack.quality = application.empty_quality_container
else:
# Set the quality containers to the preferred quality type if available, otherwise use the first quality
# type that's available.
if preferred_quality_type not in quality_group_dict:
Logger.log("w", "The preferred quality {quality_type} doesn't exist for this set-up. Choosing a random one.".format(quality_type = preferred_quality_type))
preferred_quality_type = next(iter(quality_group_dict))
quality_group = quality_group_dict.get(preferred_quality_type)
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another # Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1). # extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).

View file

@ -139,9 +139,6 @@ class ExtruderStack(CuraContainerStack):
super().deserialize(contents, file_name) super().deserialize(contents, file_name)
if "enabled" not in self.getMetaData(): if "enabled" not in self.getMetaData():
self.setMetaDataEntry("enabled", "True") self.setMetaDataEntry("enabled", "True")
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
if stacks:
self.setNextStack(stacks[0])
def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None: def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None:
# When there is a setting that is not settable per extruder that depends on a value from a setting that is, # When there is a setting that is not settable per extruder that depends on a value from a setting that is,

View file

@ -55,6 +55,16 @@ class GlobalStack(CuraContainerStack):
return "machine_stack" return "machine_stack"
return configuration_type return configuration_type
def getBuildplateName(self) -> Optional[str]:
name = None
if self.variant.getId() != "empty_variant":
name = self.variant.getName()
return name
@pyqtProperty(str, constant = True)
def preferred_output_file_formats(self) -> str:
return self.getMetaDataEntry("file_formats")
## Add an extruder to the list of extruders of this stack. ## Add an extruder to the list of extruders of this stack.
# #
# \param extruder The extruder to add. # \param extruder The extruder to add.
@ -96,6 +106,9 @@ class GlobalStack(CuraContainerStack):
# Handle the "resolve" property. # Handle the "resolve" property.
#TODO: Why the hell does this involve threading? #TODO: Why the hell does this involve threading?
# Answer: Because if multiple threads start resolving properties that have the same underlying properties that's
# related, without taking a note of which thread a resolve paths belongs to, they can bump into each other and
# generate unexpected behaviours.
if self._shouldResolve(key, property_name, context): if self._shouldResolve(key, property_name, context):
current_thread = threading.current_thread() current_thread = threading.current_thread()
self._resolving_settings[current_thread.name].add(key) self._resolving_settings[current_thread.name].add(key)
@ -172,6 +185,9 @@ class GlobalStack(CuraContainerStack):
return False return False
return True return True
def getHeadAndFansCoordinates(self):
return self.getProperty("machine_head_with_fans_polygon", "value")
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View file

@ -21,9 +21,6 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication import cura.CuraApplication
from cura.Machines.ContainerNode import ContainerNode #For typing.
from cura.Machines.QualityChangesGroup import QualityChangesGroup #For typing.
from cura.Machines.QualityGroup import QualityGroup #For typing.
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
@ -44,12 +41,16 @@ if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager from cura.Machines.QualityManager import QualityManager
from cura.Machines.VariantManager import VariantManager from cura.Machines.VariantManager import VariantManager
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from cura.Machines.QualityGroup import QualityGroup
class MachineManager(QObject): class MachineManager(QObject):
def __init__(self, parent: QObject = None) -> None: def __init__(self, parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
self._active_container_stack = None # type: Optional[ExtruderManager] self._active_container_stack = None # type: Optional[ExtruderStack]
self._global_container_stack = None # type: Optional[GlobalStack] self._global_container_stack = None # type: Optional[GlobalStack]
self._current_root_material_id = {} # type: Dict[str, str] self._current_root_material_id = {} # type: Dict[str, str]
@ -1087,7 +1088,7 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _setQualityGroup(self, quality_group: Optional[QualityGroup], empty_quality_changes: bool = True) -> None: def _setQualityGroup(self, quality_group: Optional["QualityGroup"], empty_quality_changes: bool = True) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if quality_group is None: if quality_group is None:
@ -1118,7 +1119,7 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: QualityChangesGroup) -> None: def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: "QualityChangesGroup") -> None:
nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values()) nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values())
containers = [n.getContainer() for n in nodes if n is not None] containers = [n.getContainer() for n in nodes if n is not None]
for container in containers: for container in containers:
@ -1126,7 +1127,7 @@ class MachineManager(QObject):
container.setMetaDataEntry("quality_type", "not_supported") container.setMetaDataEntry("quality_type", "not_supported")
quality_changes_group.quality_type = "not_supported" quality_changes_group.quality_type = "not_supported"
def _setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup) -> None: def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return #Can't change that. return #Can't change that.
quality_type = quality_changes_group.quality_type quality_type = quality_changes_group.quality_type
@ -1170,20 +1171,20 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _setVariantNode(self, position: str, container_node: ContainerNode) -> None: def _setVariantNode(self, position: str, container_node: "ContainerNode") -> None:
if container_node.getContainer() is None or self._global_container_stack is None: if container_node.getContainer() is None or self._global_container_stack is None:
return return
self._global_container_stack.extruders[position].variant = container_node.getContainer() self._global_container_stack.extruders[position].variant = container_node.getContainer()
self.activeVariantChanged.emit() self.activeVariantChanged.emit()
def _setGlobalVariant(self, container_node: ContainerNode) -> None: def _setGlobalVariant(self, container_node: "ContainerNode") -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
self._global_container_stack.variant = container_node.getContainer() self._global_container_stack.variant = container_node.getContainer()
if not self._global_container_stack.variant: if not self._global_container_stack.variant:
self._global_container_stack.variant = self._application.empty_variant_container self._global_container_stack.variant = self._application.empty_variant_container
def _setMaterial(self, position: str, container_node: ContainerNode = None) -> None: def _setMaterial(self, position: str, container_node: Optional["ContainerNode"] = None) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if container_node and container_node.getContainer(): if container_node and container_node.getContainer():
@ -1256,13 +1257,17 @@ class MachineManager(QObject):
else: else:
position_list = [position] position_list = [position]
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
for position_item in position_list: for position_item in position_list:
extruder = self._global_container_stack.extruders[position_item] extruder = self._global_container_stack.extruders[position_item]
current_material_base_name = extruder.material.getMetaDataEntry("base_file") current_material_base_name = extruder.material.getMetaDataEntry("base_file")
current_variant_name = None current_nozzle_name = None
if extruder.variant.getId() != self._empty_variant_container.getId(): if extruder.variant.getId() != self._empty_variant_container.getId():
current_variant_name = extruder.variant.getMetaDataEntry("name") current_nozzle_name = extruder.variant.getMetaDataEntry("name")
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from cura.Settings.CuraContainerStack import _ContainerIndexes from cura.Settings.CuraContainerStack import _ContainerIndexes
@ -1271,7 +1276,8 @@ class MachineManager(QObject):
material_diameter = extruder.getProperty("material_diameter", "value", context) material_diameter = extruder.getProperty("material_diameter", "value", context)
candidate_materials = self._material_manager.getAvailableMaterials( candidate_materials = self._material_manager.getAvailableMaterials(
self._global_container_stack.definition, self._global_container_stack.definition,
current_variant_name, current_nozzle_name,
buildplate_name,
material_diameter) material_diameter)
if not candidate_materials: if not candidate_materials:
@ -1284,7 +1290,7 @@ class MachineManager(QObject):
continue continue
# The current material is not available, find the preferred one # The current material is not available, find the preferred one
material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_variant_name) material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_nozzle_name)
if material_node is not None: if material_node is not None:
self._setMaterial(position_item, material_node) self._setMaterial(position_item, material_node)
@ -1326,7 +1332,12 @@ class MachineManager(QObject):
for extruder_configuration in configuration.extruderConfigurations: for extruder_configuration in configuration.extruderConfigurations:
position = str(extruder_configuration.position) position = str(extruder_configuration.position)
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID) variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID)
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack, position, extruder_configuration.hotendID, extruder_configuration.material.guid) material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
position,
extruder_configuration.hotendID,
configuration.buildplateConfiguration,
extruder_configuration.material.guid)
if variant_container_node: if variant_container_node:
self._setVariantNode(position, variant_container_node) self._setVariantNode(position, variant_container_node)
else: else:
@ -1378,7 +1389,7 @@ class MachineManager(QObject):
return bool(containers) return bool(containers)
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def setGlobalVariant(self, container_node: ContainerNode) -> None: def setGlobalVariant(self, container_node: "ContainerNode") -> None:
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setGlobalVariant(container_node) self._setGlobalVariant(container_node)
@ -1389,12 +1400,17 @@ class MachineManager(QObject):
def setMaterialById(self, position: str, root_material_id: str) -> None: def setMaterialById(self, position: str, root_material_id: str) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
machine_definition_id = self._global_container_stack.definition.id machine_definition_id = self._global_container_stack.definition.id
position = str(position) position = str(position)
extruder_stack = self._global_container_stack.extruders[position] extruder_stack = self._global_container_stack.extruders[position]
variant_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
material_diameter = extruder_stack.approximateMaterialDiameter material_diameter = extruder_stack.approximateMaterialDiameter
material_node = self._material_manager.getMaterialNode(machine_definition_id, variant_name, material_diameter, root_material_id) material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name,
material_diameter, root_material_id)
self.setMaterial(position, material_node) self.setMaterial(position, material_node)
## global_stack: if you want to provide your own global_stack instead of the current active one ## global_stack: if you want to provide your own global_stack instead of the current active one
@ -1423,7 +1439,7 @@ class MachineManager(QObject):
self.setVariant(position, variant_node) self.setVariant(position, variant_node)
@pyqtSlot(str, "QVariant") @pyqtSlot(str, "QVariant")
def setVariant(self, position: str, container_node: ContainerNode) -> None: def setVariant(self, position: str, container_node: "ContainerNode") -> None:
position = str(position) position = str(position)
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
@ -1447,7 +1463,7 @@ class MachineManager(QObject):
## Optionally provide global_stack if you want to use your own ## Optionally provide global_stack if you want to use your own
# The active global_stack is treated differently. # The active global_stack is treated differently.
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setQualityGroup(self, quality_group: QualityGroup, no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None:
if global_stack is not None and global_stack != self._global_container_stack: if global_stack is not None and global_stack != self._global_container_stack:
if quality_group is None: if quality_group is None:
Logger.log("e", "Could not set quality group because quality group is None") Logger.log("e", "Could not set quality group because quality group is None")
@ -1455,9 +1471,14 @@ class MachineManager(QObject):
if quality_group.node_for_global is None: if quality_group.node_for_global is None:
Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group)) Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group))
return return
# This is not changing the quality for the active machine !!!!!!!!
global_stack.quality = quality_group.node_for_global.getContainer() global_stack.quality = quality_group.node_for_global.getContainer()
for extruder_nr, extruder_stack in global_stack.extruders.items(): for extruder_nr, extruder_stack in global_stack.extruders.items():
extruder_stack.quality = quality_group.nodes_for_extruders[extruder_nr].getContainer() quality_container = self._empty_quality_container
if extruder_nr in quality_group.nodes_for_extruders:
container = quality_group.nodes_for_extruders[extruder_nr].getContainer()
quality_container = container if container is not None else quality_container
extruder_stack.quality = quality_container
return return
self.blurSettings.emit() self.blurSettings.emit()
@ -1469,11 +1490,11 @@ class MachineManager(QObject):
self._application.discardOrKeepProfileChanges() self._application.discardOrKeepProfileChanges()
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged) @pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
def activeQualityGroup(self) -> Optional[QualityGroup]: def activeQualityGroup(self) -> Optional["QualityGroup"]:
return self._current_quality_group return self._current_quality_group
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup, no_dialog: bool = False) -> None: def setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", no_dialog: bool = False) -> None:
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setQualityChangesGroup(quality_changes_group) self._setQualityChangesGroup(quality_changes_group)
@ -1492,7 +1513,7 @@ class MachineManager(QObject):
stack.userChanges.clear() stack.userChanges.clear()
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
def activeQualityChangesGroup(self) -> Optional[QualityChangesGroup]: def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
return self._current_quality_changes_group return self._current_quality_changes_group
@pyqtProperty(str, notify = activeQualityGroupChanged) @pyqtProperty(str, notify = activeQualityGroupChanged)

View file

@ -0,0 +1,41 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import pyqtSlot, Qt
class SidebarCustomMenuItemsModel(ListModel):
name_role = Qt.UserRole + 1
actions_role = Qt.UserRole + 2
menu_item_role = Qt.UserRole + 3
menu_item_icon_name_role = Qt.UserRole + 5
def __init__(self, parent=None):
super().__init__(parent)
self.addRoleName(self.name_role, "name")
self.addRoleName(self.actions_role, "actions")
self.addRoleName(self.menu_item_role, "menu_item")
self.addRoleName(self.menu_item_icon_name_role, "iconName")
self._updateExtensionList()
def _updateExtensionList(self)-> None:
from cura.CuraApplication import CuraApplication
for menu_item in CuraApplication.getInstance().getSidebarCustomMenuItems():
self.appendItem({
"name": menu_item["name"],
"icon_name": menu_item["icon_name"],
"actions": menu_item["actions"],
"menu_item": menu_item["menu_item"]
})
@pyqtSlot(str, "QVariantList", "QVariantMap")
def callMenuItemMethod(self, menu_item_name: str, menu_item_actions: list, kwargs: Any) -> None:
for item in self._items:
if menu_item_name == item["name"]:
for method in menu_item_actions:
getattr(item["menu_item"], method)(kwargs)
break

View file

@ -0,0 +1,56 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import copy
from UM.Settings.constant_instance_containers import EMPTY_CONTAINER_ID, empty_container
# Empty definition changes
EMPTY_DEFINITION_CHANGES_CONTAINER_ID = "empty_definition_changes"
empty_definition_changes_container = copy.deepcopy(empty_container)
empty_definition_changes_container.setMetaDataEntry("id", EMPTY_DEFINITION_CHANGES_CONTAINER_ID)
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes")
# Empty variant
EMPTY_VARIANT_CONTAINER_ID = "empty_variant"
empty_variant_container = copy.deepcopy(empty_container)
empty_variant_container.setMetaDataEntry("id", EMPTY_VARIANT_CONTAINER_ID)
empty_variant_container.setMetaDataEntry("type", "variant")
# Empty material
EMPTY_MATERIAL_CONTAINER_ID = "empty_material"
empty_material_container = copy.deepcopy(empty_container)
empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID)
empty_material_container.setMetaDataEntry("type", "material")
# Empty quality
EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
empty_quality_container = copy.deepcopy(empty_container)
empty_quality_container.setMetaDataEntry("id", EMPTY_QUALITY_CONTAINER_ID)
empty_quality_container.setName("Not Supported")
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_container.setMetaDataEntry("type", "quality")
empty_quality_container.setMetaDataEntry("supported", False)
# Empty quality changes
EMPTY_QUALITY_CHANGES_CONTAINER_ID = "empty_quality_changes"
empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID)
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
__all__ = ["EMPTY_CONTAINER_ID",
"empty_container", # For convenience
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
"empty_definition_changes_container",
"EMPTY_VARIANT_CONTAINER_ID",
"empty_variant_container",
"EMPTY_MATERIAL_CONTAINER_ID",
"empty_material_container",
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
"empty_quality_changes_container",
"EMPTY_QUALITY_CONTAINER_ID",
"empty_quality_container"
]

View file

@ -131,6 +131,7 @@ faulthandler.enable(all_threads = True)
# first seems to prevent Sip from going into a state where it # first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread. # tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport import Arcus #@UnusedImport
import Savitar #@UnusedImport
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
app = CuraApplication() app = CuraApplication()

View file

@ -59,7 +59,7 @@ class ThreeMFReader(MeshReader):
if transformation == "": if transformation == "":
return Matrix() return Matrix()
splitted_transformation = transformation.split() split_transformation = transformation.split()
## Transformation is saved as: ## Transformation is saved as:
## M00 M01 M02 0.0 ## M00 M01 M02 0.0
## M10 M11 M12 0.0 ## M10 M11 M12 0.0
@ -68,20 +68,20 @@ class ThreeMFReader(MeshReader):
## We switch the row & cols as that is how everyone else uses matrices! ## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix() temp_mat = Matrix()
# Rotation & Scale # Rotation & Scale
temp_mat._data[0, 0] = splitted_transformation[0] temp_mat._data[0, 0] = split_transformation[0]
temp_mat._data[1, 0] = splitted_transformation[1] temp_mat._data[1, 0] = split_transformation[1]
temp_mat._data[2, 0] = splitted_transformation[2] temp_mat._data[2, 0] = split_transformation[2]
temp_mat._data[0, 1] = splitted_transformation[3] temp_mat._data[0, 1] = split_transformation[3]
temp_mat._data[1, 1] = splitted_transformation[4] temp_mat._data[1, 1] = split_transformation[4]
temp_mat._data[2, 1] = splitted_transformation[5] temp_mat._data[2, 1] = split_transformation[5]
temp_mat._data[0, 2] = splitted_transformation[6] temp_mat._data[0, 2] = split_transformation[6]
temp_mat._data[1, 2] = splitted_transformation[7] temp_mat._data[1, 2] = split_transformation[7]
temp_mat._data[2, 2] = splitted_transformation[8] temp_mat._data[2, 2] = split_transformation[8]
# Translation # Translation
temp_mat._data[0, 3] = splitted_transformation[9] temp_mat._data[0, 3] = split_transformation[9]
temp_mat._data[1, 3] = splitted_transformation[10] temp_mat._data[1, 3] = split_transformation[10]
temp_mat._data[2, 3] = splitted_transformation[11] temp_mat._data[2, 3] = split_transformation[11]
return temp_mat return temp_mat

View file

@ -24,6 +24,7 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job from UM.Job import Job
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.Machines.VariantType import VariantType
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
@ -84,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-curaproject+xml",
comment="Cura Project File",
suffixes=["curaproject.3mf"]
)
)
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog() self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None self._3mf_mesh_reader = None
@ -629,6 +622,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
type = "extruder_train") type = "extruder_train")
extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks} extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks}
# Make sure that those extruders have the global stack as the next stack or later some value evaluation
# will fail.
for stack in extruder_stacks:
stack.setNextStack(global_stack, connect_signals = False)
Logger.log("d", "Workspace loading is checking definitions...") Logger.log("d", "Workspace loading is checking definitions...")
# Get all the definition files & check if they exist. If not, add them. # Get all the definition files & check if they exist. If not, add them.
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
@ -720,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
nodes = [] nodes = []
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
@ -889,7 +885,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
parser = self._machine_info.variant_info.parser parser = self._machine_info.variant_info.parser
variant_name = parser["general"]["name"] variant_name = parser["general"]["name"]
from cura.Machines.VariantManager import VariantType
variant_type = VariantType.BUILD_PLATE variant_type = VariantType.BUILD_PLATE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type) node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
@ -905,7 +900,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
parser = extruder_info.variant_info.parser parser = extruder_info.variant_info.parser
variant_name = parser["general"]["name"] variant_name = parser["general"]["name"]
from cura.Machines.VariantManager import VariantType
variant_type = VariantType.NOZZLE variant_type = VariantType.NOZZLE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type) node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
@ -929,12 +923,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id = extruder_info.root_material_id root_material_id = extruder_info.root_material_id
root_material_id = self._old_new_materials.get(root_material_id, root_material_id) root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
build_plate_id = global_stack.variant.getId()
# get material diameter of this extruder # get material diameter of this extruder
machine_material_diameter = extruder_stack.materialDiameter machine_material_diameter = extruder_stack.materialDiameter
material_node = material_manager.getMaterialNode(global_stack.definition.getId(), material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
extruder_stack.variant.getName(), extruder_stack.variant.getName(),
build_plate_id,
machine_material_diameter, machine_material_diameter,
root_material_id) root_material_id)
if material_node is not None and material_node.getContainer() is not None: if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() extruder_stack.material = material_node.getContainer()

View file

@ -18,11 +18,7 @@ catalog = i18nCatalog("cura")
def getMetaData() -> Dict: def getMetaData() -> Dict:
# Workaround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
if "3MFReader.ThreeMFReader" in sys.modules: if "3MFReader.ThreeMFReader" in sys.modules:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -25,6 +25,9 @@ except ImportError:
import zipfile import zipfile
import UM.Application import UM.Application
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class ThreeMFWriter(MeshWriter): class ThreeMFWriter(MeshWriter):
def __init__(self): def __init__(self):
@ -173,6 +176,7 @@ class ThreeMFWriter(MeshWriter):
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
except Exception as e: except Exception as e:
Logger.logException("e", "Error writing zip file") Logger.logException("e", "Error writing zip file")
self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
return False return False
finally: finally:
if not self._store_archive: if not self._store_archive:

View file

@ -15,11 +15,7 @@ from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("uranium")
def getMetaData(): def getMetaData():
# Workarround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
@ -36,7 +32,7 @@ def getMetaData():
"output": [{ "output": [{
"extension": workspace_extension, "extension": workspace_extension,
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
"mime_type": "application/x-curaproject+xml", "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}] }]
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Shows changes since latest checked version.", "description": "Shows changes since latest checked version.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,6 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import argparse #To run the engine in debug mode if the front-end is in debug mode.
from collections import defaultdict from collections import defaultdict
import os import os
from PyQt5.QtCore import QObject, QTimer, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSlot
@ -179,7 +180,15 @@ class CuraEngineBackend(QObject, Backend):
# \return list of commands and args / parameters. # \return list of commands and args / parameters.
def getEngineCommand(self) -> List[str]: def getEngineCommand(self) -> List[str]:
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
return [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
known_args = vars(parser.parse_known_args()[0])
if known_args["debug"]:
command.append("-vvv")
return command
## Emitted when we get a message containing print duration and material amount. ## Emitted when we get a message containing print duration and material amount.
# This also implies the slicing has finished. # This also implies the slicing has finished.
@ -541,6 +550,9 @@ class CuraEngineBackend(QObject, Backend):
## Remove old layer data (if any) ## Remove old layer data (if any)
def _clearLayerData(self, build_plate_numbers: Set = None) -> None: def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
# Clear out any old gcode
self._scene.gcode_dict = {} # type: ignore
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("getLayerData"): if node.callDecoration("getLayerData"):
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:

View file

@ -41,7 +41,7 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcode ## Formatter class that handles token expansion in start/end gcode
class GcodeStartEndFormatter(Formatter): class GcodeStartEndFormatter(Formatter):
def get_value(self, key: str, *args: str, default_extruder_nr: str = "-1", **kwargs) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class] def get_value(self, key: str, args: str, kwargs: dict, default_extruder_nr: str = "-1") -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key), # The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified # and a default_extruder_nr to use when no extruder_nr is specified
@ -220,8 +220,10 @@ class StartSliceJob(Job):
stack = global_stack stack = global_stack
skip_group = False skip_group = False
for node in group: for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]: if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True skip_group = True
has_model_with_disabled_extruders = True has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position) associated_disabled_extruders.add(extruder_position)

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend", "name": "CuraEngine Backend",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"api": 4, "api": 5,
"version": "1.0.0", "version": "1.0.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"api": 4, "api": 5,
"i18n-catalog":"cura" "i18n-catalog":"cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -10,10 +10,17 @@ from UM.Mesh.MeshWriter import MeshWriter #The class we're extending/implementin
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode #For typing. from UM.Scene.SceneNode import SceneNode #For typing.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## A file writer that writes gzipped g-code. ## A file writer that writes gzipped g-code.
# #
# If you're zipping g-code, you might as well use gzip! # If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter): class GCodeGzWriter(MeshWriter):
def __init__(self) -> None:
super().__init__(add_to_recent_files = False)
## Writes the gzipped g-code to a stream. ## Writes the gzipped g-code to a stream.
# #
# Note that even though the function accepts a collection of nodes, the # Note that even though the function accepts a collection of nodes, the
@ -28,12 +35,15 @@ class GCodeGzWriter(MeshWriter):
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
if mode != MeshWriter.OutputMode.BinaryMode: if mode != MeshWriter.OutputMode.BinaryMode:
Logger.log("e", "GCodeGzWriter does not support text mode.") Logger.log("e", "GCodeGzWriter does not support text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode."))
return False return False
#Get the g-code from the g-code writer. #Get the g-code from the g-code writer.
gcode_textio = StringIO() #We have to convert the g-code into bytes. gcode_textio = StringIO() #We have to convert the g-code into bytes.
success = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")).write(gcode_textio, None) gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
success = gcode_writer.write(gcode_textio, None)
if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code. if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code.
self.setInformation(gcode_writer.getInformation())
return False return False
result = gzip.compress(gcode_textio.getvalue().encode("utf-8")) result = gzip.compress(gcode_textio.getvalue().encode("utf-8"))

View file

@ -16,7 +16,8 @@ def getMetaData():
"extension": file_extension, "extension": file_extension,
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"), "description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
"mime_type": "application/gzip", "mime_type": "application/gzip",
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode "mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode,
"hide_in_file_dialog": True,
}] }]
} }
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Victor Larchenko", "author": "Victor Larchenko",
"version": "1.0.0", "version": "1.0.0",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -12,6 +12,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Writes g-code to a file. ## Writes g-code to a file.
# #
@ -45,7 +47,7 @@ class GCodeWriter(MeshWriter):
_setting_keyword = ";SETTING_" _setting_keyword = ";SETTING_"
def __init__(self): def __init__(self):
super().__init__() super().__init__(add_to_recent_files = False)
self._application = Application.getInstance() self._application = Application.getInstance()
@ -62,11 +64,13 @@ class GCodeWriter(MeshWriter):
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
if mode != MeshWriter.OutputMode.TextMode: if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCodeWriter does not support non-text mode.") Logger.log("e", "GCodeWriter does not support non-text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
return False return False
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
if not hasattr(scene, "gcode_dict"): if not hasattr(scene, "gcode_dict"):
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving."))
return False return False
gcode_dict = getattr(scene, "gcode_dict") gcode_dict = getattr(scene, "gcode_dict")
gcode_list = gcode_dict.get(active_build_plate, None) gcode_list = gcode_dict.get(active_build_plate, None)
@ -82,6 +86,7 @@ class GCodeWriter(MeshWriter):
stream.write(settings) stream.write(settings)
return True return True
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving."))
return False return False
## Create a new container with container 2 as base and container 1 written over it. ## Create a new container with container 2 as base and container 1 written over it.

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "Model Checker", "name": "Model Checker",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "0.1", "version": "0.1",
"api": 4, "api": 5,
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides the Per Model Settings.", "description": "Provides the Per Model Settings.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. # Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
@ -260,6 +261,9 @@ class PostProcessingPlugin(QObject, Extension):
# Create the plugin dialog component # Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
if self._view is None:
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
return
Logger.log("d", "Post processing view created.") Logger.log("d", "Post processing view created.")
# Create the save button component # Create the save button component
@ -269,6 +273,9 @@ class PostProcessingPlugin(QObject, Extension):
def showPopup(self): def showPopup(self):
if self._view is None: if self._view is None:
self._createView() self._createView()
if self._view is None:
Logger.log("e", "Not creating PostProcessing window since the QML component failed to be created.")
return
self._view.show() self._view.show()
## Property changed: trigger re-slice ## Property changed: trigger re-slice

View file

@ -2,7 +2,7 @@
"name": "Post Processing", "name": "Post Processing",
"author": "Ultimaker", "author": "Ultimaker",
"version": "2.2", "version": "2.2",
"api": 4, "api": 5,
"description": "Extension that allows for user created scripts for post processing", "description": "Extension that allows for user created scripts for post processing",
"catalog": "cura" "catalog": "cura"
} }

View file

@ -58,10 +58,10 @@ class FilamentChange(Script):
color_change = "M600" color_change = "M600"
if initial_retract is not None and initial_retract > 0.: if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract) color_change = color_change + (" E-%.2f" % initial_retract)
if later_retract is not None and later_retract > 0.: if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract) color_change = color_change + (" L-%.2f" % later_retract)
color_change = color_change + " ; Generated by FilamentChange plugin" color_change = color_change + " ; Generated by FilamentChange plugin"

View file

@ -28,7 +28,7 @@ class PauseAtHeight(Script):
"pause_height": "pause_height":
{ {
"label": "Pause Height", "label": "Pause Height",
"description": "At what height should the pause occur", "description": "At what height should the pause occur?",
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 5.0, "default_value": 5.0,
@ -39,7 +39,7 @@ class PauseAtHeight(Script):
"pause_layer": "pause_layer":
{ {
"label": "Pause Layer", "label": "Pause Layer",
"description": "At what layer should the pause occur", "description": "At what layer should the pause occur?",
"type": "int", "type": "int",
"value": "math.floor((pause_height - 0.27) / 0.1) + 1", "value": "math.floor((pause_height - 0.27) / 0.1) + 1",
"minimum_value": "0", "minimum_value": "0",
@ -142,13 +142,14 @@ class PauseAtHeight(Script):
standby_temperature = self.getSettingValueByKey("standby_temperature") standby_temperature = self.getSettingValueByKey("standby_temperature")
firmware_retract = Application.getInstance().getGlobalContainerStack().getProperty("machine_firmware_retract", "value") firmware_retract = Application.getInstance().getGlobalContainerStack().getProperty("machine_firmware_retract", "value")
control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value") control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value")
initial_layer_height = Application.getInstance().getGlobalContainerStack().getProperty("layer_height_0", "value")
is_griffin = False is_griffin = False
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z> # use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
layer_0_z = 0. layer_0_z = 0
current_z = 0 current_z = 0
got_first_g_cmd_on_layer_0 = False got_first_g_cmd_on_layer_0 = False
current_t = 0 #Tracks the current extruder for tracking the target temperature. current_t = 0 #Tracks the current extruder for tracking the target temperature.
@ -195,11 +196,10 @@ class PauseAtHeight(Script):
# This block is executed once, the first time there is a G # This block is executed once, the first time there is a G
# command, to get the z offset (z for first positive layer) # command, to get the z offset (z for first positive layer)
if not got_first_g_cmd_on_layer_0: if not got_first_g_cmd_on_layer_0:
layer_0_z = current_z layer_0_z = current_z - initial_layer_height
got_first_g_cmd_on_layer_0 = True got_first_g_cmd_on_layer_0 = True
current_height = current_z - layer_0_z current_height = current_z - layer_0_z
if current_height < pause_height: if current_height < pause_height:
break # Try the next layer. break # Try the next layer.

View file

@ -35,25 +35,39 @@ class GCodeStep():
Class to store the current value of each G_Code parameter Class to store the current value of each G_Code parameter
for any G-Code step for any G-Code step
""" """
def __init__(self, step): def __init__(self, step, in_relative_movement: bool = False):
self.step = step self.step = step
self.step_x = 0 self.step_x = 0
self.step_y = 0 self.step_y = 0
self.step_z = 0 self.step_z = 0
self.step_e = 0 self.step_e = 0
self.step_f = 0 self.step_f = 0
self.in_relative_movement = in_relative_movement
self.comment = "" self.comment = ""
def readStep(self, line): def readStep(self, line):
""" """
Reads gcode from line into self Reads gcode from line into self
""" """
self.step_x = _getValue(line, "X", self.step_x) if not self.in_relative_movement:
self.step_y = _getValue(line, "Y", self.step_y) self.step_x = _getValue(line, "X", self.step_x)
self.step_z = _getValue(line, "Z", self.step_z) self.step_y = _getValue(line, "Y", self.step_y)
self.step_e = _getValue(line, "E", self.step_e) self.step_z = _getValue(line, "Z", self.step_z)
self.step_f = _getValue(line, "F", self.step_f) self.step_e = _getValue(line, "E", self.step_e)
return self.step_f = _getValue(line, "F", self.step_f)
else:
delta_step_x = _getValue(line, "X", 0)
delta_step_y = _getValue(line, "Y", 0)
delta_step_z = _getValue(line, "Z", 0)
delta_step_e = _getValue(line, "E", 0)
self.step_x += delta_step_x
self.step_y += delta_step_y
self.step_z += delta_step_z
self.step_e += delta_step_e
self.step_f = _getValue(line, "F", self.step_f) # the feedrate is not relative
def copyPosFrom(self, step): def copyPosFrom(self, step):
""" """
@ -65,7 +79,9 @@ class GCodeStep():
self.step_e = step.step_e self.step_e = step.step_e
self.step_f = step.step_f self.step_f = step.step_f
self.comment = step.comment self.comment = step.comment
return
def setInRelativeMovement(self, value: bool) -> None:
self.in_relative_movement = value
# Execution part of the stretch plugin # Execution part of the stretch plugin
@ -86,6 +102,7 @@ class Stretcher():
# of already deposited material for current layer # of already deposited material for current layer
self.layer_z = 0 # Z position of the extrusion moves of the current layer self.layer_z = 0 # Z position of the extrusion moves of the current layer
self.layergcode = "" self.layergcode = ""
self._in_relative_movement = False
def execute(self, data): def execute(self, data):
""" """
@ -96,7 +113,8 @@ class Stretcher():
+ " and push wall stretch " + str(self.pw_stretch) + "mm") + " and push wall stretch " + str(self.pw_stretch) + "mm")
retdata = [] retdata = []
layer_steps = [] layer_steps = []
current = GCodeStep(0) in_relative_movement = False
current = GCodeStep(0, in_relative_movement)
self.layer_z = 0. self.layer_z = 0.
current_e = 0. current_e = 0.
for layer in data: for layer in data:
@ -107,20 +125,31 @@ class Stretcher():
current.comment = line[line.find(";"):] current.comment = line[line.find(";"):]
if _getValue(line, "G") == 0: if _getValue(line, "G") == 0:
current.readStep(line) current.readStep(line)
onestep = GCodeStep(0) onestep = GCodeStep(0, in_relative_movement)
onestep.copyPosFrom(current) onestep.copyPosFrom(current)
elif _getValue(line, "G") == 1: elif _getValue(line, "G") == 1:
current.readStep(line) current.readStep(line)
onestep = GCodeStep(1) onestep = GCodeStep(1, in_relative_movement)
onestep.copyPosFrom(current) onestep.copyPosFrom(current)
# end of relative movement
elif _getValue(line, "G") == 90:
in_relative_movement = False
current.setInRelativeMovement(in_relative_movement)
# start of relative movement
elif _getValue(line, "G") == 91:
in_relative_movement = True
current.setInRelativeMovement(in_relative_movement)
elif _getValue(line, "G") == 92: elif _getValue(line, "G") == 92:
current.readStep(line) current.readStep(line)
onestep = GCodeStep(-1) onestep = GCodeStep(-1, in_relative_movement)
onestep.copyPosFrom(current) onestep.copyPosFrom(current)
else: else:
onestep = GCodeStep(-1) onestep = GCodeStep(-1, in_relative_movement)
onestep.copyPosFrom(current) onestep.copyPosFrom(current)
onestep.comment = line onestep.comment = line
if line.find(";LAYER:") >= 0 and len(layer_steps): if line.find(";LAYER:") >= 0 and len(layer_steps):
# Previous plugin "forgot" to separate two layers... # Previous plugin "forgot" to separate two layers...
Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z) Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.0", "version": "1.0.0",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -40,33 +40,37 @@ Item {
property bool layersVisible: true property bool layersVisible: true
function getUpperValueFromSliderHandle () { function getUpperValueFromSliderHandle() {
return upperHandle.getValue() return upperHandle.getValue()
} }
function setUpperValue (value) { function setUpperValue(value) {
upperHandle.setValue(value) upperHandle.setValue(value)
updateRangeHandle() updateRangeHandle()
} }
function getLowerValueFromSliderHandle () { function getLowerValueFromSliderHandle() {
return lowerHandle.getValue() return lowerHandle.getValue()
} }
function setLowerValue (value) { function setLowerValue(value) {
lowerHandle.setValue(value) lowerHandle.setValue(value)
updateRangeHandle() updateRangeHandle()
} }
function updateRangeHandle () { function updateRangeHandle() {
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height) rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
} }
// set the active handle to show only one label at a time // set the active handle to show only one label at a time
function setActiveHandle (handle) { function setActiveHandle(handle) {
activeHandle = handle activeHandle = handle
} }
function normalizeValue(value) {
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
}
// slider track // slider track
Rectangle { Rectangle {
id: track id: track
@ -188,6 +192,8 @@ Item {
// set the slider position based on the upper value // set the slider position based on the upper value
function setValue (value) { function setValue (value) {
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
UM.SimulationView.setCurrentLayer(value) UM.SimulationView.setCurrentLayer(value)
@ -274,6 +280,8 @@ Item {
// set the slider position based on the lower value // set the slider position based on the lower value
function setValue (value) { function setValue (value) {
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
UM.SimulationView.setMinimumLayer(value) UM.SimulationView.setMinimumLayer(value)

View file

@ -29,6 +29,7 @@ Item {
// value properties // value properties
property real maximumValue: 100 property real maximumValue: 100
property real minimumValue: 0
property bool roundValues: true property bool roundValues: true
property real handleValue: maximumValue property real handleValue: maximumValue
@ -47,6 +48,10 @@ Item {
rangeHandle.width = handle.x - sliderRoot.handleSize rangeHandle.width = handle.x - sliderRoot.handleSize
} }
function normalizeValue(value) {
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
}
// slider track // slider track
Rectangle { Rectangle {
id: track id: track
@ -110,6 +115,8 @@ Item {
// set the slider position based on the value // set the slider position based on the value
function setValue (value) { function setValue (value) {
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
UM.SimulationView.setCurrentPath(value) UM.SimulationView.setCurrentPath(value)

View file

@ -25,10 +25,6 @@ UM.PointingRectangle {
width: valueLabel.width + UM.Theme.getSize("default_margin").width width: valueLabel.width + UM.Theme.getSize("default_margin").width
visible: false visible: false
// make sure the text field is focussed when pressing the parent handle
// needed to connect the key bindings when switching active handle
onVisibleChanged: if (visible) valueLabel.forceActiveFocus()
color: UM.Theme.getColor("tool_panel_background") color: UM.Theme.getColor("tool_panel_background")
borderColor: UM.Theme.getColor("lining") borderColor: UM.Theme.getColor("lining")
borderWidth: UM.Theme.getSize("default_lining").width borderWidth: UM.Theme.getSize("default_lining").width

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4 import QtQuick 2.4
@ -12,30 +12,43 @@ import Cura 1.0 as Cura
Item Item
{ {
id: base id: base
width: { width:
if (UM.SimulationView.compatibilityMode) { {
if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").width; return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else { }
else
{
return UM.Theme.getSize("layerview_menu_size").width; return UM.Theme.getSize("layerview_menu_size").width;
} }
} }
height: { height: {
if (viewSettings.collapsed) { if (viewSettings.collapsed)
if (UM.SimulationView.compatibilityMode) { {
if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height; return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height;
} }
return UM.Theme.getSize("layerview_menu_size_collapsed").height; return UM.Theme.getSize("layerview_menu_size_collapsed").height;
} else if (UM.SimulationView.compatibilityMode) { }
else if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").height; return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) { }
else if (UM.Preferences.getValue("layerview/layer_view_type") == 0)
{
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else { }
else
{
return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} }
} }
Behavior on height { NumberAnimation { duration: 100 } } Behavior on height { NumberAnimation { duration: 100 } }
property var buttonTarget: { property var buttonTarget:
{
if(parent != null) if(parent != null)
{ {
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
@ -44,7 +57,8 @@ Item
return Qt.point(0,0) return Qt.point(0,0)
} }
Rectangle { Rectangle
{
id: layerViewMenu id: layerViewMenu
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
@ -83,7 +97,8 @@ Item
} }
} }
ColumnLayout { ColumnLayout
{
id: viewSettings id: viewSettings
property bool collapsed: false property bool collapsed: false
@ -195,7 +210,8 @@ Item
width: width width: width
} }
Connections { Connections
{
target: UM.Preferences target: UM.Preferences
onPreferenceChanged: onPreferenceChanged:
{ {
@ -212,18 +228,22 @@ Item
} }
} }
Repeater { Repeater
{
model: Cura.ExtrudersModel{} model: Cura.ExtrudersModel{}
CheckBox { CheckBox
{
id: extrudersModelCheckBox id: extrudersModelCheckBox
checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == "" checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
onClicked: { onClicked:
{
viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0 viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|")); UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
} }
visible: !UM.SimulationView.compatibilityMode visible: !UM.SimulationView.compatibilityMode
enabled: index + 1 <= 4 enabled: index + 1 <= 4
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -253,8 +273,10 @@ Item
} }
} }
Repeater { Repeater
model: ListModel { {
model: ListModel
{
id: typesLegendModel id: typesLegendModel
Component.onCompleted: Component.onCompleted:
{ {
@ -285,13 +307,16 @@ Item
} }
} }
CheckBox { CheckBox
{
id: legendModelCheckBox id: legendModelCheckBox
checked: model.initialValue checked: model.initialValue
onClicked: { onClicked:
{
UM.Preferences.setValue(model.preference, checked); UM.Preferences.setValue(model.preference, checked);
} }
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right anchors.right: legendModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -320,18 +345,22 @@ Item
} }
} }
CheckBox { CheckBox
{
checked: viewSettings.only_show_top_layers checked: viewSettings.only_show_top_layers
onClicked: { onClicked:
{
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0); UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
} }
text: catalog.i18nc("@label", "Only Show Top Layers") text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.SimulationView.compatibilityMode visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox style: UM.Theme.styles.checkbox
} }
CheckBox { CheckBox
{
checked: viewSettings.top_layer_count == 5 checked: viewSettings.top_layer_count == 5
onClicked: { onClicked:
{
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1); UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
} }
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top") text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
@ -339,8 +368,10 @@ Item
style: UM.Theme.styles.checkbox style: UM.Theme.styles.checkbox
} }
Repeater { Repeater
model: ListModel { {
model: ListModel
{
id: typesLegendModelNoCheck id: typesLegendModelNoCheck
Component.onCompleted: Component.onCompleted:
{ {
@ -355,11 +386,13 @@ Item
} }
} }
Label { Label
{
text: label text: label
visible: viewSettings.show_legend visible: viewSettings.show_legend
id: typesLegendModelLabel id: typesLegendModelLabel
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right anchors.right: typesLegendModelLabel.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -378,30 +411,37 @@ Item
} }
// Text for the minimum, maximum and units for the feedrates and layer thickness // Text for the minimum, maximum and units for the feedrates and layer thickness
Item { Item
{
id: gradientLegend id: gradientLegend
visible: viewSettings.show_gradient visible: viewSettings.show_gradient
width: parent.width width: parent.width
height: UM.Theme.getSize("layerview_row").height height: UM.Theme.getSize("layerview_row").height
anchors { anchors
{
topMargin: UM.Theme.getSize("slider_layerview_margin").height topMargin: UM.Theme.getSize("slider_layerview_margin").height
horizontalCenter: parent.horizontalCenter horizontalCenter: parent.horizontalCenter
} }
Label { Label
{
text: minText() text: minText()
anchors.left: parent.left anchors.left: parent.left
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function minText() { function minText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2) return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
} }
} }
@ -409,20 +449,25 @@ Item
} }
} }
Label { Label
{
text: unitsText() text: unitsText()
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function unitsText() { function unitsText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return "mm/s" return "mm/s"
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return "mm" return "mm"
} }
} }
@ -430,20 +475,25 @@ Item
} }
} }
Label { Label
{
text: maxText() text: maxText()
anchors.right: parent.right anchors.right: parent.right
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function maxText() { function maxText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2) return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
} }
} }
@ -453,7 +503,8 @@ Item
} }
// Gradient colors for feedrate // Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient Rectangle
{ // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees // Invert values because then the bar is rotated 90 degrees
id: feedrateGradient id: feedrateGradient
visible: viewSettings.show_feedrate_gradient visible: viewSettings.show_feedrate_gradient
@ -463,20 +514,25 @@ Item
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90} transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient { gradient: Gradient
GradientStop { {
GradientStop
{
position: 0.000 position: 0.000
color: Qt.rgba(1, 0.5, 0, 1) color: Qt.rgba(1, 0.5, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.625 position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1) color: Qt.rgba(0.375, 0.5, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.75 position: 0.75
color: Qt.rgba(0.25, 1, 0, 1) color: Qt.rgba(0.25, 1, 0, 1)
} }
GradientStop { GradientStop
{
position: 1.0 position: 1.0
color: Qt.rgba(0, 0, 1, 1) color: Qt.rgba(0, 0, 1, 1)
} }
@ -484,7 +540,8 @@ Item
} }
// Gradient colors for layer thickness (similar to parula colormap) // Gradient colors for layer thickness (similar to parula colormap)
Rectangle { // In QML 5.9 can be changed by LinearGradient Rectangle // In QML 5.9 can be changed by LinearGradient
{
// Invert values because then the bar is rotated 90 degrees // Invert values because then the bar is rotated 90 degrees
id: thicknessGradient id: thicknessGradient
visible: viewSettings.show_thickness_gradient visible: viewSettings.show_thickness_gradient
@ -494,24 +551,30 @@ Item
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90} transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient { gradient: Gradient
GradientStop { {
GradientStop
{
position: 0.000 position: 0.000
color: Qt.rgba(1, 1, 0, 1) color: Qt.rgba(1, 1, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.25 position: 0.25
color: Qt.rgba(1, 0.75, 0.25, 1) color: Qt.rgba(1, 0.75, 0.25, 1)
} }
GradientStop { GradientStop
{
position: 0.5 position: 0.5
color: Qt.rgba(0, 0.75, 0.5, 1) color: Qt.rgba(0, 0.75, 0.5, 1)
} }
GradientStop { GradientStop
{
position: 0.75 position: 0.75
color: Qt.rgba(0, 0.375, 0.75, 1) color: Qt.rgba(0, 0.375, 0.75, 1)
} }
GradientStop { GradientStop
{
position: 1.0 position: 1.0
color: Qt.rgba(0, 0, 0.5, 1) color: Qt.rgba(0, 0, 0.5, 1)
} }
@ -520,19 +583,22 @@ Item
} }
} }
Item { Item
{
id: slidersBox id: slidersBox
width: parent.width width: parent.width
visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
anchors { anchors
{
top: parent.bottom top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height topMargin: UM.Theme.getSize("slider_layerview_margin").height
left: parent.left left: parent.left
} }
PathSlider { PathSlider
{
id: pathSlider id: pathSlider
height: UM.Theme.getSize("slider_handle").width height: UM.Theme.getSize("slider_handle").width
@ -553,25 +619,29 @@ Item
rangeColor: UM.Theme.getColor("slider_groove_fill") rangeColor: UM.Theme.getColor("slider_groove_fill")
// update values when layer data changes // update values when layer data changes
Connections { Connections
{
target: UM.SimulationView target: UM.SimulationView
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath) onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath) onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
} }
// make sure the slider handlers show the correct value after switching views // make sure the slider handlers show the correct value after switching views
Component.onCompleted: { Component.onCompleted:
{
pathSlider.setHandleValue(UM.SimulationView.currentPath) pathSlider.setHandleValue(UM.SimulationView.currentPath)
} }
} }
LayerSlider { LayerSlider
{
id: layerSlider id: layerSlider
width: UM.Theme.getSize("slider_handle").width width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height height: UM.Theme.getSize("layerview_menu_size").height
anchors { anchors
{
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0 topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right right: parent.right
@ -593,7 +663,8 @@ Item
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
// update values when layer data changes // update values when layer data changes
Connections { Connections
{
target: UM.SimulationView target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer) onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer) onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
@ -601,45 +672,54 @@ Item
} }
// make sure the slider handlers show the correct value after switching views // make sure the slider handlers show the correct value after switching views
Component.onCompleted: { Component.onCompleted:
{
layerSlider.setLowerValue(UM.SimulationView.minimumLayer) layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
layerSlider.setUpperValue(UM.SimulationView.currentLayer) layerSlider.setUpperValue(UM.SimulationView.currentLayer)
} }
} }
// Play simulation button // Play simulation button
Button { Button
{
id: playButton id: playButton
iconSource: "./resources/simulation_resume.svg" iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.small_tool_button style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode visible: !UM.SimulationView.compatibilityMode
anchors { anchors
{
verticalCenter: pathSlider.verticalCenter verticalCenter: pathSlider.verticalCenter
} }
property var status: 0 // indicates if it's stopped (0) or playing (1) property var status: 0 // indicates if it's stopped (0) or playing (1)
onClicked: { onClicked:
switch(status) { {
case 0: { switch(status)
{
case 0:
{
resumeSimulation() resumeSimulation()
break break
} }
case 1: { case 1:
{
pauseSimulation() pauseSimulation()
break break
} }
} }
} }
function pauseSimulation() { function pauseSimulation()
{
UM.SimulationView.setSimulationRunning(false) UM.SimulationView.setSimulationRunning(false)
iconSource = "./resources/simulation_resume.svg" iconSource = "./resources/simulation_resume.svg"
simulationTimer.stop() simulationTimer.stop()
status = 0 status = 0
} }
function resumeSimulation() { function resumeSimulation()
{
UM.SimulationView.setSimulationRunning(true) UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg" iconSource = "./resources/simulation_pause.svg"
simulationTimer.start() simulationTimer.start()
@ -652,7 +732,8 @@ Item
interval: 100 interval: 100
running: false running: false
repeat: true repeat: true
onTriggered: { onTriggered:
{
var currentPath = UM.SimulationView.currentPath var currentPath = UM.SimulationView.currentPath
var numPaths = UM.SimulationView.numPaths var numPaths = UM.SimulationView.numPaths
var currentLayer = UM.SimulationView.currentLayer var currentLayer = UM.SimulationView.currentLayer
@ -697,7 +778,8 @@ Item
} }
} }
FontMetrics { FontMetrics
{
id: fontMetrics id: fontMetrics
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -25,10 +25,12 @@ from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from UM.Settings.SettingInstance import SettingInstance from UM.Settings.SettingInstance import SettingInstance
import numpy
class SupportEraser(Tool): class SupportEraser(Tool):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._shortcut_key = Qt.Key_G self._shortcut_key = Qt.Key_E
self._controller = self.getController() self._controller = self.getController()
self._selection_pass = None self._selection_pass = None
@ -96,8 +98,7 @@ class SupportEraser(Tool):
node.setName("Eraser") node.setName("Eraser")
node.setSelectable(True) node.setSelectable(True)
mesh = MeshBuilder() mesh = self._createCube(10)
mesh.addCube(10,10,10)
node.setMeshData(mesh.build()) node.setMeshData(mesh.build())
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
@ -160,3 +161,28 @@ class SupportEraser(Tool):
self._skip_press = False self._skip_press = False
self._had_selection = has_selection self._had_selection = has_selection
def _createCube(self, size):
mesh = MeshBuilder()
# Can't use MeshBuilder.addCube() because that does not get per-vertex normals
# Per-vertex normals require duplication of vertices
s = size / 2
verts = [ # 6 faces with 4 corners each
[-s, -s, s], [-s, s, s], [ s, s, s], [ s, -s, s],
[-s, s, -s], [-s, -s, -s], [ s, -s, -s], [ s, s, -s],
[ s, -s, -s], [-s, -s, -s], [-s, -s, s], [ s, -s, s],
[-s, s, -s], [ s, s, -s], [ s, s, s], [-s, s, s],
[-s, -s, s], [-s, -s, -s], [-s, s, -s], [-s, s, s],
[ s, -s, -s], [ s, -s, s], [ s, s, s], [ s, s, -s]
]
mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))
indices = []
for i in range(0, 24, 4): # All 6 quads (12 triangles)
indices.append([i, i+2, i+1])
indices.append([i, i+3, i+2])
mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))
mesh.calculateNormals()
return mesh

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Creates an eraser mesh to block the printing of support in certain places", "description": "Creates an eraser mesh to block the printing of support in certain places",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,6 +2,6 @@
"name": "Toolbox", "name": "Toolbox",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"api": 4, "api": 5,
"description": "Find, manage and install new Cura packages." "description": "Find, manage and install new Cura packages."
} }

View file

@ -29,6 +29,16 @@ Item
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width width: parent.width
frameVisible: false frameVisible: false
// Workaround for scroll issues (QTBUG-49652)
flickableItem.interactive: false
Component.onCompleted:
{
for (var i = 0; i < flickableItem.children.length; ++i)
{
flickableItem.children[i].enabled = false
}
}
selectionMode: 0 selectionMode: 0
model: packageData.supported_configs model: packageData.supported_configs
headerDelegate: Rectangle headerDelegate: Rectangle

View file

@ -114,7 +114,10 @@ Item
else else
{ {
toolbox.viewPage = "author" toolbox.viewPage = "author"
toolbox.filterModelByProp("packages", "author_id", model.id) toolbox.setFilters("packages", {
"author_id": model.id,
"type": "material"
})
} }
break break
default: default:

View file

@ -105,8 +105,21 @@ Rectangle
switch(toolbox.viewCategory) switch(toolbox.viewCategory)
{ {
case "material": case "material":
toolbox.viewPage = "author"
toolbox.filterModelByProp("packages", "author_name", model.name) // If model has a type, it must be a package
if (model.type !== undefined)
{
toolbox.viewPage = "detail"
toolbox.filterModelByProp("packages", "id", model.id)
}
else
{
toolbox.viewPage = "author"
toolbox.setFilters("packages", {
"author_id": model.id,
"type": "material"
})
}
break break
default: default:
toolbox.viewPage = "detail" toolbox.viewPage = "detail"

View file

@ -28,7 +28,7 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 11, "download_url") self.addRoleName(Qt.UserRole + 11, "download_url")
self.addRoleName(Qt.UserRole + 12, "last_updated") self.addRoleName(Qt.UserRole + 12, "last_updated")
self.addRoleName(Qt.UserRole + 13, "is_bundled") self.addRoleName(Qt.UserRole + 13, "is_bundled")
self.addRoleName(Qt.UserRole + 14, "is_enabled") self.addRoleName(Qt.UserRole + 14, "is_active")
self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 16, "has_configs") self.addRoleName(Qt.UserRole + 16, "has_configs")
self.addRoleName(Qt.UserRole + 17, "supported_configs") self.addRoleName(Qt.UserRole + 17, "supported_configs")
@ -75,7 +75,7 @@ class PackagesModel(ListModel):
"download_url": package["download_url"] if "download_url" in package else None, "download_url": package["download_url"] if "download_url" in package else None,
"last_updated": package["last_updated"] if "last_updated" in package else None, "last_updated": package["last_updated"] if "last_updated" in package else None,
"is_bundled": package["is_bundled"] if "is_bundled" in package else False, "is_bundled": package["is_bundled"] if "is_bundled" in package else False,
"is_enabled": package["is_enabled"] if "is_enabled" in package else False, "is_active": package["is_active"] if "is_active" in package else False,
"is_installed": package["is_installed"] if "is_installed" in package else False, "is_installed": package["is_installed"] if "is_installed" in package else False,
"has_configs": has_configs, "has_configs": has_configs,
"supported_configs": configs_model, "supported_configs": configs_model,

View file

@ -776,17 +776,25 @@ class Toolbox(QObject, Extension):
# Filter Models: # Filter Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def filterModelByProp(self, modelType: str, filterType: str, parameter: str): def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
if not self._models[modelType]: if not self._models[model_type]:
Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", modelType) Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", model_type)
return return
self._models[modelType].setFilter({ filterType: parameter }) self._models[model_type].setFilter({filter_type: parameter})
self.filterChanged.emit() self.filterChanged.emit()
@pyqtSlot() @pyqtSlot(str, "QVariantMap")
def removeFilters(self, modelType: str): def setFilters(self, model_type: str, filter_dict: dict) -> None:
if not self._models[modelType]: if not self._models[model_type]:
Logger.log("w", "Toolbox: Couldn't remove filters on %s model because it doesn't exist.", modelType) Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", model_type)
return return
self._models[modelType].setFilter({}) self._models[model_type].setFilter(filter_dict)
self.filterChanged.emit()
@pyqtSlot(str)
def removeFilters(self, model_type: str) -> None:
if not self._models[model_type]:
Logger.log("w", "Toolbox: Couldn't remove filters on %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({})
self.filterChanged.emit() self.filterChanged.emit()

View file

@ -1,5 +1,6 @@
#Copyright (c) 2018 Ultimaker B.V. #Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher. #Cura is released under the terms of the LGPLv3 or higher.
from typing import cast
from Charon.VirtualFile import VirtualFile #To open UFP files. from Charon.VirtualFile import VirtualFile #To open UFP files.
from Charon.OpenMode import OpenMode #To indicate that we want to write to UFP files. from Charon.OpenMode import OpenMode #To indicate that we want to write to UFP files.
@ -12,11 +13,15 @@ from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer from PyQt5.QtCore import QBuffer
from cura.Snapshot import Snapshot from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UFPWriter(MeshWriter): class UFPWriter(MeshWriter):
def __init__(self): def __init__(self):
super().__init__() super().__init__(add_to_recent_files = False)
self._snapshot = None self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)
@ -25,6 +30,11 @@ class UFPWriter(MeshWriter):
Logger.log("d", "Creating thumbnail image...") Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot(width = 300, height = 300) self._snapshot = Snapshot.snapshot(width = 300, height = 300)
# This needs to be called on the main thread (Qt thread) because the serialization of material containers can
# trigger loading other containers. Because those loaded containers are QtObjects, they must be created on the
# Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled
# by the Job class.
@call_on_qt_thread
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile() archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
@ -32,7 +42,11 @@ class UFPWriter(MeshWriter):
#Store the g-code from the scene. #Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode") archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
gcode_textio = StringIO() #We have to convert the g-code into bytes. gcode_textio = StringIO() #We have to convert the g-code into bytes.
PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None) gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
success = gcode_writer.write(gcode_textio, None)
if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code.
self.setInformation(gcode_writer.getInformation())
return False
gcode = archive.getStream("/3D/model.gcode") gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8")) gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
@ -52,5 +66,50 @@ class UFPWriter(MeshWriter):
else: else:
Logger.log("d", "Thumbnail not created, cannot save it") Logger.log("d", "Thumbnail not created, cannot save it")
# Store the material.
application = Application.getInstance()
machine_manager = application.getMachineManager()
material_manager = application.getMaterialManager()
global_stack = machine_manager.activeMachine
material_extension = "xml.fdm_material"
material_mime_type = "application/x-ultimaker-material-profile"
try:
archive.addContentType(extension = material_extension, mime_type = material_mime_type)
except:
Logger.log("w", "The material extension: %s was already added", material_extension)
added_materials = []
for extruder_stack in global_stack.extruders.values():
material = extruder_stack.material
material_file_name = material.getMetaData()["base_file"] + ".xml.fdm_material"
material_file_name = "/Materials/" + material_file_name
#Same material cannot be added
if material_file_name in added_materials:
continue
material_root_id = material.getMetaDataEntry("base_file")
material_group = material_manager.getMaterialGroup(material_root_id)
if material_group is None:
Logger.log("e", "Cannot find material container with root id [%s]", material_root_id)
return False
material_container = material_group.root_material_node.getContainer()
try:
serialized_material = material_container.serialize()
except NotImplementedError:
Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
return False
material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode")
added_materials.append(material_file_name)
archive.close() archive.close()
return True return True

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for writing Ultimaker Format Packages.", "description": "Provides support for writing Ultimaker Format Packages.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -30,7 +30,12 @@ Component
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right:parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: Cura.MachineManager.printerOutputDevices[0].name text: Cura.MachineManager.printerOutputDevices[0].name
elide: Text.ElideRight
} }
Rectangle Rectangle

View file

@ -9,6 +9,7 @@ Component
{ {
Rectangle Rectangle
{ {
id: monitorFrame
width: maximumWidth width: maximumWidth
height: maximumHeight height: maximumHeight
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
@ -103,5 +104,15 @@ Component
visible: OutputDevice.activePrinter != null visible: OutputDevice.activePrinter != null
anchors.fill:parent anchors.fill:parent
} }
onVisibleChanged:
{
if (!monitorFrame.visible)
{
// After switching the Tab ensure that active printer is Null, the video stream image
// might be active
OutputDevice.setActivePrinter(null)
}
}
} }
} }

View file

@ -113,7 +113,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
machine_file_formats = [file_type.strip() for file_type in machine_file_formats] machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
#Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"): if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"):
machine_file_formats = ["application/x-ufp"] + machine_file_formats machine_file_formats = ["application/x-ufp"] + machine_file_formats
# Take the intersection between file_formats and machine_file_formats. # Take the intersection between file_formats and machine_file_formats.
@ -590,4 +590,4 @@ def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: s
for item in list: for item in list:
if item.key == key: if item.key == key:
return item return item
return None return None

View file

@ -19,5 +19,5 @@ class ClusterUM3PrinterOutputController(PrinterOutputController):
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"action\": \"%s\"}" % state data = "{\"action\": \"%s\"}" % state
self._output_device.put("print_jobs/%s/action" % job.key, data, onFinished=None) self._output_device.put("print_jobs/%s/action" % job.key, data, on_finished=None)

View file

@ -299,11 +299,11 @@ Cura.MachineAction
} }
else if (base.selectedDevice.clusterSize === 0) else if (base.selectedDevice.clusterSize === 0)
{ {
return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers."); return catalog.i18nc("@label", "This printer is not set up to host a group of printers.");
} }
else else
{ {
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedDevice.clusterSize)); return catalog.i18nc("@label", "This printer is the host for a group of %1 printers.".arg(base.selectedDevice.clusterSize));
} }
} }
@ -349,11 +349,6 @@ Cura.MachineAction
addressField.focus = true; addressField.focus = true;
} }
onAccepted:
{
manager.setManualDevice(printerKey, addressText)
}
Column { Column {
anchors.fill: parent anchors.fill: parent
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height
@ -369,7 +364,6 @@ Cura.MachineAction
{ {
id: addressField id: addressField
width: parent.width width: parent.width
maximumLength: 40
validator: RegExpValidator validator: RegExpValidator
{ {
regExp: /[a-zA-Z0-9\.\-\_]*/ regExp: /[a-zA-Z0-9\.\-\_]*/
@ -393,7 +387,7 @@ Cura.MachineAction
text: catalog.i18nc("@action:button", "OK") text: catalog.i18nc("@action:button", "OK")
onClicked: onClicked:
{ {
manualPrinterDialog.accept() manager.setManualDevice(manualPrinterDialog.printerKey, manualPrinterDialog.addressText)
manualPrinterDialog.hide() manualPrinterDialog.hide()
} }
enabled: manualPrinterDialog.addressText.trim() != "" enabled: manualPrinterDialog.addressText.trim() != ""

View file

@ -165,7 +165,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
file_name = "none.xml" file_name = "none.xml"
self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None) self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None)
except NotImplementedError: except NotImplementedError:
# If the material container is not the most "generic" one it can't be serialized an will raise a # If the material container is not the most "generic" one it can't be serialized an will raise a
@ -270,7 +270,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
onFinished=self._onPostPrintJobFinished) on_finished=self._onPostPrintJobFinished)
return return
@ -381,8 +381,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._checkAuthentication() self._checkAuthentication()
# We don't need authentication for requesting info, so we can go right ahead with requesting this. # We don't need authentication for requesting info, so we can go right ahead with requesting this.
self.get("printer", onFinished=self._onGetPrinterDataFinished) self.get("printer", on_finished=self._onGetPrinterDataFinished)
self.get("print_job", onFinished=self._onGetPrintJobFinished) self.get("print_job", on_finished=self._onGetPrintJobFinished)
def _resetAuthenticationRequestedMessage(self): def _resetAuthenticationRequestedMessage(self):
if self._authentication_requested_message: if self._authentication_requested_message:
@ -404,7 +404,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def _verifyAuthentication(self): def _verifyAuthentication(self):
Logger.log("d", "Attempting to verify authentication") Logger.log("d", "Attempting to verify authentication")
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted) self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted)
def _onVerifyAuthenticationCompleted(self, reply): def _onVerifyAuthenticationCompleted(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
@ -426,7 +426,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def _checkAuthentication(self): def _checkAuthentication(self):
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished) self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished)
def _onCheckAuthenticationFinished(self, reply): def _onCheckAuthenticationFinished(self, reply):
if str(self._authentication_id) not in reply.url().toString(): if str(self._authentication_id) not in reply.url().toString():
@ -502,7 +502,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.post("auth/request", self.post("auth/request",
json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(), json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(),
"user": self._getUserName()}).encode(), "user": self._getUserName()}).encode(),
onFinished=self._onRequestAuthenticationFinished) on_finished=self._onRequestAuthenticationFinished)
self.setAuthenticationState(AuthState.AuthenticationRequested) self.setAuthenticationState(AuthState.AuthenticationRequested)

View file

@ -31,11 +31,11 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"target\": \"%s\"}" % state data = "{\"target\": \"%s\"}" % state
self._output_device.put("print_job/state", data, onFinished=None) self._output_device.put("print_job/state", data, on_finished=None)
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
data = str(temperature) data = str(temperature)
self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted) self._output_device.put("printer/bed/temperature/target", data, on_finished=self._onPutBedTemperatureCompleted)
def _onPutBedTemperatureCompleted(self, reply): def _onPutBedTemperatureCompleted(self, reply):
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"): if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
@ -51,10 +51,10 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
new_y = head_pos.y + y new_y = head_pos.y + y
new_z = head_pos.z + z new_z = head_pos.z + z
data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z) data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
self._output_device.put("printer/heads/0/position", data, onFinished=None) self._output_device.put("printer/heads/0/position", data, on_finished=None)
def homeBed(self, printer): def homeBed(self, printer):
self._output_device.put("printer/heads/0/position/z", "0", onFinished=None) self._output_device.put("printer/heads/0/position/z", "0", on_finished=None)
def _onPreheatBedTimerFinished(self): def _onPreheatBedTimerFinished(self):
self.setTargetBedTemperature(self._preheat_printer, 0) self.setTargetBedTemperature(self._preheat_printer, 0)
@ -89,7 +89,7 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
printer.updateIsPreheating(True) printer.updateIsPreheating(True)
return return
self._output_device.put("printer/bed/pre_heat", data, onFinished = self._onPutPreheatBedCompleted) self._output_device.put("printer/bed/pre_heat", data, on_finished = self._onPutPreheatBedCompleted)
printer.updateIsPreheating(True) printer.updateIsPreheating(True)
self._preheat_request_in_progress = True self._preheat_request_in_progress = True

View file

@ -26,6 +26,10 @@ UM.Dialog
{ {
resetPrintersModel() resetPrintersModel()
} }
else
{
OutputDevice.cancelPrintSelection()
}
} }
title: catalog.i18nc("@title:window", "Print over network") title: catalog.i18nc("@title:window", "Print over network")

View file

@ -16,9 +16,9 @@ Item
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
onClicked: OutputDevice.setActivePrinter(null) onClicked: OutputDevice.setActivePrinter(null)
z: 0 z: 0
} }
Button Button
@ -28,7 +28,7 @@ Item
anchors.bottomMargin: UM.Theme.getSize("default_margin").width anchors.bottomMargin: UM.Theme.getSize("default_margin").width
anchors.right: cameraImage.right anchors.right: cameraImage.right
// TODO: Harcoded sizes // TODO: Hardcoded sizes
width: 20 * screenScaleFactor width: 20 * screenScaleFactor
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
@ -89,9 +89,11 @@ Item
MouseArea MouseArea
{ {
anchors.fill: cameraImage anchors.fill: cameraImage
onClicked: { /* no-op */ } onClicked:
z: 1 {
OutputDevice.setActivePrinter(null)
}
z: 1
} }
} }

View file

@ -39,12 +39,12 @@ class SendMaterialJob(Job):
try: try:
remote_materials_list = json.loads(remote_materials_list) remote_materials_list = json.loads(remote_materials_list)
except json.JSONDecodeError: except json.JSONDecodeError:
Logger.log("e", "Current material storage on printer was a corrupted reply.") Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.")
return return
try: try:
remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID. remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID.
except KeyError: except KeyError:
Logger.log("e", "Current material storage on printer was an invalid reply (missing GUIDs).") Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.")
return return
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()

View file

@ -198,7 +198,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
instance_name = "manual:%s" % address instance_name = "manual:%s" % address
properties = { properties = {
b"name": system_info["name"].encode("utf-8"), b"name": (system_info["name"] + " (manual)").encode("utf-8"),
b"address": address.encode("utf-8"), b"address": address.encode("utf-8"),
b"firmware_version": system_info["firmware"].encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"),
b"manual": b"true", b"manual": b"true",

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker 3 printers.", "description": "Manages network connections to Ultimaker 3 printers.",
"version": "1.0.0", "version": "1.0.0",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job from UM.Job import Job
@ -21,7 +21,6 @@ class AutoDetectBaudJob(Job):
def run(self): def run(self):
Logger.log("d", "Auto detect baud rate started.") Logger.log("d", "Auto detect baud rate started.")
timeout = 3
wait_response_timeouts = [3, 15, 30] wait_response_timeouts = [3, 15, 30]
wait_bootloader_times = [1.5, 5, 15] wait_bootloader_times = [1.5, 5, 15]
write_timeout = 3 write_timeout = 3
@ -52,7 +51,7 @@ class AutoDetectBaudJob(Job):
if serial is None: if serial is None:
try: try:
serial = Serial(str(self._serial_port), baud_rate, timeout = read_timeout, writeTimeout = write_timeout) serial = Serial(str(self._serial_port), baud_rate, timeout = read_timeout, writeTimeout = write_timeout)
except SerialException as e: except SerialException:
Logger.logException("w", "Unable to create serial") Logger.logException("w", "Unable to create serial")
continue continue
else: else:
@ -72,7 +71,7 @@ class AutoDetectBaudJob(Job):
while timeout_time > time(): while timeout_time > time():
line = serial.readline() line = serial.readline()
if b"ok T:" in line: if b"ok " in line and b"T:" in line:
successful_responses += 1 successful_responses += 1
if successful_responses >= 3: if successful_responses >= 3:
self.setResult(baud_rate) self.setResult(baud_rate)

View file

@ -15,7 +15,7 @@ from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob from .AutoDetectBaudJob import AutoDetectBaudJob
from .avr_isp import stk500v2, intelHex from .avr_isp import stk500v2, intelHex
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QUrl
from serial import Serial, SerialException, SerialTimeoutException from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread, Event from threading import Thread, Event
@ -146,8 +146,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
@pyqtSlot(str) @pyqtSlot(str)
def updateFirmware(self, file): def updateFirmware(self, file):
# the file path is qurl encoded. # the file path could be url-encoded.
self._firmware_location = file.replace("file://", "") if file.startswith("file://"):
self._firmware_location = QUrl(file).toLocalFile()
else:
self._firmware_location = file
self.showFirmwareInterface() self.showFirmwareInterface()
self.setFirmwareUpdateState(FirmwareUpdateState.updating) self.setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start() self._update_firmware_thread.start()
@ -323,7 +326,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._firmware_name is None: if self._firmware_name is None:
self.sendCommand("M115") self.sendCommand("M115")
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed if (b"ok " in line and b"T:" in line) or b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
# Update all temperature values # Update all temperature values
matched_extruder_nrs = [] matched_extruder_nrs = []

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