Stretch.py 25 KB

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