chiark / gitweb /
Make sure the print fits on the bed with dual-extrusion-support.
[cura.git] / Cura / util / sliceEngine.py
index 4f96171f1de5414035775fcf7f3faa8d437f5cd0..87c539fec2351a9a1f2d3760d166d0b16f40f47c 100644 (file)
@@ -1,3 +1,4 @@
+__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
 import subprocess
 import time
 import math
@@ -8,17 +9,25 @@ import threading
 import traceback
 import platform
 import sys
+import urllib
+import urllib2
+import hashlib
 
 from Cura.util import profile
+from Cura.util import version
 
 def getEngineFilename():
        if platform.system() == 'Windows':
                if os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
                        return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
-               return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'SteamEngine.exe'))
+               return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
        if hasattr(sys, 'frozen'):
-               return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'SteamEngine'))
-       return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'SteamEngine'))
+               return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
+       if os.path.isfile('/usr/bin/CuraEngine'):
+               return '/usr/bin/CuraEngine'
+       if os.path.isfile('/usr/local/bin/CuraEngine'):
+               return '/usr/local/bin/CuraEngine'
+       return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
 
 def getTempFilename():
        warnings.simplefilter('ignore')
@@ -37,7 +46,9 @@ class Slicer(object):
                self._objCount = 0
                self._sliceLog = []
                self._printTimeSeconds = None
-               self._filamentMM = None
+               self._filamentMM = [0.0, 0.0]
+               self._modelHash = None
+               self._id = 0
 
        def cleanup(self):
                self.abortSlicer()
@@ -57,6 +68,11 @@ class Slicer(object):
                        except:
                                pass
                        self._thread.join()
+               self._thread = None
+
+       def wait(self):
+               if self._thread is not None:
+                       self._thread.join()
 
        def getGCodeFilename(self):
                return self._exportFilename
@@ -64,43 +80,72 @@ class Slicer(object):
        def getSliceLog(self):
                return self._sliceLog
 
-       def getFilamentWeight(self):
+       def getID(self):
+               return self._id
+
+       def getFilamentWeight(self, e=0):
                #Calculates the weight of the filament in kg
                radius = float(profile.getProfileSetting('filament_diameter')) / 2
-               volumeM3 = (self._filamentMM * (math.pi * radius * radius)) / (1000*1000*1000)
+               volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
                return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
 
-       def getFilamentCost(self):
+       def getFilamentCost(self, e=0):
                cost_kg = profile.getPreferenceFloat('filament_cost_kg')
                cost_meter = profile.getPreferenceFloat('filament_cost_meter')
                if cost_kg > 0.0 and cost_meter > 0.0:
