chiark / gitweb /
d5af827edc64f699075871f15e72f94f5b53cdb5
[cura.git] / Cura / skeinforge_application / skeinforge_plugins / craft_plugins / fillet.py
1 """
2 This page is in the table of contents.
3 Fillet rounds the corners slightly in a variety of ways.  This is to reduce corner blobbing and sudden extruder acceleration.
4
5 The fillet manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Fillet
7
8 ==Operation==
9 The default 'Activate Fillet' checkbox is off.  When it is on, the functions described below will work, when it is off, nothing will be done.
10
11 ==Settings==
12 ===Fillet Procedure Choice===
13 Default is 'Bevel''.
14
15 ====Arc Point====
16 When selected, the corners will be filleted with an arc using the gcode point form.
17
18 ====Arc Radius====
19 When selected, the corners will be filleted with an arc using the gcode radius form.
20
21 ====Arc Segment====
22 When selected, the corners will be filleted with an arc composed of several segments.
23
24 ====Bevel====
25 When selected, the corners will be beveled.
26
27 ===Corner Feed Rate Multiplier===
28 Default: 1.0
29
30 Defines the ratio of the feed rate in corners over the original feed rate.  With a high value the extruder will move quickly in corners, accelerating quickly and leaving a thin extrusion.  With a low value, the extruder will move slowly in corners, accelerating gently and leaving a thick extrusion.
31
32 ===Fillet Radius over Perimeter Width===
33 Default is 0.35.
34
35 Defines the width of the fillet.
36
37 ===Reversal Slowdown over Perimeter Width===
38 Default is 0.5.
39
40 Defines how far before a path reversal the extruder will slow down.  Some tools, like nozzle wipe, double back the path of the extruder and this option will add a slowdown point in that path so there won't be a sudden jerk at the end of the path.  If the value is less than 0.1 a slowdown will not be added.
41
42 ===Use Intermediate Feed Rate in Corners===
43 Default is on.
44
45 When selected, the feed rate entering the corner will be the average of the old feed rate and the new feed rate.
46
47 ==Examples==
48 The following examples fillet the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and fillet.py.
49
50 > python fillet.py
51 This brings up the fillet dialog.
52
53 > python fillet.py Screw Holder Bottom.stl
54 The fillet tool is parsing the file:
55 Screw Holder Bottom.stl
56 ..
57 The fillet tool has created the file:
58 .. Screw Holder Bottom_fillet.gcode
59
60 """
61
62 from __future__ import absolute_import
63 #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.
64 import __init__
65
66 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
67 from fabmetheus_utilities.vector3 import Vector3
68 from fabmetheus_utilities import archive
69 from fabmetheus_utilities import euclidean
70 from fabmetheus_utilities import gcodec
71 from fabmetheus_utilities import settings
72 from skeinforge_application.skeinforge_utilities import skeinforge_craft
73 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
74 from skeinforge_application.skeinforge_utilities import skeinforge_profile
75 import math
76 import sys
77
78
79 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
80 __date__ = '$Date: 2008/21/04 $'
81 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
82
83
84 def getCraftedText( fileName, gcodeText, repository = None ):
85         "Fillet a gcode linear move file or text."
86         return getCraftedTextFromText( archive.getTextIfEmpty( fileName, gcodeText ), repository )
87
88 def getCraftedTextFromText( gcodeText, repository = None ):
89         "Fillet a gcode linear move text."
90         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'fillet'):
91                 return gcodeText
92         if repository == None:
93                 repository = settings.getReadRepository( FilletRepository() )
94         if not repository.activateFillet.value:
95                 return gcodeText
96         if repository.arcPoint.value:
97                 return ArcPointSkein().getCraftedGcode( repository, gcodeText )
98         elif repository.arcRadius.value:
99                 return ArcRadiusSkein().getCraftedGcode( repository, gcodeText )
100         elif repository.arcSegment.value:
101                 return ArcSegmentSkein().getCraftedGcode( repository, gcodeText )
102         elif repository.bevel.value:
103                 return BevelSkein().getCraftedGcode( repository, gcodeText )
104         return gcodeText
105
106 def getNewRepository():
107         'Get new repository.'
108         return FilletRepository()
109
110 def writeOutput(fileName, shouldAnalyze=True):
111         "Fillet a gcode linear move file. Depending on the settings, either arcPoint, arcRadius, arcSegment, bevel or do nothing."
112         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'fillet', shouldAnalyze)
113
114
115 class BevelSkein:
116         "A class to bevel a skein of extrusions."
117         def __init__(self):
118                 self.distanceFeedRate = gcodec.DistanceFeedRate()
119                 self.extruderActive = False
120                 self.feedRateMinute = 960.0
121                 self.filletRadius = 0.2
122                 self.lineIndex = 0
123                 self.lines = None
124                 self.oldFeedRateMinute = None
125                 self.oldLocation = None
126                 self.shouldAddLine = True
127
128         def addLinearMovePoint( self, feedRateMinute, point ):
129                 "Add a gcode linear move, feedRate and newline to the output."
130                 self.distanceFeedRate.addLine( self.distanceFeedRate.getLinearGcodeMovementWithFeedRate( feedRateMinute, point.dropAxis(), point.z ) )
131
132         def getCornerFeedRate(self):
133                 "Get the corner feed rate, which may be based on the intermediate feed rate."
134                 feedRateMinute = self.feedRateMinute
135                 if self.repository.useIntermediateFeedRateInCorners.value:
136                         if self.oldFeedRateMinute != None:
137                                 feedRateMinute = 0.5 * ( self.oldFeedRateMinute + self.feedRateMinute )
138                 return feedRateMinute * self.cornerFeedRateMultiplier
139
140         def getCraftedGcode( self, repository, gcodeText ):
141                 "Parse gcode text and store the bevel gcode."
142                 self.cornerFeedRateMultiplier = repository.cornerFeedRateMultiplier.value
143                 self.lines = archive.getTextLines(gcodeText)
144                 self.repository = repository
145                 self.parseInitialization( repository )
146                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
147                         line = self.lines[self.lineIndex]
148                         self.parseLine(line)
149                 return self.distanceFeedRate.output.getvalue()
150
151         def getExtruderOffReversalPoint( self, afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location ):
152                 "If the extruder is off and the path is reversing, add intermediate slow points."
153                 if self.repository.reversalSlowdownDistanceOverEdgeWidth.value < 0.1:
154                         return None
155                 if self.extruderActive:
156                         return None
157                 reversalBufferSlowdownDistance = self.reversalSlowdownDistance * 2.0
158                 afterSegmentComplexLength = abs( afterSegmentComplex )
159                 if afterSegmentComplexLength < reversalBufferSlowdownDistance:
160                         return None
161                 beforeSegmentComplexLength = abs( beforeSegmentComplex )
162                 if beforeSegmentComplexLength < reversalBufferSlowdownDistance:
163                         return None
164                 afterSegmentComplexNormalized = afterSegmentComplex / afterSegmentComplexLength
165                 beforeSegmentComplexNormalized = beforeSegmentComplex / beforeSegmentComplexLength
166                 if euclidean.getDotProduct( afterSegmentComplexNormalized, beforeSegmentComplexNormalized ) < 0.95:
167                         return None
168                 slowdownFeedRate = self.feedRateMinute * 0.5
169                 self.shouldAddLine = False
170                 beforePoint = euclidean.getPointPlusSegmentWithLength( self.reversalSlowdownDistance * abs( beforeSegment ) / beforeSegmentComplexLength, location, beforeSegment )
171                 self.addLinearMovePoint( self.feedRateMinute, beforePoint )
172                 self.addLinearMovePoint( slowdownFeedRate, location )
173                 afterPoint = euclidean.getPointPlusSegmentWithLength( self.reversalSlowdownDistance * abs( afterSegment ) / afterSegmentComplexLength, location, afterSegment )
174                 self.addLinearMovePoint( slowdownFeedRate, afterPoint )
175                 return afterPoint
176
177         def getNextLocation(self):
178                 "Get the next linear move.  Return none is none is found."
179                 for afterIndex in xrange( self.lineIndex + 1, len(self.lines) ):
180                         line = self.lines[ afterIndex ]
181                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
182                         if gcodec.getFirstWord(splitLine) == 'G1':
183                                 nextLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
184                                 return nextLocation
185                 return None
186
187         def linearMove( self, splitLine ):
188                 "Bevel a linear move."
189                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
190                 self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
191                 if self.oldLocation != None:
192                         nextLocation = self.getNextLocation()
193                         if nextLocation != None:
194                                 location = self.splitPointGetAfter( location, nextLocation )
195                 self.oldLocation = location
196                 self.oldFeedRateMinute = self.feedRateMinute
197
198         def parseInitialization( self, repository ):
199                 'Parse gcode initialization and store the parameters.'
200                 for self.lineIndex in xrange(len(self.lines)):
201                         line = self.lines[self.lineIndex]
202                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
203                         firstWord = gcodec.getFirstWord(splitLine)
204                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
205                         if firstWord == '(</extruderInitialization>)':
206                                 self.distanceFeedRate.addTagBracketedProcedure('fillet')
207                                 return
208                         elif firstWord == '(<edgeWidth>':
209                                 edgeWidth = abs(float(splitLine[1]))
210                                 self.curveSection = 0.7 * edgeWidth
211                                 self.filletRadius = edgeWidth * repository.filletRadiusOverEdgeWidth.value
212                                 self.minimumRadius = 0.1 * edgeWidth
213                                 self.reversalSlowdownDistance = edgeWidth * repository.reversalSlowdownDistanceOverEdgeWidth.value
214                         self.distanceFeedRate.addLine(line)
215
216         def parseLine(self, line):
217                 "Parse a gcode line and add it to the bevel gcode."
218                 self.shouldAddLine = True
219                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
220                 if len(splitLine) < 1:
221                         return
222                 firstWord = splitLine[0]
223                 if firstWord == 'G1':
224                         self.linearMove(splitLine)
225                 elif firstWord == 'M101':
226                         self.extruderActive = True
227                 elif firstWord == 'M103':
228                         self.extruderActive = False
229                 if self.shouldAddLine:
230                         self.distanceFeedRate.addLine(line)
231
232         def splitPointGetAfter( self, location, nextLocation ):
233                 "Bevel a point and return the end of the bevel.   should get complex for radius"
234                 if self.filletRadius < 2.0 * self.minimumRadius:
235                         return location
236                 afterSegment = nextLocation - location
237                 afterSegmentComplex = afterSegment.dropAxis()
238                 afterSegmentComplexLength = abs( afterSegmentComplex )
239                 thirdAfterSegmentLength = 0.333 * afterSegmentComplexLength
240                 if thirdAfterSegmentLength < self.minimumRadius:
241                         return location
242                 beforeSegment = self.oldLocation - location
243                 beforeSegmentComplex = beforeSegment.dropAxis()
244                 beforeSegmentComplexLength = abs( beforeSegmentComplex )
245                 thirdBeforeSegmentLength = 0.333 * beforeSegmentComplexLength
246                 if thirdBeforeSegmentLength < self.minimumRadius:
247                         return location
248                 extruderOffReversalPoint = self.getExtruderOffReversalPoint( afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location )
249                 if extruderOffReversalPoint != None:
250                         return extruderOffReversalPoint
251                 bevelRadius = min( thirdAfterSegmentLength, self.filletRadius )
252                 bevelRadius = min( thirdBeforeSegmentLength, bevelRadius )
253                 self.shouldAddLine = False
254                 beforePoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( beforeSegment ) / beforeSegmentComplexLength, location, beforeSegment )
255                 self.addLinearMovePoint( self.feedRateMinute, beforePoint )
256                 afterPoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( afterSegment ) / afterSegmentComplexLength, location, afterSegment )
257                 self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
258                 return afterPoint
259
260
261 class ArcSegmentSkein( BevelSkein ):
262         "A class to arc segment a skein of extrusions."
263         def addArc( self, afterCenterDifferenceAngle, afterPoint, beforeCenterSegment, beforePoint, center ):
264                 "Add arc segments to the filleted skein."
265                 absoluteDifferenceAngle = abs( afterCenterDifferenceAngle )
266 #               steps = int( math.ceil( absoluteDifferenceAngle * 1.5 ) )
267                 steps = int( math.ceil( min( absoluteDifferenceAngle * 1.5, absoluteDifferenceAngle * abs( beforeCenterSegment ) / self.curveSection ) ) )
268                 stepPlaneAngle = euclidean.getWiddershinsUnitPolar( afterCenterDifferenceAngle / steps )
269                 for step in xrange( 1, steps ):
270                         beforeCenterSegment = euclidean.getRoundZAxisByPlaneAngle( stepPlaneAngle, beforeCenterSegment )
271                         arcPoint = center + beforeCenterSegment
272                         self.addLinearMovePoint( self.getCornerFeedRate(), arcPoint )
273                 self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
274
275         def splitPointGetAfter( self, location, nextLocation ):
276                 "Fillet a point into arc segments and return the end of the last segment."
277                 if self.filletRadius < 2.0 * self.minimumRadius:
278                         return location
279                 afterSegment = nextLocation - location
280                 afterSegmentComplex = afterSegment.dropAxis()
281                 thirdAfterSegmentLength = 0.333 * abs( afterSegmentComplex )
282                 if thirdAfterSegmentLength < self.minimumRadius:
283                         return location
284                 beforeSegment = self.oldLocation - location
285                 beforeSegmentComplex = beforeSegment.dropAxis()
286                 thirdBeforeSegmentLength = 0.333 * abs( beforeSegmentComplex )
287                 if thirdBeforeSegmentLength < self.minimumRadius:
288                         return location
289                 extruderOffReversalPoint = self.getExtruderOffReversalPoint( afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location )
290                 if extruderOffReversalPoint != None:
291                         return extruderOffReversalPoint
292                 bevelRadius = min( thirdAfterSegmentLength, self.filletRadius )
293                 bevelRadius = min( thirdBeforeSegmentLength, bevelRadius )
294                 self.shouldAddLine = False
295                 beforePoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( beforeSegment ) / abs( beforeSegmentComplex ), location, beforeSegment )
296                 self.addLinearMovePoint( self.feedRateMinute, beforePoint )
297                 afterPoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( afterSegment ) / abs( afterSegmentComplex ), location, afterSegment )
298                 afterPointComplex = afterPoint.dropAxis()
299                 beforePointComplex = beforePoint.dropAxis()
300                 locationComplex = location.dropAxis()
301                 midpoint = 0.5 * ( afterPoint + beforePoint )
302                 midpointComplex = midpoint.dropAxis()
303                 midpointMinusLocationComplex = midpointComplex - locationComplex
304                 midpointLocationLength = abs( midpointMinusLocationComplex )
305                 if midpointLocationLength < 0.01 * self.filletRadius:
306                         self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
307                         return afterPoint
308                 midpointAfterPointLength = abs( midpointComplex - afterPointComplex )
309                 midpointCenterLength = midpointAfterPointLength * midpointAfterPointLength / midpointLocationLength
310                 radius = math.sqrt( midpointCenterLength * midpointCenterLength + midpointAfterPointLength * midpointAfterPointLength )
311                 centerComplex = midpointComplex + midpointMinusLocationComplex * midpointCenterLength / midpointLocationLength
312                 center = Vector3( centerComplex.real, centerComplex.imag, midpoint.z )
313                 afterCenterComplex = afterPointComplex - centerComplex
314                 beforeCenter = beforePoint - center
315                 angleDifference = euclidean.getAngleDifferenceByComplex( afterCenterComplex, beforeCenter.dropAxis() )
316                 self.addArc( angleDifference, afterPoint, beforeCenter, beforePoint, center )
317                 return afterPoint
318
319
320 class ArcPointSkein( ArcSegmentSkein ):
321         "A class to arc point a skein of extrusions."
322         def addArc( self, afterCenterDifferenceAngle, afterPoint, beforeCenterSegment, beforePoint, center ):
323                 "Add an arc point to the filleted skein."
324                 if afterCenterDifferenceAngle == 0.0:
325                         return
326                 afterPointMinusBefore = afterPoint - beforePoint
327                 centerMinusBefore = center - beforePoint
328                 firstWord = 'G3'
329                 if afterCenterDifferenceAngle < 0.0:
330                         firstWord = 'G2'
331                 centerMinusBeforeComplex = centerMinusBefore.dropAxis()
332                 if abs( centerMinusBeforeComplex ) <= 0.0:
333                         return
334                 radius = abs( centerMinusBefore )
335                 arcDistanceZ = complex( abs( afterCenterDifferenceAngle ) * radius, afterPointMinusBefore.z )
336                 distance = abs( arcDistanceZ )
337                 if distance <= 0.0:
338                         return
339                 line = self.distanceFeedRate.getFirstWordMovement( firstWord, afterPointMinusBefore ) + self.getRelativeCenter( centerMinusBeforeComplex )
340                 cornerFeedRate = self.getCornerFeedRate()
341                 if cornerFeedRate != None:
342                         line += ' F' + self.distanceFeedRate.getRounded(cornerFeedRate)
343                 self.distanceFeedRate.addLine(line)
344
345         def getRelativeCenter( self, centerMinusBeforeComplex ):
346                 "Get the relative center."
347                 return ' I%s J%s' % ( self.distanceFeedRate.getRounded( centerMinusBeforeComplex.real ), self.distanceFeedRate.getRounded( centerMinusBeforeComplex.imag ) )
348
349
350 class ArcRadiusSkein( ArcPointSkein ):
351         "A class to arc radius a skein of extrusions."
352         def getRelativeCenter( self, centerMinusBeforeComplex ):
353                 "Get the relative center."
354                 radius = abs( centerMinusBeforeComplex )
355                 return ' R' + ( self.distanceFeedRate.getRounded(radius) )
356
357
358 class FilletRepository:
359         "A class to handle the fillet settings."
360         def __init__(self):
361                 "Set the default settings, execute title & settings fileName."
362                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.fillet.html', self )
363                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File to be Filleted', self, '')
364                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Fillet')
365                 self.activateFillet = settings.BooleanSetting().getFromValue('Activate Fillet', self, False )
366                 self.filletProcedureChoiceLabel = settings.LabelDisplay().getFromName('Fillet Procedure Choice: ', self )
367                 filletLatentStringVar = settings.LatentStringVar()
368                 self.arcPoint = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Point', self, False )
369                 self.arcRadius = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Radius', self, False )
370                 self.arcSegment = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Segment', self, False )
371                 self.bevel = settings.Radio().getFromRadio( filletLatentStringVar, 'Bevel', self, True )
372                 self.cornerFeedRateMultiplier = settings.FloatSpin().getFromValue(0.8, 'Corner Feed Rate Multiplier (ratio):', self, 1.2, 1.0)
373                 self.filletRadiusOverEdgeWidth = settings.FloatSpin().getFromValue( 0.25, 'Fillet Radius over Perimeter Width (ratio):', self, 0.65, 0.35 )
374                 self.reversalSlowdownDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 0.3, 'Reversal Slowdown Distance over Perimeter Width (ratio):', self, 0.7, 0.5 )
375                 self.useIntermediateFeedRateInCorners = settings.BooleanSetting().getFromValue('Use Intermediate Feed Rate in Corners', self, True )
376                 self.executeTitle = 'Fillet'
377
378         def execute(self):
379                 "Fillet button has been clicked."
380                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
381                 for fileName in fileNames:
382                         writeOutput(fileName)
383
384
385 def main():
386         "Display the fillet dialog."
387         if len(sys.argv) > 1:
388                 writeOutput(' '.join(sys.argv[1 :]))
389         else:
390                 settings.startMainLoopFromConstructor(getNewRepository())
391
392 if __name__ == "__main__":
393         main()