Merge branch 'master' into simple_convex_hull

Conflicts:
	cura/BuildVolume.py
	cura/ConvexHullDecorator.py
	cura/ConvexHullJob.py
	cura/CuraApplication.py
This commit is contained in:
Simon Edwards 2016-06-21 14:47:10 +02:00
commit fd42a43270
215 changed files with 35977 additions and 5826 deletions

View file

@ -38,13 +38,9 @@ class BuildVolume(SceneNode):
self.setCalculateBoundingBox(False)
self._volume_aabb = None
self._active_profile = None
self._active_instance = None
Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveInstanceChanged)
self._onActiveInstanceChanged()
Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
self._onActiveProfileChanged()
self._active_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
self._onGlobalContainerStackChanged()
def setWidth(self, width):
if width: self._width = width
@ -77,7 +73,7 @@ class BuildVolume(SceneNode):
## Recalculates the build volume & disallowed areas.
def rebuild(self):
if self._width == 0 or self._height == 0 or self._depth == 0:
if not self._width or not self._height or not self._depth:
return
min_w = -self._width / 2
@ -149,9 +145,9 @@ class BuildVolume(SceneNode):
skirt_size = 0.0
profile = Application.getInstance().getMachineManager().getWorkingProfile()
if profile:
skirt_size = self._getSkirtSize(profile)
container_stack = Application.getInstance().getGlobalContainerStack()
if container_stack:
skirt_size = self._getSkirtSize(container_stack)
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
# This is probably wrong in all other cases. TODO!
@ -166,52 +162,48 @@ class BuildVolume(SceneNode):
def getBoundingBox(self):
return self._volume_aabb
def _onActiveInstanceChanged(self):
self._active_instance = Application.getInstance().getMachineManager().getActiveMachineInstance()
def _onGlobalContainerStackChanged(self):
if self._active_container_stack:
self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
if self._active_instance:
self._width = self._active_instance.getMachineSettingValue("machine_width")
if Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("print_sequence") == "one_at_a_time":
self._height = Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("gantry_height")
self._active_container_stack = Application.getInstance().getGlobalContainerStack()
if self._active_container_stack:
self._active_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
self._width = self._active_container_stack.getProperty("machine_width", "value")
if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
self._height = self._active_container_stack.getProperty("gantry_height", "value")
else:
self._height = self._active_instance.getMachineSettingValue("machine_height")
self._depth = self._active_instance.getMachineSettingValue("machine_depth")
self._height = self._active_container_stack.getProperty("machine_height", "value")
self._depth = self._active_container_stack.getProperty("machine_depth", "value")
self._updateDisallowedAreas()
self.rebuild()
def _onActiveProfileChanged(self):
if self._active_profile:
self._active_profile.settingValueChanged.disconnect(self._onSettingValueChanged)
def _onSettingPropertyChanged(self, setting_key, property_name):
if property_name != "value":
return
self._active_profile = Application.getInstance().getMachineManager().getWorkingProfile()
if self._active_profile:
self._active_profile.settingValueChanged.connect(self._onSettingValueChanged)
self._updateDisallowedAreas()
self.rebuild()
def _onSettingValueChanged(self, setting_key):
if setting_key == "print_sequence":
if Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("print_sequence") == "one_at_a_time":
self._height = Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("gantry_height")
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time":
self._height = self._active_container_stack.getProperty("gantry_height", "value")
else:
self._height = self._active_instance.getMachineSettingValue("machine_depth")
self._height = self._active_container_stack.getProperty("machine_height", "value")
self.rebuild()
if setting_key in self._skirt_settings:
self._updateDisallowedAreas()
self.rebuild()
def _updateDisallowedAreas(self):
if not self._active_instance or not self._active_profile:
if not self._active_container_stack:
return
disallowed_areas = self._active_instance.getMachineSettingValue("machine_disallowed_areas")
disallowed_areas = self._active_container_stack.getProperty("machine_disallowed_areas", "value")
areas = []
skirt_size = 0.0
if self._active_profile:
skirt_size = self._getSkirtSize(self._active_profile)
skirt_size = self._getSkirtSize(self._active_container_stack)
if disallowed_areas:
# Extend every area already in the disallowed_areas with the skirt size.
@ -232,8 +224,8 @@ class BuildVolume(SceneNode):
# Add the skirt areas around the borders of the build plate.
if skirt_size > 0:
half_machine_width = self._active_instance.getMachineSettingValue("machine_width") / 2
half_machine_depth = self._active_instance.getMachineSettingValue("machine_depth") / 2
half_machine_width = self._active_container_stack.getProperty("machine_width", "value") / 2
half_machine_depth = self._active_container_stack.getProperty("machine_depth", "value") / 2
areas.append(Polygon(numpy.array([
[-half_machine_width, -half_machine_depth],
@ -266,24 +258,24 @@ class BuildVolume(SceneNode):
self._disallowed_areas = areas
## Convenience function to calculate the size of the bed adhesion.
def _getSkirtSize(self, profile):
def _getSkirtSize(self, container_stack):
skirt_size = 0.0
adhesion_type = profile.getSettingValue("adhesion_type")
adhesion_type = container_stack.getProperty("adhesion_type", "value")
if adhesion_type == "skirt":
skirt_distance = profile.getSettingValue("skirt_gap")
skirt_line_count = profile.getSettingValue("skirt_line_count")
skirt_size = skirt_distance + (skirt_line_count * profile.getSettingValue("skirt_line_width"))
skirt_distance = container_stack.getProperty("skirt_gap", "value")
skirt_line_count = container_stack.getProperty("skirt_line_count", "value")
skirt_size = skirt_distance + (skirt_line_count * container_stack.getProperty("skirt_line_width", "value"))
elif adhesion_type == "brim":
skirt_size = profile.getSettingValue("brim_line_count") * profile.getSettingValue("skirt_line_width")
skirt_size = container_stack.getProperty("brim_line_count", "value") * container_stack.getProperty("skirt_line_width", "value")
elif adhesion_type == "raft":
skirt_size = profile.getSettingValue("raft_margin")
skirt_size = container_stack.getProperty("raft_margin", "value")
if profile.getSettingValue("draft_shield_enabled"):
skirt_size += profile.getSettingValue("draft_shield_dist")
if container_stack.getProperty("draft_shield_enabled", "value"):
skirt_size += container_stack.getProperty("draft_shield_dist", "value")
if profile.getSettingValue("xy_offset"):
skirt_size += profile.getSettingValue("xy_offset")
if container_stack.getProperty("xy_offset", "value"):
skirt_size += container_stack.getProperty("xy_offset", "value")
return skirt_size

View file

@ -0,0 +1,93 @@
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction
class ContainerSettingsModel(ListModel):
LabelRole = Qt.UserRole + 1
CategoryRole = Qt.UserRole + 2
UnitRole = Qt.UserRole + 3
ValuesRole = Qt.UserRole + 4
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.LabelRole, "label")
self.addRoleName(self.CategoryRole, "category")
self.addRoleName(self.UnitRole, "unit")
self.addRoleName(self.ValuesRole, "values")
self._container_ids = []
self._containers = []
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
self._update()
def _update(self):
self.clear()
if len(self._container_ids) == 0:
return
keys = []
for container in self._containers:
keys = keys + list(container.getAllKeys())
keys = list(set(keys)) # remove duplicate keys
keys.sort()
for key in keys:
definition = None
category = None
values = []
for container in self._containers:
instance = container.getInstance(key)
if instance:
definition = instance.definition
# Traverse up to find the category
category = definition
while category.type != "category":
category = category.parent
value = container.getProperty(key, "value")
if type(value) == SettingFunction:
values.append("=\u0192")
else:
values.append(container.getProperty(key, "value"))
else:
values.append("")
self.appendItem({
"key": key,
"values": values,
"label": definition.label,
"unit": definition.unit,
"category": category.label
})
## Set the ids of the containers which have the settings this model should list.
# Also makes sure the model updates when the containers have property changes
def setContainers(self, container_ids):
for container in self._containers:
container.propertyChanged.disconnect(self._onPropertyChanged)
self._container_ids = container_ids
self._containers = []
for container_id in self._container_ids:
containers = ContainerRegistry.getInstance().findContainers(id = container_id)
if containers:
containers[0].propertyChanged.connect(self._onPropertyChanged)
self._containers.append(containers[0])
self._update()
containersChanged = pyqtSignal()
@pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
def containers(self):
return self.container_ids

View file

@ -15,10 +15,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._convex_hull_node = None
self._init2DConvexHullCache()
self._profile = None
Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged)
self._onActiveProfileChanged()
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
## Force that a new (empty) object is created upon copy.
def __deepcopy__(self, memo):
@ -26,25 +25,31 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Get the unmodified 2D projected convex hull of the node
def getConvexHull(self):
if self._node is None:
return None
hull = self._compute2DConvexHull()
profile = Application.getInstance().getMachineManager().getWorkingProfile()
if profile:
if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_polygon"), numpy.float32)))
if self._global_stack and self._node:
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon"), numpy.float32)))
return hull
## Get the convex hull of the node with the full head size
def getConvexHullHeadFull(self):
if self._node is None:
return None
return self._compute2DConvexHeadFull()
## Get convex hull of the object + head size
# In case of printing all at once this is the same as the convex hull.
# For one at the time this is area with intersection of mirrored head
def getConvexHullHead(self):
profile = Application.getInstance().getMachineManager().getWorkingProfile()
if profile:
if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration(
"isGroup"):
if self._node is None:
return None
if self._global_stack:
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
return self._compute2DConvexHeadMin()
return None
@ -52,15 +57,19 @@ class ConvexHullDecorator(SceneNodeDecorator):
# In case of printing all at once this is the same as the convex hull.
# For one at the time this is the area without the head.
def getConvexHullBoundary(self):
profile = Application.getInstance().getMachineManager().getWorkingProfile()
if profile:
if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration(
"isGroup"):
if self._node is None:
return None
if self._global_stack:
if self._global_stack("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
# Printing one at a time and it's not an object in a group
return self._compute2DConvexHull()
return None
def recomputeConvexHull(self):
if self._node is None:
return None
convex_hull = self.getConvexHull()
if self._convex_hull_node:
if self._convex_hull_node.getHull() == convex_hull:
@ -70,23 +79,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
Application.getInstance().getController().getScene().getRoot())
self._convex_hull_node = hull_node
def _onActiveProfileChanged(self):
if self._profile:
self._profile.settingValueChanged.disconnect(self._onSettingValueChanged)
self._profile = Application.getInstance().getMachineManager().getWorkingProfile()
if self._profile:
self._profile.settingValueChanged.connect(self._onSettingValueChanged)
def _onActiveMachineInstanceChanged(self):
if self._convex_hull_node:
self._convex_hull_node.setParent(None)
self._convex_hull_node = None
def _onSettingValueChanged(self, setting):
if setting == "print_sequence":
self.recomputeConvexHull()
def _onSettingValueChanged(self, key, property_name):
if key == "print_sequence" and property_name == "value":
self._onChanged()
def _init2DConvexHullCache(self):
# Cache for the group code path in _compute2DConvexHull()
@ -178,8 +173,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
return rounded_hull
def _getHeadAndFans(self):
profile = Application.getInstance().getMachineManager().getWorkingProfile()
return Polygon(numpy.array(profile.getSettingValue("machine_head_with_fans_polygon"), numpy.float32))
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon"), numpy.float32))
def _compute2DConvexHeadFull(self):
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
@ -195,3 +189,19 @@ class ConvexHullDecorator(SceneNodeDecorator):
def _roundHull(self, convex_hull):
return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32)))
def _onChanged(self, *args):
self.recomputeConvexHull()
def _onGlobalStackChanged(self):
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
self._global_stack.containersChanged.disconnect(self._onChanged)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
self._global_stack.containersChanged.connect(self._onChanged)
self._onChanged()

