chiark / gitweb /
Merge remote-tracking branch 'upstream/master'
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import\r
2 from __future__ import division\r
3 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.\r
4 import __init__\r
5 \r
6 import ConfigParser, os, traceback, math, re, zlib, base64\r
7 \r
8 #########################################################\r
9 ## Default settings when none are found.\r
10 #########################################################\r
11 \r
12 #Single place to store the defaults, so we have a consistent set of default settings.\r
13 profileDefaultSettings = {\r
14         'nozzle_size': '0.4',\r
15         'layer_height': '0.2',\r
16         'wall_thickness': '0.8',\r
17         'solid_layer_thickness': '0.6',\r
18         'fill_density': '20',\r
19         'skirt_line_count': '1',\r
20         'skirt_gap': '3.0',\r
21         'print_speed': '50',\r
22         'print_temperature': '0',\r
23         'support': 'None',\r
24         'filament_diameter': '2.89',\r
25         'filament_density': '1.00',\r
26         'machine_center_x': '100',\r
27         'machine_center_y': '100',\r
28         'retraction_min_travel': '5.0',\r
29         'retraction_speed': '40.0',\r
30         'retraction_amount': '0.0',\r
31         'retraction_extra': '0.0',\r
32         'travel_speed': '150',\r
33         'max_z_speed': '3.0',\r
34         'bottom_layer_speed': '20',\r
35         'cool_min_layer_time': '10',\r
36         'fan_enabled': 'True',\r
37         'fan_layer': '0',\r
38         'fan_speed': '100',\r
39         'model_scale': '1.0',\r
40         'flip_x': 'False',\r
41         'flip_y': 'False',\r
42         'flip_z': 'False',\r
43         'swap_xz': 'False',\r
44         'swap_yz': 'False',\r
45         'model_rotate_base': '0',\r
46         'model_multiply_x': '1',\r
47         'model_multiply_y': '1',\r
48         'extra_base_wall_thickness': '0.0',\r
49         'sequence': 'Loops > Perimeter > Infill',\r
50         'force_first_layer_sequence': 'True',\r
51         'infill_type': 'Line',\r
52         'solid_top': 'True',\r
53         'fill_overlap': '15',\r
54         'support_rate': '50',\r
55         'support_distance': '0.5',\r
56         'support_margin': '3.0',\r
57         'joris': 'False',\r
58         'enable_skin': 'False',\r
59         'enable_raft': 'False',\r
60         'cool_min_feedrate': '5',\r
61         'bridge_speed': '100',\r
62         'bridge_material_amount': '100',\r
63         'raft_margin': '5',\r
64         'raft_base_material_amount': '100',\r
65         'raft_interface_material_amount': '100',\r
66         'bottom_thickness': '0.3',\r
67         \r
68         'add_start_end_gcode': 'True',\r
69         'gcode_extension': 'gcode',\r
70 }\r
71 alterationDefault = {\r
72 #######################################################################################\r
73         'start.gcode': """;Start GCode\r
74 G21        ;metric values\r
75 G90        ;absolute positioning\r
76 \r
77 G28 X0 Y0  ;move X/Y to min endstops\r
78 G28 Z0     ;move Z to min endstops\r
79 \r
80 ; if your prints start too high, try changing the Z0.0 below\r
81 ; to Z1.0 - the number after the Z is the actual, physical\r
82 ; height of the nozzle in mm. This can take some messing around\r
83 ; with to get just right...\r
84 G92 X0 Y0 Z0 E0         ;reset software position to front/left/z=0.0\r
85 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm\r
86 G92 E0                  ;zero the extruded length\r
87 \r
88 G1 F200 E5              ;extrude 5mm of feed stock\r
89 G1 F200 E3.5            ;reverse feed stock by 1.5mm\r
90 G92 E0                  ;zero the extruded length again\r
91 \r
92 ;go to the middle of the platform, and move to Z=0 before starting the print.\r
93 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}\r
94 G1 Z0.0 F{max_z_speed}\r
95 """,\r
96 #######################################################################################\r
97         'end.gcode': """;End GCode\r
98 M104 S0                    ;extruder heat off\r
99 G91                        ;relative positioning\r
100 G1 Z+10 E-5 F{max_z_speed} ;move Z up a bit and retract filament by 5mm\r
101 G28 X0 Y0                  ;move X/Y to min endstops, so the head is out of the way\r
102 M84                        ;steppers off\r
103 G90                        ;absolute positioning\r
104 """,\r
105 #######################################################################################\r
106         'support_start.gcode': '',\r
107         'support_end.gcode': '',\r
108         'cool_start.gcode': '',\r
109         'cool_end.gcode': '',\r
110         'replace.csv': '',\r
111 #######################################################################################\r
112         'nextobject.gcode': """;Move to next object on the platform. clear_z is the minimal z height we need to make sure we do not hit any objects.\r
113 G92 E0\r
114 G1 Z{clear_z} E-5 F{max_z_speed}\r
115 G92 E0\r
116 G1 X{machine_center_x} Y{machine_center_y} F{travel_speed}\r
117 G1 F200 E5.5\r
118 G92 E0\r
119 G1 Z0 F{max_z_speed}\r
120 """,\r
121 #######################################################################################\r
122 }\r
123 preferencesDefaultSettings = {\r
124         'wizardDone': 'False',\r
125         'startMode': 'Simple',\r
126         'lastFile': 'None',\r
127         'machine_width': '205',\r
128         'machine_depth': '205',\r
129         'machine_height': '200',\r
130         'filament_density': '1300',\r
131         'steps_per_e': '0',\r
132         'serial_port': 'AUTO',\r
133         'serial_baud': '250000',\r
134         'slicer': 'Cura (Skeinforge based)',\r
135         'save_profile': 'False',\r
136         'filament_cost_kg': '0',\r
137         'filament_cost_meter': '0',\r
138 }\r
139 \r
140 #########################################################\r
141 ## Profile and preferences functions\r
142 #########################################################\r
143 \r
144 def getDefaultProfilePath():\r
145         return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../current_profile.ini"))\r
146 \r
147 def loadGlobalProfile(filename):\r
148         #Read a configuration file as global config\r
149         global globalProfileParser\r
150         globalProfileParser = ConfigParser.ConfigParser()\r
151         globalProfileParser.read(filename)\r
152 \r
153 def saveGlobalProfile(filename):\r
154         #Save the current profile to an ini file\r
155         globalProfileParser.write(open(filename, 'w'))\r
156 \r
157 def loadGlobalProfileFromString(options):\r
158         global globalProfileParser\r
159         globalProfileParser = ConfigParser.ConfigParser()\r
160         globalProfileParser.add_section('profile')\r
161         globalProfileParser.add_section('alterations')\r
162         options = base64.b64decode(options)\r
163         options = zlib.decompress(options)\r
164         (profileOpts, alt) = options.split('\f', 1)\r
165         for option in profileOpts.split('\b'):\r
166                 (key, value) = option.split('=', 1)\r
167                 globalProfileParser.set('profile', key, value)\r
168         for option in alt.split('\b'):\r
169                 (key, value) = option.split('=', 1)\r
170                 globalProfileParser.set('alterations', key, value)\r
171 \r
172 def getGlobalProfileString():\r
173         global globalProfileParser\r
174         if not globals().has_key('globalProfileParser'):\r
175                 loadGlobalProfile(getDefaultProfilePath())\r
176         \r
177         p = []\r
178         alt = []\r
179         for key in globalProfileParser.options('profile'):\r
180                 p.append(key + "=" + globalProfileParser.get('profile', key))\r
181         for key in globalProfileParser.options('alterations'):\r
182                 alt.append(key + "=" + globalProfileParser.get('alterations', key))\r
183         ret = '\b'.join(p) + '\f' + '\b'.join(alt)\r
184         ret = base64.b64encode(zlib.compress(ret, 9))\r
185         return ret\r
186 \r
187 def getProfileSetting(name):\r
188         #Check if we have a configuration file loaded, else load the default.\r
189         if not globals().has_key('globalProfileParser'):\r
190                 loadGlobalProfile(getDefaultProfilePath())\r
191         if not globalProfileParser.has_option('profile', name):\r
192                 if name in profileDefaultSettings:\r
193                         default = profileDefaultSettings[name]\r
194                 else:\r
195                         print "Missing default setting for: '" + name + "'"\r
196                         profileDefaultSettings[name] = ''\r
197                         default = ''\r
198                 if not globalProfileParser.has_section('profile'):\r
199                         globalProfileParser.add_section('profile')\r
200                 globalProfileParser.set('profile', name, str(default))\r
201                 #print name + " not found in profile, so using default: " + str(default)\r
202                 return default\r
203         return globalProfileParser.get('profile', name)\r
204 \r
205 def getProfileSettingFloat(name):\r
206         try:\r
207                 return float(eval(getProfileSetting(name), {}, {}))\r
208         except (ValueError, SyntaxError):\r
209                 return 0.0\r
210 \r
211 def putProfileSetting(name, value):\r
212         #Check if we have a configuration file loaded, else load the default.\r
213         if not globals().has_key('globalProfileParser'):\r
214                 loadGlobalProfile(getDefaultProfilePath())\r
215         if not globalProfileParser.has_section('profile'):\r
216                 globalProfileParser.add_section('profile')\r
217         globalProfileParser.set('profile', name, str(value))\r
218 \r
219 global globalPreferenceParser\r
220 globalPreferenceParser = None\r
221 \r
222 def getPreferencePath():\r
223         return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../preferences.ini"))\r
224 \r
225 def getPreference(name):\r
226         global globalPreferenceParser\r
227         if globalPreferenceParser == None:\r
228                 globalPreferenceParser = ConfigParser.ConfigParser()\r
229                 globalPreferenceParser.read(getPreferencePath())\r
230         if not globalPreferenceParser.has_option('preference', name):\r
231                 if name in preferencesDefaultSettings:\r
232                         default = preferencesDefaultSettings[name]\r
233                 else:\r
234                         print "Missing default setting for: '" + name + "'"\r
235                         preferencesDefaultSettings[name] = ''\r
236                         default = ''\r
237                 if not globalPreferenceParser.has_section('preference'):\r
238                         globalPreferenceParser.add_section('preference')\r
239                 globalPreferenceParser.set('preference', name, str(default))\r
240                 #print name + " not found in preferences, so using default: " + str(default)\r
241                 return default\r
242         return unicode(globalPreferenceParser.get('preference', name), "utf-8")\r
243 \r
244 def putPreference(name, value):\r
245         #Check if we have a configuration file loaded, else load the default.\r
246         global globalPreferenceParser\r
247         if globalPreferenceParser == None:\r
248                 globalPreferenceParser = ConfigParser.ConfigParser()\r
249                 globalPreferenceParser.read(getPreferencePath())\r
250         if not globalPreferenceParser.has_section('preference'):\r
251                 globalPreferenceParser.add_section('preference')\r
252         globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))\r
253         globalPreferenceParser.write(open(getPreferencePath(), 'w'))\r
254 \r
255 #########################################################\r
256 ## Utility functions to calculate common profile values\r
257 #########################################################\r
258 def calculateEdgeWidth():\r
259         wallThickness = getProfileSettingFloat('wall_thickness')\r
260         nozzleSize = getProfileSettingFloat('nozzle_size')\r
261         \r
262         if wallThickness < nozzleSize:\r
263                 return wallThickness\r
264 \r
265         lineCount = int(wallThickness / nozzleSize)\r
266         lineWidth = wallThickness / lineCount\r
267         lineWidthAlt = wallThickness / (lineCount + 1)\r
268         if lineWidth > nozzleSize * 1.5:\r
269                 return lineWidthAlt\r
270         return lineWidth\r
271 \r
272 def calculateLineCount():\r
273         wallThickness = getProfileSettingFloat('wall_thickness')\r
274         nozzleSize = getProfileSettingFloat('nozzle_size')\r
275         \r
276         if wallThickness < nozzleSize:\r
277                 return 1\r
278 \r
279         lineCount = int(wallThickness / nozzleSize + 0.0001)\r
280         lineWidth = wallThickness / lineCount\r
281         lineWidthAlt = wallThickness / (lineCount + 1)\r
282         if lineWidth > nozzleSize * 1.5:\r
283                 return lineCount + 1\r
284         return lineCount\r
285 \r
286 def calculateSolidLayerCount():\r
287         layerHeight = getProfileSettingFloat('layer_height')\r
288         solidThickness = getProfileSettingFloat('solid_layer_thickness')\r
289         return int(math.ceil(solidThickness / layerHeight - 0.0001))\r
290 \r
291 #########################################################\r
292 ## Alteration file functions\r
293 #########################################################\r
294 def replaceTagMatch(m):\r
295         tag = m.group(0)[1:-1]\r
296         if tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:\r
297                 return str(getProfileSettingFloat(tag) * 60)\r
298         return str(getProfileSettingFloat(tag))\r
299 \r
300 ### Get aleration raw contents. (Used internally in Cura)\r
301 def getAlterationFile(filename):\r
302         #Check if we have a configuration file loaded, else load the default.\r
303         if not globals().has_key('globalProfileParser'):\r
304                 loadGlobalProfile(getDefaultProfilePath())\r
305         \r
306         if not globalProfileParser.has_option('alterations', filename):\r
307                 if filename in alterationDefault:\r
308                         default = alterationDefault[filename]\r
309                 else:\r
310                         print "Missing default alteration for: '" + filename + "'"\r
311                         alterationDefault[filename] = ''\r
312                         default = ''\r
313                 if not globalProfileParser.has_section('alterations'):\r
314                         globalProfileParser.add_section('alterations')\r
315                 #print "Using default for: %s" % (filename)\r
316                 globalProfileParser.set('alterations', filename, default)\r
317         return unicode(globalProfileParser.get('alterations', filename), "utf-8")\r
318 \r
319 def setAlterationFile(filename, value):\r
320         #Check if we have a configuration file loaded, else load the default.\r
321         if not globals().has_key('globalProfileParser'):\r
322                 loadGlobalProfile(getDefaultProfilePath())\r
323         if not globalProfileParser.has_section('alterations'):\r
324                 globalProfileParser.add_section('alterations')\r
325         globalProfileParser.set('alterations', filename, value.encode("utf-8"))\r
326         saveGlobalProfile(getDefaultProfilePath())\r
327 \r
328 ### Get the alteration file for output. (Used by Skeinforge)\r
329 def getAlterationFileContents(filename):\r
330         prefix = ''\r
331         alterationContents = getAlterationFile(filename)\r
332         if filename == 'start.gcode':\r
333                 #For the start code, hack the temperature and the steps per E value into it. So the temperature is reached before the start code extrusion.\r
334                 #We also set our steps per E here, if configured.\r
335                 eSteps = float(getPreference('steps_per_e'))\r
336                 if eSteps > 0:\r
337                         prefix += 'M92 E%f\n' % (eSteps)\r
338                 temp = getProfileSettingFloat('print_temperature')\r
339                 if temp > 0 and not '{print_temperature}' in alterationContents:\r
340                         prefix += 'M109 S%f\n' % (temp)\r
341         elif filename == 'replace.csv':\r
342                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.\r
343                 prefix = 'M101\nM103\n'\r
344         \r
345         return prefix + re.sub("\{[^\}]*\}", replaceTagMatch, alterationContents)\r
346 \r