diff options
Diffstat (limited to 'cura')
-rwxr-xr-x | cura/Arrange.py | 8 | ||||
-rwxr-xr-x | cura/BuildVolume.py | 31 | ||||
-rw-r--r-- | cura/ConvexHullDecorator.py | 10 | ||||
-rw-r--r-- | cura/CuraActions.py | 94 | ||||
-rwxr-xr-x | cura/CuraApplication.py | 46 | ||||
-rw-r--r-- | cura/MultiplyObjectsJob.py | 55 | ||||
-rw-r--r-- | cura/QualityManager.py | 4 | ||||
-rwxr-xr-x | cura/Settings/ExtruderManager.py | 59 | ||||
-rw-r--r-- | cura/Settings/ExtrudersModel.py | 15 | ||||
-rwxr-xr-x | cura/Settings/MachineManager.py | 23 | ||||
-rw-r--r-- | cura/Settings/ProfilesModel.py | 4 | ||||
-rw-r--r-- | cura/Settings/SetObjectExtruderOperation.py | 27 | ||||
-rw-r--r-- | cura/Settings/SettingInheritanceManager.py | 9 | ||||
-rw-r--r-- | cura/Settings/SettingOverrideDecorator.py | 1 | ||||
-rwxr-xr-x | cura/ShapeArray.py | 4 |
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) |