GCodeModelWriter.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import re # For escaping characters in the settings.
  4. import time
  5. import struct
  6. import math
  7. import numpy
  8. from collections import namedtuple
  9. from typing import Dict, List, NamedTuple, Optional, Union
  10. from cura import LayerDataBuilder
  11. from cura.LayerPolygon import LayerPolygon
  12. from cura.Settings.ExtruderManager import ExtruderManager
  13. from UM.Backend import Backend
  14. from UM.Job import Job
  15. from UM.Math.Vector import Vector
  16. from UM.Mesh.MeshData import MeshData
  17. from UM.Mesh.MeshWriter import MeshWriter
  18. from UM.Message import Message
  19. from UM.Logger import Logger
  20. from UM.Application import Application
  21. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  22. from UM.i18n import i18nCatalog
  23. catalog = i18nCatalog("cura")
  24. Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", float)])
  25. def is_pos_eq(pos_a, pos_b):
  26. return numpy.isclose(pos_a.x, pos_b.x) and numpy.isclose(pos_a.y, pos_b.y)
  27. ## Writes g-code to a file.
  28. #
  29. # While this poses as a mesh writer, what this really does is take the g-code
  30. # in the entire scene and write it to an output device. Since the g-code of a
  31. # single mesh isn't separable from the rest what with rafts and travel moves
  32. # and all, it doesn't make sense to write just a single mesh.
  33. #
  34. # So this plug-in takes the g-code that is stored in the root of the scene
  35. # node tree, adds a bit of extra information about the profiles and writes
  36. # that to the output device.
  37. class GCodeModelWriter(MeshWriter):
  38. ## The file format version of the serialised g-code.
  39. #
  40. # It can only read settings with the same version as the version it was
  41. # written with. If the file format is changed in a way that breaks reverse
  42. # compatibility, increment this version number!
  43. version = 3
  44. ## Dictionary that defines how characters are escaped when embedded in
  45. # g-code.
  46. #
  47. # Note that the keys of this dictionary are regex strings. The values are
  48. # not.
  49. escape_characters = {
  50. re.escape("\\"): "\\\\", # The escape character.
  51. re.escape("\n"): "\\n", # Newlines. They break off the comment.
  52. re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
  53. }
  54. _setting_keyword = ";SETTING_"
  55. _type_keyword = ";TYPE:"
  56. _layer_keyword = ";LAYER:"
  57. def __init__(self):
  58. super().__init__()
  59. self._application = Application.getInstance()
  60. ## Writes the g-code for the entire scene to a stream.
  61. #
  62. # Note that even though the function accepts a collection of nodes, the
  63. # entire scene is always written to the file since it is not possible to
  64. # separate the g-code for just specific nodes.
  65. #
  66. # \param stream The stream to write the g-code to.
  67. # \param nodes This is ignored.
  68. # \param mode Additional information on how to format the g-code in the
  69. # file. This must always be text mode.
  70. def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
  71. if mode != MeshWriter.OutputMode.BinaryMode:
  72. Logger.log("e", "GCodeWriter does not support text mode.")
  73. return False
  74. #self.write_stl(stream)
  75. #self.write_scad(stream, shape = "cube") # shape is cube or cylinder
  76. #self.write_f360(stream)
  77. self.write_csv(stream)
  78. return True
  79. def write_stl(self, stream):
  80. tube_type = "rectangular"
  81. #tube_type = "diamond"
  82. num_vertices = {
  83. "diamond": 16,
  84. "rectangular": 12}
  85. tube_function = {
  86. "diamond": self._generateTubeVerticesDiamond,
  87. "rectangular": self._generateTubeVerticesRectangular,
  88. }
  89. active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  90. scene = Application.getInstance().getController().getScene()
  91. if not hasattr(scene, "gcode_dict"):
  92. return False
  93. gcode_dict = getattr(scene, "gcode_dict")
  94. gcode_list = gcode_dict.get(active_build_plate, None)
  95. if gcode_list is not None:
  96. paths = self.convertGCode(gcode_list)
  97. # every path becomes 12 or 16 faces, each consisting of 3 coordinates and every coordinate has x, y, z
  98. paths_vertices = numpy.zeros([3 * num_vertices[tube_type] * len(paths), 3])
  99. current_base_index = 0
  100. len_paths = len(paths)
  101. print("len(paths): " + str(len_paths))
  102. count = 0
  103. for position_from, position_to, layer_thickness in paths:
  104. line_width = self._calculateLineWidth(position_to, position_from, layer_thickness)
  105. vertices = tube_function[tube_type](position_from, position_to, line_width + 0.02, layer_thickness + 0.02, offset = 0.02)
  106. #vertices = tube_function[tube_type](position_from, position_to, line_width, layer_thickness)
  107. paths_vertices[current_base_index:current_base_index + 3 * num_vertices[tube_type], :] = vertices[:, :]
  108. current_base_index += 3 * num_vertices[tube_type]
  109. print("processing" + str(count) + " / " + str(len_paths) + "...")
  110. count += 1
  111. mesh_data = MeshData(vertices = paths_vertices)
  112. print("Saving...")
  113. self._writeBinary(stream, mesh_data)
  114. def _writeBinary(self, stream, mesh_data: MeshData):
  115. Logger.log("d", "Writing stl...")
  116. stream.write("Uranium STLWriter {0}".format(time.strftime("%a %d %b %Y %H:%M:%S")).encode().ljust(80, b"\000"))
  117. face_count = 0
  118. if mesh_data.hasIndices():
  119. face_count += mesh_data.getFaceCount()
  120. else:
  121. face_count += mesh_data.getVertexCount() / 3
  122. stream.write(struct.pack("<I", int(face_count))) #Write number of faces to STL
  123. if mesh_data.hasIndices():
  124. verts = mesh_data.getVertices()
  125. for face in mesh_data.getIndices():
  126. v1 = verts[face[0]]
  127. v2 = verts[face[1]]
  128. v3 = verts[face[2]]
  129. stream.write(struct.pack("<fff", 0.0, 0.0, 0.0))
  130. stream.write(struct.pack("<fff", v1[0], -v1[2], v1[1]))
  131. stream.write(struct.pack("<fff", v2[0], -v2[2], v2[1]))
  132. stream.write(struct.pack("<fff", v3[0], -v3[2], v3[1]))
  133. stream.write(struct.pack("<H", 0))
  134. else:
  135. num_verts = mesh_data.getVertexCount()
  136. verts = mesh_data.getVertices()
  137. for index in range(0, num_verts - 1, 3):
  138. v1 = verts[index]
  139. v2 = verts[index + 1]
  140. v3 = verts[index + 2]
  141. stream.write(struct.pack("<fff", 0.0, 0.0, 0.0))
  142. stream.write(struct.pack("<fff", v1[0], -v1[2], v1[1]))
  143. stream.write(struct.pack("<fff", v2[0], -v2[2], v2[1]))
  144. stream.write(struct.pack("<fff", v3[0], -v3[2], v3[1]))
  145. stream.write(struct.pack("<H", 0))
  146. def write_scad(self, stream, shape = "cube"):
  147. active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  148. scene = Application.getInstance().getController().getScene()
  149. if not hasattr(scene, "gcode_dict"):
  150. return False
  151. gcode_dict = getattr(scene, "gcode_dict")
  152. gcode_list = gcode_dict.get(active_build_plate, None)
  153. if shape == "cylinder":
  154. stream.write("$fn = 24;\n".encode())
  155. if gcode_list is not None:
  156. paths = self.convertGCode(gcode_list)
  157. for position_from, position_to, layer_thickness in paths:
  158. line_width = self._calculateLineWidth(position_to, position_from, layer_thickness)
  159. stream.write(self._generateScad(position_from, position_to, line_width + 0.02, layer_thickness + 0.02, offset = 0.02, shape=shape).encode())
  160. def _generateScad(self, position_from, position_to, width, thickness, offset = 0, shape = "cube"):
  161. path_from_zero = Vector(position_to.x - position_from.x, position_to.y - position_from.y, position_to.z - position_from.z)
  162. length = path_from_zero.length()
  163. angle_radians = math.atan2(path_from_zero.x, path_from_zero.y)
  164. angle_degrees = math.degrees(angle_radians)
  165. print("length: " + str(length) + " angle: " + str(angle_degrees))
  166. if shape == "cube":
  167. return self._generateScadCube(position_from, length, width, thickness, angle_degrees, offset)
  168. elif shape == "cylinder":
  169. return self._generateScadCylinder(position_from, length, width, thickness, angle_degrees, offset)
  170. def _generateScadCube(self, position_from, length, width, thickness, angle, offset = 0):
  171. return "translate([{x}, {y}, {z}]) rotate([0, 0, {angle}]) translate([-{half_line_width}, -{half_line_width}, 0]) cube([{line_width}, {length}, {layer_thickness}]);\n".format(
  172. length = length + width - offset, line_width = width, layer_thickness = thickness,
  173. half_line_width = 0.5 * width, angle = -angle,
  174. x = position_from.x + offset, y = position_from.y, z = position_from.z)
  175. def _generateScadCylinder(self, position_from, length, width, thickness, angle, offset = 0):
  176. return """
  177. translate([{x}, {y}, {z}]) rotate([0, 0, {angle}]) rotate([-90, 0, 0]) union() {{
  178. cylinder(r = {half_line_width}, h = {length});
  179. sphere(r = {half_line_width});
  180. translate([0, 0, {length}])
  181. sphere(r = {half_line_width});
  182. }}
  183. """.format(
  184. length = length - offset, line_width = width, layer_thickness = thickness,
  185. half_line_width = 0.5 * width, angle = -angle,
  186. x = position_from.x + offset, y = position_from.y, z = position_from.z)
  187. # return """
  188. # translate([{x}, {y}, {z}])
  189. # rotate([0, 0, {angle}])
  190. # translate([0, -{half_line_width}, 0])
  191. # rotate([-90, 0, 0])
  192. # cylinder(r = {half_line_width}, h = {length});
  193. # """.format(
  194. # length = length + width - offset, line_width = width, layer_thickness = thickness,
  195. # half_line_width = 0.5 * width, angle = -angle,
  196. # x = position_from.x + offset, y = position_from.y, z = position_from.z)
  197. def write_f360(self, stream):
  198. script_template_before = """
  199. import adsk.core, adsk.fusion, traceback
  200. def run(context):
  201. ui = None
  202. try:
  203. app = adsk.core.Application.get()
  204. ui = app.userInterface
  205. doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
  206. design = app.activeProduct
  207. # Get the root component of the active design.
  208. rootComp = design.rootComponent
  209. # Create a new sketch on the xy plane.
  210. sketch = rootComp.sketches.add(rootComp.xYConstructionPlane)
  211. """
  212. script_template_after = """
  213. except:
  214. if ui:
  215. ui.messageBox('Failed:\\n{}'.format(traceback.format_exc()))"""
  216. active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  217. scene = Application.getInstance().getController().getScene()
  218. if not hasattr(scene, "gcode_dict"):
  219. return False
  220. gcode_dict = getattr(scene, "gcode_dict")
  221. gcode_list = gcode_dict.get(active_build_plate, None)
  222. if gcode_list is not None:
  223. paths = self.convertGCode(gcode_list)
  224. stream.write(script_template_before.encode())
  225. for position_from, position_to, layer_thickness in paths:
  226. stream.write(self._generateF360Line(position_from, position_to).encode())
  227. stream.write(script_template_after.encode())
  228. def _generateF360Line(self, position_from, position_to):
  229. add_line_template = """
  230. pt1 = adsk.core.Point3D.create({x0}, {y0}, {z0})
  231. pt2 = adsk.core.Point3D.create({x1}, {y1}, {z1})
  232. sketch.sketchCurves.sketchLines.addByTwoPoints(pt1, pt2)
  233. """
  234. return add_line_template.format(
  235. x0 = position_from.x, y0 = position_from.y, z0 = position_from.z,
  236. x1 = position_to.x, y1 = position_to.y, z1 = position_to.z)
  237. def write_csv(self, stream):
  238. active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  239. scene = Application.getInstance().getController().getScene()
  240. if not hasattr(scene, "gcode_dict"):
  241. return False
  242. gcode_dict = getattr(scene, "gcode_dict")
  243. gcode_list = gcode_dict.get(active_build_plate, None)
  244. if gcode_list is not None:
  245. paths = self.convertGCode(gcode_list)
  246. for position_from, position_to, layer_thickness in paths:
  247. line_width = self._calculateLineWidth(position_to, position_from, layer_thickness)
  248. stream.write(self._generate_csv_line(position_from, position_to, width = line_width, height = layer_thickness).encode())
  249. def _generate_csv_line(self, position_from, position_to, width, height):
  250. return "{x0}, {y0}, {z0}, {x1}, {y1}, {z1}\n".format(
  251. x0 = position_from.x, y0 = position_from.y, z0 = position_from.z,
  252. x1 = position_to.x, y1 = position_to.y, z1 = position_to.z, width = width, height = height)
  253. ## Return vertices corresponding to a tube-like shape that goes from 'from' to 'to'
  254. # This version has rectangular cross sections
  255. def _generateTubeVerticesRectangular(self, point_from, point_to, line_width, line_height, offset = 0):
  256. result = numpy.zeros([3 * 12, 3])
  257. real_result = numpy.zeros([3 * 12, 3]) # y and z are swapped
  258. half_width = 0.5 * line_width
  259. half_height = 0.5 * line_height
  260. v_from = Vector(point_from.x, point_from.y, point_from.z)
  261. v_to = Vector(point_to.x, point_to.y, point_to.z)
  262. direction = (v_to - v_from).normalized()
  263. v_from -= Vector(half_width * direction.x, half_width * direction.y, 0)
  264. v_to += Vector(half_width * direction.x, half_width * direction.y, 0)
  265. if offset != 0:
  266. v_from = v_from + Vector(offset * direction.x, offset * direction.y, 0)
  267. # the "long" part, or "sides"
  268. idx = 0
  269. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z + half_height]
  270. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z + half_height]
  271. result[idx + 2, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z - half_height]
  272. idx += 3
  273. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z + half_height]
  274. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z - half_height]
  275. result[idx + 2, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z - half_height]
  276. idx += 3
  277. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z - half_height]
  278. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z - half_height]
  279. result[idx + 2, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z - half_height]
  280. idx += 3
  281. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z - half_height]
  282. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z - half_height]
  283. result[idx + 2, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z - half_height]
  284. idx += 3
  285. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z - half_height]
  286. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z - half_height]
  287. result[idx + 2, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z + half_height]
  288. idx += 3
  289. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z - half_height]
  290. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z + half_height]
  291. result[idx + 2, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z + half_height]
  292. idx += 3
  293. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z + half_height]
  294. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z + half_height]
  295. result[idx + 2, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z + half_height]
  296. idx += 3
  297. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z + half_height]
  298. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z + half_height]
  299. result[idx + 2, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z + half_height]
  300. # "to"
  301. idx += 3
  302. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z + half_height]
  303. result[idx + 1, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z - half_height]
  304. result[idx + 2, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z - half_height]
  305. idx += 3
  306. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z - half_height]
  307. result[idx + 1, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z + half_height]
  308. result[idx + 2, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z + half_height]
  309. # "from"
  310. idx += 3
  311. result[idx + 0, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z - half_height]
  312. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z - half_height]
  313. result[idx + 2, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z + half_height]
  314. idx += 3
  315. result[idx + 0, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z + half_height]
  316. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z + half_height]
  317. result[idx + 2, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z - half_height]
  318. # x, z, y in printing coordinates, so swapping + 1 and + 2
  319. real_result[:, 0] = result[:, 0]
  320. real_result[:, 1] = result[:, 2]
  321. real_result[:, 2] = -result[:, 1]
  322. return real_result
  323. ## Return vertices corresponding to a tube-like shape that goes from 'from' to 'to'
  324. # This version does it in the same way as the layer view does: the cross section is diamond shaped
  325. # Assumes that z in from and to are the same
  326. # offset is also used to move some points around so the chance that they collide is smaller (i.e. in a square box)
  327. def _generateTubeVerticesDiamond(self, point_from, point_to, line_width, line_height, offset = 0):
  328. result = numpy.zeros([3 * 16, 3])
  329. real_result = numpy.zeros([3 * 16, 3]) # y and z are swapped
  330. half_width = 0.5 * line_width
  331. half_height = 0.5 * line_height
  332. v_from = Vector(point_from.x, point_from.y, point_from.z)
  333. v_to = Vector(point_to.x, point_to.y, point_to.z)
  334. direction = (v_to - v_from).normalized()
  335. if offset != 0:
  336. v_from = v_from + Vector(offset * direction.x, offset * direction.y, 0)
  337. # the "long" part, or "sides"
  338. idx = 0
  339. result[idx + 0, :] = [v_to.x, v_to.y, v_to.z + half_height]
  340. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z + half_height]
  341. result[idx + 2, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z]
  342. idx += 3
  343. result[idx + 0, :] = [v_to.x, v_to.y, v_to.z + half_height]
  344. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z]
  345. result[idx + 2, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z]
  346. idx += 3
  347. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z]
  348. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z]
  349. result[idx + 2, :] = [v_from.x, v_from.y, v_from.z - half_height]
  350. idx += 3
  351. result[idx + 0, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z]
  352. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z - half_height]
  353. result[idx + 2, :] = [v_to.x, v_to.y, v_to.z - half_height]
  354. idx += 3
  355. result[idx + 0, :] = [v_to.x, v_to.y, v_to.z - half_height]
  356. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z - half_height]
  357. result[idx + 2, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z]
  358. idx += 3
  359. result[idx + 0, :] = [v_to.x, v_to.y, v_to.z - half_height]
  360. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z]
  361. result[idx + 2, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z]
  362. idx += 3
  363. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z]
  364. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z]
  365. result[idx + 2, :] = [v_from.x, v_from.y, v_from.z + half_height]
  366. idx += 3
  367. result[idx + 0, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z]
  368. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z + half_height]
  369. result[idx + 2, :] = [v_to.x, v_to.y, v_to.z + half_height]
  370. # "from" end
  371. idx += 3
  372. result[idx + 0, :] = [v_from.x - half_width * direction.x, v_from.y - half_width * direction.y, v_from.z]
  373. result[idx + 1, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z]
  374. result[idx + 2, :] = [v_from.x, v_from.y, v_from.z + half_height]
  375. idx += 3
  376. result[idx + 0, :] = [v_from.x - half_width * direction.x, v_from.y - half_width * direction.y, v_from.z]
  377. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z - half_height]
  378. result[idx + 2, :] = [v_from.x + half_width * direction.y, v_from.y - half_width * direction.x, v_from.z]
  379. idx += 3
  380. result[idx + 0, :] = [v_from.x - half_width * direction.x, v_from.y - half_width * direction.y, v_from.z]
  381. result[idx + 1, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z]
  382. result[idx + 2, :] = [v_from.x, v_from.y, v_from.z - half_height]
  383. idx += 3
  384. result[idx + 0, :] = [v_from.x - half_width * direction.x, v_from.y - half_width * direction.y, v_from.z]
  385. result[idx + 1, :] = [v_from.x, v_from.y, v_from.z + half_height]
  386. result[idx + 2, :] = [v_from.x - half_width * direction.y, v_from.y + half_width * direction.x, v_from.z]
  387. # "to" end
  388. idx += 3
  389. result[idx + 0, :] = [v_to.x + half_width * direction.x, v_to.y + half_width * direction.y, v_to.z]
  390. result[idx + 1, :] = [v_to.x, v_to.y, v_to.z + half_height]
  391. result[idx + 2, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z]
  392. idx += 3
  393. result[idx + 0, :] = [v_to.x + half_width * direction.x, v_to.y + half_width * direction.y, v_to.z]
  394. result[idx + 1, :] = [v_to.x + half_width * direction.y, v_to.y - half_width * direction.x, v_to.z]
  395. result[idx + 2, :] = [v_to.x, v_to.y, v_to.z - half_height]
  396. idx += 3
  397. result[idx + 0, :] = [v_to.x + half_width * direction.x, v_to.y + half_width * direction.y, v_to.z]
  398. result[idx + 1, :] = [v_to.x, v_to.y, v_to.z - half_height]
  399. result[idx + 2, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z]
  400. idx += 3
  401. result[idx + 0, :] = [v_to.x + half_width * direction.x, v_to.y + half_width * direction.y, v_to.z]
  402. result[idx + 1, :] = [v_to.x - half_width * direction.y, v_to.y + half_width * direction.x, v_to.z]
  403. result[idx + 2, :] = [v_to.x, v_to.y, v_to.z + half_height]
  404. # x, z, y in printing coordinates, so swapping + 1 and + 2
  405. real_result[:, 0] = result[:, 0]
  406. real_result[:, 1] = result[:, 2]
  407. real_result[:, 2] = -result[:, 1]
  408. return real_result
  409. def convertGCode(self, gcode_layers):
  410. self._clearValues()
  411. current_path = []
  412. current_position = self._position(0, 0, 0, 0, [0])
  413. for gcode_layer in gcode_layers:
  414. for line in gcode_layer.split("\n"):
  415. G = self._getInt(line, "G")
  416. if G is not None:
  417. current_position = self.processGCode(G, line, current_position, current_path)
  418. return current_path
  419. def _calculateLineWidth(self, current_point: Position, previous_point: Position, layer_thickness: float) -> float:
  420. # Area of the filament
  421. Af = (self._filament_diameter / 2) ** 2 * numpy.pi
  422. # Length of the extruded filament
  423. de = current_point.e - previous_point.e
  424. # Volumne of the extruded filament
  425. dVe = de * Af
  426. # Length of the printed line
  427. dX = numpy.sqrt((current_point.x - previous_point.x)**2 + (current_point.y - previous_point.y)**2)
  428. # When the extruder recovers from a retraction, we get zero distance
  429. if dX == 0:
  430. return 0.1
  431. # Area of the printed line. This area is a rectangle
  432. Ae = dVe / dX
  433. # This area is a rectangle with area equal to layer_thickness * layer_width
  434. line_width = Ae / layer_thickness
  435. # A threshold is set to avoid weird paths in the GCode
  436. if line_width > 1.2:
  437. return 0.35
  438. return line_width
  439. def _clearValues(self) -> None:
  440. self._extruder_number = 0
  441. self._extrusion_length_offset = [0]
  442. self._layer_type = LayerPolygon.Inset0Type
  443. self._layer_number = 0
  444. self._previous_z = 0
  445. self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
  446. self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
  447. self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
  448. self._current_layer_thickness = 0.2 # default
  449. self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
  450. self._filament_diameter = 2.85 # default
  451. @staticmethod
  452. def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
  453. n = line.find(code)
  454. if n < 0:
  455. return None
  456. n += len(code)
  457. pattern = re.compile("[;\s]")
  458. match = pattern.search(line, n)
  459. m = match.start() if match is not None else -1
  460. try:
  461. if m < 0:
  462. return line[n:]
  463. return line[n:m]
  464. except:
  465. return None
  466. def _getInt(self, line: str, code: str) -> Optional[int]:
  467. value = self._getValue(line, code)
  468. try:
  469. return int(value)
  470. except:
  471. return None
  472. ## For showing correct x, y offsets for each extruder
  473. def _extruderOffsets(self) -> Dict[int, List[float]]:
  474. result = {}
  475. for extruder in ExtruderManager.getInstance().getExtruderStacks():
  476. result[int(extruder.getMetaData().get("position", "0"))] = [
  477. extruder.getProperty("machine_nozzle_offset_x", "value"),
  478. extruder.getProperty("machine_nozzle_offset_y", "value")]
  479. return result
  480. def _gCode0(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
  481. x, y, z, f, e = position
  482. if self._is_absolute_positioning:
  483. x = params.x if params.x is not None else x
  484. y = params.y if params.y is not None else y
  485. z = params.z if params.z is not None else z
  486. else:
  487. x += params.x if params.x is not None else 0
  488. y += params.y if params.y is not None else 0
  489. z += params.z if params.z is not None else 0
  490. f = params.f if params.f is not None else f
  491. if params.e is not None:
  492. new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
  493. position_from = self._position(x = position.x, y = position.y, z = position.z, f = position.f, e = position.e[self._extruder_number])
  494. position_to = self._position(x = x, y = y, z = z, f = f, e = new_extrusion_value + self._extrusion_length_offset[self._extruder_number])
  495. if new_extrusion_value > e[self._extruder_number] + 0.001 and not is_pos_eq(position_from, position_to):
  496. path.append((position_from, position_to, self._current_layer_thickness))
  497. # else:
  498. # path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
  499. e[self._extruder_number] = new_extrusion_value
  500. # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
  501. # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
  502. if z > self._previous_z and (z - self._previous_z < 1.5):
  503. self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
  504. self._previous_z = z
  505. # else:
  506. # path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
  507. return self._position(x, y, z, f, e)
  508. # G0 and G1 should be handled exactly the same.
  509. _gCode1 = _gCode0
  510. ## Reset the current position to the values specified.
  511. # For example: G92 X10 will set the X to 10 without any physical motion.
  512. def _gCode92(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
  513. if params.e is not None:
  514. # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
  515. self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
  516. position.e[self._extruder_number] = params.e
  517. return self._position(
  518. params.x if params.x is not None else position.x,
  519. params.y if params.y is not None else position.y,
  520. params.z if params.z is not None else position.z,
  521. params.f if params.f is not None else position.f,
  522. position.e)
  523. def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  524. func = getattr(self, "_gCode%s" % G, None)
  525. line = line.split(";", 1)[0] # Remove comments (if any)
  526. if func is not None:
  527. s = line.upper().split(" ")
  528. x, y, z, f, e = None, None, None, None, None
  529. for item in s[1:]:
  530. if len(item) <= 1:
  531. continue
  532. if item.startswith(";"):
  533. continue
  534. if item[0] == "X":
  535. x = float(item[1:])
  536. if item[0] == "Y":
  537. y = float(item[1:])
  538. if item[0] == "Z":
  539. z = float(item[1:])
  540. if item[0] == "F":
  541. f = float(item[1:]) / 60
  542. if item[0] == "E":
  543. e = float(item[1:])
  544. params = self._position(x, y, z, f, e)
  545. return func(position, params, path)
  546. return position
  547. def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  548. self._extruder_number = T
  549. if self._extruder_number + 1 > len(position.e):
  550. self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
  551. position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
  552. return position
  553. def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
  554. pass
  555. def convertGCodeOld(self, gcode_list):
  556. result = MeshData()
  557. Logger.log("d", "Converting g-code to meshes...")
  558. self._clearValues()
  559. self._cancelled = False
  560. # We obtain the filament diameter from the selected extruder to calculate line widths
  561. global_stack = Application.getInstance().getGlobalContainerStack()
  562. self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
  563. #scene_node = CuraSceneNode()
  564. # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
  565. # real data to calculate it from.
  566. #scene_node.getBoundingBox = self._getNullBoundingBox
  567. gcode_list = []
  568. self._is_layers_in_file = False
  569. self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
  570. ##############################################################################################
  571. ## This part is where the action starts
  572. ##############################################################################################
  573. file_lines = 0
  574. current_line = 0
  575. for line in gcode_list:
  576. file_lines += 1
  577. gcode_list.append(line + "\n")
  578. if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  579. self._is_layers_in_file = True
  580. file_step = max(math.floor(file_lines / 100), 1)
  581. self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
  582. lifetime=0,
  583. title = catalog.i18nc("@info:title", "G-code Details"))
  584. self._message.setProgress(0)
  585. self._message.show()
  586. Logger.log("d", "Parsing Gcode...")
  587. current_position = self._position(0, 0, 0, 0, [0], self._current_layer_thickness)
  588. current_path = []
  589. min_layer_number = 0
  590. negative_layers = 0
  591. previous_layer = 0
  592. for line in gcode_list:
  593. if self._cancelled:
  594. Logger.log("d", "Parsing Gcode file cancelled")
  595. return None
  596. current_line += 1
  597. if current_line % file_step == 0:
  598. self._message.setProgress(math.floor(current_line / file_lines * 100))
  599. Job.yieldThread()
  600. if len(line) == 0:
  601. continue
  602. if line.find(self._type_keyword) == 0:
  603. _type = line[len(self._type_keyword):].strip()
  604. if _type == "WALL-INNER":
  605. self._layer_type = LayerPolygon.InsetXType
  606. elif _type == "WALL-OUTER":
  607. self._layer_type = LayerPolygon.Inset0Type
  608. elif _type == "SKIN":
  609. self._layer_type = LayerPolygon.SkinType
  610. elif _type == "SKIRT":
  611. self._layer_type = LayerPolygon.SkirtType
  612. elif _type == "SUPPORT":
  613. self._layer_type = LayerPolygon.SupportType
  614. elif _type == "FILL":
  615. self._layer_type = LayerPolygon.InfillType
  616. else:
  617. Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", _type)
  618. # When the layer change is reached, the polygon is computed so we have just one layer per extruder
  619. if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
  620. try:
  621. layer_number = int(line[len(self._layer_keyword):])
  622. #self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  623. current_path.clear()
  624. # Start the new layer at the end position of the last layer
  625. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  626. # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
  627. # as in ProcessSlicedLayersJob
  628. if layer_number < min_layer_number:
  629. min_layer_number = layer_number
  630. if layer_number < 0:
  631. layer_number += abs(min_layer_number)
  632. negative_layers += 1
  633. else:
  634. layer_number += negative_layers
  635. # In case there is a gap in the layer count, empty layers are created
  636. # for empty_layer in range(previous_layer + 1, layer_number):
  637. # self._createEmptyLayer(empty_layer)
  638. self._layer_number = layer_number
  639. previous_layer = layer_number
  640. except:
  641. pass
  642. # This line is a comment. Ignore it (except for the layer_keyword)
  643. if line.startswith(";"):
  644. continue
  645. G = self._getInt(line, "G")
  646. if G is not None:
  647. # When find a movement, the new posistion is calculated and added to the current_path, but
  648. # don't need to create a polygon until the end of the layer
  649. current_position = self.processGCode(G, line, current_position, current_path)
  650. continue
  651. # When changing the extruder, the polygon with the stored paths is computed
  652. if line.startswith("T"):
  653. T = self._getInt(line, "T")
  654. if T is not None:
  655. # self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
  656. current_path.clear()
  657. # When changing tool, store the end point of the previous path, then process the code and finally
  658. # add another point with the new position of the head.
  659. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  660. current_position = self.processTCode(T, line, current_position, current_path)
  661. current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
  662. if line.startswith("M"):
  663. M = self._getInt(line, "M")
  664. self.processMCode(M, line, current_position, current_path)
  665. # "Flush" leftovers. Last layer paths are still stored
  666. # if len(current_path) > 1:
  667. # if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
  668. # self._layer_number += 1
  669. # current_path.clear()
  670. material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
  671. material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
  672. material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
  673. material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
  674. material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
  675. material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
  676. material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
  677. material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
  678. material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
  679. layer_mesh = self._layer_data_builder.build(material_color_map)
  680. # decorator = LayerDataDecorator()
  681. # decorator.setLayerData(layer_mesh)
  682. # scene_node.addDecorator(decorator)
  683. # gcode_list_decorator = GCodeListDecorator()
  684. # gcode_list_decorator.setGCodeList(gcode_list)
  685. # scene_node.addDecorator(gcode_list_decorator)
  686. # gcode_dict stores gcode_lists for a number of build plates.
  687. active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  688. gcode_dict = {active_build_plate_id: gcode_list}
  689. Application.getInstance().getController().getScene().gcode_dict = gcode_dict
  690. Logger.log("d", "Finished parsing Gcode")
  691. self._message.hide()
  692. if self._layer_number == 0:
  693. Logger.log("w", "File doesn't contain any valid layers")
  694. settings = Application.getInstance().getGlobalContainerStack()
  695. if not settings.getProperty("machine_center_is_zero", "value"):
  696. machine_width = settings.getProperty("machine_width", "value")
  697. machine_depth = settings.getProperty("machine_depth", "value")
  698. # scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
  699. Logger.log("d", "Saving GCode")
  700. if Application.getInstance().getPreferences().getValue("gcodereader/show_caution"):
  701. caution_message = Message(catalog.i18nc(
  702. "@info:generic",
  703. "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
  704. lifetime=0,
  705. title = catalog.i18nc("@info:title", "G-code Details"))
  706. caution_message.show()
  707. # The "save/print" button's state is bound to the backend state.
  708. backend = Application.getInstance().getBackend()
  709. backend.backendStateChange.emit(Backend.BackendState.Disabled)
  710. return result