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