MakerbotWriter.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. # Copyright (c) 2023 UltiMaker
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from io import StringIO, BufferedIOBase
  4. import json
  5. from typing import cast, List, Optional, Dict
  6. from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED
  7. from PyQt6.QtCore import QBuffer
  8. from UM.Logger import Logger
  9. from UM.Math.AxisAlignedBox import AxisAlignedBox
  10. from UM.Mesh.MeshWriter import MeshWriter
  11. from UM.PluginRegistry import PluginRegistry
  12. from UM.Scene.SceneNode import SceneNode
  13. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  14. from UM.i18n import i18nCatalog
  15. from cura.CuraApplication import CuraApplication
  16. from cura.Snapshot import Snapshot
  17. from cura.Utils.Threading import call_on_qt_thread
  18. from cura.CuraVersion import ConanInstalls
  19. catalog = i18nCatalog("cura")
  20. class MakerbotWriter(MeshWriter):
  21. """A file writer that writes '.makerbot' files."""
  22. def __init__(self) -> None:
  23. super().__init__(add_to_recent_files=False)
  24. _PNG_FORMATS = [
  25. {"prefix": "isometric_thumbnail", "width": 120, "height": 120},
  26. {"prefix": "isometric_thumbnail", "width": 320, "height": 320},
  27. {"prefix": "isometric_thumbnail", "width": 640, "height": 640},
  28. {"prefix": "thumbnail", "width": 140, "height": 106},
  29. {"prefix": "thumbnail", "width": 212, "height": 300},
  30. {"prefix": "thumbnail", "width": 960, "height": 1460},
  31. {"prefix": "thumbnail", "width": 90, "height": 90},
  32. ]
  33. _META_VERSION = "3.0.0"
  34. _PRINT_NAME_MAP = {
  35. "Makerbot Method": "fire_e",
  36. "Makerbot Method X": "lava_f",
  37. "Makerbot Method XL": "magma_10",
  38. }
  39. _EXTRUDER_NAME_MAP = {
  40. "1XA": "mk14_hot",
  41. "2XA": "mk14_hot_s",
  42. "1C": "mk14_c",
  43. "1A": "mk14",
  44. "2A": "mk14_s",
  45. }
  46. # must be called from the main thread because of OpenGL
  47. @staticmethod
  48. @call_on_qt_thread
  49. def _createThumbnail(width: int, height: int) -> Optional[QBuffer]:
  50. if not CuraApplication.getInstance().isVisible:
  51. Logger.warning("Can't create snapshot when renderer not initialized.")
  52. return
  53. try:
  54. snapshot = Snapshot.isometric_snapshot(width, height)
  55. except:
  56. Logger.logException("w", "Failed to create snapshot image")
  57. return
  58. thumbnail_buffer = QBuffer()
  59. thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
  60. snapshot.save(thumbnail_buffer, "PNG")
  61. return thumbnail_buffer
  62. def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
  63. if mode != MeshWriter.OutputMode.BinaryMode:
  64. Logger.log("e", "MakerbotWriter does not support text mode.")
  65. self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
  66. return False
  67. # The GCodeWriter plugin is bundled, so it must at least exist. (What happens if people disable that plugin?)
  68. gcode_writer = PluginRegistry.getInstance().getPluginObject("GCodeWriter")
  69. if gcode_writer is None:
  70. Logger.log("e", "Could not find the GCodeWriter plugin, is it disabled?.")
  71. self.setInformation(
  72. catalog.i18nc("@error:load", "Could not load GCodeWriter plugin. Try to re-enable the plugin."))
  73. return False
  74. gcode_writer = cast(MeshWriter, gcode_writer)
  75. gcode_text_io = StringIO()
  76. success = gcode_writer.write(gcode_text_io, None)
  77. # TODO convert gcode_text_io to json
  78. # Writing the g-code failed. Then I can also not write the gzipped g-code.
  79. if not success:
  80. self.setInformation(gcode_writer.getInformation())
  81. return False
  82. metadata = self._getMeta(nodes)
  83. png_files = []
  84. for png_format in self._PNG_FORMATS:
  85. width, height, prefix = png_format["width"], png_format["height"], png_format["prefix"]
  86. thumbnail_buffer = self._createThumbnail(width, height)
  87. if thumbnail_buffer is None:
  88. Logger.warning(f"Could not create thumbnail of size {width}x{height}.")
  89. continue
  90. png_files.append({
  91. "file": f"{prefix}_{width}x{height}.png",
  92. "data": thumbnail_buffer.data(),
  93. })
  94. try:
  95. with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
  96. zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
  97. for png_file in png_files:
  98. file, data = png_file["file"], png_file["data"]
  99. zip_stream.writestr(file, data)
  100. except (IOError, OSError, BadZipFile) as ex:
  101. Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
  102. self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
  103. return False
  104. return True
  105. def _getMeta(self, root_nodes: List[SceneNode]) -> Dict[str, any]:
  106. application = CuraApplication.getInstance()
  107. machine_manager = application.getMachineManager()
  108. global_stack = machine_manager.activeMachine
  109. extruders = global_stack.extruderList
  110. nodes = []
  111. for root_node in root_nodes:
  112. for node in DepthFirstIterator(root_node):
  113. if not getattr(node, "_outside_buildarea", False):
  114. if node.callDecoration(
  115. "isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
  116. "isNonThumbnailVisibleMesh"):
  117. nodes.append(node)
  118. meta = dict()
  119. meta["bot_type"] = MakerbotWriter._PRINT_NAME_MAP.get((name := global_stack.name), name)
  120. bounds: Optional[AxisAlignedBox] = None
  121. for node in nodes:
  122. node_bounds = node.getBoundingBox()
  123. if node_bounds is None:
  124. continue
  125. if bounds is None:
  126. bounds = node_bounds
  127. else:
  128. bounds = bounds + node_bounds
  129. if bounds is not None:
  130. meta["bounding_box"] = {
  131. "x_min": bounds.left,
  132. "x_max": bounds.right,
  133. "y_min": bounds.back,
  134. "y_max": bounds.front,
  135. "z_min": bounds.bottom,
  136. "z_max": bounds.top,
  137. }
  138. material_bed_temperature = global_stack.getProperty("material_bed_temperature", "value")
  139. meta["build_plane_temperature"] = material_bed_temperature
  140. print_information = application.getPrintInformation()
  141. meta["commanded_duration_s"] = print_information.currentPrintTime.seconds
  142. meta["duration_s"] = print_information.currentPrintTime.seconds
  143. material_lengths = list(map(meter_to_millimeter, print_information.materialLengths))
  144. meta["extrusion_distance_mm"] = material_lengths[0]
  145. meta["extrusion_distances_mm"] = material_lengths
  146. meta["extrusion_mass_g"] = print_information.materialWeights[0]
  147. meta["extrusion_masses_g"] = print_information.materialWeights
  148. meta["uuid"] = print_information.slice_uuid
  149. materials = [extruder.material.getMetaData().get("material") for extruder in extruders]
  150. meta["material"] = materials[0]
  151. meta["materials"] = materials
  152. materials_temps = [extruder.getProperty("default_material_print_temperature", "value") for extruder in
  153. extruders]
  154. meta["extruder_temperature"] = materials_temps[0]
  155. meta["extruder_temperatures"] = materials_temps
  156. meta["model_counts"] = [{"count": 1, "name": node.getName()} for node in nodes]
  157. tool_types = [MakerbotWriter._EXTRUDER_NAME_MAP.get((name := extruder.variant.getName()), name) for extruder in
  158. extruders]
  159. meta["tool_type"] = tool_types[0]
  160. meta["tool_types"] = tool_types
  161. meta["version"] = MakerbotWriter._META_VERSION
  162. meta["preferences"] = dict()
  163. for node in nodes:
  164. bound = node.getBoundingBox()
  165. meta["preferences"][str(node.getName())] = {
  166. "machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
  167. "printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
  168. }
  169. cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
  170. meta["curaengine_version"] = cura_engine_info["version"]
  171. meta["curaengine_commit_hash"] = cura_engine_info["revision"]
  172. meta["makerbot_writer_version"] = self.getVersion()
  173. # meta["makerbot_writer_commit_hash"] = self.getRevision()
  174. for name, package_info in ConanInstalls.items():
  175. if not name.startswith("curaengine_ "):
  176. continue
  177. meta[f"{name}_version"] = package_info["version"]
  178. meta[f"{name}_commit_hash"] = package_info["revision"]
  179. # TODO add the following instructions
  180. # num_tool_changes
  181. # num_z_layers
  182. # num_z_transitions
  183. # platform_temperature
  184. # total_commands
  185. return meta
  186. def meter_to_millimeter(value: float) -> float:
  187. """Converts a value in meters to millimeters."""
  188. return value * 1000.0