-                       return "%.2f / %.2f" % (self.getFilamentWeight() * cost_kg, self._filamentMM / 1000.0 * cost_meter)
+                       return "%.2f / %.2f" % (self.getFilamentWeight(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
                elif cost_kg > 0.0:
-                       return "%.2f" % (self.getFilamentWeight() * cost_kg)
+                       return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
                elif cost_meter > 0.0:
-                       return "%.2f" % (self._filamentMM / 1000.0 * cost_meter)
+                       return "%.2f" % (self._filamentMM[e] / 1000.0 * cost_meter)
                return None
 
        def getPrintTime(self):
-               return '%02d:%02d' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
+               if int(self._printTimeSeconds / 60 / 60) < 1:
+                       return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
+               if int(self._printTimeSeconds / 60 / 60) == 1:
+                       return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
+               return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
 
-       def getFilamentAmount(self):
-               return '%0.2fm %0.0fgram' % (float(self._filamentMM) / 1000.0, self.getFilamentWeight() * 1000.0)
+       def getFilamentAmount(self, e=0):
+               if self._filamentMM[e] == 0.0:
+                       return None
+               return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
 
        def runSlicer(self, scene):
-               self.abortSlicer()
-               self._callback(-1.0, False)
+               extruderCount = 1
+               for obj in scene.objects():
+                       if scene.checkPlatform(obj):
+                               extruderCount = max(extruderCount, len(obj._meshList))
+
+               extruderCount = max(extruderCount, profile.minimalExtruderCount())
 
                commandList = [getEngineFilename(), '-vv']
-               for k, v in self._engineSettings().iteritems():
+               for k, v in self._engineSettings(extruderCount).iteritems():
                        commandList += ['-s', '%s=%s' % (k, str(v))]
                commandList += ['-o', self._exportFilename]
                commandList += ['-b', self._binaryStorageFilename]
                self._objCount = 0
                with open(self._binaryStorageFilename, "wb") as f:
+                       hash = hashlib.sha512()
                        order = scene.printOrder()
                        if order is None:
-                               pos = numpy.array([profile.getPreferenceFloat('machine_width') * 1000 / 2, profile.getPreferenceFloat('machine_depth') * 1000 / 2])
+                               pos = numpy.array(profile.getMachineCenterCoords()) * 1000
+                               objMin = None
+                               objMax = None
+                               for obj in scene.objects():
+                                       if scene.checkPlatform(obj):
+                                               oMin = obj.getMinimum()[0:2] + obj.getPosition()
+                                               oMax = obj.getMaximum()[0:2] + obj.getPosition()
+                                               if objMin is None:
+                                                       objMin = oMin
+                                                       objMax = oMax
+                                               else:
+                                                       objMin[0] = min(oMin[0], objMin[0])
+                                                       objMin[1] = min(oMin[1], objMin[1])
+                                                       objMax[0] = max(oMax[0], objMax[0])
+                                                       objMax[1] = max(oMax[1], objMax[1])
+                               pos += (objMin + objMax) / 2.0 * 1000
                                commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
 
                                vertexTotal = 0
@@ -117,6 +162,7 @@ class Slicer(object):
                                                        vertexes -= obj._drawOffset
                                                        vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
                                                        f.write(vertexes.tostring())
+                                                       hash.update(mesh.vertexes.tostring())
 
                                commandList += ['#']
                                self._objCount = 1
@@ -125,27 +171,39 @@ class Slicer(object):
                                        obj = scene.objects()[n]
                                        for mesh in obj._meshList:
                                                f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
-                                               f.write(mesh.vertexes.tostring())
+                                               s = mesh.vertexes.tostring()
+                                               f.write(s)
+                                               hash.update(s)
                                        pos = obj.getPosition() * 1000
-                                       pos += numpy.array([profile.getPreferenceFloat('machine_width') * 1000 / 2, profile.getPreferenceFloat('machine_depth') * 1000 / 2])
+                                       pos += numpy.array(profile.getMachineCenterCoords()) * 1000
                                        commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
                                        commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
                                        commandList += ['#' * len(obj._meshList)]
                                        self._objCount += 1
+                       self._modelHash = hash.hexdigest()
                if self._objCount > 0:
-                       try:
-                               self._process = self._runSliceProcess(commandList)
-                               self._thread = threading.Thread(target=self._watchProcess)
-                               self._thread.daemon = True
-                               self._thread.start()
-                       except OSError:
-                               traceback.print_exc()
-
-       def _watchProcess(self):
+                       self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
+                       self._thread.daemon = True
+                       self._thread.start()
+
+       def _watchProcess(self, commandList, oldThread):
+               if oldThread is not None:
+                       if self._process is not None:
+                               self._process.terminate()
+                       oldThread.join()
+               self._id += 1
+               self._callback(-1.0, False)
+               try:
+                       self._process = self._runSliceProcess(commandList)
+               except OSError:
+                       traceback.print_exc()
+                       return
+               if self._thread != threading.currentThread():
+                       self._process.terminate()
                self._callback(0.0, False)
                self._sliceLog = []
                self._printTimeSeconds = None
-               self._filamentMM = None
+               self._filamentMM = [0.0, 0.0]
 
                line = self._process.stdout.readline()
                objectNr = 0
@@ -169,7 +227,15 @@ class Slicer(object):
                        elif line.startswith('Print time:'):
                                self._printTimeSeconds = int(line.split(':')[1].strip())
                        elif line.startswith('Filament:'):
-                               self._filamentMM = int(line.split(':')[1].strip())
+                               self._filamentMM[0] = int(line.split(':')[1].strip())
+                               if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
+                                       radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
+                                       self._filamentMM[0] /= (math.pi * radius * radius)
+                       elif line.startswith('Filament2:'):
+                               self._filamentMM[1] = int(line.split(':')[1].strip())
+                               if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
+                                       radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
+                                       self._filamentMM[1] /= (math.pi * radius * radius)
                        else:
                                self._sliceLog.append(line.strip())
                        line = self._process.stdout.readline()
@@ -178,15 +244,20 @@ class Slicer(object):
                returnCode = self._process.wait()
                try:
                        if returnCode == 0:
-                               profile.runPostProcessingPlugins(self._exportFilename)
+                               pluginError = profile.runPostProcessingPlugins(self._exportFilename)
+                               if pluginError is not None:
+                                       print pluginError
+                                       self._sliceLog.append(pluginError)
                                self._callback(1.0, True)
                        else:
+                               for line in self._sliceLog:
+                                       print line
                                self._callback(-1.0, False)
                except:
                        pass
                self._process = None
 
-       def _engineSettings(self):
+       def _engineSettings(self, extruderCount):
                settings = {
                        'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
                        'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
@@ -196,32 +267,93 @@ class Slicer(object):
                        'insetCount': int(profile.calculateLineCount()),
                        'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
                        'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
-                       'sparseInfillLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density')) if profile.getProfileSettingFloat('fill_density') > 0 else 9999999999,
+                       'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
                        'initialSpeedupLayers': int(4),
                        'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
                        'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
+                       'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
                        'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
-                       'fanOnLayerNr': int(profile.getProfileSettingFloat('fan_layer')),
+                       'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
+                       'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
                        'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
                        'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
-                       'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000),
+                       'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
+                       'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
+                       'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
+                       '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),
+                       'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
                        'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
+                       'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
+                       'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
+                       'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
+                       'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
+                       'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
                        'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
                        'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
                        'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
                        'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
-                       'startCode': profile.getAlterationFileContents('start.gcode'),
-                       'endCode': profile.getAlterationFileContents('end.gcode'),
+                       'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
+                       'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
+
+                       'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
+                       'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
+                       'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
+                       'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
+                       'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
+                       'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
+                       'fixHorrible': 0,
                }