View file

@ -5,17 +5,33 @@ import webbrowser
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# List of exceptions that should be considered "fatal" and abort the program.
# These are primarily some exception types that we simply cannot really recover from
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
fatal_exception_types = [
MemoryError,
SyntaxError,
ImportError,
SystemError,
]
def show(exception_type, value, tb):
debug_mode = False
if QCoreApplication.instance():
debug_mode = QCoreApplication.instance().getCommandLineOption("debug-mode", False)
traceback.print_exception(exception_type, value, tb)
Logger.log("c", "An uncaught exception has occurred!")
for line in traceback.format_exception(exception_type, value, tb):
for part in line.rstrip("\n").split("\n"):
Logger.log("c", part)
if not debug_mode:
if not debug_mode and exception_type not in fatal_exception_types:
return
application = QCoreApplication.instance()
@ -29,7 +45,7 @@ def show(exception_type, value, tb):
label = QLabel(dialog)
layout.addWidget(label)
label.setText(catalog.i18nc("@label", "<p>An uncaught exception has occurred!</p><p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>"))
label.setText(catalog.i18nc("@label", "<p>A fatal exception has occurred that we could not recover from!</p><p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>"))
textarea = QTextEdit(dialog)
layout.addWidget(textarea)

View file

@ -4,7 +4,7 @@
from UM.Qt.QtApplication import QtApplication
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Camera import Camera
from UM.Scene.Platform import Platform
from UM.Scene.Platform import Platform as Scene_Platform
from UM.Math.Vector import Vector
from UM.Math.Quaternion import Quaternion
from UM.Math.AxisAlignedBox import AxisAlignedBox
@ -14,18 +14,26 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Platform import Platform
from UM.JobQueue import JobQueue
from UM.SaveFile import SaveFile
from UM.Scene.Selection import Selection
from UM.Scene.GroupDecorator import GroupDecorator
import UM.Settings.Validator
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.SetParentOperation import SetParentOperation
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from . import ExtruderManager
from . import ExtrudersModel
from . import PlatformPhysics
from . import BuildVolume
from . import CameraAnimation
@ -34,20 +42,23 @@ from . import CuraActions
from . import MultiMaterialDecorator
from . import ZOffsetDecorator
from . import CuraSplashScreen
from . import MachineManagerModel
from . import ContainerSettingsModel
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtQml import qmlRegisterUncreatableType
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
import platform
import sys
import os.path
import numpy
import copy
import urllib
numpy.seterr(all="ignore")
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
if platform.system() == "Linux": # Needed for platform.linux_distribution, which is not available on Windows and OSX
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
if platform.linux_distribution()[0] in ("Ubuntu", ): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes
@ -55,15 +66,23 @@ if platform.system() == "Linux": # Needed for platform.linux_distribution, which
ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
try:
from cura.CuraVersion import CuraVersion
from cura.CuraVersion import CuraVersion, CuraBuildType
except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
class CuraApplication(QtApplication):
class ResourceTypes:
QmlFiles = Resources.UserType + 1
Firmware = Resources.UserType + 2
QualityInstanceContainer = Resources.UserType + 3
MaterialInstanceContainer = Resources.UserType + 4
VariantInstanceContainer = Resources.UserType + 5
UserInstanceContainer = Resources.UserType + 6
MachineStack = Resources.UserType + 7
ExtruderStack = Resources.UserType + 8
Q_ENUMS(ResourceTypes)
def __init__(self):
@ -73,7 +92,14 @@ class CuraApplication(QtApplication):
self._open_file_queue = [] # Files to open when plug-ins are loaded.
super().__init__(name = "cura", version = CuraVersion)
# Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True)
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True)
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@ -95,31 +121,109 @@ class CuraApplication(QtApplication):
self._i18n_catalog = None
self._previous_active_tool = None
self._platform_activity = False
self._scene_boundingbox = AxisAlignedBox.Null
self._scene_bounding_box = AxisAlignedBox.Null
self._job_name = None
self._center_after_select = False
self._camera_animation = None
self._cura_actions = None
self._started = False
self.getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineChanged)
self.getMachineManager().addMachineRequested.connect(self._onAddMachineRequested)
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
Preferences.getInstance().addPreference("cura/active_machine", "")
## Add the 4 types of profiles to storage.
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
# Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials.
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
empty_variant_container = copy.deepcopy(empty_container)
empty_variant_container._id = "empty_variant"
empty_variant_container.addMetaDataEntry("type", "variant")
ContainerRegistry.getInstance().addContainer(empty_variant_container)
empty_material_container = copy.deepcopy(empty_container)
empty_material_container._id = "empty_material"
empty_material_container.addMetaDataEntry("type", "material")
ContainerRegistry.getInstance().addContainer(empty_material_container)
empty_quality_container = copy.deepcopy(empty_container)
empty_quality_container._id = "empty_quality"
empty_quality_container.addMetaDataEntry("type", "quality")
ContainerRegistry.getInstance().addContainer(empty_quality_container)
ContainerRegistry.getInstance().load()
Preferences.getInstance().addPreference("cura/active_mode", "simple")
Preferences.getInstance().addPreference("cura/recent_files", "")
Preferences.getInstance().addPreference("cura/categories_expanded", "")
Preferences.getInstance().addPreference("cura/jobname_prefix", True)
Preferences.getInstance().addPreference("view/center_on_select", True)
Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
Preferences.getInstance().setDefault("local_file/last_used_type", "text/x-gcode")
Preferences.getInstance().setDefault("general/visible_settings", """
machine_settings
resolution
layer_height
shell
wall_thickness
top_bottom_thickness
infill
infill_sparse_density
material
material_print_temperature
material_bed_temperature
material_diameter
material_flow
retraction_enable
speed
speed_print
speed_travel
acceleration_print
acceleration_travel
jerk_print
jerk_travel
travel
cooling
cool_fan_enabled
support
support_enable
support_type
support_roof_density
platform_adhesion
adhesion_type
brim_width
raft_airgap
layer_0_z_overlap
raft_surface_layers
meshfix
blackmagic
print_sequence
dual
experimental
""".replace("\n", ";").replace(" ", ""))
JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
self.applicationShuttingDown.connect(self.saveSettings)
self._recent_files = []
files = Preferences.getInstance().getValue("cura/recent_files").split(";")
for f in files:
@ -128,6 +232,65 @@ class CuraApplication(QtApplication):
self._recent_files.append(QUrl.fromLocalFile(f))
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
#
# Note that the AutoSave plugin also calls this method.
def saveSettings(self):
if not self._started: # Do not do saving during application start
return
for instance in ContainerRegistry.getInstance().findInstanceContainers():
if not instance.isDirty():
continue
try:
data = instance.serialize()
except NotImplementedError:
continue
except Exception:
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
continue
file_name = urllib.parse.quote_plus(instance.getId()) + ".inst.cfg"
instance_type = instance.getMetaDataEntry("type")
path = None
if instance_type == "material":
path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
elif instance_type == "quality":
path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
elif instance_type == "user":
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
elif instance_type == "variant":
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
if path:
with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data)
for stack in ContainerRegistry.getInstance().findContainerStacks():
if not stack.isDirty():
continue
try:
data = stack.serialize()
except NotImplementedError:
continue
except Exception:
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
continue
file_name = urllib.parse.quote_plus(stack.getId()) + ".stack.cfg"
stack_type = stack.getMetaDataEntry("type", None)
path = None
if not stack_type or stack_type == "machine":
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
elif stack_type == "extruder_train":
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
if path:
with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data)
@pyqtSlot(result = QUrl)
def getDefaultPath(self):
return QUrl.fromLocalFile(os.path.expanduser("~/"))
@ -135,6 +298,8 @@ class CuraApplication(QtApplication):
## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistery
def _loadPlugins(self):
self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
if not hasattr(sys, "frozen"):
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
@ -176,7 +341,7 @@ class CuraApplication(QtApplication):
Selection.selectionChanged.connect(self.onSelectionChanged)
root = controller.getScene().getRoot()
self._platform = Platform(root)
self._platform = Scene_Platform(root)
self._volume = BuildVolume.BuildVolume(root)
@ -197,7 +362,13 @@ class CuraApplication(QtApplication):
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
ExtruderManager.ExtruderManager.getInstance()
qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
MachineManagerModel.createMachineManagerModel)
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
self.initializeEngine()
if self._engine.rootObjects:
@ -208,6 +379,8 @@ class CuraApplication(QtApplication):
for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
self._openFile(file_name)
self._started = True
self.exec_()
## Handle Qt events
@ -224,6 +397,9 @@ class CuraApplication(QtApplication):
def getPrintInformation(self):
return self._print_information
## Registers objects for the QML engine to use.
#
# \param engine The QML engine.
def registerObjects(self, engine):
engine.rootContext().setContextProperty("Printer", self)
self._print_information = PrintInformation.PrintInformation()
@ -233,6 +409,21 @@ class CuraApplication(QtApplication):
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
qmlRegisterType(ExtrudersModel.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(ContainerSettingsModel.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.ExtruderManager.getInstance())
for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
type_name = os.path.splitext(os.path.basename(path))[0]
if type_name in ("Cura", "Actions"):
continue
qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
def onSelectionChanged(self):
if Selection.hasSelection():
if not self.getController().getActiveTool():
@ -267,46 +458,33 @@ class CuraApplication(QtApplication):
@pyqtProperty(str, notify = sceneBoundingBoxChanged)
def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_boundingbox.width.item(), 'depth': self._scene_boundingbox.depth.item(), 'height' : self._scene_boundingbox.height.item()}
return self._i18n_catalog.i18nc("@info", "%(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()}
def updatePlatformActivity(self, node = None):
count = 0
scene_boundingbox = None
scene_bounding_box = None
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode or not node.getMeshData():
continue
count += 1
if not scene_boundingbox:
scene_boundingbox = node.getBoundingBox()
if not scene_bounding_box:
scene_bounding_box = node.getBoundingBox()
else:
other_bb = node.getBoundingBox()
if other_bb is not None:
scene_boundingbox = scene_boundingbox + node.getBoundingBox()
scene_bounding_box = scene_bounding_box + node.getBoundingBox()
if not scene_boundingbox:
scene_boundingbox = AxisAlignedBox.Null
if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null
if repr(self._scene_boundingbox) != repr(scene_boundingbox):
self._scene_boundingbox = scene_boundingbox
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
self._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit()
self._platform_activity = True if count > 0 else False
self.activityChanged.emit()
@pyqtSlot(str)
def setJobName(self, name):
name = os.path.splitext(name)[0] #when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its extension. This cuts the extension off if nescessary.
if self._job_name != name:
self._job_name = name
self.jobNameChanged.emit()
jobNameChanged = pyqtSignal()
@pyqtProperty(str, notify = jobNameChanged)
def jobName(self):
return self._job_name
# Remove all selected objects from the scene.
@pyqtSlot()
def deleteSelection(self):
@ -331,7 +509,7 @@ class CuraApplication(QtApplication):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if node:
@ -350,7 +528,7 @@ class CuraApplication(QtApplication):
def multiplyObject(self, object_id, count):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if node:
@ -372,7 +550,7 @@ class CuraApplication(QtApplication):
@pyqtSlot("quint64")
def centerObject(self, object_id):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: #Workaround for tool handles overlapping the selected object
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if not node:
@ -385,7 +563,7 @@ class CuraApplication(QtApplication):
op = SetTransformOperation(node, Vector())
op.push()
## Delete all mesh data on the scene.
## Delete all nodes containing mesh data in the scene.
@pyqtSlot()
def deleteAll(self):
if not self.getController().getToolsEnabled():
@ -396,9 +574,9 @@ class CuraApplication(QtApplication):
if type(node) is not SceneNode:
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue #Node that doesnt have a mesh and is not a group.
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node)
if nodes:
op = GroupedOperation()
@ -416,9 +594,9 @@ class CuraApplication(QtApplication):
if type(node) is not SceneNode:
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue #Node that doesnt have a mesh and is not a group.
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node)
@ -438,9 +616,9 @@ class CuraApplication(QtApplication):
if type(node) is not SceneNode:
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue #Node that doesnt have a mesh and is not a group.
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue #Grouped nodes don't need resetting as their parent (the group) is resetted)
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node)
if nodes:
@ -479,7 +657,7 @@ class CuraApplication(QtApplication):
## Get logging data of the backend engine
# \returns \type{string} Logging data
@pyqtSlot(result=str)
@pyqtSlot(result = str)
def getEngineLog(self):
log = ""
@ -509,21 +687,6 @@ class CuraApplication(QtApplication):
def expandedCategories(self):
return Preferences.getInstance().getValue("cura/categories_expanded").split(";")
@pyqtSlot(str, result = "QVariant")
def getSettingValue(self, key):
if not self.getMachineManager().getWorkingProfile():
return None
return self.getMachineManager().getWorkingProfile().getSettingValue(key)
#return self.getActiveMachine().getSettingValueByKey(key)
## Change setting by key value pair
@pyqtSlot(str, "QVariant")
def setSettingValue(self, key, value):
if not self.getMachineManager().getWorkingProfile():
return
self.getMachineManager().getWorkingProfile().setSettingValue(key, value)
@pyqtSlot()
def mergeSelected(self):
self.groupSelected()
@ -542,9 +705,10 @@ class CuraApplication(QtApplication):
# Use the previously found center of the group bounding box as the new location of the group
group_node.setPosition(group_node.getBoundingBox().center)
@pyqtSlot()
def groupSelected(self):
# Create a group-node
group_node = SceneNode()
group_decorator = GroupDecorator()
group_node.addDecorator(group_decorator)
@ -554,40 +718,34 @@ class CuraApplication(QtApplication):
group_node.setPosition(center)
group_node.setCenterPosition(center)
for node in Selection.getAllSelectedObjects():
world = node.getWorldPosition()
node.setParent(group_node)
node.setPosition(world - center)
# Move selected nodes into the group-node
Selection.applyOperation(SetParentOperation, group_node)
# Deselect individual nodes and select the group-node instead
for node in group_node.getChildren():
Selection.remove(node)
Selection.add(group_node)
@pyqtSlot()
def ungroupSelected(self):
ungrouped_nodes = []
selected_objects = Selection.getAllSelectedObjects()[:] #clone the list
selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects:
if node.callDecoration("isGroup" ):
children_to_move = []
for child in node.getChildren():
if type(child) is SceneNode:
children_to_move.append(child)
if node.callDecoration("isGroup"):
op = GroupedOperation()
for child in children_to_move:
position = child.getWorldPosition()
child.setParent(node.getParent())
child.setPosition(position - node.getParent().getWorldPosition())
child.scale(node.getScale())
child.rotate(node.getOrientation())
group_parent = node.getParent()
children = node.getChildren().copy()
for child in children:
# Set the parent of the children to the parent of the group-node
op.addOperation(SetParentOperation(child, group_parent))
# Add all individual nodes to the selection
Selection.add(child)
child.callDecoration("setConvexHull",None)
node.setParent(None)
ungrouped_nodes.append(node)
for node in ungrouped_nodes:
Selection.remove(node)
child.callDecoration("setConvexHull", None)
op.push()
# Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged
def _createSplashScreen(self):
return CuraSplashScreen.CuraSplashScreen()
@ -595,10 +753,12 @@ class CuraApplication(QtApplication):
def _onActiveMachineChanged(self):
pass
fileLoaded = pyqtSignal(str)
def _onFileLoaded(self, job):
node = job.getResult()
if node != None:
self.setJobName(os.path.basename(job.getFileName()))
self.fileLoaded.emit(job.getFileName())
node.setSelectable(True)
node.setName(os.path.basename(job.getFileName()))
op = AddSceneNodeOperation(node, self.getController().getScene().getRoot())
@ -628,14 +788,15 @@ class CuraApplication(QtApplication):
def _reloadMeshFinished(self, job):
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
job._node.setMeshData(job.getResult().getMeshData())
#job.getResult().setParent(self.getController().getScene().getRoot())
#job._node.setParent(self.getController().getScene().getRoot())
#job._node.meshDataChanged.emit(job._node)
def _openFile(self, file):
job = ReadMeshJob(os.path.abspath(file))
job.finished.connect(self._onFileLoaded)
job.start()
def _onAddMachineRequested(self):
self.requestAddPrinter.emit()
def _addProfileReader(self, profile_reader):
# TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
pass
def _addProfileWriter(self, profile_writer):
pass

