3 This page is in the table of contents.
4 Inset will inset the outside outlines by half the edge width, and outset the inside outlines by the same amount.
6 The inset manual page is at:
7 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Inset
10 ===Add Custom Code for Temperature Reading===
13 When selected, the M105 custom code for temperature reading will be added at the beginning of the file.
15 ===Infill in Direction of Bridge===
18 When selected, the infill will be in the direction of any bridge across a gap, so that the fill will be able to span a bridge easier.
20 ===Loop Order Choice===
21 Default loop order choice is 'Ascending Area'.
23 When overlap is to be removed, for each loop, the overlap is checked against the list of loops already extruded. If the latest loop overlaps an already extruded loop, the overlap is removed from the latest loop. The loops are ordered according to their areas.
25 ====Ascending Area====
26 When selected, the loops will be ordered in ascending area. With thin walled parts, if overlap is being removed the outside of the container will not be extruded. Holes will be the correct size.
28 ====Descending Area====
29 When selected, the loops will be ordered in descending area. With thin walled parts, if overlap is being removed the inside of the container will not be extruded. Holes will be missing the interior wall so they will be slightly wider than model size.
31 ===Overlap Removal Width over Perimeter Width===
34 Defines the ratio of the overlap removal width over the edge width. Any part of the extrusion that comes within the overlap removal width of another is removed. This is to prevent the extruder from depositing two extrusions right beside each other. If the 'Overlap Removal Width over Perimeter Width' is less than 0.2, the overlap will not be removed.
36 ===Turn Extruder Heater Off at Shut Down===
39 When selected, the M104 S0 gcode line will be added to the end of the file to turn the extruder heater off by setting the extruder heater temperature to 0.
44 The 'Volume Fraction' is the estimated volume of the thread compared to the box defined by the layer height and infill width. This is used in dwindle, splodge, and statistic. It is in inset because inset is a required extrusion tool, earlier in the chain than dwindle and splodge. In dwindle and splodge it is used to determine the filament volume, in statistic it is used to determine the extrusion diameter.
47 The following examples inset the file Screw Holder Bottom.stl. The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and inset.py.
50 This brings up the inset dialog.
52 > python inset.py Screw Holder Bottom.stl
53 The inset tool is parsing the file:
54 Screw Holder Bottom.stl
56 The inset tool has created the file:
57 .. Screw Holder Bottom_inset.gcode
61 from __future__ import absolute_import
67 #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.
70 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
71 from fabmetheus_utilities.geometry.solids import triangle_mesh
72 from fabmetheus_utilities.vector3 import Vector3
73 from fabmetheus_utilities import archive
74 from fabmetheus_utilities import euclidean
75 from fabmetheus_utilities import gcodec
76 from fabmetheus_utilities import intercircle
77 from fabmetheus_utilities import settings
78 from skeinforge_application.skeinforge_utilities import skeinforge_craft
79 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
80 from skeinforge_application.skeinforge_utilities import skeinforge_profile
87 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
88 __date__ = '$Date: 2008/02/05 $'
89 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
92 def addAlreadyFilledArounds( alreadyFilledArounds, loop, radius ):
93 "Add already filled loops around loop to alreadyFilledArounds."
95 alreadyFilledLoop = []
96 slightlyGreaterThanRadius = intercircle.globalIntercircleMultiplier * radius
97 muchGreaterThanRadius = 2.5 * radius
98 centers = intercircle.getCentersFromLoop( loop, slightlyGreaterThanRadius )
99 for center in centers:
100 alreadyFilledInset = intercircle.getSimplifiedInsetFromClockwiseLoop( center, radius )
101 if intercircle.isLargeSameDirection( alreadyFilledInset, center, radius ):
102 alreadyFilledLoop.append( alreadyFilledInset )
103 if len( alreadyFilledLoop ) > 0:
104 alreadyFilledArounds.append( alreadyFilledLoop )
106 def addSegmentOutline( isThick, outlines, pointBegin, pointEnd, width ):
107 "Add a diamond or hexagonal outline for a line segment."
109 exclusionWidth = 0.6 * width
113 exclusionWidth = 0.8 * width
114 segment = pointEnd - pointBegin
115 segmentLength = abs(segment)
116 if segmentLength == 0.0:
118 normalizedSegment = segment / segmentLength
120 segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
121 pointBeginRotated = segmentYMirror * pointBegin
122 pointEndRotated = segmentYMirror * pointEnd
124 alongLength = along * segmentLength
125 if alongLength > 0.1 * exclusionWidth:
126 along *= 0.1 * exclusionWidth / alongLength
127 alongEnd = 1.0 - along
128 remainingToHalf = 0.5 - along
129 alongToWidth = exclusionWidth / slope / segmentLength
130 pointBeginIntermediate = euclidean.getIntermediateLocation( along, pointBeginRotated, pointEndRotated )
131 pointEndIntermediate = euclidean.getIntermediateLocation( alongEnd, pointBeginRotated, pointEndRotated )
132 outline.append( pointBeginIntermediate )
133 verticalWidth = complex( 0.0, exclusionWidth )
134 if alongToWidth > 0.9 * remainingToHalf:
135 verticalWidth = complex( 0.0, slope * remainingToHalf * segmentLength )
136 middle = ( pointBeginIntermediate + pointEndIntermediate ) * 0.5
137 middleDown = middle - verticalWidth
138 middleUp = middle + verticalWidth
139 outline.append( middleUp )
140 outline.append( pointEndIntermediate )
141 outline.append( middleDown )
143 alongOutsideBegin = along + alongToWidth
144 alongOutsideEnd = alongEnd - alongToWidth
145 outsideBeginCenter = euclidean.getIntermediateLocation( alongOutsideBegin, pointBeginRotated, pointEndRotated )
146 outsideBeginCenterDown = outsideBeginCenter - verticalWidth
147 outsideBeginCenterUp = outsideBeginCenter + verticalWidth
148 outsideEndCenter = euclidean.getIntermediateLocation( alongOutsideEnd, pointBeginRotated, pointEndRotated )
149 outsideEndCenterDown = outsideEndCenter - verticalWidth
150 outsideEndCenterUp = outsideEndCenter + verticalWidth
151 outline.append( outsideBeginCenterUp )
152 outline.append( outsideEndCenterUp )
153 outline.append( pointEndIntermediate )
154 outline.append( outsideEndCenterDown )
155 outline.append( outsideBeginCenterDown )
156 outlines.append( euclidean.getRotatedComplexes( normalizedSegment, outline ) )
158 def getBridgeDirection(belowLoops, layerLoops, radius):
159 'Get span direction for the majority of the overhanging extrusion edge, if any.'
160 if len(belowLoops) < 1:
162 belowOutsetLoops = intercircle.getInsetLoopsFromLoops(belowLoops, -radius)
163 bridgeRotation = complex()
164 for loop in layerLoops:
165 for pointIndex, point in enumerate(loop):
166 previousIndex = (pointIndex + len(loop) - 1) % len(loop)
167 bridgeRotation += getOverhangDirection(belowOutsetLoops, loop[previousIndex], point)
168 if abs(bridgeRotation) < 0.75 * radius:
171 return cmath.sqrt(bridgeRotation / abs(bridgeRotation))
173 def getCraftedText( fileName, text='', repository=None):
174 "Inset the preface file or text."
175 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
177 def getCraftedTextFromText(gcodeText, repository=None):
178 "Inset the preface gcode text."
179 if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'inset'):
181 if repository == None:
182 repository = settings.getReadRepository( InsetRepository() )
183 return InsetSkein().getCraftedGcode(gcodeText, repository)
185 def getDoubledRoundZ( overhangingSegment, segmentRoundZ ):
186 'Get doubled plane angle around z of the overhanging segment.'
187 endpoint = overhangingSegment[0]
188 roundZ = endpoint.point - endpoint.otherEndpoint.point
189 roundZ *= segmentRoundZ
190 if abs( roundZ ) == 0.0:
192 if roundZ.real < 0.0:
194 roundZLength = abs( roundZ )
195 return roundZ * roundZ / roundZLength
197 def getInteriorSegments(loops, segments):
198 'Get segments inside the loops.'
199 interiorSegments = []
200 for segment in segments:
201 center = 0.5 * (segment[0].point + segment[1].point)
202 if euclidean.getIsInFilledRegion(loops, center):
203 interiorSegments.append(segment)
204 return interiorSegments
206 def getIsIntersectingWithinList(loop, loopList):
207 "Determine if the loop is intersecting or is within the loop list."
208 leftPoint = euclidean.getLeftPoint(loop)
209 for otherLoop in loopList:
210 if euclidean.getNumberOfIntersectionsToLeft(otherLoop, leftPoint) % 2 == 1:
212 return euclidean.isLoopIntersectingLoops(loop, loopList)
214 def getNewRepository():
215 'Get new repository.'
216 return InsetRepository()
218 def getOverhangDirection( belowOutsetLoops, segmentBegin, segmentEnd ):
219 'Add to span direction from the endpoint segments which overhang the layer below.'
220 segment = segmentEnd - segmentBegin
221 normalizedSegment = euclidean.getNormalized( complex( segment.real, segment.imag ) )
222 segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
223 segmentBegin = segmentYMirror * segmentBegin
224 segmentEnd = segmentYMirror * segmentEnd
225 solidXIntersectionList = []
226 y = segmentBegin.imag
227 solidXIntersectionList.append( euclidean.XIntersectionIndex( - 1.0, segmentBegin.real ) )
228 solidXIntersectionList.append( euclidean.XIntersectionIndex( - 1.0, segmentEnd.real ) )
229 for belowLoopIndex in xrange( len( belowOutsetLoops ) ):
230 belowLoop = belowOutsetLoops[ belowLoopIndex ]
231 rotatedOutset = euclidean.getRotatedComplexes( segmentYMirror, belowLoop )
232 euclidean.addXIntersectionIndexesFromLoopY( rotatedOutset, belowLoopIndex, solidXIntersectionList, y )
233 overhangingSegments = euclidean.getSegmentsFromXIntersectionIndexes( solidXIntersectionList, y )
234 overhangDirection = complex()
235 for overhangingSegment in overhangingSegments:
236 overhangDirection += getDoubledRoundZ( overhangingSegment, normalizedSegment )
237 return overhangDirection
239 def getSegmentsFromLoopListsPoints( loopLists, pointBegin, pointEnd ):
240 "Get endpoint segments from the beginning and end of a line segment."
241 normalizedSegment = pointEnd - pointBegin
242 normalizedSegmentLength = abs( normalizedSegment )
243 if normalizedSegmentLength == 0.0:
245 normalizedSegment /= normalizedSegmentLength
246 segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
247 pointBeginRotated = segmentYMirror * pointBegin
248 pointEndRotated = segmentYMirror * pointEnd
249 rotatedLoopLists = []
250 for loopList in loopLists:
251 rotatedLoopLists.append(euclidean.getRotatedComplexLists(segmentYMirror, loopList))
252 xIntersectionIndexList = []
253 xIntersectionIndexList.append( euclidean.XIntersectionIndex( - 1, pointBeginRotated.real ) )
254 xIntersectionIndexList.append( euclidean.XIntersectionIndex( - 1, pointEndRotated.real ) )
255 euclidean.addXIntersectionIndexesFromLoopListsY( rotatedLoopLists, xIntersectionIndexList, pointBeginRotated.imag )
256 segments = euclidean.getSegmentsFromXIntersectionIndexes( xIntersectionIndexList, pointBeginRotated.imag )
257 for segment in segments:
258 for endpoint in segment:
259 endpoint.point *= normalizedSegment
262 def isCloseToLast( paths, point, radius ):
263 "Determine if the point is close to the last point of the last path."
267 return abs( lastPath[-1] - point ) < radius
269 def isIntersectingItself( loop, width ):
270 "Determine if the loop is intersecting itself."
272 for pointIndex in xrange(len(loop)):
273 pointBegin = loop[pointIndex]
274 pointEnd = loop[(pointIndex + 1) % len(loop)]
275 if euclidean.isLineIntersectingLoops( outlines, pointBegin, pointEnd ):
277 addSegmentOutline( False, outlines, pointBegin, pointEnd, width )
280 def isIntersectingWithinLists( loop, loopLists ):
281 "Determine if the loop is intersecting or is within the loop lists."
282 for loopList in loopLists:
283 if getIsIntersectingWithinList( loop, loopList ):
287 def writeOutput(fileName, shouldAnalyze=True):
288 "Inset the carving of a gcode file."
289 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'inset', shouldAnalyze)
292 class InsetRepository:
293 "A class to handle the inset settings."
295 "Set the default settings, execute title & settings fileName."
296 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.inset.html', self)
297 self.baseNameSynonymDictionary = {
298 'Infill in Direction of Bridge' : 'carve.csv',
299 'Infill Width over Thickness (ratio):' : 'fill.csv'}
300 self.fileNameInput = settings.FileNameInput().getFromFileName(fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Inset', self, '')
301 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Inset')
302 self.addCustomCodeForTemperatureReading = settings.BooleanSetting().getFromValue('Add Custom Code for Temperature Reading', self, True)
303 self.infillInDirectionOfBridge = settings.BooleanSetting().getFromValue('Infill in Direction of Bridge', self, True)
304 self.infillWidth = settings.FloatSpin().getFromValue(0.1, 'Infill Width:', self, 1.7, 0.4)
305 self.loopOrderChoice = settings.MenuButtonDisplay().getFromName('Loop Order Choice:', self )
306 self.loopOrderAscendingArea = settings.MenuRadio().getFromMenuButtonDisplay(self.loopOrderChoice, 'Ascending Area', self, True)
307 self.loopOrderDescendingArea = settings.MenuRadio().getFromMenuButtonDisplay(self.loopOrderChoice, 'Descending Area', self, False)
308 self.overlapRemovalWidthOverEdgeWidth = settings.FloatSpin().getFromValue(0.3, 'Overlap Removal Width over Perimeter Width (ratio):', self, 0.9, 0.6)
309 self.turnExtruderHeaterOffAtShutDown = settings.BooleanSetting().getFromValue('Turn Extruder Heater Off at Shut Down', self, True)
310 self.volumeFraction = settings.FloatSpin().getFromValue(0.7, 'Volume Fraction (ratio):', self, 0.9, 0.82)
311 self.executeTitle = 'Inset'
314 "Inset button has been clicked."
315 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
316 for fileName in fileNames:
317 writeOutput(fileName)
321 "A class to inset a skein of extrusions."
326 self.distanceFeedRate = gcodec.DistanceFeedRate()
327 self.layerCount = settings.LayerCount()
329 self.loopLayer = None
331 def addGcodeFromPerimeterPaths(self, isIntersectingSelf, loop, loopLayer, loopLists, radius):
332 "Add the edge paths to the output."
336 allLoopLists = loopLists[:] + [thickOutlines]
337 aroundLists = loopLists
338 for pointIndex in xrange(len(loop)):
339 pointBegin = loop[pointIndex]
340 pointEnd = loop[(pointIndex + 1) % len(loop)]
341 if isIntersectingSelf:
342 if euclidean.isLineIntersectingLoops(outlines, pointBegin, pointEnd):
343 segments += getSegmentsFromLoopListsPoints(allLoopLists, pointBegin, pointEnd)
345 segments += getSegmentsFromLoopListsPoints(loopLists, pointBegin, pointEnd)
346 addSegmentOutline(False, outlines, pointBegin, pointEnd, self.overlapRemovalWidth)
347 addSegmentOutline(True, thickOutlines, pointBegin, pointEnd, self.overlapRemovalWidth)
349 segments += getSegmentsFromLoopListsPoints(loopLists, pointBegin, pointEnd)
352 muchSmallerThanRadius = 0.1 * radius
353 segments = getInteriorSegments(loopLayer.loops, segments)
354 for segment in segments:
355 pointBegin = segment[0].point
356 if not isCloseToLast(edgePaths, pointBegin, muchSmallerThanRadius):
358 edgePaths.append(path)
359 path.append(segment[1].point)
360 if len(edgePaths) > 1:
361 firstPath = edgePaths[0]
362 lastPath = edgePaths[-1]
363 if abs(lastPath[-1] - firstPath[0]) < 0.1 * muchSmallerThanRadius:
364 connectedBeginning = lastPath[: -1] + firstPath
365 edgePaths[0] = connectedBeginning
366 edgePaths.remove(lastPath)
367 muchGreaterThanRadius = 6.0 * radius
368 for edgePath in edgePaths:
369 if euclidean.getPathLength(edgePath) > muchGreaterThanRadius:
370 self.distanceFeedRate.addGcodeFromThreadZ(edgePath, loopLayer.z)
372 def addGcodeFromRemainingLoop(self, loop, loopLayer, loopLists, radius):
373 "Add the remainder of the loop which does not overlap the alreadyFilledArounds loops."
374 centerOutset = intercircle.getLargestCenterOutsetLoopFromLoopRegardless(loop, radius)
375 euclidean.addNestedRingBeginning(self.distanceFeedRate, centerOutset.outset, loopLayer.z)
376 self.addGcodePerimeterBlockFromRemainingLoop(centerOutset.center, loopLayer, loopLists, radius)
377 self.distanceFeedRate.addLine('(</boundaryPerimeter>)')
378 self.distanceFeedRate.addLine('(</nestedRing>)')
380 def addGcodePerimeterBlockFromRemainingLoop(self, loop, loopLayer, loopLists, radius):
381 "Add the perimter block remainder of the loop which does not overlap the alreadyFilledArounds loops."
382 if self.repository.overlapRemovalWidthOverEdgeWidth.value < 0.2:
383 self.distanceFeedRate.addPerimeterBlock(loop, loopLayer.z)
385 isIntersectingSelf = isIntersectingItself(loop, self.overlapRemovalWidth)
386 if isIntersectingWithinLists(loop, loopLists) or isIntersectingSelf:
387 self.addGcodeFromPerimeterPaths(isIntersectingSelf, loop, loopLayer, loopLists, radius)
389 self.distanceFeedRate.addPerimeterBlock(loop, loopLayer.z)
390 addAlreadyFilledArounds(loopLists, loop, self.overlapRemovalWidth)
392 def addInitializationToOutput(self):
393 "Add initialization gcode to the output."
394 if self.repository.addCustomCodeForTemperatureReading.value:
395 self.distanceFeedRate.addLine('M105') # Custom code for temperature reading.
397 def addInset(self, loopLayer):
398 "Add inset to the layer."
399 alreadyFilledArounds = []
400 extrudateLoops = intercircle.getInsetLoopsFromLoops(loopLayer.loops, self.halfEdgeWidth)
401 if self.repository.infillInDirectionOfBridge.value:
402 bridgeRotation = getBridgeDirection(self.belowLoops, extrudateLoops, self.halfEdgeWidth)
403 if bridgeRotation != None:
404 self.distanceFeedRate.addTagBracketedLine('bridgeRotation', bridgeRotation)
405 self.belowLoops = loopLayer.loops
406 triangle_mesh.sortLoopsInOrderOfArea(not self.repository.loopOrderAscendingArea.value, extrudateLoops)
407 for extrudateLoop in extrudateLoops:
408 self.addGcodeFromRemainingLoop(extrudateLoop, loopLayer, alreadyFilledArounds, self.halfEdgeWidth)
410 def getCraftedGcode(self, gcodeText, repository):
411 "Parse gcode text and store the bevel gcode."
412 self.repository = repository
413 self.lines = archive.getTextLines(gcodeText)
414 self.parseInitialization()
415 for line in self.lines[self.lineIndex :]:
417 return self.distanceFeedRate.output.getvalue()
419 def parseInitialization(self):
420 'Parse gcode initialization and store the parameters.'
421 for self.lineIndex in xrange(len(self.lines)):
422 line = self.lines[self.lineIndex]
423 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
424 firstWord = gcodec.getFirstWord(splitLine)
425 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
426 if firstWord == '(<decimalPlacesCarried>':
427 self.addInitializationToOutput()
428 elif firstWord == '(</extruderInitialization>)':
429 self.distanceFeedRate.addTagBracketedProcedure('inset')
431 elif firstWord == '(<layerHeight>':
432 layerHeight = float(splitLine[1])
433 self.infillWidth = self.repository.infillWidth.value
434 self.distanceFeedRate.addTagRoundedLine('infillWidth', self.infillWidth)
435 self.distanceFeedRate.addTagRoundedLine('volumeFraction', self.repository.volumeFraction.value)
436 elif firstWord == '(<edgeWidth>':
437 self.edgeWidth = float(splitLine[1])
438 self.halfEdgeWidth = 0.5 * self.edgeWidth
439 self.overlapRemovalWidth = self.edgeWidth * self.repository.overlapRemovalWidthOverEdgeWidth.value
440 self.distanceFeedRate.addLine(line)
442 def parseLine(self, line):
443 "Parse a gcode line and add it to the inset skein."
444 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
445 if len(splitLine) < 1:
447 firstWord = splitLine[0]
448 if firstWord == '(<boundaryPoint>':
449 location = gcodec.getLocationFromSplitLine(None, splitLine)
450 self.boundary.append(location.dropAxis())
451 elif firstWord == '(</crafting>)':
452 self.distanceFeedRate.addLine(line)
453 if self.repository.turnExtruderHeaterOffAtShutDown.value:
454 self.distanceFeedRate.addLine('M104 S0') # Turn extruder heater off.
456 elif firstWord == '(<layer>':
457 self.layerCount.printProgressIncrement('inset')
458 self.loopLayer = euclidean.LoopLayer(float(splitLine[1]))
459 self.distanceFeedRate.addLine(line)
460 elif firstWord == '(</layer>)':
461 self.addInset(self.loopLayer)
462 self.loopLayer = None
463 elif firstWord == '(<nestedRing>)':
465 self.loopLayer.loops.append(self.boundary)
466 if self.loopLayer == None:
467 self.distanceFeedRate.addLine(line)
471 "Display the inset dialog."
472 if len(sys.argv) > 1:
473 writeOutput(' '.join(sys.argv[1 :]))
475 settings.startMainLoopFromConstructor(getNewRepository())
477 if __name__ == "__main__":