ObjectsModel.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Logger import Logger
  4. import re
  5. from typing import Dict, List, Optional, Union
  6. from PyQt6.QtCore import QTimer, Qt
  7. from UM.Application import Application
  8. from UM.Qt.ListModel import ListModel
  9. from UM.Scene.Camera import Camera
  10. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  11. from UM.Scene.SceneNode import SceneNode
  12. from UM.Scene.Selection import Selection
  13. from UM.i18n import i18nCatalog
  14. from cura.PrintOrderManager import PrintOrderManager
  15. from cura.Scene.CuraSceneNode import CuraSceneNode
  16. catalog = i18nCatalog("cura")
  17. # Simple convenience class to keep stuff together. Since we're still stuck on python 3.5, we can't use the full
  18. # typed named tuple, so we have to do it like this.
  19. # Once we are at python 3.6, feel free to change this to a named tuple.
  20. class _NodeInfo:
  21. def __init__(self, index_to_node: Optional[Dict[int, SceneNode]] = None, nodes_to_rename: Optional[List[SceneNode]] = None, is_group: bool = False) -> None:
  22. if index_to_node is None:
  23. index_to_node = {}
  24. if nodes_to_rename is None:
  25. nodes_to_rename = []
  26. self.index_to_node = index_to_node # type: Dict[int, SceneNode]
  27. self.nodes_to_rename = nodes_to_rename # type: List[SceneNode]
  28. self.is_group = is_group # type: bool
  29. class ObjectsModel(ListModel):
  30. """Keep track of all objects in the project"""
  31. NameRole = Qt.ItemDataRole.UserRole + 1
  32. SelectedRole = Qt.ItemDataRole.UserRole + 2
  33. OutsideAreaRole = Qt.ItemDataRole.UserRole + 3
  34. BuilplateNumberRole = Qt.ItemDataRole.UserRole + 4
  35. NodeRole = Qt.ItemDataRole.UserRole + 5
  36. PerObjectSettingsCountRole = Qt.ItemDataRole.UserRole + 6
  37. MeshTypeRole = Qt.ItemDataRole.UserRole + 7
  38. ExtruderNumberRole = Qt.ItemDataRole.UserRole + 8
  39. def __init__(self, parent = None) -> None:
  40. super().__init__(parent)
  41. self.addRoleName(self.NameRole, "name")
  42. self.addRoleName(self.SelectedRole, "selected")
  43. self.addRoleName(self.OutsideAreaRole, "outside_build_area")
  44. self.addRoleName(self.BuilplateNumberRole, "buildplate_number")
  45. self.addRoleName(self.ExtruderNumberRole, "extruder_number")
  46. self.addRoleName(self.PerObjectSettingsCountRole, "per_object_settings_count")
  47. self.addRoleName(self.MeshTypeRole, "mesh_type")
  48. self.addRoleName(self.NodeRole, "node")
  49. Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
  50. Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
  51. Selection.selectionChanged.connect(self._updateDelayed)
  52. self._update_timer = QTimer()
  53. self._update_timer.setInterval(200)
  54. self._update_timer.setSingleShot(True)
  55. self._update_timer.timeout.connect(self._update)
  56. self._build_plate_number = -1
  57. self._group_name_template = catalog.i18nc("@label", "Group #{group_nr}")
  58. self._group_name_prefix = self._group_name_template.split("#")[0]
  59. self._naming_regex = re.compile(r"^(.+)\(([0-9]+)\)$")
  60. def setActiveBuildPlate(self, nr: int) -> None:
  61. if self._build_plate_number != nr:
  62. self._build_plate_number = nr
  63. self._update()
  64. def getNodes(self) -> List[CuraSceneNode]:
  65. return list(map(lambda n: n["node"], self.items))
  66. def _updateSceneDelayed(self, source) -> None:
  67. if not isinstance(source, Camera):
  68. self._update_timer.start()
  69. def _updateDelayed(self, *args) -> None:
  70. self._update_timer.start()
  71. def _shouldNodeBeHandled(self, node: SceneNode) -> bool:
  72. is_group = bool(node.callDecoration("isGroup"))
  73. if not node.callDecoration("isSliceable") and not is_group:
  74. return False
  75. parent = node.getParent()
  76. if parent and parent.callDecoration("isGroup"):
  77. return False # Grouped nodes don't need resetting as their parent (the group) is reset)
  78. node_build_plate_number = node.callDecoration("getBuildPlateNumber")
  79. if Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") and node_build_plate_number != self._build_plate_number:
  80. return False
  81. return True
  82. @staticmethod
  83. def _renameNodes(node_info_dict: Dict[str, _NodeInfo]) -> List[SceneNode]:
  84. # Go through all names and find out the names for all nodes that need to be renamed.
  85. all_nodes = [] # type: List[SceneNode]
  86. for name, node_info in node_info_dict.items():
  87. # First add the ones that do not need to be renamed.
  88. for node in node_info.index_to_node.values():
  89. all_nodes.append(node)
  90. # Generate new names for the nodes that need to be renamed
  91. current_index = 0
  92. for node in node_info.nodes_to_rename:
  93. current_index += 1
  94. while current_index in node_info.index_to_node:
  95. current_index += 1
  96. if not node_info.is_group:
  97. new_group_name = "{0}({1})".format(name, current_index)
  98. else:
  99. new_group_name = "{0}#{1}".format(name, current_index)
  100. node.setName(new_group_name)
  101. all_nodes.append(node)
  102. return all_nodes
  103. def _update(self, *args) -> None:
  104. nodes = [] # type: List[Dict[str, Union[str, int, bool, SceneNode]]]
  105. name_to_node_info_dict = {} # type: Dict[str, _NodeInfo]
  106. for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore
  107. if not self._shouldNodeBeHandled(node):
  108. continue
  109. is_group = bool(node.callDecoration("isGroup"))
  110. name_handled_as_group = False
  111. force_rename = False
  112. if is_group:
  113. # Handle names for grouped nodes
  114. original_name = self._group_name_prefix
  115. current_name = node.getName()
  116. if current_name.startswith(self._group_name_prefix):
  117. # This group has a standard group name, but we may need to renumber it
  118. name_index = int(current_name.split("#")[-1])
  119. name_handled_as_group = True
  120. elif not current_name:
  121. # Force rename this group because this node has not been named as a group yet, probably because
  122. # it's a newly created group.
  123. name_index = 0
  124. force_rename = True
  125. name_handled_as_group = True
  126. if not is_group or not name_handled_as_group:
  127. # Handle names for individual nodes or groups that already have a non-group name
  128. name = node.getName()
  129. name_match = self._naming_regex.fullmatch(name)
  130. if name_match is None:
  131. original_name = name
  132. name_index = 0
  133. else:
  134. original_name = name_match.groups()[0]
  135. name_index = int(name_match.groups()[1])
  136. if original_name not in name_to_node_info_dict:
  137. # Keep track of 2 things:
  138. # - known indices for nodes which doesn't need to be renamed
  139. # - a list of nodes that need to be renamed. When renaming then, we should avoid using the known indices.
  140. name_to_node_info_dict[original_name] = _NodeInfo(is_group = is_group)
  141. node_info = name_to_node_info_dict[original_name]
  142. if not force_rename and name_index not in node_info.index_to_node:
  143. node_info.index_to_node[name_index] = node
  144. else:
  145. node_info.nodes_to_rename.append(node)
  146. all_nodes = self._renameNodes(name_to_node_info_dict)
  147. user_defined_print_order_enabled = PrintOrderManager.isUserDefinedPrintOrderEnabled()
  148. if user_defined_print_order_enabled:
  149. PrintOrderManager.initializePrintOrders(all_nodes)
  150. for node in all_nodes:
  151. if hasattr(node, "isOutsideBuildArea"):
  152. is_outside_build_area = node.isOutsideBuildArea() # type: ignore
  153. else:
  154. is_outside_build_area = False
  155. node_build_plate_number = node.callDecoration("getBuildPlateNumber")
  156. node_mesh_type = ""
  157. per_object_settings_count = 0
  158. per_object_stack = node.callDecoration("getStack")
  159. if per_object_stack:
  160. per_object_settings_count = per_object_stack.getTop().getNumInstances()
  161. if node.callDecoration("isAntiOverhangMesh"):
  162. node_mesh_type = "anti_overhang_mesh"
  163. per_object_settings_count -= 1 # do not count this mesh type setting
  164. elif node.callDecoration("isSupportMesh"):
  165. node_mesh_type = "support_mesh"
  166. per_object_settings_count -= 1 # do not count this mesh type setting
  167. elif node.callDecoration("isCuttingMesh"):
  168. node_mesh_type = "cutting_mesh"
  169. per_object_settings_count -= 1 # do not count this mesh type setting
  170. elif node.callDecoration("isInfillMesh"):
  171. node_mesh_type = "infill_mesh"
  172. per_object_settings_count -= 1 # do not count this mesh type setting
  173. if per_object_settings_count > 0:
  174. if node_mesh_type == "support_mesh":
  175. # support meshes only allow support settings
  176. per_object_settings_count = 0
  177. for key in per_object_stack.getTop().getAllKeys():
  178. if per_object_stack.getTop().getInstance(key).definition.isAncestor("support"):
  179. per_object_settings_count += 1
  180. elif node_mesh_type == "anti_overhang_mesh":
  181. # anti overhang meshes ignore per model settings
  182. per_object_settings_count = 0
  183. extruder_position = node.callDecoration("getActiveExtruderPosition")
  184. if extruder_position is None:
  185. extruder_number = -1
  186. else:
  187. extruder_number = int(extruder_position)
  188. if node_mesh_type == "anti_overhang_mesh" or node.callDecoration("isGroup"):
  189. # for anti overhang meshes and groups the extruder nr is irrelevant
  190. extruder_number = -1
  191. if not user_defined_print_order_enabled:
  192. name = node.getName()
  193. else:
  194. name = "{print_order}. {name}".format(print_order = node.printOrder, name = node.getName())
  195. nodes.append({
  196. "name": name,
  197. "selected": Selection.isSelected(node),
  198. "outside_build_area": is_outside_build_area,
  199. "buildplate_number": node_build_plate_number,
  200. "extruder_number": extruder_number,
  201. "per_object_settings_count": per_object_settings_count,
  202. "mesh_type": node_mesh_type,
  203. "node": node
  204. })
  205. nodes = sorted(nodes, key=lambda n: n["name"] if not user_defined_print_order_enabled else n["node"].printOrder)
  206. self.setItems(nodes)