1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
15 import cStringIO as StringIO
17 from Cura.util import profile
18 from Cura.util import version
19 from Cura.util import gcodeInterpreter
21 def getEngineFilename():
22 if platform.system() == 'Windows':
23 if version.isDevVersion() and os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
24 return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
25 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
26 if hasattr(sys, 'frozen'):
27 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
28 if os.path.isfile('/usr/bin/CuraEngine'):
29 return '/usr/bin/CuraEngine'
30 if os.path.isfile('/usr/local/bin/CuraEngine'):
31 return '/usr/local/bin/CuraEngine'
32 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
34 def getTempFilename():
35 warnings.simplefilter('ignore')
36 ret = os.tempnam(None, "Cura_Tmp")
37 warnings.simplefilter('default')
40 class EngineResult(object):
43 self._gcodeData = StringIO.StringIO()
46 self._printTimeSeconds = None
47 self._filamentMM = [0.0] * 4
48 self._modelHash = None
49 self._profileString = profile.getProfileString()
50 self._preferencesString = profile.getPreferencesString()
51 self._gcodeInterpreter = gcodeInterpreter.gcode()
52 self._gcodeLoadThread = None
53 self._finished = False
55 def getFilamentWeight(self, e=0):
56 #Calculates the weight of the filament in kg
57 radius = float(profile.getProfileSetting('filament_diameter')) / 2
58 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
59 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
61 def getFilamentCost(self, e=0):
62 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
63 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
64 if cost_kg > 0.0 and cost_meter > 0.0:
65 return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
67 return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
68 elif cost_meter > 0.0:
69 return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
72 def getPrintTime(self):
73 if int(self._printTimeSeconds / 60 / 60) < 1:
74 return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
75 if int(self._printTimeSeconds / 60 / 60) == 1:
76 return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
77 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
79 def getFilamentAmount(self, e=0):
80 if self._filamentMM[e] == 0.0:
82 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
85 return self._engineLog
88 return self._gcodeData.getvalue()
90 def addLog(self, line):
91 self._engineLog.append(line)
93 def setHash(self, hash):
94 self._modelHash = hash
96 def setFinished(self, result):
97 self._finished = result
100 return self._finished
102 def getGCodeLayers(self):
103 if not self._finished:
105 if self._gcodeInterpreter.layerList is None and self._gcodeLoadThread is None:
106 self._gcodeInterpreter.progressCallback = self._gcodeInterpreterCallback
107 self._gcodeLoadThread = threading.Thread(target=lambda : self._gcodeInterpreter.load(self._gcodeData))
108 self._gcodeLoadThread.daemon = True
109 self._gcodeLoadThread.start()
110 return self._gcodeInterpreter.layerList
112 def _gcodeInterpreterCallback(self, progress):
113 if len(self._gcodeInterpreter.layerList) % 15 == 0:
117 def submitInfoOnline(self):
118 if profile.getPreference('submit_slice_information') != 'True':
120 if version.isDevVersion():
123 'processor': platform.processor(),
124 'machine': platform.machine(),
125 'platform': platform.platform(),
126 'profile': self._profileString,
127 'preferences': self._preferencesString,
128 'modelhash': self._modelHash,
129 'version': version.getVersion(),
132 f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
138 class Engine(object):
139 def __init__(self, progressCallback):
142 self._callback = progressCallback
143 self._binaryStorageFilename = getTempFilename()
144 self._progressSteps = ['inset', 'skin', 'export']
151 os.remove(self._binaryStorageFilename)
155 def abortEngine(self):
156 if self._process is not None:
158 self._process.terminate()
161 if self._thread is not None:
166 if self._thread is not None:
172 def runEngine(self, scene):
173 if len(scene.objects()) < 1:
176 for obj in scene.objects():
177 if scene.checkPlatform(obj):
178 extruderCount = max(extruderCount, len(obj._meshList))
180 extruderCount = max(extruderCount, profile.minimalExtruderCount())
182 commandList = [getEngineFilename(), '-vvv']
183 for k, v in self._engineSettings(extruderCount).iteritems():
184 commandList += ['-s', '%s=%s' % (k, str(v))]
185 commandList += ['-b', self._binaryStorageFilename]
187 with open(self._binaryStorageFilename, "wb") as f:
188 hash = hashlib.sha512()
189 order = scene.printOrder()
191 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
194 for obj in scene.objects():
195 if scene.checkPlatform(obj):
196 oMin = obj.getMinimum()[0:2] + obj.getPosition()
197 oMax = obj.getMaximum()[0:2] + obj.getPosition()
202 objMin[0] = min(oMin[0], objMin[0])
203 objMin[1] = min(oMin[1], objMin[1])
204 objMax[0] = max(oMax[0], objMax[0])
205 objMax[1] = max(oMax[1], objMax[1])
208 pos += (objMin + objMax) / 2.0 * 1000
209 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
211 vertexTotal = [0] * 4
213 for obj in scene.objects():
214 if scene.checkPlatform(obj):
215 meshMax = max(meshMax, len(obj._meshList))
216 for n in xrange(0, len(obj._meshList)):
217 vertexTotal[n] += obj._meshList[n].vertexCount
219 for n in xrange(0, meshMax):
220 f.write(numpy.array([vertexTotal[n]], numpy.int32).tostring())
221 for obj in scene.objects():
222 if scene.checkPlatform(obj):
223 if n < len(obj._meshList):
224 vertexes = (numpy.matrix(obj._meshList[n].vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
225 vertexes -= obj._drawOffset
226 vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
227 f.write(vertexes.tostring())
228 hash.update(obj._meshList[n].vertexes.tostring())
230 commandList += ['#' * meshMax]
234 obj = scene.objects()[n]
235 for mesh in obj._meshList:
236 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
237 s = mesh.vertexes.tostring()
240 pos = obj.getPosition() * 1000
241 pos += numpy.array(profile.getMachineCenterCoords()) * 1000
242 commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
243 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
244 commandList += ['#' * len(obj._meshList)]
246 modelHash = hash.hexdigest()
247 if self._objCount > 0:
248 self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread, modelHash))
249 self._thread.daemon = True
252 def _watchProcess(self, commandList, oldThread, modelHash):
253 if oldThread is not None:
254 if self._process is not None:
255 self._process.terminate()
259 self._process = self._runEngineProcess(commandList)
261 traceback.print_exc()
263 if self._thread != threading.currentThread():
264 self._process.terminate()
266 self._result = EngineResult()
267 self._result.setHash(modelHash)
270 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
271 logThread.daemon = True
274 data = self._process.stdout.read(4096)
276 self._result._gcodeData.write(data)
277 data = self._process.stdout.read(4096)
279 returnCode = self._process.wait()
282 pluginError = None #profile.runPostProcessingPlugins(self._exportFilename)
283 if pluginError is not None:
285 self._result.addLog(pluginError)
286 self._result.setFinished(True)
289 for line in self._result.getLog():
294 def _watchStderr(self, stderr):
297 # data = stderr.read(4096)
298 # tmp = StringIO.StringIO()
301 # data = stderr.read(4096)
302 # stderr = StringIO.StringIO(tmp.getvalue())
304 line = stderr.readline()
307 if line.startswith('Progress:'):
308 line = line.split(':')
309 if line[1] == 'process':
311 elif line[1] in self._progressSteps:
312 progressValue = float(line[2]) / float(line[3])
313 progressValue /= len(self._progressSteps)
314 progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
316 progressValue /= self._objCount
317 progressValue += 1.0 / self._objCount * objectNr
319 self._callback(progressValue)
322 elif line.startswith('Polygons:'):
323 line = line.split(':')
325 layerNr = int(line[2])
328 while len(self._result._polygons) < layerNr + 1:
329 self._result._polygons.append({})
330 polygons = self._result._polygons[layerNr]
331 for n in xrange(0, size):
332 polygon = stderr.readline().strip()
335 polygon2d = numpy.fromstring(polygon, dtype=numpy.float32, sep=' ')
336 polygon2d = polygon2d.reshape((len(polygon2d) / 2, 2))
337 polygon = numpy.empty((len(polygon2d), 3), numpy.float32)
338 polygon[:,:-1] = polygon2d
340 if typeName not in polygons:
341 polygons[typeName] = []
342 polygons[typeName].append(polygon)
343 elif line.startswith('Print time:'):
344 self._result._printTimeSeconds = int(line.split(':')[1].strip())
345 elif line.startswith('Filament:'):
346 self._result._filamentMM[0] = int(line.split(':')[1].strip())
347 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
348 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
349 self._result._filamentMM[0] /= (math.pi * radius * radius)
350 elif line.startswith('Filament2:'):
351 self._result._filamentMM[1] = int(line.split(':')[1].strip())
352 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
353 radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
354 self._result._filamentMM[1] /= (math.pi * radius * radius)
356 self._result.addLog(line)
357 line = stderr.readline()
359 def _engineSettings(self, extruderCount):
361 'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
362 'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
363 'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
364 'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
365 'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
366 'insetCount': int(profile.calculateLineCount()),
367 'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
368 'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
369 'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
370 'initialSpeedupLayers': int(4),
371 'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
372 'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
373 'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
374 'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
375 'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
376 'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
377 'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(profile.getProfileSettingFloat('support_angle')),
378 'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
379 'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
380 'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
381 'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
382 'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' and profile.minimalExtruderCount() > 1 else -1),
383 'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
384 'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
385 'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
386 'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
387 'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
388 'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
389 'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
390 'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
391 'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
392 'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
393 'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
394 'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
395 'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
397 'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
398 'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
399 'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
400 'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
401 'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
402 'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
405 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
406 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
407 if settings['fanFullOnLayerNr'] < 0:
408 settings['fanFullOnLayerNr'] = 0
410 if profile.getProfileSettingFloat('fill_density') == 0:
411 settings['sparseInfillLineDistance'] = -1
412 elif profile.getProfileSettingFloat('fill_density') == 100:
413 settings['sparseInfillLineDistance'] = settings['extrusionWidth']
414 #Set the up/down skins height to 10000 if we want a 100% filled object.
415 # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
416 settings['downSkinCount'] = 10000
417 settings['upSkinCount'] = 10000
419 settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
420 if profile.getProfileSetting('platform_adhesion') == 'Brim':
421 settings['skirtDistance'] = 0
422 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
423 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
424 settings['skirtDistance'] = 0
425 settings['skirtLineCount'] = 0
426 settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
427 settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
428 settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
429 settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
430 settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
431 settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
433 settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
434 settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
435 settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
437 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
438 settings['fixHorrible'] |= 0x01
439 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
440 settings['fixHorrible'] |= 0x02
441 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
442 settings['fixHorrible'] |= 0x10
443 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
444 settings['fixHorrible'] |= 0x04
446 if settings['layerThickness'] <= 0:
447 settings['layerThickness'] = 1000
448 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
449 settings['gcodeFlavor'] = 1
450 if profile.getProfileSetting('spiralize') == 'True':
451 settings['spiralizeMode'] = 1
452 if profile.getProfileSetting('wipe_tower') == 'True':
453 settings['wipeTowerSize'] = int(math.sqrt(profile.getProfileSettingFloat('wipe_tower_volume') * 1000 * 1000 * 1000 / settings['layerThickness']))
454 if profile.getProfileSetting('ooze_shield') == 'True':
455 settings['enableOozeShield'] = 1
458 def _runEngineProcess(self, cmdList):
460 if subprocess.mswindows:
461 su = subprocess.STARTUPINFO()
462 su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
463 su.wShowWindow = subprocess.SW_HIDE
464 kwargs['startupinfo'] = su
465 kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
466 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)