chiark / gitweb /
Move SF into its own directory, to seperate SF and Cura. Rename newui to gui.
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / smooth.py
1 """
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.
4
5 Smooth is based on ideas in Nophead's frequency limit post: 
6
7 http://hydraraptor.blogspot.com/2010/12/frequency-limit.html
8
9 The smooth manual page is at:
10 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Smooth
11
12 ==Operation==
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.
14
15 ==Settings==
16 ===Layers From===
17 Default: 1
18
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. 
20
21 ===Maximum Shortening over Width===
22 Default: 1.2
23
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.
25
26 ==Examples==
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.
28
29 > python smooth.py
30 This brings up the smooth dialog.
31
32 > python smooth.py Screw Holder Bottom.stl
33 The smooth tool is parsing the file:
34 Screw Holder Bottom.stl
35 ..
36 The smooth tool has created the file:
37 .. Screw Holder Bottom_smooth.gcode
38
39 """
40
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.
43 import __init__
44
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
53 import sys
54
55
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'
59
60
61 def getCraftedText(fileName, gcodeText, repository=None):
62         'Smooth a gcode linear move text.'
63         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
64
65 def getCraftedTextFromText(gcodeText, repository=None):
66         'Smooth a gcode linear move text.'
67         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'smooth'):
68                 return gcodeText
69         if repository == None:
70                 repository = settings.getReadRepository(SmoothRepository())
71         if not repository.activateSmooth.value:
72                 return gcodeText
73         return SmoothSkein().getCraftedGcode(gcodeText, repository)
74
75 def getNewRepository():
76         'Get new repository.'
77         return SmoothRepository()
78
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)
82
83
84 class SmoothRepository:
85         'A class to handle the smooth settings.'
86         def __init__(self):
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'
95
96         def execute(self):
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)
101
102
103 class SmoothSkein:
104         'A class to smooth a skein of extrusions.'
105         def __init__(self):
106                 'Initialize.'
107                 self.boundaryLayerIndex = -1
108                 self.distanceFeedRate = gcodec.DistanceFeedRate()
109                 self.feedRateMinute = 959.0
110                 self.infill = None
111                 self.layerCount = settings.LayerCount()
112                 self.lineIndex = 0
113                 self.lines = None
114                 self.oldLocation = None
115                 self.travelFeedRateMinute = 957.0
116
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)
121                         return
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
127                 pointIndex = 0
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:
143                                         if wasOriginalPoint:
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
150                                         pointIndex += 1
151                                 else:
152                                         self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.feedRateMinute, nextPoint, self.oldLocation.z)
153                                         wasOriginalPoint = True
154                         else:
155                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.feedRateMinute, nextPoint, self.oldLocation.z)
156                                 wasOriginalPoint = True
157                         pointIndex += 1
158                 self.distanceFeedRate.addLine('M103')
159
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]
168                         self.parseLine(line)
169                 return self.distanceFeedRate.output.getvalue()
170
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
174
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')
184                                 return
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)
192
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:
197                         return
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())
208                                 return
209                 elif firstWord == '(<infill>)':
210                         if self.boundaryLayerIndex >= self.layersFromBottom:
211                                 self.infill = []
212                 elif firstWord == '(</infill>)':
213                         self.infill = None
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]]
222                                 return
223                 elif firstWord == 'M103':
224                         if self.infill != None:
225                                 self.addSmoothedInfill()
226                                 self.infill = []
227                                 return
228                 elif firstWord == '(<rotation>':
229                         self.rotation = gcodec.getRotationBySplitLine(splitLine)
230                 self.distanceFeedRate.addLine(line)
231
232
233 def main():
234         'Display the smooth dialog.'
235         if len(sys.argv) > 1:
236                 writeOutput(' '.join(sys.argv[1 :]))
237         else:
238                 settings.startMainLoopFromConstructor(getNewRepository())
239
240 if __name__ == '__main__':
241         main()