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