chiark / gitweb /
Change how the engine is interfaced from the python code. Put the GCode viewer in...
[cura.git] / Cura / util / sliceEngine.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2 import subprocess
3 import time
4 import math
5 import numpy
6 import os
7 import warnings
8 import threading
9 import traceback
10 import platform
11 import sys
12 import urllib
13 import urllib2
14 import hashlib
15 import cStringIO as StringIO
16
17 from Cura.util import profile
18 from Cura.util import version
19 from Cura.util import gcodeInterpreter
20
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'))
33
34 def getTempFilename():
35         warnings.simplefilter('ignore')
36         ret = os.tempnam(None, "Cura_Tmp")
37         warnings.simplefilter('default')
38         return ret
39
40 class EngineResult(object):
41         def __init__(self):
42                 self._engineLog = []
43                 self._gcodeData = StringIO.StringIO()
44                 self._polygons = []
45                 self._success = False
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
54
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')
60
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)
66                 elif cost_kg > 0.0:
67                         return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
68                 elif cost_meter > 0.0:
69                         return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
70                 return None
71
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)
78
79         def getFilamentAmount(self, e=0):
80                 if self._filamentMM[e] == 0.0:
81                         return None
82                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
83
84         def getLog(self):
85                 return self._engineLog
86
87         def getGCode(self):
88                 return self._gcodeData.getvalue()
89
90         def addLog(self, line):
91                 self._engineLog.append(line)
92
93         def setHash(self, hash):
94                 self._modelHash = hash
95
96         def setFinished(self, result):
97                 self._finished = result
98
99         def isFinished(self):
100                 return self._finished
101
102         def getGCodeLayers(self):
103                 if not self._finished:
104                         return None
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
111
112         def _gcodeInterpreterCallback(self, progress):
113                 if len(self._gcodeInterpreter.layerList) % 15 == 0:
114                         time.sleep(0.1)
115                 return False
116
117         def submitInfoOnline(self):
118                 if profile.getPreference('submit_slice_information') != 'True':
119                         return
120                 if version.isDevVersion():
121                         return
122                 data = {
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(),
130                 }
131                 try:
132                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
133                         f.read()
134                         f.close()
135                 except:
136                         pass
137
138 class Engine(object):
139         def __init__(self, progressCallback):
140                 self._process = None
141                 self._thread = None
142                 self._callback = progressCallback
143                 self._binaryStorageFilename = getTempFilename()
144                 self._progressSteps = ['inset', 'skin', 'export']
145                 self._objCount = 0
146                 self._result = None
147
148         def cleanup(self):
149                 self.abortEngine()
150                 try:
151                         os.remove(self._binaryStorageFilename)
152                 except:
153                         pass
154
155         def abortEngine(self):
156                 if self._process is not None:
157                         try:
158                                 self._process.terminate()
159                         except:
160                                 pass
161                 if self._thread is not None:
162                         self._thread.join()
163                 self._thread = None
164
165         def wait(self):
166                 if self._thread is not None:
167                         self._thread.join()
168
169         def getResult(self):
170                 return self._result
171
172         def runEngine(self, scene):
173                 if len(scene.objects()) < 1:
174                         return
175                 extruderCount = 1
176                 for obj in scene.objects():
177                         if scene.checkPlatform(obj):
178                                 extruderCount = max(extruderCount, len(obj._meshList))
179
180                 extruderCount = max(extruderCount, profile.minimalExtruderCount())
181
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]
186                 self._objCount = 0
187                 with open(self._binaryStorageFilename, "wb") as f:
188                         hash = hashlib.sha512()
189                         order = scene.printOrder()
190                         if order is None:
191                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
192                                 objMin = None
193                                 objMax = None
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()
198                                                 if objMin is None:
199                                                         objMin = oMin
200                                                         objMax = oMax
201                                                 else:
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])
206                                 if objMin is None:
207                                         return
208                                 pos += (objMin + objMax) / 2.0 * 1000
209                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
210
211                                 vertexTotal = [0] * 4
212                                 meshMax = 1
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
218
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())
229
230                                 commandList += ['#' * meshMax]
231                                 self._objCount = 1
232                         else:
233                                 for n in order:
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()
238                                                 f.write(s)
239                                                 hash.update(s)
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)]
245                                         self._objCount += 1
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
250                         self._thread.start()
251
252         def _watchProcess(self, commandList, oldThread, modelHash):
253                 if oldThread is not None:
254                         if self._process is not None:
255                                 self._process.terminate()
256                         oldThread.join()
257                 self._callback(-1.0)
258                 try:
259                         self._process = self._runEngineProcess(commandList)
260                 except OSError:
261                         traceback.print_exc()
262                         return
263                 if self._thread != threading.currentThread():
264                         self._process.terminate()
265
266                 self._result = EngineResult()
267                 self._result.setHash(modelHash)
268                 self._callback(0.0)
269
270                 logThread = threading.Thread(target=self._watchStderr, args=(self._process.stderr,))
271                 logThread.daemon = True
272                 logThread.start()
273
274                 data = self._process.stdout.read(4096)
275                 while len(data) > 0:
276                         self._result._gcodeData.write(data)
277                         data = self._process.stdout.read(4096)
278
279                 returnCode = self._process.wait()
280                 logThread.join()
281                 if returnCode == 0:
282                         pluginError = None #profile.runPostProcessingPlugins(self._exportFilename)
283                         if pluginError is not None:
284                                 print pluginError
285                                 self._result.addLog(pluginError)
286                         self._result.setFinished(True)
287                         self._callback(1.0)
288                 else:
289                         for line in self._result.getLog():
290                                 print line
291                         self._callback(-1.0)
292                 self._process = None
293
294         def _watchStderr(self, stderr):
295                 objectNr = 0
296
297                 # data = stderr.read(4096)
298                 # tmp = StringIO.StringIO()
299                 # while len(data):
300                 #       tmp.write(data)
301                 #       data = stderr.read(4096)
302                 # stderr = StringIO.StringIO(tmp.getvalue())
303
304                 line = stderr.readline()
305                 while len(line) > 0:
306                         line = line.strip()
307                         if line.startswith('Progress:'):
308                                 line = line.split(':')
309                                 if line[1] == 'process':
310                                         objectNr += 1
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])
315
316                                         progressValue /= self._objCount
317                                         progressValue += 1.0 / self._objCount * objectNr
318                                         try:
319                                                 self._callback(progressValue)
320                                         except:
321                                                 pass
322                         elif line.startswith('Polygons:'):
323                                 line = line.split(':')
324                                 typeName = line[1]
325                                 layerNr = int(line[2])
326                                 size = int(line[3])
327                                 z = float(line[4])
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()
333                                         if not polygon:
334                                                 continue
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
339                                         polygon[:,2] = z
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)
355                         else:
356                                 self._result.addLog(line)
357                         line = stderr.readline()
358
359         def _engineSettings(self, extruderCount):
360                 settings = {
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),
396
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),
403                         'fixHorrible': 0,
404                 }
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
409
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
418                 else:
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)
432                 else:
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)
436
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
445
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
456                 return settings
457
458         def _runEngineProcess(self, cmdList):
459                 kwargs = {}
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)