chiark / gitweb /
Fixed positioning when using multiple extruders. Fixed GCode preview for multiple...
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / skirt.py
1 """
2 This page is in the table of contents.
3 Skirt is a plugin to give the extruder some extra time to begin extruding properly before beginning the object, and to put a baffle around the model in order to keep the extrusion warm.
4
5 The skirt manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skirt
7
8 It is loosely based on Lenbook's outline plugin:
9
10 http://www.thingiverse.com/thing:4918
11
12 it is also loosely based on the outline that Nophead sometimes uses:
13
14 http://hydraraptor.blogspot.com/2010/01/hot-metal-and-serendipity.html
15
16 and also loosely based on the baffles that Nophead made to keep corners warm:
17
18 http://hydraraptor.blogspot.com/2010/09/some-corners-like-it-hot.html
19
20 If you want only an outline, set 'Layers To' to one.  This gives the extruder some extra time to begin extruding properly before beginning your object, and gives you an early verification of where your object will be extruded.
21
22 If you also want an insulating skirt around the entire object, set 'Layers To' to a huge number, like 912345678.  This will additionally make an insulating baffle around the object; to prevent moving air from cooling the object, which increases warping, especially in corners.
23
24 ==Operation==
25 The default 'Activate Skirt' checkbox is off.  When it is on, the functions described below will work, when it is off, nothing will be done.
26
27 ==Settings==
28 ===Convex===
29 Default is on.
30
31 When selected, the skirt will be convex, going around the model with only convex angles.  If convex is not selected, the skirt will hug the model, going into every nook and cranny.
32
33 ===Gap over Perimeter Width===
34 Default is three.
35
36 Defines the ratio of the gap between the object and the skirt over the edge width.  If the ratio is too low, the skirt will connect to the object, if the ratio is too high, the skirt willl not provide much insulation for the object.
37
38 ===Layers To===
39 Default is a one.
40
41 Defines the number of layers of the skirt.  If you want only an outline, set 'Layers To' to one.  If you want an insulating skirt around the entire object, set 'Layers To' to a huge number, like 912345678.
42
43 ==Examples==
44 The following examples skirt the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and skirt.py.
45
46 > python skirt.py
47 This brings up the skirt dialog.
48
49 > python skirt.py Screw Holder Bottom.stl
50 The skirt tool is parsing the file:
51 Screw Holder Bottom.stl
52 ..
53 The skirt tool has created the file:
54 .. Screw Holder Bottom_skirt.gcode
55
56 """
57
58
59 from __future__ import absolute_import
60 #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.
61 import __init__
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 math
75 import sys
76
77
78 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
79 __date__ = '$Date: 2008/21/04 $'
80 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
81
82
83 def getCraftedText(fileName, text='', repository=None):
84         'Skirt the fill file or text.'
85         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
86
87 def getCraftedTextFromText(gcodeText, repository=None):
88         'Skirt the fill text.'
89         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'skirt'):
90                 return gcodeText
91         if repository == None:
92                 repository = settings.getReadRepository(SkirtRepository())
93         if repository.skirtLineCount.value < 1:
94                 return gcodeText
95         return SkirtSkein().getCraftedGcode(gcodeText, repository)
96
97 def getNewRepository():
98         'Get new repository.'
99         return SkirtRepository()
100
101 def getOuterLoops(loops):
102         'Get widdershins outer loops.'
103         outerLoops = []
104         for loop in loops:
105                 if not euclidean.isPathInsideLoops(outerLoops, loop):
106                         outerLoops.append(loop)
107         intercircle.directLoops(True, outerLoops)
108         return outerLoops
109
110 def writeOutput(fileName, shouldAnalyze=True):
111         'Skirt a gcode linear move file.'
112         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'skirt', shouldAnalyze)
113
114
115 class LoopCrossDictionary:
116         'Loop with a horizontal and vertical dictionary.'
117         def __init__(self):
118                 'Initialize LoopCrossDictionary.'
119                 self.loop = []
120
121         def __repr__(self):
122                 'Get the string representation of this LoopCrossDictionary.'
123                 return str(self.loop)
124
125
126 class SkirtRepository:
127         'A class to handle the skirt settings.'
128         def __init__(self):
129                 'Set the default settings, execute title & settings fileName.'
130                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.skirt.html', self)
131                 self.fileNameInput = settings.FileNameInput().getFromFileName(
132                         fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Skirt', self, '')
133                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skirt')
134                 self.skirtLineCount = settings.IntSpin().getSingleIncrementFromValue(0, 'Skirt line count', self, 20, 1)
135                 self.convex = settings.BooleanSetting().getFromValue('Convex:', self, True)
136                 self.gapWidth = settings.FloatSpin().getFromValue(1.0, 'Gap Width (mm):', self, 5.0, 3.0)
137                 self.layersTo = settings.IntSpin().getSingleIncrementFromValue(0, 'Layers To (index):', self, 912345678, 1)
138                 self.executeTitle = 'Skirt'
139
140         def execute(self):
141                 'Skirt button has been clicked.'
142                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(
143                         self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
144                 for fileName in fileNames:
145                         writeOutput(fileName)
146
147
148 class SkirtSkein:
149         'A class to skirt a skein of extrusions.'
150         def __init__(self):
151                 'Initialize variables.'
152                 self.distanceFeedRate = gcodec.DistanceFeedRate()
153                 self.feedRateMinute = 961.0
154                 self.isExtruderActive = False
155                 self.isSupportLayer = False
156                 self.layerIndex = -1
157                 self.lineIndex = 0
158                 self.lines = None
159                 self.oldFlowRate = None
160                 self.oldLocation = None
161                 self.oldTemperatureInput = None
162                 self.skirtFlowRate = None
163                 self.skirtTemperature = None
164                 self.travelFeedRateMinute = 957.0
165                 self.unifiedLoop = LoopCrossDictionary()
166
167         def addFlowRate(self, flowRate):
168                 'Add a line of temperature if different.'
169                 if flowRate != None:
170                         self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
171
172         def addSkirt(self, z):
173                 'At skirt at z to gcode output.'
174                 if len(self.outsetLoops) < 1 or len(self.outsetLoops[0]) < 1:
175                         return
176                 self.setSkirtFeedFlowTemperature()
177                 self.distanceFeedRate.addLine('(<skirt>)')
178                 oldTemperature = self.oldTemperatureInput
179                 self.addTemperatureLineIfDifferent(self.skirtTemperature)
180                 self.addFlowRate(self.skirtFlowRate)
181                 for outsetLoop in self.outsetLoops:
182                         closedLoop = outsetLoop + [outsetLoop[0]]
183                         self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, closedLoop, self.travelFeedRateMinute, z)
184                 self.addFlowRate(self.oldFlowRate)
185                 self.addTemperatureLineIfDifferent(oldTemperature)
186                 self.distanceFeedRate.addLine('(</skirt>)')
187
188         def addTemperatureLineIfDifferent(self, temperature):
189                 'Add a line of temperature if different.'
190                 if temperature == None or temperature == self.oldTemperatureInput:
191                         return
192                 self.distanceFeedRate.addLine('M104 S' + euclidean.getRoundedToThreePlaces(temperature))
193                 self.oldTemperatureInput = temperature
194
195         def createSegmentDictionaries(self, loopCrossDictionary):
196                 'Create horizontal and vertical segment dictionaries.'
197                 loopCrossDictionary.horizontalDictionary = self.getHorizontalXIntersectionsTable(loopCrossDictionary.loop)
198                 flippedLoop = euclidean.getDiagonalFlippedLoop(loopCrossDictionary.loop)
199                 loopCrossDictionary.verticalDictionary = self.getHorizontalXIntersectionsTable(flippedLoop)
200
201         def createSkirtLoops(self):
202                 'Create the skirt loops.'
203                 points = euclidean.getPointsByHorizontalDictionary(self.edgeWidth, self.unifiedLoop.horizontalDictionary)
204                 points += euclidean.getPointsByVerticalDictionary(self.edgeWidth, self.unifiedLoop.verticalDictionary)
205                 loops = triangle_mesh.getDescendingAreaOrientedLoops(points, points, 2.5 * self.edgeWidth)
206                 outerLoops = getOuterLoops(loops)
207                 self.outsetLoops = []
208                 for i in xrange(self.repository.skirtLineCount.value, 0, -1):
209                         outsetLoops = intercircle.getInsetSeparateLoopsFromLoops(outerLoops, -self.skirtOutset - i * self.edgeWidth)
210                         outsetLoops = getOuterLoops(outsetLoops)
211                         if self.repository.convex.value:
212                                 outsetLoops = [euclidean.getLoopConvex(euclidean.getConcatenatedList(outsetLoops))]
213                         self.outsetLoops.extend(outsetLoops)
214
215         def getCraftedGcode(self, gcodeText, repository):
216                 'Parse gcode text and store the skirt gcode.'
217                 self.repository = repository
218                 self.lines = archive.getTextLines(gcodeText)
219                 self.parseInitialization()
220                 self.parseBoundaries()
221                 self.createSkirtLoops()
222                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
223                         line = self.lines[self.lineIndex]
224                         self.parseLine(line)
225                 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
226
227         def getHorizontalXIntersectionsTable(self, loop):
228                 'Get the horizontal x intersections table from the loop.'
229                 horizontalXIntersectionsTable = {}
230                 euclidean.addXIntersectionsFromLoopForTable(loop, horizontalXIntersectionsTable, self.edgeWidth)
231                 return horizontalXIntersectionsTable
232
233         def parseBoundaries(self):
234                 'Parse the boundaries and union them.'
235                 self.createSegmentDictionaries(self.unifiedLoop)
236                 if self.repository.layersTo.value < 1:
237                         return
238                 loopCrossDictionary = None
239                 layerIndex = -1
240                 for line in self.lines[self.lineIndex :]:
241                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
242                         firstWord = gcodec.getFirstWord(splitLine)
243                         if firstWord == '(</boundaryPerimeter>)' or firstWord == '(</raftPerimeter>)':
244                                 self.createSegmentDictionaries(loopCrossDictionary)
245                                 self.unifyLayer(loopCrossDictionary)
246                                 loopCrossDictionary = None
247                         elif firstWord == '(<boundaryPoint>' or firstWord == '(<raftPoint>':
248                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
249                                 if loopCrossDictionary == None:
250                                         loopCrossDictionary = LoopCrossDictionary()
251                                 loopCrossDictionary.loop.append(location.dropAxis())
252                         elif firstWord == '(<layer>':
253                                 layerIndex += 1
254                                 if layerIndex > self.repository.layersTo.value:
255                                         return
256                                 settings.printProgress(layerIndex, 'skirt')
257
258         def parseInitialization(self):
259                 'Parse gcode initialization and store the parameters.'
260                 for self.lineIndex in xrange(len(self.lines)):
261                         line = self.lines[self.lineIndex]
262                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
263                         firstWord = gcodec.getFirstWord(splitLine)
264                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
265                         if firstWord == '(</extruderInitialization>)':
266                                 self.distanceFeedRate.addTagBracketedProcedure('skirt')
267                                 return
268                         elif firstWord == '(<objectNextLayersTemperature>':
269                                 self.oldTemperatureInput = float(splitLine[1])
270                                 self.skirtTemperature = self.oldTemperatureInput
271                         elif firstWord == '(<operatingFeedRatePerSecond>':
272                                 self.feedRateMinute = 60.0 * float(splitLine[1])
273                         elif firstWord == '(<operatingFlowRate>':
274                                 self.oldFlowRate = float(splitLine[1])
275                                 self.skirtFlowRate = self.oldFlowRate
276                         elif firstWord == '(<edgeWidth>':
277                                 self.edgeWidth = float(splitLine[1])
278                                 self.skirtOutset = self.repository.gapWidth.value + 0.5 * self.edgeWidth
279                                 self.distanceFeedRate.addTagRoundedLine('skirtOutset', self.skirtOutset)
280                         elif firstWord == '(<travelFeedRatePerSecond>':
281                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
282                         self.distanceFeedRate.addLine(line)
283
284         def parseLine(self, line):
285                 'Parse a gcode line and add it to the skirt skein.'
286                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
287                 if len(splitLine) < 1:
288                         return
289                 firstWord = splitLine[0]
290                 if firstWord == '(<raftPerimeter>)' or firstWord == '(</raftPerimeter>)' or firstWord == '(<raftPoint>':
291                         return
292                 self.distanceFeedRate.addLine(line)
293                 if firstWord == 'G1':
294                         self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
295                 elif firstWord == '(<layer>':
296                         self.layerIndex += 1
297                         if self.layerIndex < self.repository.layersTo.value:
298                                 self.addSkirt(float(splitLine[1]))
299                 elif firstWord == 'M101':
300                         self.isExtruderActive = True
301                 elif firstWord == 'M103':
302                         self.isExtruderActive = False
303                 elif firstWord == 'M104':
304                         self.oldTemperatureInput = gcodec.getDoubleAfterFirstLetter(splitLine[1])
305                         self.skirtTemperature = self.oldTemperatureInput
306                 elif firstWord == 'M108':
307                         self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
308                         self.skirtFlowRate = self.oldFlowRate
309                 elif firstWord == '(<supportLayer>)':
310                         self.isSupportLayer = True
311                 elif firstWord == '(</supportLayer>)':
312                         self.isSupportLayer = False
313
314         def setSkirtFeedFlowTemperature(self):
315                 'Set the skirt feed rate, flow rate and temperature to that of the next extrusion.'
316                 isExtruderActive = self.isExtruderActive
317                 isSupportLayer = self.isSupportLayer
318                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
319                         line = self.lines[lineIndex]
320                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
321                         firstWord = gcodec.getFirstWord(splitLine)
322                         if firstWord == 'G1':
323                                 self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
324                                 if isExtruderActive:
325                                         if not isSupportLayer:
326                                                 return
327                         elif firstWord == 'M101':
328                                 isExtruderActive = True
329                         elif firstWord == 'M103':
330                                 isExtruderActive = False
331                         elif firstWord == 'M104':
332                                 self.skirtTemperature = gcodec.getDoubleAfterFirstLetter(splitLine[1])
333                         elif firstWord == 'M108':
334                                 self.skirtFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
335                         elif firstWord == '(<supportLayer>)':
336                                 isSupportLayer = True
337                         elif firstWord == '(</supportLayer>)':
338                                 isSupportLayer = False
339
340         def unifyLayer(self, loopCrossDictionary):
341                 'Union the loopCrossDictionary with the unifiedLoop.'
342                 euclidean.joinXIntersectionsTables(loopCrossDictionary.horizontalDictionary, self.unifiedLoop.horizontalDictionary)
343                 euclidean.joinXIntersectionsTables(loopCrossDictionary.verticalDictionary, self.unifiedLoop.verticalDictionary)
344
345
346 def main():
347         'Display the skirt dialog.'
348         if len(sys.argv) > 1:
349                 writeOutput(' '.join(sys.argv[1 :]))
350         else:
351                 settings.startMainLoopFromConstructor(getNewRepository())
352
353 if __name__ == '__main__':
354         main()