View file

@ -0,0 +1,209 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import os
import os.path
import re
from PyQt5.QtWidgets import QMessageBox
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry #For getting the possible profile writers to write with.
from UM.Util import parseBool
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class CuraContainerRegistry(ContainerRegistry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
## Create a name that is not empty and unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param current_name \type{} Current name of the container, which may be an acceptable option
# \param new_name \type{string} Base name, which may not be unique
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
# \return \type{string} Name that is unique for the specified type and name/id
def createUniqueName(self, container_type, current_name, new_name, fallback_name):
new_name = new_name.strip()
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
if num_check:
new_name = num_check.group(1)
if new_name == "":
new_name = fallback_name
unique_name = new_name
i = 1
# In case we are renaming, the current name of the container is also a valid end-result
while self._containerExists(container_type, unique_name) and unique_name != current_name:
i += 1
unique_name = "%s #%d" % (new_name, i)
return unique_name
## Check if a container with of a certain type and a certain name or id exists
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param container_name \type{string} Name to check
def _containerExists(self, container_type, container_name):
container_class = ContainerStack if container_type == "machine" else InstanceContainer
return self.findContainers(container_class, id = container_name, type = container_type) or \
self.findContainers(container_class, name = container_name, type = container_type)
## Exports an profile to a file
#
# \param instance_id \type{str} the ID of the profile to export.
# \param file_name \type{str} the full path and filename to export to.
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
def exportProfile(self, instance_id, file_name, file_type):
Logger.log('d', 'exportProfile instance_id: '+str(instance_id))
# Parse the fileType to deduce what plugin can save the file format.
# fileType has the format "<description> (*.<extension>)"
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
if split < 0: # Not found. Invalid format.
Logger.log("e", "Invalid file format identifier %s", file_type)
return
description = file_type[:split]
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
file_name += "." + extension
# On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
if not Platform.isWindows():
if os.path.exists(file_name):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
if result == QMessageBox.No:
return
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
if not containers:
return
container = containers[0]
profile_writer = self._findProfileWriter(extension, description)
try:
success = profile_writer.write(file_name, container)
except Exception as e:
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0)
m.show()
return
if not success:
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0)
m.show()
return
m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name))
m.show()
## Gets the plugin object matching the criteria
# \param extension
# \param description
# \return The plugin object matching the given extension and description.
def _findProfileWriter(self, extension, description):
plugin_registry = PluginRegistry.getInstance()
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
supported_extension = supported_type.get("extension", None)
if supported_extension == extension: # This plugin supports a file type with the same extension.
supported_description = supported_type.get("description", None)
if supported_description == description: # The description is also identical. Assume it's the same file type.
return plugin_registry.getPluginObject(plugin_id)
return None
## Imports a profile from a file
#
# \param file_name \type{str} the full path and filename of the profile to import
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
# containing a message for the user
def importProfile(self, file_name):
if not file_name:
return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
plugin_registry = PluginRegistry.getInstance()
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
profile_reader = plugin_registry.getPluginObject(plugin_id)
try:
profile = profile_reader.read(file_name) #Try to open the file with the profile reader.
except Exception as e:
#Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s", file_name, str(e))
return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
if profile: #Success!
profile.setReadOnly(False)
new_name = self.createUniqueName("quality", "", os.path.splitext(os.path.basename(file_name))[0],
catalog.i18nc("@label", "Custom profile"))
profile.setName(new_name)
profile._id = new_name
if self._machineHasOwnQualities():
profile.setDefinition(self._activeDefinition())
if self._machineHasOwnMaterials():
profile.addMetaDataEntry("material", self._activeMaterialId())
else:
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
ContainerRegistry.getInstance().addContainer(profile)
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
#If it hasn't returned by now, none of the plugins loaded the profile successfully.
return { "status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type.", file_name)}
## Gets a list of profile writer plugins
# \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type):
plugin_registry = PluginRegistry.getInstance()
active_plugin_ids = plugin_registry.getActivePlugins()
result = []
for plugin_id in active_plugin_ids:
meta_data = plugin_registry.getMetaData(plugin_id)
if io_type in meta_data:
result.append( (plugin_id, meta_data) )
return result
## Gets the active definition
# \return the active definition object or None if there is no definition
def _activeDefinition(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
definition = global_container_stack.getBottom()
if definition:
return definition
return None
## Returns true if the current machine requires its own materials
# \return True if the current machine requires its own materials
def _machineHasOwnMaterials(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return global_container_stack.getMetaDataEntry("has_materials", False)
return False
## Gets the ID of the active material
# \return the ID of the active material or the empty string
def _activeMaterialId(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
material = global_container_stack.findContainer({"type": "material"})
if material:
return material.getId()
return ""
## Returns true if the current machien requires its own quality profiles
# \return true if the current machien requires its own quality profiles
def _machineHasOwnQualities(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
return False

View file

@ -21,6 +21,9 @@ class CuraSplashScreen(QSplashScreen):
painter.setPen(QColor(0, 0, 0, 255))
version = Application.getInstance().getVersion().split("-")
buildtype = Application.getInstance().getBuildType()
if buildtype:
version[0] += " (%s)" %(buildtype)
painter.setFont(QFont("Proxima Nova Rg", 20 ))
painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])

View file

@ -2,3 +2,4 @@
# Cura is released under the terms of the AGPLv3 or higher.
CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@"

221
cura/ExtruderManager.py Normal file
View file

@ -0,0 +1,221 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject, QVariant #For communicating data and events to Qt.
import UM.Application #To get the global container stack to find the current machine.
import UM.Logger
import UM.Settings.ContainerRegistry #Finding containers by ID.
## Manages all existing extruder stacks.
#
# This keeps a list of extruder stacks for each machine.
class ExtruderManager(QObject):
## Signal to notify other components when the list of extruders changes.
extrudersChanged = pyqtSignal(QVariant)
## Notify when the user switches the currently active extruder.
activeExtruderChanged = pyqtSignal()
## Registers listeners and such to listen to changes to the extruders.
def __init__(self, parent = None):
super().__init__(parent)
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
self._active_extruder_index = 0
UM.Application.getInstance().globalContainerStackChanged.connect(self._addCurrentMachineExtruders)
## Gets the unique identifier of the currently active extruder stack.
#
# The currently active extruder stack is the stack that is currently being
# edited.
#
# \return The unique ID of the currently active extruder stack.
@pyqtProperty(str, notify = activeExtruderChanged)
def activeExtruderStackId(self):
if not UM.Application.getInstance().getGlobalContainerStack():
return None #No active machine, so no active extruder.
try:
return self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getBottom().getId()][str(self._active_extruder_index)].getId()
except KeyError: #Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None
## The instance of the singleton pattern.
#
# It's None if the extruder manager hasn't been created yet.
__instance = None
## Gets an instance of the extruder manager, or creates one if no instance
# exists yet.
#
# This is an implementation of singleton. If an extruder manager already
# exists, it is re-used.
#
# \return The extruder manager.
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = ExtruderManager()
return cls.__instance
## Changes the active extruder by index.
#
# \param index The index of the new active extruder.
@pyqtSlot(int)
def setActiveExtruderIndex(self, index):
self._active_extruder_index = index
self.activeExtruderChanged.emit()
def getActiveExtruderStack(self):
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
if global_container_stack:
global_definition_container = UM.Application.getInstance().getGlobalContainerStack().getBottom()
if global_definition_container:
if global_definition_container.getId() in self._extruder_trains:
if str(self._active_extruder_index) in self._extruder_trains[global_definition_container.getId()]:
return self._extruder_trains[global_definition_container.getId()][str(self._active_extruder_index)]
## Adds all extruders of a specific machine definition to the extruder
# manager.
#
# \param machine_definition The machine to add the extruders for.
def addMachineExtruders(self, machine_definition):
machine_id = machine_definition.getId()
if machine_id not in self._extruder_trains:
self._extruder_trains[machine_id] = { }
container_registry = UM.Settings.ContainerRegistry.getInstance()
if not container_registry: #Then we shouldn't have any machine definition either. In any case, there are no extruder trains then so bye bye.
return
#Add the extruder trains that don't exist yet.
for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition.getId()):
position = extruder_definition.getMetaDataEntry("position", None)
if not position:
UM.Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
if not container_registry.findContainerStacks(machine = machine_id, position = position): #Doesn't exist yet.
self.createExtruderTrain(extruder_definition, machine_definition, position)
#Gets the extruder trains that we just created as well as any that still existed.
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_definition.getId())
for extruder_train in extruder_trains:
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
#Ensure that the extruder train stacks are linked to global stack.
extruder_train.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
if extruder_trains:
self.extrudersChanged.emit(machine_definition)
## Creates a container stack for an extruder train.
#
# The container stack has an extruder definition at the bottom, which is
# linked to a machine definition. Then it has a nozzle profile, a material
# profile, a quality profile and a user profile, in that order.
#
# The resulting container stack is added to the registry.
#
# \param extruder_definition The extruder to create the extruder train
# for.
# \param machine_definition The machine that the extruder train belongs
# to.
# \param position The position of this extruder train in the extruder
# slots of the machine.
def createExtruderTrain(self, extruder_definition, machine_definition, position):
#Cache some things.
container_registry = UM.Settings.ContainerRegistry.getInstance()
machine_id = machine_definition.getId()
#Create a container stack for this extruder.
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
container_stack = UM.Settings.ContainerStack(extruder_stack_id)
container_stack.setName(extruder_definition.getName()) #Take over the display name to display the stack with.
container_stack.addMetaDataEntry("type", "extruder_train")
container_stack.addMetaDataEntry("machine", machine_definition.getId())
container_stack.addMetaDataEntry("position", position)
container_stack.addContainer(extruder_definition)
#Find the nozzle to use for this extruder.
nozzle = container_registry.getEmptyInstanceContainer()
if machine_definition.getMetaDataEntry("has_nozzles", default = "False") == "True":
#First add any nozzle. Later, overwrite with preference if the preference is valid.
nozzles = container_registry.findInstanceContainers(machine = machine_id, type = "nozzle")
if len(nozzles) >= 1:
nozzle = nozzles[0]
preferred_nozzle_id = machine_definition.getMetaDataEntry("preferred_nozzle")
if preferred_nozzle_id:
preferred_nozzles = container_registry.findInstanceContainers(id = preferred_nozzle_id, type = "nozzle")
if len(preferred_nozzles) >= 1:
nozzle = preferred_nozzles[0]
else:
UM.Logger.log("w", "The preferred nozzle \"%s\" of machine %s doesn't exist or is not a nozzle profile.", preferred_nozzle_id, machine_id)
#And leave it at the default nozzle.
container_stack.addContainer(nozzle)
#Find a material to use for this nozzle.
material = container_registry.getEmptyInstanceContainer()
if machine_definition.getMetaDataEntry("has_materials", default = "False") == "True":
#First add any material. Later, overwrite with preference if the preference is valid.
if machine_definition.getMetaDataEntry("has_nozzle_materials", default = "False") == "True":
materials = container_registry.findInstanceContainers(type = "material", machine = machine_id, nozzle = nozzle.getId())
else:
materials = container_registry.findInstanceContainers(type = "material", machine = machine_id)
if len(materials) >= 1:
material = materials[0]
preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
if preferred_material_id:
preferred_materials = container_registry.findInstanceContainers(id = preferred_material_id, type = "material")
if len(preferred_materials) >= 1:
material = preferred_materials[0]
else:
UM.Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
#And leave it at the default material.
container_stack.addContainer(material)
#Find a quality to use for this extruder.
quality = container_registry.getEmptyInstanceContainer()
#First add any quality. Later, overwrite with preference if the preference is valid.
qualities = container_registry.findInstanceContainers(type = "quality")
if len(qualities) >= 1:
quality = qualities[0]
preferred_quality_id = machine_definition.getMetaDataEntry("preferred_quality")
if preferred_quality_id:
preferred_quality = container_registry.findInstanceContainers(id = preferred_quality_id, type = "quality")
if len(preferred_quality) >= 1:
quality = preferred_quality[0]
else:
UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality_id, machine_id)
#And leave it at the default quality.
container_stack.addContainer(quality)
user_profile = container_registry.findInstanceContainers(id = extruder_stack_id + "_current_settings")
if user_profile: #There was already a user profile, loaded from settings.
user_profile = user_profile[0]
else:
user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings") #Add an empty user profile.
user_profile.addMetaDataEntry("type", "user")
user_profile.setDefinition(machine_definition)
container_registry.addContainer(user_profile)
container_stack.addContainer(user_profile)
container_stack.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
container_registry.addContainer(container_stack)
## Generates extruders for a specific machine.
#
# \param machine_id The machine to get the extruders of.
def getMachineExtruders(self, machine_id):
if machine_id not in self._extruder_trains:
UM.Logger.log("w", "Tried to get the extruder trains for machine %s, which doesn't exist.", machine_id)
return
for name in self._extruder_trains[machine_id]:
yield self._extruder_trains[machine_id][name]
## Adds the extruders of the currently active machine.
def _addCurrentMachineExtruders(self):
global_stack = UM.Application.getInstance().getGlobalContainerStack()
if global_stack and global_stack.getBottom():
self.addMachineExtruders(global_stack.getBottom())

