chiark / gitweb /
Add uppercase STL and HEX to file dialog filters for linux/MacOS
[cura.git] / Cura / skeinforge_application / skeinforge_plugins / craft_plugins / clip.py
1 """
2 This page is in the table of contents.
3 The clip plugin clips the loop ends to prevent bumps from forming, and connects loops.
4
5 The clip manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Clip
7
8 ==Operation==
9 The default 'Activate Clip' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions will not be called.
10
11 ==Settings==
12 ===Clip Over Perimeter Width===
13 Default is 0.2.
14
15 Defines the ratio of the amount each end of the loop is clipped over the edge width.  The total gap will therefore be twice the clip.  If the ratio is too high loops will have a gap, if the ratio is too low there will be a bulge at the loop ends.
16
17 This setting will affect the output of clip, and the output of the skin.  In skin the half width edges will be clipped by according to this setting.
18
19 ===Maximum Connection Distance Over Perimeter Width===
20 Default is ten.
21
22 Defines the ratio of the maximum connection distance between loops over the edge width.
23
24 Clip will attempt to connect loops that end close to each other, combining them into a spiral, so that the extruder does not stop and restart.  This setting sets the maximum gap size to connect.  This feature can reduce the amount of extra material or gaps formed at the loop end.
25
26 Setting this to zero disables this feature, preventing the loops from being connected.
27
28 ==Examples==
29 The following examples clip the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and clip.py.
30
31 > python clip.py
32 This brings up the clip dialog.
33
34 > python clip.py Screw Holder Bottom.stl
35 The clip tool is parsing the file:
36 Screw Holder Bottom.stl
37 ..
38 The clip tool has created the file:
39 .. Screw Holder Bottom_clip.gcode
40
41 """
42
43 from __future__ import absolute_import
44 #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.
45 import __init__
46
47 from fabmetheus_utilities import archive
48 from fabmetheus_utilities import euclidean
49 from fabmetheus_utilities import gcodec
50 from fabmetheus_utilities import intercircle
51 from fabmetheus_utilities import settings
52 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
53 from skeinforge_application.skeinforge_utilities import skeinforge_craft
54 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
55 from skeinforge_application.skeinforge_utilities import skeinforge_profile
56 import math
57 import sys
58
59
60 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
61 __date__ = '$Date: 2008/21/04 $'
62 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
63
64
65 def getCraftedText(fileName, text, repository=None):
66         "Clip a gcode linear move file or text."
67         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
68
69 def getCraftedTextFromText(gcodeText, repository=None):
70         "Clip a gcode linear move text."
71         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'clip'):
72                 return gcodeText
73         if repository == None:
74                 repository = settings.getReadRepository(ClipRepository())
75         if not repository.activateClip.value:
76                 return gcodeText
77         return ClipSkein().getCraftedGcode(gcodeText, repository)
78
79 def getNewRepository():
80         'Get new repository.'
81         return ClipRepository()
82
83 def writeOutput(fileName, shouldAnalyze=True):
84         "Clip a gcode linear move file.  Chain clip the gcode if it is not already clipped."
85         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'clip', shouldAnalyze)
86
87
88 class ClipRepository:
89         "A class to handle the clip settings."
90         def __init__(self):
91                 "Set the default settings, execute title & settings fileName."
92                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.clip.html', self)
93                 self.fileNameInput = settings.FileNameInput().getFromFileName(fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Clip', self, '')
94                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Clip')
95                 self.activateClip = settings.BooleanSetting().getFromValue('Activate Clip', self, False)
96                 self.clipOverEdgeWidth = settings.FloatSpin().getFromValue(0.1, 'Clip Over Perimeter Width (ratio):', self, 0.8, 0.5)
97                 self.maximumConnectionDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 1.0, 'Maximum Connection Distance Over Perimeter Width (ratio):', self, 20.0, 10.0)
98                 self.executeTitle = 'Clip'
99
100         def execute(self):
101                 "Clip button has been clicked."
102                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
103                 for fileName in fileNames:
104                         writeOutput(fileName)
105
106
107 class ClipSkein:
108         "A class to clip a skein of extrusions."
109         def __init__(self):
110                 self.distanceFeedRate = gcodec.DistanceFeedRate()
111                 self.extruderActive = False
112                 self.feedRateMinute = None
113                 self.isEdge = False
114                 self.isLoop = False
115                 self.layerCount = settings.LayerCount()
116                 self.loopPath = None
117                 self.lineIndex = 0
118                 self.oldConnectionPoint = None
119                 self.oldLocation = None
120                 self.oldWiddershins = None
121                 self.travelFeedRateMinute = None
122
123         def addGcodeFromThreadZ( self, thread, z ):
124                 "Add a gcode thread to the output."
125                 if len(thread) > 0:
126                         self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.travelFeedRateMinute, thread[0], z )
127                 else:
128                         print("zero length vertex positions array which was skipped over, this should never happen")
129                 if len(thread) < 2:
130                         print("thread of only one point in clip, this should never happen")
131                         print(thread)
132                         return
133                 self.distanceFeedRate.addLine('M101')
134                 for point in thread[1 :]:
135                         self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.feedRateMinute, point, z )
136
137         def addSegmentToPixelTables(self, location, oldLocation):
138                 "Add the segment to the layer and mask table."
139                 euclidean.addValueSegmentToPixelTable(oldLocation, location, self.layerPixelTable, None, self.layerPixelWidth)
140
141         def addTailoredLoopPath(self, line):
142                 "Add a clipped loop path."
143                 if self.clipLength > 0.0:
144                         removeTable = {}
145                         euclidean.addLoopToPixelTable(self.loopPath.path, removeTable, self.layerPixelWidth)
146                         euclidean.removePixelTableFromPixelTable( removeTable, self.layerPixelTable )
147                         self.loopPath.path = euclidean.getClippedSimplifiedLoopPath(self.clipLength, self.loopPath.path, self.edgeWidth)
148                         euclidean.addLoopToPixelTable( self.loopPath.path, self.layerPixelTable, self.layerPixelWidth )
149                 if self.oldWiddershins == None:
150                         self.addGcodeFromThreadZ( self.loopPath.path, self.loopPath.z )
151                 else:
152                         if self.oldWiddershins != euclidean.isWiddershins( self.loopPath.path ):
153                                 self.loopPath.path.reverse()
154                         for point in self.loopPath.path:
155                                 self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.feedRateMinute, point, self.loopPath.z )
156                 if self.getNextThreadIsACloseLoop(self.loopPath.path):
157                         self.oldConnectionPoint = self.loopPath.path[-1]
158                         self.oldWiddershins = euclidean.isWiddershins(self.loopPath.path)
159                 else:
160                         self.oldConnectionPoint = None
161                         self.oldWiddershins = None
162                         self.distanceFeedRate.addLine(line)
163                 self.loopPath = None
164
165         def getConnectionIsCloseWithoutOverlap( self, location, path ):
166                 "Determine if the connection is close enough and does not overlap another thread."
167                 if len(path) < 1:
168                         return False
169                 locationComplex = location.dropAxis()
170                 segment = locationComplex - path[-1]
171                 segmentLength = abs(segment)
172                 if segmentLength <= 0.0:
173                         return True
174                 if segmentLength > self.maximumConnectionDistance:
175                         return False
176                 segmentTable = {}
177                 euclidean.addSegmentToPixelTable( path[-1], locationComplex, segmentTable, 2.0, 2.0, self.layerPixelWidth )
178                 if euclidean.isPixelTableIntersecting( self.layerPixelTable, segmentTable, {} ):
179                         return False
180                 euclidean.addValueSegmentToPixelTable( path[-1], locationComplex, self.layerPixelTable, None, self.layerPixelWidth )
181                 return True
182
183         def getCraftedGcode(self, gcodeText, repository):
184                 "Parse gcode text and store the clip gcode."
185                 self.lines = archive.getTextLines(gcodeText)
186                 self.repository = repository
187                 self.parseInitialization()
188                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
189                         line = self.lines[self.lineIndex]
190                         self.parseLine(line)
191                 return self.distanceFeedRate.output.getvalue()
192
193         def getNextThreadIsACloseLoop(self, path):
194                 "Determine if the next thread is a loop."
195                 if self.oldLocation == None or self.maximumConnectionDistance <= 0.0:
196                         return False
197                 isEdge = False
198                 isLoop = False
199                 location = self.oldLocation
200                 for afterIndex in xrange(self.lineIndex + 1, len(self.lines)):
201                         line = self.lines[afterIndex]
202                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
203                         firstWord = gcodec.getFirstWord(splitLine)
204                         if firstWord == 'G1':
205                                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
206                         elif firstWord == '(<loop>':
207                                 isLoop = True
208                         elif firstWord == '(<edge>':
209                                 isEdge = True
210                         elif firstWord == 'M101':
211                                 if isLoop != self.isLoop or isEdge != self.isEdge:
212                                         return False
213                                 return self.getConnectionIsCloseWithoutOverlap(location, path)
214                         elif firstWord == '(<layer>':
215                                 return False
216                 return False
217
218         def isNextExtruderOn(self):
219                 "Determine if there is an extruder on command before a move command."
220                 for afterIndex in xrange(self.lineIndex + 1, len(self.lines)):
221                         line = self.lines[afterIndex]
222                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
223                         firstWord = gcodec.getFirstWord(splitLine)
224                         if firstWord == 'G1' or firstWord == 'M103':
225                                 return False
226                         elif firstWord == 'M101':
227                                 return True
228                 return False
229
230         def linearMove(self, splitLine):
231                 "Add to loop path if this is a loop or path."
232                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
233                 self.feedRateMinute = gcodec.getFeedRateMinute(self.feedRateMinute, splitLine)
234                 if self.isLoop or self.isEdge:
235                         if self.isNextExtruderOn():
236                                 self.loopPath = euclidean.PathZ(location.z)
237                 if self.loopPath == None:
238                         if self.extruderActive:
239                                 self.oldWiddershins = None
240                 else:
241                         if self.oldConnectionPoint != None:
242                                 self.addSegmentToPixelTables(self.oldConnectionPoint, location.dropAxis())
243                                 self.oldConnectionPoint = None
244                         self.loopPath.path.append(location.dropAxis())
245                 self.oldLocation = location
246
247         def parseInitialization(self):
248                 'Parse gcode initialization and store the parameters.'
249                 for self.lineIndex in xrange(len(self.lines)):
250                         line = self.lines[self.lineIndex]
251                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
252                         firstWord = gcodec.getFirstWord(splitLine)
253                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
254                         if firstWord == '(</extruderInitialization>)':
255                                 self.distanceFeedRate.addTagBracketedProcedure('clip')
256                                 return
257                         elif firstWord == '(<edgeWidth>':
258                                 self.distanceFeedRate.addTagBracketedLine('clipOverEdgeWidth', self.repository.clipOverEdgeWidth.value)
259                                 self.edgeWidth = float(splitLine[1])
260                                 absoluteEdgeWidth = abs(self.edgeWidth)
261                                 self.clipLength = self.repository.clipOverEdgeWidth.value * self.edgeWidth
262                                 self.connectingStepLength = 0.5 * absoluteEdgeWidth
263                                 self.layerPixelWidth = 0.34321 * absoluteEdgeWidth
264                                 self.maximumConnectionDistance = self.repository.maximumConnectionDistanceOverEdgeWidth.value * absoluteEdgeWidth
265                         elif firstWord == '(<travelFeedRatePerSecond>':
266                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
267                         self.distanceFeedRate.addLine(line)
268
269         def parseLine(self, line):
270                 "Parse a gcode line and add it to the clip skein."
271                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
272                 if len(splitLine) < 1:
273                         return
274                 firstWord = splitLine[0]
275                 if firstWord == 'G1':
276                         self.linearMove(splitLine)
277                 elif firstWord == '(<layer>':
278                         self.setLayerPixelTable()
279                 elif firstWord == '(<loop>':
280                         self.isLoop = True
281                 elif firstWord == '(</loop>)':
282                         self.isLoop = False
283                 elif firstWord == 'M101':
284                         self.extruderActive = True
285                 elif firstWord == 'M103':
286                         self.extruderActive = False
287                         if self.loopPath != None:
288                                 self.addTailoredLoopPath(line)
289                                 return
290                 elif firstWord == '(<edge>':
291                         self.isEdge = True
292                 elif firstWord == '(</edge>)':
293                         self.isEdge = False
294                 if self.loopPath == None:
295                         self.distanceFeedRate.addLine(line)
296
297         def setLayerPixelTable(self):
298                 "Set the layer pixel table."
299                 self.layerCount.printProgressIncrement('clip')
300                 boundaryLoop = None
301                 extruderActive = False
302                 self.lastInactiveLocation = None
303                 self.layerPixelTable = {}
304                 oldLocation = self.oldLocation
305                 for afterIndex in xrange(self.lineIndex + 1, len(self.lines)):
306                         line = self.lines[ afterIndex ]
307                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
308                         firstWord = gcodec.getFirstWord(splitLine)
309                         if firstWord == 'G1':
310                                 location = gcodec.getLocationFromSplitLine(oldLocation, splitLine)
311                                 if extruderActive and oldLocation != None:
312                                         self.addSegmentToPixelTables(location.dropAxis(), oldLocation.dropAxis())
313                                 if extruderActive:
314                                         if self.lastInactiveLocation != None:
315                                                 self.addSegmentToPixelTables(self.lastInactiveLocation.dropAxis(), location.dropAxis())
316                                                 self.lastInactiveLocation = None
317                                 else:
318                                         self.lastInactiveLocation = location
319                                 oldLocation = location
320                         elif firstWord == 'M101':
321                                 extruderActive = True
322                         elif firstWord == 'M103':
323                                 extruderActive = False
324                         elif firstWord == '(</boundaryPerimeter>)':
325                                 euclidean.addLoopToPixelTable(boundaryLoop, self.layerPixelTable, self.layerPixelWidth)
326                                 boundaryLoop = None
327                         elif firstWord == '(<boundaryPoint>':
328                                 if boundaryLoop == None:
329                                         boundaryLoop = []
330                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
331                                 boundaryLoop.append(location.dropAxis())
332                         elif firstWord == '(</layer>)':
333                                 return
334
335 def main():
336         "Display the clip dialog."
337         if len(sys.argv) > 1:
338                 writeOutput(' '.join(sys.argv[1 :]))
339         else:
340                 settings.startMainLoopFromConstructor(getNewRepository())
341
342 if __name__ == "__main__":
343         main()