chiark / gitweb /
9123c81bbd22f09d28beb1e3be912816d29a8993
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import
2 from __future__ import division
3
4 import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat, types
5 import cPickle as pickle
6 if sys.version_info[0] < 3:
7         import ConfigParser
8 else:
9         import configparser as ConfigParser
10
11 from Cura.util import resources
12 from Cura.util import version
13 from Cura.util import validators
14
15 settingsDictionary = {}
16 settingsList = []
17 class setting(object):
18         def __init__(self, name, default, type, category, subcategory):
19                 self._name = name
20                 self._label = name
21                 self._tooltip = ''
22                 self._default = unicode(default)
23                 self._value = self._default
24                 self._type = type
25                 self._category = category
26                 self._subcategory = subcategory
27                 self._validators = []
28
29                 if type is types.FloatType:
30                         validators.validFloat(self)
31                 elif type is types.IntType:
32                         validators.validInt(self)
33
34                 global settingsDictionary
35                 settingsDictionary[name] = self
36                 global settingsList
37                 settingsList.append(self)
38
39         def setLabel(self, label, tooltip = ''):
40                 self._label = label
41                 self._tooltip = tooltip
42                 return self
43
44         def setRange(self, minValue = None, maxValue = None):
45                 if len(self._validators) < 1:
46                         return
47                 self._validators[0].minValue = minValue
48                 self._validators[0].maxValue = maxValue
49                 return self
50
51         def getLabel(self):
52                 return self._label
53
54         def getTooltip(self):
55                 return self._tooltip
56
57         def isPreference(self):
58                 return self._category == 'preference'
59
60         def isAlteration(self):
61                 return self._category == 'alteration'
62
63         def isProfile(self):
64                 return not self.isAlteration() and not self.isPreference()
65
66         def getName(self):
67                 return self._name
68
69         def getType(self):
70                 return self._type
71
72         def getValue(self):
73                 return self._value
74
75         def getDefault(self):
76                 return self._default
77
78         def setValue(self, value):
79                 self._value = unicode(value)
80
81         def validate(self):
82                 result = validators.SUCCESS
83                 msgs = []
84                 for validator in self._validators:
85                         res, err = validator.validate()
86                         if res == validators.ERROR:
87                                 result = res
88                         elif res == validators.WARNING and result != validators.ERROR:
89                                 result = res
90                         if res != validators.SUCCESS:
91                                 msgs.append(err)
92                 return result, '\n'.join(msgs)
93
94 #########################################################
95 ## Settings
96 #########################################################
97 setting('layer_height',              0.2, float, 'basic',    'Quality').setRange(0.0001).setLabel('Layer height (mm)', 'Layer height in millimeters.\n0.2 is a good value for quick prints.\n0.1 gives high quality prints.\nDepending on your printer you can go as low as 0.02mm')
98 setting('wall_thickness',            0.8, float, 'basic',    'Quality').setRange(0.0001).setLabel('Wall thickness (mm)', 'Thickness of the walls.\nThis is used in combination with the nozzle size to define the number\nof perimeter lines and the thickness of those perimeter lines.')
99 setting('retraction_enable',       False, bool,  'basic',    'Quality').setLabel('Enable retraction', 'Retract the filament when the nozzle is moving over a none-printed area. Details about the retraction can be configured in the advanced tab.')
100 setting('solid_layer_thickness',     0.6, float, 'basic',    'Fill').setRange(0).setLabel('Bottom/Top thickness (mm)', 'This controls the thickness of the bottom and top layers, the amount of solid layers put down is calculated by the layer thickness and this value.\nHaving this value a multiply of the layer thickness makes sense. And keep it near your wall thickness to make an evenly strong part.')
101 setting('fill_density',               20, float, 'basic',    'Fill').setRange(0, 100).setLabel('Fill Density (%)', 'This controls how densely filled the insides of your print will be. For a solid part use 100%, for an empty part use 0%. A value around 20% is usually enough.\nThis won\'t effect the outside of the print and only adjusts how strong the part becomes.')
102 setting('nozzle_size',               0.4, float, 'advanced', 'Machine').setRange(0.1,10).setLabel('Nozzle size (mm)', 'The nozzle size is very important, this is used to calculate the line width of the infill, and used to calculate the amount of outside wall lines and thickness for the wall thickness you entered in the print settings.')
103 setting('skirt_line_count',            1, int,   'advanced', 'Skirt').setRange(0).setLabel('Line count', 'The skirt is a line drawn around the object at the first layer. This helps to prime your extruder, and to see if the object fits on your platform.\nSetting this to 0 will disable the skirt. Multiple skirt lines can help priming your extruder better for small objects.')
104 setting('skirt_gap',                 3.0, float, 'advanced', 'Skirt').setRange(0).setLabel('Start distance (mm)', 'The distance between the skirt and the first layer.\nThis is the minimal distance, multiple skirt lines will be put outwards from this distance.')
105 setting('print_speed',                50, float, 'basic',    'Speed & Temperature').setRange(1).setLabel('Print speed (mm/s)', 'Speed at which printing happens. A well adjusted Ultimaker can reach 150mm/s, but for good quality prints you want to print slower. Printing speed depends on a lot of factors. So you will be experimenting with optimal settings for this.')
106 setting('print_temperature',         220, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('Printing temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
107 setting('print_temperature2',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('2nd nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
108 setting('print_temperature3',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('3th nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
109 setting('print_temperature4',          0, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('4th nozzle temperature (C)', 'Temperature used for printing. Set at 0 to pre-heat yourself.\nFor PLA a value of 210C is usually used.\nFor ABS a value of 230C or higher is required.')
110 setting('print_bed_temperature',      70, int,   'basic',    'Speed & Temperature').setRange(0,340).setLabel('Bed temperature (C)', 'Temperature used for the heated printer bed. Set at 0 to pre-heat yourself.')
111 setting('support',                'None', ['None', 'Touching buildplate', 'Everywhere'], 'Basic', 'Support structure').setLabel('Support type', 'Type of support structure build.\n"Exterior only" is the most commonly used support setting.\n\nNone does not do any support.\nTouching buildplate only creates support where the support structure will touch the build platform.\nEverywhere creates support even on top of parts of the model.')
112 setting('enable_raft',             False, bool,  'basic',   'Support').setLabel('Enable raft', 'A raft is a few layers of lines below the bottom of the object. It prevents warping. Full raft settings can be found in the expert settings.\nFor PLA this is usually not required. But if you print with ABS it is almost required.')
113 setting('support_dual_extrusion',  False, bool, 'basic', 'Support').setLabel('Support dual extrusion', 'Print the support material with the 2nd extruder in a dual extrusion setup. The primary extruder will be used for normal material, while the second extruder is used to print support material.')
114 setting('filament_diameter',        2.89, float, 'basic',    'Filament').setRange(1).setLabel('Diameter (mm)', 'Diameter of your filament, as accurately as possible.\nIf you cannot measure this value you will have to calibrate it, a higher number means less extrusion, a smaller number generates more extrusion.')
115 setting('filament_diameter2',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter2 (mm)', 'Diameter of your filament for the 2nd nozzle. Use 0 to use the same diameter as for nozzle 1.')
116 setting('filament_diameter3',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter3 (mm)', 'Diameter of your filament for the 3th nozzle. Use 0 to use the same diameter as for nozzle 1.')
117 setting('filament_diameter4',          0, float, 'basic',    'Filament').setRange(0).setLabel('Diameter4 (mm)', 'Diameter of your filament for the 4th nozzle. Use 0 to use the same diameter as for nozzle 1.')
118 setting('filament_density',         1.00, float, 'basic',    'Filament').setRange(0.5,1.5).setLabel('Packing Density', 'Packing density of your filament. This should be 1.00 for PLA and 0.85 for ABS')
119 setting('retraction_min_travel',     5.0, float, 'advanced', 'Retraction').setRange(0).setLabel('Minimum travel (mm)', 'Minimum amount of travel needed for a retraction to happen at all. To make sure you do not get a lot of retractions in a small area')
120 setting('retraction_speed',         40.0, float, 'advanced', 'Retraction').setRange(0.1).setLabel('Speed (mm/s)', 'Speed at which the filament is retracted, a higher retraction speed works better. But a very high retraction speed can lead to filament grinding.')
121 setting('retraction_amount',         4.5, float, 'advanced', 'Retraction').setRange(0).setLabel('Distance (mm)', 'Amount of retraction, set at 0 for no retraction at all. A value of 2.0mm seems to generate good results.')
122 setting('retraction_extra',          0.0, float, 'advanced', 'Retraction').setRange(0).setLabel('Extra length on start (mm)', 'Extra extrusion amount when restarting after a retraction, to better "Prime" your extruder after retraction.')
123 setting('bottom_thickness',          0.3, float, 'advanced', 'Quality').setRange(0).setLabel('Initial layer thickness (mm)', 'Layer thickness of the bottom layer. A thicker bottom layer makes sticking to the bed easier. Set to 0.0 to have the bottom layer thickness the same as the other layers.')
124 setting('object_sink',               0.0, float, 'advanced', 'Quality').setLabel('Cut off object bottom (mm)', 'Sinks the object into the platform, this can be used for objects that do not have a flat bottom and thus create a too small first layer.')
125 setting('enable_skin',             False, bool,  'advanced', 'Quality').setLabel('Duplicate outlines', 'Skin prints the outer lines of the prints twice, each time with half the thickness. This gives the illusion of a higher print quality.')
126 setting('retract_on_jumps_only',    True, bool,  'expert',   'Retraction').setLabel('Retract on jumps only', 'Only retract when we are making a move that is over a hole in the model, else retract on every move. This effects print quality in different ways.')
127 setting('travel_speed',            150.0, float, 'advanced', 'Speed').setRange(0.1).setLabel('Travel speed (mm/s)', 'Speed at which travel moves are done, a high quality build Ultimaker can reach speeds of 250mm/s. But some machines might miss steps then.')
128 setting('max_z_speed',               3.0, float, 'expert',   'Speed').setRange(0.1).setLabel('Max Z speed (mm/s)', 'Speed at which Z moves are done. When you Z axis is properly lubricated you can increase this for less Z blob.')
129 setting('bottom_layer_speed',         20, float, 'advanced', 'Speed').setRange(0.1).setLabel('Bottom layer speed (mm/s)', 'Print speed for the bottom layer, you want to print the first layer slower so it sticks better to the printer bed.')
130 setting('cool_min_layer_time',         5, float, 'advanced', 'Cool').setRange(0).setLabel('Minimal layer time (sec)', 'Minimum time spend in a layer, gives the layer time to cool down before the next layer is put on top. If the layer will be placed down too fast the printer will slow down to make sure it has spend at least this amount of seconds printing this layer.')
131 setting('fan_enabled',              True, bool,  'advanced', 'Cool').setLabel('Enable cooling fan', 'Enable the cooling fan during the print. The extra cooling from the cooling fan is essensial during faster prints.')
132 setting('fan_layer',                   1, int,   'expert',   'Cool').setRange(0).setLabel('Fan on layer number', 'The layer at which the fan is turned on. The first layer is layer 0. The first layer can stick better if you turn on the fan on, on the 2nd layer.')
133 setting('fan_speed',                 100, int,   'expert',   'Cool').setRange(0,100).setLabel('Fan speed min (%)', 'When the fan is turned on, it is enabled at this speed setting. If cool slows down the layer, the fan is adjusted between the min and max speed. Minimal fan speed is used if the layer is not slowed down due to cooling.')
134 setting('fan_speed_max',             100, int,   'expert',   'Cool').setRange(0,100).setLabel('Fan speed max (%)', 'When the fan is turned on, it is enabled at this speed setting. If cool slows down the layer, the fan is adjusted between the min and max speed. Maximal fan speed is used if the layer is slowed down due to cooling by more then 200%.')
135 setting('cool_min_feedrate',          10, float, 'expert',   'Cool').setRange(0).setLabel('Minimum feedrate (mm/s)', 'The minimal layer time can cause the print to slow down so much it starts to ooze. The minimal feedrate protects against this. Even if a print gets slown down it will never be slower then this minimal feedrate.')
136 setting('extra_base_wall_thickness', 0.0, float, 'expert',   'Accuracy').setRange(0).setLabel('Extra Wall thickness for bottom/top (mm)', 'Additional wall thickness of the bottom and top layers.')
137 setting('sequence', 'Loops > Perimeter > Infill', ['Loops > Perimeter > Infill', 'Loops > Infill > Perimeter', 'Infill > Loops > Perimeter', 'Infill > Perimeter > Loops', 'Perimeter > Infill > Loops', 'Perimeter > Loops > Infill'], 'expert', 'Sequence')
138 setting('force_first_layer_sequence', True, bool, 'expert', 'Sequence').setLabel('Force first layer sequence', 'This setting forces the order of the first layer to be \'Perimeter > Loops > Infill\'')
139 setting('infill_type', 'Line', ['Line', 'Grid Circular', 'Grid Hexagonal', 'Grid Rectangular'], 'expert', 'Infill').setLabel('Infill pattern', 'Pattern of the none-solid infill. Line is default, but grids can provide a strong print.')
140 setting('solid_top', True, bool, 'expert', 'Infill').setLabel('Solid infill top', 'Create a solid top surface, if set to false the top is filled with the fill percentage. Useful for cups/vases.')
141 setting('fill_overlap', 15, int, 'expert', 'Infill').setRange(0,100).setLabel('Infill overlap (%)', 'Amount of overlap between the infill and the walls. There is a slight overlap with the walls and the infill so the walls connect firmly to the infill.')
142 setting('support_rate', 50, int, 'expert', 'Support').setRange(0,100).setLabel('Material amount (%)', 'Amount of material used for support, less material gives a weaker support structure which is easier to remove.')
143 setting('support_distance',  0.5, float, 'expert', 'Support').setRange(0).setLabel('Distance from object (mm)', 'Distance between the support structure and the object. Empty gap in which no support structure is printed.')
144 setting('joris', False, bool, 'expert', 'Joris').setLabel('Spiralize the outer contour', '[Joris] is a code name for smoothing out the Z move of the outer edge. This will create a steady Z increase over the whole print. It is intended to be used with a single walled wall thickness to make cups/vases.')
145 setting('bridge_speed', 100, int, 'expert', 'Bridge').setRange(0,100).setLabel('Bridge speed (%)', 'Speed at which layers with bridges are printed, compared to normal printing speed.')
146 setting('raft_margin', 5, float, 'expert', 'Raft').setRange(0).setLabel('Extra margin (mm)', 'If the raft is enabled, this is the extra raft area around the object which is also rafted. Increasing this margin will create a stronger raft.')
147 setting('raft_base_material_amount', 100, int, 'expert', 'Raft').setRange(0,100).setLabel('Base material amount (%)', 'The base layer is the first layer put down as a raft. This layer has thick strong lines and is put firmly on the bed to prevent warping. This setting adjust the amount of material used for the base layer.')
148 setting('raft_interface_material_amount', 100, int, 'expert', 'Raft').setRange(0,100).setLabel('Interface material amount (%)', 'The interface layer is a weak thin layer between the base layer and the printed object. It is designed to has little material to make it easy to break the base off the printed object. This setting adjusts the amount of material used for the interface layer.')
149 setting('hop_on_move', False, bool, 'expert', 'Hop').setLabel('Enable hop on move', 'When moving from print position to print position, raise the printer head 0.2mm so it does not knock off the print (experimental).')
150
151 setting('model_matrix', '1,0,0,0,1,0,0,0,1', str, 'hidden', 'hidden')
152 setting('plugin_config', '', str, 'hidden', 'hidden')
153 setting('object_center_x', -1, float, 'hidden', 'hidden')
154 setting('object_center_y', -1, float, 'hidden', 'hidden')
155
156 setting('start.gcode', """;Sliced {filename} at: {day} {date} {time}
157 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
158 ;Print time: {print_time}
159 ;Filament used: {filament_amount}m {filament_weight}g
160 ;Filament cost: {filament_cost}
161 G21        ;metric values
162 G90        ;absolute positioning
163 M107       ;start with the fan off
164
165 G28 X0 Y0  ;move X/Y to min endstops
166 G28 Z0     ;move Z to min endstops
167
168 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
169
170 G92 E0                  ;zero the extruded length
171 G1 F200 E3              ;extrude 3mm of feed stock
172 G92 E0                  ;zero the extruded length again
173 G1 F{travel_speed}
174 M117 Printing...
175 """, str, 'alteration', 'alteration')
176 #######################################################################################
177 setting('end.gcode', """;End GCode
178 M104 S0                     ;extruder heater off
179 M140 S0                     ;heated bed heater off (if you have it)
180
181 G91                                    ;relative positioning
182 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
183 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
184 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
185
186 M84                         ;steppers off
187 G90                         ;absolute positioning
188 """, str, 'alteration', 'alteration')
189 #######################################################################################
190 setting('support_start.gcode', '', str, 'alteration', 'alteration')
191 setting('support_end.gcode', '', str, 'alteration', 'alteration')
192 setting('cool_start.gcode', '', str, 'alteration', 'alteration')
193 setting('cool_end.gcode', '', str, 'alteration', 'alteration')
194 setting('replace.csv', '', str, 'alteration', 'alteration')
195 #######################################################################################
196 setting('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.
197 G92 E0
198
199 G91                                    ;relative positioning
200 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
201 G1 Z+0.5 E-5 F{travel_speed}           ;move Z up a bit and retract filament even more
202 G90                                    ;absolute positioning
203
204 G1 Z{clear_z} F{max_z_speed}
205 G92 E0
206 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
207 G1 F200 E6
208 G92 E0
209 """, str, 'alteration', 'alteration')
210 #######################################################################################
211 setting('switchExtruder.gcode', """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
212 G92 E0
213 G1 E-36 F5000
214 G92 E0
215 T{extruder}
216 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
217 G1 E36 F5000
218 G92 E0
219 """, str, 'alteration', 'alteration')
220
221 setting('startMode', 'Simple', ['Simple', 'Normal'], 'preference', 'hidden')
222 setting('lastFile', os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')), str, 'preference', 'hidden')
223 setting('machine_width', '205', float, 'preference', 'hidden')
224 setting('machine_depth', '205', float, 'preference', 'hidden')
225 setting('machine_height', '200', float, 'preference', 'hidden')
226 setting('machine_type', 'unknown', str, 'preference', 'hidden')
227 setting('machine_center_is_zero', 'False', bool, 'preference', 'hidden')
228 setting('ultimaker_extruder_upgrade', 'False', bool, 'preference', 'hidden')
229 setting('has_heated_bed', 'False', bool, 'preference', 'hidden')
230 setting('reprap_name', 'RepRap', str, 'preference', 'hidden')
231 setting('extruder_amount', '1', int, 'preference', 'hidden')
232 setting('extruder_offset_x1', '-21.6', float, 'preference', 'hidden')
233 setting('extruder_offset_y1', '0.0', float, 'preference', 'hidden')
234 setting('extruder_offset_x2', '0.0', float, 'preference', 'hidden')
235 setting('extruder_offset_y2', '0.0', float, 'preference', 'hidden')
236 setting('extruder_offset_x3', '0.0', float, 'preference', 'hidden')
237 setting('extruder_offset_y3', '0.0', float, 'preference', 'hidden')
238 setting('filament_density', '1300', float, 'preference', 'hidden')
239 setting('steps_per_e', '0', float, 'preference', 'hidden')
240 setting('serial_port', 'AUTO', str, 'preference', 'hidden')
241 setting('serial_port_auto', '', str, 'preference', 'hidden')
242 setting('serial_baud', 'AUTO', str, 'preference', 'hidden')
243 setting('serial_baud_auto', '', int, 'preference', 'hidden')
244 setting('save_profile', 'False', bool, 'preference', 'hidden')
245 setting('filament_cost_kg', '0', float, 'preference', 'hidden')
246 setting('filament_cost_meter', '0', float, 'preference', 'hidden')
247 setting('sdpath', '', str, 'preference', 'hidden')
248 setting('sdshortnames', 'False', bool, 'preference', 'hidden')
249 setting('check_for_updates', 'True', bool, 'preference', 'hidden')
250 setting('submit_slice_information', 'False', bool, 'preference', 'hidden')
251
252 setting('planner_always_autoplace', 'True', bool, 'preference', 'hidden')
253 setting('extruder_head_size_min_x', '75.0', float, 'preference', 'hidden')
254 setting('extruder_head_size_min_y', '18.0', float, 'preference', 'hidden')
255 setting('extruder_head_size_max_x', '18.0', float, 'preference', 'hidden')
256 setting('extruder_head_size_max_y', '35.0', float, 'preference', 'hidden')
257 setting('extruder_head_size_height', '60.0', float, 'preference', 'hidden')
258
259 setting('model_colour', '#7AB645', str, 'preference', 'hidden')
260 setting('model_colour2', '#CB3030', str, 'preference', 'hidden')
261 setting('model_colour3', '#DDD93C', str, 'preference', 'hidden')
262 setting('model_colour4', '#4550D3', str, 'preference', 'hidden')
263
264 setting('window_maximized', 'False', bool, 'preference', 'hidden')
265 setting('window_pos_x', '-1', float, 'preference', 'hidden')
266 setting('window_pos_y', '-1', float, 'preference', 'hidden')
267 setting('window_width', '-1', float, 'preference', 'hidden')
268 setting('window_height', '-1', float, 'preference', 'hidden')
269 setting('window_normal_sash', '320', float, 'preference', 'hidden')
270
271 #########################################################
272 ## Profile and preferences functions
273 #########################################################
274
275 ## Profile functions
276 def getBasePath():
277         if platform.system() == "Windows":
278                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
279                 #If we have a frozen python install, we need to step out of the library.zip
280                 if hasattr(sys, 'frozen'):
281                         basePath = os.path.normpath(os.path.join(basePath, ".."))
282         else:
283                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
284         if not os.path.isdir(basePath):
285                 os.makedirs(basePath)
286         return basePath
287
288 def getDefaultProfilePath():
289         return os.path.join(getBasePath(), 'current_profile.ini')
290
291 def loadProfile(filename):
292         #Read a configuration file as global config
293         profileParser = ConfigParser.ConfigParser()
294         try:
295                 profileParser.read(filename)
296         except ConfigParser.ParsingError:
297                 return
298         global settingsList
299         for set in settingsList:
300                 if set.isPreference():
301                         continue
302                 section = 'profile'
303                 if set.isAlteration():
304                         section = 'alterations'
305                 if profileParser.has_option(section, set.getName()):
306                         set.setValue(unicode(profileParser.get(section, set.getName()), 'utf-8', 'replace'))
307
308 def saveProfile(filename):
309         #Save the current profile to an ini file
310         profileParser = ConfigParser.ConfigParser()
311         profileParser.add_section('profile')
312         profileParser.add_section('alterations')
313         global settingsList
314         for set in settingsList:
315                 if set.isPreference():
316                         continue
317                 if set.isAlteration():
318                         profileParser.set('alterations', set.getName(), set.getValue().encode('utf-8'))
319                 else:
320                         profileParser.set('profile', set.getName(), set.getValue().encode('utf-8'))
321
322         profileParser.write(open(filename, 'w'))
323
324 def resetProfile():
325         #Read a configuration file as global config
326         global settingsList
327         for set in settingsList:
328                 if set.isPreference():
329                         continue
330                 set.setValue(set.getDefault())
331
332         if getPreference('machine_type') == 'ultimaker':
333                 putProfileSetting('nozzle_size', '0.4')
334                 if getPreference('ultimaker_extruder_upgrade') == 'True':
335                         putProfileSetting('retraction_enable', 'True')
336         else:
337                 putProfileSetting('nozzle_size', '0.5')
338
339 def loadProfileFromString(options):
340         options = base64.b64decode(options)
341         options = zlib.decompress(options)
342         (profileOpts, alt) = options.split('\f', 1)
343         global settingsDictionary
344         for option in profileOpts.split('\b'):
345                 if len(option) > 0:
346                         (key, value) = option.split('=', 1)
347                         if key in settingsDictionary:
348                                 if settingsDictionary[key].isProfile():
349                                         settingsDictionary[key].setValue(value)
350         for option in alt.split('\b'):
351                 if len(option) > 0:
352                         (key, value) = option.split('=', 1)
353                         if key in settingsDictionary:
354                                 if settingsDictionary[key].isAlteration():
355                                         settingsDictionary[key].setValue(value)
356
357 def getProfileString():
358         p = []
359         alt = []
360         global settingsList
361         for set in settingsList:
362                 if set.isProfile():
363                         if set.getName() in tempOverride:
364                                 p.append(set.getName() + "=" + tempOverride[set.getName()])
365                         else:
366                                 p.append(set.getName() + "=" + set.getValue())
367                 if set.isAlteration():
368                         if set.getName() in tempOverride:
369                                 alt.append(set.getName() + "=" + tempOverride[set.getName()])
370                         else:
371                                 alt.append(set.getName() + "=" + set.getValue())
372         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
373         ret = base64.b64encode(zlib.compress(ret, 9))
374         return ret
375
376 def getGlobalPreferencesString():
377         p = []
378         global settingsList
379         for set in settingsList:
380                 if set.isPreference():
381                         p.append(set.getName() + "=" + set.getValue())
382         ret = '\b'.join(p)
383         ret = base64.b64encode(zlib.compress(ret, 9))
384         return ret
385
386
387 def getProfileSetting(name):
388         if name in tempOverride:
389                 return tempOverride[name]
390         global settingsDictionary
391         if name in settingsDictionary and settingsDictionary[name].isProfile():
392                 return settingsDictionary[name].getValue()
393         print 'Error: "%s" not found in profile settings' % (name)
394         return ''
395
396 def getProfileSettingFloat(name):
397         try:
398                 setting = getProfileSetting(name).replace(',', '.')
399                 return float(eval(setting, {}, {}))
400         except (ValueError, SyntaxError, TypeError):
401                 return 0.0
402
403 def putProfileSetting(name, value):
404         #Check if we have a configuration file loaded, else load the default.
405         global settingsDictionary
406         if name in settingsDictionary and settingsDictionary[name].isProfile():
407                 settingsDictionary[name].setValue(value)
408
409 def isProfileSetting(name):
410         global settingsDictionary
411         if name in settingsDictionary and settingsDictionary[name].isProfile():
412                 return True
413         return False
414
415 ## Preferences functions
416 def getPreferencePath():
417         return os.path.join(getBasePath(), 'preferences.ini')
418
419 def getPreferenceFloat(name):
420         try:
421                 setting = getPreference(name).replace(',', '.')
422                 return float(eval(setting, {}, {}))
423         except (ValueError, SyntaxError, TypeError):
424                 return 0.0
425
426 def getPreferenceColour(name):
427         colorString = getPreference(name)
428         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
429
430 def loadPreferences(filename):
431         #Read a configuration file as global config
432         profileParser = ConfigParser.ConfigParser()
433         try:
434                 profileParser.read(filename)
435         except ConfigParser.ParsingError:
436                 return
437         global settingsList
438         for set in settingsList:
439                 if set.isPreference():
440                         if profileParser.has_option('preferences', set.getName()):
441                                 set.setValue(unicode(profileParser.get('preferences', set.getName()), 'utf-8', 'replace'))
442
443 def savePreferences(filename):
444         #Save the current profile to an ini file
445         parser = ConfigParser.ConfigParser()
446         parser.add_section('preferences')
447         global settingsList
448         for set in settingsList:
449                 if set.isPreference():
450                         parser.set('preferences', set.getName(), set.getValue().encode('utf-8'))
451         parser.write(open(filename, 'w'))
452
453 def getPreference(name):
454         if name in tempOverride:
455                 return tempOverride[name]
456         global settingsDictionary
457         if name in settingsDictionary and settingsDictionary[name].isPreference():
458                 return settingsDictionary[name].getValue()
459         print 'Error: "%s" not found in profile settings' % (name)
460         return ''
461
462 def putPreference(name, value):
463         #Check if we have a configuration file loaded, else load the default.
464         global settingsDictionary
465         if name in settingsDictionary and settingsDictionary[name].isPreference():
466                 settingsDictionary[name].setValue(value)
467         savePreferences(getPreferencePath())
468
469 def isPreference(name):
470         global settingsDictionary
471         if name in settingsDictionary and settingsDictionary[name].isPreference():
472                 return True
473         return False
474
475 ## Temp overrides for multi-extruder slicing and the project planner.
476 tempOverride = {}
477 def setTempOverride(name, value):
478         tempOverride[name] = unicode(value).encode("utf-8")
479 def clearTempOverride(name):
480         del tempOverride[name]
481 def resetTempOverride():
482         tempOverride.clear()
483
484 #########################################################
485 ## Utility functions to calculate common profile values
486 #########################################################
487 def calculateEdgeWidth():
488         wallThickness = getProfileSettingFloat('wall_thickness')
489         nozzleSize = getProfileSettingFloat('nozzle_size')
490         
491         if wallThickness < nozzleSize:
492                 return wallThickness
493
494         lineCount = int(wallThickness / nozzleSize + 0.0001)
495         lineWidth = wallThickness / lineCount
496         lineWidthAlt = wallThickness / (lineCount + 1)
497         if lineWidth > nozzleSize * 1.5:
498                 return lineWidthAlt
499         return lineWidth
500
501 def calculateLineCount():
502         wallThickness = getProfileSettingFloat('wall_thickness')
503         nozzleSize = getProfileSettingFloat('nozzle_size')
504         
505         if wallThickness < nozzleSize:
506                 return 1
507
508         lineCount = int(wallThickness / nozzleSize + 0.0001)
509         lineWidth = wallThickness / lineCount
510         lineWidthAlt = wallThickness / (lineCount + 1)
511         if lineWidth > nozzleSize * 1.5:
512                 return lineCount + 1
513         return lineCount
514
515 def calculateSolidLayerCount():
516         layerHeight = getProfileSettingFloat('layer_height')
517         solidThickness = getProfileSettingFloat('solid_layer_thickness')
518         return int(math.ceil(solidThickness / layerHeight - 0.0001))
519
520 def getMachineCenterCoords():
521         if getPreference('machine_center_is_zero') == 'True':
522                 return [0, 0]
523         return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
524
525 def getObjectMatrix():
526         try:
527                 return map(float, getProfileSetting('model_matrix').split(','))
528         except ValueError:
529                 return [1,0,0, 0,1,0, 0,0,1]
530
531
532 #########################################################
533 ## Alteration file functions
534 #########################################################
535 def replaceTagMatch(m):
536         pre = m.group(1)
537         tag = m.group(2)
538         if tag == 'time':
539                 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
540         if tag == 'date':
541                 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
542         if tag == 'day':
543                 return pre + time.strftime('%a').encode('utf-8', 'replace')
544         if tag == 'print_time':
545                 return pre + '#P_TIME#'
546         if tag == 'filament_amount':
547                 return pre + '#F_AMNT#'
548         if tag == 'filament_weight':
549                 return pre + '#F_WGHT#'
550         if tag == 'filament_cost':
551                 return pre + '#F_COST#'
552         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
553                 f = getProfileSettingFloat(tag) * 60
554         elif isProfileSetting(tag):
555                 f = getProfileSettingFloat(tag)
556         elif isPreference(tag):
557                 f = getProfileSettingFloat(tag)
558         else:
559                 return '%s?%s?' % (pre, tag)
560         if (f % 1) == 0:
561                 return pre + str(int(f))
562         return pre + str(f)
563
564 def replaceGCodeTags(filename, gcodeInt):
565         f = open(filename, 'r+')
566         data = f.read(2048)
567         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
568         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
569         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
570         cost = gcodeInt.calculateCost()
571         if cost is None:
572                 cost = 'Unknown'
573         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
574         f.seek(0)
575         f.write(data)
576         f.close()
577
578 ### Get aleration raw contents. (Used internally in Cura)
579 def getAlterationFile(filename):
580         if filename in tempOverride:
581                 return tempOverride[filename]
582         global settingsDictionary
583         if filename in settingsDictionary and settingsDictionary[filename].isAlteration():
584                 return settingsDictionary[filename].getValue()
585         print 'Error: "%s" not found in profile settings' % (filename)
586         return ''
587
588 def setAlterationFile(name, value):
589         #Check if we have a configuration file loaded, else load the default.
590         global settingsDictionary
591         if name in settingsDictionary and settingsDictionary[name].isAlteration():
592                 settingsDictionary[name].setValue(value)
593         saveProfile(getDefaultProfilePath())
594
595 ### Get the alteration file for output. (Used by Skeinforge)
596 def getAlterationFileContents(filename, extruderCount = 1):
597         prefix = ''
598         postfix = ''
599         alterationContents = getAlterationFile(filename)
600         if filename == 'start.gcode':
601                 #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.
602                 #We also set our steps per E here, if configured.
603                 eSteps = getPreferenceFloat('steps_per_e')
604                 if eSteps > 0:
605                         prefix += 'M92 E%f\n' % (eSteps)
606                 temp = getProfileSettingFloat('print_temperature')
607                 bedTemp = 0
608                 if getPreference('has_heated_bed') == 'True':
609                         bedTemp = getProfileSettingFloat('print_bed_temperature')
610                 
611                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
612                         prefix += 'M140 S%f\n' % (bedTemp)
613                 if temp > 0 and not '{print_temperature}' in alterationContents:
614                         if extruderCount > 0:
615                                 for n in xrange(1, extruderCount):
616                                         t = temp
617                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
618                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
619                                         prefix += 'M104 T%d S%f\n' % (n, temp)
620                                 for n in xrange(0, extruderCount):
621                                         t = temp
622                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
623                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
624                                         prefix += 'M109 T%d S%f\n' % (n, temp)
625                                 prefix += 'T0\n'
626                         else:
627                                 prefix += 'M109 S%f\n' % (temp)
628                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
629                         prefix += 'M190 S%f\n' % (bedTemp)
630         elif filename == 'end.gcode':
631                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
632                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getProfileString())
633         elif filename == 'replace.csv':
634                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
635                 prefix = 'M101\nM103\n'
636         elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
637                 #Add support start/end code 
638                 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
639                         if filename == 'support_start.gcode':
640                                 setTempOverride('extruder', '1')
641                         else:
642                                 setTempOverride('extruder', '0')
643                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
644                         clearTempOverride('extruder')
645                 else:
646                         alterationContents = ''
647         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
648
649 ###### PLUGIN #####
650
651 def getPluginConfig():
652         try:
653                 return pickle.loads(getProfileSetting('plugin_config'))
654         except:
655                 return []
656
657 def setPluginConfig(config):
658         putProfileSetting('plugin_config', pickle.dumps(config))
659
660 def getPluginBasePaths():
661         ret = []
662         if platform.system() != "Windows":
663                 ret.append(os.path.expanduser('~/.cura/plugins/'))
664         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
665                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
666         else:
667                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
668         return ret
669
670 def getPluginList():
671         ret = []
672         for basePath in getPluginBasePaths():
673                 for filename in glob.glob(os.path.join(basePath, '*.py')):
674                         filename = os.path.basename(filename)
675                         if filename.startswith('_'):
676                                 continue
677                         with open(os.path.join(basePath, filename), "r") as f:
678                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
679                                 for line in f:
680                                         line = line.strip()
681                                         if not line.startswith('#'):
682                                                 break
683                                         line = line[1:].split(':', 1)
684                                         if len(line) != 2:
685                                                 continue
686                                         if line[0].upper() == 'NAME':
687                                                 item['name'] = line[1].strip()
688                                         elif line[0].upper() == 'INFO':
689                                                 item['info'] = line[1].strip()
690                                         elif line[0].upper() == 'TYPE':
691                                                 item['type'] = line[1].strip()
692                                         elif line[0].upper() == 'DEPEND':
693                                                 pass
694                                         elif line[0].upper() == 'PARAM':
695                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
696                                                 if m is not None:
697                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
698                                         else:
699                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
700                                 if item['name'] != None and item['type'] == 'postprocess':
701                                         ret.append(item)
702         return ret
703
704 def runPostProcessingPlugins(gcodefilename):
705         pluginConfigList = getPluginConfig()
706         pluginList = getPluginList()
707         
708         for pluginConfig in pluginConfigList:
709                 plugin = None
710                 for pluginTest in pluginList:
711                         if pluginTest['filename'] == pluginConfig['filename']:
712                                 plugin = pluginTest
713                 if plugin is None:
714                         continue
715                 
716                 pythonFile = None
717                 for basePath in getPluginBasePaths():
718                         testFilename = os.path.join(basePath, pluginConfig['filename'])
719                         if os.path.isfile(testFilename):
720                                 pythonFile = testFilename
721                 if pythonFile is None:
722                         continue
723                 
724                 locals = {'filename': gcodefilename}
725                 for param in plugin['params']:
726                         value = param['default']
727                         if param['name'] in pluginConfig['params']:
728                                 value = pluginConfig['params'][param['name']]
729                         
730                         if param['type'] == 'float':
731                                 try:
732                                         value = float(value)
733                                 except:
734                                         value = float(param['default'])
735                         
736                         locals[param['name']] = value
737                 try:
738                         execfile(pythonFile, locals)
739                 except:
740                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
741                         return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
742         return None
743
744 def getSDcardDrives():
745         drives = ['']
746         if platform.system() == "Windows":
747                 from ctypes import windll
748                 bitmask = windll.kernel32.GetLogicalDrives()
749                 for letter in string.uppercase:
750                         if bitmask & 1:
751                                 drives.append(letter + ':/')
752                         bitmask >>= 1
753         if platform.system() == "Darwin":
754                 drives = []
755                 for volume in glob.glob('/Volumes/*'):
756                         if stat.S_ISLNK(os.lstat(volume).st_mode):
757                                 continue
758                         drives.append(volume)
759         return drives