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
38 #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.
41 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
42 from fabmetheus_utilities import archive
43 from fabmetheus_utilities import euclidean
44 from fabmetheus_utilities import gcodec
45 from fabmetheus_utilities import intercircle
46 from fabmetheus_utilities import settings
47 from skeinforge_application.skeinforge_utilities import skeinforge_craft
48 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
49 from skeinforge_application.skeinforge_utilities import skeinforge_profile
54 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
55 __date__ = '$Date: 2008/21/04 $'
56 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
59 def getCraftedText(fileName, text, repository=None):
60 "Comb a gcode linear move text."
61 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
63 def getCraftedTextFromText(gcodeText, repository=None):
64 "Comb a gcode linear move text."
65 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'comb'):
67 if repository == None:
68 repository = settings.getReadRepository(CombRepository())
69 if not repository.activateComb.value:
71 return CombSkein().getCraftedGcode(gcodeText, repository)
73 def getJumpPoint(begin, end, loop, runningJumpSpace):
74 'Get running jump point inside loop.'
76 segmentLength = abs(segment)
77 if segmentLength == 0.0:
79 segment /= segmentLength
80 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segment)
81 if distancePoint.distance == runningJumpSpace:
82 return distancePoint.point
83 effectiveDistance = distancePoint.distance
84 jumpPoint = distancePoint.point
85 segmentLeft = complex(0.70710678118654757, -0.70710678118654757)
86 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentLeft)
87 distancePoint.distance *= 0.5
88 if distancePoint.distance > effectiveDistance:
89 effectiveDistance = distancePoint.distance
90 jumpPoint = distancePoint.point
91 segmentRight = complex(0.70710678118654757, 0.70710678118654757)
92 distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentRight)
93 distancePoint.distance *= 0.5
94 if distancePoint.distance > effectiveDistance:
95 effectiveDistance = distancePoint.distance
96 jumpPoint = distancePoint.point
99 def getJumpPointIfInside(boundary, otherPoint, edgeWidth, runningJumpSpace):
100 'Get the jump point if it is inside the boundary, otherwise return None.'
101 insetBoundary = intercircle.getSimplifiedInsetFromClockwiseLoop(boundary, -edgeWidth)
102 closestJumpDistanceIndex = euclidean.getClosestDistanceIndexToLine(otherPoint, insetBoundary)
103 jumpIndex = (closestJumpDistanceIndex.index + 1) % len(insetBoundary)
104 jumpPoint = euclidean.getClosestPointOnSegment(insetBoundary[closestJumpDistanceIndex.index], insetBoundary[jumpIndex], otherPoint)
105 jumpPoint = getJumpPoint(jumpPoint, otherPoint, boundary, runningJumpSpace)
106 if euclidean.isPointInsideLoop(boundary, jumpPoint):
110 def getNewRepository():
111 'Get new repository.'
112 return CombRepository()
114 def getPathsByIntersectedLoop(begin, end, loop):
115 'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
116 closestBeginDistanceIndex = euclidean.getClosestDistanceIndexToLine(begin, loop)
117 closestEndDistanceIndex = euclidean.getClosestDistanceIndexToLine(end, loop)
118 beginIndex = (closestBeginDistanceIndex.index + 1) % len(loop)
119 endIndex = (closestEndDistanceIndex.index + 1) % len(loop)
120 closestBegin = euclidean.getClosestPointOnSegment(loop[closestBeginDistanceIndex.index], loop[beginIndex], begin)
121 closestEnd = euclidean.getClosestPointOnSegment(loop[closestEndDistanceIndex.index], loop[endIndex], end)
122 clockwisePath = [closestBegin]
123 widdershinsPath = [closestBegin]
124 if closestBeginDistanceIndex.index != closestEndDistanceIndex.index:
125 widdershinsPath += euclidean.getAroundLoop(beginIndex, endIndex, loop)
126 clockwisePath += euclidean.getAroundLoop(endIndex, beginIndex, loop)[: : -1]
127 clockwisePath.append(closestEnd)
128 widdershinsPath.append(closestEnd)
129 return [clockwisePath, widdershinsPath]
131 def writeOutput(fileName, shouldAnalyze=True):
132 "Comb a gcode linear move file."
133 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'comb', shouldAnalyze)
136 class BoundarySegment:
137 'A boundary and segment.'
138 def __init__(self, begin):
140 self.segment = [begin]
142 def getSegment(self, boundarySegmentIndex, boundarySegments, edgeWidth, runningJumpSpace):
143 'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
144 nextBoundarySegment = boundarySegments[boundarySegmentIndex + 1]
145 nextBegin = nextBoundarySegment.segment[0]
146 end = getJumpPointIfInside(self.boundary, nextBegin, edgeWidth, runningJumpSpace)
148 end = self.segment[1]
149 nextBegin = getJumpPointIfInside(nextBoundarySegment.boundary, end, edgeWidth, runningJumpSpace)
150 if nextBegin != None:
151 nextBoundarySegment.segment[0] = nextBegin
152 return (self.segment[0], end)
155 class CombRepository:
156 "A class to handle the comb settings."
158 "Set the default settings, execute title & settings fileName."
159 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.comb.html', self )
160 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Comb', self, '')
161 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb')
162 self.activateComb = settings.BooleanSetting().getFromValue('Activate Comb', self, True )
163 self.runningJumpSpace = settings.FloatSpin().getFromValue(0.0, 'Running Jump Space (mm):', self, 5.0, 2.0)
164 self.executeTitle = 'Comb'
167 "Comb button has been clicked."
168 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
169 for fileName in fileNames:
170 writeOutput(fileName)
174 "A class to comb a skein of extrusions."
177 self.boundaryLoop = None
178 self.distanceFeedRate = gcodec.DistanceFeedRate()
179 self.extruderActive = False
181 self.layerCount = settings.LayerCount()
186 self.nextLayerZ = None
187 self.oldLocation = None
189 self.operatingFeedRatePerMinute = None
190 self.travelFeedRateMinute = None
191 self.widdershinTable = {}
193 def addGcodePathZ( self, feedRateMinute, path, z ):
194 "Add a gcode path, without modifying the extruder, to the output."
196 self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, point, z)
198 def addIfTravel(self, splitLine):
199 "Add travel move around loops if the extruder is off."
200 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
201 if not self.extruderActive and self.oldLocation != None:
202 if len(self.getBoundaries()) > 0:
203 highestZ = max(location.z, self.oldLocation.z)
204 self.addGcodePathZ(self.travelFeedRateMinute, self.getAroundBetweenPath(self.oldLocation.dropAxis(), location.dropAxis()), highestZ)
205 self.oldLocation = location
207 def addToLoop(self, location):
208 "Add a location to loop."
209 if self.layer == None:
210 if not self.oldZ in self.layerTable:
211 self.layerTable[self.oldZ] = []
212 self.layer = self.layerTable[self.oldZ]
213 if self.boundaryLoop == None:
214 self.boundaryLoop = []
215 self.layer.append(self.boundaryLoop)
216 self.boundaryLoop.append(location.dropAxis())
218 def getAroundBetweenLineSegment(self, begin, boundaries, end):
219 'Get the path around the loops in the way of the original line segment.'
220 aroundBetweenLineSegment = []
221 boundaries = self.getBoundaries()
223 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
224 boundaryIndexesIndex = 0
225 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
226 if boundaryIndexes[boundaryIndexesIndex + 1] == boundaryIndexes[boundaryIndexesIndex]:
227 loopFirst = boundaries[boundaryIndexes[boundaryIndexesIndex]]
228 pathBetween = self.getPathBetween(loopFirst, points[boundaryIndexesIndex : boundaryIndexesIndex + 4])
229 begin = points[boundaryIndexesIndex]
230 end = points[boundaryIndexesIndex + 3]
231 pathBetween = self.getInsidePointsAlong(begin, pathBetween[0], points) + pathBetween
232 pathBetween += self.getInsidePointsAlong(end, pathBetween[-1], points)
233 aroundBetweenLineSegment += pathBetween
234 boundaryIndexesIndex += 2
236 boundaryIndexesIndex += 1
237 return aroundBetweenLineSegment
239 def getAroundBetweenPath(self, begin, end):
240 'Get the path around the loops in the way of the original line segment.'
241 aroundBetweenPath = []
242 boundaries = self.getBoundaries()
243 boundarySegments = self.getBoundarySegments(begin, boundaries, end)
244 for boundarySegmentIndex, boundarySegment in enumerate(boundarySegments):
245 segment = boundarySegment.segment
246 if boundarySegmentIndex < len(boundarySegments) - 1 and self.runningJumpSpace > 0.0:
247 segment = boundarySegment.getSegment(boundarySegmentIndex, boundarySegments, self.edgeWidth, self.runningJumpSpace)
248 aroundBetweenPath += self.getAroundBetweenLineSegment(segment[0], boundaries, segment[1])
249 if boundarySegmentIndex < len(boundarySegments) - 1:
250 aroundBetweenPath.append(segment[1])
251 aroundBetweenPath.append(boundarySegments[boundarySegmentIndex + 1].segment[0])
252 for pointIndex in xrange(len(aroundBetweenPath) - 1, -1, -1):
254 beforeIndex = pointIndex - 1
256 pointBefore = aroundBetweenPath[beforeIndex]
258 afterIndex = pointIndex + 1
259 if afterIndex < len(aroundBetweenPath):
260 pointAfter = aroundBetweenPath[afterIndex]
261 if not euclidean.isLineIntersectingLoops(boundaries, pointBefore, pointAfter):
262 del aroundBetweenPath[pointIndex]
263 return aroundBetweenPath
265 def getBoundaries(self):
266 "Get boundaries for the layer."
267 if self.layerZ in self.layerTable:
268 return self.layerTable[self.layerZ]
271 def getBoundaryIndexes(self, begin, boundaries, end, points):
272 'Get boundary indexes and set the points in the way of the original line segment.'
276 segment = euclidean.getNormalized(end - begin)
277 segmentYMirror = complex(segment.real, - segment.imag)
278 beginRotated = segmentYMirror * begin
279 endRotated = segmentYMirror * end
280 y = beginRotated.imag
281 for boundaryIndex in xrange(len(boundaries)):
282 boundary = boundaries[boundaryIndex]
283 boundaryRotated = euclidean.getRotatedComplexes(segmentYMirror, boundary)
284 euclidean.addXIntersectionIndexesFromLoopY(boundaryRotated, boundaryIndex, switchX, y)
286 maximumX = max(beginRotated.real, endRotated.real)
287 minimumX = min(beginRotated.real, endRotated.real)
288 for xIntersection in switchX:
289 if xIntersection.x > minimumX and xIntersection.x < maximumX:
290 point = segment * complex(xIntersection.x, y)
292 boundaryIndexes.append(xIntersection.index)
294 return boundaryIndexes
296 def getBoundarySegments(self, begin, boundaries, end):
297 'Get the path broken into boundary segments whenever a different boundary is crossed.'
298 boundarySegments = []
299 boundarySegment = BoundarySegment(begin)
300 boundarySegments.append(boundarySegment)
302 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
303 boundaryIndexesIndex = 0
304 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
305 if boundaryIndexes[boundaryIndexesIndex + 1] != boundaryIndexes[boundaryIndexesIndex]:
306 boundarySegment.boundary = boundaries[boundaryIndexes[boundaryIndexesIndex]]
307 nextBoundary = boundaries[boundaryIndexes[boundaryIndexesIndex + 1]]
308 if euclidean.isWiddershins(boundarySegment.boundary) and euclidean.isWiddershins(nextBoundary):
309 boundarySegment.segment.append(points[boundaryIndexesIndex + 1])
310 boundarySegment = BoundarySegment(points[boundaryIndexesIndex + 2])
311 boundarySegment.boundary = nextBoundary
312 boundarySegments.append(boundarySegment)
313 boundaryIndexesIndex += 1
314 boundaryIndexesIndex += 1
315 boundarySegment.segment.append(points[-1])
316 return boundarySegments
318 def getCraftedGcode(self, gcodeText, repository):
319 "Parse gcode text and store the comb gcode."
320 self.runningJumpSpace = repository.runningJumpSpace.value
321 self.repository = repository
322 self.lines = archive.getTextLines(gcodeText)
323 self.parseInitialization()
324 for lineIndex in xrange(self.lineIndex, len(self.lines)):
325 line = self.lines[lineIndex]
326 self.parseBoundariesLayers(line)
327 for lineIndex in xrange(self.lineIndex, len(self.lines)):
328 line = self.lines[lineIndex]
330 return self.distanceFeedRate.output.getvalue()
332 def getInsidePointsAlong(self, begin, end, points):
333 'Get the points along the segment if it is required to keep the path inside the widdershin boundaries.'
334 segment = end - begin
335 segmentLength = abs(segment)
336 if segmentLength < self.quadrupleEdgeWidth:
338 segmentHalfPerimeter = self.halfEdgeWidth / segmentLength * segment
339 justAfterBegin = begin + segmentHalfPerimeter
340 justBeforeEnd = end - segmentHalfPerimeter
341 widdershins = self.getWiddershins()
342 if not euclidean.isLineIntersectingLoops(widdershins, justAfterBegin, justBeforeEnd):
345 stepLength = (segmentLength - self.doubleEdgeWidth) / float(numberOfSteps)
346 for step in xrange(1, numberOfSteps + 1):
347 along = begin + stepLength * step
348 if not euclidean.isLineIntersectingLoops(widdershins, along, justBeforeEnd):
352 def getPathBetween(self, loop, points):
353 "Add a path between the edge and the fill."
354 paths = getPathsByIntersectedLoop(points[1], points[2], loop)
355 shortestPath = paths[int(euclidean.getPathLength(paths[1]) < euclidean.getPathLength(paths[0]))]
356 if len(shortestPath) < 2:
358 if abs(points[1] - shortestPath[0]) > abs(points[1] - shortestPath[-1]):
359 shortestPath.reverse()
360 loopWiddershins = euclidean.isWiddershins(loop)
362 for pointIndex in xrange(len(shortestPath)):
363 center = shortestPath[pointIndex]
364 centerPerpendicular = None
365 beginIndex = pointIndex - 1
367 begin = shortestPath[beginIndex]
368 centerPerpendicular = intercircle.getWiddershinsByLength(center, begin, self.edgeWidth)
370 endIndex = pointIndex + 1
371 if endIndex < len(shortestPath):
372 end = shortestPath[endIndex]
373 centerEnd = intercircle.getWiddershinsByLength(end, center, self.edgeWidth)
374 if centerPerpendicular == None:
375 centerPerpendicular = centerEnd
376 elif centerEnd != None:
377 centerPerpendicular = 0.5 * (centerPerpendicular + centerEnd)
379 if centerPerpendicular == None:
382 centerSideWiddershins = center + centerPerpendicular
383 if euclidean.isPointInsideLoop(loop, centerSideWiddershins) == loopWiddershins:
384 between = centerSideWiddershins
386 centerSideClockwise = center - centerPerpendicular
387 if euclidean.isPointInsideLoop(loop, centerSideClockwise) == loopWiddershins:
388 between = centerSideClockwise
391 pathBetween.append(between)
394 def getWiddershins(self):
395 'Get widdershins for the layer.'
396 if self.layerZ in self.widdershinTable:
397 return self.widdershinTable[self.layerZ]
398 self.widdershinTable[self.layerZ] = []
399 for boundary in self.getBoundaries():
400 if euclidean.isWiddershins(boundary):
401 self.widdershinTable[self.layerZ].append(boundary)
402 return self.widdershinTable[self.layerZ]
404 def parseBoundariesLayers(self, line):
405 "Parse a gcode line."
406 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
407 if len(splitLine) < 1:
409 firstWord = splitLine[0]
410 if firstWord == 'M103':
411 self.boundaryLoop = None
412 elif firstWord == '(<boundaryPoint>':
413 location = gcodec.getLocationFromSplitLine(None, splitLine)
414 self.addToLoop(location)
415 elif firstWord == '(<layer>':
416 self.boundaryLoop = None
418 self.oldZ = float(splitLine[1])
420 def parseInitialization(self):
421 'Parse gcode initialization and store the parameters.'
422 for self.lineIndex in xrange(len(self.lines)):
423 line = self.lines[self.lineIndex]
424 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
425 firstWord = gcodec.getFirstWord(splitLine)
426 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
427 if firstWord == '(</extruderInitialization>)':
428 self.distanceFeedRate.addTagBracketedProcedure('comb')
430 elif firstWord == '(<edgeWidth>':
431 self.edgeWidth = float(splitLine[1])
432 self.doubleEdgeWidth = self.edgeWidth + self.edgeWidth
433 self.halfEdgeWidth = 0.5 * self.edgeWidth
434 self.quadrupleEdgeWidth = self.doubleEdgeWidth + self.doubleEdgeWidth
435 elif firstWord == '(<travelFeedRatePerSecond>':
436 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
437 self.distanceFeedRate.addLine(line)
439 def parseLine(self, line):
440 "Parse a gcode line and add it to the comb skein."
441 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
442 if len(splitLine) < 1:
444 firstWord = splitLine[0]
445 if self.distanceFeedRate.getIsAlteration(line):
447 if firstWord == 'G1':
448 self.addIfTravel(splitLine)
449 self.layerZ = self.nextLayerZ
450 elif firstWord == 'M101':
451 self.extruderActive = True
452 elif firstWord == 'M103':
453 self.extruderActive = False
454 elif firstWord == '(<layer>':
455 self.layerCount.printProgressIncrement('comb')
456 self.nextLayerZ = float(splitLine[1])
457 if self.layerZ == None:
458 self.layerZ = self.nextLayerZ
459 self.distanceFeedRate.addLineCheckAlteration(line)
463 'A class to get the distance of the point along a segment inside a loop.'
464 def __init__(self, begin, loop, runningJumpSpace, segment):
469 spaceOverSteps = runningJumpSpace / float(steps)
470 for numerator in xrange(1, steps + 1):
471 distance = float(numerator) * spaceOverSteps
472 point = begin + segment * distance
473 if euclidean.isPointInsideLoop(loop, point):
474 self.distance = distance
481 "Display the comb dialog."
482 if len(sys.argv) > 1:
483 writeOutput(' '.join(sys.argv[1 :]))
485 settings.startMainLoopFromConstructor(getNewRepository())
487 if __name__ == "__main__":