From bc29211507a0ad61006e0507eeacc937b94db8f2 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 15 Mar 2021 19:29:48 +0200 Subject: [PATCH] - the GCode generation takes now into consideration the Toolchange X-Y parameter as a starting point - Milling Plugin - work on it; upgraded the form-to-data_storage methods --- CHANGELOG.md | 5 + appObjects/FlatCAMGeometry.py | 4 +- appPlugins/ToolDrilling.py | 2 +- appPlugins/ToolMilling.py | 232 +++++++++++++++++++++++++--------- appPlugins/ToolSolderPaste.py | 2 +- camlib.py | 30 ++++- 6 files changed, 201 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d015cb..8478edba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ CHANGELOG for FlatCAM beta ================================================= +15.03.2021 + +- the GCode generation takes now into consideration the Toolchange X-Y parameter as a starting point +- Milling Plugin - work on it; upgraded the form-to-data_storage methods + 14.03.2021 - Geometry Editor can now modify the edited tool diameter diff --git a/appObjects/FlatCAMGeometry.py b/appObjects/FlatCAMGeometry.py index 9235fb73..d99841d6 100644 --- a/appObjects/FlatCAMGeometry.py +++ b/appObjects/FlatCAMGeometry.py @@ -2388,7 +2388,7 @@ class GeometryObject(FlatCAMObj, Geometry): total_gcode += res self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) - dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse(tool_data=tools_dict[tooluid_key]['data']) app_obj.inform.emit('[success] %s' % _("G-Code parsing finished...")) # commented this; there is no need for the actual GCode geometry - the original one will serve as well @@ -2538,7 +2538,7 @@ class GeometryObject(FlatCAMObj, Geometry): job_obj.gc_start = start_gcode app_obj.inform.emit('[success] %s' % _("G-Code parsing in progress...")) - dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse(tool_data=tools_dict[tooluid_key]['data']) app_obj.inform.emit('[success] %s' % _("G-Code parsing finished...")) # commented this; there is no need for the actual GCode geometry - the original one will serve as well diff --git a/appPlugins/ToolDrilling.py b/appPlugins/ToolDrilling.py index d908b7e9..386ad5e0 100644 --- a/appPlugins/ToolDrilling.py +++ b/appPlugins/ToolDrilling.py @@ -1226,7 +1226,7 @@ class ToolDrilling(AppTool, Excellon): # update UI only if only one row is selected otherwise having multiple rows selected will deform information # for the rows other that the current one (first selected) - if len(sel_rows) == 1: + if len(sel_rows) <= 1: self.update_ui() def update_ui(self): diff --git a/appPlugins/ToolMilling.py b/appPlugins/ToolMilling.py index 398c9169..438c673a 100644 --- a/appPlugins/ToolMilling.py +++ b/appPlugins/ToolMilling.py @@ -127,6 +127,7 @@ class ToolMilling(AppTool, Excellon): # updated in the self.set_tool_ui() self.form_fields = {} + self.general_form_fields = {} self.old_tool_dia = None self.poly_drawn = False @@ -459,7 +460,9 @@ class ToolMilling(AppTool, Excellon): "tools_mill_spindlespeed": self.ui.spindlespeed_entry, "tools_mill_dwell": self.ui.dwell_cb, "tools_mill_dwelltime": self.ui.dwelltime_entry, + }) + self.general_form_fields.update({ "tools_mill_toolchange": self.ui.toolchange_cb, "tools_mill_toolchangez": self.ui.toolchangez_entry, "tools_mill_toolchangexy": self.ui.toolchangexy_entry, @@ -478,6 +481,55 @@ class ToolMilling(AppTool, Excellon): "tools_mill_area_overz": self.ui.over_z_entry, }) + self.name2option.update({ + "milling_type": "tools_mill_milling_type", + "milling_dia": "tools_mill_milling_dia", + + "mill_offset_type": "tools_mill_offset_type", + "mill_offset": "tools_mill_offset", + "mill_job_type": "tools_mill_job_type", + + "mill_polish_margin": "tools_mill_polish_margin", + "mill_polish_overlap": "tools_mill_polish_overlap", + "mill_polish_method": "tools_mill_polish_method", + + "mill_tipdia": "tools_mill_vtipdia", + "mill_tipangle": "tools_mill_vtipangle", + + "mill_cutz": "tools_mill_cutz", + "mill_multidepth": "tools_mill_multidepth", + "mill_depthperpass": "tools_mill_depthperpass", + + "mill_travelz": "tools_mill_travelz", + "mill_feedratexy": "tools_mill_feedrate", + "mill_feedratez": "tools_mill_feedrate_z", + "mill_fr_rapid": "tools_mill_feedrate_rapid", + + "mill_extracut": "tools_mill_extracut", + "mill_extracut_length": "tools_mill_extracut_length", + + "mill_spindlespeed": "tools_mill_spindlespeed", + "mill_dwell": "tools_mill_dwell", + "mill_dwelltime": "tools_mill_dwelltime", + + # General Parameters + "mill_toolchange": "tools_mill_toolchange", + "mill_toolchangez": "tools_mill_toolchangez", + "mill_toolchangexy": "tools_mill_toolchangexy", + + "mill_endz": "tools_mill_endz", + "mill_endxy": "tools_mill_endxy", + + "mill_depth_probe": "tools_mill_z_pdepth", + "mill_fr_probe": "tools_mill_feedrate_probe", + "mill_ppname_g": "tools_mill_ppname_g", + + "mill_exclusion": "tools_mill_area_exclusion", + "mill_area_shape": "tools_mill_area_shape", + "mill_strategy": "tools_mill_area_strategy", + "mill_overz": "tools_mill_area_overz", + }) + # reset the Geometry preprocessor combo self.ui.pp_geo_name_cb.clear() # populate Geometry (milling) preprocessor combobox list @@ -490,31 +542,6 @@ class ToolMilling(AppTool, Excellon): # Fill form fields self.to_form() - # # Show/Hide Advanced Options - # if app_mode == 'b': - # self.ui.level.setText('%s' % _('Beginner')) - # self.ui.level.setStyleSheet(""" - # QToolButton - # { - # color: green; - # } - # """) - # self.ui.feedrate_rapid_label.hide() - # self.ui.feedrate_rapid_entry.hide() - # self.ui.pdepth_label.hide() - # self.ui.pdepth_entry.hide() - # self.ui.feedrate_probe_label.hide() - # self.ui.feedrate_probe_entry.hide() - # - # else: - # self.ui.level.setText('%s' % _('Advanced')) - # self.ui.level.setStyleSheet(""" - # QToolButton - # { - # color: red; - # } - # """) - self.ui.tools_frame.show() self.ui.order_radio.set_value(self.app.defaults["tools_drill_tool_order"]) @@ -1070,6 +1097,8 @@ class ToolMilling(AppTool, Excellon): sel_items = self.ui.geo_tools_table.selectedItems() for it in sel_items: sel_rows.add(it.row()) + it.setSelected(True) + if len(sel_rows) > 1: self.ui.tool_data_label.setText( "%s: %s" % (_('Parameters for'), _("Multiple Tools")) @@ -1397,6 +1426,22 @@ class ToolMilling(AppTool, Excellon): current_widget.returnPressed.connect(self.form_to_storage) elif isinstance(current_widget, FCComboBox): current_widget.currentIndexChanged.connect(self.form_to_storage) + elif isinstance(current_widget, FCComboBox2): + current_widget.currentIndexChanged.connect(self.form_to_storage) + + # General Parameters + for opt in self.general_form_fields: + current_widget = self.general_form_fields[opt] + if isinstance(current_widget, FCCheckBox): + current_widget.stateChanged.connect(self.form_to_storage) + if isinstance(current_widget, RadioSet): + current_widget.activated_custom.connect(self.form_to_storage) + elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner): + current_widget.returnPressed.connect(self.form_to_storage) + elif isinstance(current_widget, FCComboBox): + current_widget.currentIndexChanged.connect(self.form_to_storage) + elif isinstance(current_widget, FCComboBox2): + current_widget.currentIndexChanged.connect(self.form_to_storage) self.ui.order_radio.activated_custom[str].connect(self.on_order_changed) @@ -1466,6 +1511,40 @@ class ToolMilling(AppTool, Excellon): current_widget.currentIndexChanged.disconnect(self.form_to_storage) except (TypeError, ValueError, RuntimeError): pass + elif isinstance(current_widget, FCComboBox2): + try: + current_widget.currentIndexChanged.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass + + # General Parameters + for opt in self.general_form_fields: + current_widget = self.general_form_fields[opt] + if isinstance(current_widget, FCCheckBox): + try: + current_widget.stateChanged.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass + if isinstance(current_widget, RadioSet): + try: + current_widget.activated_custom.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass + elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner): + try: + current_widget.returnPressed.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass + elif isinstance(current_widget, FCComboBox): + try: + current_widget.currentIndexChanged.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass + elif isinstance(current_widget, FCComboBox2): + try: + current_widget.currentIndexChanged.disconnect(self.form_to_storage) + except (TypeError, ValueError, RuntimeError): + pass try: self.ui.order_radio.activated_custom[str].disconnect() @@ -1536,6 +1615,13 @@ class ToolMilling(AppTool, Excellon): "%s: %s" % (_('Parameters for'), _("Multiple Tools")) ) + if not sel_rows or len(sel_rows) == 0: + self.ui.param_frame.setDisabled(False) + self.ui.generate_cnc_button.setDisabled(False) + else: + self.ui.param_frame.setDisabled(True) + self.ui.generate_cnc_button.setDisabled(True) + def on_row_selection_change(self): if self.ui.target_radio.get_value() == 'exc': # ######################################################################################################### @@ -1551,7 +1637,7 @@ class ToolMilling(AppTool, Excellon): # update UI only if only one row is selected otherwise having multiple rows selected will deform information # for the rows other that the current one (first selected) - if len(sel_rows) == 1: + if len(sel_rows) <= 1: self.update_ui() else: # ######################################################################################################### @@ -1567,7 +1653,7 @@ class ToolMilling(AppTool, Excellon): # update UI only if only one row is selected otherwise having multiple rows selected will deform information # for the rows other that the current one (first selected) - if len(sel_rows) == 1: + if len(sel_rows) <= 1: self.update_ui() # synchronize selection in the Geometry Milling Tool Table with the selection in the Geometry UI Tool Table @@ -1607,6 +1693,7 @@ class ToolMilling(AppTool, Excellon): return else: self.ui.generate_cnc_button.setDisabled(False) + self.ui.param_frame.setDisabled(False) if len(sel_rows) == 1: # update the QLabel that shows for which Tool we have the parameters in the UI form @@ -1679,19 +1766,25 @@ class ToolMilling(AppTool, Excellon): t_table = self.ui.tools_table self.current_row = t_table.currentRow() - for k in self.form_fields: + for k in list(self.form_fields.keys()) + list(self.general_form_fields.keys()): for option in storage: if option.startswith('tools_mill_'): if k == option: try: - self.form_fields[k].set_value(storage[option]) + if k in self.form_fields: + self.form_fields[k].set_value(storage[option]) + else: + self.general_form_fields[k].set_value(storage[option]) except Exception: # it may fail for form fields found in the tools tables if there are no rows pass elif option.startswith('geometry_'): if k == option.replace('geometry_', ''): try: - self.form_fields[k].set_value(storage[option]) + if k in self.form_fields: + self.form_fields[k].set_value(storage[option]) + else: + self.general_form_fields[k].set_value(storage[option]) except Exception: # it may fail for form fields found in the tools tables if there are no rows pass @@ -1727,7 +1820,10 @@ class ToolMilling(AppTool, Excellon): else: self.form_fields[storage_key].set_value(dict_storage[storage_key]) except Exception as e: - self.app.log.error("ToolDrilling.storage_to_form() --> %s" % str(e)) + self.app.log.error( + "ToolDrilling.storage_to_form() for key: %s with value: %s--> %s" % + (str(storage_key), str(dict_storage[storage_key]), str(e)) + ) pass def form_to_storage(self): @@ -1755,17 +1851,9 @@ class ToolMilling(AppTool, Excellon): self.ui_disconnect() - # we get the current row in the (geo) tools table for the form fields found in the table - if self.ui.target_radio.get_value() == 'geo': - t_table = self.ui.geo_tools_table - else: - t_table = self.ui.tools_table - self.current_row = t_table.currentRow() - - # those are the general parameters that are common to all tools - general_parameters = ["tools_mill_toolchange", "tools_mill_toolchangez", "tools_mill_endxy", "tools_mill_endz", - "tools_mill_ppname_g", "tools_mill_area_exclusion", - "tools_mill_area_shape", "tools_mill_area_strategy", "tools_mill_area_overz"] + widget_changed = self.sender() + wdg_objname = widget_changed.objectName() + option_changed = self.name2option[wdg_objname] # update the tool specific parameters rows = sorted(set(index.row() for index in used_tools_table.selectedIndexes())) @@ -1774,28 +1862,35 @@ class ToolMilling(AppTool, Excellon): row = 0 tooluid_item = int(used_tools_table.item(row, 3).text()) + # update tool parameters for tooluid_key, tooluid_val in self.target_obj.tools.items(): if int(tooluid_key) == tooluid_item: - for form_key, form_val in self.form_fields.items(): - if form_key in general_parameters: - continue + if option_changed in self.form_fields: + new_option_value = self.form_fields[option_changed].get_value() - try: - # widgets in the tools table - if form_key == 'tools_mill_tool_type': - tt_wdg = self.ui.geo_tools_table.cellWidget(self.current_row, 2) - self.target_obj.tools[tooluid_key]['data'][form_key] = tt_wdg.get_value() - else: - self.target_obj.tools[tooluid_key]['data'][form_key] = form_val.get_value() - except Exception as e: - self.app.log.error("ToolMilling.form_to_storage() --> %s" % str(e)) + # widgets in the tools table + if option_changed == 'tools_mill_tool_type': + try: + tt_wdg = self.ui.geo_tools_table.cellWidget(row, 2) + self.target_obj.tools[tooluid_key]['data'][option_changed] = tt_wdg.get_value() + except Exception as e: + self.app.log.error( + "ToolMilling.form_to_storage() for cell widget --> %s" % str(e)) + else: + try: + self.target_obj.tools[tooluid_key]['data'][option_changed] = new_option_value + except Exception as e: + self.app.log.error( + "ToolMilling.form_to_storage() for key: %s with value: %s --> %s" % + (str(option_changed), str(new_option_value), str(e)) + ) # update the general parameters in all tools - for general_option in general_parameters: - new_opt_val = self.form_fields[general_option].get_value() - for tool in self.target_obj.tools: + for tooluid_key, tooluid_val in self.target_obj.tools.items(): + if option_changed in self.general_form_fields: + new_opt_val = self.general_form_fields[option_changed].get_value() try: - self.target_obj.tools[tool]['data'][general_option] = new_opt_val + self.target_obj.tools[tooluid_key]['data'][option_changed] = new_opt_val except Exception as err: self.app.log.error("ToolMilling.form_to_storage() general parameters --> %s" % str(err)) self.ui_connect() @@ -2812,7 +2907,7 @@ class ToolMilling(AppTool, Excellon): }) self.mtool_gen_cncjob() - self.ui.geo_tools_table.clearSelection() + # self.ui.geo_tools_table.clearSelection() elif self.ui.geo_tools_table.rowCount() == 1: tooluid = int(self.ui.geo_tools_table.item(0, 3).text()) @@ -2823,7 +2918,7 @@ class ToolMilling(AppTool, Excellon): tooluid: deepcopy(tooluid_value) }) self.mtool_gen_cncjob() - self.ui.geo_tools_table.clearSelection() + # self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No tool selected in the tool table ...")) @@ -3008,7 +3103,7 @@ class ToolMilling(AppTool, Excellon): total_gcode += res self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) - dia_cnc_dict['gcode_parsed'] = new_cncjob_obj.gcode_parse() + dia_cnc_dict['gcode_parsed'] = new_cncjob_obj.gcode_parse(tool_data=tools_dict[tooluid_key]['data']) app_obj.inform.emit('[success] %s' % _("G-Code parsing finished...")) # commented this; there is no need for the actual GCode geometry - the original one will serve as well @@ -3162,7 +3257,7 @@ class ToolMilling(AppTool, Excellon): new_cncjob_obj.gc_start = start_gcode app_obj.inform.emit('[success] %s' % _("G-Code parsing in progress...")) - dia_cnc_dict['gcode_parsed'] = new_cncjob_obj.gcode_parse() + dia_cnc_dict['gcode_parsed'] = new_cncjob_obj.gcode_parse(tool_data=tools_dict[tooluid_key]['data']) app_obj.inform.emit('[success] %s' % _("G-Code parsing finished...")) # commented this; there is no need for the actual GCode geometry - the original one will serve as well @@ -4288,6 +4383,7 @@ class MillingUI: _("Include tool-change sequence\n" "in G-Code (Pause for tool change).") ) + self.toolchange_cb.setObjectName("mill_toolchange") self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message) self.toolchangez_entry.set_precision(self.decimals) @@ -4296,6 +4392,7 @@ class MillingUI: "tool change.") ) self.toolchangez_entry.set_range(-10000.0000, 10000.0000) + self.toolchangez_entry.setObjectName("mill_toolchangez") self.toolchangez_entry.setSingleStep(0.1) @@ -4308,7 +4405,7 @@ class MillingUI: _("Toolchange X,Y position.") ) self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9') - self.toolchangexy_entry.setObjectName("e_toolchangexy") + self.toolchangexy_entry.setObjectName("mill_toolchangexy") self.grid3.addWidget(self.toolchange_xy_label, 8, 0) self.grid3.addWidget(self.toolchangexy_entry, 8, 1) @@ -4329,6 +4426,7 @@ class MillingUI: self.endz_entry = FCDoubleSpinner(callback=self.confirmation_message) self.endz_entry.set_precision(self.decimals) self.endz_entry.set_range(-10000.0000, 10000.0000) + self.endz_entry.setObjectName("mill_endz") self.endz_entry.setSingleStep(0.1) @@ -4344,6 +4442,8 @@ class MillingUI: ) self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.endxy_entry.setPlaceholderText(_("X,Y coordinates")) + self.endxy_entry.setObjectName("mill_endxy") + self.grid3.addWidget(self.endmove_xy_label, 12, 0) self.grid3.addWidget(self.endxy_entry, 12, 1) @@ -4392,6 +4492,7 @@ class MillingUI: ) self.pp_geo_name_cb = FCComboBox() self.pp_geo_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus) + self.pp_geo_name_cb.setObjectName("mill_ppname_g") self.grid3.addWidget(pp_geo_label, 16, 0) self.grid3.addWidget(self.pp_geo_name_cb, 16, 1) @@ -4409,6 +4510,8 @@ class MillingUI: "is forbidden." ) ) + self.exclusion_cb.setObjectName("mill_exclusion") + self.grid3.addWidget(self.exclusion_cb, 20, 0, 1, 2) self.exclusion_frame = QtWidgets.QFrame() @@ -4451,6 +4554,7 @@ class MillingUI: "- Around -> will avoid the exclusion area by going around the area")) self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'}, {'label': _('Around'), 'value': 'around'}]) + self.strategy_radio.setObjectName("mill_strategy") grid_a1.addWidget(self.strategy_label, 1, 0) grid_a1.addWidget(self.strategy_radio, 1, 1) @@ -4462,6 +4566,7 @@ class MillingUI: self.over_z_entry = FCDoubleSpinner() self.over_z_entry.set_range(-10000.0000, 10000.0000) self.over_z_entry.set_precision(self.decimals) + self.over_z_entry.setObjectName("mill_overz") grid_a1.addWidget(self.over_z_label, 2, 0) grid_a1.addWidget(self.over_z_entry, 2, 1) @@ -4476,6 +4581,7 @@ class MillingUI: self.area_shape_radio.setToolTip( _("The kind of selection shape used for area selection.") ) + self.area_shape_radio.setObjectName("mill_area_shape") grid_a1.addWidget(self.add_area_button, 4, 0) grid_a1.addWidget(self.area_shape_radio, 4, 1) diff --git a/appPlugins/ToolSolderPaste.py b/appPlugins/ToolSolderPaste.py index c96a35a4..b7d54938 100644 --- a/appPlugins/ToolSolderPaste.py +++ b/appPlugins/ToolSolderPaste.py @@ -964,7 +964,7 @@ class SolderPaste(AppTool): total_gcode += res # ## PARSE GCODE # ## - tool_cnc_dict['gcode_parsed'] = new_obj.gcode_parse() + tool_cnc_dict['gcode_parsed'] = new_obj.gcode_parse(tool_data=tool_cnc_dict['data']) # TODO this serve for bounding box creation only; should be optimized tool_cnc_dict['solid_geometry'] = unary_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']]) diff --git a/camlib.py b/camlib.py index 03342484..1d705150 100644 --- a/camlib.py +++ b/camlib.py @@ -6365,7 +6365,7 @@ class CNCjob(Geometry): match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline) return command - def gcode_parse(self, force_parsing=None): + def gcode_parse(self, force_parsing=None, tool_data=None): """ G-Code parser (from self.gcode). Generates dictionary with single-segment LineString's and "kind" indicating cut or travel, @@ -6380,6 +6380,8 @@ class CNCjob(Geometry): :param force_parsing: :type force_parsing: + :param tool_data: when dealing with multi tool objects we need the tool data + :type tool_data: dict :return: :rtype: dict """ @@ -6392,25 +6394,39 @@ class CNCjob(Geometry): # Last known instruction current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0} + if tool_data is None: + toolchange_xy_mill = self.app.defaults["tools_mill_toolchangexy"] + toolchange_xy_drill = self.app.defaults["tools_drill_toolchangexy"] + else: + if tool_data["tools_mill_toolchange"] is True: + toolchange_xy_mill = tool_data["tools_mill_toolchangexy"] + else: + toolchange_xy_mill = (0, 0) + + if tool_data["tools_drill_toolchange"] is True: + toolchange_xy_drill = tool_data["tools_drill_toolchangexy"] + else: + toolchange_xy_drill = (0, 0) + # Current path: temporary storage until tool is # lifted or lowered. if self.options['type'].lower() == "excellon": - if self.app.defaults["tools_drill_toolchangexy"] == '' or \ - self.app.defaults["tools_drill_toolchangexy"] is None: + if toolchange_xy_drill == '' or toolchange_xy_drill is None: pos_xy = (0, 0) else: - pos_xy = self.app.defaults["tools_drill_toolchangexy"] + pos_xy = toolchange_xy_drill + # if it's a string try: pos_xy = [float(eval(a)) for a in pos_xy.split(",")] except Exception: if len(pos_xy) != 2: pos_xy = (0, 0) else: - if self.app.defaults["tools_mill_toolchangexy"] == '' or \ - self.app.defaults["tools_mill_toolchangexy"] is None: + if toolchange_xy_mill == '' or toolchange_xy_mill is None: pos_xy = (0, 0) else: - pos_xy = self.app.defaults["tools_mill_toolchangexy"] + pos_xy = toolchange_xy_mill + # if it's a string try: pos_xy = [float(eval(a)) for a in pos_xy.split(",")] except Exception: