""" Carve shape is a script to carve a list of slice layers. Carve carves a list of slices into svg slice layers. The 'Layer Thickness' is the thickness the extrusion layer at default extruder speed, this is the most important carve preference. The 'Extrusion Width over Thickness' is the ratio of the extrusion width over the layer thickness, the default is 1.5. A ratio of one means the extrusion is a circle, a typical ratio of 1.5 means the extrusion is a wide oval. These values should be measured from a test extrusion line. Rarely changed preferences are 'Import Coarseness', 'Mesh Type', 'Infill Bridge Width Over Thickness', 'Infill in Direction of Bridges' & 'Layer Thickness over Precision'. When a triangle mesh has holes in it, the triangle mesh slicer switches over to a slow algorithm that spans gaps in the mesh. The higher the import coarseness, the wider the gaps in the mesh it will span. An import coarseness of one means it will span gaps the width of the extrusion. When the Mesh Type preference is Correct Mesh, the mesh will be accurately carved, and if a hole is found, carve will switch over to the algorithm that spans gaps. If the Mesh Type preference is Unproven Mesh, carve will use the gap spanning algorithm from the start. The problem with the gap spanning algothm is that it will span gaps, even if there actually is a gap in the model. Infill bridge width over thickness ratio is the ratio of the extrusion width over the layer thickness on a bridge layer. If the infill in direction of bridges preference is chosen, the infill will be in the direction of bridges across gaps, so that the fill will be able to span a bridge easier. The 'Layer Thickness over Precision' is the ratio of the layer thickness over the smallest change in value. The higher the layer thickness over precision, the more significant figures the output numbers will have, the default is ten. To run carve, in a shell type: > python carve.py The following examples carve the GNU Triangulated Surface file Screw Holder Bottom.stl. The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and carve.py. The preferences can be set in the dialog or by changing the preferences file 'carve.csv' with a text editor or a spreadsheet program set to separate tabs. > python carve.py This brings up the dialog, after clicking 'Carve', the following is printed: File Screw Holder Bottom.stl is being carved. The carved file is saved as Screw Holder Bottom_carve.svg >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 carve >>> carve.main() File Screw Holder Bottom.stl is being carved. The carved file is saved as Screw Holder Bottom_carve.svg It took 3 seconds to carve the file. >>> carve.writeOutput() File Screw Holder Bottom.gcode is being carved. The carved file is saved as Screw Holder Bottom_carve.svg It took 3 seconds to carve the file. >>> carve.getCarveGcode(" 54 162 108 Number of Vertices,Number of Edges,Number of Faces -5.800000000000001 5.341893939393939 4.017841892579603 Vertex Coordinates XYZ 5.800000000000001 5.341893939393939 4.017841892579603 .. many lines of GNU Triangulated Surface vertices, edges and faces .. ") """ from __future__ import absolute_import try: import psyco psyco.full() except: pass #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.skeinforge_utilities import euclidean from skeinforge_tools.skeinforge_utilities import gcodec from skeinforge_tools.skeinforge_utilities import preferences from skeinforge_tools.skeinforge_utilities import triangle_mesh from skeinforge_tools import analyze from skeinforge_tools.skeinforge_utilities import interpret from skeinforge_tools import polyfile import cStringIO import math import os import sys import time import webbrowser __author__ = "Enrique Perez (perez_enrique@yahoo.com)" __date__ = "$Date: 2008/02/05 $" __license__ = "GPL 3.0" def getCarveGcode( fileName, carvePreferences = None ): "Carve a shape file." carving = getCarving( fileName ) if carving == None: return '' if carvePreferences == None: carvePreferences = CarvePreferences() preferences.readPreferences( carvePreferences ) skein = CarveSkein() skein.parseCarving( carvePreferences, carving, fileName ) return skein.output.getvalue() def getCarving( fileName ): "Get a carving for the file using an import plugin." importPluginFilenames = interpret.getImportPluginFilenames() for importPluginFilename in importPluginFilenames: fileTypeDot = '.' + importPluginFilename if fileName[ - len( fileTypeDot ) : ].lower() == fileTypeDot: pluginModule = gcodec.getModule( importPluginFilename, 'import_plugins', __file__ ) if pluginModule != None: return pluginModule.getCarving( fileName ) print( 'Could not find plugin to handle ' + fileName ) return None def getParameterFromJavascript( lines, parameterName, parameterValue ): "Get a paramater from lines of javascript." for line in lines: strippedLine = line.replace( ';', ' ' ).lstrip() splitLine = strippedLine.split() firstWord = gcodec.getFirstWord( splitLine ) if firstWord == parameterName: return float( splitLine[ 2 ] ) return parameterValue def getReplacedInQuotes( original, replacement, text ): "Replace what follows in quotes after the word." wordAndQuote = original + '="' originalIndexStart = text.find( wordAndQuote ) if originalIndexStart == - 1: return text originalIndexEnd = text.find( '"', originalIndexStart + len( wordAndQuote ) ) if originalIndexEnd == - 1: return text wordAndBothQuotes = text[ originalIndexStart : originalIndexEnd + 1 ] return text.replace( wordAndBothQuotes, wordAndQuote + replacement + '"' ) def getReplacedTagString( replacementTagString, tagID, text ): "Get text with the tag string replaced." idString = 'id="' + tagID + '"' idStringIndexStart = text.find( idString ) if idStringIndexStart == - 1: return text tagBeginIndex = text.rfind( '<', 0, idStringIndexStart ) tagEndIndex = text.find( '>', idStringIndexStart ) if tagBeginIndex == - 1 or tagEndIndex == - 1: return text originalTagString = text[ tagBeginIndex : tagEndIndex + 1 ] return text.replace( originalTagString, replacementTagString ) def getReplacedWordAndInQuotes( original, replacement, text ): "Replace the word in the text and replace what follows in quotes after the word." text = text.replace( 'replaceWith' + original, replacement ) return getReplacedInQuotes( original, replacement, text ) def writeOutput( fileName = '' ): "Carve a GNU Triangulated Surface file. If no fileName is specified, carve the first GNU Triangulated Surface file in this folder." if fileName == '': unmodified = gcodec.getFilesWithFileTypesWithoutWords( interpret.getImportPluginFilenames() ) if len( unmodified ) == 0: print( "There are no GNU Triangulated Surface files in this folder." ) return fileName = unmodified[ 0 ] startTime = time.time() carvePreferences = CarvePreferences() preferences.readPreferences( carvePreferences ) print( 'File ' + gcodec.getSummarizedFilename( fileName ) + ' is being carved.' ) carveGcode = getCarveGcode( fileName, carvePreferences ) if carveGcode == '': return suffixFilename = fileName[ : fileName.rfind( '.' ) ] + '_carve.svg' suffixFilename = suffixFilename.replace( ' ', '_' ) gcodec.writeFileText( suffixFilename, carveGcode ) print( 'The carved file is saved as ' + gcodec.getSummarizedFilename( suffixFilename ) ) # packageFilePath = os.path.abspath( __file__ ) # for level in xrange( numberOfLevelsDeepInPackageHierarchy + 1 ): # packageFilePath = os.path.dirname( packageFilePath ) # documentationPath = os.path.join( os.path.join( packageFilePath, 'documentation' ), self.displayPreferences.fileNameHelp ) # os.system( webbrowser.get().name + ' ' + documentationPath )#used this instead of webbrowser.open() to workaround webbrowser open() bug # analyze.writeOutput( suffixFilename, carveGcode ) os.system( webbrowser.get().name + ' ' + suffixFilename )#used this instead of webbrowser.open() to workaround webbrowser open() bug print( 'It took ' + str( int( round( time.time() - startTime ) ) ) + ' seconds to carve the file.' ) class CarvePreferences: "A class to handle the carve preferences." def __init__( self ): "Set the default preferences, execute title & preferences fileName." #Set the default preferences. self.archive = [] self.extrusionWidthOverThickness = preferences.FloatPreference().getFromValue( 'Extrusion Width over Thickness (ratio):', 1.5 ) self.archive.append( self.extrusionWidthOverThickness ) self.fileNameInput = preferences.Filename().getFromFilename( interpret.getTranslatorFileTypeTuples(), 'Open File to be Carved', '' ) self.archive.append( self.fileNameInput ) self.importCoarseness = preferences.FloatPreference().getFromValue( 'Import Coarseness (ratio):', 1.0 ) self.archive.append( self.importCoarseness ) self.meshTypeLabel = preferences.LabelDisplay().getFromName( 'Mesh Type: ' ) self.archive.append( self.meshTypeLabel ) importRadio = [] self.correctMesh = preferences.Radio().getFromRadio( 'Correct Mesh', importRadio, True ) self.archive.append( self.correctMesh ) self.unprovenMesh = preferences.Radio().getFromRadio( 'Unproven Mesh', importRadio, False ) self.archive.append( self.unprovenMesh ) self.infillBridgeThicknessOverLayerThickness = preferences.FloatPreference().getFromValue( 'Infill Bridge Thickness over Layer Thickness (ratio):', 1.0 ) self.archive.append( self.infillBridgeThicknessOverLayerThickness ) self.infillBridgeWidthOverExtrusionWidth = preferences.FloatPreference().getFromValue( 'Infill Bridge Width over Extrusion Width (ratio):', 1.0 ) self.archive.append( self.infillBridgeWidthOverExtrusionWidth ) self.infillDirectionBridge = preferences.BooleanPreference().getFromValue( 'Infill in Direction of Bridges', True ) self.archive.append( self.infillDirectionBridge ) self.layerThickness = preferences.FloatPreference().getFromValue( 'Layer Thickness (mm):', 0.4 ) self.archive.append( self.layerThickness ) self.layerThicknessOverPrecision = preferences.FloatPreference().getFromValue( 'Layer Thickness over Precision (ratio):', 10.0 ) self.archive.append( self.layerThicknessOverPrecision ) #Create the archive, title of the execute button, title of the dialog & preferences fileName. self.executeTitle = 'Carve' self.saveTitle = 'Save Preferences' preferences.setHelpPreferencesFileNameTitleWindowPosition( self, 'skeinforge_tools.carve.html' ) def execute( self ): "Carve button has been clicked." fileNames = polyfile.getFileOrDirectoryTypes( self.fileNameInput.value, interpret.getImportPluginFilenames(), self.fileNameInput.wasCancelled ) for fileName in fileNames: writeOutput( fileName ) class CarveSkein: "A class to carve a GNU Triangulated Surface." def __init__( self ): self.margin = 20 self.output = cStringIO.StringIO() self.textHeight = 22.5 self.unitScale = 3.7 def addInitializationToOutputSVG( self ): "Add initialization gcode to the output." endOfSVGHeaderIndex = self.svgTemplateLines.index( '//End of svg header' ) self.addLines( self.svgTemplateLines[ : endOfSVGHeaderIndex ] ) self.addLine( '\tdecimalPlacesCarried = ' + str( self.decimalPlacesCarried ) ) # Set decimal places carried. self.addLine( '\tlayerThickness = ' + self.getRounded( self.layerThickness ) ) # Set layer thickness. self.addLine( '\textrusionWidth = ' + self.getRounded( self.extrusionWidth ) ) # Set extrusion width. self.addLine( '\tinfillBridgeWidthOverExtrusionWidth = ' + euclidean.getRoundedToThreePlaces( self.carvePreferences.infillBridgeWidthOverExtrusionWidth.value ) ) self.addLine( '\tprocedureDone = "carve"' ) # The skein has been carved. self.addLine( '\textrusionStart = 1' ) # Initialization is finished, extrusion is starting. beginningOfPathSectionIndex = self.svgTemplateLines.index( '<!--Beginning of path section-->' ) self.addLines( self.svgTemplateLines[ endOfSVGHeaderIndex + 1 : beginningOfPathSectionIndex ] ) def addLayerStart( self, layerIndex, z ): "Add the start lines for the layer." # y = (1 * i + 1) * ( margin + sliceDimY * unitScale) + i * txtHeight layerTranslateY = layerIndex * self.textHeight + ( layerIndex + 1 ) * ( self.extent.y * self.unitScale + self.margin ) zRounded = self.getRounded( z ) self.addLine( '\t\t<g id="z %s" transform="translate(%s, %s)">' % ( zRounded, self.getRounded( self.margin ), self.getRounded( layerTranslateY ) ) ) self.addLine( '\t\t\t<text y="15" fill="#000" stroke="none">Layer %s, z %s</text>' % ( layerIndex, zRounded ) ) # <g id="z 0.1" transform="translate(20, 242)"> # <text y="15" fill="#000" stroke="none">Layer 1, z 0.1</text> # unit scale (mm=3.7, in=96) # # g transform # x = margin # y = (layer + 1) * ( margin + (slice height * unit scale)) + (layer * 20) # # text # y = text height # # path transform # scale = (unit scale) (-1 * unitscale) # translate = (-1 * minX) (-1 * minY) def addLine( self, line ): "Add a line of text and a newline to the output." self.output.write( line + "\n" ) def addLines( self, lines ): "Add lines of text to the output." for line in lines: self.addLine( line ) def addRotatedLoopLayersToOutput( self, rotatedBoundaryLayers ): "Add rotated boundary layers to the output." for rotatedBoundaryLayerIndex in xrange( len( rotatedBoundaryLayers ) ): rotatedBoundaryLayer = rotatedBoundaryLayers[ rotatedBoundaryLayerIndex ] self.addRotatedLoopLayerToOutput( rotatedBoundaryLayerIndex, rotatedBoundaryLayer ) def addRotatedLoopLayerToOutput( self, layerIndex, rotatedBoundaryLayer ): "Add rotated boundary layer to the output." self.addLayerStart( layerIndex, rotatedBoundaryLayer.z ) if rotatedBoundaryLayer.rotation != None: self.addLine('\t\t\t<!--bridgeDirection--> %s' % rotatedBoundaryLayer.rotation ) # Indicate the bridge direction. # <path transform="scale(3.7, -3.7) translate(0, 5)" d="M 0 -5 L 50 0 L60 50 L 5 50 z M 5 3 L5 15 L15 15 L15 5 z"/> # transform = 'scale(' + unitScale + ' ' + (unitScale * -1) + ') translate(' + (sliceMinX * -1) + ' ' + (sliceMinY * -1) + ')' pathString = '\t\t\t<path transform="scale(%s, %s) translate(%s, %s)" d="' % ( self.unitScale, - self.unitScale, self.getRounded( - self.cornerMinimum.x ), self.getRounded( - self.cornerMinimum.y ) ) if len( rotatedBoundaryLayer.loops ) > 0: pathString += self.getSVGLoopString( rotatedBoundaryLayer.loops[ 0 ] ) for loop in rotatedBoundaryLayer.loops[ 1 : ]: pathString += ' ' + self.getSVGLoopString( loop ) pathString += '"/>' self.addLine( pathString ) self.addLine( '\t\t</g>' ) def addShutdownToOutput( self ): "Add shutdown svg to the output." endOfPathSectionIndex = self.svgTemplateLines.index( '<!--End of path section-->' ) self.addLines( self.svgTemplateLines[ endOfPathSectionIndex + 1 : ] ) def getReplacedSVGTemplateLines( self, fileName, rotatedBoundaryLayers ): "Get the lines of text from the svg_template.txt file." #( layers.length + 1 ) * (margin + sliceDimY * unitScale + txtHeight) + margin + txtHeight + margin + 110 svgTemplateText = gcodec.getFileTextInFileDirectory( __file__, 'svg_template.svg' ) originalTextLines = gcodec.getTextLines( svgTemplateText ) self.margin = getParameterFromJavascript( originalTextLines, 'margin', self.margin ) self.textHeight = getParameterFromJavascript( originalTextLines, 'textHeight', self.textHeight ) javascriptControlsWidth = getParameterFromJavascript( originalTextLines, 'javascripControlBoxX', 510.0 ) noJavascriptControlsHeight = getParameterFromJavascript( originalTextLines, 'noJavascriptControlBoxY', 110.0 ) controlTop = len( rotatedBoundaryLayers ) * ( self.margin + self.extent.y * self.unitScale + self.textHeight ) + 2.0 * self.margin + self.textHeight # width = margin + (sliceDimX * unitScale) + margin; svgTemplateText = getReplacedInQuotes( 'height', self.getRounded( controlTop + noJavascriptControlsHeight + self.margin ), svgTemplateText ) width = 2.0 * self.margin + max( self.extent.y * self.unitScale, javascriptControlsWidth ) svgTemplateText = getReplacedInQuotes( 'width', self.getRounded( width ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'layerThickness', self.getRounded( self.layerThickness ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'maxX', self.getRounded( self.cornerMaximum.x ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'minX', self.getRounded( self.cornerMinimum.x ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'dimX', self.getRounded( self.extent.x ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'maxY', self.getRounded( self.cornerMaximum.y ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'minY', self.getRounded( self.cornerMinimum.y ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'dimY', self.getRounded( self.extent.y ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'maxZ', self.getRounded( self.cornerMaximum.z ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'minZ', self.getRounded( self.cornerMinimum.z ), svgTemplateText ) svgTemplateText = getReplacedWordAndInQuotes( 'dimZ', self.getRounded( self.extent.z ), svgTemplateText ) summarizedFilename = gcodec.getSummarizedFilename( fileName ) + ' SVG Slice File' svgTemplateText = getReplacedWordAndInQuotes( 'Title', summarizedFilename, svgTemplateText ) noJavascriptControlsTagString = '<g id="noJavascriptControls" fill="#000" transform="translate(%s, %s)">' % ( self.getRounded( self.margin ), self.getRounded( controlTop ) ) svgTemplateText = getReplacedTagString( noJavascriptControlsTagString, 'noJavascriptControls', svgTemplateText ) # <g id="noJavascriptControls" fill="#000" transform="translate(20, 1400)"> return gcodec.getTextLines( svgTemplateText ) def getRounded( self, number ): "Get number rounded to the number of carried decimal places as a string." return euclidean.getRoundedToDecimalPlacesString( self.decimalPlacesCarried, number ) def getRoundedComplexString( self, point ): "Get the rounded complex string." return self.getRounded( point.real ) + ' ' + self.getRounded( point.imag ) def getSVGLoopString( self, loop ): "Get the svg loop string." svgLoopString = '' if len( loop ) < 1: return '' oldRoundedComplexString = self.getRoundedComplexString( loop[ - 1 ] ) for pointIndex in xrange( len( loop ) ): point = loop[ pointIndex ] stringBeginning = 'M ' if len( svgLoopString ) > 0: stringBeginning = ' L ' roundedComplexString = self.getRoundedComplexString( point ) if roundedComplexString != oldRoundedComplexString: svgLoopString += stringBeginning + roundedComplexString oldRoundedComplexString = roundedComplexString if len( svgLoopString ) < 1: return '' return svgLoopString + ' z' def parseCarving( self, carvePreferences, carving, fileName ): "Parse gnu triangulated surface text and store the carved gcode." self.carvePreferences = carvePreferences self.layerThickness = carvePreferences.layerThickness.value self.setExtrusionDiameterWidth( carvePreferences ) if carvePreferences.infillDirectionBridge.value: carving.setCarveBridgeLayerThickness( self.bridgeLayerThickness ) carving.setCarveLayerThickness( self.layerThickness ) carving.setCarveImportRadius( 0.5 * carvePreferences.importCoarseness.value * abs( self.extrusionWidth ) ) carving.setCarveIsCorrectMesh( carvePreferences.correctMesh.value ) rotatedBoundaryLayers = carving.getCarveRotatedBoundaryLayers() if len( rotatedBoundaryLayers ) < 1: return self.cornerMaximum = carving.getCarveCornerMaximum() self.cornerMinimum = carving.getCarveCornerMinimum() #reset from slicable self.layerThickness = carving.getCarveLayerThickness() self.setExtrusionDiameterWidth( carvePreferences ) self.decimalPlacesCarried = int( max( 0.0, math.ceil( 1.0 - math.log10( self.layerThickness / carvePreferences.layerThicknessOverPrecision.value ) ) ) ) self.extent = self.cornerMaximum - self.cornerMinimum self.svgTemplateLines = self.getReplacedSVGTemplateLines( fileName, rotatedBoundaryLayers ) self.addInitializationToOutputSVG() self.addRotatedLoopLayersToOutput( rotatedBoundaryLayers ) self.addShutdownToOutput() def setExtrusionDiameterWidth( self, carvePreferences ): "Set the extrusion diameter & width and the bridge thickness & width." self.extrusionWidth = carvePreferences.extrusionWidthOverThickness.value * self.layerThickness self.bridgeLayerThickness = self.layerThickness * carvePreferences.infillBridgeThicknessOverLayerThickness.value def main(): "Display the carve dialog." if len( sys.argv ) > 1: writeOutput( ' '.join( sys.argv[ 1 : ] ) ) else: preferences.displayDialog( CarvePreferences() ) if __name__ == "__main__": main()