chiark / gitweb /
Add back the ultimaker platform, and made the platform mesh simpler.
[cura.git] / Cura / slice / cura_sf / 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
58 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
59 from fabmetheus_utilities.geometry.solids import triangle_mesh
60 from fabmetheus_utilities.vector3 import Vector3
61 from fabmetheus_utilities import archive
62 from fabmetheus_utilities import euclidean
63 from fabmetheus_utilities import gcodec
64 from fabmetheus_utilities import intercircle
65 from fabmetheus_utilities import settings
66 from skeinforge_application.skeinforge_utilities import skeinforge_craft
67 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
68 from skeinforge_application.skeinforge_utilities import skeinforge_profile
69 import sys
70
71
72 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
73 __date__ = '$Date: 2008/21/04 $'
74 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
75
76
77 def getCraftedText( fileName, gcodeText = '', repository=None):
78         'Mill the file or gcodeText.'
79         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), repository )
80
81 def getCraftedTextFromText(gcodeText, repository=None):
82         'Mill a gcode linear move gcodeText.'
83         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'mill'):
84                 return gcodeText
85         if repository == None:
86                 repository = settings.getReadRepository( MillRepository() )
87         if not repository.activateMill.value:
88                 return gcodeText
89         return MillSkein().getCraftedGcode(gcodeText, repository)
90
91 def getNewRepository():
92         'Get new repository.'
93         return MillRepository()
94
95 def getPointsFromSegmentTable(segmentTable):
96         'Get the points from the segment table.'
97         points = []
98         segmentTableKeys = segmentTable.keys()
99         segmentTableKeys.sort()
100         for segmentTableKey in segmentTableKeys:
101                 for segment in segmentTable[segmentTableKey]:
102                         for endpoint in segment:
103                                 points.append(endpoint.point)
104         return points
105
106 def isPointOfTableInLoop( loop, pointTable ):
107         'Determine if a point in the point table is in the loop.'
108         for point in loop:
109                 if point in pointTable:
110                         return True
111         return False
112
113 def writeOutput(fileName, shouldAnalyze=True):
114         'Mill a gcode linear move file.'
115         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'mill', shouldAnalyze)
116
117
118 class Average(object):
119         'A class to hold values and get the average.'
120         def __init__(self):
121                 self.reset()
122
123         def addValue( self, value ):
124                 'Add a value to the total and the number of values.'
125                 self.numberOfValues += 1
126                 self.total += value
127
128         def getAverage(self):
129                 'Get the average.'
130                 if self.numberOfValues == 0:
131                         print('should never happen, self.numberOfValues in Average is zero')
132                         return 0.0
133                 return self.total / float( self.numberOfValues )
134
135         def reset(self):
136                 'Set the number of values and the total to the default.'
137                 self.numberOfValues = 0
138                 self.total = 0.0
139
140
141 class MillRepository(object):
142         'A class to handle the mill settings.'
143         def __init__(self):
144                 'Set the default settings, execute title & settings fileName.'
145                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.mill.html', self )
146                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Mill', self, '')
147                 self.activateMill = settings.BooleanSetting().getFromValue('Activate Mill', self, True )
148                 settings.LabelDisplay().getFromName('- Add Loops -', self )
149                 self.addInnerLoops = settings.BooleanSetting().getFromValue('Add Inner Loops', self, True )
150                 self.addOuterLoops = settings.BooleanSetting().getFromValue('Add Outer Loops', self, True )
151                 self.crossHatch = settings.BooleanSetting().getFromValue('Cross Hatch', self, True )
152                 settings.LabelDisplay().getFromName('- Loop Outset -', self )
153                 self.loopInnerOutsetOverEdgeWidth = settings.FloatSpin().getFromValue( 0.3, 'Loop Inner Outset over Perimeter Width (ratio):', self, 0.7, 0.5 )
154                 self.loopOuterOutsetOverEdgeWidth = settings.FloatSpin().getFromValue( 0.8, 'Loop Outer Outset over Perimeter Width (ratio):', self, 1.4, 1.0 )
155                 self.millWidthOverEdgeWidth = settings.FloatSpin().getFromValue( 0.8, 'Mill Width over Edge Width (ratio):', self, 1.8, 1.0 )
156                 self.executeTitle = 'Mill'
157
158         def execute(self):
159                 'Mill button has been clicked.'
160                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
161                 for fileName in fileNames:
162                         writeOutput(fileName)
163
164
165
166 class MillSkein(object):
167         'A class to mill a skein of extrusions.'
168         def __init__(self):
169                 self.aroundPixelTable = {}
170                 self.average = Average()
171                 self.boundaryLayers = []
172                 self.distanceFeedRate = gcodec.DistanceFeedRate()
173                 self.edgeWidth = 0.6
174                 self.isExtruderActive = False
175                 self.layerIndex = 0
176                 self.lineIndex = 0
177                 self.lines = None
178                 self.oldLocation = None
179
180         def addGcodeFromLoops(self, loops, z):
181                 'Add gcode from loops.'
182                 if self.oldLocation == None:
183                         self.oldLocation = Vector3()
184                 self.oldLocation.z = z
185                 for loop in loops:
186                         self.distanceFeedRate.addGcodeFromThreadZ(loop, z)
187                         euclidean.addToThreadsFromLoop(self.halfEdgeWidth, 'loop', loop, self.oldLocation, self)
188
189         def addGcodeFromThreadZ( self, thread, z ):
190                 'Add a thread to the output.'
191                 self.distanceFeedRate.addGcodeFromThreadZ( thread, z )
192
193         def addMillThreads(self):
194                 'Add the mill threads to the skein.'
195                 boundaryLayer = self.boundaryLayers[self.layerIndex]
196                 endpoints = euclidean.getEndpointsFromSegmentTable( boundaryLayer.segmentTable )
197                 if len(endpoints) < 1:
198                         return
199                 paths = euclidean.getPathsFromEndpoints(endpoints, 5.0 * self.millWidth, self.aroundPixelTable, 1.0, self.aroundWidth)
200                 averageZ = self.average.getAverage()
201                 if self.repository.addInnerLoops.value:
202                         self.addGcodeFromLoops( boundaryLayer.innerLoops, averageZ )
203                 if self.repository.addOuterLoops.value:
204                         self.addGcodeFromLoops( boundaryLayer.outerLoops, averageZ )
205                 for path in paths:
206                         simplifiedPath = euclidean.getSimplifiedPath( path, self.millWidth )
207                         self.distanceFeedRate.addGcodeFromThreadZ( simplifiedPath, averageZ )
208
209         def addSegmentTableLoops( self, boundaryLayerIndex ):
210                 'Add the segment tables and loops to the boundary.'
211                 boundaryLayer = self.boundaryLayers[boundaryLayerIndex]
212                 euclidean.subtractXIntersectionsTable(boundaryLayer.outerHorizontalTable, boundaryLayer.innerHorizontalTable)
213                 euclidean.subtractXIntersectionsTable(boundaryLayer.outerVerticalTable, boundaryLayer.innerVerticalTable)
214                 boundaryLayer.horizontalSegmentTable = self.getHorizontalSegmentTableForXIntersectionsTable(
215                         boundaryLayer.outerHorizontalTable)
216                 boundaryLayer.verticalSegmentTable = self.getVerticalSegmentTableForXIntersectionsTable(
217                         boundaryLayer.outerVerticalTable)
218                 betweenPoints = getPointsFromSegmentTable(boundaryLayer.horizontalSegmentTable)
219                 betweenPoints += getPointsFromSegmentTable(boundaryLayer.verticalSegmentTable)
220                 innerPoints = euclidean.getPointsByHorizontalDictionary(self.millWidth, boundaryLayer.innerHorizontalTable)
221                 innerPoints += euclidean.getPointsByVerticalDictionary(self.millWidth, boundaryLayer.innerVerticalTable)
222                 innerPointTable = {}
223                 for innerPoint in innerPoints:
224                         innerPointTable[innerPoint] = None
225                 boundaryLayer.innerLoops = []
226                 boundaryLayer.outerLoops = []
227                 millRadius = 0.75 * self.millWidth
228                 loops = triangle_mesh.getDescendingAreaOrientedLoops(betweenPoints, betweenPoints, millRadius)
229                 for loop in loops:
230                         if isPointOfTableInLoop(loop, innerPointTable):
231                                 boundaryLayer.innerLoops.append(loop)
232                         else:
233                                 boundaryLayer.outerLoops.append(loop)
234                 if self.repository.crossHatch.value and boundaryLayerIndex % 2 == 1:
235                         boundaryLayer.segmentTable = boundaryLayer.verticalSegmentTable
236                 else:
237                         boundaryLayer.segmentTable = boundaryLayer.horizontalSegmentTable
238
239         def getCraftedGcode(self, gcodeText, repository):
240                 'Parse gcode text and store the mill gcode.'
241                 self.repository = repository
242                 self.lines = archive.getTextLines(gcodeText)
243                 self.parseInitialization()
244                 self.parseBoundaries()
245                 for line in self.lines[self.lineIndex :]:
246                         self.parseLine(line)
247                 return self.distanceFeedRate.output.getvalue()
248
249         def getHorizontalSegmentTableForXIntersectionsTable( self, xIntersectionsTable ):
250                 'Get the horizontal segment table from the xIntersectionsTable.'
251                 horizontalSegmentTable = {}
252                 xIntersectionsTableKeys = xIntersectionsTable.keys()
253                 xIntersectionsTableKeys.sort()
254                 for xIntersectionsTableKey in xIntersectionsTableKeys:
255                         xIntersections = xIntersectionsTable[ xIntersectionsTableKey ]
256                         segments = euclidean.getSegmentsFromXIntersections( xIntersections, xIntersectionsTableKey * self.millWidth )
257                         horizontalSegmentTable[ xIntersectionsTableKey ] = segments
258                 return horizontalSegmentTable
259
260         def getHorizontalXIntersectionsTable(self, loops):
261                 'Get the horizontal x intersections table from the loops.'
262                 horizontalXIntersectionsTable = {}
263                 euclidean.addXIntersectionsFromLoopsForTable(loops, horizontalXIntersectionsTable, self.millWidth)
264                 return horizontalXIntersectionsTable
265
266         def getVerticalSegmentTableForXIntersectionsTable( self, xIntersectionsTable ):
267                 'Get the vertical segment table from the xIntersectionsTable which has the x and y swapped.'
268                 verticalSegmentTable = {}
269                 xIntersectionsTableKeys = xIntersectionsTable.keys()
270                 xIntersectionsTableKeys.sort()
271                 for xIntersectionsTableKey in xIntersectionsTableKeys:
272                         xIntersections = xIntersectionsTable[ xIntersectionsTableKey ]
273                         segments = euclidean.getSegmentsFromXIntersections( xIntersections, xIntersectionsTableKey * self.millWidth )
274                         for segment in segments:
275                                 for endpoint in segment:
276                                         endpoint.point = complex( endpoint.point.imag, endpoint.point.real )
277                         verticalSegmentTable[ xIntersectionsTableKey ] = segments
278                 return verticalSegmentTable
279
280         def parseBoundaries(self):
281                 'Parse the boundaries and add them to the boundary layers.'
282                 boundaryLoop = None
283                 boundaryLayer = None
284                 for line in self.lines[self.lineIndex :]:
285                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
286                         firstWord = gcodec.getFirstWord(splitLine)
287                         if firstWord == '(</boundaryPerimeter>)':
288                                 boundaryLoop = None
289                         elif firstWord == '(<boundaryPoint>':
290                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
291                                 if boundaryLoop == None:
292                                         boundaryLoop = []
293                                         boundaryLayer.loops.append(boundaryLoop)
294                                 boundaryLoop.append(location.dropAxis())
295                         elif firstWord == '(<layer>':
296                                 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
297                                 self.boundaryLayers.append(boundaryLayer)
298                 if len(self.boundaryLayers) < 2:
299                         return
300                 for boundaryLayer in self.boundaryLayers:
301                         boundaryLayer.innerOutsetLoops = intercircle.getInsetSeparateLoopsFromLoops(boundaryLayer.loops, -self.loopInnerOutset)
302                         boundaryLayer.outerOutsetLoops = intercircle.getInsetSeparateLoopsFromLoops(boundaryLayer.loops, -self.loopOuterOutset)
303                         boundaryLayer.innerHorizontalTable = self.getHorizontalXIntersectionsTable( boundaryLayer.innerOutsetLoops )
304                         boundaryLayer.outerHorizontalTable = self.getHorizontalXIntersectionsTable( boundaryLayer.outerOutsetLoops )
305                         boundaryLayer.innerVerticalTable = self.getHorizontalXIntersectionsTable( euclidean.getDiagonalFlippedLoops( boundaryLayer.innerOutsetLoops ) )
306                         boundaryLayer.outerVerticalTable = self.getHorizontalXIntersectionsTable( euclidean.getDiagonalFlippedLoops( boundaryLayer.outerOutsetLoops ) )
307                 for boundaryLayerIndex in xrange( len(self.boundaryLayers) - 2, - 1, - 1 ):
308                         boundaryLayer = self.boundaryLayers[ boundaryLayerIndex ]
309                         boundaryLayerBelow = self.boundaryLayers[ boundaryLayerIndex + 1 ]
310                         euclidean.joinXIntersectionsTables( boundaryLayerBelow.outerHorizontalTable, boundaryLayer.outerHorizontalTable )
311                         euclidean.joinXIntersectionsTables( boundaryLayerBelow.outerVerticalTable, boundaryLayer.outerVerticalTable )
312                 for boundaryLayerIndex in xrange( 1, len(self.boundaryLayers) ):
313                         boundaryLayer = self.boundaryLayers[ boundaryLayerIndex ]
314                         boundaryLayerAbove = self.boundaryLayers[ boundaryLayerIndex - 1 ]
315                         euclidean.joinXIntersectionsTables( boundaryLayerAbove.innerHorizontalTable, boundaryLayer.innerHorizontalTable )
316                         euclidean.joinXIntersectionsTables( boundaryLayerAbove.innerVerticalTable, boundaryLayer.innerVerticalTable )
317                 for boundaryLayerIndex in xrange( len(self.boundaryLayers) ):
318                         self.addSegmentTableLoops(boundaryLayerIndex)
319
320         def parseInitialization(self):
321                 'Parse gcode initialization and store the parameters.'
322                 for self.lineIndex in xrange(len(self.lines)):
323                         line = self.lines[self.lineIndex]
324                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
325                         firstWord = gcodec.getFirstWord(splitLine)
326                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
327                         if firstWord == '(</extruderInitialization>)':
328                                 self.distanceFeedRate.addTagBracketedProcedure('mill')
329                                 return
330                         elif firstWord == '(<edgeWidth>':
331                                 self.edgeWidth = float(splitLine[1])
332                                 self.aroundWidth = 0.1 * self.edgeWidth
333                                 self.halfEdgeWidth = 0.5 * self.edgeWidth
334                                 self.millWidth = self.edgeWidth * self.repository.millWidthOverEdgeWidth.value
335                                 self.loopInnerOutset = self.halfEdgeWidth + self.edgeWidth * self.repository.loopInnerOutsetOverEdgeWidth.value
336                                 self.loopOuterOutset = self.halfEdgeWidth + self.edgeWidth * self.repository.loopOuterOutsetOverEdgeWidth.value
337                         self.distanceFeedRate.addLine(line)
338
339         def parseLine(self, line):
340                 'Parse a gcode line and add it to the mill skein.'
341                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
342                 if len(splitLine) < 1:
343                         return
344                 firstWord = splitLine[0]
345                 if firstWord == 'G1':
346                         location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
347                         if self.isExtruderActive:
348                                 self.average.addValue(location.z)
349                                 if self.oldLocation != None:
350                                         euclidean.addValueSegmentToPixelTable( self.oldLocation.dropAxis(), location.dropAxis(), self.aroundPixelTable, None, self.aroundWidth )
351                         self.oldLocation = location
352                 elif firstWord == 'M101':
353                         self.isExtruderActive = True
354                 elif firstWord == 'M103':
355                         self.isExtruderActive = False
356                 elif firstWord == '(<layer>':
357                         settings.printProgress(self.layerIndex, 'mill')
358                         self.aroundPixelTable = {}
359                         self.average.reset()
360                 elif firstWord == '(</layer>)':
361                         if len(self.boundaryLayers) > self.layerIndex:
362                                 self.addMillThreads()
363                         self.layerIndex += 1
364                 self.distanceFeedRate.addLine(line)
365
366
367 def main():
368         'Display the mill dialog.'
369         if len(sys.argv) > 1:
370                 writeOutput(' '.join(sys.argv[1 :]))
371         else:
372                 settings.startMainLoopFromConstructor(getNewRepository())
373
374 if __name__ == '__main__':
375         main()