2 This page is in the table of contents.
3 Mill is a script to mill the outlines.
6 The default 'Activate Mill' checkbox is on. When it is on, the functions described below will work, when it is off, the functions will not be called.
10 ====Add Inner Loops====
13 When selected, the inner milling loops will be added.
15 ====Add Outer Loops====
18 When selected, the outer milling loops will be added.
23 When selected, there will be alternating horizontal and vertical milling paths, if it is off there will only be horizontal milling paths.
26 ====Loop Inner Outset over Perimeter Width====
29 Defines the ratio of the amount the inner milling loop will be outset over the edge width.
31 ====Loop Outer Outset over Perimeter Width====
34 Defines the ratio of the amount the outer milling loop will be outset over the edge width. The 'Loop Outer Outset over Perimeter Width' ratio should be greater than the 'Loop Inner Outset over Perimeter Width' ratio.
36 ===Mill Width over Perimeter Width===
39 Defines the ratio of the mill line width over the edge width. If the ratio is one, all the material will be milled. The greater the 'Mill Width over Perimeter Width' the farther apart the mill lines will be and so less of the material will be directly milled, the remaining material might still be removed in chips if the ratio is not much greater than one.
42 The following examples mill the file Screw Holder Bottom.stl. The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and mill.py.
45 This brings up the mill dialog.
47 > python mill.py Screw Holder Bottom.stl
48 The mill tool is parsing the file:
49 Screw Holder Bottom.stl
51 The mill tool has created the file:
52 Screw Holder Bottom_mill.gcode
56 from __future__ import absolute_import
57 #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.
60 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
61 from fabmetheus_utilities.geometry.solids import triangle_mesh
62 from fabmetheus_utilities.vector3 import Vector3
63 from fabmetheus_utilities import archive
64 from fabmetheus_utilities import euclidean
65 from fabmetheus_utilities import gcodec
66 from fabmetheus_utilities import intercircle
67 from fabmetheus_utilities import settings
68 from skeinforge_application.skeinforge_utilities import skeinforge_craft
69 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
70 from skeinforge_application.skeinforge_utilities import skeinforge_profile
76 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
77 __date__ = '$Date: 2008/21/04 $'
78 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
81 def getCraftedText( fileName, gcodeText = '', repository=None):
82 'Mill the file or gcodeText.'
83 return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), repository )
85 def getCraftedTextFromText(gcodeText, repository=None):
86 'Mill a gcode linear move gcodeText.'
87 if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'mill'):
89 if repository == None:
90 repository = settings.getReadRepository( MillRepository() )
91 if not repository.activateMill.value:
93 return MillSkein().getCraftedGcode(gcodeText, repository)
95 def getNewRepository():
97 return MillRepository()
99 def getPointsFromSegmentTable(segmentTable):
100 'Get the points from the segment table.'
102 segmentTableKeys = segmentTable.keys()
103 segmentTableKeys.sort()
104 for segmentTableKey in segmentTableKeys:
105 for segment in segmentTable[segmentTableKey]:
106 for endpoint in segment:
107 points.append(endpoint.point)
110 def isPointOfTableInLoop( loop, pointTable ):
111 'Determine if a point in the point table is in the loop.'
113 if point in pointTable:
117 def writeOutput(fileName, shouldAnalyze=True):
118 'Mill a gcode linear move file.'
119 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'mill', shouldAnalyze)
123 'A class to hold values and get the average.'
127 def addValue( self, value ):
128 'Add a value to the total and the number of values.'
129 self.numberOfValues += 1
132 def getAverage(self):
134 if self.numberOfValues == 0:
135 print('should never happen, self.numberOfValues in Average is zero')
137 return self.total / float( self.numberOfValues )
140 'Set the number of values and the total to the default.'
141 self.numberOfValues = 0
145 class MillRepository:
146 'A class to handle the mill settings.'
148 'Set the default settings, execute title & settings fileName.'
149 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.mill.html', self )
150 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Mill', self, '')
151 self.activateMill = settings.BooleanSetting().getFromValue('Activate Mill', self, True )
152 settings.LabelDisplay().getFromName('- Add Loops -', self )
153 self.addInnerLoops = settings.BooleanSetting().getFromValue('Add Inner Loops', self, True )
154 self.addOuterLoops = settings.BooleanSetting().getFromValue('Add Outer Loops', self, True )
155 self.crossHatch = settings.BooleanSetting().getFromValue('Cross Hatch', self, True )
156 settings.LabelDisplay().getFromName('- Loop Outset -', self )
157 self.loopInnerOutsetOverEdgeWidth = settings.FloatSpin().getFromValue( 0.3, 'Loop Inner Outset over Perimeter Width (ratio):', self, 0.7, 0.5 )
158 self.loopOuterOutsetOverEdgeWidth = settings.FloatSpin().getFromValue( 0.8, 'Loop Outer Outset over Perimeter Width (ratio):', self, 1.4, 1.0 )
159 self.millWidthOverEdgeWidth = settings.FloatSpin().getFromValue( 0.8, 'Mill Width over Edge Width (ratio):', self, 1.8, 1.0 )
160 self.executeTitle = 'Mill'
163 'Mill button has been clicked.'
164 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
165 for fileName in fileNames:
166 writeOutput(fileName)
171 'A class to mill a skein of extrusions.'
173 self.aroundPixelTable = {}
174 self.average = Average()
175 self.boundaryLayers = []
176 self.distanceFeedRate = gcodec.DistanceFeedRate()
178 self.isExtruderActive = False
182 self.oldLocation = None
184 def addGcodeFromLoops(self, loops, z):
185 'Add gcode from loops.'
186 if self.oldLocation == None:
187 self.oldLocation = Vector3()
188 self.oldLocation.z = z
190 self.distanceFeedRate.addGcodeFromThreadZ(loop, z)
191 euclidean.addToThreadsFromLoop(self.halfEdgeWidth, 'loop', loop, self.oldLocation, self)
193 def addGcodeFromThreadZ( self, thread, z ):
194 'Add a thread to the output.'
195 self.distanceFeedRate.addGcodeFromThreadZ( thread, z )
197 def addMillThreads(self):
198 'Add the mill threads to the skein.'
199 boundaryLayer = self.boundaryLayers[self.layerIndex]
200 endpoints = euclidean.getEndpointsFromSegmentTable( boundaryLayer.segmentTable )
201 if len(endpoints) < 1:
203 paths = euclidean.getPathsFromEndpoints(endpoints, 5.0 * self.millWidth, self.aroundPixelTable, 1.0, self.aroundWidth)
204 averageZ = self.average.getAverage()
205 if self.repository.addInnerLoops.value:
206 self.addGcodeFromLoops( boundaryLayer.innerLoops, averageZ )
207 if self.repository.addOuterLoops.value:
208 self.addGcodeFromLoops( boundaryLayer.outerLoops, averageZ )
210 simplifiedPath = euclidean.getSimplifiedPath( path, self.millWidth )
211 self.distanceFeedRate.addGcodeFromThreadZ( simplifiedPath, averageZ )
213 def addSegmentTableLoops( self, boundaryLayerIndex ):
214 'Add the segment tables and loops to the boundary.'
215 boundaryLayer = self.boundaryLayers[boundaryLayerIndex]
216 euclidean.subtractXIntersectionsTable(boundaryLayer.outerHorizontalTable, boundaryLayer.innerHorizontalTable)
217 euclidean.subtractXIntersectionsTable(boundaryLayer.outerVerticalTable, boundaryLayer.innerVerticalTable)
218 boundaryLayer.horizontalSegmentTable = self.getHorizontalSegmentTableForXIntersectionsTable(
219 boundaryLayer.outerHorizontalTable)
220 boundaryLayer.verticalSegmentTable = self.getVerticalSegmentTableForXIntersectionsTable(
221 boundaryLayer.outerVerticalTable)
222 betweenPoints = getPointsFromSegmentTable(boundaryLayer.horizontalSegmentTable)
223 betweenPoints += getPointsFromSegmentTable(boundaryLayer.verticalSegmentTable)
224 innerPoints = euclidean.getPointsByHorizontalDictionary(self.millWidth, boundaryLayer.innerHorizontalTable)
225 innerPoints += euclidean.getPointsByVerticalDictionary(self.millWidth, boundaryLayer.innerVerticalTable)
227 for innerPoint in innerPoints:
228 innerPointTable[innerPoint] = None
229 boundaryLayer.innerLoops = []
230 boundaryLayer.outerLoops = []
231 millRadius = 0.75 * self.millWidth
232 loops = triangle_mesh.getDescendingAreaOrientedLoops(betweenPoints, betweenPoints, millRadius)
234 if isPointOfTableInLoop(loop, innerPointTable):
235 boundaryLayer.innerLoops.append(loop)
237 boundaryLayer.outerLoops.append(loop)
238 if self.repository.crossHatch.value and boundaryLayerIndex % 2 == 1:
239 boundaryLayer.segmentTable = boundaryLayer.verticalSegmentTable
241 boundaryLayer.segmentTable = boundaryLayer.horizontalSegmentTable
243 def getCraftedGcode(self, gcodeText, repository):
244 'Parse gcode text and store the mill gcode.'
245 self.repository = repository
246 self.lines = archive.getTextLines(gcodeText)
247 self.parseInitialization()
248 self.parseBoundaries()
249 for line in self.lines[self.lineIndex :]:
251 return self.distanceFeedRate.output.getvalue()
253 def getHorizontalSegmentTableForXIntersectionsTable( self, xIntersectionsTable ):
254 'Get the horizontal segment table from the xIntersectionsTable.'
255 horizontalSegmentTable = {}
256 xIntersectionsTableKeys = xIntersectionsTable.keys()
257 xIntersectionsTableKeys.sort()
258 for xIntersectionsTableKey in xIntersectionsTableKeys:
259 xIntersections = xIntersectionsTable[ xIntersectionsTableKey ]
260 segments = euclidean.getSegmentsFromXIntersections( xIntersections, xIntersectionsTableKey * self.millWidth )
261 horizontalSegmentTable[ xIntersectionsTableKey ] = segments
262 return horizontalSegmentTable
264 def getHorizontalXIntersectionsTable(self, loops):
265 'Get the horizontal x intersections table from the loops.'
266 horizontalXIntersectionsTable = {}
267 euclidean.addXIntersectionsFromLoopsForTable(loops, horizontalXIntersectionsTable, self.millWidth)
268 return horizontalXIntersectionsTable
270 def getVerticalSegmentTableForXIntersectionsTable( self, xIntersectionsTable ):
271 'Get the vertical segment table from the xIntersectionsTable which has the x and y swapped.'
272 verticalSegmentTable = {}
273 xIntersectionsTableKeys = xIntersectionsTable.keys()
274 xIntersectionsTableKeys.sort()
275 for xIntersectionsTableKey in xIntersectionsTableKeys:
276 xIntersections = xIntersectionsTable[ xIntersectionsTableKey ]
277 segments = euclidean.getSegmentsFromXIntersections( xIntersections, xIntersectionsTableKey * self.millWidth )
278 for segment in segments:
279 for endpoint in segment:
280 endpoint.point = complex( endpoint.point.imag, endpoint.point.real )
281 verticalSegmentTable[ xIntersectionsTableKey ] = segments
282 return verticalSegmentTable
284 def parseBoundaries(self):
285 'Parse the boundaries and add them to the boundary layers.'
288 for line in self.lines[self.lineIndex :]:
289 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
290 firstWord = gcodec.getFirstWord(splitLine)
291 if firstWord == '(</boundaryPerimeter>)':
293 elif firstWord == '(<boundaryPoint>':
294 location = gcodec.getLocationFromSplitLine(None, splitLine)
295 if boundaryLoop == None:
297 boundaryLayer.loops.append(boundaryLoop)
298 boundaryLoop.append(location.dropAxis())
299 elif firstWord == '(<layer>':
300 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
301 self.boundaryLayers.append(boundaryLayer)
302 if len(self.boundaryLayers) < 2:
304 for boundaryLayer in self.boundaryLayers:
305 boundaryLayer.innerOutsetLoops = intercircle.getInsetSeparateLoopsFromLoops(boundaryLayer.loops, -self.loopInnerOutset)
306 boundaryLayer.outerOutsetLoops = intercircle.getInsetSeparateLoopsFromLoops(boundaryLayer.loops, -self.loopOuterOutset)
307 boundaryLayer.innerHorizontalTable = self.getHorizontalXIntersectionsTable( boundaryLayer.innerOutsetLoops )
308 boundaryLayer.outerHorizontalTable = self.getHorizontalXIntersectionsTable( boundaryLayer.outerOutsetLoops )
309 boundaryLayer.innerVerticalTable = self.getHorizontalXIntersectionsTable( euclidean.getDiagonalFlippedLoops( boundaryLayer.innerOutsetLoops ) )
310 boundaryLayer.outerVerticalTable = self.getHorizontalXIntersectionsTable( euclidean.getDiagonalFlippedLoops( boundaryLayer.outerOutsetLoops ) )
311 for boundaryLayerIndex in xrange( len(self.boundaryLayers) - 2, - 1, - 1 ):
312 boundaryLayer = self.boundaryLayers[ boundaryLayerIndex ]
313 boundaryLayerBelow = self.boundaryLayers[ boundaryLayerIndex + 1 ]
314 euclidean.joinXIntersectionsTables( boundaryLayerBelow.outerHorizontalTable, boundaryLayer.outerHorizontalTable )
315 euclidean.joinXIntersectionsTables( boundaryLayerBelow.outerVerticalTable, boundaryLayer.outerVerticalTable )
316 for boundaryLayerIndex in xrange( 1, len(self.boundaryLayers) ):
317 boundaryLayer = self.boundaryLayers[ boundaryLayerIndex ]
318 boundaryLayerAbove = self.boundaryLayers[ boundaryLayerIndex - 1 ]
319 euclidean.joinXIntersectionsTables( boundaryLayerAbove.innerHorizontalTable, boundaryLayer.innerHorizontalTable )
320 euclidean.joinXIntersectionsTables( boundaryLayerAbove.innerVerticalTable, boundaryLayer.innerVerticalTable )
321 for boundaryLayerIndex in xrange( len(self.boundaryLayers) ):
322 self.addSegmentTableLoops(boundaryLayerIndex)
324 def parseInitialization(self):
325 'Parse gcode initialization and store the parameters.'
326 for self.lineIndex in xrange(len(self.lines)):
327 line = self.lines[self.lineIndex]
328 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
329 firstWord = gcodec.getFirstWord(splitLine)
330 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
331 if firstWord == '(</extruderInitialization>)':
332 self.distanceFeedRate.addTagBracketedProcedure('mill')
334 elif firstWord == '(<edgeWidth>':
335 self.edgeWidth = float(splitLine[1])
336 self.aroundWidth = 0.1 * self.edgeWidth
337 self.halfEdgeWidth = 0.5 * self.edgeWidth
338 self.millWidth = self.edgeWidth * self.repository.millWidthOverEdgeWidth.value
339 self.loopInnerOutset = self.halfEdgeWidth + self.edgeWidth * self.repository.loopInnerOutsetOverEdgeWidth.value
340 self.loopOuterOutset = self.halfEdgeWidth + self.edgeWidth * self.repository.loopOuterOutsetOverEdgeWidth.value
341 self.distanceFeedRate.addLine(line)
343 def parseLine(self, line):
344 'Parse a gcode line and add it to the mill skein.'
345 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
346 if len(splitLine) < 1:
348 firstWord = splitLine[0]
349 if firstWord == 'G1':
350 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
351 if self.isExtruderActive:
352 self.average.addValue(location.z)
353 if self.oldLocation != None:
354 euclidean.addValueSegmentToPixelTable( self.oldLocation.dropAxis(), location.dropAxis(), self.aroundPixelTable, None, self.aroundWidth )
355 self.oldLocation = location
356 elif firstWord == 'M101':
357 self.isExtruderActive = True
358 elif firstWord == 'M103':
359 self.isExtruderActive = False
360 elif firstWord == '(<layer>':
361 settings.printProgress(self.layerIndex, 'mill')
362 self.aroundPixelTable = {}
364 elif firstWord == '(</layer>)':
365 if len(self.boundaryLayers) > self.layerIndex:
366 self.addMillThreads()
368 self.distanceFeedRate.addLine(line)
372 'Display the mill dialog.'
373 if len(sys.argv) > 1:
374 writeOutput(' '.join(sys.argv[1 :]))
376 settings.startMainLoopFromConstructor(getNewRepository())
378 if __name__ == '__main__':