chiark / gitweb /
Add uppercase STL and HEX to file dialog filters for linux/MacOS
[cura.git] / Cura / skeinforge_application / skeinforge_plugins / craft_plugins / mill.py
1 """
2 This page is in the table of contents.
3 Mill is a script to mill the outlines.
4
5 ==Operation==
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.
7
8 ==Settings==
9 ===Add Loops===
10 ====Add Inner Loops====
11 Default is on.
12
13 When selected, the inner milling loops will be added.
14
15 ====Add Outer Loops====
16 Default is on.
17
18 When selected, the outer milling loops will be added.
19
20 ===Cross Hatch===
21 Default is on.
22
23 When selected, there will be alternating horizontal and vertical milling paths, if it is off there will only be horizontal milling paths.
24
25 ===Loop Outset===
26 ====Loop Inner Outset over Perimeter Width====
27 Default is 0.5.
28
29 Defines the ratio of the amount the inner milling loop will be outset over the edge width.
30
31 ====Loop Outer Outset over Perimeter Width====
32 Default is one.
33
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.
35
36 ===Mill Width over Perimeter Width===
37 Default is one.
38
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.
40
41 ==Examples==
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.
43
44 > python mill.py
45 This brings up the mill dialog.
46
47 > python mill.py Screw Holder Bottom.stl
48 The mill tool is parsing the file:
49 Screw Holder Bottom.stl
50 ..
51 The mill tool has created the file:
52 Screw Holder Bottom_mill.gcode
53
54 """
55
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.
58 import __init__
59
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
71 import math
72 import os
73 import sys
74
75
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'
79
80
81 def getCraftedText( fileName, gcodeText = '', repository=None):
82         'Mill the file or gcodeText.'
83         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), repository )
84
85 def getCraftedTextFromText(gcodeText, repository=None):
86         'Mill a gcode linear move gcodeText.'
87         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'mill'):
88                 return gcodeText
89         if repository == None:
90                 repository = settings.getReadRepository( MillRepository() )
91         if not repository.activateMill.value:
92                 return gcodeText
93         return MillSkein().getCraftedGcode(gcodeText, repository)
94
95 def getNewRepository():
96         'Get new repository.'
97         return MillRepository()
98
99 def getPointsFromSegmentTable(segmentTable):
100         'Get the points from the segment table.'
101         points = []
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)
108         return points
109
110 def isPointOfTableInLoop( loop, pointTable ):
111         'Determine if a point in the point table is in the loop.'
112         for point in loop:
113                 if point in pointTable:
114                         return True
115         return False
116
117 def writeOutput(fileName, shouldAnalyze=True):
118         'Mill a gcode linear move file.'
119         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'mill', shouldAnalyze)
120
121
122 class Average:
123         'A class to hold values and get the average.'
124         def __init__(self):
125                 self.reset()
126
127         def addValue( self, value ):
128                 'Add a value to the total and the number of values.'
129                 self.numberOfValues += 1
130                 self.total += value
131
132         def getAverage(self):
133                 'Get the average.'
134                 if self.numberOfValues == 0:
135                         print('should never happen, self.numberOfValues in Average is zero')
136                         return 0.0
137                 return self.total / float( self.numberOfValues )
138
139         def reset(self):
140                 'Set the number of values and the total to the default.'
141                 self.numberOfValues = 0
142                 self.total = 0.0
143
144
145 class MillRepository:
146         'A class to handle the mill settings.'
147         def __init__(self):
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'
161
162         def execute(self):
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)
167
168
169
170 class MillSkein:
171         'A class to mill a skein of extrusions.'
172         def __init__(self):
173                 self.aroundPixelTable = {}
174                 self.average = Average()
175                 self.boundaryLayers = []
176                 self.distanceFeedRate = gcodec.DistanceFeedRate()
177                 self.edgeWidth = 0.6
178                 self.isExtruderActive = False
179                 self.layerIndex = 0
180                 self.lineIndex = 0
181                 self.lines = None
182                 self.oldLocation = None
183
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
189                 for loop in loops:
190                         self.distanceFeedRate.addGcodeFromThreadZ(loop, z)
191                         euclidean.addToThreadsFromLoop(self.halfEdgeWidth, 'loop', loop, self.oldLocation, self)
192
193         def addGcodeFromThreadZ( self, thread, z ):
194                 'Add a thread to the output.'
195                 self.distanceFeedRate.addGcodeFromThreadZ( thread, z )
196
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:
202                         return
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 )
209                 for path in paths:
210                         simplifiedPath = euclidean.getSimplifiedPath( path, self.millWidth )
211                         self.distanceFeedRate.addGcodeFromThreadZ( simplifiedPath, averageZ )
212
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)
226                 innerPointTable = {}
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)
233                 for loop in loops:
234                         if isPointOfTableInLoop(loop, innerPointTable):
235                                 boundaryLayer.innerLoops.append(loop)
236                         else:
237                                 boundaryLayer.outerLoops.append(loop)
238                 if self.repository.crossHatch.value and boundaryLayerIndex % 2 == 1:
239                         boundaryLayer.segmentTable = boundaryLayer.verticalSegmentTable
240                 else:
241                         boundaryLayer.segmentTable = boundaryLayer.horizontalSegmentTable
242
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 :]:
250                         self.parseLine(line)
251                 return self.distanceFeedRate.output.getvalue()
252
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
263
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
269
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
283
284         def parseBoundaries(self):
285                 'Parse the boundaries and add them to the boundary layers.'
286                 boundaryLoop = None
287                 boundaryLayer = None
288                 for line in self.lines[self.lineIndex :]:
289                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
290                         firstWord = gcodec.getFirstWord(splitLine)
291                         if firstWord == '(</boundaryPerimeter>)':
292                                 boundaryLoop = None
293                         elif firstWord == '(<boundaryPoint>':
294                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
295                                 if boundaryLoop == None:
296                                         boundaryLoop = []
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:
303                         return
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)
323
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')
333                                 return
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)
342
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:
347                         return
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 = {}
363                         self.average.reset()
364                 elif firstWord == '(</layer>)':
365                         if len(self.boundaryLayers) > self.layerIndex:
366                                 self.addMillThreads()
367                         self.layerIndex += 1
368                 self.distanceFeedRate.addLine(line)
369
370
371 def main():
372         'Display the mill dialog.'
373         if len(sys.argv) > 1:
374                 writeOutput(' '.join(sys.argv[1 :]))
375         else:
376                 settings.startMainLoopFromConstructor(getNewRepository())
377
378 if __name__ == '__main__':
379         main()