LayerPolygon.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import math
  4. import numpy
  5. from typing import Optional, cast
  6. from UM.Qt.Bindings.Theme import Theme
  7. from UM.Qt.QtApplication import QtApplication
  8. from UM.Logger import Logger
  9. class LayerPolygon:
  10. NoneType = 0
  11. Inset0Type = 1
  12. InsetXType = 2
  13. SkinType = 3
  14. SupportType = 4
  15. SkirtType = 5
  16. InfillType = 6
  17. SupportInfillType = 7
  18. MoveCombingType = 8
  19. MoveRetractionType = 9
  20. SupportInterfaceType = 10
  21. PrimeTowerType = 11
  22. MoveUnretractionType = 12
  23. __number_of_types = 13
  24. __jump_map = numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType)
  25. __jump_map = numpy.logical_or(__jump_map, numpy.arange(__number_of_types) == MoveRetractionType)
  26. __jump_map = numpy.logical_or(__jump_map, numpy.arange(__number_of_types) == MoveUnretractionType)
  27. def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray,
  28. line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
  29. """LayerPolygon, used in ProcessSlicedLayersJob
  30. :param extruder: The position of the extruder
  31. :param line_types: array with line_types
  32. :param data: new_points
  33. :param line_widths: array with line widths
  34. :param line_thicknesses: array with type as index and thickness as value
  35. :param line_feedrates: array with line feedrates
  36. """
  37. self._extruder = extruder
  38. self._types = line_types
  39. unknown_types = numpy.where(self._types >= self.__number_of_types, self._types, None)
  40. if unknown_types.any():
  41. # Got faulty line data from the engine.
  42. for idx in unknown_types:
  43. Logger.warning(f"Found an unknown line type at: {idx}")
  44. self._types[idx] = self.NoneType
  45. self._data = data
  46. self._line_widths = line_widths
  47. self._line_thicknesses = line_thicknesses
  48. self._line_feedrates = line_feedrates
  49. self._vertex_begin = 0
  50. self._vertex_end = 0
  51. self._index_begin = 0
  52. self._index_end = 0
  53. self._jump_mask = self.__jump_map[self._types]
  54. self._jump_count = numpy.sum(self._jump_mask)
  55. self._mesh_line_count = len(self._types) - self._jump_count
  56. self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
  57. # Buffering the colors shouldn't be necessary as it is not
  58. # re-used and can save a lot of memory usage.
  59. self._color_map = LayerPolygon.getColorMap()
  60. self._colors: numpy.ndarray = self._color_map[self._types]
  61. # When type is used as index returns true if type == LayerPolygon.InfillType
  62. # or type == LayerPolygon.SkinType
  63. # or type == LayerPolygon.SupportInfillType
  64. # Should be generated in better way, not hardcoded.
  65. self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
  66. self._build_cache_line_mesh_mask: Optional[numpy.ndarray] = None
  67. self._build_cache_needed_points: Optional[numpy.ndarray] = None
  68. def buildCache(self) -> None:
  69. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
  70. self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
  71. self._index_begin = 0
  72. self._index_end = cast(int, numpy.sum(self._build_cache_line_mesh_mask))
  73. self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = bool)
  74. # Only if the type of line segment changes do we need to add an extra vertex to change colors
  75. self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
  76. # Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
  77. numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points)
  78. self._vertex_begin = 0
  79. self._vertex_end = cast(int, numpy.sum(self._build_cache_needed_points))
  80. def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray,
  81. colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray,
  82. extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
  83. """Set all the arrays provided by the function caller, representing the LayerPolygon
  84. The arrays are either by vertex or by indices.
  85. :param vertex_offset: determines where to start and end filling the arrays
  86. :param index_offset: determines where to start and end filling the arrays
  87. :param vertices: vertex numpy array to be filled
  88. :param colors: vertex numpy array to be filled
  89. :param line_dimensions: vertex numpy array to be filled
  90. :param feedrates: vertex numpy array to be filled
  91. :param extruders: vertex numpy array to be filled
  92. :param line_types: vertex numpy array to be filled
  93. :param indices: index numpy array to be filled
  94. """
  95. if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
  96. self.buildCache()
  97. if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
  98. Logger.log("w", "Failed to build cache for layer polygon")
  99. return
  100. line_mesh_mask = self._build_cache_line_mesh_mask
  101. needed_points_list = self._build_cache_needed_points
  102. # Index to the points we need to represent the line mesh.
  103. # This is constructed by generating simple start and end points for each line.
  104. # For line segment n, these are points n and n+1. Row n reads [n n+1]
  105. # Then the indices for the points we don't need are thrown away based on the pre-calculated list.
  106. index_list = (numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]])).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
  107. # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
  108. self._vertex_begin += vertex_offset
  109. self._vertex_end += vertex_offset
  110. # Points are picked based on the index list to get the vertices needed.
  111. vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
  112. # Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
  113. colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
  114. # Create an array with line widths and thicknesses for each vertex.
  115. line_dimensions[self._vertex_begin:self._vertex_end, 0] = numpy.tile(self._line_widths, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
  116. line_dimensions[self._vertex_begin:self._vertex_end, 1] = numpy.tile(self._line_thicknesses, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
  117. # Create an array with feedrates for each line
  118. feedrates[self._vertex_begin:self._vertex_end] = numpy.tile(self._line_feedrates, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
  119. extruders[self._vertex_begin:self._vertex_end] = self._extruder
  120. # Convert type per vertex to type per line
  121. line_types[self._vertex_begin:self._vertex_end] = numpy.tile(self._types, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
  122. # The relative values of begin and end indices have already been set in buildCache,
  123. # so we only need to offset them to the parents offset.
  124. self._index_begin += index_offset
  125. self._index_end += index_offset
  126. indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
  127. # When the line type changes the index needs to be increased by 2.
  128. indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
  129. # Each line segment goes from it's starting point p to p+1, offset by the vertex index.
  130. # The -1 is to compensate for the necessarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
  131. indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
  132. self._build_cache_line_mesh_mask = None
  133. self._build_cache_needed_points = None
  134. def getColors(self):
  135. return self._colors
  136. def mapLineTypeToColor(self, line_types: numpy.ndarray) -> numpy.ndarray:
  137. return self._color_map[line_types]
  138. def isInfillOrSkinType(self, line_types: numpy.ndarray) -> numpy.ndarray:
  139. return self._is_infill_or_skin_type_map[line_types]
  140. def lineMeshVertexCount(self) -> int:
  141. return self._vertex_end - self._vertex_begin
  142. def lineMeshElementCount(self) -> int:
  143. return self._index_end - self._index_begin
  144. @property
  145. def extruder(self):
  146. return self._extruder
  147. @property
  148. def types(self):
  149. return self._types
  150. @property
  151. def lineLengths(self):
  152. data_array = numpy.array(self._data)
  153. return numpy.linalg.norm(data_array[1:] - data_array[:-1], axis=1)
  154. @property
  155. def data(self):
  156. return self._data
  157. @property
  158. def elementCount(self):
  159. return (self._index_end - self._index_begin) * 2 # The range of vertices multiplied by 2 since each vertex is used twice
  160. @property
  161. def lineWidths(self):
  162. return self._line_widths
  163. @property
  164. def lineThicknesses(self):
  165. return self._line_thicknesses
  166. @property
  167. def lineFeedrates(self):
  168. return self._line_feedrates
  169. @property
  170. def jumpMask(self):
  171. return self._jump_mask
  172. @property
  173. def meshLineCount(self):
  174. return self._mesh_line_count
  175. @property
  176. def jumpCount(self):
  177. return self._jump_count
  178. def getNormals(self) -> numpy.ndarray:
  179. """Calculate normals for the entire polygon using numpy.
  180. :return: normals for the entire polygon
  181. """
  182. normals = numpy.copy(self._data)
  183. normals[:, 1] = 0.0 # We are only interested in 2D normals
  184. # Calculate the edges between points.
  185. # The call to numpy.roll shifts the entire array by one
  186. # so that we end up subtracting each next point from the current, wrapping around.
  187. # This gives us the edges from the next point to the current point.
  188. normals = numpy.diff(normals, 1, 0)
  189. # Calculate the length of each edge using standard Pythagoras
  190. lengths = numpy.sqrt(normals[:, 0] ** 2 + normals[:, 2] ** 2)
  191. # The normal of a 2D vector is equal to its x and y coordinates swapped
  192. # and then x inverted. This code does that.
  193. normals[:, [0, 2]] = normals[:, [2, 0]]
  194. normals[:, 0] *= -1
  195. # Normalize the normals.
  196. normals[:, 0] /= lengths
  197. normals[:, 2] /= lengths
  198. return normals
  199. __color_map = None # type: numpy.ndarray
  200. @classmethod
  201. def getColorMap(cls) -> numpy.ndarray:
  202. """Gets the instance of the VersionUpgradeManager, or creates one."""
  203. if cls.__color_map is None:
  204. theme = cast(Theme, QtApplication.getInstance().getTheme())
  205. cls.__color_map = numpy.array([
  206. theme.getColor("layerview_none").getRgbF(), # NoneType
  207. theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
  208. theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
  209. theme.getColor("layerview_skin").getRgbF(), # SkinType
  210. theme.getColor("layerview_support").getRgbF(), # SupportType
  211. theme.getColor("layerview_skirt").getRgbF(), # SkirtType
  212. theme.getColor("layerview_infill").getRgbF(), # InfillType
  213. theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
  214. theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
  215. theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
  216. theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
  217. theme.getColor("layerview_prime_tower").getRgbF(), # PrimeTowerType
  218. theme.getColor("layerview_move_unretraction").getRgbF(), # MoveUnretractionType
  219. ])
  220. return cls.__color_map