chiark / gitweb /
Add ooze-shield and wipe-tower settings.
[cura.git] / Cura / util / profile.py
1 from __future__ import absolute_import
2 from __future__ import division
3 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
4
5 import os, traceback, math, re, zlib, base64, time, sys, platform, glob, string, stat, types
6 import cPickle as pickle
7 if sys.version_info[0] < 3:
8         import ConfigParser
9 else:
10         import configparser as ConfigParser
11
12 from Cura.util import resources
13 from Cura.util import version
14 from Cura.util import validators
15
16 #The settings dictionary contains a key/value reference to all possible settings. With the setting name as key.
17 settingsDictionary = {}
18 #The settings list is used to keep a full list of all the settings. This is needed to keep the settings in the proper order,
19 # as the dictionary will not contain insertion order.
20 settingsList = []
21
22 #Currently selected machine (by index) Cura support multiple machines in the same preferences and can switch between them.
23 # Each machine has it's own index and unique name.
24 _selectedMachineIndex = 0
25
26 class setting(object):
27         #A setting object contains a configuration setting. These are globally accessible trough the quick access functions
28         # and trough the settingsDictionary function.
29         # Settings can be:
30         # * profile settings (settings that effect the slicing process and the print result)
31         # * preferences (settings that effect how cura works and acts)
32         # * machine settings (settings that relate to the physical configuration of your machine)
33         # * alterations (bad name copied from Skeinforge. These are the start/end code pieces)
34         # Settings have validators that check if the value is valid, but do not prevent invalid values!
35         # Settings have conditions that enable/disable this setting depending on other settings. (Ex: Dual-extrusion)
36         def __init__(self, name, default, type, category, subcategory):
37                 self._name = name
38                 self._label = name
39                 self._tooltip = ''
40                 self._default = unicode(default)
41                 self._values = []
42                 self._type = type
43                 self._category = category
44                 self._subcategory = subcategory
45                 self._validators = []
46                 self._conditions = []
47
48                 if type is types.FloatType:
49                         validators.validFloat(self)
50                 elif type is types.IntType:
51                         validators.validInt(self)
52
53                 global settingsDictionary
54                 settingsDictionary[name] = self
55                 global settingsList
56                 settingsList.append(self)
57
58         def setLabel(self, label, tooltip = ''):
59                 self._label = label
60                 self._tooltip = tooltip
61                 return self
62
63         def setRange(self, minValue=None, maxValue=None):
64                 if len(self._validators) < 1:
65                         return
66                 self._validators[0].minValue = minValue
67                 self._validators[0].maxValue = maxValue
68                 return self
69
70         def getLabel(self):
71                 return _(self._label)
72
73         def getTooltip(self):
74                 return _(self._tooltip)
75
76         def getCategory(self):
77                 return self._category
78
79         def getSubCategory(self):
80                 return self._subcategory
81
82         def isPreference(self):
83                 return self._category == 'preference'
84
85         def isMachineSetting(self):
86                 return self._category == 'machine'
87
88         def isAlteration(self):
89                 return self._category == 'alteration'
90
91         def isProfile(self):
92                 return not self.isAlteration() and not self.isPreference() and not self.isMachineSetting()
93
94         def getName(self):
95                 return self._name
96
97         def getType(self):
98                 return self._type
99
100         def getValue(self, index = None):
101                 if index is None:
102                         index = self.getValueIndex()
103                 if index >= len(self._values):
104                         return self._default
105                 return self._values[index]
106
107         def getDefault(self):
108                 return self._default
109
110         def setValue(self, value, index = None):
111                 if index is None:
112                         index = self.getValueIndex()
113                 while index >= len(self._values):
114                         self._values.append(self._default)
115                 self._values[index] = unicode(value)
116
117         def getValueIndex(self):
118                 if self.isMachineSetting():
119                         global _selectedMachineIndex
120                         return _selectedMachineIndex
121                 return 0
122
123         def validate(self):
124                 result = validators.SUCCESS
125                 msgs = []
126                 for validator in self._validators:
127                         res, err = validator.validate()
128                         if res == validators.ERROR:
129                                 result = res
130                         elif res == validators.WARNING and result != validators.ERROR:
131                                 result = res
132                         if res != validators.SUCCESS:
133                                 msgs.append(err)
134                 return result, '\n'.join(msgs)
135
136         def addCondition(self, conditionFunction):
137                 self._conditions.append(conditionFunction)
138
139         def checkConditions(self):
140                 for condition in self._conditions:
141                         if not condition():
142                                 return False
143                 return True
144
145 #########################################################
146 ## Settings
147 #########################################################
148
149 #Define a fake _() function to fake the gettext tools in to generating strings for the profile settings.
150 def _(n):
151         return n
152
153 setting('layer_height',              0.1, float, 'basic',    _('Quality')).setRange(0.0001).setLabel(_("Layer height (mm)"), _("Layer height in millimeters.\nThis is the most important setting to determine the quality of your print. Normal quality prints are 0.1mm, high quality is 0.06mm. You can go up to 0.25mm with an Ultimaker for very fast prints at low quality."))
154 setting('wall_thickness',            0.8, float, 'basic',    _('Quality')).setRange(0.0).setLabel(_("Shell thickness (mm)"), _("Thickness of the outside shell in the horizontal direction.\nThis is used in combination with the nozzle size to define the number\nof perimeter lines and the thickness of those perimeter lines."))
155 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."))
156 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 multiple of the layer thickness makes sense. And keep it near your wall thickness to make an evenly strong part."))
157 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 affect the outside of the print and only adjusts how strong the part becomes."))
158 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."))
159 setting('print_speed',                50, float, 'basic',    _('Speed and 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."))
160 setting('print_temperature',         220, int,   'basic',    _('Speed and 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."))
161 setting('print_temperature2',          0, int,   'basic',    _('Speed and 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."))
162 setting('print_temperature3',          0, int,   'basic',    _('Speed and 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."))
163 setting('print_temperature4',          0, int,   'basic',    _('Speed and 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."))
164 setting('print_bed_temperature',      70, int,   'basic',    _('Speed and Temperature')).setRange(0,340).setLabel(_("Bed temperature (C)"), _("Temperature used for the heated printer bed. Set at 0 to pre-heat yourself."))
165 setting('support',                'None', [_('None'), _('Touching buildplate'), _('Everywhere')], 'basic', _('Support')).setLabel(_("Support type"), _("Type of support structure build.\n\"Touching buildplate\" 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."))
166 setting('platform_adhesion',      'None', [_('None'), _('Brim'), _('Raft')], 'basic', _('Support')).setLabel(_("Platform adhesion type"), _("Different options that help in preventing corners from lifting due to warping.\nBrim adds a single layer thick flat area around your object which is easy to cut off afterwards, and the recommended option.\nRaft adds a thick raster at below the object and a thin interface between this and your object.\n(Note that enabling the brim or raft disables the skirt)"))
167 setting('support_dual_extrusion',  'Both', [_('Both'), _('First extruder'), _('Second extruder')], 'basic', _('Support')).setLabel(_("Support dual extrusion"), _("Which extruder to use for support material, for break-away support you can use both extruders.\nBut if one of the materials is more expensive then the other you could select an extruder to use for support material. This causes more extruder switches.\nYou can also use the 2nd extruder for soluble support materials."))
168 setting('wipe_tower',              False, bool,  'basic',    _('Dual extrusion')).setLabel(_("Wipe tower"), _("The wipe-tower is a tower printed on every layer when switching between nozzles.\nThe old nozzle is wiped off on the tower before the new nozzle is used to print the 2nd color."))
169 setting('ooze_shield',             False, bool,  'basic',    _('Dual extrusion')).setLabel(_("Ooze shield"), _("The ooze shield is a 1 line thick shell around the object which stands a few mm from the object.\nThis shield catches any oozing from the unused nozzle in dual-extrusion."))
170 setting('filament_diameter',        2.85, 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."))
171 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."))
172 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."))
173 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."))
174 setting('filament_flow',            100., float, 'basic',    _('Filament')).setRange(5,300).setLabel(_("Flow (%)"), _("Flow compensation, the amount of material extruded is multiplied by this value"))
175 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."))
176 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 4.5mm seems to generate good results."))
177 setting('retraction_dual_amount',   16.5, float, 'advanced', _('Retraction')).setRange(0).setLabel(_("Dual extrusion switch amount (mm)"), _("Amount of retraction when switching nozzle with dual-extrusion, set at 0 for no retraction at all. A value of 16.0mm seems to generate good results."))
178 setting('retraction_min_travel',     1.5, float, 'expert',   _('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."))
179 setting('retraction_combing',       True, bool,  'expert',   _('Retraction')).setLabel(_("Enable combing"), _("Combing is the act of avoiding holes in the print for the head to travel over. If combing is disabled the printer head moves straight from the start point to the end point and it will always retract."))
180 setting('retraction_minimal_extrusion',0.5, float,'expert',  _('Retraction')).setRange(0).setLabel(_("Minimal extrusion before retracting (mm)"), _("The minimal amount of extrusion that needs to be done before retracting again if a retraction needs to happen before this minimal is reached the retraction is ignored.\nThis avoids retraction a lot on the same piece of filament which flattens the filament and causes grinding issues."))
181 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."))
182 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."))
183 #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."))
184 setting('overlap_dual',              0.2, float, 'advanced', _('Quality')).setLabel(_("Dual extrusion overlap (mm)"), _("Add a certain amount of overlapping extrusion on dual-extrusion prints. This bonds the different colors better together."))
185 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."))
186 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."))
187 setting('infill_speed',              0.0, float, 'advanced', _('Speed')).setRange(0.0).setLabel(_("Infill speed (mm/s)"), _("Speed at which infill parts are printed. If set to 0 then the print speed is used for the infill. Printing the infill faster can greatly reduce printing, but this can negatively effect print quality.."))
188 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."))
189 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 essential during faster prints."))
190
191 setting('skirt_line_count',            1, int,   'expert', '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."))
192 setting('skirt_gap',                 3.0, float, 'expert', '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."))
193 setting('skirt_minimal_length',    150.0, float, 'expert', 'Skirt').setRange(0).setLabel(_("Minimal length (mm)"), _("The minimal length of the skirt, if this minimal length is not reached it will add more skirt lines to reach this minimal lenght.\nNote: If the line count is set to 0 this is ignored."))
194 #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."))
195 #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.'))
196 setting('fan_full_height',           0.5, float, 'expert',   _('Cool')).setRange(0).setLabel(_("Fan full on at height"), _("The height at which the fan is turned on completely. For the layers below this the fan speed is scaled linear with the fan off at layer 0."))
197 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."))
198 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 than 200%."))
199 setting('cool_min_feedrate',          10, float, 'expert',   _('Cool')).setRange(0).setLabel(_("Minimum speed (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 than this minimal speed."))
200 setting('cool_head_lift',          False, bool,  'expert',   _('Cool')).setLabel(_("Cool head lift"), _("Lift the head if the minimal speed is hit because of cool slowdown, and wait the extra time so the minimal layer time is always hit."))
201 #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."))
202 #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')
203 #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\''))
204 #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."))
205 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."))
206 setting('solid_bottom', True, bool, 'expert', _('Infill')).setLabel(_("Solid infill bottom"), _("Create a solid bottom surface, if set to false the bottom is filled with the fill percentage. Useful for buildings."))
207 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."))
208 setting('support_fill_rate', 15, int, 'expert', _('Support')).setRange(0,100).setLabel(_("Fill amount (%)"), _("Amount of infill structure in the support material, less material gives weaker support which is easier to remove. 15% seems to be a good average."))
209 setting('support_xy_distance', 0.7, float, 'expert', _('Support')).setRange(0,10).setLabel(_("Distance X/Y (mm)"), _("Distance of the support material from the print, in the X/Y directions.\n0.7mm gives a nice distance from the print so the support does not stick to the print."))
210 setting('support_z_distance', 0.15, float, 'expert', _('Support')).setRange(0,10).setLabel(_("Distance Z (mm)"), _("Distance from the top/bottom of the support to the print. A small gap here makes it easier to remove the support but makes the print a bit uglier.\n0.15mm gives a good seperation of the support material."))
211 setting('spiralize', False, bool, 'expert', 'Spiralize').setLabel(_("Spiralize the outer contour"), _("Spiralize is smoothing out the Z move of the outer edge. This will create a steady Z increase over the whole print. This feature turns a solid object into a single walled print with a solid bottom."))
212 #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."))
213 setting('brim_line_count', 20, int, 'expert', _('Brim')).setRange(1,100).setLabel(_("Brim line amount"), _("The amount of lines used for a brim, more lines means a larger brim which sticks better, but this also makes your effective print area smaller."))
214 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 while using more material and leaving less are for your print."))
215 setting('raft_line_spacing', 1.0, float, 'expert', _('Raft')).setRange(0).setLabel(_("Line spacing (mm)"), _("When you are using the raft this is the distance between the centerlines of the raft line."))
216 setting('raft_base_thickness', 0.3, float, 'expert', _('Raft')).setRange(0).setLabel(_("Base thickness (mm)"), _("When you are using the raft this is the thickness of the base layer which is put down."))
217 setting('raft_base_linewidth', 0.7, float, 'expert', _('Raft')).setRange(0).setLabel(_("Base line width (mm)"), _("When you are using the raft this is the width of the base layer lines which are put down."))
218 setting('raft_interface_thickness', 0.2, float, 'expert', _('Raft')).setRange(0).setLabel(_("Interface thickness (mm)"), _("When you are using the raft this is the thickness of the interface layer which is put down."))
219 setting('raft_interface_linewidth', 0.2, float, 'expert', _('Raft')).setRange(0).setLabel(_("Interface line width (mm)"), _("When you are using the raft this is the width of the interface layer lines which are put down."))
220 #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)."))
221 setting('fix_horrible_union_all_type_a', False, bool, 'expert', _('Fix horrible')).setLabel(_("Combine everything (Type-A)"), _("This expert option adds all parts of the model together. The result is usually that internal cavities disappear. Depending on the model this can be intended or not. Enabling this option is at your own risk. Type-A is depended on the model normals and tries to keep some internal holes intact. Type-B ignores all internal holes and only keeps the outside shape per layer."))
222 setting('fix_horrible_union_all_type_b', False, bool, 'expert', _('Fix horrible')).setLabel(_("Combine everything (Type-B)"), _("This expert option adds all parts of the model together. The result is usually that internal cavities disappear. Depending on the model this can be intended or not. Enabling this option is at your own risk. Type-A is depended on the model normals and tries to keep some internal holes intact. Type-B ignores all internal holes and only keeps the outside shape per layer."))
223 setting('fix_horrible_use_open_bits', False, bool, 'expert', _('Fix horrible')).setLabel(_("Keep open faces"), _("This expert option keeps all the open bits of the model intact. Normally Cura tries to stitch up small holes and remove everything with big holes, but this option keeps bits that are not properly part of anything and just goes with whatever it is left. This option is usually not what you want, but it might enable you to slice models otherwise failing to produce proper paths.\nAs with all \"Fix horrible\" options, results may vary and use at your own risk."))
224 setting('fix_horrible_extensive_stitching', False, bool, 'expert', _('Fix horrible')).setLabel(_("Extensive stitching"), _("Extrensive stitching tries to fix up open holes in the model by closing the hole with touching polygons. This algorthm is quite expensive and could introduce a lot of processing time.\nAs with all \"Fix horrible\" options, results may vary and use at your own risk."))
225
226 setting('plugin_config', '', str, 'hidden', 'hidden')
227 setting('object_center_x', -1, float, 'hidden', 'hidden')
228 setting('object_center_y', -1, float, 'hidden', 'hidden')
229
230 setting('start.gcode', """;Sliced at: {day} {date} {time}
231 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
232 ;Print time: {print_time}
233 ;Filament used: {filament_amount}m {filament_weight}g
234 ;Filament cost: {filament_cost}
235 ;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line
236 ;M109 S{print_temperature} ;Uncomment to add your own temperature line
237 G21        ;metric values
238 G90        ;absolute positioning
239 M107       ;start with the fan off
240
241 G28 X0 Y0  ;move X/Y to min endstops
242 G28 Z0     ;move Z to min endstops
243
244 G1 Z15.0 F{travel_speed} ;move the platform down 15mm
245
246 G92 E0                  ;zero the extruded length
247 G1 F200 E3              ;extrude 3mm of feed stock
248 G92 E0                  ;zero the extruded length again
249 G1 F{travel_speed}
250 ;Put printing message on LCD screen
251 M117 Printing...
252 """, str, 'alteration', 'alteration')
253 #######################################################################################
254 setting('end.gcode', """;End GCode
255 M104 S0                     ;extruder heater off
256 M140 S0                     ;heated bed heater off (if you have it)
257
258 G91                                    ;relative positioning
259 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
260 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
261 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
262
263 M84                         ;steppers off
264 G90                         ;absolute positioning
265 """, str, 'alteration', 'alteration')
266 #######################################################################################
267 setting('start2.gcode', """;Sliced at: {day} {date} {time}
268 ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}
269 ;Print time: {print_time}
270 ;Filament used: {filament_amount}m {filament_weight}g
271 ;Filament cost: {filament_cost}
272 ;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line
273 ;M104 S{print_temperature} ;Uncomment to add your own temperature line
274 ;M109 T1 S{print_temperature2} ;Uncomment to add your own temperature line
275 ;M109 T0 S{print_temperature} ;Uncomment to add your own temperature line
276 G21        ;metric values
277 G90        ;absolute positioning
278 M107       ;start with the fan off
279
280 G28 X0 Y0  ;move X/Y to min endstops
281 G28 Z0     ;move Z to min endstops
282
283 G1 Z15.0 F{travel_speed} ;move the platform down 15mm
284
285 T1                      ;Switch to the 2nd extruder
286 G92 E0                  ;zero the extruded length
287 G1 F200 E10             ;extrude 10mm of feed stock
288 G92 E0                  ;zero the extruded length again
289 G1 F200 E-{retraction_dual_amount}
290
291 T0                      ;Switch to the first extruder
292 G92 E0                  ;zero the extruded length
293 G1 F200 E10             ;extrude 10mm of feed stock
294 G92 E0                  ;zero the extruded length again
295 G1 F{travel_speed}
296 ;Put printing message on LCD screen
297 M117 Printing...
298 """, str, 'alteration', 'alteration')
299 #######################################################################################
300 setting('end2.gcode', """;End GCode
301 M104 T0 S0                     ;extruder heater off
302 M104 T1 S0                     ;extruder heater off
303 M140 S0                     ;heated bed heater off (if you have it)
304
305 G91                                    ;relative positioning
306 G1 E-1 F300                            ;retract the filament a bit before lifting the nozzle, to release some of the pressure
307 G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more
308 G28 X0 Y0                              ;move X/Y to min endstops, so the head is out of the way
309
310 M84                         ;steppers off
311 G90                         ;absolute positioning
312 """, str, 'alteration', 'alteration')
313 #######################################################################################
314 setting('support_start.gcode', '', str, 'alteration', 'alteration')
315 setting('support_end.gcode', '', str, 'alteration', 'alteration')
316 setting('cool_start.gcode', '', str, 'alteration', 'alteration')
317 setting('cool_end.gcode', '', str, 'alteration', 'alteration')
318 setting('replace.csv', '', str, 'alteration', 'alteration')
319 #######################################################################################
320 setting('switchExtruder.gcode', """;Switch between the current extruder and the next extruder, when printing with multiple extruders.
321 G92 E0
322 G1 E-36 F5000
323 G92 E0
324 T{extruder}
325 G1 X{new_x} Y{new_y} Z{new_z} F{travel_speed}
326 G1 E36 F5000
327 G92 E0
328 """, str, 'alteration', 'alteration')
329
330 setting('startMode', 'Simple', ['Simple', 'Normal'], 'preference', 'hidden')
331 setting('lastFile', os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'example', 'UltimakerRobot_support.stl')), str, 'preference', 'hidden')
332 setting('save_profile', 'False', bool, 'preference', 'hidden').setLabel(_("Save profile on slice"), _("When slicing save the profile as [stl_file]_profile.ini next to the model."))
333 setting('filament_cost_kg', '0', float, 'preference', 'hidden').setLabel(_("Cost (price/kg)"), _("Cost of your filament per kg, to estimate the cost of the final print."))
334 setting('filament_cost_meter', '0', float, 'preference', 'hidden').setLabel(_("Cost (price/m)"), _("Cost of your filament per meter, to estimate the cost of the final print."))
335 setting('auto_detect_sd', 'True', bool, 'preference', 'hidden').setLabel(_("Auto detect SD card drive"), _("Auto detect the SD card. You can disable this because on some systems external hard-drives or USB sticks are detected as SD card."))
336 setting('check_for_updates', 'True', bool, 'preference', 'hidden').setLabel(_("Check for updates"), _("Check for newer versions of Cura on startup"))
337 setting('submit_slice_information', 'False', bool, 'preference', 'hidden').setLabel(_("Send usage statistics"), _("Submit anonymous usage information to improve next versions of Cura"))
338 setting('youmagine_token', '', str, 'preference', 'hidden')
339 setting('filament_physical_density', '1240', float, 'preference', 'hidden').setRange(500.0, 3000.0).setLabel(_("Density (kg/m3)"), _("Weight of the filament per m3. Around 1240 for PLA. And around 1040 for ABS. This value is used to estimate the weight if the filament used for the print."))
340 setting('language', 'English', str, 'preference', 'hidden').setLabel(_('Language'), _('Change the language in which Cura runs. Switching language requires a restart of Cura'))
341 setting('active_machine', '0', int, 'preference', 'hidden')
342
343 setting('model_colour', '#FFC924', str, 'preference', 'hidden').setLabel(_('Model colour'))
344 setting('model_colour2', '#CB3030', str, 'preference', 'hidden').setLabel(_('Model colour (2)'))
345 setting('model_colour3', '#DDD93C', str, 'preference', 'hidden').setLabel(_('Model colour (3)'))
346 setting('model_colour4', '#4550D3', str, 'preference', 'hidden').setLabel(_('Model colour (4)'))
347
348 setting('window_maximized', 'True', bool, 'preference', 'hidden')
349 setting('window_pos_x', '-1', float, 'preference', 'hidden')
350 setting('window_pos_y', '-1', float, 'preference', 'hidden')
351 setting('window_width', '-1', float, 'preference', 'hidden')
352 setting('window_height', '-1', float, 'preference', 'hidden')
353 setting('window_normal_sash', '320', float, 'preference', 'hidden')
354
355 setting('machine_name', '', str, 'machine', 'hidden')
356 setting('machine_type', 'unknown', str, 'machine', 'hidden') #Ultimaker, Ultimaker2, RepRap
357 setting('machine_width', '205', float, 'machine', 'hidden').setLabel(_("Maximum width (mm)"), _("Size of the machine in mm"))
358 setting('machine_depth', '205', float, 'machine', 'hidden').setLabel(_("Maximum depth (mm)"), _("Size of the machine in mm"))
359 setting('machine_height', '200', float, 'machine', 'hidden').setLabel(_("Maximum height (mm)"), _("Size of the machine in mm"))
360 setting('machine_center_is_zero', 'False', bool, 'machine', 'hidden').setLabel(_("Machine center 0,0"), _("Machines firmware defines the center of the bed as 0,0 instead of the front left corner."))
361 setting('ultimaker_extruder_upgrade', 'False', bool, 'machine', 'hidden')
362 setting('has_heated_bed', 'False', bool, 'machine', 'hidden').setLabel(_("Heated bed"), _("If you have an heated bed, this enabled heated bed settings (requires restart)"))
363 setting('gcode_flavor', 'RepRap (Marlin/Sprinter)', ['RepRap (Marlin/Sprinter)', 'UltiGCode', 'MakerBot'], 'machine', 'hidden').setLabel(_("GCode Flavor"), _("Flavor of generated GCode.\nRepRap is normal 5D GCode which works on Marlin/Sprinter based firmwares.\nUltiGCode is a variation of the RepRap GCode which puts more settings in the machine instead of the slicer.\nMakerBot GCode has a few changes in the way GCode is generated, but still requires MakerWare to generate to X3G."))
364 setting('extruder_amount', '1', ['1','2','3','4'], 'machine', 'hidden').setLabel(_("Extruder count"), _("Amount of extruders in your machine."))
365 setting('extruder_offset_x1', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your secondary extruder compared to the primary."))
366 setting('extruder_offset_y1', '-21.6', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your secondary extruder compared to the primary."))
367 setting('extruder_offset_x2', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your tertiary extruder compared to the primary."))
368 setting('extruder_offset_y2', '0.0', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your tertiary extruder compared to the primary."))
369 setting('extruder_offset_x3', '0.0', float, 'machine', 'hidden').setLabel(_("Offset X"), _("The offset of your forth extruder compared to the primary."))
370 setting('extruder_offset_y3', '0.0', float, 'machine', 'hidden').setLabel(_("Offset Y"), _("The offset of your forth extruder compared to the primary."))
371 setting('steps_per_e', '0', float, 'machine', 'hidden').setLabel(_("E-Steps per 1mm filament"), _("Amount of steps per mm filament extrusion. If set to 0 then this value is ignored and the value in your firmware is used."))
372 setting('serial_port', 'AUTO', str, 'machine', 'hidden').setLabel(_("Serial port"), _("Serial port to use for communication with the printer"))
373 setting('serial_port_auto', '', str, 'machine', 'hidden')
374 setting('serial_baud', 'AUTO', str, 'machine', 'hidden').setLabel(_("Baudrate"), _("Speed of the serial port communication\nNeeds to match your firmware settings\nCommon values are 250000, 115200, 57600"))
375 setting('serial_baud_auto', '', int, 'machine', 'hidden')
376
377 setting('extruder_head_size_min_x', '0.0', float, 'machine', 'hidden').setLabel(_("Head size towards X min (mm)"), _("The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 75mm for an Ultimaker if the fan is on the left side."))
378 setting('extruder_head_size_min_y', '0.0', float, 'machine', 'hidden').setLabel(_("Head size towards Y min (mm)"), _("The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 18mm for an Ultimaker if the fan is on the left side."))
379 setting('extruder_head_size_max_x', '0.0', float, 'machine', 'hidden').setLabel(_("Head size towards X max (mm)"), _("The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 18mm for an Ultimaker if the fan is on the left side."))
380 setting('extruder_head_size_max_y', '0.0', float, 'machine', 'hidden').setLabel(_("Head size towards Y max (mm)"), _("The head size when printing multiple objects, measured from the tip of the nozzle towards the outer part of the head. 35mm for an Ultimaker if the fan is on the left side."))
381 setting('extruder_head_size_height', '0.0', float, 'machine', 'hidden').setLabel(_("Printer gantry height (mm)"), _("The height of the gantry holding up the printer head. If an object is higher then this then you cannot print multiple objects one for one. 60mm for an Ultimaker."))
382
383 validators.warningAbove(settingsDictionary['filament_flow'], 150, _("More flow than 150% is rare and usually not recommended."))
384 validators.warningBelow(settingsDictionary['filament_flow'], 50, _("Less flow than 50% is rare and usually not recommended."))
385 validators.warningAbove(settingsDictionary['layer_height'], lambda : (float(getProfileSetting('nozzle_size')) * 80.0 / 100.0), _("Thicker layers then %.2fmm (80%% nozzle size) usually give bad results and are not recommended."))
386 validators.wallThicknessValidator(settingsDictionary['wall_thickness'])
387 validators.warningAbove(settingsDictionary['print_speed'], 150.0, _("It is highly unlikely that your machine can achieve a printing speed above 150mm/s"))
388 validators.printSpeedValidator(settingsDictionary['print_speed'])
389 validators.warningAbove(settingsDictionary['print_temperature'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
390 validators.warningAbove(settingsDictionary['print_temperature2'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
391 validators.warningAbove(settingsDictionary['print_temperature3'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
392 validators.warningAbove(settingsDictionary['print_temperature4'], 260.0, _("Temperatures above 260C could damage your machine, be careful!"))
393 validators.warningAbove(settingsDictionary['filament_diameter'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
394 validators.warningAbove(settingsDictionary['filament_diameter2'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
395 validators.warningAbove(settingsDictionary['filament_diameter3'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
396 validators.warningAbove(settingsDictionary['filament_diameter4'], 3.5, _("Are you sure your filament is that thick? Normal filament is around 3mm or 1.75mm."))
397 validators.warningAbove(settingsDictionary['travel_speed'], 300.0, _("It is highly unlikely that your machine can achieve a travel speed above 300mm/s"))
398 validators.warningAbove(settingsDictionary['bottom_thickness'], lambda : (float(getProfileSetting('nozzle_size')) * 3.0 / 4.0), _("A bottom layer of more then %.2fmm (3/4 nozzle size) usually give bad results and is not recommended."))
399
400 #Conditions for multiple extruders
401 settingsDictionary['print_temperature2'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
402 settingsDictionary['print_temperature3'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 2)
403 settingsDictionary['print_temperature4'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 3)
404 settingsDictionary['filament_diameter2'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
405 settingsDictionary['filament_diameter3'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 2)
406 settingsDictionary['filament_diameter4'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 3)
407 settingsDictionary['support_dual_extrusion'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
408 settingsDictionary['retraction_dual_amount'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
409 settingsDictionary['wipe_tower'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
410 settingsDictionary['ooze_shield'].addCondition(lambda : int(getMachineSetting('extruder_amount')) > 1)
411 #Heated bed
412 settingsDictionary['print_bed_temperature'].addCondition(lambda : getMachineSetting('has_heated_bed') == 'True')
413
414 #UltiGCode uses less settings, as these settings are located inside the machine instead of gcode.
415 settingsDictionary['print_temperature'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
416 settingsDictionary['print_temperature2'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
417 settingsDictionary['print_temperature3'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
418 settingsDictionary['print_temperature4'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
419 settingsDictionary['filament_diameter'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
420 settingsDictionary['filament_diameter2'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
421 settingsDictionary['filament_diameter3'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
422 settingsDictionary['filament_diameter4'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
423 settingsDictionary['filament_flow'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
424 settingsDictionary['print_bed_temperature'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
425 settingsDictionary['retraction_speed'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
426 settingsDictionary['retraction_amount'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
427 settingsDictionary['retraction_dual_amount'].addCondition(lambda : getMachineSetting('gcode_flavor') != 'UltiGCode')
428
429 #Remove fake defined _() because later the localization will define a global _()
430 del _
431
432 #########################################################
433 ## Profile and preferences functions
434 #########################################################
435
436 def getSubCategoriesFor(category):
437         done = {}
438         ret = []
439         for s in settingsList:
440                 if s.getCategory() == category and not s.getSubCategory() in done and s.checkConditions():
441                         done[s.getSubCategory()] = True
442                         ret.append(s.getSubCategory())
443         return ret
444
445 def getSettingsForCategory(category, subCategory = None):
446         ret = []
447         for s in settingsList:
448                 if s.getCategory() == category and (subCategory is None or s.getSubCategory() == subCategory) and s.checkConditions():
449                         ret.append(s)
450         return ret
451
452 ## Profile functions
453 def getBasePath():
454         if platform.system() == "Windows":
455                 basePath = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
456                 #If we have a frozen python install, we need to step out of the library.zip
457                 if hasattr(sys, 'frozen'):
458                         basePath = os.path.normpath(os.path.join(basePath, ".."))
459         else:
460                 basePath = os.path.expanduser('~/.cura/%s' % version.getVersion(False))
461         if not os.path.isdir(basePath):
462                 os.makedirs(basePath)
463         return basePath
464
465 def getAlternativeBasePaths():
466         paths = []
467         basePath = os.path.normpath(os.path.join(getBasePath(), '..'))
468         for subPath in os.listdir(basePath):
469                 path = os.path.join(basePath, subPath)
470                 if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'preferences.ini')) and path != getBasePath():
471                         paths.append(path)
472                 path = os.path.join(basePath, subPath, 'Cura')
473                 if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'preferences.ini')) and path != getBasePath():
474                         paths.append(path)
475         return paths
476
477 def getDefaultProfilePath():
478         return os.path.join(getBasePath(), 'current_profile.ini')
479
480 def loadProfile(filename):
481         #Read a configuration file as global config
482         profileParser = ConfigParser.ConfigParser()
483         try:
484                 profileParser.read(filename)
485         except ConfigParser.ParsingError:
486                 return
487         global settingsList
488         for set in settingsList:
489                 if set.isPreference():
490                         continue
491                 section = 'profile'
492                 if set.isAlteration():
493                         section = 'alterations'
494                 if profileParser.has_option(section, set.getName()):
495                         set.setValue(unicode(profileParser.get(section, set.getName()), 'utf-8', 'replace'))
496
497 def saveProfile(filename):
498         #Save the current profile to an ini file
499         profileParser = ConfigParser.ConfigParser()
500         profileParser.add_section('profile')
501         profileParser.add_section('alterations')
502         global settingsList
503         for set in settingsList:
504                 if set.isPreference() or set.isMachineSetting():
505                         continue
506                 if set.isAlteration():
507                         profileParser.set('alterations', set.getName(), set.getValue().encode('utf-8'))
508                 else:
509                         profileParser.set('profile', set.getName(), set.getValue().encode('utf-8'))
510
511         profileParser.write(open(filename, 'w'))
512
513 def resetProfile():
514         #Read a configuration file as global config
515         global settingsList
516         for set in settingsList:
517                 if not set.isProfile():
518                         continue
519                 set.setValue(set.getDefault())
520
521         if getMachineSetting('machine_type') == 'ultimaker':
522                 putProfileSetting('nozzle_size', '0.4')
523                 if getMachineSetting('ultimaker_extruder_upgrade') == 'True':
524                         putProfileSetting('retraction_enable', 'True')
525         elif getMachineSetting('machine_type') == 'ultimaker2':
526                 putProfileSetting('nozzle_size', '0.4')
527                 putProfileSetting('retraction_enable', 'True')
528         else:
529                 putProfileSetting('nozzle_size', '0.5')
530                 putProfileSetting('retraction_enable', 'True')
531
532 def setProfileFromString(options):
533         options = base64.b64decode(options)
534         options = zlib.decompress(options)
535         (profileOpts, alt) = options.split('\f', 1)
536         global settingsDictionary
537         for option in profileOpts.split('\b'):
538                 if len(option) > 0:
539                         (key, value) = option.split('=', 1)
540                         if key in settingsDictionary:
541                                 if settingsDictionary[key].isProfile():
542                                         settingsDictionary[key].setValue(value)
543         for option in alt.split('\b'):
544                 if len(option) > 0:
545                         (key, value) = option.split('=', 1)
546                         if key in settingsDictionary:
547                                 if settingsDictionary[key].isAlteration():
548                                         settingsDictionary[key].setValue(value)
549
550 def getProfileString():
551         p = []
552         alt = []
553         global settingsList
554         for set in settingsList:
555                 if set.isProfile():
556                         if set.getName() in tempOverride:
557                                 p.append(set.getName() + "=" + tempOverride[set.getName()])
558                         else:
559                                 p.append(set.getName() + "=" + set.getValue())
560                 elif set.isAlteration():
561                         if set.getName() in tempOverride:
562                                 alt.append(set.getName() + "=" + tempOverride[set.getName()])
563                         else:
564                                 alt.append(set.getName() + "=" + set.getValue())
565         ret = '\b'.join(p) + '\f' + '\b'.join(alt)
566         ret = base64.b64encode(zlib.compress(ret, 9))
567         return ret
568
569 def insertNewlines(string, every=64): #This should be moved to a better place then profile.
570         lines = []
571         for i in xrange(0, len(string), every):
572                 lines.append(string[i:i+every])
573         return '\n'.join(lines)
574
575 def getPreferencesString():
576         p = []
577         global settingsList
578         for set in settingsList:
579                 if set.isPreference():
580                         p.append(set.getName() + "=" + set.getValue())
581         ret = '\b'.join(p)
582         ret = base64.b64encode(zlib.compress(ret, 9))
583         return ret
584
585
586 def getProfileSetting(name):
587         if name in tempOverride:
588                 return tempOverride[name]
589         global settingsDictionary
590         if name in settingsDictionary and settingsDictionary[name].isProfile():
591                 return settingsDictionary[name].getValue()
592         traceback.print_stack()
593         sys.stderr.write('Error: "%s" not found in profile settings\n' % (name))
594         return ''
595
596 def getProfileSettingFloat(name):
597         try:
598                 setting = getProfileSetting(name).replace(',', '.')
599                 return float(eval(setting, {}, {}))
600         except:
601                 return 0.0
602
603 def putProfileSetting(name, value):
604         #Check if we have a configuration file loaded, else load the default.
605         global settingsDictionary
606         if name in settingsDictionary and settingsDictionary[name].isProfile():
607                 settingsDictionary[name].setValue(value)
608
609 def isProfileSetting(name):
610         global settingsDictionary
611         if name in settingsDictionary and settingsDictionary[name].isProfile():
612                 return True
613         return False
614
615 ## Preferences functions
616 def getPreferencePath():
617         return os.path.join(getBasePath(), 'preferences.ini')
618
619 def getPreferenceFloat(name):
620         try:
621                 setting = getPreference(name).replace(',', '.')
622                 return float(eval(setting, {}, {}))
623         except:
624                 return 0.0
625
626 def getPreferenceColour(name):
627         colorString = getPreference(name)
628         return [float(int(colorString[1:3], 16)) / 255, float(int(colorString[3:5], 16)) / 255, float(int(colorString[5:7], 16)) / 255, 1.0]
629
630 def loadPreferences(filename):
631         global settingsList
632         #Read a configuration file as global config
633         profileParser = ConfigParser.ConfigParser()
634         try:
635                 profileParser.read(filename)
636         except ConfigParser.ParsingError:
637                 return
638
639         for set in settingsList:
640                 if set.isPreference():
641                         if profileParser.has_option('preference', set.getName()):
642                                 set.setValue(unicode(profileParser.get('preference', set.getName()), 'utf-8', 'replace'))
643
644         n = 0
645         while profileParser.has_section('machine_%d' % (n)):
646                 for set in settingsList:
647                         if set.isMachineSetting():
648                                 if profileParser.has_option('machine_%d' % (n), set.getName()):
649                                         set.setValue(unicode(profileParser.get('machine_%d' % (n), set.getName()), 'utf-8', 'replace'), n)
650                 n += 1
651
652         setActiveMachine(int(getPreferenceFloat('active_machine')))
653
654 def loadMachineSettings(filename):
655         global settingsList
656         #Read a configuration file as global config
657         profileParser = ConfigParser.ConfigParser()
658         try:
659                 profileParser.read(filename)
660         except ConfigParser.ParsingError:
661                 return
662
663         for set in settingsList:
664                 if set.isMachineSetting():
665                         if profileParser.has_option('machine', set.getName()):
666                                 set.setValue(unicode(profileParser.get('machine', set.getName()), 'utf-8', 'replace'))
667         checkAndUpdateMachineName()
668
669 def savePreferences(filename):
670         global settingsList
671         #Save the current profile to an ini file
672         parser = ConfigParser.ConfigParser()
673         parser.add_section('preference')
674
675         for set in settingsList:
676                 if set.isPreference():
677                         parser.set('preference', set.getName(), set.getValue().encode('utf-8'))
678
679         n = 0
680         while getMachineSetting('machine_name', n) != '':
681                 parser.add_section('machine_%d' % (n))
682                 for set in settingsList:
683                         if set.isMachineSetting():
684                                 parser.set('machine_%d' % (n), set.getName(), set.getValue(n).encode('utf-8'))
685                 n += 1
686         parser.write(open(filename, 'w'))
687
688 def getPreference(name):
689         if name in tempOverride:
690                 return tempOverride[name]
691         global settingsDictionary
692         if name in settingsDictionary and settingsDictionary[name].isPreference():
693                 return settingsDictionary[name].getValue()
694         traceback.print_stack()
695         sys.stderr.write('Error: "%s" not found in preferences\n' % (name))
696         return ''
697
698 def putPreference(name, value):
699         #Check if we have a configuration file loaded, else load the default.
700         global settingsDictionary
701         if name in settingsDictionary and settingsDictionary[name].isPreference():
702                 settingsDictionary[name].setValue(value)
703                 savePreferences(getPreferencePath())
704                 return
705         traceback.print_stack()
706         sys.stderr.write('Error: "%s" not found in preferences\n' % (name))
707
708 def isPreference(name):
709         global settingsDictionary
710         if name in settingsDictionary and settingsDictionary[name].isPreference():
711                 return True
712         return False
713
714 def getMachineSettingFloat(name):
715         try:
716                 setting = getMachineSetting(name).replace(',', '.')
717                 return float(eval(setting, {}, {}))
718         except:
719                 return 0.0
720
721 def getMachineSetting(name, index = None):
722         if name in tempOverride:
723                 return tempOverride[name]
724         global settingsDictionary
725         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
726                 return settingsDictionary[name].getValue(index)
727         traceback.print_stack()
728         sys.stderr.write('Error: "%s" not found in machine settings\n' % (name))
729         return ''
730
731 def putMachineSetting(name, value):
732         #Check if we have a configuration file loaded, else load the default.
733         global settingsDictionary
734         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
735                 settingsDictionary[name].setValue(value)
736         savePreferences(getPreferencePath())
737
738 def isMachineSetting(name):
739         global settingsDictionary
740         if name in settingsDictionary and settingsDictionary[name].isMachineSetting():
741                 return True
742         return False
743
744 def checkAndUpdateMachineName():
745         global _selectedMachineIndex
746         name = getMachineSetting('machine_name')
747         index = None
748         if name == '':
749                 name = getMachineSetting('machine_type')
750         for n in xrange(0, getMachineCount()):
751                 if n == _selectedMachineIndex:
752                         continue
753                 if index is None:
754                         if name == getMachineSetting('machine_name', n):
755                                 index = 1
756                 else:
757                         if '%s (%d)' % (name, index) == getMachineSetting('machine_name', n):
758                                 index += 1
759         if index is not None:
760                 name = '%s (%d)' % (name, index)
761         putMachineSetting('machine_name', name)
762         putPreference('active_machine', _selectedMachineIndex)
763
764 def getMachineCount():
765         n = 0
766         while getMachineSetting('machine_name', n) != '':
767                 n += 1
768         if n < 1:
769                 return 1
770         return n
771
772 def setActiveMachine(index):
773         global _selectedMachineIndex
774         _selectedMachineIndex = index
775         putPreference('active_machine', _selectedMachineIndex)
776
777 def removeMachine(index):
778         global _selectedMachineIndex
779         global settingsList
780         if getMachineCount() < 2:
781                 return
782         for n in xrange(index, getMachineCount()):
783                 for setting in settingsList:
784                         if setting.isMachineSetting():
785                                 setting.setValue(setting.getValue(n+1), n)
786
787         if _selectedMachineIndex >= index:
788                 setActiveMachine(getMachineCount() - 1)
789
790 ## Temp overrides for multi-extruder slicing and the project planner.
791 tempOverride = {}
792 def setTempOverride(name, value):
793         tempOverride[name] = unicode(value).encode("utf-8")
794 def clearTempOverride(name):
795         del tempOverride[name]
796 def resetTempOverride():
797         tempOverride.clear()
798
799 #########################################################
800 ## Utility functions to calculate common profile values
801 #########################################################
802 def calculateEdgeWidth():
803         wallThickness = getProfileSettingFloat('wall_thickness')
804         nozzleSize = getProfileSettingFloat('nozzle_size')
805
806         if getProfileSetting('spiralize') == 'True':
807                 return wallThickness
808
809         if wallThickness < 0.01:
810                 return nozzleSize
811         if wallThickness < nozzleSize:
812                 return wallThickness
813
814         lineCount = int(wallThickness / (nozzleSize - 0.0001))
815         if lineCount == 0:
816                 return nozzleSize
817         lineWidth = wallThickness / lineCount
818         lineWidthAlt = wallThickness / (lineCount + 1)
819         if lineWidth > nozzleSize * 1.5:
820                 return lineWidthAlt
821         return lineWidth
822
823 def calculateLineCount():
824         wallThickness = getProfileSettingFloat('wall_thickness')
825         nozzleSize = getProfileSettingFloat('nozzle_size')
826
827         if wallThickness < 0.01:
828                 return 0
829         if wallThickness < nozzleSize:
830                 return 1
831         if getProfileSetting('spiralize') == 'True':
832                 return 1
833
834         lineCount = int(wallThickness / (nozzleSize - 0.0001))
835         if lineCount < 1:
836                 lineCount = 1
837         lineWidth = wallThickness / lineCount
838         lineWidthAlt = wallThickness / (lineCount + 1)
839         if lineWidth > nozzleSize * 1.5:
840                 return lineCount + 1
841         return lineCount
842
843 def calculateSolidLayerCount():
844         layerHeight = getProfileSettingFloat('layer_height')
845         solidThickness = getProfileSettingFloat('solid_layer_thickness')
846         if layerHeight == 0.0:
847                 return 1
848         return int(math.ceil(solidThickness / (layerHeight - 0.0001)))
849
850 def calculateObjectSizeOffsets():
851         size = 0.0
852
853         if getProfileSetting('platform_adhesion') == 'Brim':
854                 size += getProfileSettingFloat('brim_line_count') * calculateEdgeWidth()
855         elif getProfileSetting('platform_adhesion') == 'Raft':
856                 pass
857         else:
858                 if getProfileSettingFloat('skirt_line_count') > 0:
859                         size += getProfileSettingFloat('skirt_line_count') * calculateEdgeWidth() + getProfileSettingFloat('skirt_gap')
860
861         #if getProfileSetting('enable_raft') != 'False':
862         #       size += profile.getProfileSettingFloat('raft_margin') * 2
863         #if getProfileSetting('support') != 'None':
864         #       extraSizeMin = extraSizeMin + numpy.array([3.0, 0, 0])
865         #       extraSizeMax = extraSizeMax + numpy.array([3.0, 0, 0])
866         return [size, size]
867
868 def getMachineCenterCoords():
869         if getMachineSetting('machine_center_is_zero') == 'True':
870                 return [0, 0]
871         return [getMachineSettingFloat('machine_width') / 2, getMachineSettingFloat('machine_depth') / 2]
872
873 #########################################################
874 ## Alteration file functions
875 #########################################################
876 def replaceTagMatch(m):
877         pre = m.group(1)
878         tag = m.group(2)
879         if tag == 'time':
880                 return pre + time.strftime('%H:%M:%S')
881         if tag == 'date':
882                 return pre + time.strftime('%d-%m-%Y')
883         if tag == 'day':
884                 return pre + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
885         if tag == 'print_time':
886                 return pre + '#P_TIME#'
887         if tag == 'filament_amount':
888                 return pre + '#F_AMNT#'
889         if tag == 'filament_weight':
890                 return pre + '#F_WGHT#'
891         if tag == 'filament_cost':
892                 return pre + '#F_COST#'
893         if pre == 'F' and tag == 'max_z_speed':
894                 f = getProfileSettingFloat('travel_speed') * 60
895         if pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'bottom_layer_speed', 'cool_min_feedrate']:
896                 f = getProfileSettingFloat(tag) * 60
897         elif isProfileSetting(tag):
898                 f = getProfileSettingFloat(tag)
899         elif isPreference(tag):
900                 f = getProfileSettingFloat(tag)
901         else:
902                 return '%s?%s?' % (pre, tag)
903         if (f % 1) == 0:
904                 return pre + str(int(f))
905         return pre + str(f)
906
907 def replaceGCodeTags(filename, gcodeInt):
908         f = open(filename, 'r+')
909         data = f.read(2048)
910         data = data.replace('#P_TIME#', ('%5d:%02d' % (int(gcodeInt.totalMoveTimeMinute / 60), int(gcodeInt.totalMoveTimeMinute % 60)))[-8:])
911         data = data.replace('#F_AMNT#', ('%8.2f' % (gcodeInt.extrusionAmount / 1000))[-8:])
912         data = data.replace('#F_WGHT#', ('%8.2f' % (gcodeInt.calculateWeight() * 1000))[-8:])
913         cost = gcodeInt.calculateCost()
914         if cost is None:
915                 cost = 'Unknown'
916         data = data.replace('#F_COST#', ('%8s' % (cost.split(' ')[0]))[-8:])
917         f.seek(0)
918         f.write(data)
919         f.close()
920
921 ### Get aleration raw contents. (Used internally in Cura)
922 def getAlterationFile(filename):
923         if filename in tempOverride:
924                 return tempOverride[filename]
925         global settingsDictionary
926         if filename in settingsDictionary and settingsDictionary[filename].isAlteration():
927                 return settingsDictionary[filename].getValue()
928         traceback.print_stack()
929         sys.stderr.write('Error: "%s" not found in alteration settings\n' % (filename))
930         return ''
931
932 def setAlterationFile(name, value):
933         #Check if we have a configuration file loaded, else load the default.
934         global settingsDictionary
935         if name in settingsDictionary and settingsDictionary[name].isAlteration():
936                 settingsDictionary[name].setValue(value)
937         saveProfile(getDefaultProfilePath())
938
939 def isTagIn(tag, contents):
940         contents = re.sub(';[^\n]*\n', '', contents)
941         return tag in contents
942
943 ### Get the alteration file for output. (Used by Skeinforge)
944 def getAlterationFileContents(filename, extruderCount = 1):
945         prefix = ''
946         postfix = ''
947         alterationContents = getAlterationFile(filename)
948         if getMachineSetting('gcode_flavor') == 'UltiGCode':
949                 return ''
950         if filename == 'start.gcode':
951                 if extruderCount > 1:
952                         alterationContents = getAlterationFile("start%d.gcode" % (extruderCount))
953                 #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.
954                 #We also set our steps per E here, if configured.
955                 eSteps = getMachineSettingFloat('steps_per_e')
956                 if eSteps > 0:
957                         prefix += 'M92 E%f\n' % (eSteps)
958                 temp = getProfileSettingFloat('print_temperature')
959                 bedTemp = 0
960                 if getMachineSetting('has_heated_bed') == 'True':
961                         bedTemp = getProfileSettingFloat('print_bed_temperature')
962
963                 if bedTemp > 0 and not isTagIn('{print_bed_temperature}', alterationContents):
964                         prefix += 'M140 S%f\n' % (bedTemp)
965                 if temp > 0 and not isTagIn('{print_temperature}', alterationContents):
966                         if extruderCount > 0:
967                                 for n in xrange(1, extruderCount):
968                                         t = temp
969                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
970                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
971                                         prefix += 'M104 T%d S%f\n' % (n, t)
972                                 for n in xrange(0, extruderCount):
973                                         t = temp
974                                         if n > 0 and getProfileSettingFloat('print_temperature%d' % (n+1)) > 0:
975                                                 t = getProfileSettingFloat('print_temperature%d' % (n+1))
976                                         prefix += 'M109 T%d S%f\n' % (n, t)
977                                 prefix += 'T0\n'
978                         else:
979                                 prefix += 'M109 S%f\n' % (temp)
980                 if bedTemp > 0 and not isTagIn('{print_bed_temperature}', alterationContents):
981                         prefix += 'M190 S%f\n' % (bedTemp)
982         elif filename == 'end.gcode':
983                 if extruderCount > 1:
984                         alterationContents = getAlterationFile("end%d.gcode" % (extruderCount))
985                 #Append the profile string to the end of the GCode, so we can load it from the GCode file later.
986                 postfix = ';CURA_PROFILE_STRING:%s\n' % (getProfileString())
987         return unicode(prefix + re.sub("(.)\{([^\}]*)\}", replaceTagMatch, alterationContents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
988
989 ###### PLUGIN #####
990
991 def getPluginConfig():
992         try:
993                 return pickle.loads(str(getProfileSetting('plugin_config')))
994         except:
995                 return []
996
997 def setPluginConfig(config):
998         putProfileSetting('plugin_config', pickle.dumps(config))
999
1000 def getPluginBasePaths():
1001         ret = []
1002         if platform.system() != "Windows":
1003                 ret.append(os.path.expanduser('~/.cura/plugins/'))
1004         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
1005                 ret.append(os.path.normpath(os.path.join(resources.resourceBasePath, "Cura/plugins")))
1006         else:
1007                 ret.append(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins')))
1008         return ret
1009
1010 def getPluginList():
1011         ret = []
1012         for basePath in getPluginBasePaths():
1013                 for filename in glob.glob(os.path.join(basePath, '*.py')):
1014                         filename = os.path.basename(filename)
1015                         if filename.startswith('_'):
1016                                 continue
1017                         with open(os.path.join(basePath, filename), "r") as f:
1018                                 item = {'filename': filename, 'name': None, 'info': None, 'type': None, 'params': []}
1019                                 for line in f:
1020                                         line = line.strip()
1021                                         if not line.startswith('#'):
1022                                                 break
1023                                         line = line[1:].split(':', 1)
1024                                         if len(line) != 2:
1025                                                 continue
1026                                         if line[0].upper() == 'NAME':
1027                                                 item['name'] = line[1].strip()
1028                                         elif line[0].upper() == 'INFO':
1029                                                 item['info'] = line[1].strip()
1030                                         elif line[0].upper() == 'TYPE':
1031                                                 item['type'] = line[1].strip()
1032                                         elif line[0].upper() == 'DEPEND':
1033                                                 pass
1034                                         elif line[0].upper() == 'PARAM':
1035                                                 m = re.match('([a-zA-Z][a-zA-Z0-9_]*)\(([a-zA-Z_]*)(?::([^\)]*))?\) +(.*)', line[1].strip())
1036                                                 if m is not None:
1037                                                         item['params'].append({'name': m.group(1), 'type': m.group(2), 'default': m.group(3), 'description': m.group(4)})
1038                                         else:
1039                                                 print "Unknown item in effect meta data: %s %s" % (line[0], line[1])
1040                                 if item['name'] is not None and item['type'] == 'postprocess':
1041                                         ret.append(item)
1042         return ret
1043
1044 def runPostProcessingPlugins(gcodefilename):
1045         pluginConfigList = getPluginConfig()
1046         pluginList = getPluginList()
1047
1048         for pluginConfig in pluginConfigList:
1049                 plugin = None
1050                 for pluginTest in pluginList:
1051                         if pluginTest['filename'] == pluginConfig['filename']:
1052                                 plugin = pluginTest
1053                 if plugin is None:
1054                         continue
1055
1056                 pythonFile = None
1057                 for basePath in getPluginBasePaths():
1058                         testFilename = os.path.join(basePath, pluginConfig['filename'])
1059                         if os.path.isfile(testFilename):
1060                                 pythonFile = testFilename
1061                 if pythonFile is None:
1062                         continue
1063
1064                 locals = {'filename': gcodefilename}
1065                 for param in plugin['params']:
1066                         value = param['default']
1067                         if param['name'] in pluginConfig['params']:
1068                                 value = pluginConfig['params'][param['name']]
1069
1070                         if param['type'] == 'float':
1071                                 try:
1072                                         value = float(value)
1073                                 except:
1074                                         value = float(param['default'])
1075
1076                         locals[param['name']] = value
1077                 try:
1078                         execfile(pythonFile, locals)
1079                 except:
1080                         locationInfo = traceback.extract_tb(sys.exc_info()[2])[-1]
1081                         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])
1082         return None