Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/cura
diff options
context:
space:
mode:
Diffstat (limited to 'cura')
-rwxr-xr-xcura/Arrange.py8
-rwxr-xr-xcura/BuildVolume.py31
-rw-r--r--cura/ConvexHullDecorator.py10
-rw-r--r--cura/CuraActions.py94
-rwxr-xr-xcura/CuraApplication.py46
-rw-r--r--cura/MultiplyObjectsJob.py55
-rw-r--r--cura/QualityManager.py4
-rwxr-xr-xcura/Settings/ExtruderManager.py59
-rw-r--r--cura/Settings/ExtrudersModel.py15
-rwxr-xr-xcura/Settings/MachineManager.py23
-rw-r--r--cura/Settings/ProfilesModel.py4
-rw-r--r--cura/Settings/SetObjectExtruderOperation.py27
-rw-r--r--cura/Settings/SettingInheritanceManager.py9
-rw-r--r--cura/Settings/SettingOverrideDecorator.py1
-rwxr-xr-xcura/ShapeArray.py4
15 files changed, 317 insertions, 73 deletions
diff --git a/cura/Arrange.py b/cura/Arrange.py
index 2348535efc..0d1f2e0c06 100755
--- a/cura/Arrange.py
+++ b/cura/Arrange.py
@@ -114,7 +114,7 @@ class Arrange:
self._priority_unique_values.sort()
## Return the amount of "penalty points" for polygon, which is the sum of priority
- # 999999 if occupied
+ # None if occupied
# \param x x-coordinate to check shape
# \param y y-coordinate
# \param shape_arr the ShapeArray object to place
@@ -128,9 +128,9 @@ class Arrange:
offset_x:offset_x + shape_arr.arr.shape[1]]
try:
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
- return 999999
+ return None
except IndexError: # out of bounds if you try to place an object outside
- return 999999
+ return None
prio_slice = self._priority[
offset_y:offset_y + shape_arr.arr.shape[0],
offset_x:offset_x + shape_arr.arr.shape[1]]
@@ -157,7 +157,7 @@ class Arrange:
# array to "world" coordinates
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
- if penalty_points != 999999:
+ if penalty_points is not None:
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py
index 16a11fbc1c..fbf4ba5080 100755
--- a/cura/BuildVolume.py
+++ b/cura/BuildVolume.py
@@ -25,6 +25,8 @@ catalog = i18nCatalog("cura")
import numpy
import math
+from typing import List
+
# Setting for clearance around the prime
PRIME_CLEARANCE = 6.5
@@ -129,7 +131,7 @@ class BuildVolume(SceneNode):
## Updates the listeners that listen for changes in per-mesh stacks.
#
# \param node The node for which the decorators changed.
- def _updateNodeListeners(self, node):
+ def _updateNodeListeners(self, node: SceneNode):
per_mesh_stack = node.callDecoration("getStack")
if per_mesh_stack:
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
@@ -139,21 +141,25 @@ class BuildVolume(SceneNode):
self._updateDisallowedAreasAndRebuild()
def setWidth(self, width):
- if width: self._width = width
+ if width is not None:
+ self._width = width
def setHeight(self, height):
- if height: self._height = height
+ if height is not None:
+ self._height = height
def setDepth(self, depth):
- if depth: self._depth = depth
+ if depth is not None:
+ self._depth = depth
- def setShape(self, shape):
- if shape: self._shape = shape
+ def setShape(self, shape: str):
+ if shape:
+ self._shape = shape
- def getDisallowedAreas(self):
+ def getDisallowedAreas(self) -> List[Polygon]:
return self._disallowed_areas
- def setDisallowedAreas(self, areas):
+ def setDisallowedAreas(self, areas: List[Polygon]):
self._disallowed_areas = areas
def render(self, renderer):
@@ -196,7 +202,6 @@ class BuildVolume(SceneNode):
return
for node in nodes:
-
# Need to check group nodes later
if node.callDecoration("isGroup"):
group_nodes.append(node) # Keep list of affected group_nodes
@@ -412,10 +417,10 @@ class BuildVolume(SceneNode):
self.updateNodeBoundaryCheck()
- def getBoundingBox(self):
+ def getBoundingBox(self) -> AxisAlignedBox:
return self._volume_aabb
- def getRaftThickness(self):
+ def getRaftThickness(self) -> float:
return self._raft_thickness
def _updateRaftThickness(self):
@@ -492,7 +497,7 @@ class BuildVolume(SceneNode):
self._engine_ready = True
self.rebuild()
- def _onSettingPropertyChanged(self, setting_key, property_name):
+ def _onSettingPropertyChanged(self, setting_key: str, property_name: str):
if property_name != "value":
return
@@ -525,7 +530,7 @@ class BuildVolume(SceneNode):
if rebuild_me:
self.rebuild()
- def hasErrors(self):
+ def hasErrors(self) -> bool:
return self._has_errors
## Calls _updateDisallowedAreas and makes sure the changes appear in the
diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py
index da72ffdbe3..404342fb78 100644
--- a/cura/ConvexHullDecorator.py
+++ b/cura/ConvexHullDecorator.py
@@ -59,7 +59,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
hull = self._compute2DConvexHull()
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"):
+ # Parent can be None if node is just loaded.
+ if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
hull = self._add2DAdhesionMargin(hull)
return hull
@@ -79,7 +80,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
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"):
+ if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
head_with_fans = self._compute2DConvexHeadMin()
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
return head_with_fans_with_adhesion_margin
@@ -93,8 +94,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
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"):
-
+ if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or 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
@@ -335,4 +335,4 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Settings that change the convex hull.
#
# If these settings change, the convex hull should be recalculated.
- _influencing_settings = {"xy_offset", "mold_enabled", "mold_width"} \ No newline at end of file
+ _influencing_settings = {"xy_offset", "mold_enabled", "mold_width"}
diff --git a/cura/CuraActions.py b/cura/CuraActions.py
index df26a9a9a6..eeebd3b6b2 100644
--- a/cura/CuraActions.py
+++ b/cura/CuraActions.py
@@ -1,10 +1,23 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtGui import QDesktopServices
from UM.FlameProfiler import pyqtSlot
from UM.Event import CallFunctionEvent
from UM.Application import Application
+from UM.Math.Vector import Vector
+from UM.Scene.Selection import Selection
+from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
+from UM.Operations.GroupedOperation import GroupedOperation
+from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
+from UM.Operations.SetTransformOperation import SetTransformOperation
+from cura.SetParentOperation import SetParentOperation
+from cura.MultiplyObjectsJob import MultiplyObjectsJob
+from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
+from cura.Settings.ExtruderManager import ExtruderManager
class CuraActions(QObject):
def __init__(self, parent = None):
@@ -23,5 +36,84 @@ class CuraActions(QObject):
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
Application.getInstance().functionEvent(event)
+ ## Center all objects in the selection
+ @pyqtSlot()
+ def centerSelection(self) -> None:
+ operation = GroupedOperation()
+ for node in Selection.getAllSelectedObjects():
+ current_node = node
+ while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
+ current_node = current_node.getParent()
+
+ center_operation = SetTransformOperation(current_node, Vector())
+ operation.addOperation(center_operation)
+ operation.push()
+
+ ## Multiply all objects in the selection
+ #
+ # \param count The number of times to multiply the selection.
+ @pyqtSlot(int)
+ def multiplySelection(self, count: int) -> None:
+ job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8)
+ job.start()
+
+ ## Delete all selected objects.
+ @pyqtSlot()
+ def deleteSelection(self) -> None:
+ if not Application.getInstance().getController().getToolsEnabled():
+ return
+
+ removed_group_nodes = []
+ op = GroupedOperation()
+ nodes = Selection.getAllSelectedObjects()
+ for node in nodes:
+ op.addOperation(RemoveSceneNodeOperation(node))
+ group_node = node.getParent()
+ if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
+ remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
+ if len(remaining_nodes_in_group) == 1:
+ removed_group_nodes.append(group_node)
+ op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
+ op.addOperation(RemoveSceneNodeOperation(group_node))
+ op.push()
+
+ ## Set the extruder that should be used to print the selection.
+ #
+ # \param extruder_id The ID of the extruder stack to use for the selected objects.
+ @pyqtSlot(str)
+ def setExtruderForSelection(self, extruder_id: str) -> None:
+ operation = GroupedOperation()
+
+ nodes_to_change = []
+ for node in Selection.getAllSelectedObjects():
+ # Do not change any nodes that already have the right extruder set.
+ if node.callDecoration("getActiveExtruder") == extruder_id:
+ continue
+
+ # If the node is a group, apply the active extruder to all children of the group.
+ if node.callDecoration("isGroup"):
+ for grouped_node in BreadthFirstIterator(node):
+ if grouped_node.callDecoration("getActiveExtruder") == extruder_id:
+ continue
+
+ if grouped_node.callDecoration("isGroup"):
+ continue
+
+ nodes_to_change.append(grouped_node)
+ continue
+
+ nodes_to_change.append(node)
+
+ if not nodes_to_change:
+ # If there are no changes to make, we still need to reset the selected extruders.
+ # This is a workaround for checked menu items being deselected while still being
+ # selected.
+ ExtruderManager.getInstance().resetSelectedObjectExtruders()
+ return
+
+ for node in nodes_to_change:
+ operation.addOperation(SetObjectExtruderOperation(node, extruder_id))
+ operation.push()
+
def _openUrl(self, url):
- QDesktopServices.openUrl(url) \ No newline at end of file
+ QDesktopServices.openUrl(url)
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index af23fcb4cf..5786c82147 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -26,6 +26,7 @@ from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Platform import Platform
+from UM.Decorators import deprecated
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
@@ -109,6 +110,10 @@ class CuraApplication(QtApplication):
Q_ENUMS(ResourceTypes)
def __init__(self):
+ # this list of dir names will be used by UM to detect an old cura directory
+ for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
+ Resources.addExpectedDirNameInData(dir_name)
+
Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
if not hasattr(sys, "frozen"):
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
@@ -214,6 +219,7 @@ class CuraApplication(QtApplication):
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
+ self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
@@ -803,6 +809,7 @@ class CuraApplication(QtApplication):
# Remove all selected objects from the scene.
@pyqtSlot()
+ @deprecated("Moved to CuraActions", "2.6")
def deleteSelection(self):
if not self.getController().getToolsEnabled():
return
@@ -823,6 +830,7 @@ class CuraApplication(QtApplication):
## Remove an object from the scene.
# Note that this only removes an object if it is selected.
@pyqtSlot("quint64")
+ @deprecated("Use deleteSelection instead", "2.6")
def deleteObject(self, object_id):
if not self.getController().getToolsEnabled():
return
@@ -850,13 +858,22 @@ class CuraApplication(QtApplication):
# \param count number of copies
# \param min_offset minimum offset to other objects.
@pyqtSlot("quint64", int)
+ @deprecated("Use CuraActions::multiplySelection", "2.6")
def multiplyObject(self, object_id, count, min_offset = 8):
- job = MultiplyObjectsJob(object_id, count, min_offset)
+ node = self.getController().getScene().findObject(object_id)
+ if not node:
+ node = Selection.getSelectedObject(0)
+
+ while node.getParent() and node.getParent().callDecoration("isGroup"):
+ node = node.getParent()
+
+ job = MultiplyObjectsJob([node], count, min_offset)
job.start()
return
## Center object on platform.
@pyqtSlot("quint64")
+ @deprecated("Use CuraActions::centerSelection", "2.6")
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
@@ -984,7 +1001,9 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
- nodes.append(node)
+ # Skip nodes that are too big
+ if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
+ nodes.append(node)
self.arrange(nodes, fixed_nodes = [])
## Arrange Selection
@@ -1278,13 +1297,18 @@ class CuraApplication(QtApplication):
# If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator):
node.addDecorator(ConvexHullDecorator())
+ for child in node.getAllChildren():
+ if not child.getDecorator(ConvexHullDecorator):
+ child.addDecorator(ConvexHullDecorator())
if node.callDecoration("isSliceable"):
- # Find node location
- offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
+ # Only check position if it's not already blatantly obvious that it won't fit.
+ if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
+ # Find node location
+ offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
- # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
- node,_ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
+ # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
+ node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()
@@ -1309,3 +1333,13 @@ class CuraApplication(QtApplication):
except Exception as e:
Logger.log("e", "Could not check file %s: %s", file_url, e)
return False
+
+ def _onContextMenuRequested(self, x: float, y: float) -> None:
+ # Ensure we select the object if we request a context menu over an object without having a selection.
+ if not Selection.hasSelection():
+ node = self.getController().getScene().findObject(self.getRenderer().getRenderPass("selection").getIdAtPosition(x, y))
+ if node:
+ while(node.getParent() and node.getParent().callDecoration("isGroup")):
+ node = node.getParent()
+
+ Selection.add(node)
diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py
index 870f165487..a795e0bc10 100644
--- a/cura/MultiplyObjectsJob.py
+++ b/cura/MultiplyObjectsJob.py
@@ -24,9 +24,9 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
class MultiplyObjectsJob(Job):
- def __init__(self, object_id, count, min_offset = 8):
+ def __init__(self, objects, count, min_offset = 8):
super().__init__()
- self._object_id = object_id
+ self._objects = objects
self._count = count
self._min_offset = min_offset
@@ -35,33 +35,42 @@ class MultiplyObjectsJob(Job):
dismissable=False, progress=0)
status_message.show()
scene = Application.getInstance().getController().getScene()
- node = scene.findObject(self._object_id)
- if not node and self._object_id != 0: # Workaround for tool handles overlapping the selected object
- node = Selection.getSelectedObject(0)
-
- # If object is part of a group, multiply group
- current_node = node
- while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
- current_node = current_node.getParent()
+ total_progress = len(self._objects) * self._count
+ current_progress = 0
root = scene.getRoot()
arranger = Arrange.create(scene_root=root)
- offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
nodes = []
- found_solution_for_all = True
- for i in range(self._count):
- # We do place the nodes one by one, as we want to yield in between.
- node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
- if not solution_found:
- found_solution_for_all = False
- new_location = node.getPosition()
- new_location = new_location.set(z = 100 - i * 20)
- node.setPosition(new_location)
+ for node in self._objects:
+ # If object is part of a group, multiply group
+ current_node = node
+ while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
+ current_node = current_node.getParent()
+
+ node_too_big = False
+ if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
+ offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
+ else:
+ node_too_big = True
+
+ found_solution_for_all = True
+ for i in range(self._count):
+ # We do place the nodes one by one, as we want to yield in between.
+ if not node_too_big:
+ node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
+ if node_too_big or not solution_found:
+ found_solution_for_all = False
+ new_location = node.getPosition()
+ new_location = new_location.set(z = 100 - i * 20)
+ node.setPosition(new_location)
+
+ nodes.append(node)
+ current_progress += 1
+ status_message.setProgress((current_progress / total_progress) * 100)
+ Job.yieldThread()
- nodes.append(node)
Job.yieldThread()
- status_message.setProgress((i + 1) / self._count * 100)
if nodes:
op = GroupedOperation()
@@ -72,4 +81,4 @@ class MultiplyObjectsJob(Job):
if not found_solution_for_all:
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"))
- no_full_solution_message.show() \ No newline at end of file
+ no_full_solution_message.show()
diff --git a/cura/QualityManager.py b/cura/QualityManager.py
index d7b2c7d705..f0f095b912 100644
--- a/cura/QualityManager.py
+++ b/cura/QualityManager.py
@@ -16,9 +16,9 @@ class QualityManager:
## Get the singleton instance for this class.
@classmethod
- def getInstance(cls):
+ def getInstance(cls) -> "QualityManager":
# Note: Explicit use of class name to prevent issues with inheritance.
- if QualityManager.__instance is None:
+ if not QualityManager.__instance:
QualityManager.__instance = cls()
return QualityManager.__instance
diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py
index 07a32143c1..21cd164ed4 100755
--- a/cura/Settings/ExtruderManager.py
+++ b/cura/Settings/ExtruderManager.py
@@ -8,12 +8,14 @@ from UM.Application import Application #To get the global container stack to fin
from UM.Logger import Logger
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
+from UM.Scene.Selection import Selection
+from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers by ID.
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
-from typing import Optional
+from typing import Optional, List
## Manages all existing extruder stacks.
#
@@ -34,10 +36,13 @@ class ExtruderManager(QObject):
super().__init__(parent)
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
self._active_extruder_index = 0
+ self._selected_object_extruders = []
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
self._global_container_stack_definition_id = None
self._addCurrentMachineExtruders()
+ Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
+
## Gets the unique identifier of the currently active extruder stack.
#
# The currently active extruder stack is the stack that is currently being
@@ -117,6 +122,48 @@ class ExtruderManager(QObject):
except IndexError:
return ""
+ ## Emitted whenever the selectedObjectExtruders property changes.
+ selectedObjectExtrudersChanged = pyqtSignal()
+
+ ## Provides a list of extruder IDs used by the current selected objects.
+ @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
+ def selectedObjectExtruders(self) -> List[str]:
+ if not self._selected_object_extruders:
+ object_extruders = set()
+
+ # First, build a list of the actual selected objects (including children of groups, excluding group nodes)
+ selected_nodes = []
+ for node in Selection.getAllSelectedObjects():
+ if node.callDecoration("isGroup"):
+ for grouped_node in BreadthFirstIterator(node):
+ if grouped_node.callDecoration("isGroup"):
+ continue
+
+ selected_nodes.append(grouped_node)
+ else:
+ selected_nodes.append(node)
+
+ # Then, figure out which nodes are used by those selected nodes.
+ for node in selected_nodes:
+ extruder = node.callDecoration("getActiveExtruder")
+ if extruder:
+ object_extruders.add(extruder)
+ else:
+ global_stack = Application.getInstance().getGlobalContainerStack()
+ object_extruders.add(self._extruder_trains[global_stack.getId()]["0"].getId())
+
+ self._selected_object_extruders = list(object_extruders)
+
+ return self._selected_object_extruders
+
+ ## Reset the internal list used for the selectedObjectExtruders property
+ #
+ # This will trigger a recalculation of the extruders used for the
+ # selection.
+ def resetSelectedObjectExtruders(self) -> None:
+ self._selected_object_extruders = []
+ self.selectedObjectExtrudersChanged.emit()
+
def getActiveExtruderStack(self) -> ContainerStack:
global_container_stack = Application.getInstance().getGlobalContainerStack()
@@ -244,7 +291,13 @@ class ExtruderManager(QObject):
material = materials[0]
preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
if preferred_material_id:
- search_criteria = { "type": "material", "id": preferred_material_id}
+ global_stack = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
+ if global_stack:
+ approximate_material_diameter = round(global_stack[0].getProperty("material_diameter", "value"))
+ else:
+ approximate_material_diameter = round(machine_definition.getProperty("material_diameter", "value"))
+
+ search_criteria = { "type": "material", "id": preferred_material_id, "approximate_diameter": approximate_material_diameter}
if machine_definition.getMetaDataEntry("has_machine_materials"):
search_criteria["definition"] = machine_definition_id
@@ -443,6 +496,8 @@ class ExtruderManager(QObject):
self.globalContainerStackDefinitionChanged.emit()
self.activeExtruderChanged.emit()
+ self.resetSelectedObjectExtruders()
+
## Adds the extruders of the currently active machine.
def _addCurrentMachineExtruders(self) -> None:
global_stack = Application.getInstance().getGlobalContainerStack()
diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py
index 7f4a77eb5f..62b1f0af2c 100644
--- a/cura/Settings/ExtrudersModel.py
+++ b/cura/Settings/ExtrudersModel.py
@@ -1,7 +1,7 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
-from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
+from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, pyqtSlot
import UM.Qt.ListModel
from UM.Application import Application
@@ -33,6 +33,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# The ID of the definition of the extruder.
DefinitionRole = Qt.UserRole + 5
+ # The material of the extruder.
+ MaterialRole = Qt.UserRole + 6
+
+ # The variant of the extruder.
+ VariantRole = Qt.UserRole + 7
+
## List of colours to display if there is no material or the material has no known
# colour.
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
@@ -49,6 +55,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
self.addRoleName(self.ColorRole, "color")
self.addRoleName(self.IndexRole, "index")
self.addRoleName(self.DefinitionRole, "definition")
+ self.addRoleName(self.MaterialRole, "material")
+ self.addRoleName(self.VariantRole, "variant")
self._add_global = False
self._simple_names = False
@@ -140,6 +148,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
extruder_name = extruder.getName()
material = extruder.findContainer({ "type": "material" })
+ variant = extruder.findContainer({"type": "variant"})
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
try:
position = int(position)
@@ -152,7 +161,9 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"name": extruder_name,
"color": color,
"index": position,
- "definition": extruder.getBottom().getId()
+ "definition": extruder.getBottom().getId(),
+ "material": material.getName() if material else "",
+ "variant": variant.getName() if variant else "",
}
items.append(item)
changed = True
diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py
index 638b475094..493f8fcf07 100755
--- a/cura/Settings/MachineManager.py
+++ b/cura/Settings/MachineManager.py
@@ -15,9 +15,7 @@ from UM.Message import Message
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
-from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.SettingFunction import SettingFunction
-from UM.Settings.Validator import ValidatorState
from UM.Signal import postponeSignals
from cura.QualityManager import QualityManager
@@ -27,6 +25,11 @@ from cura.Settings.ExtruderManager import ExtruderManager
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from UM.Settings.DefinitionContainer import DefinitionContainer
+
import os
class MachineManager(QObject):
@@ -329,10 +332,11 @@ class MachineManager(QObject):
name = self._createUniqueName("machine", "", name, definition.getName())
new_global_stack = ContainerStack(name)
new_global_stack.addMetaDataEntry("type", "machine")
+ new_global_stack.addContainer(definition)
container_registry.addContainer(new_global_stack)
variant_instance_container = self._updateVariantContainer(definition)
- material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
+ material_instance_container = self._updateMaterialContainer(definition, new_global_stack, variant_instance_container)
quality_instance_container = self._updateQualityContainer(definition, variant_instance_container, material_instance_container)
current_settings_instance_container = InstanceContainer(name + "_current_settings")
@@ -341,7 +345,7 @@ class MachineManager(QObject):
current_settings_instance_container.setDefinition(definitions[0])
container_registry.addContainer(current_settings_instance_container)
- new_global_stack.addContainer(definition)
+
if variant_instance_container:
new_global_stack.addContainer(variant_instance_container)
if material_instance_container:
@@ -760,7 +764,7 @@ class MachineManager(QObject):
if old_material:
preferred_material_name = old_material.getName()
- self.setActiveMaterial(self._updateMaterialContainer(self._global_container_stack.getBottom(), containers[0], preferred_material_name).id)
+ self.setActiveMaterial(self._updateMaterialContainer(self._global_container_stack.getBottom(), self._global_container_stack, containers[0], preferred_material_name).id)
else:
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
@@ -1094,7 +1098,7 @@ class MachineManager(QObject):
def createMachineManager(engine=None, script_engine=None):
return MachineManager()
- def _updateVariantContainer(self, definition):
+ def _updateVariantContainer(self, definition: "DefinitionContainer"):
if not definition.getMetaDataEntry("has_variants"):
return self._empty_variant_container
machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(definition)
@@ -1110,11 +1114,12 @@ class MachineManager(QObject):
return self._empty_variant_container
- def _updateMaterialContainer(self, definition, variant_container = None, preferred_material_name = None):
+ def _updateMaterialContainer(self, definition: "DefinitionContainer", stack: "ContainerStack", variant_container: Optional["InstanceContainer"] = None, preferred_material_name: Optional[str] = None):
if not definition.getMetaDataEntry("has_materials"):
return self._empty_material_container
- search_criteria = { "type": "material" }
+ approximate_material_diameter = round(stack.getProperty("material_diameter", "value"))
+ search_criteria = { "type": "material", "approximate_diameter": approximate_material_diameter }
if definition.getMetaDataEntry("has_machine_materials"):
search_criteria["definition"] = self.getQualityDefinitionId(definition)
@@ -1146,7 +1151,7 @@ class MachineManager(QObject):
Logger.log("w", "Unable to find a material container with provided criteria, returning an empty one instead.")
return self._empty_material_container
- def _updateQualityContainer(self, definition, variant_container, material_container = None, preferred_quality_name = None):
+ def _updateQualityContainer(self, definition: "DefinitionContainer", variant_container: "ContainerStack", material_container = None, preferred_quality_name: Optional[str] = None):
container_registry = ContainerRegistry.getInstance()
search_criteria = { "type": "quality" }
diff --git a/cura/Settings/ProfilesModel.py b/cura/Settings/ProfilesModel.py
index 404bb569a5..9056273216 100644
--- a/cura/Settings/ProfilesModel.py
+++ b/cura/Settings/ProfilesModel.py
@@ -32,9 +32,9 @@ class ProfilesModel(InstanceContainersModel):
## Get the singleton instance for this class.
@classmethod
- def getInstance(cls):
+ def getInstance(cls) -> "ProfilesModel":
# Note: Explicit use of class name to prevent issues with inheritance.
- if ProfilesModel.__instance is None:
+ if not ProfilesModel.__instance:
ProfilesModel.__instance = cls()
return ProfilesModel.__instance
diff --git a/cura/Settings/SetObjectExtruderOperation.py b/cura/Settings/SetObjectExtruderOperation.py
new file mode 100644
index 0000000000..31c996529a
--- /dev/null
+++ b/cura/Settings/SetObjectExtruderOperation.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+from UM.Scene.SceneNode import SceneNode
+from UM.Operations.Operation import Operation
+
+from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
+
+## Simple operation to set the extruder a certain object should be printed with.
+class SetObjectExtruderOperation(Operation):
+ def __init__(self, node: SceneNode, extruder_id: str) -> None:
+ self._node = node
+ self._extruder_id = extruder_id
+ self._previous_extruder_id = None
+ self._decorator_added = False
+
+ def undo(self):
+ if self._previous_extruder_id:
+ self._node.callDecoration("setActiveExtruder", self._previous_extruder_id)
+
+ def redo(self):
+ stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
+ if not stack:
+ self._node.addDecorator(SettingOverrideDecorator())
+
+ self._previous_extruder_id = self._node.callDecoration("getActiveExtruder")
+ self._node.callDecoration("setActiveExtruder", self._extruder_id)
diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py
index 2f81526813..ff0d1d81c0 100644
--- a/cura/Settings/SettingInheritanceManager.py
+++ b/cura/Settings/SettingInheritanceManager.py
@@ -109,10 +109,13 @@ class SettingInheritanceManager(QObject):
self._settings_with_inheritance_warning.remove(key)
settings_with_inheritance_warning_changed = True
- # Find the topmost parent (Assumed to be a category)
parent = definitions[0].parent
- while parent.parent is not None:
- parent = parent.parent
+ # Find the topmost parent (Assumed to be a category)
+ if parent is not None:
+ while parent.parent is not None:
+ parent = parent.parent
+ else:
+ parent = definitions[0] # Already at a category
if parent.key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
# Category was not in the list yet, so needs to be added now.
diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py
index 76c155cb99..d754b6bc6d 100644
--- a/cura/Settings/SettingOverrideDecorator.py
+++ b/cura/Settings/SettingOverrideDecorator.py
@@ -109,6 +109,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def setActiveExtruder(self, extruder_stack_id):
self._extruder_stack = extruder_stack_id
self._updateNextStack()
+ ExtruderManager.getInstance().resetSelectedObjectExtruders()
self.activeExtruderChanged.emit()
def getStack(self):
diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py
index 534fa78e4d..95d0201c38 100755
--- a/cura/ShapeArray.py
+++ b/cura/ShapeArray.py
@@ -43,8 +43,10 @@ class ShapeArray:
transform_x = transform._data[0][3]
transform_y = transform._data[2][3]
hull_verts = node.callDecoration("getConvexHull")
+ # For one_at_a_time printing you need the convex hull head.
+ hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
- offset_verts = hull_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
+ offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
offset_points = copy.deepcopy(offset_verts._points) # x, y
offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)
offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y)