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:
7 http://adventuresin3-dprinting.blogspot.com/2011/05/skinning.html
9 The skin manual page is at:
10 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Skin
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.
17 ====Horizontal Infill Divisions====
20 Defines the number of times the skinned infill is divided horizontally.
22 ====Horizontal Perimeter Divisions====
25 Defines the number of times the skinned edges are divided horizontally.
27 ====Vertical Divisions====
30 Defines the number of times the skinned infill and edges are divided vertically.
32 ===Hop When Extruding Infill===
35 When selected, the extruder will hop before and after extruding the lower infill in order to avoid the regular thickness threads.
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.
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
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.
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.
51 This brings up the skin dialog.
53 > python skin.py Screw Holder Bottom.stl
54 The skin tool is parsing the file:
55 Screw Holder Bottom.stl
57 The skin tool has created the file:
58 .. Screw Holder Bottom_skin.gcode
62 from __future__ import absolute_import
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
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'
83 def getCraftedText(fileName, gcodeText, repository=None):
84 'Skin a gcode linear move text.'
85 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
87 def getCraftedTextFromText(gcodeText, repository=None):
88 'Skin a gcode linear move text.'
89 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'skin'):
91 if repository == None:
92 repository = settings.getReadRepository(SkinRepository())
93 if not repository.activateSkin.value:
95 return SkinSkein().getCraftedGcode(gcodeText, repository)
97 def getIsMinimumSides(loops, sides=3):
98 'Determine if all the loops have at least the given number of sides.'
100 if len(loop) < sides:
104 def getNewRepository():
105 'Get new repository.'
106 return SkinRepository()
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)
113 class SkinRepository(object):
114 'A class to handle the skin settings.'
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'
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)
138 class SkinSkein(object):
139 'A class to skin a skein of extrusions.'
142 self.clipOverEdgeWidth = 0.0
143 self.distanceFeedRate = gcodec.DistanceFeedRate()
145 self.feedRateMinute = 959.0
147 self.infillBoundaries = None
148 self.infillBoundary = None
152 self.maximumZFeedRateMinute = 60.0
153 self.oldFlowRate = None
154 self.oldLocation = None
155 self.sharpestProduct = 0.94
156 self.travelFeedRateMinute = 957.0
158 def addFlowRateLine(self, flowRate):
159 'Add a flow rate line.'
161 self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
163 def addPerimeterLoop(self, thread, z):
164 'Add the edge loop to the gcode.'
165 self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, thread, self.travelFeedRateMinute, z)
167 def addSkinnedInfill(self):
168 'Add skinned infill.'
169 if self.infillBoundaries == None:
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
181 def addSkinnedInfillBoundary(self, infillBoundaries, offsetY, upperZ, z):
182 'Add skinned infill boundary.'
184 aroundWidth = 0.34321 * self.skinInfillInset
188 for infillBoundary in infillBoundaries:
189 infillBoundaryRotated = euclidean.getRotatedComplexes(self.reverseRotation, infillBoundary)
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)
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)
231 def addSkinnedPerimeter(self):
233 if self.edge == None:
235 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
236 edgeThread = self.edge[: -1]
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)
252 self.addPerimeterLoop(edge, z)
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)
261 def getClippedSimplifiedLoopPathByLoop(self, loop):
262 'Get clipped and simplified loop path from a loop.'
265 loopPath = loop + [loop[0]]
266 return euclidean.getClippedSimplifiedLoopPath(self.clipLength, loopPath, self.halfEdgeWidth)
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]
284 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
286 def parseBoundaries(self):
287 'Parse the boundaries and add them to the boundary layers.'
288 self.boundaryLayers = []
289 self.layerIndexTop = -1
292 for line in self.lines[self.lineIndex :]:
293 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
294 firstWord = gcodec.getFirstWord(splitLine)
295 if firstWord == '(</boundaryPerimeter>)':
297 elif firstWord == '(<boundaryPoint>':
298 location = gcodec.getLocationFromSplitLine(None, splitLine)
299 if boundaryLoop == None:
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
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')
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)
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:
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:
355 if self.edge != None:
356 self.edge.append(self.oldLocation.dropAxis())
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>':
373 settings.printProgress(self.layerIndex, 'skin')
374 elif firstWord == 'M101' or firstWord == 'M103':
375 if self.infillBoundaries != None or self.edge != None:
377 elif firstWord == 'M108':
378 self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
379 elif firstWord == '(<edge>':
380 if self.layerIndex >= self.layersFromBottom:
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)
391 'Display the skin dialog.'
392 if len(sys.argv) > 1:
393 writeOutput(' '.join(sys.argv[1 :]))
395 settings.startMainLoopFromConstructor(getNewRepository())
397 if __name__ == '__main__':