"""
This page is in the table of contents.
Fillet rounds the corners slightly in a variety of ways. This is to reduce corner blobbing and sudden extruder acceleration.
The fillet manual page is at:
http://www.bitsfrombytes.com/wiki/index.php?title=Skeinforge_Fillet
==Operation==
The default 'Activate Fillet' checkbox is off. When it is on, the functions described below will work, when it is off, the functions will not be called.
==Settings==
===Fillet Procedure Choice===
Default is 'Bevel''.
====Arc Point====
When selected, the corners will be filleted with an arc using the gcode point form.
====Arc Radius====
When selected, the corners will be filleted with an arc using the gcode radius form.
====Arc Segment====
When selected, the corners will be filleted with an arc composed of several segments.
====Bevel====
When selected, the corners will be beveled.
===Corner Feed Rate over Operating Feed Rate===
Default is one.
Defines the ratio of the feed rate in corners over the operating 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.
===Fillet Radius over Perimeter Width===
Default is 0.35.
Defines the width of the fillet.
===Reversal Slowdown over Perimeter Width===
Default is 0.5.
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.
===Use Intermediate Feed Rate in Corners===
Default is on.
When selected, the feed rate entering the corner will be the average of the old feed rate and the new feed rate.
==Examples==
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.
> python fillet.py
This brings up the fillet dialog.
> python fillet.py Screw Holder Bottom.stl
The fillet tool is parsing the file:
Screw Holder Bottom.stl
..
The fillet tool has created the file:
.. Screw Holder Bottom_fillet.gcode
> python
Python 2.5.1 (r251:54863, Sep 22 2007, 01:43:31)
[GCC 4.2.1 (SUSE Linux)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import fillet
>>> fillet.main()
This brings up the fillet dialog.
>>> fillet.writeOutput( 'Screw Holder Bottom.stl' )
The fillet tool is parsing the file:
Screw Holder Bottom.stl
..
The fillet tool has created the file:
.. Screw Holder Bottom_fillet.gcode
"""
from __future__ import absolute_import
#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.
import __init__
from skeinforge_tools import profile
from skeinforge_tools.meta_plugins import polyfile
from skeinforge_tools.skeinforge_utilities import consecution
from skeinforge_tools.skeinforge_utilities import euclidean
from skeinforge_tools.skeinforge_utilities import gcodec
from skeinforge_tools.skeinforge_utilities import interpret
from skeinforge_tools.skeinforge_utilities import settings
from skeinforge_tools.skeinforge_utilities.vector3 import Vector3
import math
import sys
__author__ = "Enrique Perez (perez_enrique@yahoo.com)"
__date__ = "$Date: 2008/21/04 $"
__license__ = "GPL 3.0"
def getCraftedText( fileName, text, filletRepository = None ):
"Fillet a gcode linear move file or text."
return getCraftedTextFromText( gcodec.getTextIfEmpty( fileName, text ), filletRepository )
def getCraftedTextFromText( gcodeText, filletRepository = None ):
"Fillet a gcode linear move text."
if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'fillet' ):
return gcodeText
if filletRepository == None:
filletRepository = settings.getReadRepository( FilletRepository() )
if not filletRepository.activateFillet.value:
return gcodeText
if filletRepository.arcPoint.value:
return ArcPointSkein().getCraftedGcode( filletRepository, gcodeText )
elif filletRepository.arcRadius.value:
return ArcRadiusSkein().getCraftedGcode( filletRepository, gcodeText )
elif filletRepository.arcSegment.value:
return ArcSegmentSkein().getCraftedGcode( filletRepository, gcodeText )
elif filletRepository.bevel.value:
return BevelSkein().getCraftedGcode( filletRepository, gcodeText )
return gcodeText
def getNewRepository():
"Get the repository constructor."
return FilletRepository()
def writeOutput( fileName = '' ):
"Fillet a gcode linear move file. Depending on the settings, either arcPoint, arcRadius, arcSegment, bevel or do nothing."
fileName = interpret.getFirstTranslatorFileNameUnmodified( fileName )
if fileName != '':
consecution.writeChainTextWithNounMessage( fileName, 'fillet' )
class BevelSkein:
"A class to bevel a skein of extrusions."
def __init__( self ):
self.distanceFeedRate = gcodec.DistanceFeedRate()
self.extruderActive = False
self.feedRateMinute = 960.0
self.filletRadius = 0.2
self.lineIndex = 0
self.lines = None
self.oldFeedRateMinute = None
self.oldLocation = None
self.shouldAddLine = True
def addLinearMovePoint( self, feedRateMinute, point ):
"Add a gcode linear move, feedRate and newline to the output."
self.distanceFeedRate.addLine( self.distanceFeedRate.getLinearGcodeMovementWithFeedRate( feedRateMinute, point.dropAxis( 2 ), point.z ) )
def getCornerFeedRate( self ):
"Get the corner feed rate, which may be based on the intermediate feed rate."
feedRateMinute = self.feedRateMinute
if self.filletRepository.useIntermediateFeedRateInCorners.value:
if self.oldFeedRateMinute != None:
feedRateMinute = 0.5 * ( self.oldFeedRateMinute + self.feedRateMinute )
return feedRateMinute * self.cornerFeedRateOverOperatingFeedRate
def getCraftedGcode( self, filletRepository, gcodeText ):
"Parse gcode text and store the bevel gcode."
self.cornerFeedRateOverOperatingFeedRate = filletRepository.cornerFeedRateOverOperatingFeedRate.value
self.lines = gcodec.getTextLines( gcodeText )
self.filletRepository = filletRepository
self.parseInitialization( filletRepository )
for self.lineIndex in xrange( self.lineIndex, len( self.lines ) ):
line = self.lines[ self.lineIndex ]
self.parseLine( line )
return self.distanceFeedRate.output.getvalue()
def getExtruderOffReversalPoint( self, afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location ):
"If the extruder is off and the path is reversing, add intermediate slow points."
if self.filletRepository.reversalSlowdownDistanceOverPerimeterWidth.value < 0.1:
return None
if self.extruderActive:
return None
reversalBufferSlowdownDistance = self.reversalSlowdownDistance * 2.0
afterSegmentComplexLength = abs( afterSegmentComplex )
if afterSegmentComplexLength < reversalBufferSlowdownDistance:
return None
beforeSegmentComplexLength = abs( beforeSegmentComplex )
if beforeSegmentComplexLength < reversalBufferSlowdownDistance:
return None
afterSegmentComplexNormalized = afterSegmentComplex / afterSegmentComplexLength
beforeSegmentComplexNormalized = beforeSegmentComplex / beforeSegmentComplexLength
if euclidean.getDotProduct( afterSegmentComplexNormalized, beforeSegmentComplexNormalized ) < 0.95:
return None
slowdownFeedRate = self.feedRateMinute * 0.5
self.shouldAddLine = False
beforePoint = euclidean.getPointPlusSegmentWithLength( self.reversalSlowdownDistance * abs( beforeSegment ) / beforeSegmentComplexLength, location, beforeSegment )
self.addLinearMovePoint( self.feedRateMinute, beforePoint )
self.addLinearMovePoint( slowdownFeedRate, location )
afterPoint = euclidean.getPointPlusSegmentWithLength( self.reversalSlowdownDistance * abs( afterSegment ) / afterSegmentComplexLength, location, afterSegment )
self.addLinearMovePoint( slowdownFeedRate, afterPoint )
return afterPoint
def getNextLocation( self ):
"Get the next linear move. Return none is none is found."
for afterIndex in xrange( self.lineIndex + 1, len( self.lines ) ):
line = self.lines[ afterIndex ]
splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
if gcodec.getFirstWord( splitLine ) == 'G1':
nextLocation = gcodec.getLocationFromSplitLine( self.oldLocation, splitLine )
return nextLocation
return None
def linearMove( self, splitLine ):
"Bevel a linear move."
location = gcodec.getLocationFromSplitLine( self.oldLocation, splitLine )
self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
if self.oldLocation != None:
nextLocation = self.getNextLocation()
if nextLocation != None:
location = self.splitPointGetAfter( location, nextLocation )
self.oldLocation = location
self.oldFeedRateMinute = self.feedRateMinute
def parseInitialization( self, filletRepository ):
"Parse gcode initialization and store the parameters."
for self.lineIndex in xrange( len( self.lines ) ):
line = self.lines[ self.lineIndex ]
splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
firstWord = gcodec.getFirstWord( splitLine )
self.distanceFeedRate.parseSplitLine( firstWord, splitLine )
if firstWord == '()':
self.distanceFeedRate.addLine( '( fillet )' )
return
elif firstWord == '(':
perimeterWidth = abs( float( splitLine[ 1 ] ) )
self.curveSection = 0.7 * perimeterWidth
self.filletRadius = perimeterWidth * filletRepository.filletRadiusOverPerimeterWidth.value
self.minimumRadius = 0.1 * perimeterWidth
self.reversalSlowdownDistance = perimeterWidth * filletRepository.reversalSlowdownDistanceOverPerimeterWidth.value
self.distanceFeedRate.addLine( line )
def parseLine( self, line ):
"Parse a gcode line and add it to the bevel gcode."
self.shouldAddLine = True
splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
if len( splitLine ) < 1:
return
firstWord = splitLine[ 0 ]
if firstWord == 'G1':
self.linearMove( splitLine )
elif firstWord == 'M101':
self.extruderActive = True
elif firstWord == 'M103':
self.extruderActive = False
if self.shouldAddLine:
self.distanceFeedRate.addLine( line )
def splitPointGetAfter( self, location, nextLocation ):
"Bevel a point and return the end of the bevel. should get complex for radius"
if self.filletRadius < 2.0 * self.minimumRadius:
return location
afterSegment = nextLocation - location
afterSegmentComplex = afterSegment.dropAxis( 2 )
afterSegmentComplexLength = abs( afterSegmentComplex )
thirdAfterSegmentLength = 0.333 * afterSegmentComplexLength
if thirdAfterSegmentLength < self.minimumRadius:
return location
beforeSegment = self.oldLocation - location
beforeSegmentComplex = beforeSegment.dropAxis( 2 )
beforeSegmentComplexLength = abs( beforeSegmentComplex )
thirdBeforeSegmentLength = 0.333 * beforeSegmentComplexLength
if thirdBeforeSegmentLength < self.minimumRadius:
return location
extruderOffReversalPoint = self.getExtruderOffReversalPoint( afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location )
if extruderOffReversalPoint != None:
return extruderOffReversalPoint
bevelRadius = min( thirdAfterSegmentLength, self.filletRadius )
bevelRadius = min( thirdBeforeSegmentLength, bevelRadius )
self.shouldAddLine = False
beforePoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( beforeSegment ) / beforeSegmentComplexLength, location, beforeSegment )
self.addLinearMovePoint( self.feedRateMinute, beforePoint )
afterPoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( afterSegment ) / afterSegmentComplexLength, location, afterSegment )
self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
return afterPoint
class ArcSegmentSkein( BevelSkein ):
"A class to arc segment a skein of extrusions."
def addArc( self, afterCenterDifferenceAngle, afterPoint, beforeCenterSegment, beforePoint, center ):
"Add arc segments to the filleted skein."
absoluteDifferenceAngle = abs( afterCenterDifferenceAngle )
# steps = int( math.ceil( absoluteDifferenceAngle * 1.5 ) )
steps = int( math.ceil( min( absoluteDifferenceAngle * 1.5, absoluteDifferenceAngle * abs( beforeCenterSegment ) / self.curveSection ) ) )
stepPlaneAngle = euclidean.getUnitPolar( afterCenterDifferenceAngle / steps, 1.0 )
for step in xrange( 1, steps ):
beforeCenterSegment = euclidean.getRoundZAxisByPlaneAngle( stepPlaneAngle, beforeCenterSegment )
arcPoint = center + beforeCenterSegment
self.addLinearMovePoint( self.getCornerFeedRate(), arcPoint )
self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
def splitPointGetAfter( self, location, nextLocation ):
"Fillet a point into arc segments and return the end of the last segment."
if self.filletRadius < 2.0 * self.minimumRadius:
return location
afterSegment = nextLocation - location
afterSegmentComplex = afterSegment.dropAxis( 2 )
thirdAfterSegmentLength = 0.333 * abs( afterSegmentComplex )
if thirdAfterSegmentLength < self.minimumRadius:
return location
beforeSegment = self.oldLocation - location
beforeSegmentComplex = beforeSegment.dropAxis( 2 )
thirdBeforeSegmentLength = 0.333 * abs( beforeSegmentComplex )
if thirdBeforeSegmentLength < self.minimumRadius:
return location
extruderOffReversalPoint = self.getExtruderOffReversalPoint( afterSegment, afterSegmentComplex, beforeSegment, beforeSegmentComplex, location )
if extruderOffReversalPoint != None:
return extruderOffReversalPoint
bevelRadius = min( thirdAfterSegmentLength, self.filletRadius )
bevelRadius = min( thirdBeforeSegmentLength, bevelRadius )
self.shouldAddLine = False
beforePoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( beforeSegment ) / abs( beforeSegmentComplex ), location, beforeSegment )
self.addLinearMovePoint( self.feedRateMinute, beforePoint )
afterPoint = euclidean.getPointPlusSegmentWithLength( bevelRadius * abs( afterSegment ) / abs( afterSegmentComplex ), location, afterSegment )
afterPointComplex = afterPoint.dropAxis( 2 )
beforePointComplex = beforePoint.dropAxis( 2 )
locationComplex = location.dropAxis( 2 )
midPoint = 0.5 * ( afterPoint + beforePoint )
midPointComplex = midPoint.dropAxis( 2 )
midPointMinusLocationComplex = midPointComplex - locationComplex
midPointLocationLength = abs( midPointMinusLocationComplex )
if midPointLocationLength < 0.01 * self.filletRadius:
self.addLinearMovePoint( self.getCornerFeedRate(), afterPoint )
return afterPoint
midPointAfterPointLength = abs( midPointComplex - afterPointComplex )
midPointCenterLength = midPointAfterPointLength * midPointAfterPointLength / midPointLocationLength
radius = math.sqrt( midPointCenterLength * midPointCenterLength + midPointAfterPointLength * midPointAfterPointLength )
centerComplex = midPointComplex + midPointMinusLocationComplex * midPointCenterLength / midPointLocationLength
center = Vector3( centerComplex.real, centerComplex.imag, midPoint.z )
afterCenterComplex = afterPointComplex - centerComplex
beforeMinusCenterCenterComplex = beforePointComplex - centerComplex
beforeCenter = beforePoint - center
beforeCenterComplex = beforeCenter.dropAxis( 2 )
subtractComplexMirror = complex( beforeCenterComplex.real , - beforeCenterComplex.imag )
differenceComplex = subtractComplexMirror * afterCenterComplex
differenceAngle = math.atan2( differenceComplex.imag, differenceComplex.real )
self.addArc( differenceAngle, afterPoint, beforeCenter, beforePoint, center )
return afterPoint
class ArcPointSkein( ArcSegmentSkein ):
"A class to arc point a skein of extrusions."
def addArc( self, afterCenterDifferenceAngle, afterPoint, beforeCenterSegment, beforePoint, center ):
"Add an arc point to the filleted skein."
if afterCenterDifferenceAngle == 0.0:
return
afterPointMinusBefore = afterPoint - beforePoint
centerMinusBefore = center - beforePoint
firstWord = 'G3'
if afterCenterDifferenceAngle < 0.0:
firstWord = 'G2'
centerMinusBeforeComplex = centerMinusBefore.dropAxis( 2 )
if abs( centerMinusBeforeComplex ) <= 0.0:
return
deltaZ = abs( afterPointMinusBefore.z )
radius = abs( centerMinusBefore )
arcDistanceZ = complex( abs( afterCenterDifferenceAngle ) * radius, afterPointMinusBefore.z )
distance = abs( arcDistanceZ )
if distance <= 0.0:
return
line = self.distanceFeedRate.getFirstWordMovement( firstWord, afterPointMinusBefore ) + self.getRelativeCenter( centerMinusBeforeComplex )
cornerFeedRate = self.getCornerFeedRate()
if cornerFeedRate != None:
line += ' F' + self.distanceFeedRate.getRounded( self.distanceFeedRate.getZLimitedFeedRate( deltaZ, distance, cornerFeedRate ) )
self.distanceFeedRate.addLine( line )
def getRelativeCenter( self, centerMinusBeforeComplex ):
"Get the relative center."
return ' I%s J%s' % ( self.distanceFeedRate.getRounded( centerMinusBeforeComplex.real ), self.distanceFeedRate.getRounded( centerMinusBeforeComplex.imag ) )
class ArcRadiusSkein( ArcPointSkein ):
"A class to arc radius a skein of extrusions."
def getRelativeCenter( self, centerMinusBeforeComplex ):
"Get the relative center."
radius = abs( centerMinusBeforeComplex )
return ' R' + ( self.distanceFeedRate.getRounded( radius ) )
class FilletRepository:
"A class to handle the fillet settings."
def __init__( self ):
"Set the default settings, execute title & settings fileName."
settings.addListsToRepository( 'skeinforge_tools.craft_plugins.fillet.html', '', self )
self.fileNameInput = settings.FileNameInput().getFromFileName( interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File to be Filleted', self, '' )
self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute( 'http://www.bitsfrombytes.com/wiki/index.php?title=Skeinforge_Fillet' )
self.activateFillet = settings.BooleanSetting().getFromValue( 'Activate Fillet', self, False )
self.filletProcedureChoiceLabel = settings.LabelDisplay().getFromName( 'Fillet Procedure Choice: ', self )
filletLatentStringVar = settings.LatentStringVar()
self.arcPoint = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Point', self, False )
self.arcRadius = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Radius', self, False )
self.arcSegment = settings.Radio().getFromRadio( filletLatentStringVar, 'Arc Segment', self, False )
self.bevel = settings.Radio().getFromRadio( filletLatentStringVar, 'Bevel', self, True )
self.cornerFeedRateOverOperatingFeedRate = settings.FloatSpin().getFromValue( 0.8, 'Corner Feed Rate over Operating Feed Rate (ratio):', self, 1.2, 1.0 )
self.filletRadiusOverPerimeterWidth = settings.FloatSpin().getFromValue( 0.25, 'Fillet Radius over Perimeter Width (ratio):', self, 0.65, 0.35 )
self.reversalSlowdownDistanceOverPerimeterWidth = settings.FloatSpin().getFromValue( 0.3, 'Reversal Slowdown Distance over Perimeter Width (ratio):', self, 0.7, 0.5 )
self.useIntermediateFeedRateInCorners = settings.BooleanSetting().getFromValue( 'Use Intermediate Feed Rate in Corners', self, True )
self.executeTitle = 'Fillet'
def execute( self ):
"Fillet button has been clicked."
fileNames = polyfile.getFileOrDirectoryTypesUnmodifiedGcode( self.fileNameInput.value, interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled )
for fileName in fileNames:
writeOutput( fileName )
def main():
"Display the fillet dialog."
if len( sys.argv ) > 1:
writeOutput( ' '.join( sys.argv[ 1 : ] ) )
else:
settings.startMainLoopFromConstructor( getNewRepository() )
if __name__ == "__main__":
main()