chiark / gitweb /
Add spialize mode which makes printing of cups and vases easy.
[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 = None
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):
87                 #Calculates the weight of the filament in kg
88                 radius = float(profile.getProfileSetting('filament_diameter')) / 2
89                 volumeM3 = (self._filamentMM * (math.pi * radius * radius)) / (1000*1000*1000)
90                 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
91
92         def getFilamentCost(self):
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() * cost_kg, self._filamentMM / 1000.0 * cost_meter)
97                 elif cost_kg > 0.0:
98                         return "%.2f" % (self.getFilamentWeight() * cost_kg)
99                 elif cost_meter > 0.0:
100                         return "%.2f" % (self._filamentMM / 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):
111                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM) / 1000.0, self.getFilamentWeight() * 1000.0)
112
113         def runSlicer(self, scene):
114                 extruderCount = 1
115                 for obj in scene.objects():
116                         if scene.checkPlatform(obj):
117                                 extruderCount = max(extruderCount, len(obj._meshList))
118                 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder':
119                         extruderCount = max(extruderCount, 2)
120
121                 commandList = [getEngineFilename(), '-vv']
122                 for k, v in self._engineSettings(extruderCount).iteritems():
123                         commandList += ['-s', '%s=%s' % (k, str(v))]
124                 commandList += ['-o', self._exportFilename]
125                 commandList += ['-b', self._binaryStorageFilename]
126                 self._objCount = 0
127                 with open(self._binaryStorageFilename, "wb") as f:
128                         hash = hashlib.sha512()
129                         order = scene.printOrder()
130                         if order is None:
131                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
132                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
133
134                                 vertexTotal = 0
135                                 for obj in scene.objects():
136                                         if scene.checkPlatform(obj):
137                                                 for mesh in obj._meshList:
138                                                         vertexTotal += mesh.vertexCount
139
140                                 f.write(numpy.array([vertexTotal], numpy.int32).tostring())
141                                 for obj in scene.objects():
142                                         if scene.checkPlatform(obj):
143                                                 for mesh in obj._meshList:
144                                                         vertexes = (numpy.matrix(mesh.vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
145                                                         vertexes -= obj._drawOffset
146                                                         vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
147                                                         f.write(vertexes.tostring())
148                                                         hash.update(mesh.vertexes.tostring())
149
150                                 commandList += ['#']
151                                 self._objCount = 1
152                         else:
153                                 for n in order:
154                                         obj = scene.objects()[n]
155                                         for mesh in obj._meshList:
156                                                 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
157                                                 s = mesh.vertexes.tostring()
158                                                 f.write(s)
159                                                 hash.update(s)
160                                         pos = obj.getPosition() * 1000
161                                         pos += numpy.array(profile.getMachineCenterCoords()) * 1000
162                                         commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
163                                         commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
164                                         commandList += ['#' * len(obj._meshList)]
165                                         self._objCount += 1
166                         self._modelHash = hash.hexdigest()
167                 if self._objCount > 0:
168                         self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
169                         self._thread.daemon = True
170                         self._thread.start()
171
172         def _watchProcess(self, commandList, oldThread):
173                 if oldThread is not None:
174                         if self._process is not None:
175                                 self._process.terminate()
176                         oldThread.join()
177                 self._id += 1
178                 self._callback(-1.0, False)
179                 try:
180                         self._process = self._runSliceProcess(commandList)
181                 except OSError:
182                         traceback.print_exc()
183                         return
184                 if self._thread != threading.currentThread():
185                         self._process.terminate()
186                 self._callback(0.0, False)
187                 self._sliceLog = []
188                 self._printTimeSeconds = None
189                 self._filamentMM = None
190
191                 line = self._process.stdout.readline()
192                 objectNr = 0
193                 while len(line):
194                         line = line.strip()
195                         if line.startswith('Progress:'):
196                                 line = line.split(':')
197                                 if line[1] == 'process':
198                                         objectNr += 1
199                                 elif line[1] in self._progressSteps:
200                                         progressValue = float(line[2]) / float(line[3])
201                                         progressValue /= len(self._progressSteps)
202                                         progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
203
204                                         progressValue /= self._objCount
205                                         progressValue += 1.0 / self._objCount * objectNr
206                                         try:
207                                                 self._callback(progressValue, False)
208                                         except:
209                                                 pass
210                         elif line.startswith('Print time:'):
211                                 self._printTimeSeconds = int(line.split(':')[1].strip())
212                         elif line.startswith('Filament:'):
213                                 self._filamentMM = int(line.split(':')[1].strip())
214                                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
215                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
216                                         self._filamentMM /= (math.pi * radius * radius)
217                         else:
218                                 self._sliceLog.append(line.strip())
219                         line = self._process.stdout.readline()
220                 for line in self._process.stderr:
221                         self._sliceLog.append(line.strip())
222                 returnCode = self._process.wait()
223                 try:
224                         if returnCode == 0:
225                                 pluginError = profile.runPostProcessingPlugins(self._exportFilename)
226                                 if pluginError is not None:
227                                         print pluginError
228                                         self._sliceLog.append(pluginError)
229                                 self._callback(1.0, True)
230                         else:
231                                 for line in self._sliceLog:
232                                         print line
233                                 self._callback(-1.0, False)
234                 except:
235                         pass
236                 self._process = None
237
238         def _engineSettings(self, extruderCount):
239                 settings = {
240                         'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
241                         'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
242                         'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
243                         'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
244                         'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
245                         'insetCount': int(profile.calculateLineCount()),
246                         'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
247                         'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
248                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
249                         'initialSpeedupLayers': int(4),
250                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
251                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
252                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
253                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
254                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
255                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
256                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
257                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
258                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
259                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
260                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
261                         'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' else -1),
262                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
263                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
264                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
265                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
266                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
267                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
268                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
269                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
270                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
271                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
272                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
273                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
274                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
275
276                         'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
277                         'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
278                         'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
279                         'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
280                         'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
281                         'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
282                         'fixHorrible': 0,
283                 }
284                 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
285                 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness']) / settings['layerThickness'] + 1
286                 if settings['fanFullOnLayerNr'] < 0:
287                         settings['fanFullOnLayerNr'] = 0
288
289                 if profile.getProfileSettingFloat('fill_density') == 0:
290                         settings['sparseInfillLineDistance'] = -1
291                 elif profile.getProfileSettingFloat('fill_density') == 100:
292                         settings['sparseInfillLineDistance'] = settings['extrusionWidth']
293                         #Set the up/down skins height to 10000 if we want a 100% filled object.
294                         # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
295                         settings['downSkinCount'] = 10000
296                         settings['upSkinCount'] = 10000
297                 else:
298                         settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
299                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
300                         settings['skirtDistance'] = 0
301                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
302                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
303                         settings['skirtDistance'] = 0
304                         settings['skirtLineCount'] = 0
305                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
306                         settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
307                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
308                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
309                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
310                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
311                 else:
312                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
313                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
314                         settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
315
316                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
317                         settings['fixHorrible'] |= 0x01
318                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
319                         settings['fixHorrible'] |= 0x02
320                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
321                         settings['fixHorrible'] |= 0x10
322                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
323                         settings['fixHorrible'] |= 0x04
324
325                 if settings['layerThickness'] <= 0:
326                         settings['layerThickness'] = 1000
327                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
328                         settings['gcodeFlavor'] = 1
329                 if profile.getProfileSetting('spiralize') == 'True':
330                         settings['spiralizeMode'] = 1
331                 return settings
332
333         def _runSliceProcess(self, cmdList):
334                 kwargs = {}
335                 if subprocess.mswindows:
336                         su = subprocess.STARTUPINFO()
337                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
338                         su.wShowWindow = subprocess.SW_HIDE
339                         kwargs['startupinfo'] = su
340                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
341                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
342
343         def submitSliceInfoOnline(self):
344                 if profile.getPreference('submit_slice_information') != 'True':
345                         return
346                 if version.isDevVersion():
347                         return
348                 data = {
349                         'processor': platform.processor(),
350                         'machine': platform.machine(),
351                         'platform': platform.platform(),
352                         'profile': profile.getProfileString(),
353                         'preferences': profile.getPreferencesString(),
354                         'modelhash': self._modelHash,
355                         'version': version.getVersion(),
356                 }
357                 try:
358                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
359                         f.read()
360                         f.close()
361                 except:
362                         pass