diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py
index ca4c8e7b34..a25604d165 100644
--- a/cura/BuildVolume.py
+++ b/cura/BuildVolume.py
@@ -67,6 +67,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = []
self._disallowed_area_mesh = None
+ self._prime_tower_area = None
+ self._prime_tower_area_mesh = None
+
self.setCalculateBoundingBox(False)
self._volume_aabb = None
@@ -82,6 +85,8 @@ class BuildVolume(SceneNode):
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self._onActiveExtruderStackChanged()
+ self._has_errors = False
+
def setWidth(self, width):
if width: self._width = width
@@ -110,6 +115,10 @@ class BuildVolume(SceneNode):
if self._disallowed_area_mesh:
renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
+ if self._prime_tower_area_mesh:
+ renderer.queueNode(self, mesh = self._prime_tower_area_mesh, shader = self._shader, transparent=True,
+ backface_cull=True, sort=-8)
+
return True
## Recalculates the build volume & disallowed areas.
@@ -184,6 +193,24 @@ class BuildVolume(SceneNode):
else:
self._disallowed_area_mesh = None
+ if self._prime_tower_area:
+ mb = MeshBuilder()
+ color = Color(1.0, 0.0, 0.0, 0.5)
+ points = self._prime_tower_area.getPoints()
+ first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+ self._clamp(points[0][1], min_d, max_d))
+ previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+ self._clamp(points[0][1], min_d, max_d))
+ for point in points:
+ new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
+ self._clamp(point[1], min_d, max_d))
+ mb.addFace(first, previous_point, new_point, color=color)
+ previous_point = new_point
+
+ self._prime_tower_area_mesh = mb.build()
+ else:
+ self._prime_tower_area_mesh = None
+
self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d),
maximum = Vector(max_w, max_h - self._raft_thickness, max_d))
@@ -291,24 +318,34 @@ class BuildVolume(SceneNode):
if rebuild_me:
self.rebuild()
+ def hasErrors(self):
+ return self._has_errors
+
def _updateDisallowedAreas(self):
if not self._global_container_stack:
return
-
+ self._has_errors = False # Reset.
disallowed_areas = copy.deepcopy(
self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
areas = []
machine_width = self._global_container_stack.getProperty("machine_width", "value")
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
-
+ self._prime_tower_area = None
# Add prime tower location as disallowed area.
if self._global_container_stack.getProperty("prime_tower_enable", "value") == True:
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
- disallowed_areas.append([
+ '''disallowed_areas.append([
+ [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
+ [prime_tower_x, prime_tower_y - prime_tower_size],
+ [prime_tower_x, prime_tower_y],
+ [prime_tower_x - prime_tower_size, prime_tower_y],
+ ])'''
+
+ self._prime_tower_area = Polygon([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y],
@@ -344,6 +381,9 @@ class BuildVolume(SceneNode):
areas.append(poly)
+ if self._prime_tower_area:
+ self._prime_tower_area = self._prime_tower_area.getMinkowskiHull(Polygon(approximatedCircleVertices(bed_adhesion_size)))
+
# Add the skirt areas around the borders of the build plate.
if bed_adhesion_size > 0:
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
@@ -377,6 +417,19 @@ class BuildVolume(SceneNode):
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
], numpy.float32)))
+ # Check if the prime tower area intersects with any of the other areas.
+ # If this is the case, keep the polygon seperate, so it can be drawn in red.
+ # If not, add it back to disallowed area's, so it's rendered as normal.
+ collision = False
+ if self._prime_tower_area:
+ for area in areas:
+ if self._prime_tower_area.intersectsPolygon(area) is not None:
+ collision = True
+ break
+ if not collision:
+ areas.append(self._prime_tower_area)
+ self._prime_tower_area = None
+ self._has_errors = collision
self._disallowed_areas = areas
## Convenience function to calculate the size of the bed adhesion in directions x, y.
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 892440cba0..7651cad930 100644
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -322,6 +322,7 @@ class CuraApplication(QtApplication):
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
if path:
+ instance.setPath(path)
with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data)
@@ -346,6 +347,7 @@ class CuraApplication(QtApplication):
elif stack_type == "extruder_train":
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
if path:
+ stack.setPath(path)
with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data)
@@ -693,17 +695,17 @@ class CuraApplication(QtApplication):
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)
-
nodes.append(node)
if nodes:
op = GroupedOperation()
for node in nodes:
+ # Ensure that the object is above the build platform
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
op.addOperation(SetTransformOperation(node, Vector(0, node.getWorldPosition().y - node.getBoundingBox().bottom, 0)))
op.push()
-
- ## Reset all transformations on nodes with mesh data.
+
+ ## Reset all transformations on nodes with mesh data.
@pyqtSlot()
def resetAll(self):
Logger.log("i", "Resetting all scene transformations")
@@ -719,15 +721,17 @@ class CuraApplication(QtApplication):
if nodes:
op = GroupedOperation()
-
for node in nodes:
# Ensure that the object is above the build platform
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
-
- op.addOperation(SetTransformOperation(node, Vector(0, node.getMeshData().getCenterPosition().y, 0), Quaternion(), Vector(1, 1, 1)))
-
+ center_y = 0
+ if node.callDecoration("isGroup"):
+ center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
+ else:
+ center_y = node.getMeshData().getCenterPosition().y
+ op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
op.push()
-
+
## Reload all mesh data on the screen from file.
@pyqtSlot()
def reloadAll(self):
diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py
index c5eb4b699e..d3da01e590 100644
--- a/cura/LayerPolygon.py
+++ b/cura/LayerPolygon.py
@@ -16,7 +16,7 @@ class LayerPolygon:
MoveRetractionType = 9
SupportInterfaceType = 10
- __jump_map = numpy.logical_or( numpy.arange(11) == NoneType, numpy.arange(11) >= MoveRetractionType )
+ __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType)
def __init__(self, mesh, extruder, line_types, data, line_widths):
self._mesh = mesh
@@ -42,7 +42,7 @@ class LayerPolygon:
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded.
- self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool)
+ self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
self._build_cache_line_mesh_mask = None
self._build_cache_needed_points = None
diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py
index d718f25b87..2a5bd4091c 100644
--- a/cura/PlatformPhysics.py
+++ b/cura/PlatformPhysics.py
@@ -15,6 +15,9 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
from . import PlatformPhysicsOperation
from . import ZOffsetDecorator
+import random # used for list shuffling
+
+
class PlatformPhysics:
def __init__(self, controller, volume):
super().__init__()
@@ -48,7 +51,12 @@ class PlatformPhysics:
# same direction.
transformed_nodes = []
- for node in BreadthFirstIterator(root):
+ group_nodes = []
+ # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
+ # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
+ nodes = list(BreadthFirstIterator(root))
+ random.shuffle(nodes)
+ for node in nodes:
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
continue
@@ -69,6 +77,9 @@ class PlatformPhysics:
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node._outside_buildarea = True
+ if node.callDecoration("isGroup"):
+ group_nodes.append(node) # Keep list of affected group_nodes
+
# Move it downwards if bottom is above platform
move_vector = Vector()
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
@@ -102,7 +113,6 @@ class PlatformPhysics:
continue # Other node is already moving, wait for next pass.
overlap = (0, 0) # Start loop with no overlap
- move_vector = move_vector.set(x=overlap[0] * self._move_factor, z=overlap[1] * self._move_factor)
current_overlap_checks = 0
# Continue to check the overlap until we no longer find one.
while overlap and current_overlap_checks < self._max_overlap_checks:
@@ -144,7 +154,6 @@ class PlatformPhysics:
overlap = convex_hull.intersectsPolygon(area)
if overlap is None:
continue
-
node._outside_buildarea = True
if not Vector.Null.equals(move_vector, epsilon=1e-5):
@@ -152,6 +161,12 @@ class PlatformPhysics:
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
op.push()
+ # Group nodes should override the _outside_buildarea property of their children.
+ for group_node in group_nodes:
+ for child_node in group_node.getAllChildren():
+ child_node._outside_buildarea = group_node._outside_buildarea
+
+
def _onToolOperationStarted(self, tool):
self._enabled = False
diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py
index 40e036eba5..27a9bd90dd 100644
--- a/cura/Settings/ContainerManager.py
+++ b/cura/Settings/ContainerManager.py
@@ -167,6 +167,19 @@ class ContainerManager(QObject):
return True
+ @pyqtSlot(str, str, result=str)
+ def getContainerMetaDataEntry(self, container_id, entry_name):
+ containers = self._container_registry.findContainers(None, id=container_id)
+ if not containers:
+ UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
+ return False
+
+ result = containers[0].getMetaDataEntry(entry_name)
+ if result:
+ return result
+ else:
+ return ""
+
## Set a metadata entry of the specified container.
#
# This will set the specified entry of the container's metadata to the specified
@@ -254,6 +267,10 @@ class ContainerManager(QObject):
return True
return False
+ @pyqtSlot(str, result = str)
+ def makeUniqueName(self, original_name):
+ return self._container_registry.uniqueName(original_name)
+
## Get a list of string that can be used as name filters for a Qt File Dialog
#
# This will go through the list of available container types and generate a list of strings
@@ -573,6 +590,28 @@ class ContainerManager(QObject):
return new_name
+ @pyqtSlot(str, result = str)
+ def duplicateMaterial(self, material_id):
+ containers = self._container_registry.findInstanceContainers(id=material_id)
+ if not containers:
+ UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
+ return ""
+
+ # Ensure all settings are saved.
+ UM.Application.getInstance().saveSettings()
+
+ # Create a new ID & container to hold the data.
+ new_id = self._container_registry.uniqueName(material_id)
+ container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer
+ duplicated_container = container_type(new_id)
+
+ # Instead of duplicating we load the data from the basefile again.
+ # This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
+ # are also correctly created.
+ with open(containers[0].getPath(), encoding="utf-8") as f:
+ duplicated_container.deserialize(f.read())
+ self._container_registry.addContainer(duplicated_container)
+
# Factory function, used by QML
@staticmethod
def createContainerManager(engine, js_engine):
diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py
index ea3ca30118..89fa5e3e88 100644
--- a/plugins/3MFReader/ThreeMFReader.py
+++ b/plugins/3MFReader/ThreeMFReader.py
@@ -71,7 +71,7 @@ class ThreeMFReader(MeshReader):
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
# TODO: We currently do not check for normals and simply recalculate them.
- mesh_builder.calculateNormals()
+ mesh_builder.calculateNormals(flip = True)
mesh_builder.setFileName(file_name)
node.setMeshData(mesh_builder.build().getTransformed(rotation))
node.setSelectable(True)
diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py
index 4d40da899d..fc26bd086b 100644
--- a/plugins/CuraEngineBackend/StartSliceJob.py
+++ b/plugins/CuraEngineBackend/StartSliceJob.py
@@ -78,6 +78,10 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.SettingError)
return
+ if Application.getInstance().getBuildVolume().hasErrors():
+ self.setResult(StartJobResult.SettingError)
+ return
+
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable():
diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py
index 772b11890b..2bccb8e3cb 100644
--- a/plugins/CuraProfileReader/CuraProfileReader.py
+++ b/plugins/CuraProfileReader/CuraProfileReader.py
@@ -1,8 +1,8 @@
-# Copyright (c) 2015 Ultimaker B.V.
+# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
+import configparser
-import os.path
-
+from UM import PluginRegistry
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.ProfileReader import ProfileReader
@@ -27,22 +27,76 @@ class CuraProfileReader(ProfileReader):
# returned.
def read(self, file_name):
try:
- archive = zipfile.ZipFile(file_name, "r")
- except Exception:
- # zipfile doesn't give proper exceptions, so we can only catch broad ones
+ with zipfile.ZipFile(file_name, "r") as archive:
+ results = []
+ for profile_id in archive.namelist():
+ with archive.open(profile_id) as f:
+ serialized = f.read()
+ profile = self._loadProfile(serialized.decode("utf-8"), profile_id)
+ if profile is not None:
+ results.append(profile)
+ return results
+
+ except zipfile.BadZipFile:
+ # It must be an older profile from Cura 2.1.
+ with open(file_name, encoding="utf-8") as fhandle:
+ serialized = fhandle.read()
+ return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
+
+ ## Convert a profile from an old Cura to this Cura if needed.
+ #
+ # \param serialized \type{str} The profile data to convert in the serialized on-disk format.
+ # \param profile_id \type{str} The name of the profile.
+ # \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
+ def _upgradeProfile(self, serialized, profile_id):
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ if not "general" in parser:
+ Logger.log("w", "Missing required section 'general'.")
+ return None
+ if not "version" in parser["general"]:
+ Logger.log("w", "Missing required 'version' property")
+ return None
+
+ version = int(parser["general"]["version"])
+ if InstanceContainer.Version != version:
+ name = parser["general"]["name"]
+ return self._upgradeProfileVersion(serialized, name, version)
+ else:
+ return [(serialized, profile_id)]
+
+ ## Load a profile from a serialized string.
+ #
+ # \param serialized \type{str} The profile data to read.
+ # \param profile_id \type{str} The name of the profile.
+ # \return \type{InstanceContainer|None}
+ def _loadProfile(self, serialized, profile_id):
+ # Create an empty profile.
+ profile = InstanceContainer(profile_id)
+ profile.addMetaDataEntry("type", "quality_changes")
+ try:
+ profile.deserialize(serialized)
+ except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
+ Logger.log("e", "Error while trying to parse profile: %s", str(e))
+ return None
+ return profile
+
+ ## Upgrade a serialized profile to the current profile format.
+ #
+ # \param serialized \type{str} The profile data to convert.
+ # \param profile_id \type{str} The name of the profile.
+ # \param source_version \type{int} The profile version of 'serialized'.
+ # \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
+ def _upgradeProfileVersion(self, serialized, profile_id, source_version):
+ converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter={"version_upgrade": {} }, active_only=True)
+
+ source_format = ("profile", source_version)
+ profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins
+ if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version]
+
+ if not profile_convert_funcs:
return []
- results = []
- for profile_id in archive.namelist():
- # Create an empty profile.
- profile = InstanceContainer(profile_id)
- profile.addMetaDataEntry("type", "quality_changes")
- serialized = ""
- with archive.open(profile_id) as f:
- serialized = f.read()
- try:
- profile.deserialize(serialized.decode("utf-8") )
- except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
- Logger.log("e", "Error while trying to parse profile: %s", str(e))
- continue
- results.append(profile)
- return results
\ No newline at end of file
+
+ filenames, outputs = profile_convert_funcs[0](serialized, profile_id)
+ return list(zip(outputs, filenames))
diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py
index c15b079097..95d6659dd1 100644
--- a/plugins/GCodeWriter/GCodeWriter.py
+++ b/plugins/GCodeWriter/GCodeWriter.py
@@ -66,7 +66,10 @@ class GCodeWriter(MeshWriter):
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
flat_container = InstanceContainer(instance_container2.getName())
- flat_container.setDefinition(instance_container2.getDefinition())
+ if instance_container1.getDefinition():
+ flat_container.setDefinition(instance_container1.getDefinition())
+ else:
+ flat_container.setDefinition(instance_container2.getDefinition())
flat_container.setMetaData(instance_container2.getMetaData())
for key in instance_container2.getAllKeys():
diff --git a/plugins/LegacyProfileReader/DictionaryOfDoom.json b/plugins/LegacyProfileReader/DictionaryOfDoom.json
index db0d26b8e4..15ef792f83 100644
--- a/plugins/LegacyProfileReader/DictionaryOfDoom.json
+++ b/plugins/LegacyProfileReader/DictionaryOfDoom.json
@@ -70,7 +70,8 @@
"magic_spiralize": "spiralize",
"prime_tower_enable": "wipe_tower",
"prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))",
- "ooze_shield_enabled": "ooze_shield"
+ "ooze_shield_enabled": "ooze_shield",
+ "skin_overlap": "fill_overlap"
},
"defaults": {
diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py
index b4086291ca..25c2290b37 100644
--- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py
+++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py
@@ -69,7 +69,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:
stack = UM.Application.getInstance().getGlobalContainerStack()
- new_instance.setProperty("value", stack.getProperty(item, "value"))
+ new_instance.setProperty("value", stack.getRawProperty(item, "value"))
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
visibility_changed = True
diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py
index b5c4c0f22c..953f60a33d 100644
--- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py
+++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py
@@ -7,6 +7,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Application import Application
from UM.Preferences import Preferences
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
+from cura.Settings.ExtruderManager import ExtruderManager
## This tool allows the user to add & change settings per node in the scene.
@@ -71,11 +72,20 @@ class PerObjectSettingsTool(Tool):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
+
+ # Ensure that all extruder data is reset
if not self._multi_extrusion:
- # Ensure that all extruder data is reset
- root_node = Application.getInstance().getController().getScene().getRoot()
- for node in DepthFirstIterator(root_node):
- node.callDecoration("setActiveExtruder", global_container_stack.getId())
+ default_stack_id = global_container_stack.getId()
+ else:
+ default_stack = ExtruderManager.getInstance().getExtruderStack(0)
+ if default_stack:
+ default_stack_id = default_stack.getId()
+ else: default_stack_id = global_container_stack.getId()
+
+ root_node = Application.getInstance().getController().getScene().getRoot()
+ for node in DepthFirstIterator(root_node):
+ node.callDecoration("setActiveExtruder", default_stack_id)
+
self._updateEnabled()
def _updateEnabled(self):
diff --git a/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
index 37f4422a11..90adac73b1 100644
--- a/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
+++ b/plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
@@ -49,7 +49,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
message.show()
else:
- message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName()))
+ message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(device.getName()))
message.show()
return result
diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py
index 047a03575d..117ac2a178 100644
--- a/plugins/SliceInfoPlugin/SliceInfo.py
+++ b/plugins/SliceInfoPlugin/SliceInfo.py
@@ -65,7 +65,7 @@ class SliceInfo(Extension):
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
- self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False)
+ self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in preferences"), lifetime = 0, dismissable = False)
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py
index 3b56ac1881..fd9f106334 100644
--- a/plugins/SolidView/SolidView.py
+++ b/plugins/SolidView/SolidView.py
@@ -13,6 +13,7 @@ from UM.Settings.Validator import ValidatorState
from UM.View.GL.OpenGL import OpenGL
import cura.Settings
+from cura.Settings.ExtruderManager import ExtruderManager
import math
@@ -45,8 +46,18 @@ class SolidView(View):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
+ multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
+
+ if multi_extrusion:
+ support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
+ support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
+ if not support_angle_stack:
+ support_angle_stack = global_container_stack
+ else:
+ support_angle_stack = global_container_stack
+
if Preferences.getInstance().getValue("view/show_overhang"):
- angle = global_container_stack.getProperty("support_angle", "value")
+ angle = support_angle_stack.getProperty("support_angle", "value")
# Make sure the overhang angle is valid before passing it to the shader
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]:
@@ -56,7 +67,6 @@ class SolidView(View):
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
- multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer):
diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py
index 920a71d40f..4838fe9b96 100644
--- a/plugins/USBPrinting/USBPrinterOutputDevice.py
+++ b/plugins/USBPrinting/USBPrinterOutputDevice.py
@@ -140,7 +140,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# \param gcode_list List with gcode (strings).
def printGCode(self, gcode_list):
if self._progress or self._connection_state != ConnectionState.connected:
- self._error_message = Message(catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job."))
+ self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected."))
self._error_message.show()
Logger.log("d", "Printer is busy or not connected, aborting print")
self.writeError.emit(self)
diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
index 248649f431..4dec2e3a06 100644
--- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
+++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
@@ -105,9 +105,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
@pyqtSlot(str)
def updateAllFirmware(self, file_name):
- file_name = file_name.replace("file://", "") # File dialogs prepend the path with file://, which we don't need / want
+ if file_name.startswith("file://"):
+ file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices:
- Message(i18n_catalog.i18nc("@info", "Cannot update firmware, there were no connected printers found.")).show()
+ Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected.")).show()
return
for printer_connection in self._usb_output_devices:
diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py
index 64c9ae1180..b92dc30c68 100644
--- a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py
+++ b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py
@@ -31,7 +31,7 @@ class UMOUpgradeSelection(MachineAction):
if variant:
if variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant)
- self._createVariant(global_container_stack, variant_index)
+ variant = self._createVariant(global_container_stack, variant_index)
variant.setProperty("machine_heated_bed", "value", heated_bed)
self.heatedBedChanged.emit()
@@ -41,4 +41,5 @@ class UMOUpgradeSelection(MachineAction):
new_variant.addMetaDataEntry("type", "variant")
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
- global_container_stack.replaceContainer(variant_index, new_variant)
\ No newline at end of file
+ global_container_stack.replaceContainer(variant_index, new_variant)
+ return new_variant
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
index 0c3a4d1055..c8ec559702 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
@@ -279,6 +279,8 @@ class VersionUpgrade21to22(VersionUpgrade):
elif key in _setting_name_translations:
del settings[key]
settings[_setting_name_translations[key]] = value
+ if "infill_overlap" in settings: # New setting, added in 2.3
+ settings["skin_overlap"] = settings["infill_overlap"]
return settings
## Translates a setting name for the change from Cura 2.1 to 2.2.
diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json
index a586f45b21..3568f9bc14 100644
--- a/resources/definitions/fdmprinter.def.json
+++ b/resources/definitions/fdmprinter.def.json
@@ -10,6 +10,7 @@
"manufacturer": "Ultimaker",
"file_formats": "text/x-gcode;application/x-stl-ascii;application/x-stl-binary;application/x-wavefront-obj;application/x3g",
"visible": false,
+ "has_materials": true,
"preferred_material": "*generic_pla*",
"preferred_quality": "*normal*",
"machine_extruder_trains":
@@ -403,7 +404,7 @@
"description": "The maximum speed of the filament.",
"unit": "mm/s",
"type": "float",
- "default_value": 25,
+ "default_value": 299792458000,
"settable_per_mesh": false,
"settable_per_extruder": false,
"settable_per_meshgroup": false
@@ -1161,7 +1162,7 @@
"default_value": 25,
"minimum_value": "0",
"maximum_value": "machine_max_feedrate_e",
- "maximum_value_warning": "100",
+ "maximum_value_warning": "25",
"enabled": "retraction_enable",
"settable_per_mesh": false,
"settable_per_extruder": true,
@@ -1174,7 +1175,7 @@
"default_value": 25,
"minimum_value": "0",
"maximum_value": "machine_max_feedrate_e",
- "maximum_value_warning": "100",
+ "maximum_value_warning": "25",
"enabled": "retraction_enable",
"value": "retraction_speed",
"settable_per_mesh": false,
@@ -1188,7 +1189,7 @@
"default_value": 25,
"minimum_value": "0",
"maximum_value": "machine_max_feedrate_e",
- "maximum_value_warning": "100",
+ "maximum_value_warning": "25",
"enabled": "retraction_enable",
"value": "retraction_speed",
"settable_per_mesh": false,
@@ -3207,6 +3208,7 @@
"type": "category",
"icon": "category_dual",
"description": "Settings used for printing with multiple extruders.",
+ "enabled": "machine_extruder_count > 1",
"children":
{
"adhesion_extruder_nr":
@@ -3284,6 +3286,7 @@
"default_value": 15,
"value": "15 if prime_tower_enable else 0",
"minimum_value": "0",
+ "maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)",
"maximum_value_warning": "20",
"settable_per_mesh": false,
"settable_per_extruder": false
diff --git a/resources/definitions/ultimaker.def.json b/resources/definitions/ultimaker.def.json
index dc52b00dcc..0a3729c766 100644
--- a/resources/definitions/ultimaker.def.json
+++ b/resources/definitions/ultimaker.def.json
@@ -9,6 +9,9 @@
"visible": false
},
"overrides": {
+ "machine_max_feedrate_e": {
+ "default_value": 45
+ },
"material_print_temperature": {
"minimum_value": "0"
},
diff --git a/resources/definitions/ultimaker2.def.json b/resources/definitions/ultimaker2.def.json
index 81723ae10d..a0ead36bff 100644
--- a/resources/definitions/ultimaker2.def.json
+++ b/resources/definitions/ultimaker2.def.json
@@ -14,6 +14,7 @@
"platform": "ultimaker2_platform.obj",
"platform_texture": "Ultimaker2backplate.png",
"platform_offset": [9, 0, 0],
+ "has_materials": false,
"supported_actions":["UpgradeFirmware"]
},
"overrides": {
diff --git a/resources/definitions/ultimaker_original_dual.def.json b/resources/definitions/ultimaker_original_dual.def.json
index 6feaa07470..8d2d5d85d6 100644
--- a/resources/definitions/ultimaker_original_dual.def.json
+++ b/resources/definitions/ultimaker_original_dual.def.json
@@ -78,7 +78,7 @@
"default_value": 185
},
"prime_tower_position_y": {
- "default_value": 175
+ "default_value": 160
}
}
}
diff --git a/resources/materials/generic_nylon.xml.fdm_material b/resources/materials/generic_nylon.xml.fdm_material
index d7391e2984..02b2d5414b 100644
--- a/resources/materials/generic_nylon.xml.fdm_material
+++ b/resources/materials/generic_nylon.xml.fdm_material
@@ -14,11 +14,10 @@ Generic Nylon profile. Serves as an example file, data in this file is not corre
#3DF266
- 1.19
+ 1.14
2.85
- no
250
60
175
diff --git a/resources/materials/generic_pc.xml.fdm_material b/resources/materials/generic_pc.xml.fdm_material
index 292b3fd098..77d7e60bf5 100644
--- a/resources/materials/generic_pc.xml.fdm_material
+++ b/resources/materials/generic_pc.xml.fdm_material
@@ -14,7 +14,7 @@ Generic PC profile. Serves as an example file, data in this file is not correct.
#F29030
- 1.18
+ 1.19
2.85
diff --git a/resources/materials/generic_tpu.xml.fdm_material b/resources/materials/generic_tpu.xml.fdm_material
index 455e7b89be..5a0c793cda 100644
--- a/resources/materials/generic_tpu.xml.fdm_material
+++ b/resources/materials/generic_tpu.xml.fdm_material
@@ -14,7 +14,7 @@ Generic TPU 95A profile. Serves as an example file, data in this file is not cor
#B22744
- 1.19
+ 1.22
2.85
diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml
index 42c2ad9cf4..005a0892a3 100644
--- a/resources/qml/Cura.qml
+++ b/resources/qml/Cura.qml
@@ -464,12 +464,11 @@ UM.MainWindow
target: Cura.Actions.addProfile
onTriggered:
{
- Cura.ContainerManager.createQualityChanges(null);
preferences.setPage(4);
preferences.show();
- // Show the renameDialog after a very short delay so the preference page has time to initiate
- showProfileNameDialogTimer.start();
+ // Create a new profile after a very short delay so the preference page has time to initiate
+ createProfileTimer.start();
}
}
@@ -516,11 +515,11 @@ UM.MainWindow
Timer
{
- id: showProfileNameDialogTimer
+ id: createProfileTimer
repeat: false
interval: 1
- onTriggered: preferences.getCurrentItem().showProfileNameDialog()
+ onTriggered: preferences.getCurrentItem().createProfile()
}
// BlurSettings is a way to force the focus away from any of the setting items.
diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml
index 3041115ad2..6115671563 100644
--- a/resources/qml/Preferences/MaterialView.qml
+++ b/resources/qml/Preferences/MaterialView.qml
@@ -115,7 +115,7 @@ TabView
width: base.secondColumnWidth;
value: properties.density;
decimals: 2
- suffix: "g/cm"
+ suffix: "g/cm³"
stepSize: 0.01
readOnly: !base.editingEnabled;
@@ -128,7 +128,7 @@ TabView
width: base.secondColumnWidth;
value: properties.diameter;
decimals: 2
- suffix: "mm³"
+ suffix: "mm"
stepSize: 0.01
readOnly: !base.editingEnabled;
diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml
index d2d7c1b0a2..b1f0afe52f 100644
--- a/resources/qml/Preferences/MaterialsPage.qml
+++ b/resources/qml/Preferences/MaterialsPage.qml
@@ -129,30 +129,24 @@ UM.ManagementPage
enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId
onClicked: Cura.MachineManager.setActiveMaterial(base.currentItem.id)
},
- /* // apparently visible does not work on OS X
- Button
+ // apparently visible does not work on OS X
+ /*Button
{
text: catalog.i18nc("@action:button", "Duplicate");
iconName: "list-add";
enabled: base.currentItem != null
onClicked:
{
- var material_id = Cura.ContainerManager.duplicateContainer(base.currentItem.id)
+ var base_file = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "base_file")
+ // We need to copy the base container instead of the specific variant.
+ var material_id = base_file == "" ? Cura.ContainerManager.duplicateMaterial(base.currentItem.id): Cura.ContainerManager.duplicateMaterial(base_file)
if(material_id == "")
{
return
}
- if(Cura.MachineManager.filterQualityByMachine)
- {
- var quality_id = Cura.ContainerManager.duplicateContainer(Cura.MachineManager.activeQualityId)
- Cura.ContainerManager.setContainerMetaDataEntry(quality_id, "material", material_id)
- Cura.MachineManager.setActiveQuality(quality_id)
- }
-
Cura.MachineManager.setActiveMaterial(material_id)
}
- visible: false;
},
*/
Button
diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml
index 56d8bd41a4..4e872bdc2b 100644
--- a/resources/qml/Preferences/ProfilesPage.qml
+++ b/resources/qml/Preferences/ProfilesPage.qml
@@ -84,7 +84,7 @@ UM.ManagementPage
onClicked:
{
- newNameDialog.object = base.currentItem != null ? base.currentItem.name : "";
+ newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : "";
newNameDialog.open();
newNameDialog.selectText();
}
@@ -100,7 +100,7 @@ UM.ManagementPage
onClicked:
{
- newDuplicateNameDialog.object = base.currentItem.name;
+ newDuplicateNameDialog.object = Cura.ContainerManager.makeUniqueName(base.currentItem.name);
newDuplicateNameDialog.open();
newDuplicateNameDialog.selectText();
}
@@ -141,11 +141,12 @@ UM.ManagementPage
scrollviewCaption: catalog.i18nc("@label %1 is printer name","Printer: %1").arg(Cura.MachineManager.activeMachineName)
- signal showProfileNameDialog()
- onShowProfileNameDialog:
+ signal createProfile()
+ onCreateProfile:
{
- renameDialog.open();
- renameDialog.selectText();
+ newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : "";
+ newNameDialog.open();
+ newNameDialog.selectText();
}
signal selectContainer(string name)
@@ -267,6 +268,7 @@ UM.ManagementPage
UM.RenameDialog
{
+ title: catalog.i18nc("@title:window", "Rename Profile")
id: renameDialog;
object: base.currentItem != null ? base.currentItem.name : ""
onAccepted:
@@ -279,6 +281,7 @@ UM.ManagementPage
// Dialog to request a name when creating a new profile
UM.RenameDialog
{
+ title: catalog.i18nc("@title:window", "Create Profile")
id: newNameDialog;
object: "";
onAccepted:
@@ -292,6 +295,7 @@ UM.ManagementPage
// Dialog to request a name when duplicating a new profile
UM.RenameDialog
{
+ title: catalog.i18nc("@title:window", "Duplicate Profile")
id: newDuplicateNameDialog;
object: "";
onAccepted:
diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml
index ed16b722dd..cdc557a089 100644
--- a/resources/qml/Settings/SettingItem.qml
+++ b/resources/qml/Settings/SettingItem.qml
@@ -138,7 +138,7 @@ Item {
{
id: linkedSettingIcon;
- visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && !definition.settable_per_extruder && base.showLinkedSettingIcon
+ visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && (!definition.settable_per_extruder || definition.global_inherits_stack != "-1") && base.showLinkedSettingIcon
height: parent.height;
width: height;
diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml
index 7f8c1488ae..c50c2cc4e0 100644
--- a/resources/qml/Settings/SettingView.qml
+++ b/resources/qml/Settings/SettingView.qml
@@ -40,7 +40,7 @@ ScrollView
id: delegate
width: UM.Theme.getSize("sidebar").width;
- height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : 0
+ height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
diff --git a/resources/themes/cura/styles.qml b/resources/themes/cura/styles.qml
index 3cfb4514ee..91a512b6db 100644
--- a/resources/themes/cura/styles.qml
+++ b/resources/themes/cura/styles.qml
@@ -199,18 +199,32 @@ QtObject {
property Component progressbar: Component{
ProgressBarStyle {
- background:Rectangle {
+ background: Rectangle {
implicitWidth: Theme.getSize("message").width - (Theme.getSize("default_margin").width * 2)
implicitHeight: Theme.getSize("progressbar").height
radius: Theme.getSize("progressbar_radius").width
- color: Theme.getColor("progressbar_background")
+ color: control.hasOwnProperty("backgroundColor") ? control.backgroundColor : Theme.getColor("progressbar_background")
}
progress: Rectangle {
- color: control.indeterminate ? "transparent" : Theme.getColor("progressbar_control")
+ color:
+ {
+ if(control.indeterminate)
+ {
+ return "transparent";
+ }
+ else if(control.hasOwnProperty("controlColor"))
+ {
+ return control.controlColor;
+ }
+ else
+ {
+ return Theme.getColor("progressbar_control");
+ }
+ }
radius: Theme.getSize("progressbar_radius").width
Rectangle{
radius: Theme.getSize("progressbar_radius").width
- color: Theme.getColor("progressbar_control")
+ color: control.hasOwnProperty("controlColor") ? control.controlColor : Theme.getColor("progressbar_control")
width: Theme.getSize("progressbar_control").width
height: Theme.getSize("progressbar_control").height
visible: control.indeterminate
diff --git a/resources/themes/cura/theme.json b/resources/themes/cura/theme.json
index 69fc2c2c71..f90467c9e8 100644
--- a/resources/themes/cura/theme.json
+++ b/resources/themes/cura/theme.json
@@ -159,9 +159,17 @@
"tooltip": [12, 169, 227, 255],
"tooltip_text": [255, 255, 255, 255],
- "message_background": [255, 255, 255, 255],
- "message_text": [32, 166, 219, 255],
- "message_dismiss": [127, 127, 127, 255],
+ "message_background": [24, 41, 77, 255],
+ "message_text": [255, 255, 255, 255],
+ "message_border": [24, 41, 77, 255],
+ "message_button": [255, 255, 255, 255],
+ "message_button_hover": [12, 169, 227, 255],
+ "message_button_active": [32, 166, 219, 255],
+ "message_button_text": [24, 41, 77, 255],
+ "message_button_text_hover": [255, 255, 255, 255],
+ "message_button_text_active": [255, 255, 255, 255],
+ "message_progressbar_background": [255, 255, 255, 255],
+ "message_progressbar_control": [12, 169, 227, 255],
"tool_panel_background": [255, 255, 255, 255],