LayerView.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.PluginRegistry import PluginRegistry
  4. from UM.View.View import View
  5. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  6. from UM.Resources import Resources
  7. from UM.Event import Event, KeyEvent
  8. from UM.Signal import Signal
  9. from UM.Scene.Selection import Selection
  10. from UM.Math.Color import Color
  11. from UM.Mesh.MeshBuilder import MeshBuilder
  12. from UM.Job import Job
  13. from UM.Preferences import Preferences
  14. from UM.Logger import Logger
  15. from UM.View.GL.OpenGL import OpenGL
  16. from UM.Message import Message
  17. from UM.Application import Application
  18. from cura.ConvexHullNode import ConvexHullNode
  19. from cura.Settings.ExtruderManager import ExtruderManager
  20. from PyQt5.QtCore import Qt
  21. from PyQt5.QtWidgets import QApplication
  22. from . import LayerViewProxy
  23. from UM.i18n import i18nCatalog
  24. catalog = i18nCatalog("cura")
  25. from . import LayerPass
  26. import numpy
  27. import os.path
  28. ## View used to display g-code paths.
  29. class LayerView(View):
  30. def __init__(self):
  31. super().__init__()
  32. self._max_layers = 0
  33. self._current_layer_num = 0
  34. self._minimum_layer_num = 0
  35. self._current_layer_mesh = None
  36. self._current_layer_jumps = None
  37. self._top_layers_job = None
  38. self._activity = False
  39. self._old_max_layers = 0
  40. self._busy = False
  41. self._ghost_shader = None
  42. self._layer_pass = None
  43. self._composite_pass = None
  44. self._old_layer_bindings = None
  45. self._layerview_composite_shader = None
  46. self._old_composite_shader = None
  47. self._global_container_stack = None
  48. self._proxy = LayerViewProxy.LayerViewProxy()
  49. self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
  50. self._resetSettings()
  51. self._legend_items = None
  52. Preferences.getInstance().addPreference("view/top_layer_count", 5)
  53. Preferences.getInstance().addPreference("view/only_show_top_layers", False)
  54. Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
  55. self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
  56. self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
  57. self._compatibility_mode = True # for safety
  58. self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"))
  59. def _resetSettings(self):
  60. self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
  61. self._extruder_count = 0
  62. self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
  63. self._show_travel_moves = 0
  64. self._show_support = 1
  65. self._show_adhesion = 1
  66. self._show_skin = 1
  67. self._show_infill = 1
  68. def getActivity(self):
  69. return self._activity
  70. def getLayerPass(self):
  71. if not self._layer_pass:
  72. # Currently the RenderPass constructor requires a size > 0
  73. # This should be fixed in RenderPass's constructor.
  74. self._layer_pass = LayerPass.LayerPass(1, 1)
  75. self._compatibility_mode = not self.getRenderer().getSupportsGeometryShader() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
  76. self._layer_pass.setLayerView(self)
  77. self.getRenderer().addRenderPass(self._layer_pass)
  78. return self._layer_pass
  79. def getCurrentLayer(self):
  80. return self._current_layer_num
  81. def getMinimumLayer(self):
  82. return self._minimum_layer_num
  83. def _onSceneChanged(self, node):
  84. self.calculateMaxLayers()
  85. def getMaxLayers(self):
  86. return self._max_layers
  87. busyChanged = Signal()
  88. def isBusy(self):
  89. return self._busy
  90. def setBusy(self, busy):
  91. if busy != self._busy:
  92. self._busy = busy
  93. self.busyChanged.emit()
  94. def resetLayerData(self):
  95. self._current_layer_mesh = None
  96. self._current_layer_jumps = None
  97. def beginRendering(self):
  98. scene = self.getController().getScene()
  99. renderer = self.getRenderer()
  100. if not self._ghost_shader:
  101. self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
  102. self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb()))
  103. for node in DepthFirstIterator(scene.getRoot()):
  104. # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
  105. # However, it is somewhat relevant when the node is selected, so do render it then.
  106. if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
  107. continue
  108. if not node.render(renderer):
  109. if (node.getMeshData()) and node.isVisible():
  110. renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
  111. def setLayer(self, value):
  112. if self._current_layer_num != value:
  113. self._current_layer_num = value
  114. if self._current_layer_num < 0:
  115. self._current_layer_num = 0
  116. if self._current_layer_num > self._max_layers:
  117. self._current_layer_num = self._max_layers
  118. self._startUpdateTopLayers()
  119. self.currentLayerNumChanged.emit()
  120. def setMinimumLayer(self, value):
  121. if self._minimum_layer_num != value:
  122. self._minimum_layer_num = value
  123. if self._minimum_layer_num < 0:
  124. self._minimum_layer_num = 0
  125. self._startUpdateTopLayers()
  126. self.currentLayerNumChanged.emit()
  127. def setLayerViewType(self, layer_view_type):
  128. self._layer_view_type = layer_view_type
  129. self.currentLayerNumChanged.emit()
  130. def getLayerViewType(self):
  131. return self._layer_view_type
  132. def setExtruderOpacity(self, extruder_nr, opacity):
  133. self._extruder_opacity[extruder_nr] = opacity
  134. self.currentLayerNumChanged.emit()
  135. def getExtruderOpacities(self):
  136. return self._extruder_opacity
  137. def setShowTravelMoves(self, show):
  138. self._show_travel_moves = show
  139. self.currentLayerNumChanged.emit()
  140. def getShowTravelMoves(self):
  141. return self._show_travel_moves
  142. def setShowSupport(self, show):
  143. self._show_support = show
  144. self.currentLayerNumChanged.emit()
  145. def getShowSupport(self):
  146. return self._show_support
  147. def setShowAdhesion(self, show):
  148. self._show_adhesion = show
  149. self.currentLayerNumChanged.emit()
  150. def getShowAdhesion(self):
  151. return self._show_adhesion
  152. def setShowSkin(self, show):
  153. self._show_skin = show
  154. self.currentLayerNumChanged.emit()
  155. def getShowSkin(self):
  156. return self._show_skin
  157. def setShowInfill(self, show):
  158. self._show_infill = show
  159. self.currentLayerNumChanged.emit()
  160. def getShowInfill(self):
  161. return self._show_infill
  162. def getCompatibilityMode(self):
  163. return self._compatibility_mode
  164. def getExtruderCount(self):
  165. return self._extruder_count
  166. def calculateMaxLayers(self):
  167. scene = self.getController().getScene()
  168. self._activity = True
  169. self._old_max_layers = self._max_layers
  170. ## Recalculate num max layers
  171. new_max_layers = 0
  172. for node in DepthFirstIterator(scene.getRoot()):
  173. layer_data = node.callDecoration("getLayerData")
  174. if not layer_data:
  175. continue
  176. if new_max_layers < len(layer_data.getLayers()):
  177. new_max_layers = len(layer_data.getLayers()) - 1
  178. if new_max_layers > 0 and new_max_layers != self._old_max_layers:
  179. self._max_layers = new_max_layers
  180. # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
  181. # if it's the largest value. If we don't do this, we can have a slider block outside of the
  182. # slider.
  183. if new_max_layers > self._current_layer_num:
  184. self.maxLayersChanged.emit()
  185. self.setLayer(int(self._max_layers))
  186. else:
  187. self.setLayer(int(self._max_layers))
  188. self.maxLayersChanged.emit()
  189. self._startUpdateTopLayers()
  190. maxLayersChanged = Signal()
  191. currentLayerNumChanged = Signal()
  192. globalStackChanged = Signal()
  193. ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
  194. # as this caused some issues.
  195. def getProxy(self, engine, script_engine):
  196. return self._proxy
  197. def endRendering(self):
  198. pass
  199. def event(self, event):
  200. modifiers = QApplication.keyboardModifiers()
  201. ctrl_is_active = modifiers == Qt.ControlModifier
  202. if event.type == Event.KeyPressEvent and ctrl_is_active:
  203. if event.key == KeyEvent.UpKey:
  204. self.setLayer(self._current_layer_num + 1)
  205. return True
  206. if event.key == KeyEvent.DownKey:
  207. self.setLayer(self._current_layer_num - 1)
  208. return True
  209. if event.type == Event.ViewActivateEvent:
  210. # Make sure the LayerPass is created
  211. self.getLayerPass()
  212. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
  213. self._onGlobalStackChanged()
  214. if not self._layerview_composite_shader:
  215. self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader"))
  216. theme = Application.getInstance().getTheme()
  217. self._layerview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
  218. self._layerview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
  219. if not self._composite_pass:
  220. self._composite_pass = self.getRenderer().getRenderPass("composite")
  221. self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
  222. self._composite_pass.getLayerBindings().append("layerview")
  223. self._old_composite_shader = self._composite_pass.getCompositeShader()
  224. self._composite_pass.setCompositeShader(self._layerview_composite_shader)
  225. Application.getInstance().setViewLegendItems(self._getLegendItems())
  226. elif event.type == Event.ViewDeactivateEvent:
  227. self._wireprint_warning_message.hide()
  228. Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
  229. if self._global_container_stack:
  230. self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
  231. self._composite_pass.setLayerBindings(self._old_layer_bindings)
  232. self._composite_pass.setCompositeShader(self._old_composite_shader)
  233. Application.getInstance().setViewLegendItems([])
  234. def _onGlobalStackChanged(self):
  235. if self._global_container_stack:
  236. self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
  237. self._global_container_stack = Application.getInstance().getGlobalContainerStack()
  238. if self._global_container_stack:
  239. self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
  240. self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
  241. self._onPropertyChanged("wireframe_enabled", "value")
  242. self.globalStackChanged.emit()
  243. else:
  244. self._wireprint_warning_message.hide()
  245. def _onPropertyChanged(self, key, property_name):
  246. if key == "wireframe_enabled" and property_name == "value":
  247. if self._global_container_stack.getProperty("wireframe_enabled", "value"):
  248. self._wireprint_warning_message.show()
  249. else:
  250. self._wireprint_warning_message.hide()
  251. def _startUpdateTopLayers(self):
  252. if not self._compatibility_mode:
  253. return
  254. if self._top_layers_job:
  255. self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
  256. self._top_layers_job.cancel()
  257. self.setBusy(True)
  258. self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
  259. self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
  260. self._top_layers_job.start()
  261. def _updateCurrentLayerMesh(self, job):
  262. self.setBusy(False)
  263. if not job.getResult():
  264. return
  265. self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
  266. self._current_layer_mesh = job.getResult().get("layers")
  267. self._current_layer_jumps = job.getResult().get("jumps")
  268. self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
  269. self._top_layers_job = None
  270. def _onPreferencesChanged(self, preference):
  271. if preference not in {"view/top_layer_count", "view/only_show_top_layers", "view/compatibility_mode"}:
  272. return
  273. self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
  274. self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
  275. self._startUpdateTopLayers()
  276. def _getLegendItems(self):
  277. if self._legend_items is None:
  278. theme = Application.getInstance().getTheme()
  279. self._legend_items = [
  280. {"color": theme.getColor("layerview_inset_0").name(), "title": catalog.i18nc("@label:layerview polygon type", "Outer Wall")}, # Inset0Type
  281. {"color": theme.getColor("layerview_inset_x").name(), "title": catalog.i18nc("@label:layerview polygon type", "Inner Wall")}, # InsetXType
  282. {"color": theme.getColor("layerview_skin").name(), "title": catalog.i18nc("@label:layerview polygon type", "Top / Bottom")}, # SkinType
  283. {"color": theme.getColor("layerview_infill").name(), "title": catalog.i18nc("@label:layerview polygon type", "Infill")}, # InfillType
  284. {"color": theme.getColor("layerview_support").name(), "title": catalog.i18nc("@label:layerview polygon type", "Support Skin")}, # SupportType
  285. {"color": theme.getColor("layerview_support_infill").name(), "title": catalog.i18nc("@label:layerview polygon type", "Support Infill")}, # SupportInfillType
  286. {"color": theme.getColor("layerview_support_interface").name(), "title": catalog.i18nc("@label:layerview polygon type", "Support Interface")}, # SupportInterfaceType
  287. {"color": theme.getColor("layerview_skirt").name(), "title": catalog.i18nc("@label:layerview polygon type", "Build Plate Adhesion")}, # SkirtType
  288. {"color": theme.getColor("layerview_move_combing").name(), "title": catalog.i18nc("@label:layerview polygon type", "Travel Move")}, # MoveCombingType
  289. {"color": theme.getColor("layerview_move_retraction").name(), "title": catalog.i18nc("@label:layerview polygon type", "Retraction Move")}, # MoveRetractionType
  290. #{"color": theme.getColor("layerview_none").name(), "title": catalog.i18nc("@label:layerview polygon type", "Unknown")} # NoneType
  291. ]
  292. return self._legend_items
  293. class _CreateTopLayersJob(Job):
  294. def __init__(self, scene, layer_number, solid_layers):
  295. super().__init__()
  296. self._scene = scene
  297. self._layer_number = layer_number
  298. self._solid_layers = solid_layers
  299. self._cancel = False
  300. def run(self):
  301. layer_data = None
  302. for node in DepthFirstIterator(self._scene.getRoot()):
  303. layer_data = node.callDecoration("getLayerData")
  304. if layer_data:
  305. break
  306. if self._cancel or not layer_data:
  307. return
  308. layer_mesh = MeshBuilder()
  309. for i in range(self._solid_layers):
  310. layer_number = self._layer_number - i
  311. if layer_number < 0:
  312. continue
  313. try:
  314. layer = layer_data.getLayer(layer_number).createMesh()
  315. except Exception:
  316. Logger.logException("w", "An exception occurred while creating layer mesh.")
  317. return
  318. if not layer or layer.getVertices() is None:
  319. continue
  320. layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices())
  321. layer_mesh.addVertices(layer.getVertices())
  322. # Scale layer color by a brightness factor based on the current layer number
  323. # This will result in a range of 0.5 - 1.0 to multiply colors by.
  324. brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0
  325. brightness[0, 3] = 1.0
  326. layer_mesh.addColors(layer.getColors() * brightness)
  327. if self._cancel:
  328. return
  329. Job.yieldThread()
  330. if self._cancel:
  331. return
  332. Job.yieldThread()
  333. jump_mesh = layer_data.getLayer(self._layer_number).createJumps()
  334. if not jump_mesh or jump_mesh.getVertices() is None:
  335. jump_mesh = None
  336. self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
  337. def cancel(self):
  338. self._cancel = True
  339. super().cancel()