MakerbotWriter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. import pyDulcificum as du
  8. from PyQt6.QtCore import QBuffer
  9. from UM.Logger import Logger
  10. from UM.Math.AxisAlignedBox import AxisAlignedBox
  11. from UM.Mesh.MeshWriter import MeshWriter
  12. from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
  13. from UM.PluginRegistry import PluginRegistry
  14. from UM.Scene.SceneNode import SceneNode
  15. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  16. from UM.i18n import i18nCatalog
  17. from cura.CuraApplication import CuraApplication
  18. from cura.Snapshot import Snapshot
  19. from cura.Utils.Threading import call_on_qt_thread
  20. from cura.CuraVersion import ConanInstalls
  21. catalog = i18nCatalog("cura")
  22. class MakerbotWriter(MeshWriter):
  23. """A file writer that writes '.makerbot' files."""
  24. def __init__(self) -> None:
  25. super().__init__(add_to_recent_files=False)
  26. Logger.info(f"Using PyDulcificum: {du.__version__}")
  27. MimeTypeDatabase.addMimeType(
  28. MimeType(
  29. name="application/x-makerbot",
  30. comment="Makerbot Toolpath Package",
  31. suffixes=["makerbot"]
  32. )
  33. )
  34. _PNG_FORMATS = [
  35. {"prefix": "isometric_thumbnail", "width": 120, "height": 120},
  36. {"prefix": "isometric_thumbnail", "width": 320, "height": 320},
  37. {"prefix": "isometric_thumbnail", "width": 640, "height": 640},
  38. {"prefix": "thumbnail", "width": 140, "height": 106},
  39. {"prefix": "thumbnail", "width": 212, "height": 300},
  40. {"prefix": "thumbnail", "width": 960, "height": 1460},
  41. {"prefix": "thumbnail", "width": 90, "height": 90},
  42. ]
  43. _META_VERSION = "3.0.0"
  44. _PRINT_NAME_MAP = {
  45. "UltiMaker Method": "fire_e",
  46. "UltiMaker Method X": "lava_f",
  47. "UltiMaker Method XL": "magma_10",
  48. }
  49. _EXTRUDER_NAME_MAP = {
  50. "1XA": "mk14_hot",
  51. "2XA": "mk14_hot_s",
  52. "1C": "mk14_c",
  53. "1A": "mk14",
  54. "2A": "mk14_s",
  55. }
  56. _MATERIAL_MAP = {"2780b345-577b-4a24-a2c5-12e6aad3e690": "abs",
  57. "88c8919c-6a09-471a-b7b6-e801263d862d": "abs-wss1",
  58. "416eead4-0d8e-4f0b-8bfc-a91a519befa5": "asa",
  59. "85bbae0e-938d-46fb-989f-c9b3689dc4f0": "nylon-cf",
  60. "283d439a-3490-4481-920c-c51d8cdecf9c": "nylon",
  61. "62414577-94d1-490d-b1e4-7ef3ec40db02": "pc",
  62. "69386c85-5b6c-421a-bec5-aeb1fb33f060": "petg",
  63. "0ff92885-617b-4144-a03c-9989872454bc": "pla",
  64. "a4255da2-cb2a-4042-be49-4a83957a2f9a": "pva",
  65. "a140ef8f-4f26-4e73-abe0-cfc29d6d1024": "wss1",
  66. "77873465-83a9-4283-bc44-4e542b8eb3eb": "sr30",
  67. "96fca5d9-0371-4516-9e96-8e8182677f3c": "im-pla",
  68. "9f52c514-bb53-46a6-8c0c-d507cd6ee742": "abs",
  69. "0f9a2a91-f9d6-4b6b-bd9b-a120a29391be": "abs",
  70. "d3e972f2-68c0-4d2f-8cfd-91028dfc3381": "abs",
  71. "495a0ce5-9daf-4a16-b7b2-06856d82394d": "abs-cf10",
  72. "cb76bd6e-91fd-480c-a191-12301712ec77": "abs-wss1",
  73. "a017777e-3f37-4d89-a96c-dc71219aac77": "abs-wss1",
  74. "4d96000d-66de-4d54-a580-91827dcfd28f": "abs-wss1",
  75. "0ecb0e1a-6a66-49fb-b9ea-61a8924e0cf5": "asa",
  76. "efebc2ea-2381-4937-926f-e824524524a5": "asa",
  77. "b0199512-5714-4951-af85-be19693430f8": "asa",
  78. "b9f55a0a-a2b6-4b8d-8d48-07802c575bd1": "pla",
  79. "c439d884-9cdc-4296-a12c-1bacae01003f": "pla",
  80. "16a723e3-44df-49f4-82ec-2a1173c1e7d9": "pla",
  81. "74d0f5c2-fdfd-4c56-baf1-ff5fa92d177e": "pla",
  82. "64dcb783-470d-4400-91b1-7001652f20da": "pla",
  83. "3a1b479b-899c-46eb-a2ea-67050d1a4937": "pla",
  84. "4708ac49-5dde-4cc2-8c0a-87425a92c2b3": "pla",
  85. "4b560eda-1719-407f-b085-1c2c1fc8ffc1": "pla",
  86. "e10a287d-0067-4a58-9083-b7054f479991": "im-pla",
  87. "01a6b5b0-fab1-420c-a5d9-31713cbeb404": "im-pla",
  88. "f65df4ad-a027-4a48-a51d-975cc8b87041": "im-pla",
  89. "f48739f8-6d96-4a3d-9a2e-8505a47e2e35": "im-pla",
  90. "5c7d7672-e885-4452-9a78-8ba90ec79937": "petg",
  91. "91e05a6e-2f5b-4964-b973-d83b5afe6db4": "petg",
  92. "bdc7dd03-bf38-48ee-aeca-c3e11cee799e": "petg",
  93. "54f66c89-998d-4070-aa60-1cb0fd887518": "nylon",
  94. "002c84b3-84ac-4b5a-b57d-fe1f555a6351": "pva",
  95. "e4da5fcb-f62d-48a2-aaef-0b645aa6973b": "wss1",
  96. "77f06146-6569-437d-8380-9edb0d635a32": "sr30"}
  97. # must be called from the main thread because of OpenGL
  98. @staticmethod
  99. @call_on_qt_thread
  100. def _createThumbnail(width: int, height: int) -> Optional[QBuffer]:
  101. if not CuraApplication.getInstance().isVisible:
  102. Logger.warning("Can't create snapshot when renderer not initialized.")
  103. return
  104. try:
  105. snapshot = Snapshot.isometricSnapshot(width, height)
  106. thumbnail_buffer = QBuffer()
  107. thumbnail_buffer.open(QBuffer.OpenModeFlag.WriteOnly)
  108. snapshot.save(thumbnail_buffer, "PNG")
  109. return thumbnail_buffer
  110. except:
  111. Logger.logException("w", "Failed to create snapshot image")
  112. return None
  113. def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
  114. if mode != MeshWriter.OutputMode.BinaryMode:
  115. Logger.log("e", "MakerbotWriter does not support text mode.")
  116. self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
  117. return False
  118. # The GCodeWriter plugin is always available since it is in the "required" list of plugins.
  119. gcode_writer = PluginRegistry.getInstance().getPluginObject("GCodeWriter")
  120. if gcode_writer is None:
  121. Logger.log("e", "Could not find the GCodeWriter plugin, is it disabled?.")
  122. self.setInformation(
  123. catalog.i18nc("@error:load", "Could not load GCodeWriter plugin. Try to re-enable the plugin."))
  124. return False
  125. gcode_writer = cast(MeshWriter, gcode_writer)
  126. gcode_text_io = StringIO()
  127. success = gcode_writer.write(gcode_text_io, None)
  128. # Writing the g-code failed. Then I can also not write the gzipped g-code.
  129. if not success:
  130. self.setInformation(gcode_writer.getInformation())
  131. return False
  132. json_toolpaths = du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
  133. metadata = self._getMeta(nodes)
  134. png_files = []
  135. for png_format in self._PNG_FORMATS:
  136. width, height, prefix = png_format["width"], png_format["height"], png_format["prefix"]
  137. thumbnail_buffer = self._createThumbnail(width, height)
  138. if thumbnail_buffer is None:
  139. Logger.warning(f"Could not create thumbnail of size {width}x{height}.")
  140. continue
  141. png_files.append({
  142. "file": f"{prefix}_{width}x{height}.png",
  143. "data": thumbnail_buffer.data(),
  144. })
  145. try:
  146. with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
  147. zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
  148. zip_stream.writestr("print.jsontoolpath", json_toolpaths)
  149. for png_file in png_files:
  150. file, data = png_file["file"], png_file["data"]
  151. zip_stream.writestr(file, data)
  152. except (IOError, OSError, BadZipFile) as ex:
  153. Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
  154. self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
  155. return False
  156. return True
  157. def _getMeta(self, root_nodes: List[SceneNode]) -> Dict[str, any]:
  158. application = CuraApplication.getInstance()
  159. machine_manager = application.getMachineManager()
  160. global_stack = machine_manager.activeMachine
  161. extruders = global_stack.extruderList
  162. nodes = []
  163. for root_node in root_nodes:
  164. for node in DepthFirstIterator(root_node):
  165. if not getattr(node, "_outside_buildarea", False):
  166. if node.callDecoration(
  167. "isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
  168. "isNonThumbnailVisibleMesh"):
  169. nodes.append(node)
  170. meta = dict()
  171. meta["bot_type"] = MakerbotWriter._PRINT_NAME_MAP.get((name := global_stack.definition.name), name)
  172. bounds: Optional[AxisAlignedBox] = None
  173. for node in nodes:
  174. node_bounds = node.getBoundingBox()
  175. if node_bounds is None:
  176. continue
  177. if bounds is None:
  178. bounds = node_bounds
  179. else:
  180. bounds = bounds + node_bounds
  181. if bounds is not None:
  182. meta["bounding_box"] = {
  183. "x_min": bounds.left,
  184. "x_max": bounds.right,
  185. "y_min": bounds.back,
  186. "y_max": bounds.front,
  187. "z_min": bounds.bottom,
  188. "z_max": bounds.top,
  189. }
  190. material_bed_temperature = global_stack.getProperty("material_bed_temperature", "value")
  191. meta["platform_temperature"] = material_bed_temperature
  192. build_volume_temperature = global_stack.getProperty("build_volume_temperature", "value")
  193. meta["build_plane_temperature"] = build_volume_temperature
  194. print_information = application.getPrintInformation()
  195. meta["commanded_duration_s"] = int(print_information.currentPrintTime)
  196. meta["duration_s"] = int(print_information.currentPrintTime)
  197. material_lengths = list(map(meterToMillimeter, print_information.materialLengths))
  198. meta["extrusion_distance_mm"] = material_lengths[0]
  199. meta["extrusion_distances_mm"] = material_lengths
  200. meta["extrusion_mass_g"] = print_information.materialWeights[0]
  201. meta["extrusion_masses_g"] = print_information.materialWeights
  202. meta["uuid"] = print_information.slice_uuid
  203. materials = []
  204. for extruder in extruders:
  205. guid = extruder.material.getMetaData().get("GUID")
  206. material_name = extruder.material.getMetaData().get("material")
  207. material = self._MATERIAL_MAP.get(guid, material_name)
  208. materials.append(material)
  209. meta["material"] = materials[0]
  210. meta["materials"] = materials
  211. materials_temps = [extruder.getProperty("default_material_print_temperature", "value") for extruder in
  212. extruders]
  213. meta["extruder_temperature"] = materials_temps[0]
  214. meta["extruder_temperatures"] = materials_temps
  215. meta["model_counts"] = [{"count": 1, "name": node.getName()} for node in nodes]
  216. tool_types = [MakerbotWriter._EXTRUDER_NAME_MAP.get((name := extruder.variant.getName()), name) for extruder in
  217. extruders]
  218. meta["tool_type"] = tool_types[0]
  219. meta["tool_types"] = tool_types
  220. meta["version"] = MakerbotWriter._META_VERSION
  221. meta["preferences"] = dict()
  222. for node in nodes:
  223. bounds = node.getBoundingBox()
  224. meta["preferences"][str(node.getName())] = {
  225. "machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
  226. "printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
  227. }
  228. meta["miracle_config"] = {"gaggles": {str(node.getName()): {} for node in nodes}}
  229. cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
  230. meta["curaengine_version"] = cura_engine_info["version"]
  231. meta["curaengine_commit_hash"] = cura_engine_info["revision"]
  232. dulcificum_info = ConanInstalls.get("dulcificum", {"version": "unknown", "revision": "unknown"})
  233. meta["dulcificum_version"] = dulcificum_info["version"]
  234. meta["dulcificum_commit_hash"] = dulcificum_info["revision"]
  235. meta["makerbot_writer_version"] = self.getVersion()
  236. meta["pyDulcificum_version"] = du.__version__
  237. # Add engine plugin information to the metadata
  238. for name, package_info in ConanInstalls.items():
  239. if not name.startswith("curaengine_"):
  240. continue
  241. meta[f"{name}_version"] = package_info["version"]
  242. meta[f"{name}_commit_hash"] = package_info["revision"]
  243. # TODO add the following instructions
  244. # num_tool_changes
  245. # num_z_layers
  246. # num_z_transitions
  247. # platform_temperature
  248. # total_commands
  249. return meta
  250. def meterToMillimeter(value: float) -> float:
  251. """Converts a value in meters to millimeters."""
  252. return value * 1000.0