104
cura/ExtrudersModel.py Normal file
View file

@ -0,0 +1,104 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
import cura.ExtruderManager
import UM.Qt.ListModel
## Model that holds extruders.
#
# This model is designed for use by any list of extruders, but specifically
# intended for drop-down lists of the current machine's extruders in place of
# settings.
class ExtrudersModel(UM.Qt.ListModel.ListModel):
# The ID of the container stack for the extruder.
IdRole = Qt.UserRole + 1
## Human-readable name of the extruder.
NameRole = Qt.UserRole + 2
## Colour of the material loaded in the extruder.
ColourRole = Qt.UserRole + 3
## Index of the extruder, which is also the value of the setting itself.
#
# An index of 0 indicates the first extruder, an index of 1 the second
# one, and so on. This is the value that will be saved in instance
# containers.
IndexRole = Qt.UserRole + 4
## List of colours to display if there is no material or the material has no known
# colour.
defaultColours = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
## Initialises the extruders model, defining the roles and listening for
# changes in the data.
#
# \param parent Parent QtObject of this list.
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ColourRole, "colour")
self.addRoleName(self.IndexRole, "index")
self._add_global = False
#Listen to changes.
manager = cura.ExtruderManager.ExtruderManager.getInstance()
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
UM.Application.globalContainerStackChanged.connect(self._updateExtruders) #When the current machine changes.
self._updateExtruders()
def setAddGlobal(self, add):
if add != self._add_global:
self._add_global = add
self._updateExtruders()
self.addGlobalChanged.emit()
addGlobalChanged = pyqtSignal()
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
def addGlobal(self):
return self._add_global
## Update the list of extruders.
#
# This should be called whenever the list of extruders changes.
def _updateExtruders(self):
self.clear()
manager = cura.ExtruderManager.ExtruderManager.getInstance()
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
if not global_container_stack:
return #There is no machine to get the extruders of.
if self._add_global:
material = global_container_stack.findContainer({ "type": "material" })
colour = material.getMetaDataEntry("color_code", default = self.defaultColours[0]) if material else self.defaultColours[0]
item = {
"id": global_container_stack.getId(),
"name": "Global",
"colour": colour,
"index": -1
}
self.appendItem(item)
for extruder in manager.getMachineExtruders(global_container_stack.getBottom().getId()):
material = extruder.findContainer({ "type": "material" })
position = extruder.getBottom().getMetaDataEntry("position", default = "0") #Position in the definition.
try:
position = int(position)
except ValueError: #Not a proper int.
position = -1
default_colour = self.defaultColours[position] if position >= 0 and position < len(self.defaultColours) else defaultColours[0]
colour = material.getMetaDataEntry("color_code", default = default_colour) if material else default_colour
item = { #Construct an item with only the relevant information.
"id": extruder.getId(),
"name": extruder.getName(),
"colour": colour,
"index": position
}
self.appendItem(item)
self.sort(lambda item: item["index"])

