LayerView.py 18 KB

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