chiark / gitweb /
665f7bfc7af04d1543c9aa6a4b1ff47e3d04b783
[cura.git] / Cura / skeinforge_application / skeinforge_plugins / craft_plugins / comb.py
1 """
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.
4
5 The comb manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb
7
8 ==Operation==
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.
10
11 ==Settings==
12 ===Running Jump Space===
13 Default: 2 mm
14
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.
16
17 ==Examples==
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.
19
20 > python comb.py
21 This brings up the comb dialog.
22
23 > python comb.py Screw Holder Bottom.stl
24 The comb tool is parsing the file:
25 Screw Holder Bottom.stl
26 ..
27 The comb tool has created the file:
28 .. Screw Holder Bottom_comb.gcode
29
30 """
31
32 from __future__ import absolute_import
33 try:
34         import psyco
35         psyco.full()
36 except:
37         pass
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.
39 import __init__
40
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
50 import math
51 import sys
52
53
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'
57
58
59 def getCraftedText(fileName, text, repository=None):
60         "Comb a gcode linear move text."
61         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
62
63 def getCraftedTextFromText(gcodeText, repository=None):
64         "Comb a gcode linear move text."
65         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'comb'):
66                 return gcodeText
67         if repository == None:
68                 repository = settings.getReadRepository(CombRepository())
69         if not repository.activateComb.value:
70                 return gcodeText
71         return CombSkein().getCraftedGcode(gcodeText, repository)
72
73 def getJumpPoint(begin, end, loop, runningJumpSpace):
74         'Get running jump point inside loop.'
75         segment = begin - end
76         segmentLength = abs(segment)
77         if segmentLength == 0.0:
78                 return begin
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
97         return jumpPoint
98
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):
107                 return jumpPoint
108         return None
109
110 def getNewRepository():
111         'Get new repository.'
112         return CombRepository()
113
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]
130
131 def writeOutput(fileName, shouldAnalyze=True):
132         "Comb a gcode linear move file."
133         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'comb', shouldAnalyze)
134
135
136 class BoundarySegment:
137         'A boundary and segment.'
138         def __init__(self, begin):
139                 'Initialize'
140                 self.segment = [begin]
141
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)
147                 if end == None:
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)
153
154
155 class CombRepository:
156         "A class to handle the comb settings."
157         def __init__(self):
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'
165
166         def execute(self):
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)
171
172
173 class CombSkein:
174         "A class to comb a skein of extrusions."
175         def __init__(self):
176                 'Initialize'
177                 self.boundaryLoop = None
178                 self.distanceFeedRate = gcodec.DistanceFeedRate()
179                 self.extruderActive = False
180                 self.layer = None
181                 self.layerCount = settings.LayerCount()
182                 self.layerTable = {}
183                 self.layerZ = None
184                 self.lineIndex = 0
185                 self.lines = None
186                 self.nextLayerZ = None
187                 self.oldLocation = None
188                 self.oldZ = None
189                 self.operatingFeedRatePerMinute = None
190                 self.travelFeedRateMinute = None
191                 self.widdershinTable = {}
192
193         def addGcodePathZ( self, feedRateMinute, path, z ):
194                 "Add a gcode path, without modifying the extruder, to the output."
195                 for point in path:
196                         self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, point, z)
197
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
206
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())
217
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()
222                 points = []
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
235                         else:
236                                 boundaryIndexesIndex += 1
237                 return aroundBetweenLineSegment
238
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):
253                         pointBefore = begin
254                         beforeIndex = pointIndex - 1
255                         if beforeIndex >= 0:
256                                 pointBefore = aroundBetweenPath[beforeIndex]
257                         pointAfter = end
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
264
265         def getBoundaries(self):
266                 "Get boundaries for the layer."
267                 if self.layerZ in self.layerTable:
268                         return self.layerTable[self.layerZ]
269                 return []
270
271         def getBoundaryIndexes(self, begin, boundaries, end, points):
272                 'Get boundary indexes and set the points in the way of the original line segment.'
273                 boundaryIndexes = []
274                 points.append(begin)
275                 switchX = []
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)
285                 switchX.sort()
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)
291                                 points.append(point)
292                                 boundaryIndexes.append(xIntersection.index)
293                 points.append(end)
294                 return boundaryIndexes
295
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)
301                 points = []
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
317
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]
329                         self.parseLine(line)
330                 return self.distanceFeedRate.output.getvalue()
331
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:
337                         return []
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):
343                         return []
344                 numberOfSteps = 10
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):
349                                 return [along]
350                 return []
351
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:
357                         return shortestPath
358                 if abs(points[1] - shortestPath[0]) > abs(points[1] - shortestPath[-1]):
359                         shortestPath.reverse()
360                 loopWiddershins = euclidean.isWiddershins(loop)
361                 pathBetween = []
362                 for pointIndex in xrange(len(shortestPath)):
363                         center = shortestPath[pointIndex]
364                         centerPerpendicular = None
365                         beginIndex = pointIndex - 1
366                         if beginIndex >= 0:
367                                 begin = shortestPath[beginIndex]
368                                 centerPerpendicular = intercircle.getWiddershinsByLength(center, begin, self.edgeWidth)
369                         centerEnd = None
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)
378                         between = None
379                         if centerPerpendicular == None:
380                                 between = center
381                         if between == None:
382                                 centerSideWiddershins = center + centerPerpendicular
383                                 if euclidean.isPointInsideLoop(loop, centerSideWiddershins) == loopWiddershins:
384                                         between = centerSideWiddershins
385                         if between == None:
386                                 centerSideClockwise = center - centerPerpendicular
387                                 if euclidean.isPointInsideLoop(loop, centerSideClockwise) == loopWiddershins:
388                                         between = centerSideClockwise
389                         if between == None:
390                                 between = center
391                         pathBetween.append(between)
392                 return pathBetween
393
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]
403
404         def parseBoundariesLayers(self, line):
405                 "Parse a gcode line."
406                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
407                 if len(splitLine) < 1:
408                         return
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
417                         self.layer = None
418                         self.oldZ = float(splitLine[1])
419
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')
429                                 return
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)
438
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:
443                         return
444                 firstWord = splitLine[0]
445                 if self.distanceFeedRate.getIsAlteration(line):
446                         return
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)
460
461
462 class DistancePoint:
463         'A class to get the distance of the point along a segment inside a loop.'
464         def __init__(self, begin, loop, runningJumpSpace, segment):
465                 'Initialize'
466                 self.distance = 0.0
467                 self.point = begin
468                 steps = 10
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
475                                 self.point = point
476                         else:
477                                 return
478
479
480 def main():
481         "Display the comb dialog."
482         if len(sys.argv) > 1:
483                 writeOutput(' '.join(sys.argv[1 :]))
484         else:
485                 settings.startMainLoopFromConstructor(getNewRepository())
486
487 if __name__ == "__main__":
488         main()