552
cura/MachineManagerModel.py Normal file
View file

@ -0,0 +1,552 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Application import Application
from UM.Preferences import Preferences
import UM.Settings
from UM.Settings.Validator import ValidatorState
from UM.Settings.InstanceContainer import InstanceContainer
from cura.PrinterOutputDevice import PrinterOutputDevice
from UM.Settings.ContainerStack import ContainerStack
from . import ExtruderManager
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class MachineManagerModel(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._active_container_stack = None
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._global_stack_valid = None
self._onGlobalContainerChanged()
ExtruderManager.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self.globalContainerChanged.connect(self._onActiveExtruderStackChanged)
self._onActiveExtruderStackChanged()
## When the global container is changed, active material probably needs to be updated.
self.globalContainerChanged.connect(self.activeMaterialChanged)
self.globalContainerChanged.connect(self.activeVariantChanged)
self.globalContainerChanged.connect(self.activeQualityChanged)
ExtruderManager.ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
ExtruderManager.ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
ExtruderManager.ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
self.globalContainerChanged.connect(self.activeStackChanged)
self.globalValueChanged.connect(self.activeStackChanged)
ExtruderManager.ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged)
self._empty_variant_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
self._empty_material_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
self._empty_quality_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
Preferences.getInstance().addPreference("cura/active_machine", "")
active_machine_id = Preferences.getInstance().getValue("cura/active_machine")
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
if active_machine_id != "":
# An active machine was saved, so restore it.
self.setActiveMachine(active_machine_id)
pass
globalContainerChanged = pyqtSignal()
activeMaterialChanged = pyqtSignal()
activeVariantChanged = pyqtSignal()
activeQualityChanged = pyqtSignal()
activeStackChanged = pyqtSignal()
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
globalValidationChanged = pyqtSignal() # Emitted whenever a validation inside global container is changed
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
outputDevicesChanged = pyqtSignal()
def _onOutputDevicesChanged(self):
self.outputDevicesChanged.emit()
def _onGlobalPropertyChanged(self, key, property_name):
if property_name == "value":
self.globalValueChanged.emit()
if property_name == "validationState":
if self._global_stack_valid:
changed_validation_state = self._active_container_stack.getProperty(key, property_name)
if changed_validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
self._global_stack_valid = False
self.globalValidationChanged.emit()
else:
new_validation_state = self._checkStackForErrors(self._active_container_stack)
if new_validation_state:
self._global_stack_valid = True
self.globalValidationChanged.emit()
def _onGlobalContainerChanged(self):
if self._global_container_stack:
self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
self._global_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
self.globalContainerChanged.emit()
if self._global_container_stack:
Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId())
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
self._global_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
self._global_stack_valid = not self._checkStackForErrors(self._global_container_stack)
def _onActiveExtruderStackChanged(self):
if self._active_container_stack and self._active_container_stack != self._global_container_stack:
self._active_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
self._active_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
self._active_container_stack = ExtruderManager.ExtruderManager.getInstance().getActiveExtruderStack()
if self._active_container_stack:
self._active_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
self._active_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
else:
self._active_container_stack = self._global_container_stack
def _onInstanceContainersChanged(self, container):
container_type = container.getMetaDataEntry("type")
if container_type == "material":
self.activeMaterialChanged.emit()
elif container_type == "variant":
self.activeVariantChanged.emit()
elif container_type == "quality":
self.activeQualityChanged.emit()
@pyqtSlot(str)
def setActiveMachine(self, stack_id):
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
if containers:
Application.getInstance().setGlobalContainerStack(containers[0])
@pyqtSlot(str, str)
def addMachine(self, name, definition_id):
definitions = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
if definitions:
definition = definitions[0]
name = self._createUniqueName("machine", "", name, definition.getName())
new_global_stack = UM.Settings.ContainerStack(name)
new_global_stack.addMetaDataEntry("type", "machine")
UM.Settings.ContainerRegistry.getInstance().addContainer(new_global_stack)
variant_instance_container = self._updateVariantContainer(definition)
material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
quality_instance_container = self._updateQualityContainer(definition, material_instance_container)
current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
current_settings_instance_container.addMetaDataEntry("machine", name)
current_settings_instance_container.addMetaDataEntry("type", "user")
current_settings_instance_container.setDefinition(definitions[0])
UM.Settings.ContainerRegistry.getInstance().addContainer(current_settings_instance_container)
# If a definition is found, its a list. Should only have one item.
new_global_stack.addContainer(definition)
if variant_instance_container:
new_global_stack.addContainer(variant_instance_container)
if material_instance_container:
new_global_stack.addContainer(material_instance_container)
if quality_instance_container:
new_global_stack.addContainer(quality_instance_container)
new_global_stack.addContainer(current_settings_instance_container)
ExtruderManager.ExtruderManager.getInstance().addMachineExtruders(definition)
Application.getInstance().setGlobalContainerStack(new_global_stack)
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
def printerOutputDevices(self):
return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
## Create a name that is not empty and unique
# \param container_type \type{string} Type of the container (machine, quality, ...)
# \param current_name \type{} Current name of the container, which may be an acceptable option
# \param new_name \type{string} Base name, which may not be unique
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
# \return \type{string} Name that is unique for the specified type and name/id
def _createUniqueName(self, container_type, current_name, new_name, fallback_name):
return UM.Settings.ContainerRegistry.getInstance().createUniqueName(container_type, current_name, new_name, fallback_name)
## Convenience function to check if a stack has errors.
def _checkStackForErrors(self, stack):
if stack is None:
return False
for key in stack.getAllKeys():
validation_state = stack.getProperty(key, "validationState")
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
return True
return False
## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
@pyqtSlot()
def clearUserSettings(self):
if not self._active_container_stack:
return
self.blurSettings.emit()
user_settings = self._active_container_stack.getTop()
user_settings.clear()
## Check if the global_container has instances in the user container
@pyqtProperty(bool, notify = activeStackChanged)
def hasUserSettings(self):
if not self._active_container_stack:
return
user_settings = self._active_container_stack.getTop().findInstances(**{})
return len(user_settings) != 0
## Check if the global profile does not contain error states
# Note that the _global_stack_valid is cached due to performance issues
# Calling _checkStackForErrors on every change is simply too expensive
@pyqtProperty(bool, notify = globalValidationChanged)
def isGlobalStackValid(self):
return self._global_stack_valid
@pyqtProperty(str, notify = activeStackChanged)
def activeUserProfileId(self):
if self._active_container_stack:
return self._active_container_stack.getTop().getId()
return ""
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineName(self):
if self._global_container_stack:
return self._global_container_stack.getName()
return ""
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineId(self):
if self._global_container_stack:
return self._global_container_stack.getId()
return ""
@pyqtProperty(str, notify = activeMaterialChanged)
def activeMaterialName(self):
if self._active_container_stack:
material = self._active_container_stack.findContainer({"type":"material"})
if material:
return material.getName()
return ""
@pyqtProperty(str, notify=activeMaterialChanged)
def activeMaterialId(self):
if self._active_container_stack:
material = self._active_container_stack.findContainer({"type": "material"})
if material:
return material.getId()
return ""
@pyqtProperty(str, notify=activeQualityChanged)
def activeQualityName(self):
if self._active_container_stack:
quality = self._active_container_stack.findContainer({"type": "quality"})
if quality:
return quality.getName()
return ""
@pyqtProperty(str, notify=activeQualityChanged)
def activeQualityId(self):
if self._active_container_stack:
quality = self._active_container_stack.findContainer({"type": "quality"})
if quality:
return quality.getId()
return ""
## Check if a container is read_only
@pyqtSlot(str, result = bool)
def isReadOnly(self, container_id):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not containers or not self._active_container_stack:
return True
return containers[0].isReadOnly()
@pyqtSlot(result = str)
def newQualityContainerFromQualityAndUser(self):
new_container_id = self.duplicateContainer(self.activeQualityId)
if new_container_id == "":
return
self.blurSettings.emit()
self.setActiveQuality(new_container_id)
self.updateQualityContainerFromUserContainer()
@pyqtSlot(str, result=str)
def duplicateContainer(self, container_id):
if not self._active_container_stack:
return ""
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if containers:
new_name = self._createUniqueName("quality", "", containers[0].getName(), catalog.i18nc("@label", "Custom profile"))
new_container = InstanceContainer("")
## Copy all values
new_container.deserialize(containers[0].serialize())
new_container.setReadOnly(False)
new_container.setName(new_name)
new_container._id = new_name
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
return new_name
return ""
@pyqtSlot(str, str)
def renameQualityContainer(self, container_id, new_name):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id, type = "quality")
if containers:
new_name = self._createUniqueName("quality", containers[0].getName(), new_name,
catalog.i18nc("@label", "Custom profile"))
if containers[0].getName() == new_name:
# Nothing to do.
return
# As we also want the id of the container to be changed (so that profile name is the name of the file
# on disk. We need to create a new instance and remove it (so the old file of the container is removed)
# If we don't do that, we might get duplicates & other weird issues.
new_container = InstanceContainer("")
new_container.deserialize(containers[0].serialize())
# Actually set the name
new_container.setName(new_name)
new_container._id = new_name # Todo: Fix proper id change function for this.
# Add the "new" container.
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
# Ensure that the renamed profile is saved -before- we remove the old profile.
Application.getInstance().saveSettings()
# Actually set & remove new / old quality.
self.setActiveQuality(new_name)
self.removeQualityContainer(containers[0].getId())
@pyqtSlot(str)
def removeQualityContainer(self, container_id):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not containers or not self._active_container_stack:
return
# If the container that is being removed is the currently active container, set another machine as the active container
activate_new_container = container_id == self.activeQualityId
UM.Settings.ContainerRegistry.getInstance().removeContainer(container_id)
if activate_new_container:
definition_id = "fdmprinter" if not self.filterQualityByMachine else self.activeDefinitionId
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "quality", definition = definition_id)
if containers:
self.setActiveQuality(containers[0].getId())
self.activeQualityChanged.emit()
@pyqtSlot()
def updateQualityContainerFromUserContainer(self):
if not self._active_container_stack:
return
user_settings = self._active_container_stack.getTop()
quality = self._active_container_stack.findContainer({"type": "quality"})
for key in user_settings.getAllKeys():
quality.setProperty(key, "value", user_settings.getProperty(key, "value"))
self.clearUserSettings() # As all users settings are noq a quality, remove them.
@pyqtSlot(str)
def setActiveMaterial(self, material_id):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
if not containers or not self._active_container_stack:
return
old_material = self._active_container_stack.findContainer({"type":"material"})
if old_material:
material_index = self._active_container_stack.getContainerIndex(old_material)
self._active_container_stack.replaceContainer(material_index, containers[0])
self.setActiveQuality(self._updateQualityContainer(self._active_container_stack.getBottom(), containers[0]).id)
@pyqtSlot(str)
def setActiveVariant(self, variant_id):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
if not containers or not self._active_container_stack:
return
old_variant = self._active_container_stack.findContainer({"type": "variant"})
if old_variant:
variant_index = self._active_container_stack.getContainerIndex(old_variant)
self._active_container_stack.replaceContainer(variant_index, containers[0])
self.setActiveMaterial(self._updateMaterialContainer(self._active_container_stack.getBottom(), containers[0]).id)
@pyqtSlot(str)
def setActiveQuality(self, quality_id):
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
if not containers or not self._active_container_stack:
return
old_quality = self._active_container_stack.findContainer({"type": "quality"})
if old_quality:
quality_index = self._active_container_stack.getContainerIndex(old_quality)
self._active_container_stack.replaceContainer(quality_index, containers[0])
@pyqtProperty(str, notify = activeVariantChanged)
def activeVariantName(self):
if self._active_container_stack:
variant = self._active_container_stack.findContainer({"type": "variant"})
if variant:
return variant.getName()
return ""
@pyqtProperty(str, notify = activeVariantChanged)
def activeVariantId(self):
if self._active_container_stack:
variant = self._active_container_stack.findContainer({"type": "variant"})
if variant:
return variant.getId()
return ""
@pyqtProperty(str, notify = globalContainerChanged)
def activeDefinitionId(self):
if self._global_container_stack:
definition = self._global_container_stack.getBottom()
if definition:
return definition.id
return ""
@pyqtSlot(str, str)
def renameMachine(self, machine_id, new_name):
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
if containers:
new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName())
containers[0].setName(new_name)
self.globalContainerChanged.emit()
@pyqtSlot(str)
def removeMachine(self, machine_id):
# If the machine that is being removed is the currently active machine, set another machine as the active machine
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
current_settings_id = machine_id + "_current_settings"
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = current_settings_id)
for container in containers:
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
UM.Settings.ContainerRegistry.getInstance().removeContainer(machine_id)
if activate_new_machine:
stacks = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(type = "machine")
if stacks:
Application.getInstance().setGlobalContainerStack(stacks[0])
@pyqtProperty(bool, notify = globalContainerChanged)
def hasMaterials(self):
if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_materials", False))
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def hasVariants(self):
if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_variants", False))
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def filterMaterialsByMachine(self):
if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def filterQualityByMachine(self):
if self._global_container_stack:
return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
return False
def _updateVariantContainer(self, definition):
if not definition.getMetaDataEntry("has_variants"):
return self._empty_variant_container
containers = []
preferred_variant = definition.getMetaDataEntry("preferred_variant")
if preferred_variant:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
if not containers:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
if containers:
return containers[0]
return self._empty_variant_container
def _updateMaterialContainer(self, definition, variant_container = None):
if not definition.getMetaDataEntry("has_materials"):
return self._empty_material_container
search_criteria = { "type": "material" }
if definition.getMetaDataEntry("has_machine_materials"):
search_criteria["definition"] = definition.id
if definition.getMetaDataEntry("has_variants") and variant_container:
search_criteria["variant"] = variant_container.id
else:
search_criteria["definition"] = "fdmprinter"
preferred_material = definition.getMetaDataEntry("preferred_material")
if preferred_material:
search_criteria["id"] = preferred_material
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if containers:
return containers[0]
return self._empty_material_container
def _updateQualityContainer(self, definition, material_container = None):
search_criteria = { "type": "quality" }
if definition.getMetaDataEntry("has_machine_quality"):
search_criteria["definition"] = definition.id
if definition.getMetaDataEntry("has_materials") and material_container:
search_criteria["material"] = material_container.id
else:
search_criteria["definition"] = "fdmprinter"
preferred_quality = definition.getMetaDataEntry("preferred_quality")
if preferred_quality:
search_criteria["id"] = preferred_quality
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
if containers:
return containers[0]
return self._empty_quality_container
def createMachineManagerModel(engine, script_engine):
return MachineManagerModel()

