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.
5 The tower manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Tower
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.
12 ===Maximum Tower Height===
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.
17 ===Extruder Possible Collision Cone Angle===
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.
22 ===Tower Start Layer===
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.
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.
31 This brings up the tower dialog.
33 > python tower.py Screw Holder Bottom.stl
34 The tower tool is parsing the file:
35 Screw Holder Bottom.stl
37 The tower tool has created the file:
38 .. Screw Holder Bottom_tower.gcode
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.
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
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'
64 def getCraftedText( fileName, text, towerRepository = None ):
65 "Tower a gcode linear move file or text."
66 return getCraftedTextFromText( archive.getTextIfEmpty(fileName, text), towerRepository )
68 def getCraftedTextFromText( gcodeText, towerRepository = None ):
69 "Tower a gcode linear move text."
70 if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'tower'):
72 if towerRepository == None:
73 towerRepository = settings.getReadRepository( TowerRepository() )
74 if not towerRepository.activateTower.value:
76 return TowerSkein().getCraftedGcode( gcodeText, towerRepository )
78 def getNewRepository():
80 return TowerRepository()
82 def writeOutput(fileName, shouldAnalyze=True):
83 "Tower a gcode linear move file."
84 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'tower', shouldAnalyze)
88 "A class to hold the boundary and lines."
91 self.boundingLoop = None
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())
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 )
108 "A layer of loops and paths."
110 "Thread layer constructor."
111 self.afterExtrusionLines = []
112 self.beforeExtrusionLines = []
116 "Get the string representation of this thread layer."
117 return '%s' % self.islands
120 class TowerRepository:
121 "A class to handle the tower settings."
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'
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)
141 "A class to tower a skein of extrusions."
143 self.afterExtrusionLines = []
144 self.beforeExtrusionLines = []
145 self.distanceFeedRate = gcodec.DistanceFeedRate()
147 self.highestZ = - 987654321.0
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
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 )
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 )
177 def addThreadLayerIfNone(self):
178 "Add a thread layer if it is none."
179 if self.threadLayer != None:
181 self.threadLayer = ThreadLayer()
182 self.threadLayers.append( self.threadLayer )
183 self.threadLayer.beforeExtrusionLines = self.beforeExtrusionLines
184 self.beforeExtrusionLines = []
188 bottomLayerIndex = self.getBottomLayerIndex()
189 if bottomLayerIndex == None:
191 removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( self.threadLayers[ bottomLayerIndex ].islands, bottomLayerIndex )
193 self.climbTower( removedIsland )
194 bottomLayerIndex = self.getBottomLayerIndex()
195 if bottomLayerIndex == None:
197 removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( self.threadLayers[ bottomLayerIndex ].islands, bottomLayerIndex )
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 ):
206 outsetRemovedLoop = removedIsland.boundingLoop.getOutsetBoundingLoop( outsetDistance )
208 for island in self.threadLayers[ aboveIndex ].islands:
209 if self.isInsideRemovedOutsideCone( island, outsetRemovedLoop, aboveIndex ):
210 islandsWithin.append( island )
211 if len( islandsWithin ) < 1:
213 removedIsland = self.getRemovedIslandAddLayerLinesIfDifferent( islandsWithin, aboveIndex )
214 self.threadLayers[ aboveIndex ].islands.remove( removedIsland )
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
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 : ]
237 self.distanceFeedRate.addLines( self.lines[ self.shutdownLineIndex : ] )
238 return self.distanceFeedRate.output.getvalue()
240 def getRemovedIslandAddLayerLinesIfDifferent( self, islands, layerIndex ):
241 "Add gcode lines for the layer if it is different than the old bottom layer index."
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 )
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
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 ):
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 ):
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
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>':
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)
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:
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 ]
344 self.nestedRingCount = 0
345 self.threadLayer = None
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:
368 if len( self.beforeExtrusionLines ) > 0:
369 self.beforeExtrusionLines.append(line)
373 "Display the tower dialog."
374 if len(sys.argv) > 1:
375 writeOutput(' '.join(sys.argv[1 :]))
377 settings.startMainLoopFromConstructor(getNewRepository())
379 if __name__ == "__main__":