SimulationPass.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import math
  4. from UM.Math.Color import Color
  5. from UM.Math.Vector import Vector
  6. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  7. from UM.Resources import Resources
  8. from UM.Scene.SceneNode import SceneNode
  9. from UM.Scene.ToolHandle import ToolHandle
  10. from UM.Application import Application
  11. from UM.PluginRegistry import PluginRegistry
  12. from UM.View.RenderPass import RenderPass
  13. from UM.View.RenderBatch import RenderBatch
  14. from UM.View.GL.OpenGL import OpenGL
  15. from cura.Settings.ExtruderManager import ExtruderManager
  16. from cura.LayerPolygon import LayerPolygon
  17. import os.path
  18. import numpy
  19. ## RenderPass used to display g-code paths.
  20. from .NozzleNode import NozzleNode
  21. class SimulationPass(RenderPass):
  22. def __init__(self, width, height):
  23. super().__init__("simulationview", width, height)
  24. self._layer_shader = None
  25. self._layer_shadow_shader = None
  26. self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
  27. self._tool_handle_shader = None
  28. self._nozzle_shader = None
  29. self._disabled_shader = None
  30. self._old_current_layer = 0
  31. self._old_current_path: float = 0.0
  32. self._switching_layers = True # Tracking whether the user is moving across layers (True) or across paths (False). If false, lower layers render as shadowy.
  33. self._gl = OpenGL.getInstance().getBindingsObject()
  34. self._scene = Application.getInstance().getController().getScene()
  35. self._extruder_manager = ExtruderManager.getInstance()
  36. self._layer_view = None
  37. self._compatibility_mode = None
  38. self._scene.sceneChanged.connect(self._onSceneChanged)
  39. def setSimulationView(self, layerview):
  40. self._layer_view = layerview
  41. self._compatibility_mode = layerview.getCompatibilityMode()
  42. def render(self):
  43. if not self._layer_shader:
  44. if self._compatibility_mode:
  45. shader_filename = "layers.shader"
  46. shadow_shader_filename = "layers_shadow.shader"
  47. else:
  48. shader_filename = "layers3d.shader"
  49. shadow_shader_filename = "layers3d_shadow.shader"
  50. self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
  51. self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
  52. self._current_shader = self._layer_shader
  53. # Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
  54. self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
  55. if not self._compatibility_mode:
  56. self._layer_shader.setUniformValue("u_starts_color", Color(*Application.getInstance().getTheme().getColor("layerview_starts").getRgb()))
  57. if self._layer_view:
  58. self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
  59. self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
  60. self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness())
  61. self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
  62. self._layer_shader.setUniformValue("u_max_line_width", self._layer_view.getMaxLineWidth())
  63. self._layer_shader.setUniformValue("u_min_line_width", self._layer_view.getMinLineWidth())
  64. self._layer_shader.setUniformValue("u_max_flow_rate", self._layer_view.getMaxFlowRate())
  65. self._layer_shader.setUniformValue("u_min_flow_rate", self._layer_view.getMinFlowRate())
  66. self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
  67. self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
  68. self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
  69. self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
  70. self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
  71. self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
  72. self._layer_shader.setUniformValue("u_show_starts", self._layer_view.getShowStarts())
  73. else:
  74. #defaults
  75. self._layer_shader.setUniformValue("u_max_feedrate", 1)
  76. self._layer_shader.setUniformValue("u_min_feedrate", 0)
  77. self._layer_shader.setUniformValue("u_max_thickness", 1)
  78. self._layer_shader.setUniformValue("u_min_thickness", 0)
  79. self._layer_shader.setUniformValue("u_max_flow_rate", 1)
  80. self._layer_shader.setUniformValue("u_min_flow_rate", 0)
  81. self._layer_shader.setUniformValue("u_max_line_width", 1)
  82. self._layer_shader.setUniformValue("u_min_line_width", 0)
  83. self._layer_shader.setUniformValue("u_layer_view_type", 1)
  84. self._layer_shader.setUniformValue("u_extruder_opacity", [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
  85. self._layer_shader.setUniformValue("u_show_travel_moves", 0)
  86. self._layer_shader.setUniformValue("u_show_helpers", 1)
  87. self._layer_shader.setUniformValue("u_show_skin", 1)
  88. self._layer_shader.setUniformValue("u_show_infill", 1)
  89. self._layer_shader.setUniformValue("u_show_starts", 1)
  90. if not self._tool_handle_shader:
  91. self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
  92. if not self._nozzle_shader:
  93. self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
  94. self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
  95. if not self._disabled_shader:
  96. self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
  97. self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*Application.getInstance().getTheme().getColor("model_unslicable").getRgb()))
  98. self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*Application.getInstance().getTheme().getColor("model_unslicable_alt").getRgb()))
  99. self._disabled_shader.setUniformValue("u_width", 50.0)
  100. self._disabled_shader.setUniformValue("u_opacity", 0.6)
  101. self.bind()
  102. tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
  103. disabled_batch = RenderBatch(self._disabled_shader)
  104. head_position = None # Indicates the current position of the print head
  105. nozzle_node = None
  106. not_a_vector = Vector(math.nan, math.nan, math.nan)
  107. for node in DepthFirstIterator(self._scene.getRoot()):
  108. if isinstance(node, ToolHandle):
  109. tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
  110. elif isinstance(node, NozzleNode):
  111. nozzle_node = node
  112. nozzle_node.setVisible(False) # Don't set to true, we render it separately!
  113. elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"):
  114. disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData())
  115. elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
  116. layer_data = node.callDecoration("getLayerData")
  117. if not layer_data:
  118. continue
  119. # Render all layers below a certain number as line mesh instead of vertices.
  120. if self._layer_view.getCurrentLayer() > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
  121. start = 0
  122. end = 0
  123. vertex_before_head = not_a_vector
  124. vertex_after_head = not_a_vector
  125. vertex_distance_ratio = 0.0
  126. towards_next_vertex = 0
  127. element_counts = layer_data.getElementCounts()
  128. for layer in sorted(element_counts.keys()):
  129. # In the current layer, we show just the indicated paths
  130. if layer == self._layer_view._current_layer_num:
  131. # We look for the position of the head, searching the point of the current path
  132. index = int(self._layer_view.getCurrentPath()) if not math.isnan(
  133. self._layer_view.getCurrentPath()) else 0
  134. for polygon in layer_data.getLayer(layer).polygons:
  135. # The size indicates all values in the two-dimension array, and the second dimension is
  136. # always size 3 because we have 3D points.
  137. if index >= polygon.data.size // 3 :
  138. index -= polygon.data.size // 3
  139. continue
  140. # The head position is calculated and translated
  141. ratio = self._layer_view.getCurrentPath() - math.floor(self._layer_view.getCurrentPath())
  142. pos_a = Vector(polygon.data[index][0], polygon.data[index][1],
  143. polygon.data[index][2])
  144. vertex_before_head = pos_a
  145. vertex_distance_ratio = ratio
  146. if ratio <= 0.0001 or index + 1 == len(polygon.data):
  147. # in case there multiple polygons and polygon changes, the first point has the same value as the last point in the previous polygon
  148. head_position = pos_a + node.getWorldPosition()
  149. else:
  150. pos_b = Vector(polygon.data[index + 1][0],
  151. polygon.data[index + 1][1],
  152. polygon.data[index + 1][2])
  153. vec = pos_a * (1.0 - ratio) + pos_b * ratio
  154. head_position = vec + node.getWorldPosition()
  155. vertex_after_head = pos_b
  156. towards_next_vertex = 2 # Add two to the index to print the current and next vertices as an 'unfinished' line (to the nozzle).
  157. break
  158. break
  159. if self._layer_view.getMinimumLayer() > layer:
  160. start += element_counts[layer]
  161. end += element_counts[layer]
  162. # Calculate the range of paths in the last layer
  163. current_layer_start = end
  164. current_layer_end = end + int( self._layer_view.getCurrentPath()) * 2 # Because each point is used twice
  165. # This uses glDrawRangeElements internally to only draw a certain range of lines.
  166. # All the layers but the current selected layer are rendered first
  167. if self._old_current_path != self._layer_view.getCurrentPath():
  168. self._current_shader = self._layer_shadow_shader
  169. self._switching_layers = False
  170. if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view.getCurrentLayer():
  171. self._current_shader = self._layer_shader
  172. self._switching_layers = True
  173. # reset 'last vertex'
  174. self._layer_shader.setUniformValue("u_last_vertex", not_a_vector)
  175. self._layer_shader.setUniformValue("u_next_vertex", not_a_vector)
  176. self._layer_shader.setUniformValue("u_last_line_ratio", 1.0)
  177. # The first line does not have a previous line: add a MoveCombingType in front for start detection
  178. # this way the first start of the layer can also be drawn
  179. prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
  180. # Remove the last element
  181. prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size]
  182. layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'}
  183. layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end), backface_cull = True)
  184. layers_batch.addItem(node.getWorldTransformation(), layer_data)
  185. layers_batch.render(self._scene.getActiveCamera())
  186. # Current selected layer is rendered
  187. current_layer_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (current_layer_start, current_layer_end))
  188. current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
  189. current_layer_batch.render(self._scene.getActiveCamera())
  190. # Last line may be partial
  191. if vertex_after_head != not_a_vector and vertex_after_head != not_a_vector:
  192. self._layer_shader.setUniformValue("u_last_vertex", vertex_before_head)
  193. self._layer_shader.setUniformValue("u_next_vertex", vertex_after_head)
  194. self._layer_shader.setUniformValue("u_last_line_ratio", vertex_distance_ratio)
  195. last_line_start = current_layer_end
  196. last_line_end = current_layer_end + towards_next_vertex
  197. last_line_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode=RenderBatch.RenderMode.Lines, range = (last_line_start, last_line_end))
  198. last_line_batch.addItem(node.getWorldTransformation(), layer_data)
  199. last_line_batch.render(self._scene.getActiveCamera())
  200. self._old_current_layer = self._layer_view.getCurrentLayer()
  201. self._old_current_path = self._layer_view.getCurrentPath()
  202. # Create a new batch that is not range-limited
  203. batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
  204. if self._layer_view.getCurrentLayerMesh():
  205. batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
  206. if self._layer_view.getCurrentLayerJumps():
  207. batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
  208. if len(batch.items) > 0:
  209. batch.render(self._scene.getActiveCamera())
  210. # The nozzle is drawn when once we know the correct position of the head,
  211. # but the user is not using the layer slider, and the compatibility mode is not enabled
  212. if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
  213. if head_position is not None:
  214. nozzle_node.setPosition(head_position)
  215. nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
  216. nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
  217. nozzle_batch.render(self._scene.getActiveCamera())
  218. if len(disabled_batch.items) > 0:
  219. disabled_batch.render(self._scene.getActiveCamera())
  220. # Render toolhandles on top of the layerview
  221. if len(tool_handle_batch.items) > 0:
  222. tool_handle_batch.render(self._scene.getActiveCamera())
  223. self.release()
  224. def _onSceneChanged(self, changed_object: SceneNode):
  225. if changed_object.callDecoration("getLayerData"): # Any layer data has changed.
  226. self._switching_layers = True
  227. self._old_current_layer = 0
  228. self._old_current_path = 0.0