View file

@ -1,14 +1,17 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from UM.Application import Application
from UM.Qt.Duration import Duration
from UM.Preferences import Preferences
import math
import os.path
import unicodedata
## A class for processing and calculating minimum, current and maximum print time.
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
#
# This class contains all the logic relating to calculation and slicing for the
# time/quality slider concept. It is a rather tricky combination of event handling
@ -22,6 +25,8 @@ import math
# - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
# - When the high quality pass is done, we update the maximum print time.
#
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
# This job name is requested by the JobSpecs qml file.
class PrintInformation(QObject):
class SlicePass:
CurrentSettings = 1
@ -45,14 +50,20 @@ class PrintInformation(QObject):
if self._backend:
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
self._job_name = ""
self._abbr_machine = ""
Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
Application.getInstance().fileLoaded.connect(self.setJobName)
currentPrintTimeChanged = pyqtSignal()
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
def currentPrintTime(self):
return self._current_print_time
materialAmountChanged = pyqtSignal()
@pyqtProperty(float, notify = materialAmountChanged)
def materialAmount(self):
return self._material_amount
@ -63,6 +74,49 @@ class PrintInformation(QObject):
self.currentPrintTimeChanged.emit()
# Material amount is sent as an amount of mm^3, so calculate length from that
r = Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("material_diameter") / 2
r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
self._material_amount = round((amount / (math.pi * r ** 2)) / 1000, 2)
self.materialAmountChanged.emit()
@pyqtSlot(str)
def setJobName(self, name):
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
# extension. This cuts the extension off if necessary.
name = os.path.splitext(name)[0]
if self._job_name != name:
self._job_name = name
self.jobNameChanged.emit()
jobNameChanged = pyqtSignal()
@pyqtProperty(str, notify = jobNameChanged)
def jobName(self):
return self._job_name
@pyqtSlot(str, result = str)
def createJobName(self, base_name):
base_name = self._stripAccents(base_name)
if Preferences.getInstance().getValue("cura/jobname_prefix"):
return self._abbr_machine + "_" + base_name
else:
return base_name
## Created an acronymn-like abbreviated machine name from the currently active machine name
# Called each time the global stack is switched
def _setAbbreviatedMachineName(self):
global_stack_name = Application.getInstance().getGlobalContainerStack().getName()
split_name = global_stack_name.split(" ")
abbr_machine = ""
for word in split_name:
if word.lower() == "ultimaker":
abbr_machine += "UM"
elif word.isdigit():
abbr_machine += word
else:
abbr_machine += self._stripAccents(word.strip("()[]{}#").upper())[0]
self._abbr_machine = abbr_machine
## Utility method that strips accents from characters (eg: â -> a)
def _stripAccents(self, str):
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')

