chiark / gitweb /
Fix head size on object loading.
[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
16 from Cura.util import profile
17 from Cura.util import version
18
19 def getEngineFilename():
20         if platform.system() == 'Windows':
21                 if os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
22                         return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
23                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
24         if hasattr(sys, 'frozen'):
25                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
26         if os.path.isfile('/usr/bin/CuraEngine'):
27                 return '/usr/bin/CuraEngine'
28         if os.path.isfile('/usr/local/bin/CuraEngine'):
29                 return '/usr/local/bin/CuraEngine'
30         return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
31
32 def getTempFilename():
33         warnings.simplefilter('ignore')
34         ret = os.tempnam(None, "Cura_Tmp")
35         warnings.simplefilter('default')
36         return ret
37
38 class Slicer(object):
39         def __init__(self, progressCallback):
40                 self._process = None
41                 self._thread = None
42                 self._callback = progressCallback
43                 self._binaryStorageFilename = getTempFilename()
44                 self._exportFilename = getTempFilename()
45                 self._progressSteps = ['inset', 'skin', 'export']
46                 self._objCount = 0
47                 self._sliceLog = []
48                 self._printTimeSeconds = None
49                 self._filamentMM = [0.0, 0.0]
50                 self._modelHash = None
51                 self._id = 0
52
53         def cleanup(self):
54                 self.abortSlicer()
55                 try:
56                         os.remove(self._binaryStorageFilename)
57                 except:
58                         pass
59                 try:
60                         os.remove(self._exportFilename)
61                 except:
62                         pass
63
64         def abortSlicer(self):
65                 if self._process is not None:
66                         try:
67                                 self._process.terminate()
68                         except:
69                                 pass
70                         self._thread.join()
71                 self._thread = None
72
73         def wait(self):
74                 if self._thread is not None:
75                         self._thread.join()
76
77         def getGCodeFilename(self):
78                 return self._exportFilename
79
80         def getSliceLog(self):
81                 return self._sliceLog
82
83         def getID(self):
84                 return self._id
85
86         def getFilamentWeight(self, e=0):
87                 #Calculates the weight of the filament in kg
88                 radius = float(profile.getProfileSetting('filament_diameter')) / 2
89                 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
90                 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
91
92         def getFilamentCost(self, e=0):
93                 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
94                 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
95                 if cost_kg > 0.0 and cost_meter > 0.0:
96                         return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
97                 elif cost_kg > 0.0:
98                         return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
99                 elif cost_meter > 0.0:
100                         return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
101                 return None
102
103         def getPrintTime(self):
104                 if int(self._printTimeSeconds / 60 / 60) < 1:
105                         return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
106                 if int(self._printTimeSeconds / 60 / 60) == 1:
107                         return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
108                 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
109
110         def getFilamentAmount(self, e=0):
111                 if self._filamentMM[e] == 0.0:
112                         return None
113                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
114
115         def runSlicer(self, scene):
116                 if len(scene.objects()) < 1:
117                         return
118                 extruderCount = 1
119                 for obj in scene.objects():
120                         if scene.checkPlatform(obj):
121                                 extruderCount = max(extruderCount, len(obj._meshList))
122
123                 extruderCount = max(extruderCount, profile.minimalExtruderCount())
124
125                 commandList = [getEngineFilename(), '-vv']
126                 for k, v in self._engineSettings(extruderCount).iteritems():
127                         commandList += ['-s', '%s=%s' % (k, str(v))]
128                 commandList += ['-o', self._exportFilename]
129                 commandList += ['-b', self._binaryStorageFilename]
130                 self._objCount = 0
131                 with open(self._binaryStorageFilename, "wb") as f:
132                         hash = hashlib.sha512()
133                         order = scene.printOrder()
134                         if order is None:
135                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
136                                 objMin = None
137                                 objMax = None
138                                 for obj in scene.objects():
139                                         if scene.checkPlatform(obj):
140                                                 oMin = obj.getMinimum()[0:2] + obj.getPosition()
141                                                 oMax = obj.getMaximum()[0:2] + obj.getPosition()
142                                                 if objMin is None:
143                                                         objMin = oMin
144                                                         objMax = oMax
145                                                 else:
146                                                         objMin[0] = min(oMin[0], objMin[0])
147                                                         objMin[1] = min(oMin[1], objMin[1])
148                                                         objMax[0] = max(oMax[0], objMax[0])
149                                                         objMax[1] = max(oMax[1], objMax[1])
150                                 pos += (objMin + objMax) / 2.0 * 1000
151                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
152
153                                 vertexTotal = 0
154                                 for obj in scene.objects():
155                                         if scene.checkPlatform(obj):
156                                                 for mesh in obj._meshList:
157                                                         vertexTotal += mesh.vertexCount
158
159                                 f.write(numpy.array([vertexTotal], numpy.int32).tostring())
160                                 for obj in scene.objects():
161                                         if scene.checkPlatform(obj):
162                                                 for mesh in obj._meshList:
163                                                         vertexes = (numpy.matrix(mesh.vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
164                                                         vertexes -= obj._drawOffset
165                                                         vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
166                                                         f.write(vertexes.tostring())
167                                                         hash.update(mesh.vertexes.tostring())
168
169                                 commandList += ['#']
170                                 self._objCount = 1
171                         else:
172                                 for n in order:
173                                         obj = scene.objects()[n]
174                                         for mesh in obj._meshList:
175                                                 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
176                                                 s = mesh.vertexes.tostring()
177                                                 f.write(s)
178                                                 hash.update(s)
179                                         pos = obj.getPosition() * 1000
180                                         pos += numpy.array(profile.getMachineCenterCoords()) * 1000
181                                         commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
182                                         commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
183                                         commandList += ['#' * len(obj._meshList)]
184                                         self._objCount += 1
185                         self._modelHash = hash.hexdigest()
186                 if self._objCount > 0:
187                         self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
188                         self._thread.daemon = True
189                         self._thread.start()
190
191         def _watchProcess(self, commandList, oldThread):
192                 if oldThread is not None:
193                         if self._process is not None:
194                                 self._process.terminate()
195                         oldThread.join()
196                 self._id += 1
197                 self._callback(-1.0, False)
198                 try:
199                         self._process = self._runSliceProcess(commandList)
200                 except OSError:
201                         traceback.print_exc()
202                         return
203                 if self._thread != threading.currentThread():
204                         self._process.terminate()
205                 self._callback(0.0, False)
206                 self._sliceLog = []
207                 self._printTimeSeconds = None
208                 self._filamentMM = [0.0, 0.0]
209
210                 line = self._process.stdout.readline()
211                 objectNr = 0
212                 while len(line):
213                         line = line.strip()
214                         if line.startswith('Progress:'):
215                                 line = line.split(':')
216                                 if line[1] == 'process':
217                                         objectNr += 1
218                                 elif line[1] in self._progressSteps:
219                                         progressValue = float(line[2]) / float(line[3])
220                                         progressValue /= len(self._progressSteps)
221                                         progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
222
223                                         progressValue /= self._objCount
224                                         progressValue += 1.0 / self._objCount * objectNr
225                                         try:
226                                                 self._callback(progressValue, False)
227                                         except:
228                                                 pass
229                         elif line.startswith('Print time:'):
230                                 self._printTimeSeconds = int(line.split(':')[1].strip())
231                         elif line.startswith('Filament:'):
232                                 self._filamentMM[0] = int(line.split(':')[1].strip())
233                                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
234                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
235                                         self._filamentMM[0] /= (math.pi * radius * radius)
236                         elif line.startswith('Filament2:'):
237                                 self._filamentMM[1] = int(line.split(':')[1].strip())
238                                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
239                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
240                                         self._filamentMM[1] /= (math.pi * radius * radius)
241                         else:
242                                 self._sliceLog.append(line.strip())
243                         line = self._process.stdout.readline()
244                 for line in self._process.stderr:
245                         self._sliceLog.append(line.strip())
246                 returnCode = self._process.wait()
247                 try:
248                         if returnCode == 0:
249                                 pluginError = profile.runPostProcessingPlugins(self._exportFilename)
250                                 if pluginError is not None:
251                                         print pluginError
252                                         self._sliceLog.append(pluginError)
253                                 self._callback(1.0, True)
254                         else:
255                                 for line in self._sliceLog:
256                                         print line
257                                 self._callback(-1.0, False)
258                 except:
259                         pass
260                 self._process = None
261
262         def _engineSettings(self, extruderCount):
263                 settings = {
264                         'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
265                         'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
266                         'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
267                         'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
268                         'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
269                         'insetCount': int(profile.calculateLineCount()),
270                         'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
271                         'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
272                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
273                         'initialSpeedupLayers': int(4),
274                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
275                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
276                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
277                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
278                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
279                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
280                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
281                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
282                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
283                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
284                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
285                         '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),
286                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
287                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
288                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
289                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
290                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
291                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
292                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
293                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
294                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
295                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
296                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
297                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
298                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
299
300                         'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
301                         'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
302                         'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
303                         'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
304                         'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
305                         'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
306                         'fixHorrible': 0,
307                 }
308                 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
309                 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
310                 if settings['fanFullOnLayerNr'] < 0:
311                         settings['fanFullOnLayerNr'] = 0
312
313                 if profile.getProfileSettingFloat('fill_density') == 0:
314                         settings['sparseInfillLineDistance'] = -1
315                 elif profile.getProfileSettingFloat('fill_density') == 100:
316                         settings['sparseInfillLineDistance'] = settings['extrusionWidth']
317                         #Set the up/down skins height to 10000 if we want a 100% filled object.
318                         # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
319                         settings['downSkinCount'] = 10000
320                         settings['upSkinCount'] = 10000
321                 else:
322                         settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
323                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
324                         settings['skirtDistance'] = 0
325                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
326                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
327                         settings['skirtDistance'] = 0
328                         settings['skirtLineCount'] = 0
329                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
330                         settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
331                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
332                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
333                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
334                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
335                 else:
336                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
337                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
338                         settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
339
340                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
341                         settings['fixHorrible'] |= 0x01
342                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
343                         settings['fixHorrible'] |= 0x02
344                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
345                         settings['fixHorrible'] |= 0x10
346                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
347                         settings['fixHorrible'] |= 0x04
348
349                 if settings['layerThickness'] <= 0:
350                         settings['layerThickness'] = 1000
351                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
352                         settings['gcodeFlavor'] = 1
353                 if profile.getProfileSetting('spiralize') == 'True':
354                         settings['spiralizeMode'] = 1
355                 if profile.getProfileSetting('wipe_tower') == 'True':
356                         settings['enableWipeTower'] = 1
357                 if profile.getProfileSetting('ooze_shield') == 'True':
358                         settings['enableOozeShield'] = 1
359                 return settings
360
361         def _runSliceProcess(self, cmdList):
362                 kwargs = {}
363                 if subprocess.mswindows:
364                         su = subprocess.STARTUPINFO()
365                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
366                         su.wShowWindow = subprocess.SW_HIDE
367                         kwargs['startupinfo'] = su
368                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
369                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
370
371         def submitSliceInfoOnline(self):
372                 if profile.getPreference('submit_slice_information') != 'True':
373                         return
374                 if version.isDevVersion():
375                         return
376                 data = {
377                         'processor': platform.processor(),
378                         'machine': platform.machine(),
379                         'platform': platform.platform(),
380                         'profile': profile.getProfileString(),
381                         'preferences': profile.getPreferencesString(),
382                         'modelhash': self._modelHash,
383                         'version': version.getVersion(),
384                 }
385                 try:
386                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
387                         f.read()
388                         f.close()
389                 except:
390                         pass