chiark / gitweb /
Move SF into its own directory, to seperate SF and Cura. Rename newui to gui.
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / tower.py
1 """
2 This page is in the table of contents.
3 Tower commands the fabricator to extrude a disconnected region for a few layers, then go to another disconnected region and extrude there.  Its purpose is to reduce the number of stringers between a shape and reduce extruder travel.
4
5 The tower manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Tower
7
8 ==Operation==
9 The default 'Activate Tower' checkbox is off.  The default is off because tower could result in the extruder colliding with an already extruded part of the shape and because extruding in one region for more than one layer could result in the shape melting.  When it is on, the functions described below will work, when it is off, nothing will be done.
10
11 ==Settings==
12 ===Maximum Tower Height===
13 Default: 5
14
15 Defines the maximum number of layers that the extruder will extrude in one region before going to another.  This is the most important value for tower.
16
17 ===Extruder Possible Collision Cone Angle===
18 Default: 60 degrees
19
20 Tower works by looking for islands in each layer and if it finds another island in the layer above, it goes to the next layer above instead of going across to other regions on the original layer.  It checks for collision with shapes already extruded within a cone from the nozzle tip.  The 'Extruder Possible Collision Cone Angle' setting is the angle of that cone.  Realistic values for the cone angle range between zero and ninety.  The higher the angle, the less likely a collision with the rest of the shape is, generally the extruder will stay in the region for only a few layers before a collision is detected with the wide cone.
21
22 ===Tower Start Layer===
23 Default: 1
24
25 Defines the layer index which the script starts extruding towers, after the last raft layer which does not have support material.  It is best to not tower at least the first layer because the temperature of the first layer is sometimes different than that of the other layers.
26
27 ==Examples==
28 The following examples tower the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and tower.py.
29
30 > python tower.py
31 This brings up the tower dialog.
32
33 > python tower.py Screw Holder Bottom.stl
34 The tower tool is parsing the file:
35 Screw Holder Bottom.stl
36 ..
37 The tower tool has created the file:
38 .. Screw Holder Bottom_tower.gcode
39
40 """
41
42 from __future__ import absolute_import
43 #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.
44 import __init__
45
46 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
47 from fabmetheus_utilities.vector3 import Vector3
48 from fabmetheus_utilities import archive
49 from fabmetheus_utilities import euclidean
50 from fabmetheus_utilities import gcodec
51 from fabmetheus_utilities import intercircle
52 from fabmetheus_utilities import settings
53 from skeinforge_application.skeinforge_utilities import skeinforge_craft
54 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
55 from skeinforge_application.skeinforge_utilities import skeinforge_profile
56 import math
57 import sys
58
59
60 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
61 __date__ = '$Date: 2008/21/04 $'
62 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
63
64 def getCraftedText( fileName, text, towerRepository = None ):
65         "Tower a gcode linear move file or text."
66         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, text), towerRepository )
67
68 def getCraftedTextFromText( gcodeText, towerRepository = None ):
69         "Tower a gcode linear move text."
70         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'tower'):
71                 return gcodeText
72         if towerRepository == None:
73                 towerRepository = settings.getReadRepository( TowerRepository() )
74         if not towerRepository.activateTower.value:
75                 return gcodeText
76         return TowerSkein().getCraftedGcode( gcodeText, towerRepository )
77
78 def getNewRepository():
79         'Get new repository.'
80         return TowerRepository()
81
82 def writeOutput(fileName, shouldAnalyze=True):
83         "Tower a gcode linear move file."
84         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'tower', shouldAnalyze)
85
86
87 class Island:
88         "A class to hold the boundary and lines."
89         def __init__(self):
90                 self.boundary = []
91                 self.boundingLoop = None
92                 self.lines = []
93
94         def addToBoundary( self, splitLine ):
95                 "Add to the boundary if it is not complete."
96                 if self.boundingLoop == None:
97                         location = gcodec.getLocationFromSplitLine(None, splitLine)
98                         self.boundary.append(location.dropAxis())
99                         self.z = location.z
100
101         def createBoundingLoop(self):
102                 "Create the bounding loop if it is not already created."
103                 if self.boundingLoop == None:
104                         self.boundingLoop = intercircle.BoundingLoop().getFromLoop( self.boundary )
105
106
107 class ThreadLayer:
108         "A layer of loops and paths."
109         def __init__(self):
110                 "Thread layer constructor."
111                 self.afterExtrusionLines = []
112                 self.beforeExtrusionLines = []
113                 self.islands = []
114
115         def __repr__(self):
116                 "Get the string representation of this thread layer."
117                 return '%s' % self.islands
118
119
120 class TowerRepository:
121         "A class to handle the tower settings."
122         def __init__(self):
123                 "Set the default settings, execute title & settings fileName."
124                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.tower.html', self )
125                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Tower', self, '')
126                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Tower')
127                 self.activateTower = settings.BooleanSetting().getFromValue('Activate Tower', self, False )
128                 self.extruderPossibleCollisionConeAngle = settings.FloatSpin().getFromValue( 40.0, 'Extruder Possible Collision Cone Angle (degrees):', self, 80.0, 60.0 )
129                 self.maximumTowerHeight = settings.IntSpin().getFromValue( 2, 'Maximum Tower Height (layers):', self, 10, 5 )
130                 self.towerStartLayer = settings.IntSpin().getFromValue( 1, 'Tower Start Layer (integer):', self, 5, 1 )
131                 self.executeTitle = 'Tower'
132
133         def execute(self):
134                 "Tower button has been clicked."
135                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
136                 for fileName in fileNames:
137                         writeOutput(fileName)
138
139
140 class TowerSkein:
141         "A class to tower a skein of extrusions."
142         def __init__(self):
143                 self.afterExtrusionLines = []
144                 self.beforeExtrusionLines = []
145                 self.distanceFeedRate = gcodec.DistanceFeedRate()
146                 self.edgeWidth = 0.6
147                 self.highestZ = - 987654321.0
148                 self.island = None
149                 self.layerIndex = 0
150                 self.lineIndex = 0
151                 self.lines = None
152                 self.minimumBelow = 0.1
153                 self.oldLayerIndex = None
154                 self.oldLocation = None
155                 self.oldOrderedLocation = Vector3()
156                 self.shutdownLineIndex = sys.maxint
157                 self.nestedRingCount = 0
158                 self.threadLayer = None
159                 self.threadLayers = []
160                 self.travelFeedRateMinute = None
161
162         def addEntireLayer( self, threadLayer ):
163                 "Add entire thread layer."
164                 self.distanceFeedRate.addLines( threadLayer.beforeExtrusionLines )
165                 for island in threadLayer.islands:
166                         self.distanceFeedRate.addLines( island.lines )
167                 self.distanceFeedRate.addLines( threadLayer.afterExtrusionLines )
168
169         def addHighThread(self, location):
170                 "Add thread with a high move if necessary to clear the previous extrusion."
171                 if self.oldLocation != None:
172                         if self.oldLocation.z + self.minimumBelow < self.highestZ:
173                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.travelFeedRateMinute, self.oldLocation.dropAxis(), self.highestZ )
174                 if location.z + self.minimumBelow < self.highestZ:
175                         self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.travelFeedRateMinute, location.dropAxis(), self.highestZ )
176
177         def addThreadLayerIfNone(self):
178                 "Add a thread layer if it is none."
179                 if self.threadLayer != None:
180                         return
181                 self.threadLayer = ThreadLayer()
182                 self.threadLayers.append( self.threadLayer )
183                 self.threadLayer.beforeExtrusionLines = self.beforeExtrusionLines
184                 self.beforeExtrusionLines = []
185
186         def addTowers(self):
187                 "Add towers."
188                 bottomLayerIndex = self.getBottomLayerIndex()
189                 if bottomLayerIndex == None:
190                         return
191                 removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( self.threadLayers[ bottomLayerIndex ].islands, bottomLayerIndex )
192                 while 1:
193                         self.climbTower( removedIsland )
194                         bottomLayerIndex = self.getBottomLayerIndex()
195                         if bottomLayerIndex == None:
196                                 return
197                         removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( self.threadLayers[ bottomLayerIndex ].islands, bottomLayerIndex )
198
199         def climbTower( self, removedIsland ):
200                 "Climb up the island to any islands directly above."
201                 outsetDistance = 1.5 * self.edgeWidth
202                 for step in xrange( self.towerRepository.maximumTowerHeight.value ):
203                         aboveIndex = self.oldLayerIndex + 1
204                         if aboveIndex >= len( self.threadLayers ):
205                                 return
206                         outsetRemovedLoop = removedIsland.boundingLoop.getOutsetBoundingLoop( outsetDistance )
207                         islandsWithin = []
208                         for island in self.threadLayers[ aboveIndex ].islands:
209                                 if self.isInsideRemovedOutsideCone( island, outsetRemovedLoop, aboveIndex ):
210                                         islandsWithin.append( island )
211                         if len( islandsWithin ) < 1:
212                                 return
213                         removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( islandsWithin, aboveIndex )
214                         self.threadLayers[ aboveIndex ].islands.remove( removedIsland )
215
216         def getBottomLayerIndex(self):
217                 "Get the index of the first island layer which has islands."
218                 for islandLayerIndex in xrange( len( self.threadLayers ) ):
219                         if len( self.threadLayers[ islandLayerIndex ].islands ) > 0:
220                                 return islandLayerIndex
221                 return None
222
223         def getCraftedGcode( self, gcodeText, towerRepository ):
224                 "Parse gcode text and store the tower gcode."
225                 self.lines = archive.getTextLines(gcodeText)
226                 self.towerRepository = towerRepository
227                 self.parseInitialization()
228                 self.parseIfWordUntilWord('(<operatingLayerEnd>')
229                 self.parseIfWordUntilWord('(</skirt>)')
230                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
231                         self.parseLine( lineIndex )
232                 concatenateEndIndex = min( len( self.threadLayers ), towerRepository.towerStartLayer.value )
233                 for threadLayer in self.threadLayers[ : concatenateEndIndex ]:
234                         self.addEntireLayer( threadLayer )
235                 self.threadLayers = self.threadLayers[ concatenateEndIndex : ]
236                 self.addTowers()
237                 self.distanceFeedRate.addLines( self.lines[ self.shutdownLineIndex : ] )
238                 return self.distanceFeedRate.output.getvalue()
239
240         def getRemovedIslandAddLayerLinesIfDifferent( self, islands, layerIndex ):
241                 "Add gcode lines for the layer if it is different than the old bottom layer index."
242                 threadLayer = None
243                 if layerIndex != self.oldLayerIndex:
244                         self.oldLayerIndex = layerIndex
245                         threadLayer = self.threadLayers[layerIndex]
246                         self.distanceFeedRate.addLines( threadLayer.beforeExtrusionLines )
247                 removedIsland = self.getTransferClosestNestedRingLines( self.oldOrderedLocation, islands )
248                 if threadLayer != None:
249                         self.distanceFeedRate.addLines( threadLayer.afterExtrusionLines )
250                 return removedIsland
251
252         def getTransferClosestNestedRingLines( self, oldOrderedLocation, remainingNestedRings ):
253                 "Get and transfer the closest remaining nested ring."
254                 if len( remainingNestedRings ) > 0:
255                         oldOrderedLocation.z = remainingNestedRings[0].z
256                 closestDistance = 999999999987654321.0
257                 closestNestedRing = None
258                 for remainingNestedRing in remainingNestedRings:
259                         distance = euclidean.getClosestDistanceIndexToLine(oldOrderedLocation.dropAxis(), remainingNestedRing.boundary).distance
260                         if distance < closestDistance:
261                                 closestDistance = distance
262                                 closestNestedRing = remainingNestedRing
263                 remainingNestedRings.remove(closestNestedRing)
264                 hasTravelledHighRoad = False
265                 for line in closestNestedRing.lines:
266                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
267                         firstWord = gcodec.getFirstWord(splitLine)
268                         if firstWord == 'G1':
269                                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
270                                 if not hasTravelledHighRoad:
271                                         hasTravelledHighRoad = True
272                                         self.addHighThread(location)
273                                 if location.z > self.highestZ:
274                                         self.highestZ = location.z
275                                 self.oldLocation = location
276                         self.distanceFeedRate.addLine(line)
277                 return closestNestedRing
278
279         def isInsideRemovedOutsideCone( self, island, removedBoundingLoop, untilLayerIndex ):
280                 "Determine if the island is entirely inside the removed bounding loop and outside the collision cone of the remaining islands."
281                 if not island.boundingLoop.isEntirelyInsideAnother( removedBoundingLoop ):
282                         return False
283                 bottomLayerIndex = self.getBottomLayerIndex()
284                 coneAngleTangent = math.tan( math.radians( self.towerRepository.extruderPossibleCollisionConeAngle.value ) )
285                 for layerIndex in xrange( bottomLayerIndex, untilLayerIndex ):
286                         islands = self.threadLayers[layerIndex].islands
287                         outsetDistance = self.edgeWidth * ( untilLayerIndex - layerIndex ) * coneAngleTangent + 0.5 * self.edgeWidth
288                         for belowIsland in self.threadLayers[layerIndex].islands:
289                                 outsetIslandLoop = belowIsland.boundingLoop.getOutsetBoundingLoop( outsetDistance )
290                                 if island.boundingLoop.isOverlappingAnother( outsetIslandLoop ):
291                                         return False
292                 return True
293
294         def parseIfWordUntilWord(self, word):
295                 "Parse gcode if there is a word until the word is reached."
296                 for self.lineIndex in xrange(self.lineIndex, gcodec.getFirstWordIndexReverse(word, self.lines, self.lineIndex)):
297                         line = self.lines[self.lineIndex]
298                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
299                         firstWord = gcodec.getFirstWord(splitLine)
300                         self.distanceFeedRate.addLine(line)
301                         if firstWord == 'G1':
302                                 self.oldLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
303                                 if self.oldLocation.z > self.highestZ:
304                                         self.highestZ = self.oldLocation.z
305
306         def parseInitialization(self):
307                 'Parse gcode initialization and store the parameters.'
308                 for self.lineIndex in xrange(len(self.lines)):
309                         line = self.lines[self.lineIndex]
310                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
311                         firstWord = gcodec.getFirstWord(splitLine)
312                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
313                         if firstWord == '(</extruderInitialization>)':
314                                 self.distanceFeedRate.addTagBracketedProcedure('tower')
315                         elif firstWord == '(<layer>':
316                                 return
317                         elif firstWord == '(<layerHeight>':
318                                 self.minimumBelow = 0.1 * float(splitLine[1])
319                         elif firstWord == '(<edgeWidth>':
320                                 self.edgeWidth = float(splitLine[1])
321                         elif firstWord == '(<travelFeedRatePerSecond>':
322                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
323                         self.distanceFeedRate.addLine(line)
324
325         def parseLine( self, lineIndex ):
326                 "Parse a gcode line."
327                 line = self.lines[lineIndex]
328                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
329                 if len(splitLine) < 1:
330                         return
331                 firstWord = splitLine[0]
332                 self.afterExtrusionLines.append(line)
333                 if firstWord == 'M103':
334                         self.afterExtrusionLines = []
335                 elif firstWord == '(</boundaryPerimeter>)':
336                         self.island.createBoundingLoop()
337                 elif firstWord == '(<boundaryPoint>':
338                         self.island.addToBoundary(splitLine)
339                 elif firstWord == '(</crafting>)':
340                         self.shutdownLineIndex = lineIndex
341                 elif firstWord == '(<layer>':
342                         self.beforeExtrusionLines = [ line ]
343                         self.island = None
344                         self.nestedRingCount = 0
345                         self.threadLayer = None
346                         return
347                 elif firstWord == '(</layer>)':
348                         if self.threadLayer != None:
349                                 self.threadLayer.afterExtrusionLines = self.afterExtrusionLines
350                         self.afterExtrusionLines = []
351                 elif firstWord == '(</loop>)':
352                         self.afterExtrusionLines = []
353                 elif firstWord == '(<nestedRing>)':
354                         self.nestedRingCount += 1
355                         if self.island == None:
356                                 self.island = Island()
357                                 self.addThreadLayerIfNone()
358                                 self.threadLayer.islands.append( self.island )
359                 elif firstWord == '(</edge>)':
360                         self.afterExtrusionLines = []
361                 if self.island != None:
362                         self.island.lines.append(line)
363                 if firstWord == '(</nestedRing>)':
364                         self.afterExtrusionLines = []
365                         self.nestedRingCount -= 1
366                         if self.nestedRingCount == 0:
367                                 self.island = None
368                 if len( self.beforeExtrusionLines ) > 0:
369                         self.beforeExtrusionLines.append(line)
370
371
372 def main():
373         "Display the tower dialog."
374         if len(sys.argv) > 1:
375                 writeOutput(' '.join(sys.argv[1 :]))
376         else:
377                 settings.startMainLoopFromConstructor(getNewRepository())
378
379 if __name__ == "__main__":
380         main()