chiark / gitweb /
Add back the ultimaker platform, and made the platform mesh simpler.
[cura.git] / Cura / slice / 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
64 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
65 from fabmetheus_utilities.geometry.solids import triangle_mesh
66 from fabmetheus_utilities.vector3 import Vector3
67 from fabmetheus_utilities import archive
68 from fabmetheus_utilities import euclidean
69 from fabmetheus_utilities import gcodec
70 from fabmetheus_utilities import intercircle
71 from fabmetheus_utilities import settings
72 from skeinforge_application.skeinforge_utilities import skeinforge_craft
73 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
74 from skeinforge_application.skeinforge_utilities import skeinforge_profile
75 import sys
76
77
78 __author__ = 'Enrique Perez (perez_enrique aht yahoo.com) & James Blackwell (jim_blag ahht hotmail.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, gcodeText, repository=None):
84         'Skin a gcode linear move text.'
85         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
86
87 def getCraftedTextFromText(gcodeText, repository=None):
88         'Skin a gcode linear move text.'
89         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'skin'):
90                 return gcodeText
91         if repository == None:
92                 repository = settings.getReadRepository(SkinRepository())
93         if not repository.activateSkin.value:
94                 return gcodeText
95         return SkinSkein().getCraftedGcode(gcodeText, repository)
96
97 def getIsMinimumSides(loops, sides=3):
98         'Determine if all the loops have at least the given number of sides.'
99         for loop in loops:
100                 if len(loop) < sides:
101                         return False
102         return True
103
104 def getNewRepository():
105         'Get new repository.'
106         return SkinRepository()
107
108 def writeOutput(fileName, shouldAnalyze=True):
109         'Skin a gcode linear move file.  Chain skin the gcode if it is not already skinned.'
110         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'skin', shouldAnalyze)
111
112
113 class SkinRepository(object):
114         'A class to handle the skin settings.'
115         def __init__(self):
116                 'Set the default settings, execute title & settings fileName.'
117                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.skin.html', self )
118                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Skin', self, '')
119                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skin')
120                 self.activateSkin = settings.BooleanSetting().getFromValue('Activate Skin', self, False)
121                 settings.LabelSeparator().getFromRepository(self)
122                 settings.LabelDisplay().getFromName('- Division -', self)
123                 self.horizontalInfillDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Horizontal Infill Divisions (integer):', self, 3, 2)
124                 self.horizontalPerimeterDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Horizontal Perimeter Divisions (integer):', self, 3, 1)
125                 self.verticalDivisions = settings.IntSpin().getSingleIncrementFromValue(1, 'Vertical Divisions (integer):', self, 3, 2)
126                 settings.LabelSeparator().getFromRepository(self)
127                 self.hopWhenExtrudingInfill = settings.BooleanSetting().getFromValue('Hop When Extruding Infill', self, False)
128                 self.layersFrom = settings.IntSpin().getSingleIncrementFromValue(0, 'Layers From (index):', self, 912345678, 1)
129                 self.executeTitle = 'Skin'
130
131         def execute(self):
132                 'Skin button has been clicked.'
133                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
134                 for fileName in fileNames:
135                         writeOutput(fileName)
136
137
138 class SkinSkein(object):
139         'A class to skin a skein of extrusions.'
140         def __init__(self):
141                 'Initialize.'
142                 self.clipOverEdgeWidth = 0.0
143                 self.distanceFeedRate = gcodec.DistanceFeedRate()
144                 self.edge = None
145                 self.feedRateMinute = 959.0
146                 self.infill = None
147                 self.infillBoundaries = None
148                 self.infillBoundary = None
149                 self.layerIndex = -1
150                 self.lineIndex = 0
151                 self.lines = None
152                 self.maximumZFeedRateMinute = 60.0
153                 self.oldFlowRate = None
154                 self.oldLocation = None
155                 self.sharpestProduct = 0.94
156                 self.travelFeedRateMinute = 957.0
157
158         def addFlowRateLine(self, flowRate):
159                 'Add a flow rate line.'
160                 if flowRate != None:
161                         self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
162
163         def addPerimeterLoop(self, thread, z):
164                 'Add the edge loop to the gcode.'
165                 self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, thread, self.travelFeedRateMinute, z)
166
167         def addSkinnedInfill(self):
168                 'Add skinned infill.'
169                 if self.infillBoundaries == None:
170                         return
171                 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
172                 offsetY = 0.5 * self.skinInfillWidth
173                 if self.oldFlowRate != None:
174                         self.addFlowRateLine(self.oldFlowRate / self.verticalDivisionsFloat / self.horizontalInfillDivisionsFloat)
175                 for verticalDivisionIndex in xrange(self.verticalDivisions):
176                         z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
177                         self.addSkinnedInfillBoundary(self.infillBoundaries, offsetY * (verticalDivisionIndex % 2 == 0), self.oldLocation.z, z)
178                 self.addFlowRateLine(self.oldFlowRate)
179                 self.infillBoundaries = None
180
181         def addSkinnedInfillBoundary(self, infillBoundaries, offsetY, upperZ, z):
182                 'Add skinned infill boundary.'
183                 arounds = []
184                 aroundWidth = 0.34321 * self.skinInfillInset
185                 endpoints = []
186                 pixelTable = {}
187                 rotatedLoops = []
188                 for infillBoundary in infillBoundaries:
189                         infillBoundaryRotated = euclidean.getRotatedComplexes(self.reverseRotation, infillBoundary)
190                         if offsetY != 0.0:
191                                 for infillPointRotatedIndex, infillPointRotated in enumerate(infillBoundaryRotated):
192                                         infillBoundaryRotated[infillPointRotatedIndex] = complex(infillPointRotated.real, infillPointRotated.imag - offsetY)
193                         rotatedLoops.append(infillBoundaryRotated)
194                 infillDictionary = triangle_mesh.getInfillDictionary(
195                         arounds, aroundWidth, self.skinInfillInset, self.skinInfillWidth, pixelTable, rotatedLoops)
196                 for infillDictionaryKey in infillDictionary.keys():
197                         xIntersections = infillDictionary[infillDictionaryKey]
198                         xIntersections.sort()
199                         for segment in euclidean.getSegmentsFromXIntersections(xIntersections, infillDictionaryKey * self.skinInfillWidth):
200                                 for endpoint in segment:
201                                         endpoint.point = complex(endpoint.point.real, endpoint.point.imag + offsetY)
202                                         endpoints.append(endpoint)
203                 infillPaths = euclidean.getPathsFromEndpoints(endpoints, 5.0 * self.skinInfillWidth, pixelTable, self.sharpestProduct, aroundWidth)
204                 for infillPath in infillPaths:
205                         addPointBeforeThread = True
206                         infillRotated = euclidean.getRotatedComplexes(self.rotation, infillPath)
207                         if upperZ > z and self.repository.hopWhenExtrudingInfill.value:
208                                 feedRateMinute = self.travelFeedRateMinute
209                                 infillRotatedFirst = infillRotated[0]
210                                 location = Vector3(infillRotatedFirst.real, infillRotatedFirst.imag, upperZ)
211                                 distance = abs(location - self.oldLocation)
212                                 if distance > 0.0:
213                                         deltaZ = abs(upperZ - self.oldLocation.z)
214                                         zFeedRateComponent = feedRateMinute * deltaZ / distance
215                                         if zFeedRateComponent > self.maximumZFeedRateMinute:
216                                                 feedRateMinute *= self.maximumZFeedRateMinute / zFeedRateComponent
217                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, infillRotatedFirst, upperZ)
218                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.maximumZFeedRateMinute, infillRotatedFirst, z)
219                                 addPointBeforeThread = False
220                         if addPointBeforeThread:
221                                 self.distanceFeedRate.addGcodeMovementZ(infillRotated[0], z)
222                         self.distanceFeedRate.addLine('M101')
223                         for point in infillRotated[1 :]:
224                                 self.distanceFeedRate.addGcodeMovementZ(point, z)
225                         self.distanceFeedRate.addLine('M103')
226                         lastPointRotated = infillRotated[-1]
227                         self.oldLocation = Vector3(lastPointRotated.real, lastPointRotated.imag, upperZ)
228                         if upperZ > z and self.repository.hopWhenExtrudingInfill.value:
229                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate(self.maximumZFeedRateMinute, lastPointRotated, upperZ)
230
231         def addSkinnedPerimeter(self):
232                 'Add skinned edge.'
233                 if self.edge == None:
234                         return
235                 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
236                 edgeThread = self.edge[: -1]
237                 edges = []
238                 radiusAddition = self.edgeWidth / self.horizontalPerimeterDivisionsFloat
239                 radius = 0.5 * radiusAddition - self.halfEdgeWidth
240                 for division in xrange(self.repository.horizontalPerimeterDivisions.value):
241                         edges.append(self.getClippedSimplifiedLoopPathByLoop(intercircle.getLargestInsetLoopFromLoop(edgeThread, radius)))
242                         radius += radiusAddition
243                 skinnedPerimeterFlowRate = None
244                 if self.oldFlowRate != None:
245                         skinnedPerimeterFlowRate = self.oldFlowRate / self.verticalDivisionsFloat
246                 if getIsMinimumSides(edges):
247                         if self.oldFlowRate != None:
248                                 self.addFlowRateLine(skinnedPerimeterFlowRate / self.horizontalPerimeterDivisionsFloat)
249                         for verticalDivisionIndex in xrange(self.verticalDivisions):
250                                 z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
251                                 for edge in edges:
252                                         self.addPerimeterLoop(edge, z)
253                 else:
254                         self.addFlowRateLine(skinnedPerimeterFlowRate)
255                         for verticalDivisionIndex in xrange(self.verticalDivisions):
256                                 z = bottomZ + self.layerHeight / self.verticalDivisionsFloat * float(verticalDivisionIndex)
257                                 self.addPerimeterLoop(self.edge, z)
258                 self.addFlowRateLine(self.oldFlowRate)
259                 self.edge = None
260
261         def getClippedSimplifiedLoopPathByLoop(self, loop):
262                 'Get clipped and simplified loop path from a loop.'
263                 if len(loop) == 0:
264                         return []
265                 loopPath = loop + [loop[0]]
266                 return euclidean.getClippedSimplifiedLoopPath(self.clipLength, loopPath, self.halfEdgeWidth)
267
268         def getCraftedGcode( self, gcodeText, repository ):
269                 'Parse gcode text and store the skin gcode.'
270                 self.lines = archive.getTextLines(gcodeText)
271                 self.repository = repository
272                 self.layersFromBottom = repository.layersFrom.value
273                 self.horizontalInfillDivisionsFloat = float(repository.horizontalInfillDivisions.value)
274                 self.horizontalPerimeterDivisionsFloat = float(repository.horizontalPerimeterDivisions.value)
275                 self.verticalDivisions = max(repository.verticalDivisions.value, 1)
276                 self.verticalDivisionsFloat = float(self.verticalDivisions)
277                 self.parseInitialization()
278                 self.clipLength = 0.5 * self.clipOverEdgeWidth * self.edgeWidth
279                 self.skinInfillInset = 0.5 * (self.infillWidth + self.skinInfillWidth) * (1.0 - self.infillPerimeterOverlap)
280                 self.parseBoundaries()
281                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
282                         line = self.lines[self.lineIndex]
283                         self.parseLine(line)
284                 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
285
286         def parseBoundaries(self):
287                 'Parse the boundaries and add them to the boundary layers.'
288                 self.boundaryLayers = []
289                 self.layerIndexTop = -1
290                 boundaryLoop = None
291                 boundaryLayer = None
292                 for line in self.lines[self.lineIndex :]:
293                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
294                         firstWord = gcodec.getFirstWord(splitLine)
295                         if firstWord == '(</boundaryPerimeter>)':
296                                 boundaryLoop = None
297                         elif firstWord == '(<boundaryPoint>':
298                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
299                                 if boundaryLoop == None:
300                                         boundaryLoop = []
301                                         boundaryLayer.loops.append(boundaryLoop)
302                                 boundaryLoop.append(location.dropAxis())
303                         elif firstWord == '(<layer>':
304                                 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
305                                 self.boundaryLayers.append(boundaryLayer)
306                                 self.layerIndexTop += 1
307                 for boundaryLayerIndex, boundaryLayer in enumerate(self.boundaryLayers):
308                         if len(boundaryLayer.loops) > 0:
309                                 self.layersFromBottom += boundaryLayerIndex
310                                 return
311
312         def parseInitialization(self):
313                 'Parse gcode initialization and store the parameters.'
314                 for self.lineIndex in xrange(len(self.lines)):
315                         line = self.lines[self.lineIndex]
316                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
317                         firstWord = gcodec.getFirstWord(splitLine)
318                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
319                         if firstWord == '(<clipOverEdgeWidth>':
320                                 self.clipOverEdgeWidth = float(splitLine[1])
321                         elif firstWord == '(<edgeWidth>':
322                                 self.edgeWidth = float(splitLine[1])
323                                 self.halfEdgeWidth = 0.5 * self.edgeWidth
324                         elif firstWord == '(</extruderInitialization>)':
325                                 self.distanceFeedRate.addTagBracketedProcedure('skin')
326                                 return
327                         elif firstWord == '(<infillPerimeterOverlap>':
328                                 self.infillPerimeterOverlap = float(splitLine[1])
329                         elif firstWord == '(<infillWidth>':
330                                 self.infillWidth = float(splitLine[1])
331                                 self.skinInfillWidth = self.infillWidth / self.horizontalInfillDivisionsFloat
332                         elif firstWord == '(<layerHeight>':
333                                 self.layerHeight = float(splitLine[1])
334                         elif firstWord == '(<maximumZFeedRatePerSecond>':
335                                 self.maximumZFeedRateMinute = 60.0 * float(splitLine[1])
336                         elif firstWord == '(<operatingFlowRate>':
337                                 self.oldFlowRate = float(splitLine[1])
338                         elif firstWord == '(<sharpestProduct>':
339                                 self.sharpestProduct = float(splitLine[1])
340                         elif firstWord == '(<travelFeedRatePerSecond>':
341                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
342                         self.distanceFeedRate.addLine(line)
343
344         def parseLine(self, line):
345                 'Parse a gcode line and add it to the skin skein.'
346                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
347                 if len(splitLine) < 1:
348                         return
349                 firstWord = splitLine[0]
350                 if firstWord == 'G1':
351                         self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
352                         self.oldLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
353                         if self.infillBoundaries != None:
354                                 return
355                         if self.edge != None:
356                                 self.edge.append(self.oldLocation.dropAxis())
357                                 return
358                 elif firstWord == '(<infill>)':
359                         if self.layerIndex >= self.layersFromBottom and self.layerIndex == self.layerIndexTop:
360                                 self.infillBoundaries = []
361                 elif firstWord == '(</infill>)':
362                         self.addSkinnedInfill()
363                 elif firstWord == '(<infillBoundary>)':
364                         if self.infillBoundaries != None:
365                                 self.infillBoundary = []
366                                 self.infillBoundaries.append(self.infillBoundary)
367                 elif firstWord == '(<infillPoint>':
368                         if self.infillBoundaries != None:
369                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
370                                 self.infillBoundary.append(location.dropAxis())
371                 elif firstWord == '(<layer>':
372                         self.layerIndex += 1
373                         settings.printProgress(self.layerIndex, 'skin')
374                 elif firstWord == 'M101' or firstWord == 'M103':
375                         if self.infillBoundaries != None or self.edge != None:
376                                 return
377                 elif firstWord == 'M108':
378                         self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
379                 elif firstWord == '(<edge>':
380                         if self.layerIndex >= self.layersFromBottom:
381                                 self.edge = []
382                 elif firstWord == '(<rotation>':
383                         self.rotation = gcodec.getRotationBySplitLine(splitLine)
384                         self.reverseRotation = complex(self.rotation.real, -self.rotation.imag)
385                 elif firstWord == '(</edge>)':
386                         self.addSkinnedPerimeter()
387                 self.distanceFeedRate.addLine(line)
388
389
390 def main():
391         'Display the skin dialog.'
392         if len(sys.argv) > 1:
393                 writeOutput(' '.join(sys.argv[1 :]))
394         else:
395                 settings.startMainLoopFromConstructor(getNewRepository())
396
397 if __name__ == '__main__':
398         main()