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 / inset.py
1 #! /usr/bin/env python
2 """
3 This page is in the table of contents.
4 Inset will inset the outside outlines by half the edge width, and outset the inside outlines by the same amount.
5
6 The inset manual page is at:
7 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Inset
8
9 ==Settings==
10 ===Add Custom Code for Temperature Reading===
11 Default is on.
12
13 When selected, the M105 custom code for temperature reading will be added at the beginning of the file.
14
15 ===Infill in Direction of Bridge===
16 Default is on.
17
18 When selected, the infill will be in the direction of any bridge across a gap, so that the fill will be able to span a bridge easier.
19
20 ===Loop Order Choice===
21 Default loop order choice is 'Ascending Area'.
22
23 When overlap is to be removed, for each loop, the overlap is checked against the list of loops already extruded.  If the latest loop overlaps an already extruded loop, the overlap is removed from the latest loop.  The loops are ordered according to their areas.
24
25 ====Ascending Area====
26 When selected, the loops will be ordered in ascending area.  With thin walled parts, if overlap is being removed the outside of the container will not be extruded.  Holes will be the correct size.
27
28 ====Descending Area====
29 When selected, the loops will be ordered in descending area.  With thin walled parts, if overlap is being removed the inside of the container will not be extruded.  Holes will be missing the interior wall so they will be slightly wider than model size.
30
31 ===Overlap Removal Width over Perimeter Width===
32 Default is 0.6.
33
34 Defines the ratio of the overlap removal width over the edge width.  Any part of the extrusion that comes within the overlap removal width of another is removed.  This is to prevent the extruder from depositing two extrusions right beside each other.  If the 'Overlap Removal Width over Perimeter Width' is less than 0.2, the overlap will not be removed.
35
36 ===Turn Extruder Heater Off at Shut Down===
37 Default is on.
38
39 When selected, the M104 S0 gcode line will be added to the end of the file to turn the extruder heater off by setting the extruder heater temperature to 0.
40
41 ===Volume Fraction===
42 Default: 0.82
43
44 The 'Volume Fraction' is the estimated volume of the thread compared to the box defined by the layer height and infill width. This is used in dwindle, splodge, and statistic. It is in inset because inset is a required extrusion tool, earlier in the chain than dwindle and splodge. In dwindle and splodge it is used to determine the filament volume, in statistic it is used to determine the extrusion diameter.
45
46 ==Examples==
47 The following examples inset the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and inset.py.
48
49 > python inset.py
50 This brings up the inset dialog.
51
52 > python inset.py Screw Holder Bottom.stl
53 The inset tool is parsing the file:
54 Screw Holder Bottom.stl
55 ..
56 The inset tool has created the file:
57 .. Screw Holder Bottom_inset.gcode
58
59 """
60
61 from __future__ import absolute_import
62
63 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
64 from fabmetheus_utilities.geometry.solids import triangle_mesh
65 from fabmetheus_utilities.vector3 import Vector3
66 from fabmetheus_utilities import archive
67 from fabmetheus_utilities import euclidean
68 from fabmetheus_utilities import gcodec
69 from fabmetheus_utilities import intercircle
70 from fabmetheus_utilities import settings
71 from skeinforge_application.skeinforge_utilities import skeinforge_craft
72 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
73 from skeinforge_application.skeinforge_utilities import skeinforge_profile
74 import cmath
75 import sys
76
77
78 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
79 __date__ = '$Date: 2008/02/05 $'
80 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
81
82
83 def addAlreadyFilledArounds( alreadyFilledArounds, loop, radius ):
84         "Add already filled loops around loop to alreadyFilledArounds."
85         radius = abs(radius)
86         alreadyFilledLoop = []
87         slightlyGreaterThanRadius = intercircle.globalIntercircleMultiplier * radius
88         muchGreaterThanRadius = 2.5 * radius
89         centers = intercircle.getCentersFromLoop( loop, slightlyGreaterThanRadius )
90         for center in centers:
91                 alreadyFilledInset = intercircle.getSimplifiedInsetFromClockwiseLoop( center, radius )
92                 if intercircle.isLargeSameDirection( alreadyFilledInset, center, radius ):
93                         alreadyFilledLoop.append( alreadyFilledInset )
94         if len( alreadyFilledLoop ) > 0:
95                 alreadyFilledArounds.append( alreadyFilledLoop )
96
97 def addSegmentOutline( isThick, outlines, pointBegin, pointEnd, width ):
98         "Add a diamond or hexagonal outline for a line segment."
99         width = abs( width )
100         exclusionWidth = 0.6 * width
101         slope = 0.2
102         if isThick:
103                 slope = 3.0
104                 exclusionWidth = 0.8 * width
105         segment = pointEnd - pointBegin
106         segmentLength = abs(segment)
107         if segmentLength == 0.0:
108                 return
109         normalizedSegment = segment / segmentLength
110         outline = []
111         segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
112         pointBeginRotated = segmentYMirror * pointBegin
113         pointEndRotated = segmentYMirror * pointEnd
114         along = 0.05
115         alongLength = along * segmentLength
116         if alongLength > 0.1 * exclusionWidth:
117                 along *= 0.1 * exclusionWidth / alongLength
118         alongEnd = 1.0 - along
119         remainingToHalf = 0.5 - along
120         alongToWidth = exclusionWidth / slope / segmentLength
121         pointBeginIntermediate = euclidean.getIntermediateLocation( along, pointBeginRotated, pointEndRotated )
122         pointEndIntermediate = euclidean.getIntermediateLocation( alongEnd, pointBeginRotated, pointEndRotated )
123         outline.append( pointBeginIntermediate )
124         verticalWidth = complex( 0.0, exclusionWidth )
125         if alongToWidth > 0.9 * remainingToHalf:
126                 verticalWidth = complex( 0.0, slope * remainingToHalf * segmentLength )
127                 middle = ( pointBeginIntermediate + pointEndIntermediate ) * 0.5
128                 middleDown = middle - verticalWidth
129                 middleUp = middle + verticalWidth
130                 outline.append( middleUp )
131                 outline.append( pointEndIntermediate )
132                 outline.append( middleDown )
133         else:
134                 alongOutsideBegin = along + alongToWidth
135                 alongOutsideEnd = alongEnd - alongToWidth
136                 outsideBeginCenter = euclidean.getIntermediateLocation( alongOutsideBegin, pointBeginRotated, pointEndRotated )
137                 outsideBeginCenterDown = outsideBeginCenter - verticalWidth
138                 outsideBeginCenterUp = outsideBeginCenter + verticalWidth
139                 outsideEndCenter = euclidean.getIntermediateLocation( alongOutsideEnd, pointBeginRotated, pointEndRotated )
140                 outsideEndCenterDown = outsideEndCenter - verticalWidth
141                 outsideEndCenterUp = outsideEndCenter + verticalWidth
142                 outline.append( outsideBeginCenterUp )
143                 outline.append( outsideEndCenterUp )
144                 outline.append( pointEndIntermediate )
145                 outline.append( outsideEndCenterDown )
146                 outline.append( outsideBeginCenterDown )
147         outlines.append( euclidean.getRotatedComplexes( normalizedSegment, outline ) )
148
149 def getBridgeDirection(belowLoops, layerLoops, radius):
150         'Get span direction for the majority of the overhanging extrusion edge, if any.'
151         if len(belowLoops) < 1:
152                 return None
153         belowOutsetLoops = intercircle.getInsetLoopsFromLoops(belowLoops, -radius)
154         bridgeRotation = complex()
155         for loop in layerLoops:
156                 for pointIndex, point in enumerate(loop):
157                         previousIndex = (pointIndex + len(loop) - 1) % len(loop)
158                         bridgeRotation += getOverhangDirection(belowOutsetLoops, loop[previousIndex], point)
159         if abs(bridgeRotation) < 0.75 * radius:
160                 return None
161         else:
162                 return cmath.sqrt(bridgeRotation / abs(bridgeRotation))
163
164 def getCraftedText( fileName, text='', repository=None):
165         "Inset the preface file or text."
166         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
167
168 def getCraftedTextFromText(gcodeText, repository=None):
169         "Inset the preface gcode text."
170         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'inset'):
171                 return gcodeText
172         if repository == None:
173                 repository = settings.getReadRepository( InsetRepository() )
174         return InsetSkein().getCraftedGcode(gcodeText, repository)
175
176 def getDoubledRoundZ( overhangingSegment, segmentRoundZ ):
177         'Get doubled plane angle around z of the overhanging segment.'
178         endpoint = overhangingSegment[0]
179         roundZ = endpoint.point - endpoint.otherEndpoint.point
180         roundZ *= segmentRoundZ
181         if abs( roundZ ) == 0.0:
182                 return complex()
183         if roundZ.real < 0.0:
184                 roundZ *= - 1.0
185         roundZLength = abs( roundZ )
186         return roundZ * roundZ / roundZLength
187
188 def getInteriorSegments(loops, segments):
189         'Get segments inside the loops.'
190         interiorSegments = []
191         for segment in segments:
192                 center = 0.5 * (segment[0].point + segment[1].point)
193                 if euclidean.getIsInFilledRegion(loops, center):
194                         interiorSegments.append(segment)
195         return interiorSegments
196
197 def getIsIntersectingWithinList(loop, loopList):
198         "Determine if the loop is intersecting or is within the loop list."
199         leftPoint = euclidean.getLeftPoint(loop)
200         for otherLoop in loopList:
201                 if euclidean.getNumberOfIntersectionsToLeft(otherLoop, leftPoint) % 2 == 1:
202                         return True
203         return euclidean.isLoopIntersectingLoops(loop, loopList)
204
205 def getNewRepository():
206         'Get new repository.'
207         return InsetRepository()
208
209 def getOverhangDirection( belowOutsetLoops, segmentBegin, segmentEnd ):
210         'Add to span direction from the endpoint segments which overhang the layer below.'
211         segment = segmentEnd - segmentBegin
212         normalizedSegment = euclidean.getNormalized( complex( segment.real, segment.imag ) )
213         segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
214         segmentBegin = segmentYMirror * segmentBegin
215         segmentEnd = segmentYMirror * segmentEnd
216         solidXIntersectionList = []
217         y = segmentBegin.imag
218         solidXIntersectionList.append( euclidean.XIntersectionIndex( - 1.0, segmentBegin.real ) )
219         solidXIntersectionList.append( euclidean.XIntersectionIndex( - 1.0, segmentEnd.real ) )
220         for belowLoopIndex in xrange( len( belowOutsetLoops ) ):
221                 belowLoop = belowOutsetLoops[ belowLoopIndex ]
222                 rotatedOutset = euclidean.getRotatedComplexes( segmentYMirror, belowLoop )
223                 euclidean.addXIntersectionIndexesFromLoopY( rotatedOutset, belowLoopIndex, solidXIntersectionList, y )
224         overhangingSegments = euclidean.getSegmentsFromXIntersectionIndexes( solidXIntersectionList, y )
225         overhangDirection = complex()
226         for overhangingSegment in overhangingSegments:
227                 overhangDirection += getDoubledRoundZ( overhangingSegment, normalizedSegment )
228         return overhangDirection
229
230 def getSegmentsFromLoopListsPoints( loopLists, pointBegin, pointEnd ):
231         "Get endpoint segments from the beginning and end of a line segment."
232         normalizedSegment = pointEnd - pointBegin
233         normalizedSegmentLength = abs( normalizedSegment )
234         if normalizedSegmentLength == 0.0:
235                 return []
236         normalizedSegment /= normalizedSegmentLength
237         segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
238         pointBeginRotated = segmentYMirror * pointBegin
239         pointEndRotated = segmentYMirror * pointEnd
240         rotatedLoopLists = []
241         for loopList in loopLists:
242                 rotatedLoopLists.append(euclidean.getRotatedComplexLists(segmentYMirror, loopList))
243         xIntersectionIndexList = []
244         xIntersectionIndexList.append( euclidean.XIntersectionIndex( - 1, pointBeginRotated.real ) )
245         xIntersectionIndexList.append( euclidean.XIntersectionIndex( - 1, pointEndRotated.real ) )
246         euclidean.addXIntersectionIndexesFromLoopListsY( rotatedLoopLists, xIntersectionIndexList, pointBeginRotated.imag )
247         segments = euclidean.getSegmentsFromXIntersectionIndexes( xIntersectionIndexList, pointBeginRotated.imag )
248         for segment in segments:
249                 for endpoint in segment:
250                         endpoint.point *= normalizedSegment
251         return segments
252
253 def isCloseToLast( paths, point, radius ):
254         "Determine if the point is close to the last point of the last path."
255         if len(paths) < 1:
256                 return False
257         lastPath = paths[-1]
258         return abs( lastPath[-1] - point ) < radius
259
260 def isIntersectingItself( loop, width ):
261         "Determine if the loop is intersecting itself."
262         outlines = []
263         for pointIndex in xrange(len(loop)):
264                 pointBegin = loop[pointIndex]
265                 pointEnd = loop[(pointIndex + 1) % len(loop)]
266                 if euclidean.isLineIntersectingLoops( outlines, pointBegin, pointEnd ):
267                         return True
268                 addSegmentOutline( False, outlines, pointBegin, pointEnd, width )
269         return False
270
271 def isIntersectingWithinLists( loop, loopLists ):
272         "Determine if the loop is intersecting or is within the loop lists."
273         for loopList in loopLists:
274                 if getIsIntersectingWithinList( loop, loopList ):
275                         return True
276         return False
277
278 def writeOutput(fileName, shouldAnalyze=True):
279         "Inset the carving of a gcode file."
280         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'inset', shouldAnalyze)
281
282
283 class InsetRepository(object):
284         "A class to handle the inset settings."
285         def __init__(self):
286                 "Set the default settings, execute title & settings fileName."
287                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.inset.html', self)
288                 self.baseNameSynonymDictionary = {
289                         'Infill in Direction of Bridge' : 'carve.csv',
290                         'Infill Width over Thickness (ratio):' : 'fill.csv'}
291                 self.fileNameInput = settings.FileNameInput().getFromFileName(fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Inset', self, '')
292                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Inset')
293                 self.addCustomCodeForTemperatureReading = settings.BooleanSetting().getFromValue('Add Custom Code for Temperature Reading', self, True)
294                 self.infillInDirectionOfBridge = settings.BooleanSetting().getFromValue('Infill in Direction of Bridge', self, True)
295                 self.infillWidth = settings.FloatSpin().getFromValue(0.1, 'Infill Width:', self, 1.7, 0.4)
296                 self.loopOrderChoice = settings.MenuButtonDisplay().getFromName('Loop Order Choice:', self )
297                 self.loopOrderAscendingArea = settings.MenuRadio().getFromMenuButtonDisplay(self.loopOrderChoice, 'Ascending Area', self, True)
298                 self.loopOrderDescendingArea = settings.MenuRadio().getFromMenuButtonDisplay(self.loopOrderChoice, 'Descending Area', self, False)
299                 self.overlapRemovalWidthOverEdgeWidth = settings.FloatSpin().getFromValue(0.3, 'Overlap Removal Width over Perimeter Width (ratio):', self, 0.9, 0.6)
300                 self.turnExtruderHeaterOffAtShutDown = settings.BooleanSetting().getFromValue('Turn Extruder Heater Off at Shut Down', self, True)
301                 self.volumeFraction = settings.FloatSpin().getFromValue(0.7, 'Volume Fraction (ratio):', self, 0.9, 0.82)
302                 self.executeTitle = 'Inset'
303
304         def execute(self):
305                 "Inset button has been clicked."
306                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
307                 for fileName in fileNames:
308                         writeOutput(fileName)
309
310
311 class InsetSkein(object):
312         "A class to inset a skein of extrusions."
313         def __init__(self):
314                 'Initialize.'
315                 self.belowLoops = []
316                 self.boundary = None
317                 self.distanceFeedRate = gcodec.DistanceFeedRate()
318                 self.layerCount = settings.LayerCount()
319                 self.lineIndex = 0
320                 self.loopLayer = None
321
322         def addGcodeFromPerimeterPaths(self, isIntersectingSelf, loop, loopLayer, loopLists, radius):
323                 "Add the edge paths to the output."
324                 segments = []
325                 outlines = []
326                 thickOutlines = []
327                 allLoopLists = loopLists[:] + [thickOutlines]
328                 aroundLists = loopLists
329                 for pointIndex in xrange(len(loop)):
330                         pointBegin = loop[pointIndex]
331                         pointEnd = loop[(pointIndex + 1) % len(loop)]
332                         if isIntersectingSelf:
333                                 if euclidean.isLineIntersectingLoops(outlines, pointBegin, pointEnd):
334                                         segments += getSegmentsFromLoopListsPoints(allLoopLists, pointBegin, pointEnd)
335                                 else:
336                                         segments += getSegmentsFromLoopListsPoints(loopLists, pointBegin, pointEnd)
337                                 addSegmentOutline(False, outlines, pointBegin, pointEnd, self.overlapRemovalWidth)
338                                 addSegmentOutline(True, thickOutlines, pointBegin, pointEnd, self.overlapRemovalWidth)
339                         else:
340                                 segments += getSegmentsFromLoopListsPoints(loopLists, pointBegin, pointEnd)
341                 edgePaths = []
342                 path = []
343                 muchSmallerThanRadius = 0.1 * radius
344                 segments = getInteriorSegments(loopLayer.loops, segments)
345                 for segment in segments:
346                         pointBegin = segment[0].point
347                         if not isCloseToLast(edgePaths, pointBegin, muchSmallerThanRadius):
348                                 path = [pointBegin]
349                                 edgePaths.append(path)
350                         path.append(segment[1].point)
351                 if len(edgePaths) > 1:
352                         firstPath = edgePaths[0]
353                         lastPath = edgePaths[-1]
354                         if abs(lastPath[-1] - firstPath[0]) < 0.1 * muchSmallerThanRadius:
355                                 connectedBeginning = lastPath[: -1] + firstPath
356                                 edgePaths[0] = connectedBeginning
357                                 edgePaths.remove(lastPath)
358                 muchGreaterThanRadius = 6.0 * radius
359                 for edgePath in edgePaths:
360                         if euclidean.getPathLength(edgePath) > muchGreaterThanRadius:
361                                 self.distanceFeedRate.addGcodeFromThreadZ(edgePath, loopLayer.z)
362
363         def addGcodeFromRemainingLoop(self, loop, loopLayer, loopLists, radius):
364                 "Add the remainder of the loop which does not overlap the alreadyFilledArounds loops."
365                 centerOutset = intercircle.getLargestCenterOutsetLoopFromLoopRegardless(loop, radius)
366                 euclidean.addNestedRingBeginning(self.distanceFeedRate, centerOutset.outset, loopLayer.z)
367                 self.addGcodePerimeterBlockFromRemainingLoop(centerOutset.center, loopLayer, loopLists, radius)
368                 self.distanceFeedRate.addLine('(</boundaryPerimeter>)')
369                 self.distanceFeedRate.addLine('(</nestedRing>)')
370
371         def addGcodePerimeterBlockFromRemainingLoop(self, loop, loopLayer, loopLists, radius):
372                 "Add the perimter block remainder of the loop which does not overlap the alreadyFilledArounds loops."
373                 if self.repository.overlapRemovalWidthOverEdgeWidth.value < 0.2:
374                         self.distanceFeedRate.addPerimeterBlock(loop, loopLayer.z)
375                         return
376                 isIntersectingSelf = isIntersectingItself(loop, self.overlapRemovalWidth)
377                 if isIntersectingWithinLists(loop, loopLists) or isIntersectingSelf:
378                         self.addGcodeFromPerimeterPaths(isIntersectingSelf, loop, loopLayer, loopLists, radius)
379                 else:
380                         self.distanceFeedRate.addPerimeterBlock(loop, loopLayer.z)
381                 addAlreadyFilledArounds(loopLists, loop, self.overlapRemovalWidth)
382
383         def addInitializationToOutput(self):
384                 "Add initialization gcode to the output."
385                 if self.repository.addCustomCodeForTemperatureReading.value:
386                         self.distanceFeedRate.addLine('M105') # Custom code for temperature reading.
387
388         def addInset(self, loopLayer):
389                 "Add inset to the layer."
390                 alreadyFilledArounds = []
391                 extrudateLoops = intercircle.getInsetLoopsFromLoops(loopLayer.loops, self.halfEdgeWidth)
392                 if self.repository.infillInDirectionOfBridge.value:
393                         bridgeRotation = getBridgeDirection(self.belowLoops, extrudateLoops, self.halfEdgeWidth)
394                         if bridgeRotation != None:
395                                 self.distanceFeedRate.addTagBracketedLine('bridgeRotation', bridgeRotation)
396                 self.belowLoops = loopLayer.loops
397                 triangle_mesh.sortLoopsInOrderOfArea(not self.repository.loopOrderAscendingArea.value, extrudateLoops)
398                 for extrudateLoop in extrudateLoops:
399                         self.addGcodeFromRemainingLoop(extrudateLoop, loopLayer, alreadyFilledArounds, self.halfEdgeWidth)
400
401         def getCraftedGcode(self, gcodeText, repository):
402                 "Parse gcode text and store the bevel gcode."
403                 self.repository = repository
404                 self.lines = archive.getTextLines(gcodeText)
405                 self.parseInitialization()
406                 for line in self.lines[self.lineIndex :]:
407                         self.parseLine(line)
408                 return self.distanceFeedRate.output.getvalue()
409
410         def parseInitialization(self):
411                 'Parse gcode initialization and store the parameters.'
412                 for self.lineIndex in xrange(len(self.lines)):
413                         line = self.lines[self.lineIndex]
414                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
415                         firstWord = gcodec.getFirstWord(splitLine)
416                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
417                         if firstWord == '(<decimalPlacesCarried>':
418                                 self.addInitializationToOutput()
419                         elif firstWord == '(</extruderInitialization>)':
420                                 self.distanceFeedRate.addTagBracketedProcedure('inset')
421                                 return
422                         elif firstWord == '(<layerHeight>':
423                                 layerHeight = float(splitLine[1])
424                                 self.infillWidth = self.repository.infillWidth.value
425                                 self.distanceFeedRate.addTagRoundedLine('infillWidth', self.infillWidth)
426                                 self.distanceFeedRate.addTagRoundedLine('volumeFraction', self.repository.volumeFraction.value)
427                         elif firstWord == '(<edgeWidth>':
428                                 self.edgeWidth = float(splitLine[1])
429                                 self.halfEdgeWidth = 0.5 * self.edgeWidth
430                                 self.overlapRemovalWidth = self.edgeWidth * self.repository.overlapRemovalWidthOverEdgeWidth.value
431                         self.distanceFeedRate.addLine(line)
432
433         def parseLine(self, line):
434                 "Parse a gcode line and add it to the inset skein."
435                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
436                 if len(splitLine) < 1:
437                         return
438                 firstWord = splitLine[0]
439                 if firstWord == '(<boundaryPoint>':
440                         location = gcodec.getLocationFromSplitLine(None, splitLine)
441                         self.boundary.append(location.dropAxis())
442                 elif firstWord == '(</crafting>)':
443                                 self.distanceFeedRate.addLine(line)
444                                 if self.repository.turnExtruderHeaterOffAtShutDown.value:
445                                         self.distanceFeedRate.addLine('M104 S0') # Turn extruder heater off.
446                                 return
447                 elif firstWord == '(<layer>':
448                         self.layerCount.printProgressIncrement('inset')
449                         self.loopLayer = euclidean.LoopLayer(float(splitLine[1]))
450                         self.distanceFeedRate.addLine(line)
451                 elif firstWord == '(</layer>)':
452                         self.addInset(self.loopLayer)
453                         self.loopLayer = None
454                 elif firstWord == '(<nestedRing>)':
455                         self.boundary = []
456                         self.loopLayer.loops.append(self.boundary)
457                 if self.loopLayer == None:
458                         self.distanceFeedRate.addLine(line)
459
460
461 def main():
462         "Display the inset dialog."
463         if len(sys.argv) > 1:
464                 writeOutput(' '.join(sys.argv[1 :]))
465         else:
466                 settings.startMainLoopFromConstructor(getNewRepository())
467
468 if __name__ == "__main__":
469         main()