2 This page is in the table of contents.
3 This plugin smooths jagged extruder paths. It takes shortcuts through jagged paths and decreases the feed rate to compensate.
5 Smooth is based on ideas in Nophead's frequency limit post:
7 http://hydraraptor.blogspot.com/2010/12/frequency-limit.html
9 The smooth manual page is at:
10 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Smooth
13 The default 'Activate Smooth' checkbox is off. When it is on, the functions described below will work, when it is off, nothing will be done.
19 Defines which layer of the print the smoothing process starts from. If this is set this to zero, that might cause the smoothed parts of the bottom edge not to adhere well to the print surface. However, this is just a potential problem in theory, no bottom adhesion problem has been reported.
21 ===Maximum Shortening over Width===
24 Defines the maximum shortening of the shortcut compared to the original path. Smooth goes over the path and if the shortcut between the midpoint of one line and the midpoint of the second line after is not too short compared to the original and the shortcut is not too long, it replaces the jagged original with the shortcut. If the maximum shortening is too much, smooth will shorten paths which should not of been shortened and will leave blobs and holes in the model. If the maximum shortening is too little, even jagged paths that could be shortened safely won't be smoothed.
27 The following examples smooth the file Screw Holder Bottom.stl. The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and smooth.py.
30 This brings up the smooth dialog.
32 > python smooth.py Screw Holder Bottom.stl
33 The smooth tool is parsing the file:
34 Screw Holder Bottom.stl
36 The smooth tool has created the file:
37 .. Screw Holder Bottom_smooth.gcode
41 from __future__ import absolute_import
42 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
45 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
46 from fabmetheus_utilities import archive
47 from fabmetheus_utilities import euclidean
48 from fabmetheus_utilities import gcodec
49 from fabmetheus_utilities import settings
50 from skeinforge_application.skeinforge_utilities import skeinforge_craft
51 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
52 from skeinforge_application.skeinforge_utilities import skeinforge_profile
56 __author__ = 'Enrique Perez (perez_enrique aht yahoo.com) & James Blackwell (jim_blag ahht hotmail.com)'
57 __date__ = '$Date: 2008/21/04 $'
58 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
61 def getCraftedText(fileName, gcodeText, repository=None):
62 'Smooth a gcode linear move text.'
63 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
65 def getCraftedTextFromText(gcodeText, repository=None):
66 'Smooth a gcode linear move text.'
67 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'smooth'):
69 if repository == None:
70 repository = settings.getReadRepository(SmoothRepository())
71 if not repository.activateSmooth.value:
73 return SmoothSkein().getCraftedGcode(gcodeText, repository)
75 def getNewRepository():
77 return SmoothRepository()
79 def writeOutput(fileName, shouldAnalyze=True):
80 'Smooth a gcode linear move file. Chain smooth the gcode if it is not already smoothed.'
81 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'smooth', shouldAnalyze)
84 class SmoothRepository:
85 'A class to handle the smooth settings.'
87 'Set the default settings, execute title & settings fileName.'
88 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.smooth.html', self )
89 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Smooth', self, '')
90 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Smooth')
91 self.activateSmooth = settings.BooleanSetting().getFromValue('Activate Smooth', self, False)
92 self.layersFrom = settings.IntSpin().getSingleIncrementFromValue(0, 'Layers From (index):', self, 912345678, 1)
93 self.maximumShorteningOverWidth = settings.FloatSpin().getFromValue(0.2, 'Maximum Shortening over Width (float):', self, 2.0, 1.2)
94 self.executeTitle = 'Smooth'
97 'Smooth button has been clicked.'
98 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
99 for fileName in fileNames:
100 writeOutput(fileName)
104 'A class to smooth a skein of extrusions.'
107 self.boundaryLayerIndex = -1
108 self.distanceFeedRate = gcodec.DistanceFeedRate()
109 self.feedRateMinute = 959.0
111 self.layerCount = settings.LayerCount()
114 self.oldLocation = None
115 self.travelFeedRateMinute = 957.0
117 def addSmoothedInfill(self):
118 'Add smoothed infill.'
119 if len(self.infill) < 4:
120 self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, self.infill, self.travelFeedRateMinute, self.oldLocation.z)
122 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.travelFeedRateMinute, self.infill[0], self.oldLocation.z)
123 self.distanceFeedRate.addLine('M101')
124 lengthMinusOne = len(self.infill) - 1
125 lengthMinusTwo = lengthMinusOne - 1
126 wasOriginalPoint = True
128 while pointIndex < lengthMinusOne:
129 nextPoint = self.infill[pointIndex + 1]
130 afterNextIndex = pointIndex + 2
131 if afterNextIndex < lengthMinusTwo:
132 point = self.infill[pointIndex]
133 midpoint = 0.5 * (point + nextPoint)
134 afterNextPoint = self.infill[afterNextIndex]
135 afterNextNextPoint = self.infill[afterNextIndex + 1]
136 afterNextMidpoint = 0.5 * (afterNextPoint + afterNextNextPoint)
137 shortcutDistance = abs(afterNextMidpoint - midpoint)
138 originalDistance = abs(midpoint - point) + abs(afterNextPoint - nextPoint) + abs(afterNextMidpoint - afterNextPoint)
139 segment = euclidean.getNormalized(nextPoint - point)
140 afterNextSegment = euclidean.getNormalized(afterNextNextPoint - afterNextPoint)
141 sameDirection = self.getIsParallelToRotation(segment) and self.getIsParallelToRotation(afterNextSegment)
142 if originalDistance - shortcutDistance < self.maximumShortening and shortcutDistance < self.maximumDistance and sameDirection:
144 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.feedRateMinute, midpoint, self.oldLocation.z)
145 feedrate = self.feedRateMinute
146 if originalDistance != 0.0:
147 feedrate *= shortcutDistance / originalDistance
148 self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedrate, afterNextMidpoint, self.oldLocation.z)
149 wasOriginalPoint = False
152 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.feedRateMinute, nextPoint, self.oldLocation.z)
153 wasOriginalPoint = True
155 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.feedRateMinute, nextPoint, self.oldLocation.z)
156 wasOriginalPoint = True
158 self.distanceFeedRate.addLine('M103')
160 def getCraftedGcode( self, gcodeText, repository ):
161 'Parse gcode text and store the smooth gcode.'
162 self.lines = archive.getTextLines(gcodeText)
163 self.repository = repository
164 self.layersFromBottom = repository.layersFrom.value
165 self.parseInitialization()
166 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
167 line = self.lines[self.lineIndex]
169 return self.distanceFeedRate.output.getvalue()
171 def getIsParallelToRotation(self, segment):
172 'Determine if the segment is parallel to the rotation.'
173 return abs(euclidean.getDotProduct(segment, self.rotation)) > 0.99999
175 def parseInitialization(self):
176 'Parse gcode initialization and store the parameters.'
177 for self.lineIndex in xrange(len(self.lines)):
178 line = self.lines[self.lineIndex]
179 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
180 firstWord = gcodec.getFirstWord(splitLine)
181 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
182 if firstWord == '(</extruderInitialization>)':
183 self.distanceFeedRate.addTagBracketedProcedure('smooth')
185 elif firstWord == '(<infillWidth>':
186 self.infillWidth = float(splitLine[1])
187 self.maximumShortening = self.repository.maximumShorteningOverWidth.value * self.infillWidth
188 self.maximumDistance = 1.5 * self.maximumShortening
189 elif firstWord == '(<travelFeedRatePerSecond>':
190 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
191 self.distanceFeedRate.addLine(line)
193 def parseLine(self, line):
194 'Parse a gcode line and add it to the smooth skein.'
195 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
196 if len(splitLine) < 1:
198 firstWord = splitLine[0]
199 if firstWord == '(<boundaryPerimeter>)':
200 if self.boundaryLayerIndex < 0:
201 self.boundaryLayerIndex = 0
202 elif firstWord == 'G1':
203 self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
204 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
205 self.oldLocation = location
206 if self.infill != None:
207 self.infill.append(location.dropAxis())
209 elif firstWord == '(<infill>)':
210 if self.boundaryLayerIndex >= self.layersFromBottom:
212 elif firstWord == '(</infill>)':
214 elif firstWord == '(<layer>':
215 self.layerCount.printProgressIncrement('smooth')
216 if self.boundaryLayerIndex >= 0:
217 self.boundaryLayerIndex += 1
218 elif firstWord == 'M101':
219 if self.infill != None:
220 if len(self.infill) > 1:
221 self.infill = [self.infill[0]]
223 elif firstWord == 'M103':
224 if self.infill != None:
225 self.addSmoothedInfill()
228 elif firstWord == '(<rotation>':
229 self.rotation = gcodec.getRotationBySplitLine(splitLine)
230 self.distanceFeedRate.addLine(line)
234 'Display the smooth dialog.'
235 if len(sys.argv) > 1:
236 writeOutput(' '.join(sys.argv[1 :]))
238 settings.startMainLoopFromConstructor(getNewRepository())
240 if __name__ == '__main__':