2 This page is in the table of contents.
3 Comb is a craft plugin to bend the extruder travel paths around holes in the slices, to avoid stringers.
5 The comb manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb
9 The default 'Activate Comb' checkbox is off. When it is on, the functions described below will work, when it is off, nothing will be done.
12 ===Running Jump Space===
15 Defines the running jump space that is added before going from one island to another. If the running jump space is greater than zero, the departure from the island will also be brought closer to the arrival point on the next island so that the stringer between islands will be shorter. For an extruder with acceleration code, an extra space before leaving the island means that it will be going at high speed as it exits the island, which means the stringer between islands will be thinner.
18 The following examples comb the file Screw Holder Bottom.stl. The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and comb.py.
21 This brings up the comb dialog.
23 > python comb.py Screw Holder Bottom.stl
24 The comb tool is parsing the file:
25 Screw Holder Bottom.stl
27 The comb tool has created the file:
28 .. Screw Holder Bottom_comb.gcode
32 from __future__ import absolute_import
34 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
35 from fabmetheus_utilities import archive
36 from fabmetheus_utilities import euclidean
37 from fabmetheus_utilities import gcodec
38 from fabmetheus_utilities import intercircle
39 from fabmetheus_utilities import settings
40 from skeinforge_application.skeinforge_utilities import skeinforge_craft
41 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
42 from skeinforge_application.skeinforge_utilities import skeinforge_profile
46 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
47 __date__ = '$Date: 2008/21/04 $'
48 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
51 def getCraftedText(fileName, text, repository=None):
52 "Comb a gcode linear move text."
53 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
55 def getCraftedTextFromText(gcodeText, repository=None):
56 "Comb a gcode linear move text."
57 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'comb'):
59 if repository == None:
60 repository = settings.getReadRepository(CombRepository())
61 if not repository.activateComb.value:
63 return CombSkein().getCraftedGcode(gcodeText, repository)
65 def getJumpPoint(begin, end, loop, runningJumpSpace):
66 'Get running jump point inside loop.'
68 segmentLength = abs(segment)
69 if segmentLength == 0.0:
71 segment /= segmentLength
72 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segment)
73 if distancePoint.distance == runningJumpSpace:
74 return distancePoint.point
75 effectiveDistance = distancePoint.distance
76 jumpPoint = distancePoint.point
77 segmentLeft = complex(0.70710678118654757, -0.70710678118654757)
78 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentLeft)
79 distancePoint.distance *= 0.5
80 if distancePoint.distance > effectiveDistance:
81 effectiveDistance = distancePoint.distance
82 jumpPoint = distancePoint.point
83 segmentRight = complex(0.70710678118654757, 0.70710678118654757)
84 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentRight)
85 distancePoint.distance *= 0.5
86 if distancePoint.distance > effectiveDistance:
87 effectiveDistance = distancePoint.distance
88 jumpPoint = distancePoint.point
91 def getJumpPointIfInside(boundary, otherPoint, edgeWidth, runningJumpSpace):
92 'Get the jump point if it is inside the boundary, otherwise return None.'
93 insetBoundary = intercircle.getSimplifiedInsetFromClockwiseLoop(boundary, -edgeWidth)
94 closestJumpDistanceIndex = euclidean.getClosestDistanceIndexToLine(otherPoint, insetBoundary)
95 jumpIndex = (closestJumpDistanceIndex.index + 1) % len(insetBoundary)
96 jumpPoint = euclidean.getClosestPointOnSegment(insetBoundary[closestJumpDistanceIndex.index], insetBoundary[jumpIndex], otherPoint)
97 jumpPoint = getJumpPoint(jumpPoint, otherPoint, boundary, runningJumpSpace)
98 if euclidean.isPointInsideLoop(boundary, jumpPoint):
102 def getNewRepository():
103 'Get new repository.'
104 return CombRepository()
106 def getPathsByIntersectedLoop(begin, end, loop):
107 'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
108 closestBeginDistanceIndex = euclidean.getClosestDistanceIndexToLine(begin, loop)
109 closestEndDistanceIndex = euclidean.getClosestDistanceIndexToLine(end, loop)
110 beginIndex = (closestBeginDistanceIndex.index + 1) % len(loop)
111 endIndex = (closestEndDistanceIndex.index + 1) % len(loop)
112 closestBegin = euclidean.getClosestPointOnSegment(loop[closestBeginDistanceIndex.index], loop[beginIndex], begin)
113 closestEnd = euclidean.getClosestPointOnSegment(loop[closestEndDistanceIndex.index], loop[endIndex], end)
114 clockwisePath = [closestBegin]
115 widdershinsPath = [closestBegin]
116 if closestBeginDistanceIndex.index != closestEndDistanceIndex.index:
117 widdershinsPath += euclidean.getAroundLoop(beginIndex, endIndex, loop)
118 clockwisePath += euclidean.getAroundLoop(endIndex, beginIndex, loop)[: : -1]
119 clockwisePath.append(closestEnd)
120 widdershinsPath.append(closestEnd)
121 return [clockwisePath, widdershinsPath]
123 def writeOutput(fileName, shouldAnalyze=True):
124 "Comb a gcode linear move file."
125 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'comb', shouldAnalyze)
128 class BoundarySegment(object):
129 'A boundary and segment.'
130 def __init__(self, begin):
132 self.segment = [begin]
134 def getSegment(self, boundarySegmentIndex, boundarySegments, edgeWidth, runningJumpSpace):
135 'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
136 nextBoundarySegment = boundarySegments[boundarySegmentIndex + 1]
137 nextBegin = nextBoundarySegment.segment[0]
138 end = getJumpPointIfInside(self.boundary, nextBegin, edgeWidth, runningJumpSpace)
140 end = self.segment[1]
141 nextBegin = getJumpPointIfInside(nextBoundarySegment.boundary, end, edgeWidth, runningJumpSpace)
142 if nextBegin != None:
143 nextBoundarySegment.segment[0] = nextBegin
144 return (self.segment[0], end)
147 class CombRepository(object):
148 "A class to handle the comb settings."
150 "Set the default settings, execute title & settings fileName."
151 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.comb.html', self )
152 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Comb', self, '')
153 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb')
154 self.activateComb = settings.BooleanSetting().getFromValue('Activate Comb', self, True )
155 self.runningJumpSpace = settings.FloatSpin().getFromValue(0.0, 'Running Jump Space (mm):', self, 5.0, 2.0)
156 self.executeTitle = 'Comb'
159 "Comb button has been clicked."
160 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
161 for fileName in fileNames:
162 writeOutput(fileName)
165 class CombSkein(object):
166 "A class to comb a skein of extrusions."
169 self.boundaryLoop = None
170 self.distanceFeedRate = gcodec.DistanceFeedRate()
171 self.extruderActive = False
173 self.layerCount = settings.LayerCount()
178 self.nextLayerZ = None
179 self.oldLocation = None
181 self.operatingFeedRatePerMinute = None
182 self.travelFeedRateMinute = None
183 self.widdershinTable = {}
185 def addGcodePathZ( self, feedRateMinute, path, z ):
186 "Add a gcode path, without modifying the extruder, to the output."
188 self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, point, z)
190 def addIfTravel(self, splitLine):
191 "Add travel move around loops if the extruder is off."
192 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
193 if not self.extruderActive and self.oldLocation != None:
194 if len(self.getBoundaries()) > 0:
195 highestZ = max(location.z, self.oldLocation.z)
196 self.addGcodePathZ(self.travelFeedRateMinute, self.getAroundBetweenPath(self.oldLocation.dropAxis(), location.dropAxis()), highestZ)
197 self.oldLocation = location
199 def addToLoop(self, location):
200 "Add a location to loop."
201 if self.layer == None:
202 if not self.oldZ in self.layerTable:
203 self.layerTable[self.oldZ] = []
204 self.layer = self.layerTable[self.oldZ]
205 if self.boundaryLoop == None:
206 self.boundaryLoop = []
207 self.layer.append(self.boundaryLoop)
208 self.boundaryLoop.append(location.dropAxis())
210 def getAroundBetweenLineSegment(self, begin, boundaries, end):
211 'Get the path around the loops in the way of the original line segment.'
212 aroundBetweenLineSegment = []
213 boundaries = self.getBoundaries()
215 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
216 boundaryIndexesIndex = 0
217 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
218 if boundaryIndexes[boundaryIndexesIndex + 1] == boundaryIndexes[boundaryIndexesIndex]:
219 loopFirst = boundaries[boundaryIndexes[boundaryIndexesIndex]]
220 pathBetween = self.getPathBetween(loopFirst, points[boundaryIndexesIndex : boundaryIndexesIndex + 4])
221 begin = points[boundaryIndexesIndex]
222 end = points[boundaryIndexesIndex + 3]
223 pathBetween = self.getInsidePointsAlong(begin, pathBetween[0], points) + pathBetween
224 pathBetween += self.getInsidePointsAlong(end, pathBetween[-1], points)
225 aroundBetweenLineSegment += pathBetween
226 boundaryIndexesIndex += 2
228 boundaryIndexesIndex += 1
229 return aroundBetweenLineSegment
231 def getAroundBetweenPath(self, begin, end):
232 'Get the path around the loops in the way of the original line segment.'
233 aroundBetweenPath = []
234 boundaries = self.getBoundaries()
235 boundarySegments = self.getBoundarySegments(begin, boundaries, end)
236 for boundarySegmentIndex, boundarySegment in enumerate(boundarySegments):
237 segment = boundarySegment.segment
238 if boundarySegmentIndex < len(boundarySegments) - 1 and self.runningJumpSpace > 0.0:
239 segment = boundarySegment.getSegment(boundarySegmentIndex, boundarySegments, self.edgeWidth, self.runningJumpSpace)
240 aroundBetweenPath += self.getAroundBetweenLineSegment(segment[0], boundaries, segment[1])
241 if boundarySegmentIndex < len(boundarySegments) - 1:
242 aroundBetweenPath.append(segment[1])
243 aroundBetweenPath.append(boundarySegments[boundarySegmentIndex + 1].segment[0])
244 for pointIndex in xrange(len(aroundBetweenPath) - 1, -1, -1):
246 beforeIndex = pointIndex - 1
248 pointBefore = aroundBetweenPath[beforeIndex]
250 afterIndex = pointIndex + 1
251 if afterIndex < len(aroundBetweenPath):
252 pointAfter = aroundBetweenPath[afterIndex]
253 if not euclidean.isLineIntersectingLoops(boundaries, pointBefore, pointAfter):
254 del aroundBetweenPath[pointIndex]
255 return aroundBetweenPath
257 def getBoundaries(self):
258 "Get boundaries for the layer."
259 if self.layerZ in self.layerTable:
260 return self.layerTable[self.layerZ]
263 def getBoundaryIndexes(self, begin, boundaries, end, points):
264 'Get boundary indexes and set the points in the way of the original line segment.'
268 segment = euclidean.getNormalized(end - begin)
269 segmentYMirror = complex(segment.real, - segment.imag)
270 beginRotated = segmentYMirror * begin
271 endRotated = segmentYMirror * end
272 y = beginRotated.imag
273 for boundaryIndex in xrange(len(boundaries)):
274 boundary = boundaries[boundaryIndex]
275 boundaryRotated = euclidean.getRotatedComplexes(segmentYMirror, boundary)
276 euclidean.addXIntersectionIndexesFromLoopY(boundaryRotated, boundaryIndex, switchX, y)
278 maximumX = max(beginRotated.real, endRotated.real)
279 minimumX = min(beginRotated.real, endRotated.real)
280 for xIntersection in switchX:
281 if minimumX < xIntersection.x < maximumX:
282 point = segment * complex(xIntersection.x, y)
284 boundaryIndexes.append(xIntersection.index)
286 return boundaryIndexes
288 def getBoundarySegments(self, begin, boundaries, end):
289 'Get the path broken into boundary segments whenever a different boundary is crossed.'
290 boundarySegments = []
291 boundarySegment = BoundarySegment(begin)
292 boundarySegments.append(boundarySegment)
294 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
295 boundaryIndexesIndex = 0
296 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
297 if boundaryIndexes[boundaryIndexesIndex + 1] != boundaryIndexes[boundaryIndexesIndex]:
298 boundarySegment.boundary = boundaries[boundaryIndexes[boundaryIndexesIndex]]
299 nextBoundary = boundaries[boundaryIndexes[boundaryIndexesIndex + 1]]
300 if euclidean.isWiddershins(boundarySegment.boundary) and euclidean.isWiddershins(nextBoundary):
301 boundarySegment.segment.append(points[boundaryIndexesIndex + 1])
302 boundarySegment = BoundarySegment(points[boundaryIndexesIndex + 2])
303 boundarySegment.boundary = nextBoundary
304 boundarySegments.append(boundarySegment)
305 boundaryIndexesIndex += 1
306 boundaryIndexesIndex += 1
307 boundarySegment.segment.append(points[-1])
308 return boundarySegments
310 def getCraftedGcode(self, gcodeText, repository):
311 "Parse gcode text and store the comb gcode."
312 self.runningJumpSpace = repository.runningJumpSpace.value
313 self.repository = repository
314 self.lines = archive.getTextLines(gcodeText)
315 self.parseInitialization()
316 for lineIndex in xrange(self.lineIndex, len(self.lines)):
317 line = self.lines[lineIndex]
318 self.parseBoundariesLayers(line)
319 for lineIndex in xrange(self.lineIndex, len(self.lines)):
320 line = self.lines[lineIndex]
322 return self.distanceFeedRate.output.getvalue()
324 def getInsidePointsAlong(self, begin, end, points):
325 'Get the points along the segment if it is required to keep the path inside the widdershin boundaries.'
326 segment = end - begin
327 segmentLength = abs(segment)
328 if segmentLength < self.quadrupleEdgeWidth:
330 segmentHalfPerimeter = self.halfEdgeWidth / segmentLength * segment
331 justAfterBegin = begin + segmentHalfPerimeter
332 justBeforeEnd = end - segmentHalfPerimeter
333 widdershins = self.getWiddershins()
334 if not euclidean.isLineIntersectingLoops(widdershins, justAfterBegin, justBeforeEnd):
337 stepLength = (segmentLength - self.doubleEdgeWidth) / float(numberOfSteps)
338 for step in xrange(1, numberOfSteps + 1):
339 along = begin + stepLength * step
340 if not euclidean.isLineIntersectingLoops(widdershins, along, justBeforeEnd):
344 def getPathBetween(self, loop, points):
345 "Add a path between the edge and the fill."
346 paths = getPathsByIntersectedLoop(points[1], points[2], loop)
347 shortestPath = paths[int(euclidean.getPathLength(paths[1]) < euclidean.getPathLength(paths[0]))]
348 if len(shortestPath) < 2:
350 if abs(points[1] - shortestPath[0]) > abs(points[1] - shortestPath[-1]):
351 shortestPath.reverse()
352 loopWiddershins = euclidean.isWiddershins(loop)
354 for pointIndex in xrange(len(shortestPath)):
355 center = shortestPath[pointIndex]
356 centerPerpendicular = None
357 beginIndex = pointIndex - 1
359 begin = shortestPath[beginIndex]
360 centerPerpendicular = intercircle.getWiddershinsByLength(center, begin, self.edgeWidth*2.0)
362 endIndex = pointIndex + 1
363 if endIndex < len(shortestPath):
364 end = shortestPath[endIndex]
365 centerEnd = intercircle.getWiddershinsByLength(end, center, self.edgeWidth*2.0)
366 if centerPerpendicular == None:
367 centerPerpendicular = centerEnd
368 elif centerEnd != None:
369 centerPerpendicular = 0.5 * (centerPerpendicular + centerEnd)
371 if centerPerpendicular == None:
374 centerSideWiddershins = center + centerPerpendicular
375 if euclidean.isPointInsideLoop(loop, centerSideWiddershins) == loopWiddershins:
376 between = centerSideWiddershins
378 centerSideClockwise = center - centerPerpendicular
379 if euclidean.isPointInsideLoop(loop, centerSideClockwise) == loopWiddershins:
380 between = centerSideClockwise
383 pathBetween.append(between)
386 def getWiddershins(self):
387 'Get widdershins for the layer.'
388 if self.layerZ in self.widdershinTable:
389 return self.widdershinTable[self.layerZ]
390 self.widdershinTable[self.layerZ] = []
391 for boundary in self.getBoundaries():
392 if euclidean.isWiddershins(boundary):
393 self.widdershinTable[self.layerZ].append(boundary)
394 return self.widdershinTable[self.layerZ]
396 def parseBoundariesLayers(self, line):
397 "Parse a gcode line."
398 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
399 if len(splitLine) < 1:
401 firstWord = splitLine[0]
402 if firstWord == 'M103':
403 self.boundaryLoop = None
404 elif firstWord == '(<boundaryPoint>':
405 location = gcodec.getLocationFromSplitLine(None, splitLine)
406 self.addToLoop(location)
407 elif firstWord == '(<layer>':
408 self.boundaryLoop = None
410 self.oldZ = float(splitLine[1])
412 def parseInitialization(self):
413 'Parse gcode initialization and store the parameters.'
414 for self.lineIndex in xrange(len(self.lines)):
415 line = self.lines[self.lineIndex]
416 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
417 firstWord = gcodec.getFirstWord(splitLine)
418 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
419 if firstWord == '(</extruderInitialization>)':
420 self.distanceFeedRate.addTagBracketedProcedure('comb')
422 elif firstWord == '(<edgeWidth>':
423 self.edgeWidth = float(splitLine[1])
424 self.doubleEdgeWidth = self.edgeWidth + self.edgeWidth
425 self.halfEdgeWidth = 0.5 * self.edgeWidth
426 self.quadrupleEdgeWidth = self.doubleEdgeWidth + self.doubleEdgeWidth
427 elif firstWord == '(<travelFeedRatePerSecond>':
428 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
429 self.distanceFeedRate.addLine(line)
431 def parseLine(self, line):
432 "Parse a gcode line and add it to the comb skein."
433 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
434 if len(splitLine) < 1:
436 firstWord = splitLine[0]
437 if self.distanceFeedRate.getIsAlteration(line):
439 if firstWord == 'G1':
440 self.addIfTravel(splitLine)
441 self.layerZ = self.nextLayerZ
442 elif firstWord == 'M101':
443 self.extruderActive = True
444 elif firstWord == 'M103':
445 self.extruderActive = False
446 elif firstWord == '(<layer>':
447 self.layerCount.printProgressIncrement('comb')
448 self.nextLayerZ = float(splitLine[1])
449 if self.layerZ == None:
450 self.layerZ = self.nextLayerZ
451 self.distanceFeedRate.addLineCheckAlteration(line)
454 class DistancePoint(object):
455 'A class to get the distance of the point along a segment inside a loop.'
456 def __init__(self, begin, loop, runningJumpSpace, segment):
461 spaceOverSteps = runningJumpSpace / float(steps)
462 for numerator in xrange(1, steps + 1):
463 distance = float(numerator) * spaceOverSteps
464 point = begin + segment * distance
465 if euclidean.isPointInsideLoop(loop, point):
466 self.distance = distance
473 "Display the comb dialog."
474 if len(sys.argv) > 1:
475 writeOutput(' '.join(sys.argv[1 :]))
477 settings.startMainLoopFromConstructor(getNewRepository())
479 if __name__ == "__main__":