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
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.
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
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'
85 def getCraftedText(fileName, gcodeText, repository=None):
86 'Skin a gcode linear move text.'
87 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, gcodeText), repository)
89 def getCraftedTextFromText(gcodeText, repository=None):
90 'Skin a gcode linear move text.'
91 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'skin'):
93 if repository == None:
94 repository = settings.getReadRepository(SkinRepository())
95 if not repository.activateSkin.value:
97 return SkinSkein().getCraftedGcode(gcodeText, repository)
99 def getIsMinimumSides(loops, sides=3):
100 'Determine if all the loops have at least the given number of sides.'
102 if len(loop) < sides:
106 def getNewRepository():
107 'Get new repository.'
108 return SkinRepository()
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)
115 class SkinRepository:
116 'A class to handle the skin settings.'
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'
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)
141 'A class to skin a skein of extrusions.'
144 self.clipOverEdgeWidth = 0.0
145 self.distanceFeedRate = gcodec.DistanceFeedRate()
147 self.feedRateMinute = 959.0
149 self.infillBoundaries = None
150 self.infillBoundary = None
154 self.maximumZFeedRateMinute = 60.0
155 self.oldFlowRate = None
156 self.oldLocation = None
157 self.sharpestProduct = 0.94
158 self.travelFeedRateMinute = 957.0
160 def addFlowRateLine(self, flowRate):
161 'Add a flow rate line.'
163 self.distanceFeedRate.addLine('M108 S' + euclidean.getFourSignificantFigures(flowRate))
165 def addPerimeterLoop(self, thread, z):
166 'Add the edge loop to the gcode.'
167 self.distanceFeedRate.addGcodeFromFeedRateThreadZ(self.feedRateMinute, thread, self.travelFeedRateMinute, z)
169 def addSkinnedInfill(self):
170 'Add skinned infill.'
171 if self.infillBoundaries == None:
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
183 def addSkinnedInfillBoundary(self, infillBoundaries, offsetY, upperZ, z):
184 'Add skinned infill boundary.'
186 aroundWidth = 0.34321 * self.skinInfillInset
190 for infillBoundary in infillBoundaries:
191 infillBoundaryRotated = euclidean.getRotatedComplexes(self.reverseRotation, infillBoundary)
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)
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)
233 def addSkinnedPerimeter(self):
235 if self.edge == None:
237 bottomZ = self.oldLocation.z + self.layerHeight / self.verticalDivisionsFloat - self.layerHeight
238 edgeThread = self.edge[: -1]
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)
254 self.addPerimeterLoop(edge, z)
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)
263 def getClippedSimplifiedLoopPathByLoop(self, loop):
264 'Get clipped and simplified loop path from a loop.'
267 loopPath = loop + [loop[0]]
268 return euclidean.getClippedSimplifiedLoopPath(self.clipLength, loopPath, self.halfEdgeWidth)
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]
286 return gcodec.getGcodeWithoutDuplication('M108', self.distanceFeedRate.output.getvalue())
288 def parseBoundaries(self):
289 'Parse the boundaries and add them to the boundary layers.'
290 self.boundaryLayers = []
291 self.layerIndexTop = -1
294 for line in self.lines[self.lineIndex :]:
295 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
296 firstWord = gcodec.getFirstWord(splitLine)
297 if firstWord == '(</boundaryPerimeter>)':
299 elif firstWord == '(<boundaryPoint>':
300 location = gcodec.getLocationFromSplitLine(None, splitLine)
301 if boundaryLoop == None:
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
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')
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)
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:
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:
357 if self.edge != None:
358 self.edge.append(self.oldLocation.dropAxis())
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>':
375 settings.printProgress(self.layerIndex, 'skin')
376 elif firstWord == 'M101' or firstWord == 'M103':
377 if self.infillBoundaries != None or self.edge != None:
379 elif firstWord == 'M108':
380 self.oldFlowRate = gcodec.getDoubleAfterFirstLetter(splitLine[1])
381 elif firstWord == '(<edge>':
382 if self.layerIndex >= self.layersFromBottom:
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)
393 'Display the skin dialog.'
394 if len(sys.argv) > 1:
395 writeOutput(' '.join(sys.argv[1 :]))
397 settings.startMainLoopFromConstructor(getNewRepository())
399 if __name__ == '__main__':