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 / skin.py
1 """
2 This page is in the table of contents.
3 Skin is a plugin to smooth the surface skin of an object by replacing the edge surface with a surface printed at a fraction of the carve
4 height.  This gives the impression that the object was carved at a much thinner height giving a high-quality finish, but still prints 
5 in a relatively short time.  The latest process has some similarities with a description at:
6
7 http://adventuresin3-dprinting.blogspot.com/2011/05/skinning.html
8
9 The skin manual page is at:
10 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skin
11
12 ==Operation==
13 The default 'Activate Skin' checkbox is off.  When it is on, the functions described below will work, when it is off, nothing will be done.
14
15 ==Settings==
16 ===Division===
17 ====Horizontal Infill Divisions====
18 Default: 2
19
20 Defines the number of times the skinned infill is divided horizontally.
21
22 ====Horizontal Perimeter Divisions====
23 Default: 1
24
25 Defines the number of times the skinned edges are divided horizontally.
26
27 ====Vertical Divisions====
28 Default: 2
29
30 Defines the number of times the skinned infill and edges are divided vertically.
31
32 ===Hop When Extruding Infill===
33 Default is off.
34
35 When selected, the extruder will hop before and after extruding the lower infill in order to avoid the regular thickness threads.
36
37 ===Layers From===
38 Default: 1
39
40 Defines which layer of the print the skinning process starts from. It is not wise to set this to zero, skinning the bottom layer is likely to cause the bottom edge not to adhere well to the print surface.
41
42 ==Tips==
43 Due to the very small Z-axis moves skinning can generate as it prints the edge, it can cause the Z-axis speed to be limited by the Limit plug-in, if you have it enabled. This can cause some printers to pause excessively during each layer change. To overcome this, ensure that the Z-axis max speed in the Limit tool is set to an appropriate value for your printer, e.g. 10mm/s
44
45 Since Skin prints a number of fractional-height edge layers for each layer, printing the edge last causes the print head to travel down from the current print height. Depending on the shape of your extruder nozzle, you may get higher quality prints if you print the edges first, so the print head always travels up.  This is set via the Thread Sequence Choice setting in the Fill tool.
46
47 ==Examples==
48 The following examples skin the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and skin.py.
49
50 > python skin.py
51 This brings up the skin dialog.
52
53 > python skin.py Screw Holder Bottom.stl
54 The skin tool is parsing the file:
55 Screw Holder Bottom.stl
56 ..
57 The skin tool has created the file:
58 .. Screw Holder Bottom_skin.gcode
59
60 """
61
62 from __future__ import absolute_import
63 #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.
64 import __init__
65
66 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
67 from fabmetheus_utilities.geometry.solids import triangle_mesh
68 from fabmetheus_utilities.vector3 import Vector3
69 from fabmetheus_utilities import archive
70 from fabmetheus_utilities import euclidean
71 from fabmetheus_utilities import gcodec
72 from fabmetheus_utilities import intercircle
73 from fabmetheus_utilities import settings
74 from skeinforge_application.skeinforge_utilities import skeinforge_craft
75 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
76 from skeinforge_application.skeinforge_utilities import skeinforge_profile
77 import sys
78
79
80 __author__ = 'Enrique Perez (perez_enrique aht yahoo.com) & James Blackwell (jim_blag ahht hotmail.com)'
81 __date__ = '$Date: 2008/21/04 $'
82 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
83
84
85 def getCraftedText(fileName, gcodeText, repository=None):
86         'Skin a gcode linear move text.'
87         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
88
89 def getCraftedTextFromText(gcodeText, repository=None):
90         'Skin a gcode linear move text.'
91         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'skin'):
92                 return gcodeText
93         if repository == None:
94                 repository = settings.getReadRepository(SkinRepository())
95         if not repository.activateSkin.value:
96                 return gcodeText
97         return SkinSkein().getCraftedGcode(gcodeText, repository)
98
99 def getIsMinimumSides(loops, sides=3):
100         'Determine if all the loops have at least the given number of sides.'
101         for loop in loops:
102                 if len(loop) < sides:
103                         return False
104         return True
105
106 def getNewRepository():
107         'Get new repository.'
108         return SkinRepository()
109
110 def writeOutput(fileName, shouldAnalyze=True):
111         'Skin a gcode linear move file.  Chain skin the gcode if it is not already skinned.'
112         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'skin', shouldAnalyze)
113
114
115 class SkinRepository:
116         'A class to handle the skin settings.'
117         def __init__(self):
118                 'Set the default settings, execute title & settings fileName.'
119                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.skin.html', self )
120                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Skin', self, '')
121                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skin')
122                 self.activateSkin = settings.BooleanSetting().getFromValue('Activate Skin', self, False)
123                 settings.LabelSeparator().getFromRepository(self)
124                 settings.LabelDisplay().getFromName('- Division -', self)
125                 self.horizontalInfillDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Horizontal Infill Divisions (integer):', self, 3, 2)
126                 self.horizontalPerimeterDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Horizontal Perimeter Divisions (integer):', self, 3, 1)
127                 self.verticalDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Vertical Divisions (integer):', self, 3, 2)
128                 settings.LabelSeparator().getFromRepository(self)
129                 self.hopWhenExtrudingInfill = settings.BooleanSetting().getFromValue('Hop When Extruding Infill', self, False)
130                 self.layersFrom = settings.IntSpin().getSingleIncrementFromValue(0, 'Layers From (index):', self, 912345678, 1)
131                 self.executeTitle = 'Skin'
132
133         def execute(self):
134                 'Skin button has been clicked.'
135                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
136                 for fileName in fileNames:
137                         writeOutput(fileName)
138
139
140 class SkinSkein:
141         'A class to skin a skein of extrusions.'
142         def __init__(self):
143                 'Initialize.'
144                 self.clipOverEdgeWidth = 0.0
145                 self.distanceFeedRate = gcodec.DistanceFeedRate()
146                 self.edge = None
147                 self.feedRateMinute = 959.0
148                 self.infill = None
149                 self.infillBoundaries = None
150                 self.infillBoundary = None
151                 self.layerIndex = -1
152                 self.lineIndex = 0
153                 self.lines = None
154                 self.maximumZFeedRateMinute = 60.0
155                 self.oldFlowRate = None
156                 self.oldLocation = None
157                 self.sharpestProduct = 0.94
158                 self.travelFeedRateMinute = 957.0
159
160         def addFlowRateLine(self, flowRate):
161                 'Add a flow rate line.'
162                 if flowRate != None:
163                         self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
164
165         def addPerimeterLoop(self, thread, z):
166                 'Add the edge loop to the gcode.'
167                 self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, thread, self.travelFeedRateMinute, z)
168
169         def addSkinnedInfill(self):
170                 'Add skinned infill.'
171                 if self.infillBoundaries == None:
172                         return
173                 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
174                 offsetY = 0.5 * self.skinInfillWidth
175                 if self.oldFlowRate != None:
176                         self.addFlowRateLine(self.oldFlowRate / self.verticalDivisionsFloat / self.horizontalInfillDivisionsFloat)
177                 for verticalDivisionIndex in xrange(self.verticalDivisions):
178                         z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
179                         self.addSkinnedInfillBoundary(self.infillBoundaries, offsetY * (verticalDivisionIndex % 2 == 0), self.oldLocation.z, z)
180                 self.addFlowRateLine(self.oldFlowRate)
181                 self.infillBoundaries = None
182
183         def addSkinnedInfillBoundary(self, infillBoundaries, offsetY, upperZ, z):
184                 'Add skinned infill boundary.'
185                 arounds = []
186                 aroundWidth = 0.34321 * self.skinInfillInset
187                 endpoints = []
188                 pixelTable = {}
189                 rotatedLoops = []
190                 for infillBoundary in infillBoundaries:
191                         infillBoundaryRotated = euclidean.getRotatedComplexes(self.reverseRotation, infillBoundary)
192                         if offsetY != 0.0:
193                                 for infillPointRotatedIndex, infillPointRotated in enumerate(infillBoundaryRotated):
194                                         infillBoundaryRotated[infillPointRotatedIndex] = complex(infillPointRotated.real, infillPointRotated.imag - offsetY)
195                         rotatedLoops.append(infillBoundaryRotated)
196                 infillDictionary = triangle_mesh.getInfillDictionary(
197                         arounds, aroundWidth, self.skinInfillInset, self.skinInfillWidth, pixelTable, rotatedLoops)
198                 for infillDictionaryKey in infillDictionary.keys():
199                         xIntersections = infillDictionary[infillDictionaryKey]
200                         xIntersections.sort()
201                         for segment in euclidean.getSegmentsFromXIntersections(xIntersections, infillDictionaryKey * self.skinInfillWidth):
202                                 for endpoint in segment:
203                                         endpoint.point = complex(endpoint.point.real, endpoint.point.imag + offsetY)
204                                         endpoints.append(endpoint)
205                 infillPaths = euclidean.getPathsFromEndpoints(endpoints, 5.0 * self.skinInfillWidth, pixelTable, self.sharpestProduct, aroundWidth)
206                 for infillPath in infillPaths:
207                         addPointBeforeThread = True
208                         infillRotated = euclidean.getRotatedComplexes(self.rotation, infillPath)
209                         if upperZ > z and self.repository.hopWhenExtrudingInfill.value:
210                                 feedRateMinute = self.travelFeedRateMinute
211                                 infillRotatedFirst = infillRotated[0]
212                                 location = Vector3(infillRotatedFirst.real, infillRotatedFirst.imag, upperZ)
213                                 distance = abs(location - self.oldLocation)
214                                 if distance > 0.0:
215                                         deltaZ = abs(upperZ - self.oldLocation.z)
216                                         zFeedRateComponent = feedRateMinute * deltaZ / distance
217                                         if zFeedRateComponent > self.maximumZFeedRateMinute:
218                                                 feedRateMinute *= self.maximumZFeedRateMinute / zFeedRateComponent
219                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, infillRotatedFirst, upperZ)
220                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.maximumZFeedRateMinute, infillRotatedFirst, z)
221                                 addPointBeforeThread = False
222                         if addPointBeforeThread:
223                                 self.distanceFeedRate.addGcodeMovementZ(infillRotated[0], z)
224                         self.distanceFeedRate.addLine('M101')
225                         for point in infillRotated[1 :]:
226                                 self.distanceFeedRate.addGcodeMovementZ(point, z)
227                         self.distanceFeedRate.addLine('M103')
228                         lastPointRotated = infillRotated[-1]
229                         self.oldLocation = Vector3(lastPointRotated.real, lastPointRotated.imag, upperZ)
230                         if upperZ > z and self.repository.hopWhenExtrudingInfill.value:
231                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.maximumZFeedRateMinute, lastPointRotated, upperZ)
232
233         def addSkinnedPerimeter(self):
234                 'Add skinned edge.'
235                 if self.edge == None:
236                         return
237                 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
238                 edgeThread = self.edge[: -1]
239                 edges = []
240                 radiusAddition = self.edgeWidth / self.horizontalPerimeterDivisionsFloat
241                 radius = 0.5 * radiusAddition - self.halfEdgeWidth
242                 for division in xrange(self.repository.horizontalPerimeterDivisions.value):
243                         edges.append(self.getClippedSimplifiedLoopPathByLoop(intercircle.getLargestInsetLoopFromLoop(edgeThread, radius)))
244                         radius += radiusAddition
245                 skinnedPerimeterFlowRate = None
246                 if self.oldFlowRate != None:
247                         skinnedPerimeterFlowRate = self.oldFlowRate / self.verticalDivisionsFloat
248                 if getIsMinimumSides(edges):
249                         if self.oldFlowRate != None:
250                                 self.addFlowRateLine(skinnedPerimeterFlowRate / self.horizontalPerimeterDivisionsFloat)
251                         for verticalDivisionIndex in xrange(self.verticalDivisions):
252                                 z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
253                                 for edge in edges:
254                                         self.addPerimeterLoop(edge, z)
255                 else:
256                         self.addFlowRateLine(skinnedPerimeterFlowRate)
257                         for verticalDivisionIndex in xrange(self.verticalDivisions):
258                                 z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
259                                 self.addPerimeterLoop(self.edge, z)
260                 self.addFlowRateLine(self.oldFlowRate)
261                 self.edge = None
262
263         def getClippedSimplifiedLoopPathByLoop(self, loop):
264                 'Get clipped and simplified loop path from a loop.'
265                 if len(loop) == 0:
266                         return []
267                 loopPath = loop + [loop[0]]
268                 return euclidean.getClippedSimplifiedLoopPath(self.clipLength, loopPath, self.halfEdgeWidth)
269
270         def getCraftedGcode( self, gcodeText, repository ):
271                 'Parse gcode text and store the skin gcode.'
272                 self.lines = archive.getTextLines(gcodeText)
273                 self.repository = repository
274                 self.layersFromBottom = repository.layersFrom.value
275                 self.horizontalInfillDivisionsFloat = float(repository.horizontalInfillDivisions.value)
276                 self.horizontalPerimeterDivisionsFloat = float(repository.horizontalPerimeterDivisions.value)
277                 self.verticalDivisions = max(repository.verticalDivisions.value, 1)
278                 self.verticalDivisionsFloat = float(self.verticalDivisions)
279                 self.parseInitialization()
280                 self.clipLength = 0.5 * self.clipOverEdgeWidth * self.edgeWidth
281                 self.skinInfillInset = 0.5 * (self.infillWidth + self.skinInfillWidth) * (1.0 - self.infillPerimeterOverlap)
282                 self.parseBoundaries()
283                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
284                         line = self.lines[self.lineIndex]
285                         self.parseLine(line)
286                 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
287
288         def parseBoundaries(self):
289                 'Parse the boundaries and add them to the boundary layers.'
290                 self.boundaryLayers = []
291                 self.layerIndexTop = -1
292                 boundaryLoop = None
293                 boundaryLayer = None
294                 for line in self.lines[self.lineIndex :]:
295                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
296                         firstWord = gcodec.getFirstWord(splitLine)
297                         if firstWord == '(</boundaryPerimeter>)':
298                                 boundaryLoop = None
299                         elif firstWord == '(<boundaryPoint>':
300                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
301                                 if boundaryLoop == None:
302                                         boundaryLoop = []
303                                         boundaryLayer.loops.append(boundaryLoop)
304                                 boundaryLoop.append(location.dropAxis())
305                         elif firstWord == '(<layer>':
306                                 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
307                                 self.boundaryLayers.append(boundaryLayer)
308                                 self.layerIndexTop += 1
309                 for boundaryLayerIndex, boundaryLayer in enumerate(self.boundaryLayers):
310                         if len(boundaryLayer.loops) > 0:
311                                 self.layersFromBottom += boundaryLayerIndex
312                                 return
313
314         def parseInitialization(self):
315                 'Parse gcode initialization and store the parameters.'
316                 for self.lineIndex in xrange(len(self.lines)):
317                         line = self.lines[self.lineIndex]
318                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
319                         firstWord = gcodec.getFirstWord(splitLine)
320                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
321                         if firstWord == '(<clipOverEdgeWidth>':
322                                 self.clipOverEdgeWidth = float(splitLine[1])
323                         elif firstWord == '(<edgeWidth>':
324                                 self.edgeWidth = float(splitLine[1])
325                                 self.halfEdgeWidth = 0.5 * self.edgeWidth
326                         elif firstWord == '(</extruderInitialization>)':
327                                 self.distanceFeedRate.addTagBracketedProcedure('skin')
328                                 return
329                         elif firstWord == '(<infillPerimeterOverlap>':
330                                 self.infillPerimeterOverlap = float(splitLine[1])
331                         elif firstWord == '(<infillWidth>':
332                                 self.infillWidth = float(splitLine[1])
333                                 self.skinInfillWidth = self.infillWidth / self.horizontalInfillDivisionsFloat
334                         elif firstWord == '(<layerHeight>':
335                                 self.layerHeight = float(splitLine[1])
336                         elif firstWord == '(<maximumZFeedRatePerSecond>':
337                                 self.maximumZFeedRateMinute = 60.0 * float(splitLine[1])
338                         elif firstWord == '(<operatingFlowRate>':
339                                 self.oldFlowRate = float(splitLine[1])
340                         elif firstWord == '(<sharpestProduct>':
341                                 self.sharpestProduct = float(splitLine[1])
342                         elif firstWord == '(<travelFeedRatePerSecond>':
343                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
344                         self.distanceFeedRate.addLine(line)
345
346         def parseLine(self, line):
347                 'Parse a gcode line and add it to the skin skein.'
348                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
349                 if len(splitLine) < 1:
350                         return
351                 firstWord = splitLine[0]
352                 if firstWord == 'G1':
353                         self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
354                         self.oldLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
355                         if self.infillBoundaries != None:
356                                 return
357                         if self.edge != None:
358                                 self.edge.append(self.oldLocation.dropAxis())
359                                 return
360                 elif firstWord == '(<infill>)':
361                         if self.layerIndex >= self.layersFromBottom and self.layerIndex == self.layerIndexTop:
362                                 self.infillBoundaries = []
363                 elif firstWord == '(</infill>)':
364                         self.addSkinnedInfill()
365                 elif firstWord == '(<infillBoundary>)':
366                         if self.infillBoundaries != None:
367                                 self.infillBoundary = []
368                                 self.infillBoundaries.append(self.infillBoundary)
369                 elif firstWord == '(<infillPoint>':
370                         if self.infillBoundaries != None:
371                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
372                                 self.infillBoundary.append(location.dropAxis())
373                 elif firstWord == '(<layer>':
374                         self.layerIndex += 1
375                         settings.printProgress(self.layerIndex, 'skin')
376                 elif firstWord == 'M101' or firstWord == 'M103':
377                         if self.infillBoundaries != None or self.edge != None:
378                                 return
379                 elif firstWord == 'M108':
380                         self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
381                 elif firstWord == '(<edge>':
382                         if self.layerIndex >= self.layersFromBottom:
383                                 self.edge = []
384                 elif firstWord == '(<rotation>':
385                         self.rotation = gcodec.getRotationBySplitLine(splitLine)
386                         self.reverseRotation = complex(self.rotation.real, -self.rotation.imag)
387                 elif firstWord == '(</edge>)':
388                         self.addSkinnedPerimeter()
389                 self.distanceFeedRate.addLine(line)
390
391
392 def main():
393         'Display the skin dialog.'
394         if len(sys.argv) > 1:
395                 writeOutput(' '.join(sys.argv[1 :]))
396         else:
397                 settings.startMainLoopFromConstructor(getNewRepository())
398
399 if __name__ == '__main__':
400         main()