chiark / gitweb /
Move SF into its own directory, to seperate SF and Cura. Rename newui to gui.
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / cool.py
1 """
2 This page is in the table of contents.
3 Cool is a craft tool to cool the shape.
4
5 Cool works well with a stepper extruder, it does not work well with a DC motor extruder.
6
7 If enabled, before each layer that takes less then "Minimum Layer Time" to print the tool head will orbit around the printed area for 'Minimum Layer Time' minus 'the time it takes to print the layer' before it starts printing the layer. This is great way to let layers with smaller area cool before you start printing on top of them (so you do not overheat the area). 
8
9 The cool manual page is at:
10 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Cool
11
12 Allan Ecker aka The Masked Retriever's has written the "Skeinforge Quicktip: Cool" at:
13 http://blog.thingiverse.com/2009/07/28/skeinforge-quicktip-cool/
14
15 ==Operation==
16 The default 'Activate Cool' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions will not be called.
17
18 ==Settings==
19 ===Bridge Cool===
20 Default is one degree Celcius.
21
22 If the layer is a bridge layer, then cool will lower the temperature by 'Bridge Cool' degrees Celcius.
23
24 ===Cool Type===
25 Default is 'Slow Down'.
26
27 ====Orbit====
28 When selected, cool will add orbits with the extruder off to give the layer time to cool, so that the next layer is not extruded on a molten base.  The orbits will be around the largest island on that layer.  Orbit should only be chosen if you can not upgrade to a stepper extruder.
29
30 ====Slow Down====
31 When selected, cool will slow down the extruder so that it will take the minimum layer time to extrude the layer.  DC motors do not operate properly at slow flow rates, so if you have a DC motor extruder, you should upgrade to a stepper extruder, but if you can't do that, you can try using the 'Orbit' option.
32
33 ===Maximum Cool===
34 Default is 2 degrees Celcius.
35
36 If it takes less time to extrude the layer than the minimum layer time, then cool will lower the temperature by the 'Maximum Cool' setting times the layer time over the minimum layer time.
37
38 ===Minimum Layer Time===
39 Default is 60 seconds.
40
41 Defines the minimum amount of time the extruder will spend on a layer, this is an important setting.
42
43 ===Minimum Orbital Radius===
44 Default is 10 millimeters.
45
46 When the orbit cool type is selected, if the area of the largest island is as large as the square of the "Minimum Orbital Radius" then the orbits will be just within the island.  If the island is smaller, then the orbits will be in a square of the "Minimum Orbital Radius" around the center of the island.  This is so that the hot extruder does not stay too close to small islands.
47
48 ===Name of Alteration Files===
49 Cool looks for alteration files in the alterations folder in the .skeinforge folder in the home directory.  Cool does not care if the text file names are capitalized, but some file systems do not handle file name cases properly, so to be on the safe side you should give them lower case names.  If it doesn't find the file it then looks in the alterations folder in the skeinforge_plugins folder.  The cool start and end text idea is from:
50 http://makerhahn.blogspot.com/2008/10/yay-minimug.html
51
52 ====Name of Cool End File====
53 Default is cool_end.gcode.
54
55 If there is a file with the name of the "Name of Cool End File" setting, it will be added to the end of the orbits.
56
57 ====Name of Cool Start File====
58 Default is cool_start.gcode.
59
60 If there is a file with the name of the "Name of Cool Start File" setting, it will be added to the start of the orbits.
61
62 ===Orbital Outset===
63 Default is 2 millimeters.
64
65 When the orbit cool type is selected, the orbits will be outset around the largest island by 'Orbital Outset' millimeters.  If 'Orbital Outset' is negative, the orbits will be inset instead.
66
67 ===Turn Fan On at Beginning===
68 Default is on.
69
70 When selected, cool will turn the fan on at the beginning of the fabrication by adding the M106 command.
71
72 ===Turn Fan Off at Ending===
73 Default is on.
74
75 When selected, cool will turn the fan off at the ending of the fabrication by adding the M107 command.
76
77 ==Examples==
78 The following examples cool the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and cool.py.
79
80 > python cool.py
81 This brings up the cool dialog.
82
83 > python cool.py Screw Holder Bottom.stl
84 The cool tool is parsing the file:
85 Screw Holder Bottom.stl
86 ..
87 The cool tool has created the file:
88 .. Screw Holder Bottom_cool.gcode
89
90 """
91
92 from __future__ import absolute_import
93 #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.
94 import __init__
95
96 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
97 from fabmetheus_utilities import archive
98 from fabmetheus_utilities import euclidean
99 from fabmetheus_utilities import gcodec
100 from fabmetheus_utilities import intercircle
101 from fabmetheus_utilities import settings
102 from skeinforge_application.skeinforge_utilities import skeinforge_craft
103 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
104 from skeinforge_application.skeinforge_utilities import skeinforge_profile
105 import os
106 import sys
107
108
109 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
110 __date__ = '$Date: 2008/21/04 $'
111 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
112
113
114 def getCraftedText(fileName, text, repository=None):
115         'Cool a gcode linear move text.'
116         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
117
118 def getCraftedTextFromText(gcodeText, repository=None):
119         'Cool a gcode linear move text.'
120         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'cool'):
121                 return gcodeText
122         if repository == None:
123                 repository = settings.getReadRepository(CoolRepository())
124         if not repository.activateCool.value:
125                 return gcodeText
126         return CoolSkein().getCraftedGcode(gcodeText, repository)
127
128 def getNewRepository():
129         'Get new repository.'
130         return CoolRepository()
131
132 def writeOutput(fileName, shouldAnalyze=True):
133         'Cool a gcode linear move file.  Chain cool the gcode if it is not already cooled.'
134         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'cool', shouldAnalyze)
135
136
137 class CoolRepository:
138         'A class to handle the cool settings.'
139         def __init__(self):
140                 'Set the default settings, execute title & settings fileName.'
141                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.cool.html', self )
142                 self.fileNameInput = settings.FileNameInput().getFromFileName(
143                         fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Cool', self, '')
144                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute(
145                         'http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Cool')
146                 self.activateCool = settings.BooleanSetting().getFromValue('Activate Cool', self, True)
147                 self.bridgeCool = settings.FloatSpin().getFromValue(0.0, 'Bridge Cool (Celcius):', self, 10.0, 1.0)
148                 self.coolType = settings.MenuButtonDisplay().getFromName('Cool Type:', self)
149                 self.orbit = settings.MenuRadio().getFromMenuButtonDisplay(self.coolType, 'Orbit', self, False)
150                 self.slowDown = settings.MenuRadio().getFromMenuButtonDisplay(self.coolType, 'Slow Down', self, True)
151                 self.maximumCool = settings.FloatSpin().getFromValue(0.0, 'Maximum Cool (Celcius):', self, 10.0, 2.0)
152                 self.minimumLayerTime = settings.FloatSpin().getFromValue(0.0, 'Minimum Layer Time (seconds):', self, 120.0, 10.0)
153                 self.minimumOrbitalRadius = settings.FloatSpin().getFromValue(
154                         0.0, 'Minimum Orbital Radius (millimeters):', self, 20.0, 10.0)
155                 settings.LabelSeparator().getFromRepository(self)
156                 settings.LabelDisplay().getFromName('- Name of Alteration Files -', self )
157                 self.nameOfCoolEndFile = settings.StringSetting().getFromValue('Name of Cool End File:', self, 'cool_end.gcode')
158                 self.nameOfCoolStartFile = settings.StringSetting().getFromValue('Name of Cool Start File:', self, 'cool_start.gcode')
159                 settings.LabelSeparator().getFromRepository(self)
160                 self.orbitalOutset = settings.FloatSpin().getFromValue(1.0, 'Orbital Outset (millimeters):', self, 5.0, 2.0)
161                 self.turnFanOnAtBeginning = settings.BooleanSetting().getFromValue('Turn Fan On at Beginning', self, True)
162                 self.turnFanOffAtEnding = settings.BooleanSetting().getFromValue('Turn Fan Off at Ending', self, True)
163                 self.executeTitle = 'Cool'
164                 
165                 self.minimumFeedRate = settings.FloatSpin().getFromValue(0.0, 'Minimum feed rate (mm/s):', self, 10.0, 5.0)
166
167         def execute(self):
168                 'Cool button has been clicked.'
169                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(
170                         self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
171                 for fileName in fileNames:
172                         writeOutput(fileName)
173
174
175 class CoolSkein:
176         'A class to cool a skein of extrusions.'
177         def __init__(self):
178                 self.boundaryLayer = None
179                 self.coolTemperature = None
180                 self.distanceFeedRate = gcodec.DistanceFeedRate()
181                 self.feedRateMinute = 960.0
182                 self.highestZ = 1.0
183                 self.isBridgeLayer = False
184                 self.isExtruderActive = False
185                 self.layerCount = settings.LayerCount()
186                 self.lineIndex = 0
187                 self.lines = None
188                 self.multiplier = 1.0
189                 self.oldFlowRate = None
190                 self.oldFlowRateString = None
191                 self.oldLocation = None
192                 self.oldTemperature = None
193                 self.minFeedrate = 0
194
195         def addCoolOrbits(self, remainingOrbitTime):
196                 'Add the minimum radius cool orbits.'
197                 if len(self.boundaryLayer.loops) < 1:
198                         return
199                 insetBoundaryLoops = self.boundaryLayer.loops
200                 if abs(self.repository.orbitalOutset.value) > 0.1 * abs(self.edgeWidth):
201                         insetBoundaryLoops = intercircle.getInsetLoopsFromLoops(self.boundaryLayer.loops, -self.repository.orbitalOutset.value)
202                 if len(insetBoundaryLoops) < 1:
203                         insetBoundaryLoops = self.boundaryLayer.loops
204                 largestLoop = euclidean.getLargestLoop(insetBoundaryLoops)
205                 loopArea = euclidean.getAreaLoopAbsolute(largestLoop)
206                 if loopArea < self.minimumArea:
207                         center = 0.5 * (euclidean.getMaximumByComplexPath(largestLoop) + euclidean.getMinimumByComplexPath(largestLoop))
208                         centerXBounded = max(center.real, self.boundingRectangle.cornerMinimum.real)
209                         centerXBounded = min(centerXBounded, self.boundingRectangle.cornerMaximum.real)
210                         centerYBounded = max(center.imag, self.boundingRectangle.cornerMinimum.imag)
211                         centerYBounded = min(centerYBounded, self.boundingRectangle.cornerMaximum.imag)
212                         center = complex(centerXBounded, centerYBounded)
213                         maximumCorner = center + self.halfCorner
214                         minimumCorner = center - self.halfCorner
215                         largestLoop = euclidean.getSquareLoopWiddershins(minimumCorner, maximumCorner)
216                 pointComplex = euclidean.getXYComplexFromVector3(self.oldLocation)
217                 if pointComplex != None:
218                         largestLoop = euclidean.getLoopStartingClosest(self.edgeWidth, pointComplex, largestLoop)
219                 intercircle.addOrbitsIfLarge(
220                         self.distanceFeedRate, largestLoop, self.orbitalFeedRatePerSecond, remainingOrbitTime, self.highestZ)
221
222         def addCoolTemperature(self, remainingOrbitTime):
223                 'Parse a gcode line and add it to the cool skein.'
224                 layerCool = self.repository.maximumCool.value * remainingOrbitTime / self.repository.minimumLayerTime.value
225                 if self.isBridgeLayer:
226                         layerCool = max(self.repository.bridgeCool.value, layerCool)
227                 if self.oldTemperature != None and layerCool != 0.0:
228                         self.coolTemperature = self.oldTemperature - layerCool
229                         self.addTemperature(self.coolTemperature)
230
231         def addFlowRate(self, flowRate):
232                 'Add a multipled line of flow rate if different.'
233                 if flowRate != None:
234                         self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
235
236         def addGcodeFromFeedRateMovementZ(self, feedRateMinute, point, z):
237                 'Add a movement to the output.'
238                 self.distanceFeedRate.addLine(self.distanceFeedRate.getLinearGcodeMovementWithFeedRate(feedRateMinute, point, z))
239
240         def addOrbitsIfNecessary(self, remainingOrbitTime):
241                 'Parse a gcode line and add it to the cool skein.'
242                 if remainingOrbitTime > 0.0 and self.boundaryLayer != None:
243                         self.addCoolOrbits(remainingOrbitTime)
244
245         def addTemperature(self, temperature):
246                 'Add a line of temperature.'
247                 self.distanceFeedRate.addLine('M104 S' + euclidean.getRoundedToThreePlaces(temperature))
248
249         def getCoolMove(self, line, location, splitLine):
250                 'Get cool line according to time spent on layer.'
251                 self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
252                 return self.distanceFeedRate.getLineWithFeedRate(max(self.minFeedrate, self.multiplier * self.feedRateMinute), line, splitLine)
253
254         def getCraftedGcode(self, gcodeText, repository):
255                 'Parse gcode text and store the cool gcode.'
256                 self.repository = repository
257                 self.coolEndLines = settings.getAlterationFileLines(repository.nameOfCoolEndFile.value)
258                 self.coolStartLines = settings.getAlterationFileLines(repository.nameOfCoolStartFile.value)
259                 self.halfCorner = complex(repository.minimumOrbitalRadius.value, repository.minimumOrbitalRadius.value)
260                 self.lines = archive.getTextLines(gcodeText)
261                 self.minimumArea = 4.0 * repository.minimumOrbitalRadius.value * repository.minimumOrbitalRadius.value
262                 self.minFeedrate = repository.minimumFeedRate.value * 60
263                 self.parseInitialization()
264                 self.boundingRectangle = gcodec.BoundingRectangle().getFromGcodeLines(self.lines[self.lineIndex :], 0.5 * self.edgeWidth)
265                 margin = 0.2 * self.edgeWidth
266                 halfCornerMargin = self.halfCorner + complex(margin, margin)
267                 self.boundingRectangle.cornerMaximum -= halfCornerMargin
268                 self.boundingRectangle.cornerMinimum += halfCornerMargin
269                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
270                         line = self.lines[self.lineIndex]
271                         self.parseLine(line)
272                 if repository.turnFanOffAtEnding.value:
273                         self.distanceFeedRate.addLine('M107')
274                 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
275
276         def getLayerTime(self):
277                 'Get the time the extruder spends on the layer.'
278                 feedRateMinute = self.feedRateMinute
279                 layerTime = 0.0
280                 lastThreadLocation = self.oldLocation
281                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
282                         line = self.lines[lineIndex]
283                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
284                         firstWord = gcodec.getFirstWord(splitLine)
285                         if firstWord == 'G1':
286                                 location = gcodec.getLocationFromSplitLine(lastThreadLocation, splitLine)
287                                 feedRateMinute = gcodec.getFeedRateMinute(feedRateMinute, splitLine)
288                                 if lastThreadLocation != None:
289                                         feedRateSecond = feedRateMinute / 60.0
290                                         layerTime += location.distance(lastThreadLocation) / feedRateSecond
291                                 lastThreadLocation = location
292                         elif firstWord == '(<bridgeRotation>':
293                                 self.isBridgeLayer = True
294                         elif firstWord == '(</layer>)':
295                                 return layerTime
296                 return layerTime
297
298         def getLayerTimeActive(self):
299                 'Get the time the extruder spends on the layer while active.'
300                 feedRateMinute = self.feedRateMinute
301                 isExtruderActive = self.isExtruderActive
302                 layerTime = 0.0
303                 lastThreadLocation = self.oldLocation
304                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
305                         line = self.lines[lineIndex]
306                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
307                         firstWord = gcodec.getFirstWord(splitLine)
308                         if firstWord == 'G1':
309                                 location = gcodec.getLocationFromSplitLine(lastThreadLocation, splitLine)
310                                 feedRateMinute = gcodec.getFeedRateMinute(feedRateMinute, splitLine)
311                                 if lastThreadLocation != None and isExtruderActive:
312                                         feedRateSecond = feedRateMinute / 60.0
313                                         layerTime += location.distance(lastThreadLocation) / feedRateSecond
314                                 lastThreadLocation = location
315                         elif firstWord == 'M101':
316                                 isExtruderActive = True
317                         elif firstWord == 'M103':
318                                 isExtruderActive = False
319                         elif firstWord == '(<bridgeRotation>':
320                                 self.isBridgeLayer = True
321                         elif firstWord == '(</layer>)':
322                                 return layerTime
323                 return layerTime
324
325         def parseInitialization(self):
326                 'Parse gcode initialization and store the parameters.'
327                 for self.lineIndex in xrange(len(self.lines)):
328                         line = self.lines[self.lineIndex]
329                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
330                         firstWord = gcodec.getFirstWord(splitLine)
331                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
332                         if firstWord == 'M108':
333                                 self.oldFlowRate = float(splitLine[1][1 :])
334                         elif firstWord == '(<edgeWidth>':
335                                 self.edgeWidth = float(splitLine[1])
336                                 if self.repository.turnFanOnAtBeginning.value:
337                                         self.distanceFeedRate.addLine('M106')
338                         elif firstWord == '(</extruderInitialization>)':
339                                 self.distanceFeedRate.addTagBracketedProcedure('cool')
340                                 return
341                         elif firstWord == '(<operatingFlowRate>':
342                                 self.oldFlowRate = float(splitLine[1])
343                         elif firstWord == '(<orbitalFeedRatePerSecond>':
344                                 self.orbitalFeedRatePerSecond = float(splitLine[1])
345                         self.distanceFeedRate.addLine(line)
346
347         def parseLine(self, line):
348                 'Parse a gcode line and add it to the cool skein.'
349                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
350                 if len(splitLine) < 1:
351                         return
352                 firstWord = splitLine[0]
353                 if firstWord == 'G1':
354                         location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
355                         self.highestZ = max(location.z, self.highestZ)
356                         if self.isExtruderActive:
357                                 line = self.getCoolMove(line, location, splitLine)
358                         self.oldLocation = location
359                 elif firstWord == 'M101':
360                         self.isExtruderActive = True
361                 elif firstWord == 'M103':
362                         self.isExtruderActive = False
363                 elif firstWord == 'M104':
364                         self.oldTemperature = gcodec.getDoubleAfterFirstLetter(splitLine[1])
365                 elif firstWord == 'M108':
366                         self.oldFlowRate = float(splitLine[1][1 :])
367                         self.addFlowRate(self.multiplier * self.oldFlowRate)
368                         return
369                 elif firstWord == '(<boundaryPoint>':
370                         self.boundaryLoop.append(gcodec.getLocationFromSplitLine(None, splitLine).dropAxis())
371                 elif firstWord == '(<layer>':
372                         self.layerCount.printProgressIncrement('cool')
373                         self.distanceFeedRate.addLine(line)
374                         self.distanceFeedRate.addLinesSetAbsoluteDistanceMode(self.coolStartLines)
375                         layerTime = self.getLayerTime()
376                         remainingOrbitTime = max(self.repository.minimumLayerTime.value - layerTime, 0.0)
377                         self.addCoolTemperature(remainingOrbitTime)
378                         if self.repository.orbit.value:
379                                 self.addOrbitsIfNecessary(remainingOrbitTime)
380                         else:
381                                 self.setMultiplier(remainingOrbitTime)
382                                 if self.oldFlowRate != None:
383                                         self.addFlowRate(self.multiplier * self.oldFlowRate)
384                         z = float(splitLine[1])
385                         self.boundaryLayer = euclidean.LoopLayer(z)
386                         self.highestZ = max(z, self.highestZ)
387                         self.distanceFeedRate.addLinesSetAbsoluteDistanceMode(self.coolEndLines)
388                         return
389                 elif firstWord == '(</layer>)':
390                         self.isBridgeLayer = False
391                         self.multiplier = 1.0
392                         if self.coolTemperature != None:
393                                 self.addTemperature(self.oldTemperature)
394                                 self.coolTemperature = None
395                         if self.oldFlowRate != None:
396                                 self.addFlowRate(self.oldFlowRate)
397                 elif firstWord == '(<nestedRing>)':
398                         self.boundaryLoop = []
399                         self.boundaryLayer.loops.append(self.boundaryLoop)
400                 self.distanceFeedRate.addLine(line)
401
402         def setMultiplier(self, remainingOrbitTime):
403                 'Set the feed and flow rate multiplier.'
404                 layerTimeActive = self.getLayerTimeActive()
405                 self.multiplier = min(1.0, layerTimeActive / (remainingOrbitTime + layerTimeActive))
406                 
407
408
409 def main():
410         'Display the cool dialog.'
411         if len(sys.argv) > 1:
412                 writeOutput(' '.join(sys.argv[1 :]))
413         else:
414                 settings.startMainLoopFromConstructor(getNewRepository())
415
416 if __name__ == '__main__':
417         main()