View file

@ -3,6 +3,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from enum import IntEnum # For the connection state tracking.
from UM.Logger import Logger
from UM.Signal import signalemitter
## Printer output device adds extra interface options on top of output device.
#
@ -13,7 +14,8 @@ from UM.Logger import Logger
# functions to actually have the implementation.
#
# For all other uses it should be used in the same way as a "regular" OutputDevice.
class PrinterOutputDevice(OutputDevice, QObject):
@signalemitter
class PrinterOutputDevice(QObject, OutputDevice):
def __init__(self, device_id, parent = None):
super().__init__(device_id = device_id, parent = parent)

17
cura/ProfileReader.py Normal file
View file

@ -0,0 +1,17 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.PluginObject import PluginObject
## A type of plug-ins that reads profiles from a file.
#
# The profile is then stored as instance container of the type user profile.
class ProfileReader(PluginObject):
def __init__(self):
super().__init__()
## Read profile data from a file and return a filled profile.
#
# \return \type{Profile} The profile that was obtained from the file.
def read(self, file_name):
raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.")

25
cura/ProfileWriter.py Normal file
View file

@ -0,0 +1,25 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.PluginObject import PluginObject
## Base class for profile writer plugins.
#
# This class defines a write() function to write profiles to files with.
class ProfileWriter(PluginObject):
## Initialises the profile writer.
#
# This currently doesn't do anything since the writer is basically static.
def __init__(self):
super().__init__()
## Writes a profile to the specified file path.
#
# The profile writer may write its own file format to the specified file.
#
# \param path \type{string} The file to output to.
# \param profile \type{Profile} The profile to write to the file.
# \return \code True \endcode if the writing was successful, or \code
# False \endcode if it wasn't.
def write(self, path, node):
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")

