ModelChecker.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
  5. from UM.Application import Application
  6. from UM.Extension import Extension
  7. from UM.Logger import Logger
  8. from UM.Message import Message
  9. from UM.Scene.Camera import Camera
  10. from UM.i18n import i18nCatalog
  11. from UM.PluginRegistry import PluginRegistry
  12. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  13. catalog = i18nCatalog("cura")
  14. class ModelChecker(QObject, Extension):
  15. onChanged = pyqtSignal()
  16. """Signal that gets emitted when anything changed that we need to check."""
  17. def __init__(self):
  18. super().__init__()
  19. self._button_view = None
  20. self._caution_message = Message("", #Message text gets set when the message gets shown, to display the models in question.
  21. lifetime = 0,
  22. title = catalog.i18nc("@info:title", "3D Model Assistant"),
  23. message_type = Message.MessageType.WARNING)
  24. self._change_timer = QTimer()
  25. self._change_timer.setInterval(200)
  26. self._change_timer.setSingleShot(True)
  27. self._change_timer.timeout.connect(self.onChanged)
  28. Application.getInstance().initializationFinished.connect(self._pluginsInitialized)
  29. Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged)
  30. Application.getInstance().globalContainerStackChanged.connect(self._onChanged)
  31. def _onChanged(self, *args, **kwargs):
  32. # Ignore camera updates.
  33. if len(args) == 0:
  34. self._change_timer.start()
  35. return
  36. if not isinstance(args[0], Camera):
  37. self._change_timer.start()
  38. def _pluginsInitialized(self):
  39. """Called when plug-ins are initialized.
  40. This makes sure that we listen to changes of the material and that the
  41. button is created that indicates warnings with the current set-up.
  42. """
  43. Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged)
  44. self._createView()
  45. def checkObjectsForShrinkage(self):
  46. shrinkage_threshold = 100.5 #From what shrinkage percentage a warning will be issued about the model size.
  47. warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials.
  48. warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials.
  49. # This function can be triggered in the middle of a machine change, so do not proceed if the machine change
  50. # has not done yet.
  51. global_container_stack = Application.getInstance().getGlobalContainerStack()
  52. if global_container_stack is None:
  53. return False
  54. material_shrinkage = self._getMaterialShrinkage()
  55. warning_nodes = []
  56. # Check node material shrinkage and bounding box size
  57. for node in self.sliceableNodes():
  58. node_extruder_position = node.callDecoration("getActiveExtruderPosition")
  59. if node_extruder_position is None:
  60. continue
  61. # This function can be triggered in the middle of a machine change, so do not proceed if the machine change
  62. # has not done yet.
  63. try:
  64. global_container_stack.extruderList[int(node_extruder_position)]
  65. except IndexError:
  66. Application.getInstance().callLater(lambda: self.onChanged.emit())
  67. return False
  68. if material_shrinkage > shrinkage_threshold:
  69. bbox = node.getBoundingBox()
  70. if bbox is not None and (bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z):
  71. warning_nodes.append(node)
  72. self._caution_message.setText(catalog.i18nc(
  73. "@info:status",
  74. "<p>One or more 3D models may not print optimally due to the model size and material configuration:</p>\n"
  75. "<p>{model_names}</p>\n"
  76. "<p>Find out how to ensure the best possible print quality and reliability.</p>\n"
  77. "<p><a href=\"https://ultimaker.com/3D-model-assistant\">View print quality guide</a></p>"
  78. ).format(model_names = ", ".join([n.getName() for n in warning_nodes])))
  79. return len(warning_nodes) > 0
  80. def sliceableNodes(self):
  81. # Add all sliceable scene nodes to check
  82. scene = Application.getInstance().getController().getScene()
  83. for node in DepthFirstIterator(scene.getRoot()):
  84. if node.callDecoration("isSliceable"):
  85. yield node
  86. def _createView(self):
  87. """Creates the view used by show popup.
  88. The view is saved because of the fairly aggressive garbage collection.
  89. """
  90. Logger.log("d", "Creating model checker view.")
  91. # Create the plugin dialog component
  92. path = os.path.join(PluginRegistry.getInstance().getPluginPath("ModelChecker"), "ModelChecker.qml")
  93. self._button_view = Application.getInstance().createQmlComponent(path, {"manager": self})
  94. # The qml is only the button
  95. Application.getInstance().addAdditionalComponent("jobSpecsButton", self._button_view)
  96. Logger.log("d", "Model checker view created.")
  97. @pyqtProperty(bool, notify = onChanged)
  98. def hasWarnings(self):
  99. danger_shrinkage = self.checkObjectsForShrinkage()
  100. return any((danger_shrinkage, )) #If any of the checks fail, show the warning button.
  101. @pyqtSlot()
  102. def showWarnings(self):
  103. self._caution_message.show()
  104. def _getMaterialShrinkage(self) -> float:
  105. global_container_stack = Application.getInstance().getGlobalContainerStack()
  106. if global_container_stack is None:
  107. return 100
  108. return global_container_stack.getProperty("material_shrinkage_percentage", "value")