+               fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
+               settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
+               if settings['fanFullOnLayerNr'] < 0:
+                       settings['fanFullOnLayerNr'] = 0
+
+               if profile.getProfileSettingFloat('fill_density') == 0:
+                       settings['sparseInfillLineDistance'] = -1
+               elif profile.getProfileSettingFloat('fill_density') == 100:
+                       settings['sparseInfillLineDistance'] = settings['extrusionWidth']
+                       #Set the up/down skins height to 10000 if we want a 100% filled object.
+                       # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
+                       settings['downSkinCount'] = 10000
+                       settings['upSkinCount'] = 10000
+               else:
+                       settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
                if profile.getProfileSetting('platform_adhesion') == 'Brim':
-                       settings['skirtDistance'] = 0.0
+                       settings['skirtDistance'] = 0
                        settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
                elif profile.getProfileSetting('platform_adhesion') == 'Raft':
                        settings['skirtDistance'] = 0
                        settings['skirtLineCount'] = 0
+                       settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
+                       settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
+                       settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
+                       settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
+                       settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
+                       settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
                else:
                        settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
                        settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
+                       settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
+
+               if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
+                       settings['fixHorrible'] |= 0x01
+               if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
+                       settings['fixHorrible'] |= 0x02
+               if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
+                       settings['fixHorrible'] |= 0x10
+               if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
+                       settings['fixHorrible'] |= 0x04
+
+               if settings['layerThickness'] <= 0:
+                       settings['layerThickness'] = 1000
+               if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
+                       settings['gcodeFlavor'] = 1
+               if profile.getProfileSetting('spiralize') == 'True':
+                       settings['spiralizeMode'] = 1
+               if profile.getProfileSetting('wipe_tower') == 'True':
+                       settings['enableWipeTower'] = 1
+               if profile.getProfileSetting('ooze_shield') == 'True':
+                       settings['enableOozeShield'] = 1
                return settings
 
        def _runSliceProcess(self, cmdList):
@@ -233,3 +365,24 @@ class Slicer(object):
                        kwargs['startupinfo'] = su
                        kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
                return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
+
+       def submitSliceInfoOnline(self):
+               if profile.getPreference('submit_slice_information') != 'True':
+                       return
+               if version.isDevVersion():
+                       return
+               data = {
+                       'processor': platform.processor(),
+                       'machine': platform.machine(),
+                       'platform': platform.platform(),
+                       'profile': profile.getProfileString(),
+                       'preferences': profile.getPreferencesString(),
+                       'modelhash': self._modelHash,
+                       'version': version.getVersion(),
+               }
+               try:
+                       f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
+                       f.read()
+                       f.close()
+               except:
+                       pass