chiark / gitweb /
Add uppercase STL and HEX to file dialog filters for linux/MacOS
[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                 self.setSkirtFeedFlowTemperature()
175                 self.distanceFeedRate.addLine('(<skirt>)')
176                 oldTemperature = self.oldTemperatureInput
177                 self.addTemperatureLineIfDifferent(self.skirtTemperature)
178                 self.addFlowRate(self.skirtFlowRate)
179                 for outsetLoop in self.outsetLoops:
180                         closedLoop = outsetLoop + [outsetLoop[0]]
181                         self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, closedLoop, self.travelFeedRateMinute, z)
182                 self.addFlowRate(self.oldFlowRate)
183                 self.addTemperatureLineIfDifferent(oldTemperature)
184                 self.distanceFeedRate.addLine('(</skirt>)')
185
186         def addTemperatureLineIfDifferent(self, temperature):
187                 'Add a line of temperature if different.'
188                 if temperature == None or temperature == self.oldTemperatureInput:
189                         return
190                 self.distanceFeedRate.addLine('M104 S' + euclidean.getRoundedToThreePlaces(temperature))
191                 self.oldTemperatureInput = temperature
192
193         def createSegmentDictionaries(self, loopCrossDictionary):
194                 'Create horizontal and vertical segment dictionaries.'
195                 loopCrossDictionary.horizontalDictionary = self.getHorizontalXIntersectionsTable(loopCrossDictionary.loop)
196                 flippedLoop = euclidean.getDiagonalFlippedLoop(loopCrossDictionary.loop)
197                 loopCrossDictionary.verticalDictionary = self.getHorizontalXIntersectionsTable(flippedLoop)
198
199         def createSkirtLoops(self):
200                 'Create the skirt loops.'
201                 points = euclidean.getPointsByHorizontalDictionary(self.edgeWidth, self.unifiedLoop.horizontalDictionary)
202                 points += euclidean.getPointsByVerticalDictionary(self.edgeWidth, self.unifiedLoop.verticalDictionary)
203                 loops = triangle_mesh.getDescendingAreaOrientedLoops(points, points, 2.5 * self.edgeWidth)
204                 outerLoops = getOuterLoops(loops)
205                 self.outsetLoops = []
206                 for i in xrange(self.repository.skirtLineCount.value, 0, -1):
207                         outsetLoops = intercircle.getInsetSeparateLoopsFromLoops(outerLoops, -self.skirtOutset - i * self.edgeWidth)
208                         outsetLoops = getOuterLoops(outsetLoops)
209                         if self.repository.convex.value:
210                                 outsetLoops = [euclidean.getLoopConvex(euclidean.getConcatenatedList(outsetLoops))]
211                         self.outsetLoops.extend(outsetLoops)
212
213         def getCraftedGcode(self, gcodeText, repository):
214                 'Parse gcode text and store the skirt gcode.'
215                 self.repository = repository
216                 self.lines = archive.getTextLines(gcodeText)
217                 self.parseInitialization()
218                 self.parseBoundaries()
219                 self.createSkirtLoops()
220                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
221                         line = self.lines[self.lineIndex]
222                         self.parseLine(line)
223                 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
224
225         def getHorizontalXIntersectionsTable(self, loop):
226                 'Get the horizontal x intersections table from the loop.'
227                 horizontalXIntersectionsTable = {}
228                 euclidean.addXIntersectionsFromLoopForTable(loop, horizontalXIntersectionsTable, self.edgeWidth)
229                 return horizontalXIntersectionsTable
230
231         def parseBoundaries(self):
232                 'Parse the boundaries and union them.'
233                 self.createSegmentDictionaries(self.unifiedLoop)
234                 if self.repository.layersTo.value < 1:
235                         return
236                 loopCrossDictionary = None
237                 layerIndex = -1
238                 for line in self.lines[self.lineIndex :]:
239                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
240                         firstWord = gcodec.getFirstWord(splitLine)
241                         if firstWord == '(</boundaryPerimeter>)' or firstWord == '(</raftPerimeter>)':
242                                 self.createSegmentDictionaries(loopCrossDictionary)
243                                 self.unifyLayer(loopCrossDictionary)
244                                 loopCrossDictionary = None
245                         elif firstWord == '(<boundaryPoint>' or firstWord == '(<raftPoint>':
246                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
247                                 if loopCrossDictionary == None:
248                                         loopCrossDictionary = LoopCrossDictionary()
249                                 loopCrossDictionary.loop.append(location.dropAxis())
250                         elif firstWord == '(<layer>':
251                                 layerIndex += 1
252                                 if layerIndex > self.repository.layersTo.value:
253                                         return
254                                 settings.printProgress(layerIndex, 'skirt')
255
256         def parseInitialization(self):
257                 'Parse gcode initialization and store the parameters.'
258                 for self.lineIndex in xrange(len(self.lines)):
259                         line = self.lines[self.lineIndex]
260                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
261                         firstWord = gcodec.getFirstWord(splitLine)
262                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
263                         if firstWord == '(</extruderInitialization>)':
264                                 self.distanceFeedRate.addTagBracketedProcedure('skirt')
265                                 return
266                         elif firstWord == '(<objectNextLayersTemperature>':
267                                 self.oldTemperatureInput = float(splitLine[1])
268                                 self.skirtTemperature = self.oldTemperatureInput
269                         elif firstWord == '(<operatingFeedRatePerSecond>':
270                                 self.feedRateMinute = 60.0 * float(splitLine[1])
271                         elif firstWord == '(<operatingFlowRate>':
272                                 self.oldFlowRate = float(splitLine[1])
273                                 self.skirtFlowRate = self.oldFlowRate
274                         elif firstWord == '(<edgeWidth>':
275                                 self.edgeWidth = float(splitLine[1])
276                                 self.skirtOutset = self.repository.gapWidth.value + 0.5 * self.edgeWidth
277                                 self.distanceFeedRate.addTagRoundedLine('skirtOutset', self.skirtOutset)
278                         elif firstWord == '(<travelFeedRatePerSecond>':
279                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
280                         self.distanceFeedRate.addLine(line)
281
282         def parseLine(self, line):
283                 'Parse a gcode line and add it to the skirt skein.'
284                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
285                 if len(splitLine) < 1:
286                         return
287                 firstWord = splitLine[0]
288                 if firstWord == '(<raftPerimeter>)' or firstWord == '(</raftPerimeter>)' or firstWord == '(<raftPoint>':
289                         return
290                 self.distanceFeedRate.addLine(line)
291                 if firstWord == 'G1':
292                         self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
293                 elif firstWord == '(<layer>':
294                         self.layerIndex += 1
295                         if self.layerIndex < self.repository.layersTo.value:
296                                 self.addSkirt(float(splitLine[1]))
297                 elif firstWord == 'M101':
298                         self.isExtruderActive = True
299                 elif firstWord == 'M103':
300                         self.isExtruderActive = False
301                 elif firstWord == 'M104':
302                         self.oldTemperatureInput = gcodec.getDoubleAfterFirstLetter(splitLine[1])
303                         self.skirtTemperature = self.oldTemperatureInput
304                 elif firstWord == 'M108':
305                         self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
306                         self.skirtFlowRate = self.oldFlowRate
307                 elif firstWord == '(<supportLayer>)':
308                         self.isSupportLayer = True
309                 elif firstWord == '(</supportLayer>)':
310                         self.isSupportLayer = False
311
312         def setSkirtFeedFlowTemperature(self):
313                 'Set the skirt feed rate, flow rate and temperature to that of the next extrusion.'
314                 isExtruderActive = self.isExtruderActive
315                 isSupportLayer = self.isSupportLayer
316                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
317                         line = self.lines[lineIndex]
318                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
319                         firstWord = gcodec.getFirstWord(splitLine)
320                         if firstWord == 'G1':
321                                 self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
322                                 if isExtruderActive:
323                                         if not isSupportLayer:
324                                                 return
325                         elif firstWord == 'M101':
326                                 isExtruderActive = True
327                         elif firstWord == 'M103':
328                                 isExtruderActive = False
329                         elif firstWord == 'M104':
330                                 self.skirtTemperature = gcodec.getDoubleAfterFirstLetter(splitLine[1])
331                         elif firstWord == 'M108':
332                                 self.skirtFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
333                         elif firstWord == '(<supportLayer>)':
334                                 isSupportLayer = True
335                         elif firstWord == '(</supportLayer>)':
336                                 isSupportLayer = False
337
338         def unifyLayer(self, loopCrossDictionary):
339                 'Union the loopCrossDictionary with the unifiedLoop.'
340                 euclidean.joinXIntersectionsTables(loopCrossDictionary.horizontalDictionary, self.unifiedLoop.horizontalDictionary)
341                 euclidean.joinXIntersectionsTables(loopCrossDictionary.verticalDictionary, self.unifiedLoop.verticalDictionary)
342
343
344 def main():
345         'Display the skirt dialog.'
346         if len(sys.argv) > 1:
347                 writeOutput(' '.join(sys.argv[1 :]))
348         else:
349                 settings.startMainLoopFromConstructor(getNewRepository())
350
351 if __name__ == '__main__':
352         main()