chiark / gitweb /
Use proper start2.gcode when doing dual-extrusion-support-material.
[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                         'fanOnLayerNr': int(profile.getProfileSettingFloat('fan_layer')),
255                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
256                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
257                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
258                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
259                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
260                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
261                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
262                         'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' else -1),
263                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
264                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
265                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
266                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
267                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
268                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
269                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
270                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
271                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
272                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
273                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
274                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
275                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
276
277                         'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
278                         'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
279                         'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
280                         'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
281                         'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
282                         'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
283                         'fixHorrible': 0,
284                 }
285                 if profile.getProfileSettingFloat('fill_density') == 0:
286                         settings['sparseInfillLineDistance'] = -1
287                 elif profile.getProfileSettingFloat('fill_density') == 100:
288                         settings['sparseInfillLineDistance'] = settings['extrusionWidth']
289                         #Set the up/down skins height to 10000 if we want a 100% filled object.
290                         # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
291                         settings['downSkinCount'] = 10000
292                         settings['upSkinCount'] = 10000
293                 else:
294                         settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
295                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
296                         settings['skirtDistance'] = 0
297                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
298                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
299                         settings['skirtDistance'] = 0
300                         settings['skirtLineCount'] = 0
301                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
302                         settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
303                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
304                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
305                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
306                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
307                 else:
308                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
309                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
310                         settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
311
312                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
313                         settings['fixHorrible'] |= 0x01
314                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
315                         settings['fixHorrible'] |= 0x02
316                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
317                         settings['fixHorrible'] |= 0x10
318                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
319                         settings['fixHorrible'] |= 0x04
320
321                 if settings['layerThickness'] <= 0:
322                         settings['layerThickness'] = 1000
323                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
324                         settings['gcodeFlavor'] = 1
325                 return settings
326
327         def _runSliceProcess(self, cmdList):
328                 kwargs = {}
329                 if subprocess.mswindows:
330                         su = subprocess.STARTUPINFO()
331                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
332                         su.wShowWindow = subprocess.SW_HIDE
333                         kwargs['startupinfo'] = su
334                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
335                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
336
337         def submitSliceInfoOnline(self):
338                 if profile.getPreference('submit_slice_information') != 'True':
339                         return
340                 if version.isDevVersion():
341                         return
342                 data = {
343                         'processor': platform.processor(),
344                         'machine': platform.machine(),
345                         'platform': platform.platform(),
346                         'profile': profile.getProfileString(),
347                         'preferences': profile.getPreferencesString(),
348                         'modelhash': self._modelHash,
349                         'version': version.getVersion(),
350                 }
351                 try:
352                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
353                         f.read()
354                         f.close()
355                 except:
356                         pass