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.
5 The clip manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Clip
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.
12 ===Clip Over Perimeter Width===
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.
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.
19 ===Maximum Connection Distance Over Perimeter Width===
22 Defines the ratio of the maximum connection distance between loops over the edge width.
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.
26 Setting this to zero disables this feature, preventing the loops from being connected.
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.
32 This brings up the clip dialog.
34 > python clip.py Screw Holder Bottom.stl
35 The clip tool is parsing the file:
36 Screw Holder Bottom.stl
38 The clip tool has created the file:
39 .. Screw Holder Bottom_clip.gcode
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.
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
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'
65 def getCraftedText(fileName, text, repository=None):
66 "Clip a gcode linear move file or text."
67 return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
69 def getCraftedTextFromText(gcodeText, repository=None):
70 "Clip a gcode linear move text."
71 if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'clip'):
73 if repository == None:
74 repository = settings.getReadRepository(ClipRepository())
75 if not repository.activateClip.value:
77 return ClipSkein().getCraftedGcode(gcodeText, repository)
79 def getNewRepository():
81 return ClipRepository()
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)
89 "A class to handle the clip settings."
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'
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)
108 "A class to clip a skein of extrusions."
110 self.distanceFeedRate = gcodec.DistanceFeedRate()
111 self.extruderActive = False
112 self.feedRateMinute = None
115 self.layerCount = settings.LayerCount()
118 self.oldConnectionPoint = None
119 self.oldLocation = None
120 self.oldWiddershins = None
121 self.travelFeedRateMinute = None
123 def addGcodeFromThreadZ( self, thread, z ):
124 "Add a gcode thread to the output."
126 self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.travelFeedRateMinute, thread[0], z )
128 print("zero length vertex positions array which was skipped over, this should never happen")
130 print("thread of only one point in clip, this should never happen")
133 self.distanceFeedRate.addLine('M101')
134 for point in thread[1 :]:
135 self.distanceFeedRate.addGcodeMovementZWithFeedRate( self.feedRateMinute, point, z )
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)
141 def addTailoredLoopPath(self, line):
142 "Add a clipped loop path."
143 if self.clipLength > 0.0:
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 )
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)
160 self.oldConnectionPoint = None
161 self.oldWiddershins = None
162 self.distanceFeedRate.addLine(line)
165 def getConnectionIsCloseWithoutOverlap( self, location, path ):
166 "Determine if the connection is close enough and does not overlap another thread."
169 locationComplex = location.dropAxis()
170 segment = locationComplex - path[-1]
171 segmentLength = abs(segment)
172 if segmentLength <= 0.0:
174 if segmentLength > self.maximumConnectionDistance:
177 euclidean.addSegmentToPixelTable( path[-1], locationComplex, segmentTable, 2.0, 2.0, self.layerPixelWidth )
178 if euclidean.isPixelTableIntersecting( self.layerPixelTable, segmentTable, {} ):
180 euclidean.addValueSegmentToPixelTable( path[-1], locationComplex, self.layerPixelTable, None, self.layerPixelWidth )
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]
191 return self.distanceFeedRate.output.getvalue()
193 def getNextThreadIsACloseLoop(self, path):
194 "Determine if the next thread is a loop."
195 if self.oldLocation == None or self.maximumConnectionDistance <= 0.0:
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>':
208 elif firstWord == '(<edge>':
210 elif firstWord == 'M101':
211 if isLoop != self.isLoop or isEdge != self.isEdge:
213 return self.getConnectionIsCloseWithoutOverlap(location, path)
214 elif firstWord == '(<layer>':
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':
226 elif firstWord == 'M101':
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
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
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')
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)
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:
274 firstWord = splitLine[0]
275 if firstWord == 'G1':
276 self.linearMove(splitLine)
277 elif firstWord == '(<layer>':
278 self.setLayerPixelTable()
279 elif firstWord == '(<loop>':
281 elif firstWord == '(</loop>)':
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)
290 elif firstWord == '(<edge>':
292 elif firstWord == '(</edge>)':
294 if self.loopPath == None:
295 self.distanceFeedRate.addLine(line)
297 def setLayerPixelTable(self):
298 "Set the layer pixel table."
299 self.layerCount.printProgressIncrement('clip')
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())
314 if self.lastInactiveLocation != None:
315 self.addSegmentToPixelTables(self.lastInactiveLocation.dropAxis(), location.dropAxis())
316 self.lastInactiveLocation = None
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)
327 elif firstWord == '(<boundaryPoint>':
328 if boundaryLoop == None:
330 location = gcodec.getLocationFromSplitLine(None, splitLine)
331 boundaryLoop.append(location.dropAxis())
332 elif firstWord == '(</layer>)':
336 "Display the clip dialog."
337 if len(sys.argv) > 1:
338 writeOutput(' '.join(sys.argv[1 :]))
340 settings.startMainLoopFromConstructor(getNewRepository())
342 if __name__ == "__main__":