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