View file

@ -0,0 +1,50 @@
# Copyright (c) 2016 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.Scene.SceneNode import SceneNode
from UM.Operations import Operation
from UM.Math.Vector import Vector
## An operation that parents a scene node to another scene node.
class SetParentOperation(Operation.Operation):
## Initialises this SetParentOperation.
#
# \param node The node which will be reparented.
# \param parent_node The node which will be the parent.
def __init__(self, node, parent_node):
super().__init__()
self._node = node
self._parent = parent_node
self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
## Undoes the set-parent operation, restoring the old parent.
def undo(self):
self._set_parent(self._old_parent)
## Re-applies the set-parent operation.
def redo(self):
self._set_parent(self._parent)
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
#
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
def _set_parent(self, new_parent):
if new_parent:
self._node.setPosition(self._node.getWorldPosition() - new_parent.getWorldPosition())
current_parent = self._node.getParent()
if current_parent:
self._node.scale(current_parent.getScale() / new_parent.getScale())
self._node.rotate(current_parent.getOrientation())
else:
self._node.scale(Vector(1, 1, 1) / new_parent.getScale())
self._node.rotate(new_parent.getOrientation().getInverse())
self._node.setParent(new_parent)
## Returns a programmer-readable representation of this operation.
#
# \return A programmer-readable representation of this operation.
def __repr__(self):
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)

View file

@ -0,0 +1,81 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import copy
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.Signal import Signal, signalemitter
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
import UM.Logger
from UM.Application import Application
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
# this stack still resolve.
@signalemitter
class SettingOverrideDecorator(SceneNodeDecorator):
## Event indicating that the user selected a different extruder.
activeExtruderChanged = Signal()
def __init__(self):
super().__init__()
self._stack = ContainerStack(stack_id = id(self))
self._stack.setDirty(False) # This stack does not need to be saved.
self._instance = InstanceContainer(container_id = "SettingOverrideInstanceContainer")
self._stack.addContainer(self._instance)
self._extruder_stack = None #Stack upon which our stack is based.
self._stack.propertyChanged.connect(self._onSettingChanged)
ContainerRegistry.getInstance().addContainer(self._stack)
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
self.activeExtruderChanged.connect(self._updateNextStack)
self._updateNextStack()
def __deepcopy__(self, memo):
## Create a fresh decorator object
deep_copy = SettingOverrideDecorator()
## Copy the instance
deep_copy._instance = copy.deepcopy(self._instance, memo)
## Set the copied instance as the first (and only) instance container of the stack.
deep_copy._stack.replaceContainer(0, deep_copy._instance)
return deep_copy
## Gets the currently active extruder to print this object with.
#
# \return An extruder's container stack.
def getActiveExtruder(self):
return self._extruder_stack
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
if property_name == "value": # Only reslice if the value has changed.
Application.getInstance().getBackend().forceSlice()
## Makes sure that the stack upon which the container stack is placed is
# kept up to date.
def _updateNextStack(self):
if self._extruder_stack:
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
if extruder_stack:
old_extruder_stack_id = self._stack.getNextStack().getId()
self._stack.setNextStack(extruder_stack[0])
if self._stack.getNextStack().getId() != old_extruder_stack_id: #Only reslice if the extruder changed.
Application.getInstance().getBackend().forceSlice()
else:
UM.Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
else:
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
## Changes the extruder with which to print this node.
#
# \param extruder_stack_id The new extruder stack to print with.
def setActiveExtruder(self, extruder_stack_id):
self._extruder_stack = extruder_stack_id
self.activeExtruderChanged.emit()
def getStack(self):
return self._stack