StructureView.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt5.QtCore import QTimer # To refresh not too often.
  4. from typing import Optional, TYPE_CHECKING
  5. import numpy
  6. from cura.CuraApplication import CuraApplication
  7. from cura.CuraView import CuraView
  8. from UM.Mesh.MeshData import MeshData
  9. from UM.PluginRegistry import PluginRegistry
  10. from .StructureNode import StructureNode
  11. if TYPE_CHECKING:
  12. import pyArcus
  13. class StructureView(CuraView):
  14. """
  15. View the structure types.
  16. """
  17. _color_map = {
  18. 0: "layerview_inset_x",
  19. 1: "layerview_skin",
  20. 2: "layerview_infill",
  21. 3: "layerview_support",
  22. 4: "layerview_ghost",
  23. 5: "layerview_skirt"
  24. }
  25. """
  26. Mapping which color theme entry to display for each type of structure.
  27. The numbers here should match the structure types in Cura.proto under StructurePolygon.Type
  28. """
  29. _refresh_cooldown = 0.1 # In seconds, minimum time between refreshes of the rendered mesh.
  30. def __init__(self):
  31. super().__init__(parent = None, use_empty_menu_placeholder = True)
  32. self._scene_node = None # type: Optional[StructureNode] # All structure data will be under this node. Will be generated on first message received (since there is no scene yet at init).
  33. self._capacity = 3 * 1000000 # Start with some allocation to prevent having to reallocate all the time. Preferably a multiple of 3 (for triangles).
  34. self._vertices = numpy.ndarray((self._capacity, 3), dtype = numpy.single)
  35. self._indices = numpy.arange(self._capacity, dtype = numpy.int32).reshape((int(self._capacity / 3), 3)) # Since we're using a triangle list, the indices are simply increasing linearly.
  36. self._normals = numpy.repeat(numpy.array([[0.0, 1.0, 0.0]], dtype = numpy.single), self._capacity, axis = 0) # All normals are pointing up (to positive Y).
  37. self._colors = numpy.repeat(numpy.array([[0.0, 0.0, 0.0, 1.0]], dtype = numpy.single), self._capacity, axis = 0) # No colors yet.
  38. self._layers = numpy.repeat(-1, self._capacity) # To mask out certain layers for layer view.
  39. self._current_index = 0 # type: int # Where to add new data.
  40. self._refresh_timer = QTimer()
  41. self._refresh_timer.setInterval(1000 * self._refresh_cooldown)
  42. self._refresh_timer.setSingleShot(True)
  43. self._refresh_timer.timeout.connect(self._updateScene)
  44. plugin_registry = PluginRegistry.getInstance()
  45. self._enabled = "StructureView" not in plugin_registry.getDisabledPlugins() # Don't influence performance if this plug-in is disabled.
  46. if self._enabled:
  47. engine = plugin_registry.getPluginObject("CuraEngineBackend")
  48. engine.structurePolygonReceived.connect(self._onStructurePolygonReceived) # type: ignore
  49. CuraApplication.getInstance().initializationFinished.connect(self._createSceneNode)
  50. def _createSceneNode(self):
  51. scene = CuraApplication.getInstance().getController().getScene()
  52. if not self._scene_node:
  53. self._scene_node = StructureNode(parent = scene.getRoot())
  54. scene.sceneChanged.connect(self._clear)
  55. def _onStructurePolygonReceived(self, message: "pyArcus.PythonMessage") -> None:
  56. """
  57. Store the structure polygon in the scene node's mesh data when we receive one.
  58. :param message: A message received from CuraEngine containing a structure polygon.
  59. """
  60. num_vertices = int(len(message.points) / 4 / 3) # Number of bytes, / 4 for bytes per float, / 3 for X, Y and Z coordinates.
  61. to = self._current_index + num_vertices
  62. if to >= self._capacity:
  63. self._reallocate(self._current_index + num_vertices)
  64. # Fill the existing buffers with data in our region.
  65. vertex_data = numpy.frombuffer(message.points, dtype = numpy.single)
  66. vertex_data = numpy.reshape(vertex_data, newshape = (num_vertices, 3))
  67. self._vertices[self._current_index:to] = vertex_data
  68. self._colors[self._current_index:to] = CuraApplication.getInstance().getTheme().getColor(self._color_map.get(message.type, "layerview_none")).getRgbF()
  69. self._layers[self._current_index:to] = message.layer_index
  70. self._current_index += num_vertices
  71. if not self._refresh_timer.isActive(): # Don't refresh if the previous refresh was too recent.
  72. self._refresh_timer.start()
  73. def _reallocate(self, minimum_capacity: int) -> None:
  74. """
  75. Increase capacity to be able to hold at least a given amount of vertices.
  76. """
  77. new_capacity = self._capacity
  78. while minimum_capacity > new_capacity:
  79. new_capacity *= 2
  80. self._vertices.resize((new_capacity, 3))
  81. self._indices = numpy.arange(new_capacity, dtype = numpy.int32).reshape((int(new_capacity / 3), 3))
  82. self._normals = numpy.repeat(numpy.array([[0.0, 1.0, 0.0]], dtype = numpy.single), new_capacity, axis = 0)
  83. self._colors.resize((new_capacity, 4))
  84. self._layers.resize((new_capacity, ))
  85. self._capacity = new_capacity
  86. def _updateScene(self) -> None:
  87. """
  88. After receiving new data, makes sure that the data gets visualised in the 3D scene.
  89. """
  90. if not self._scene_node:
  91. return
  92. self._scene_node.setMeshData(MeshData(
  93. vertices = self._vertices[0:self._current_index],
  94. normals = self._normals[0:self._current_index],
  95. indices = self._indices[0:int(self._current_index / 3)],
  96. colors = self._colors[0:self._current_index]
  97. ))
  98. def _clear(self, source: "SceneNode") -> None:
  99. """
  100. Removes the structure data from the scene when the scene changes.
  101. :param source: The scene node that changed.
  102. """
  103. scene = CuraApplication.getInstance().getController().getScene()
  104. if not source.callDecoration("isSliceable") and source != scene.getRoot():
  105. return
  106. if source.callDecoration("getBuildPlateNumber") is None: # Not on the build plate.
  107. return
  108. if not source.callDecoration("isGroup"):
  109. mesh_data = source.getMeshData()
  110. if mesh_data is None or mesh_data.getVertices() is None:
  111. return
  112. if self._current_index != 0:
  113. self._current_index = 0
  114. self._updateScene()