chiark / gitweb /
Add new way of storing settings.
[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
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
14 settingsDictionary = {}
15 settingsList = []
16 class setting:
17         def __init__(self, name, default, type, category, subcategory):
18                 self._name = name
19                 self._label = name
20                 self._tooltip = ''
21                 self._default = unicode(default)
22                 self._value = self._default
23                 self._type = type
24                 self._category = category
25                 self._subcategory = subcategory
26                 self._validators = []
27
28                 global settingsDictionary
29                 settingsDictionary[name] = self
30                 global settingsList
31                 settingsList.append(self)
32
33         def setLabel(self, label, tooltip = ''):
34                 self._label = label
35                 self._tooltip = tooltip
36                 return self
37
38 #########################################################
39 ## Settings
40 #########################################################
41 setting('layer_height',              0.2, float, 'basic',    'Quality').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')
42 setting('wall_thickness',            0.8, float, 'basic',    'Quality').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.')
43 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.')
44 setting('solid_layer_thickness',     0.6, float, 'basic',    'Fill').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.')
45 setting('fill_density',               20, float, 'basic',    'Fill').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.')
46 setting('nozzle_size',               0.4, float, 'advanced', 'Machine').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.')
47 setting('skirt_line_count',            1, int,   'advanced', 'Skirt').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.')
48 setting('skirt_gap',                 3.0, float, 'advanced', 'Skirt').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.')
49 setting('print_speed',                50, float, 'basic',    'Speed & Temperature').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.')
50 setting('print_temperature',         220, int,   'basic',    'Speed & Temperature').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.')
51 setting('print_temperature2',          0, int,   'basic',    'Speed & Temperature').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.')
52 setting('print_temperature3',          0, int,   'basic',    'Speed & Temperature').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.')
53 setting('print_temperature4',          0, int,   'basic',    'Speed & Temperature').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.')
54 setting('print_bed_temperature',      70, int,   'basic',    'Speed & Temperature').setLabel('Bed temperature (C)', 'Temperature used for the heated printer bed. Set at 0 to pre-heat yourself.')
55 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.')
56 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.')
57 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.')
58 setting('filament_diameter',        2.89, float, 'basic',    'Filament').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.')
59 setting('filament_diameter2',          0, float, 'basic',    'Filament').setLabel('Diameter2 (mm)', 'Diameter of your filament for the 2nd nozzle. Use 0 to use the same diameter as for nozzle 1.')
60 setting('filament_diameter3',          0, float, 'basic',    'Filament').setLabel('Diameter3 (mm)', 'Diameter of your filament for the 3th nozzle. Use 0 to use the same diameter as for nozzle 1.')
61 setting('filament_diameter4',          0, float, 'basic',    'Filament').setLabel('Diameter4 (mm)', 'Diameter of your filament for the 4th nozzle. Use 0 to use the same diameter as for nozzle 1.')
62 setting('filament_density',         1.00, float, 'basic',    'Filament').setLabel('Packing Density', 'Packing density of your filament. This should be 1.00 for PLA and 0.85 for ABS')
63 setting('retraction_min_travel',     5.0, float, 'advanced', 'Retraction').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')
64 setting('retraction_speed',         40.0, float, 'advanced', 'Retraction').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.')
65 setting('retraction_amount',         4.5, float, 'advanced', 'Retraction').setLabel('Distance (mm)', 'Amount of retraction, set at 0 for no retraction at all. A value of 2.0mm seems to generate good results.')
66 setting('retraction_extra',          0.0, float, 'advanced', 'Retraction').setLabel('Extra length on start (mm)', 'Extra extrusion amount when restarting after a retraction, to better "Prime" your extruder after retraction.')
67 setting('bottom_thickness',          0.3, float, 'advanced', 'Quality').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.')
68 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.')
69 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.')
70 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.')
71 setting('travel_speed',            150.0, float, 'advanced', 'Speed').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.')
72 setting('max_z_speed',               3.0, float, 'expert',   'Speed').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.')
73 setting('bottom_layer_speed',         20, float, 'advanced', 'Speed').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.')
74 setting('cool_min_layer_time',         5, float, 'advanced', 'Cool').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.')
75 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.')
76 setting('fan_layer',                   1, int,   'expert',   'Cool').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.')
77 setting('fan_speed',                 100, int,   'expert',   'Cool').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.')
78 setting('fan_speed_max',             100, int,   'expert',   'Cool').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%.')
79 setting('cool_min_feedrate',          10, float, 'expert',   'Cool').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.')
80 setting('extra_base_wall_thickness', 0.0, float, 'expert', 'Accuracy').setLabel('Extra Wall thickness for bottom/top (mm)', 'Additional wall thickness of the bottom and top layers.')
81 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')
82 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\'')
83 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.')
84 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.')
85 setting('fill_overlap', 15, int, 'expert', 'Infill').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.')
86 setting('support_rate', 50, int, 'expert', 'Support').setLabel('Material amount (%)', 'Amount of material used for support, less material gives a weaker support structure which is easier to remove.')
87 setting('support_distance',  0.5, float, 'expert', 'Support').setLabel('Distance from object (mm)', 'Distance between the support structure and the object. Empty gap in which no support structure is printed.')
88 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.')
89 setting('bridge_speed', 100, int, 'expert', 'Bridge').setLabel('Bridge speed (%)', 'Speed at which layers with bridges are printed, compared to normal printing speed.')
90 setting('raft_margin', 5, float, 'expert', 'Raft').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.')
91 setting('raft_base_material_amount', 100, int, 'expert', 'Raft').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.')
92 setting('raft_interface_material_amount', 100, int, 'expert', 'Raft').setLabel('Interface material amount (%)', 'raft_interface_material_amount', '100', '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.')
93 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).')
94
95 setting('model_matrix', '1,0,0,0,1,0,0,0,1', string, 'hidden', 'hidden')
96 setting('plugin_config', '', string, 'hidden', 'hidden')
97 setting('object_center_x', -1, float, 'hidden', 'hidden')
98 setting('object_center_y', -1, float, 'hidden', 'hidden')
99
100 #Single place to store the defaults, so we have a consistent set of default settings.
101 profileDefaultSettings = {
102         'nozzle_size': '0.4',
103         'layer_height': '0.2',
104         'wall_thickness': '0.8',
105         'solid_layer_thickness': '0.6',
106         'fill_density': '20',
107         'skirt_line_count': '1',
108         'skirt_gap': '3.0',
109         'print_speed': '50',
110         'print_temperature': '220',
111         'print_temperature2': '0',
112         'print_temperature3': '0',
113         'print_temperature4': '0',
114         'print_bed_temperature': '70',
115         'support': 'None',
116         'filament_diameter': '2.89',
117         'filament_diameter2': '0',
118         'filament_diameter3': '0',
119         'filament_diameter4': '0',
120         'filament_density': '1.00',
121         'retraction_min_travel': '5.0',
122         'retraction_enable': 'False',
123         'retraction_speed': '40.0',
124         'retraction_amount': '4.5',
125         'retraction_extra': '0.0',
126         'retract_on_jumps_only': 'True',
127         'travel_speed': '150',
128         'max_z_speed': '3.0',
129         'bottom_layer_speed': '20',
130         'cool_min_layer_time': '5',
131         'fan_enabled': 'True',
132         'fan_layer': '1',
133         'fan_speed': '100',
134         'fan_speed_max': '100',
135         'model_matrix': '1,0,0,0,1,0,0,0,1',
136         'extra_base_wall_thickness': '0.0',
137         'sequence': 'Loops > Perimeter > Infill',
138         'force_first_layer_sequence': 'True',
139         'infill_type': 'Line',
140         'solid_top': 'True',
141         'fill_overlap': '15',
142         'support_rate': '50',
143         'support_distance': '0.5',
144         'support_dual_extrusion': 'False',
145         'joris': 'False',
146         'enable_skin': 'False',
147         'enable_raft': 'False',
148         'cool_min_feedrate': '10',
149         'bridge_speed': '100',
150         'raft_margin': '5',
151         'raft_base_material_amount': '100',
152         'raft_interface_material_amount': '100',
153         'bottom_thickness': '0.3',
154         'hop_on_move': 'False',
155         'plugin_config': '',
156         'object_center_x': '-1',
157         'object_center_y': '-1',
158         'object_sink': '0.0',
159         
160         'gcode_extension': 'gcode',
161         'alternative_center': '',
162         'clear_z': '0.0',
163         'extruder': '0',
164         'new_x': '0',
165         'new_y': '0',
166         'new_z': '0',
167 }
168 alterationDefault = {
169 #######################################################################################
170         'start.gcode': """;Sliced {filename} at: {day} {date} {time}
171 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
172 ;Print time: {print_time}
173 ;Filament used: {filament_amount}m {filament_weight}g
174 ;Filament cost: {filament_cost}
175 G21        ;metric values
176 G90        ;absolute positioning
177 M107       ;start with the fan off
178
179 G28 X0 Y0  ;move X/Y to min endstops
180 G28 Z0     ;move Z to min endstops
181
182 G1 Z15.0 F{max_z_speed} ;move the platform down 15mm
183
184 G92 E0                  ;zero the extruded length
185 G1 F200 E3              ;extrude 3mm of feed stock
186 G92 E0                  ;zero the extruded length again
187 G1 F{travel_speed}
188 M117 Printing...
189 """,
190 #######################################################################################
191         'end.gcode': """;End GCode
192 M104 S0                     ;extruder heater off
193 M140 S0                     ;heated bed heater off (if you have it)
194
195 G91                                    ;relative positioning
196 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
197 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
198 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
199
200 M84                         ;steppers off
201 G90                         ;absolute positioning
202 """,
203 #######################################################################################
204         'support_start.gcode': '',
205         'support_end.gcode': '',
206         'cool_start.gcode': '',
207         'cool_end.gcode': '',
208         'replace.csv': '',
209 #######################################################################################
210         '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.
211 G92 E0
212
213 G91                                    ;relative positioning
214 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
215 G1 Z+0.5 E-5 F{travel_speed}           ;move Z up a bit and retract filament even more
216 G90                                    ;absolute positioning
217
218 G1 Z{clear_z} F{max_z_speed}
219 G92 E0
220 G1 X{object_center_x} Y{object_center_y} F{travel_speed}
221 G1 F200 E6
222 G92 E0
223 """,
224 #######################################################################################
225         'switchExtruder.gcode': """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
226 G92 E0
227 G1 E-36 F5000
228 G92 E0
229 T{extruder}
230 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
231 G1 E36 F5000
232 G92 E0
233 """,
234 }
235 preferencesDefaultSettings = {
236         'startMode': 'Simple',
237         'lastFile': os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')),
238         'machine_width': '205',
239         'machine_depth': '205',
240         'machine_height': '200',
241         'machine_type': 'unknown',
242         'machine_center_is_zero': 'False',
243         'ultimaker_extruder_upgrade': 'False',
244         'has_heated_bed': 'False',
245         'reprap_name': 'RepRap',
246         'extruder_amount': '1',
247         'extruder_offset_x1': '-22.0',
248         'extruder_offset_y1': '0.0',
249         'extruder_offset_x2': '0.0',
250         'extruder_offset_y2': '0.0',
251         'extruder_offset_x3': '0.0',
252         'extruder_offset_y3': '0.0',
253         'filament_density': '1300',
254         'steps_per_e': '0',
255         'serial_port': 'AUTO',
256         'serial_port_auto': '',
257         'serial_baud': 'AUTO',
258         'serial_baud_auto': '',
259         'slicer': 'Cura (Skeinforge based)',
260         'save_profile': 'False',
261         'filament_cost_kg': '0',
262         'filament_cost_meter': '0',
263         'sdpath': '',
264         'sdshortnames': 'False',
265         'check_for_updates': 'True',
266         'submit_slice_information': 'False',
267
268         'planner_always_autoplace': 'True',
269         'extruder_head_size_min_x': '75.0',
270         'extruder_head_size_min_y': '18.0',
271         'extruder_head_size_max_x': '18.0',
272         'extruder_head_size_max_y': '35.0',
273         'extruder_head_size_height': '60.0',
274         
275         'model_colour': '#7AB645',
276         'model_colour2': '#CB3030',
277         'model_colour3': '#DDD93C',
278         'model_colour4': '#4550D3',
279
280         'window_maximized': 'False',
281         'window_pos_x': '-1',
282         'window_pos_y': '-1',
283         'window_width': '-1',
284         'window_height': '-1',
285         'window_normal_sash': '320',
286 }
287
288 #########################################################
289 ## Profile and preferences functions
290 #########################################################
291
292 ## Profile functions
293 def getBasePath():
294         if platform.system() == "Windows":
295                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
296                 #If we have a frozen python install, we need to step out of the library.zip
297                 if hasattr(sys, 'frozen'):
298                         basePath = os.path.normpath(os.path.join(basePath, ".."))
299         else:
300                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
301         if not os.path.isdir(basePath):
302                 os.makedirs(basePath)
303         return basePath
304
305 def getDefaultProfilePath():
306         return os.path.join(getBasePath(), 'current_profile.ini')
307
308 def loadGlobalProfile(filename):
309         #Read a configuration file as global config
310         global globalProfileParser
311         globalProfileParser = ConfigParser.ConfigParser()
312         try:
313                 globalProfileParser.read(filename)
314         except ConfigParser.ParsingError:
315                 pass
316
317 def resetGlobalProfile():
318         #Read a configuration file as global config
319         global globalProfileParser
320         globalProfileParser = ConfigParser.ConfigParser()
321
322         if getPreference('machine_type') == 'ultimaker':
323                 putProfileSetting('nozzle_size', '0.4')
324                 if getPreference('ultimaker_extruder_upgrade') == 'True':
325                         putProfileSetting('retraction_enable', 'True')
326         else:
327                 putProfileSetting('nozzle_size', '0.5')
328
329 def saveGlobalProfile(filename):
330         #Save the current profile to an ini file
331         globalProfileParser.write(open(filename, 'w'))
332
333 def loadGlobalProfileFromString(options):
334         global globalProfileParser
335         globalProfileParser = ConfigParser.ConfigParser()
336         globalProfileParser.add_section('profile')
337         globalProfileParser.add_section('alterations')
338         options = base64.b64decode(options)
339         options = zlib.decompress(options)
340         (profileOpts, alt) = options.split('\f', 1)
341         for option in profileOpts.split('\b'):
342                 if len(option) > 0:
343                         (key, value) = option.split('=', 1)
344                         globalProfileParser.set('profile', key, value)
345         for option in alt.split('\b'):
346                 if len(option) > 0:
347                         (key, value) = option.split('=', 1)
348                         globalProfileParser.set('alterations', key, value)
349
350 def getGlobalProfileString():
351         global globalProfileParser
352         if not globals().has_key('globalProfileParser'):
353                 loadGlobalProfile(getDefaultProfilePath())
354         
355         p = []
356         alt = []
357         tempDone = []
358         if globalProfileParser.has_section('profile'):
359                 for key in globalProfileParser.options('profile'):
360                         if key in tempOverride:
361                                 p.append(key + "=" + tempOverride[key])
362                                 tempDone.append(key)
363                         else:
364                                 p.append(key + "=" + globalProfileParser.get('profile', key))
365         if globalProfileParser.has_section('alterations'):
366                 for key in globalProfileParser.options('alterations'):
367                         if key in tempOverride:
368                                 p.append(key + "=" + tempOverride[key])
369                                 tempDone.append(key)
370                         else:
371                                 alt.append(key + "=" + globalProfileParser.get('alterations', key))
372         for key in tempOverride:
373                 if key not in tempDone:
374                         p.append(key + "=" + tempOverride[key])
375         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
376         ret = base64.b64encode(zlib.compress(ret, 9))
377         return ret
378
379 def getGlobalPreferencesString():
380         global globalPreferenceParser
381         if globalPreferenceParser is None:
382                 globalPreferenceParser = ConfigParser.ConfigParser()
383                 try:
384                         globalPreferenceParser.read(getPreferencePath())
385                 except ConfigParser.ParsingError:
386                         pass
387
388         p = []
389         if globalPreferenceParser.has_section('preference'):
390                 for key in globalPreferenceParser.options('preference'):
391                         p.append(key + "=" + globalPreferenceParser.get('preference', key))
392         ret = '\b'.join(p)
393         ret = base64.b64encode(zlib.compress(ret, 9))
394         return ret
395
396
397 def getProfileSetting(name):
398         if name in tempOverride:
399                 return unicode(tempOverride[name], "utf-8")
400         #Check if we have a configuration file loaded, else load the default.
401         if not globals().has_key('globalProfileParser'):
402                 loadGlobalProfile(getDefaultProfilePath())
403         if not globalProfileParser.has_option('profile', name):
404                 if name in profileDefaultSettings:
405                         default = profileDefaultSettings[name]
406                 else:
407                         print("Missing default setting for: '" + name + "'")
408                         profileDefaultSettings[name] = ''
409                         default = ''
410                 if not globalProfileParser.has_section('profile'):
411                         globalProfileParser.add_section('profile')
412                 globalProfileParser.set('profile', name, str(default))
413                 #print(name + " not found in profile, so using default: " + str(default))
414                 return default
415         return globalProfileParser.get('profile', name)
416
417 def getProfileSettingFloat(name):
418         try:
419                 setting = getProfileSetting(name).replace(',', '.')
420                 return float(eval(setting, {}, {}))
421         except (ValueError, SyntaxError, TypeError):
422                 return 0.0
423
424 def putProfileSetting(name, value):
425         #Check if we have a configuration file loaded, else load the default.
426         if not globals().has_key('globalProfileParser'):
427                 loadGlobalProfile(getDefaultProfilePath())
428         if not globalProfileParser.has_section('profile'):
429                 globalProfileParser.add_section('profile')
430         globalProfileParser.set('profile', name, str(value))
431
432 def isProfileSetting(name):
433         if name in profileDefaultSettings:
434                 return True
435         return False
436
437 ## Preferences functions
438 global globalPreferenceParser
439 globalPreferenceParser = None
440
441 def getPreferencePath():
442         return os.path.join(getBasePath(), 'preferences.ini')
443
444 def getPreferenceFloat(name):
445         try:
446                 setting = getPreference(name).replace(',', '.')
447                 return float(eval(setting, {}, {}))
448         except (ValueError, SyntaxError, TypeError):
449                 return 0.0
450
451 def getPreferenceColour(name):
452         colorString = getPreference(name)
453         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
454
455 def getPreference(name):
456         if name in tempOverride:
457                 return unicode(tempOverride[name])
458         global globalPreferenceParser
459         if globalPreferenceParser is None:
460                 globalPreferenceParser = ConfigParser.ConfigParser()
461                 try:
462                         globalPreferenceParser.read(getPreferencePath())
463                 except ConfigParser.ParsingError:
464                         pass
465         if not globalPreferenceParser.has_option('preference', name):
466                 if name in preferencesDefaultSettings:
467                         default = preferencesDefaultSettings[name]
468                 else:
469                         print("Missing default setting for: '" + name + "'")
470                         preferencesDefaultSettings[name] = ''
471                         default = ''
472                 if not globalPreferenceParser.has_section('preference'):
473                         globalPreferenceParser.add_section('preference')
474                 globalPreferenceParser.set('preference', name, str(default))
475                 #print(name + " not found in preferences, so using default: " + str(default))
476                 return default
477         return unicode(globalPreferenceParser.get('preference', name), "utf-8")
478
479 def putPreference(name, value):
480         #Check if we have a configuration file loaded, else load the default.
481         global globalPreferenceParser
482         if globalPreferenceParser == None:
483                 globalPreferenceParser = ConfigParser.ConfigParser()
484                 try:
485                         globalPreferenceParser.read(getPreferencePath())
486                 except ConfigParser.ParsingError:
487                         pass
488         if not globalPreferenceParser.has_section('preference'):
489                 globalPreferenceParser.add_section('preference')
490         globalPreferenceParser.set('preference', name, unicode(value).encode("utf-8"))
491         globalPreferenceParser.write(open(getPreferencePath(), 'w'))
492
493 def isPreference(name):
494         if name in preferencesDefaultSettings:
495                 return True
496         return False
497
498 ## Temp overrides for multi-extruder slicing and the project planner.
499 tempOverride = {}
500 def setTempOverride(name, value):
501         tempOverride[name] = unicode(value).encode("utf-8")
502 def clearTempOverride(name):
503         del tempOverride[name]
504 def resetTempOverride():
505         tempOverride.clear()
506
507 #########################################################
508 ## Utility functions to calculate common profile values
509 #########################################################
510 def calculateEdgeWidth():
511         wallThickness = getProfileSettingFloat('wall_thickness')
512         nozzleSize = getProfileSettingFloat('nozzle_size')
513         
514         if wallThickness < nozzleSize:
515                 return wallThickness
516
517         lineCount = int(wallThickness / nozzleSize + 0.0001)
518         lineWidth = wallThickness / lineCount
519         lineWidthAlt = wallThickness / (lineCount + 1)
520         if lineWidth > nozzleSize * 1.5:
521                 return lineWidthAlt
522         return lineWidth
523
524 def calculateLineCount():
525         wallThickness = getProfileSettingFloat('wall_thickness')
526         nozzleSize = getProfileSettingFloat('nozzle_size')
527         
528         if wallThickness < nozzleSize:
529                 return 1
530
531         lineCount = int(wallThickness / nozzleSize + 0.0001)
532         lineWidth = wallThickness / lineCount
533         lineWidthAlt = wallThickness / (lineCount + 1)
534         if lineWidth > nozzleSize * 1.5:
535                 return lineCount + 1
536         return lineCount
537
538 def calculateSolidLayerCount():
539         layerHeight = getProfileSettingFloat('layer_height')
540         solidThickness = getProfileSettingFloat('solid_layer_thickness')
541         return int(math.ceil(solidThickness / layerHeight - 0.0001))
542
543 def getMachineCenterCoords():
544         if getPreference('machine_center_is_zero') == 'True':
545                 return [0, 0]
546         return [getPreferenceFloat('machine_width') / 2, getPreferenceFloat('machine_depth') / 2]
547
548 def getObjectMatrix():
549         try:
550                 return map(float, getProfileSetting('model_matrix').split(','))
551         except ValueError:
552                 return [1,0,0, 0,1,0, 0,0,1]
553
554
555 #########################################################
556 ## Alteration file functions
557 #########################################################
558 def replaceTagMatch(m):
559         pre = m.group(1)
560         tag = m.group(2)
561         if tag == 'time':
562                 return pre + time.strftime('%H:%M:%S').encode('utf-8', 'replace')
563         if tag == 'date':
564                 return pre + time.strftime('%d %b %Y').encode('utf-8', 'replace')
565         if tag == 'day':
566                 return pre + time.strftime('%a').encode('utf-8', 'replace')
567         if tag == 'print_time':
568                 return pre + '#P_TIME#'
569         if tag == 'filament_amount':
570                 return pre + '#F_AMNT#'
571         if tag == 'filament_weight':
572                 return pre + '#F_WGHT#'
573         if tag == 'filament_cost':
574                 return pre + '#F_COST#'
575         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'max_z_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
576                 f = getProfileSettingFloat(tag) * 60
577         elif isProfileSetting(tag):
578                 f = getProfileSettingFloat(tag)
579         elif isPreference(tag):
580                 f = getProfileSettingFloat(tag)
581         else:
582                 return '%s?%s?' % (pre, tag)
583         if (f % 1) == 0:
584                 return pre + str(int(f))
585         return pre + str(f)
586
587 def replaceGCodeTags(filename, gcodeInt):
588         f = open(filename, 'r+')
589         data = f.read(2048)
590         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
591         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
592         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
593         cost = gcodeInt.calculateCost()
594         if cost is None:
595                 cost = 'Unknown'
596         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
597         f.seek(0)
598         f.write(data)
599         f.close()
600
601 ### Get aleration raw contents. (Used internally in Cura)
602 def getAlterationFile(filename):
603         #Check if we have a configuration file loaded, else load the default.
604         if not globals().has_key('globalProfileParser'):
605                 loadGlobalProfile(getDefaultProfilePath())
606         
607         if not globalProfileParser.has_option('alterations', filename):
608                 if filename in alterationDefault:
609                         default = alterationDefault[filename]
610                 else:
611                         print("Missing default alteration for: '" + filename + "'")
612                         alterationDefault[filename] = ''
613                         default = ''
614                 if not globalProfileParser.has_section('alterations'):
615                         globalProfileParser.add_section('alterations')
616                 #print("Using default for: %s" % (filename))
617                 globalProfileParser.set('alterations', filename, default)
618         return unicode(globalProfileParser.get('alterations', filename), "utf-8")
619
620 def setAlterationFile(filename, value):
621         #Check if we have a configuration file loaded, else load the default.
622         if not globals().has_key('globalProfileParser'):
623                 loadGlobalProfile(getDefaultProfilePath())
624         if not globalProfileParser.has_section('alterations'):
625                 globalProfileParser.add_section('alterations')
626         globalProfileParser.set('alterations', filename, value.encode("utf-8"))
627         saveGlobalProfile(getDefaultProfilePath())
628
629 ### Get the alteration file for output. (Used by Skeinforge)
630 def getAlterationFileContents(filename, extruderCount = 1):
631         prefix = ''
632         postfix = ''
633         alterationContents = getAlterationFile(filename)
634         if filename == 'start.gcode':
635                 #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.
636                 #We also set our steps per E here, if configured.
637                 eSteps = getPreferenceFloat('steps_per_e')
638                 if eSteps > 0:
639                         prefix += 'M92 E%f\n' % (eSteps)
640                 temp = getProfileSettingFloat('print_temperature')
641                 bedTemp = 0
642                 if getPreference('has_heated_bed') == 'True':
643                         bedTemp = getProfileSettingFloat('print_bed_temperature')
644                 
645                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
646                         prefix += 'M140 S%f\n' % (bedTemp)
647                 if temp > 0 and not '{print_temperature}' in alterationContents:
648                         if extruderCount > 0:
649                                 for n in xrange(1, extruderCount):
650                                         t = temp
651                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
652                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
653                                         prefix += 'M104 T%d S%f\n' % (n, temp)
654                                 for n in xrange(0, extruderCount):
655                                         t = temp
656                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
657                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
658                                         prefix += 'M109 T%d S%f\n' % (n, temp)
659                                 prefix += 'T0\n'
660                         else:
661                                 prefix += 'M109 S%f\n' % (temp)
662                 if bedTemp > 0 and not '{print_bed_temperature}' in alterationContents:
663                         prefix += 'M190 S%f\n' % (bedTemp)
664         elif filename == 'end.gcode':
665                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
666                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getGlobalProfileString())
667         elif filename == 'replace.csv':
668                 #Always remove the extruder on/off M codes. These are no longer needed in 5D printing.
669                 prefix = 'M101\nM103\n'
670         elif filename == 'support_start.gcode' or filename == 'support_end.gcode':
671                 #Add support start/end code 
672                 if getProfileSetting('support_dual_extrusion') == 'True' and int(getPreference('extruder_amount')) > 1:
673                         if filename == 'support_start.gcode':
674                                 setTempOverride('extruder', '1')
675                         else:
676                                 setTempOverride('extruder', '0')
677                         alterationContents = getAlterationFileContents('switchExtruder.gcode')
678                         clearTempOverride('extruder')
679                 else:
680                         alterationContents = ''
681         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
682
683 ###### PLUGIN #####
684
685 def getPluginConfig():
686         try:
687                 return pickle.loads(getProfileSetting('plugin_config'))
688         except:
689                 return []
690
691 def setPluginConfig(config):
692         putProfileSetting('plugin_config', pickle.dumps(config))
693
694 def getPluginBasePaths():
695         ret = []
696         if platform.system() != "Windows":
697                 ret.append(os.path.expanduser('~/.cura/plugins/'))
698         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
699                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
700         else:
701                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
702         return ret
703
704 def getPluginList():
705         ret = []
706         for basePath in getPluginBasePaths():
707                 for filename in glob.glob(os.path.join(basePath, '*.py')):
708                         filename = os.path.basename(filename)
709                         if filename.startswith('_'):
710                                 continue
711                         with open(os.path.join(basePath, filename), "r") as f:
712                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
713                                 for line in f:
714                                         line = line.strip()
715                                         if not line.startswith('#'):
716                                                 break
717                                         line = line[1:].split(':', 1)
718                                         if len(line) != 2:
719                                                 continue
720                                         if line[0].upper() == 'NAME':
721                                                 item['name'] = line[1].strip()
722                                         elif line[0].upper() == 'INFO':
723                                                 item['info'] = line[1].strip()
724                                         elif line[0].upper() == 'TYPE':
725                                                 item['type'] = line[1].strip()
726                                         elif line[0].upper() == 'DEPEND':
727                                                 pass
728                                         elif line[0].upper() == 'PARAM':
729                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
730                                                 if m is not None:
731                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
732                                         else:
733                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
734                                 if item['name'] != None and item['type'] == 'postprocess':
735                                         ret.append(item)
736         return ret
737
738 def runPostProcessingPlugins(gcodefilename):
739         pluginConfigList = getPluginConfig()
740         pluginList = getPluginList()
741         
742         for pluginConfig in pluginConfigList:
743                 plugin = None
744                 for pluginTest in pluginList:
745                         if pluginTest['filename'] == pluginConfig['filename']:
746                                 plugin = pluginTest
747                 if plugin is None:
748                         continue
749                 
750                 pythonFile = None
751                 for basePath in getPluginBasePaths():
752                         testFilename = os.path.join(basePath, pluginConfig['filename'])
753                         if os.path.isfile(testFilename):
754                                 pythonFile = testFilename
755                 if pythonFile is None:
756                         continue
757                 
758                 locals = {'filename': gcodefilename}
759                 for param in plugin['params']:
760                         value = param['default']
761                         if param['name'] in pluginConfig['params']:
762                                 value = pluginConfig['params'][param['name']]
763                         
764                         if param['type'] == 'float':
765                                 try:
766                                         value = float(value)
767                                 except:
768                                         value = float(param['default'])
769                         
770                         locals[param['name']] = value
771                 try:
772                         execfile(pythonFile, locals)
773                 except:
774                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
775                         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])
776         return None
777
778 def getSDcardDrives():
779         drives = ['']
780         if platform.system() == "Windows":
781                 from ctypes import windll
782                 bitmask = windll.kernel32.GetLogicalDrives()
783                 for letter in string.uppercase:
784                         if bitmask & 1:
785                                 drives.append(letter + ':/')
786                         bitmask >>= 1
787         if platform.system() == "Darwin":
788                 drives = []
789                 for volume in glob.glob('/Volumes/*'):
790                         if stat.S_ISLNK(os.lstat(volume).st_mode):
791                                 continue
792                         drives.append(volume)
793         return drives