Stretch.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. # This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
  2. """
  3. Copyright (c) 2017 Christophe Baribaud 2017
  4. Python implementation of https://github.com/electrocbd/post_stretch
  5. Correction of hole sizes, cylinder diameters and curves
  6. See the original description in https://github.com/electrocbd/post_stretch
  7. WARNING This script has never been tested with several extruders
  8. """
  9. from ..Script import Script
  10. import numpy as np
  11. from UM.Logger import Logger
  12. from UM.Application import Application
  13. import re
  14. from cura.Settings.ExtruderManager import ExtruderManager
  15. def _getValue(line, key, default=None):
  16. """
  17. Convenience function that finds the value in a line of g-code.
  18. When requesting key = x from line "G1 X100" the value 100 is returned.
  19. It is a copy of Stript's method, so it is no DontRepeatYourself, but
  20. I split the class into setup part (Stretch) and execution part (Strecher)
  21. and only the setup part inherits from Script
  22. """
  23. if not key in line or (";" in line and line.find(key) > line.find(";")):
  24. return default
  25. sub_part = line[line.find(key) + 1:]
  26. number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part)
  27. if number is None:
  28. return default
  29. return float(number.group(0))
  30. class GCodeStep():
  31. """
  32. Class to store the current value of each G_Code parameter
  33. for any G-Code step
  34. """
  35. def __init__(self, step):
  36. self.step = step
  37. self.step_x = 0
  38. self.step_y = 0
  39. self.step_z = 0
  40. self.step_e = 0
  41. self.step_f = 0
  42. self.comment = ""
  43. def readStep(self, line):
  44. """
  45. Reads gcode from line into self
  46. """
  47. self.step_x = _getValue(line, "X", self.step_x)
  48. self.step_y = _getValue(line, "Y", self.step_y)
  49. self.step_z = _getValue(line, "Z", self.step_z)
  50. self.step_e = _getValue(line, "E", self.step_e)
  51. self.step_f = _getValue(line, "F", self.step_f)
  52. return
  53. def copyPosFrom(self, step):
  54. """
  55. Copies positions of step into self
  56. """
  57. self.step_x = step.step_x
  58. self.step_y = step.step_y
  59. self.step_z = step.step_z
  60. self.step_e = step.step_e
  61. self.step_f = step.step_f
  62. self.comment = step.comment
  63. return
  64. # Execution part of the stretch plugin
  65. class Stretcher():
  66. """
  67. Execution part of the stretch algorithm
  68. """
  69. def __init__(self, line_width, wc_stretch, pw_stretch):
  70. self.line_width = line_width
  71. self.wc_stretch = wc_stretch
  72. self.pw_stretch = pw_stretch
  73. if self.pw_stretch > line_width / 4:
  74. self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance
  75. self.outpos = GCodeStep(0)
  76. self.vd1 = np.empty((0, 2)) # Start points of segments
  77. # of already deposited material for current layer
  78. self.vd2 = np.empty((0, 2)) # End points of segments
  79. # of already deposited material for current layer
  80. self.layer_z = 0 # Z position of the extrusion moves of the current layer
  81. self.layergcode = ""
  82. def execute(self, data):
  83. """
  84. Computes the new X and Y coordinates of all g-code steps
  85. """
  86. Logger.log("d", "Post stretch with line width " + str(self.line_width)
  87. + "mm wide circle stretch " + str(self.wc_stretch)+ "mm"
  88. + " and push wall stretch " + str(self.pw_stretch) + "mm")
  89. retdata = []
  90. layer_steps = []
  91. current = GCodeStep(0)
  92. self.layer_z = 0.
  93. current_e = 0.
  94. for layer in data:
  95. lines = layer.rstrip("\n").split("\n")
  96. for line in lines:
  97. current.comment = ""
  98. if line.find(";") >= 0:
  99. current.comment = line[line.find(";"):]
  100. if _getValue(line, "G") == 0:
  101. current.readStep(line)
  102. onestep = GCodeStep(0)
  103. onestep.copyPosFrom(current)
  104. elif _getValue(line, "G") == 1:
  105. current.readStep(line)
  106. onestep = GCodeStep(1)
  107. onestep.copyPosFrom(current)
  108. elif _getValue(line, "G") == 92:
  109. current.readStep(line)
  110. onestep = GCodeStep(-1)
  111. onestep.copyPosFrom(current)
  112. else:
  113. onestep = GCodeStep(-1)
  114. onestep.copyPosFrom(current)
  115. onestep.comment = line
  116. if line.find(";LAYER:") >= 0 and len(layer_steps):
  117. # Previous plugin "forgot" to separate two layers...
  118. Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
  119. + " " + str(len(layer_steps)) + " steps")
  120. retdata.append(self.processLayer(layer_steps))
  121. layer_steps = []
  122. layer_steps.append(onestep)
  123. # self.layer_z is the z position of the last extrusion move (not travel move)
  124. if current.step_z != self.layer_z and current.step_e != current_e:
  125. self.layer_z = current.step_z
  126. current_e = current.step_e
  127. if len(layer_steps): # Force a new item in the array
  128. Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
  129. + " " + str(len(layer_steps)) + " steps")
  130. retdata.append(self.processLayer(layer_steps))
  131. layer_steps = []
  132. retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n")
  133. retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n")
  134. return retdata
  135. def extrusionBreak(self, layer_steps, i_pos):
  136. """
  137. Returns true if the command layer_steps[i_pos] breaks the extruded filament
  138. i.e. it is a travel move
  139. """
  140. if i_pos == 0:
  141. return True # Begining a layer always breaks filament (for simplicity)
  142. step = layer_steps[i_pos]
  143. prev_step = layer_steps[i_pos - 1]
  144. if step.step_e != prev_step.step_e:
  145. return False
  146. delta_x = step.step_x - prev_step.step_x
  147. delta_y = step.step_y - prev_step.step_y
  148. if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4:
  149. # This is a very short movement, less than 0.5 * line_width
  150. # It does not break filament, we should stay in the same extrusion sequence
  151. return False
  152. return True # New sequence
  153. def processLayer(self, layer_steps):
  154. """
  155. Computes the new coordinates of g-code steps
  156. for one layer (all the steps at the same Z coordinate)
  157. """
  158. self.outpos.step_x = -1000 # Force output of X and Y coordinates
  159. self.outpos.step_y = -1000 # at each start of layer
  160. self.layergcode = ""
  161. self.vd1 = np.empty((0, 2))
  162. self.vd2 = np.empty((0, 2))
  163. orig_seq = np.empty((0, 2))
  164. modif_seq = np.empty((0, 2))
  165. iflush = 0
  166. for i, step in enumerate(layer_steps):
  167. if step.step == 0 or step.step == 1:
  168. if self.extrusionBreak(layer_steps, i):
  169. # No extrusion since the previous step, so it is a travel move
  170. # Let process steps accumulated into orig_seq,
  171. # which are a sequence of continuous extrusion
  172. modif_seq = np.copy(orig_seq)
  173. if len(orig_seq) >= 2:
  174. self.workOnSequence(orig_seq, modif_seq)
  175. self.generate(layer_steps, iflush, i, modif_seq)
  176. iflush = i
  177. orig_seq = np.empty((0, 2))
  178. orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])])
  179. if len(orig_seq):
  180. modif_seq = np.copy(orig_seq)
  181. if len(orig_seq) >= 2:
  182. self.workOnSequence(orig_seq, modif_seq)
  183. self.generate(layer_steps, iflush, len(layer_steps), modif_seq)
  184. return self.layergcode
  185. def stepToGcode(self, onestep):
  186. """
  187. Converts a step into G-Code
  188. For each of the X, Y, Z, E and F parameter,
  189. the parameter is written only if its value changed since the
  190. previous g-code step.
  191. """
  192. sout = ""
  193. if onestep.step_f != self.outpos.step_f:
  194. self.outpos.step_f = onestep.step_f
  195. sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".")
  196. if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y:
  197. assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails,
  198. # something went really wrong !
  199. self.outpos.step_x = onestep.step_x
  200. sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".")
  201. assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails,
  202. # something went really wrong !
  203. self.outpos.step_y = onestep.step_y
  204. sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".")
  205. if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z:
  206. self.outpos.step_z = onestep.step_z
  207. sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".")
  208. if onestep.step_e != self.outpos.step_e:
  209. self.outpos.step_e = onestep.step_e
  210. sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".")
  211. return sout
  212. def generate(self, layer_steps, ibeg, iend, orig_seq):
  213. """
  214. Appends g-code lines to the plugin's returned string
  215. starting from step ibeg included and until step iend excluded
  216. """
  217. ipos = 0
  218. for i in range(ibeg, iend):
  219. if layer_steps[i].step == 0:
  220. layer_steps[i].step_x = orig_seq[ipos][0]
  221. layer_steps[i].step_y = orig_seq[ipos][1]
  222. sout = "G0" + self.stepToGcode(layer_steps[i])
  223. self.layergcode = self.layergcode + sout + "\n"
  224. ipos = ipos + 1
  225. elif layer_steps[i].step == 1:
  226. layer_steps[i].step_x = orig_seq[ipos][0]
  227. layer_steps[i].step_y = orig_seq[ipos][1]
  228. sout = "G1" + self.stepToGcode(layer_steps[i])
  229. self.layergcode = self.layergcode + sout + "\n"
  230. ipos = ipos + 1
  231. else:
  232. self.layergcode = self.layergcode + layer_steps[i].comment + "\n"
  233. def workOnSequence(self, orig_seq, modif_seq):
  234. """
  235. Computes new coordinates for a sequence
  236. A sequence is a list of consecutive g-code steps
  237. of continuous material extrusion
  238. """
  239. d_contact = self.line_width / 2.0
  240. if (len(orig_seq) > 2 and
  241. ((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact):
  242. # Starting and ending point of the sequence are nearby
  243. # It is a closed loop
  244. #self.layergcode = self.layergcode + ";wideCircle\n"
  245. self.wideCircle(orig_seq, modif_seq)
  246. else:
  247. #self.layergcode = self.layergcode + ";wideTurn\n"
  248. self.wideTurn(orig_seq, modif_seq) # It is an open curve
  249. if len(orig_seq) > 6: # Don't try push wall on a short sequence
  250. self.pushWall(orig_seq, modif_seq)
  251. if len(orig_seq):
  252. self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])])
  253. self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])])
  254. def wideCircle(self, orig_seq, modif_seq):
  255. """
  256. Similar to wideTurn
  257. The first and last point of the sequence are the same,
  258. so it is possible to extend the end of the sequence
  259. with its beginning when seeking for triangles
  260. It is necessary to find the direction of the curve, knowing three points (a triangle)
  261. If the triangle is not wide enough, there is a huge risk of finding
  262. an incorrect orientation, due to insufficient accuracy.
  263. So, when the consecutive points are too close, the method
  264. use following and preceding points to form a wider triangle around
  265. the current point
  266. dmin_tri is the minimum distance between two consecutive points
  267. of an acceptable triangle
  268. """
  269. dmin_tri = 0.5
  270. iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
  271. ibeg = 0 # Index of first point of the triangle
  272. iend = 0 # Index of the third point of the triangle
  273. for i, step in enumerate(orig_seq):
  274. if i == 0 or i == len(orig_seq) - 1:
  275. # First and last point of the sequence are the same,
  276. # so it is necessary to skip one of these two points
  277. # when creating a triangle containing the first or the last point
  278. iextra = iextra_base + 1
  279. else:
  280. iextra = iextra_base
  281. # i is the index of the second point of the triangle
  282. # pos_after is the array of positions of the original sequence
  283. # after the current point
  284. pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2))
  285. # Vector of distances between the current point and each following point
  286. dist_from_point = ((step - pos_after) ** 2).sum(1)
  287. if np.amax(dist_from_point) < dmin_tri * dmin_tri:
  288. continue
  289. iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
  290. # pos_before is the array of positions of the original sequence
  291. # before the current point
  292. pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2))
  293. # This time, vector of distances between the current point and each preceding point
  294. dist_from_point = ((step - pos_before) ** 2).sum(1)
  295. if np.amax(dist_from_point) < dmin_tri * dmin_tri:
  296. continue
  297. ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
  298. # See https://github.com/electrocbd/post_stretch for explanations
  299. # relpos is the relative position of the projection of the second point
  300. # of the triangle on the segment from the first to the third point
  301. # 0 means the position of the first point, 1 means the position of the third,
  302. # intermediate values are positions between
  303. length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0)
  304. relpos = ((step - pos_before[ibeg])
  305. * (pos_after[iend] - pos_before[ibeg])).sum(0)
  306. if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
  307. relpos /= length_base
  308. else:
  309. relpos = 0.5 # To avoid division by zero or precision loss
  310. projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
  311. dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
  312. if dist_from_proj > 0.0003: # Move central point only if points are not aligned
  313. modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
  314. * (projection - step))
  315. return
  316. def wideTurn(self, orig_seq, modif_seq):
  317. '''
  318. We have to select three points in order to form a triangle
  319. These three points should be far enough from each other to have
  320. a reliable estimation of the orientation of the current turn
  321. '''
  322. dmin_tri = self.line_width / 2.0
  323. ibeg = 0
  324. iend = 2
  325. for i in range(1, len(orig_seq) - 1):
  326. dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1)
  327. if np.amax(dist_from_point) < dmin_tri * dmin_tri:
  328. continue
  329. iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri)
  330. dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1)
  331. if np.amax(dist_from_point) < dmin_tri * dmin_tri:
  332. continue
  333. ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri)
  334. length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0)
  335. relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0)
  336. if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
  337. relpos /= length_base
  338. else:
  339. relpos = 0.5
  340. projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg])
  341. dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0))
  342. if dist_from_proj > 0.001:
  343. modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj)
  344. * (projection - orig_seq[i]))
  345. return
  346. def pushWall(self, orig_seq, modif_seq):
  347. """
  348. The algorithm tests for each segment if material was
  349. already deposited at one or the other side of this segment.
  350. If material was deposited at one side but not both,
  351. the segment is moved into the direction of the deposited material,
  352. to "push the wall"
  353. Already deposited material is stored as segments.
  354. vd1 is the array of the starting points of the segments
  355. vd2 is the array of the ending points of the segments
  356. For example, segment nr 8 starts at position self.vd1[8]
  357. and ends at position self.vd2[8]
  358. """
  359. dist_palp = self.line_width # Palpation distance to seek for a wall
  360. mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn
  361. for i in range(len(orig_seq)):
  362. ibeg = i # Index of the first point of the segment
  363. iend = i + 1 # Index of the last point of the segment
  364. if iend == len(orig_seq):
  365. iend = i - 1
  366. xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg])
  367. xperp = xperp / np.sqrt((xperp ** 2).sum(-1))
  368. testleft = orig_seq[ibeg] + xperp * dist_palp
  369. materialleft = False # Is there already extruded material at the left of the segment
  370. testright = orig_seq[ibeg] - xperp * dist_palp
  371. materialright = False # Is there already extruded material at the right of the segment
  372. if self.vd1.shape[0]:
  373. relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1)
  374. / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
  375. nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
  376. # nearpoints is the array of the nearest points of each segment
  377. # from the point testleft
  378. dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1)
  379. # dist is the array of the squares of the distances between testleft
  380. # and each segment
  381. if np.amin(dist) <= dist_palp * dist_palp:
  382. materialleft = True
  383. # Now the same computation with the point testright at the other side of the
  384. # current segment
  385. relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1)
  386. / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
  387. nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
  388. dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1)
  389. if np.amin(dist) <= dist_palp * dist_palp:
  390. materialright = True
  391. if materialleft and not materialright:
  392. modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
  393. elif not materialleft and materialright:
  394. modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
  395. # Setup part of the stretch plugin
  396. class Stretch(Script):
  397. """
  398. Setup part of the stretch algorithm
  399. The only parameter is the stretch distance
  400. """
  401. def __init__(self):
  402. super().__init__()
  403. def getSettingDataString(self):
  404. return """{
  405. "name":"Post stretch script",
  406. "key": "Stretch",
  407. "metadata": {},
  408. "version": 2,
  409. "settings":
  410. {
  411. "wc_stretch":
  412. {
  413. "label": "Wide circle stretch distance",
  414. "description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
  415. "unit": "mm",
  416. "type": "float",
  417. "default_value": 0.1,
  418. "minimum_value": 0,
  419. "minimum_value_warning": 0,
  420. "maximum_value_warning": 0.2
  421. },
  422. "pw_stretch":
  423. {
  424. "label": "Push Wall stretch distance",
  425. "description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect",
  426. "unit": "mm",
  427. "type": "float",
  428. "default_value": 0.1,
  429. "minimum_value": 0,
  430. "minimum_value_warning": 0,
  431. "maximum_value_warning": 0.2
  432. }
  433. }
  434. }"""
  435. def execute(self, data):
  436. """
  437. Entry point of the plugin.
  438. data is the list of original g-code instructions,
  439. the returned string is the list of modified g-code instructions
  440. """
  441. stretcher = Stretcher(
  442. ExtruderManager.getInstance().getActiveExtruderStack().getProperty("machine_nozzle_size", "value")
  443. , self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
  444. return stretcher.execute(data)