diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42401302..701a9352 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta
=================================================
+21.06.2020
+
+- wip
+
18.06.2020
- fixed bug in the Cutout Tool that did not allowed the manual cutous to be added on a Geometry created in the Tool
diff --git a/appObjects/FlatCAMExcellon.py b/appObjects/FlatCAMExcellon.py
index 85f9bdc5..2a3d668f 100644
--- a/appObjects/FlatCAMExcellon.py
+++ b/appObjects/FlatCAMExcellon.py
@@ -552,6 +552,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
"milling_type": self.ui.milling_type_radio,
"milling_dia": self.ui.mill_dia_entry,
+
"cutz": self.ui.cutz_entry,
"multidepth": self.ui.mpass_cb,
"depthperpass": self.ui.maxdepth_entry,
diff --git a/appTools/ToolDrilling.py b/appTools/ToolDrilling.py
index bc293bde..27e33c68 100644
--- a/appTools/ToolDrilling.py
+++ b/appTools/ToolDrilling.py
@@ -14,10 +14,10 @@ from appParsers.ParseExcellon import Excellon
from copy import deepcopy
-# import numpy as np
-# import math
+import numpy as np
+import math
-# from shapely.ops import cascaded_union
+from shapely.ops import unary_union
from shapely.geometry import Point, LineString
from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -26,11 +26,20 @@ import logging
import gettext
import appTranslation as fcTranslate
import builtins
+import platform
+import re
+import traceback
+
+from Common import GracefulException as grace
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
+if platform.architecture()[0] == '64bit':
+ from ortools.constraint_solver import pywrapcp
+ from ortools.constraint_solver import routing_enums_pb2
+
log = logging.getLogger('base')
settings = QtCore.QSettings("Open Source", "FlatCAM")
@@ -117,11 +126,33 @@ class ToolDrilling(AppTool, Excellon):
self.poly_sel_disconnect_flag = False
self.form_fields = {
- "excellon_milling_type": self.ui.milling_type_radio,
+ "cutz": self.ui.cutz_entry,
+ "multidepth": self.ui.mpass_cb,
+ "depthperpass": self.ui.maxdepth_entry,
+ "travelz": self.ui.travelz_entry,
+ "feedrate_z": self.ui.feedrate_z_entry,
+ "feedrate_rapid": self.ui.feedrate_rapid_entry,
+
+ "spindlespeed": self.ui.spindlespeed_entry,
+ "dwell": self.ui.dwell_cb,
+ "dwelltime": self.ui.dwelltime_entry,
+
+ "offset": self.ui.offset_entry
}
self.name2option = {
- "e_milling_type": "excellon_milling_type",
+ "e_cutz": "cutz",
+ "e_multidepth": "multidepth",
+ "e_depthperpass": "depthperpass",
+ "e_travelz": "travelz",
+ "e_feedratez": "feedrate_z",
+ "e_fr_rapid": "feedrate_rapid",
+
+ "e_spindlespeed": "spindlespeed",
+ "e_dwell": "dwell",
+ "e_dwelltime": "dwelltime",
+
+ "e_offset": "offset"
}
self.old_tool_dia = None
@@ -186,9 +217,6 @@ class ToolDrilling(AppTool, Excellon):
self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
- self.on_operation_type(val='drill')
- self.ui.operation_radio.activated_custom.connect(self.on_operation_type)
-
self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed)
self.ui.reset_button.clicked.connect(self.set_tool_ui)
@@ -199,7 +227,7 @@ class ToolDrilling(AppTool, Excellon):
self.units = self.app.defaults['units'].upper()
self.old_tool_dia = self.app.defaults["tools_iso_newdia"]
- # try to select in the Gerber combobox the active object
+ # try to select in the Excellon combobox the active object
try:
selected_obj = self.app.collection.get_active()
if selected_obj.kind == 'excellon':
@@ -208,67 +236,6 @@ class ToolDrilling(AppTool, Excellon):
except Exception:
pass
- self.form_fields.update({
-
- "operation": self.ui.operation_radio,
- "milling_type": self.ui.milling_type_radio,
-
- "milling_dia": self.ui.mill_dia_entry,
- "cutz": self.ui.cutz_entry,
- "multidepth": self.ui.mpass_cb,
- "depthperpass": self.ui.maxdepth_entry,
- "travelz": self.ui.travelz_entry,
- "feedrate_z": self.ui.feedrate_z_entry,
- "feedrate": self.ui.xyfeedrate_entry,
- "feedrate_rapid": self.ui.feedrate_rapid_entry,
- # "tooldia": self.ui.tooldia_entry,
- # "slot_tooldia": self.ui.slot_tooldia_entry,
- "toolchange": self.ui.toolchange_cb,
- "toolchangez": self.ui.toolchangez_entry,
- "extracut": self.ui.extracut_cb,
- "extracut_length": self.ui.e_cut_entry,
-
- "spindlespeed": self.ui.spindlespeed_entry,
- "dwell": self.ui.dwell_cb,
- "dwelltime": self.ui.dwelltime_entry,
-
- "startz": self.ui.estartz_entry,
- "endz": self.ui.endz_entry,
- "endxy": self.ui.endxy_entry,
-
- "offset": self.ui.offset_entry,
-
- "ppname_e": self.ui.pp_excellon_name_cb,
- "ppname_g": self.ui.pp_geo_name_cb,
- "z_pdepth": self.ui.pdepth_entry,
- "feedrate_probe": self.ui.feedrate_probe_entry,
- # "gcode_type": self.ui.excellon_gcode_type_radio,
- "area_exclusion": self.ui.exclusion_cb,
- "area_shape": self.ui.area_shape_radio,
- "area_strategy": self.ui.strategy_radio,
- "area_overz": self.ui.over_z_entry,
- })
-
- self.name2option = {
- "e_operation": "operation",
- "e_milling_type": "milling_type",
- "e_milling_dia": "milling_dia",
- "e_cutz": "cutz",
- "e_multidepth": "multidepth",
- "e_depthperpass": "depthperpass",
-
- "e_travelz": "travelz",
- "e_feedratexy": "feedrate",
- "e_feedratez": "feedrate_z",
- "e_fr_rapid": "feedrate_rapid",
- "e_extracut": "extracut",
- "e_extracut_length": "extracut_length",
- "e_spindlespeed": "spindlespeed",
- "e_dwell": "dwell",
- "e_dwelltime": "dwelltime",
- "e_offset": "offset",
- }
-
# populate Excellon preprocessor combobox list
for name in list(self.app.preprocessors.keys()):
# the HPGL preprocessor is only for Geometry not for Excellon job therefore don't add it
@@ -276,12 +243,6 @@ class ToolDrilling(AppTool, Excellon):
continue
self.ui.pp_excellon_name_cb.addItem(name)
- # populate Geometry (milling) preprocessor combobox list
- for name in list(self.app.preprocessors.keys()):
- self.ui.pp_geo_name_cb.addItem(name)
-
- # Fill form fields
- # self.to_form()
# update the changes in UI depending on the selected preprocessor in Preferences
# after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
@@ -307,7 +268,6 @@ class ToolDrilling(AppTool, Excellon):
self.ui.tools_frame.show()
self.ui.order_radio.set_value(self.app.defaults["excellon_tool_order"])
- self.ui.milling_type_radio.set_value(self.app.defaults["excellon_milling_type"])
loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
if loaded_obj:
@@ -318,43 +278,72 @@ class ToolDrilling(AppTool, Excellon):
# init the working variables
self.default_data.clear()
self.default_data = {
- "name": outname + '_iso',
- "plot": self.app.defaults["excellon_plot"],
- "solid": False,
- "multicolored": False,
+ "name": outname + '_iso',
+ "plot": self.app.defaults["excellon_plot"],
+ "solid": self.app.defaults["excellon_solid"],
+ "multicolored": self.app.defaults["excellon_multicolored"],
+ "merge_fuse_tools": self.app.defaults["excellon_merge_fuse_tools"],
+ "format_upper_in": self.app.defaults["excellon_format_upper_in"],
+ "format_lower_in": self.app.defaults["excellon_format_lower_in"],
+ "format_upper_mm": self.app.defaults["excellon_format_upper_mm"],
+ "lower_mm": self.app.defaults["excellon_format_lower_mm"],
+ "zeros": self.app.defaults["excellon_zeros"],
+ "excellon_units": self.app.defaults["excellon_units"],
+ "excellon_update": self.app.defaults["excellon_update"],
- "operation": "drill",
- "milling_type": "drills",
+ "excellon_optimization_type": self.app.defaults["excellon_optimization_type"],
- "milling_dia": 0.04,
+ "excellon_search_time": self.app.defaults["excellon_search_time"],
- "cutz": -0.1,
- "multidepth": False,
- "depthperpass": 0.7,
- "travelz": 0.1,
- "feedrate": self.app.defaults["geometry_feedrate"],
- "feedrate_z": 5.0,
- "feedrate_rapid": 5.0,
- "tooldia": 0.1,
- "slot_tooldia": 0.1,
- "toolchange": False,
- "toolchangez": 1.0,
- "toolchangexy": "0.0, 0.0",
- "extracut": self.app.defaults["geometry_extracut"],
- "extracut_length": self.app.defaults["geometry_extracut_length"],
- "endz": 2.0,
- "endxy": '',
+ "excellon_plot_fill": self.app.defaults["excellon_plot_fill"],
+ "excellon_plot_line": self.app.defaults["excellon_plot_line"],
- "startz": None,
- "offset": 0.0,
- "spindlespeed": 0,
- "dwell": True,
- "dwelltime": 1000,
- "ppname_e": 'default',
- "ppname_g": self.app.defaults["geometry_ppname_g"],
- "z_pdepth": -0.02,
- "feedrate_probe": 3.0,
- "optimization_type": "B",
+ # Excellon Options
+ "excellon_tool_order": self.app.defaults["excellon_tool_order"],
+
+ "cutz": self.app.defaults["excellon_cutz"],
+ "multidepth": self.app.defaults["excellon_multidepth"],
+ "depthperpass": self.app.defaults["excellon_depthperpass"],
+
+ "travelz": self.app.defaults["excellon_travelz"],
+ "endz": self.app.defaults["excellon_endz"],
+ "endxy": self.app.defaults["excellon_endxy"],
+
+ "feedrate_z": self.app.defaults["excellon_feedrate_z"],
+
+ "spindlespeed": self.app.defaults["excellon_spindlespeed"],
+ "dwell": self.app.defaults["excellon_dwell"],
+ "dwelltime": self.app.defaults["excellon_dwelltime"],
+
+ "toolchange": self.app.defaults["excellon_toolchange"],
+ "toolchangez": self.app.defaults["excellon_toolchangez"],
+
+ "ppname_e": self.app.defaults["excellon_ppname_e"],
+
+ "tooldia": self.app.defaults["excellon_tooldia"],
+ "slot_tooldia": self.app.defaults["excellon_slot_tooldia"],
+
+ "gcode_type": self.app.defaults["excellon_gcode_type"],
+
+ "excellon_area_exclusion": self.app.defaults["excellon_area_exclusion"],
+ "excellon_area_shape": self.app.defaults["excellon_area_shape"],
+ "excellon_area_strategy": self.app.defaults["excellon_area_strategy"],
+ "excellon_area_overz": self.app.defaults["excellon_area_overz"],
+
+ # Excellon Advanced Options
+ "offset": self.app.defaults["excellon_offset"],
+ "toolchangexy": self.app.defaults["excellon_toolchangexy"],
+ "startz": self.app.defaults["excellon_startz"],
+ "feedrate_rapid": self.app.defaults["excellon_feedrate_rapid"],
+ "z_pdepth": self.app.defaults["excellon_z_pdepth"],
+ "feedrate_probe": self.app.defaults["excellon_feedrate_probe"],
+ "spindledir": self.app.defaults["excellon_spindledir"],
+ "f_plunge": self.app.defaults["excellon_f_plunge"],
+ "f_retract": self.app.defaults["excellon_f_retract"],
+
+ "gcode": '',
+ "gcode_parsed": '',
+ "geometry": [],
}
# fill in self.default_data values from self.options
@@ -377,8 +366,6 @@ class ToolDrilling(AppTool, Excellon):
# ########################################
# #######3 TEMP SETTINGS #################
# ########################################
- self.ui.operation_radio.set_value("drill")
- self.ui.operation_radio.setEnabled(False)
self.on_object_changed()
if self.excellon_obj:
@@ -420,9 +407,10 @@ class ToolDrilling(AppTool, Excellon):
# Get source object.
try:
self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
- except Exception as e:
+ except Exception:
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
- return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
+ return
+
if self.excellon_obj:
self.ui.exc_param_frame.setDisabled(False)
@@ -783,8 +771,8 @@ class ToolDrilling(AppTool, Excellon):
try:
item = self.ui.tools_table.item(c_row, 3)
if type(item) is not None:
- tooluid = item.text()
- self.storage_to_form(self.excellon_tools[str(tooluid)]['data'])
+ tooluid = int(item.text())
+ self.storage_to_form(self.excellon_tools[tooluid]['data'])
else:
self.blockSignals(False)
return
@@ -848,35 +836,6 @@ class ToolDrilling(AppTool, Excellon):
self.blockSignals(False)
- def on_operation_type(self, val):
- """
- Called by a RadioSet activated_custom signal
-
- :param val: Parameter passes by the signal that called this method
- :type val: str
- :return: None
- :rtype:
- """
- if val == 'mill':
- self.ui.mill_type_label.show()
- self.ui.milling_type_radio.show()
- self.ui.mill_dia_label.show()
- self.ui.mill_dia_entry.show()
- self.ui.frxylabel.show()
- self.ui.xyfeedrate_entry.show()
- self.ui.extracut_cb.show()
- self.ui.e_cut_entry.show()
- else:
- self.ui.mill_type_label.hide()
- self.ui.milling_type_radio.hide()
- self.ui.mill_dia_label.hide()
- self.ui.mill_dia_entry.hide()
-
- self.ui.frxylabel.hide()
- self.ui.xyfeedrate_entry.hide()
- self.ui.extracut_cb.hide()
- self.ui.e_cut_entry.hide()
-
def get_selected_tools_list(self):
"""
Returns the keys to the self.tools dictionary corresponding
@@ -1296,121 +1255,6 @@ class ToolDrilling(AppTool, Excellon):
except AttributeError:
pass
- def on_cnc_button_click(self):
- self.obj_name = self.ui.object_combo.currentText()
-
- # Get source object.
- try:
- self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
- except Exception as e:
- self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
- return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
-
- if self.excellon_obj is None:
- self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
- return
-
- # Get the tools from the list
- tools = self.get_selected_tools_list()
-
- if len(tools) == 0:
- # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
- # tool number) it means that there are 3 rows (1 tool and 2 totals).
- # in this case regardless of the selection status of that tool, use it.
- if self.ui.tools_table.rowCount() == 3:
- tools.append(self.ui.tools_table.item(0, 0).text())
- else:
- self.app.inform.emit('[ERROR_NOTCL] %s' %
- _("Please select one or more tools from the list and try again."))
- return
-
- xmin = self.options['xmin']
- ymin = self.options['ymin']
- xmax = self.options['xmax']
- ymax = self.options['ymax']
-
- job_name = self.options["name"] + "_cnc"
- pp_excellon_name = self.options["ppname_e"]
-
- # Object initialization function for app.app_obj.new_object()
- def job_init(job_obj, app_obj):
- assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
-
- app_obj.inform.emit(_("Generating Excellon CNCJob..."))
-
- # get the tool_table items in a list of row items
- tool_table_items = self.get_selected_tools_table_items()
- # insert an information only element in the front
- tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
-
- # ## Add properties to the object
-
- job_obj.origin_kind = 'excellon'
-
- job_obj.options['Tools_in_use'] = tool_table_items
- job_obj.options['type'] = 'Excellon'
- job_obj.options['ppname_e'] = pp_excellon_name
-
- job_obj.multidepth = self.options["multidepth"]
- job_obj.z_depthpercut = self.options["depthperpass"]
-
- job_obj.z_move = float(self.options["travelz"])
- job_obj.feedrate = float(self.options["feedrate_z"])
- job_obj.z_feedrate = float(self.options["feedrate_z"])
- job_obj.feedrate_rapid = float(self.options["feedrate_rapid"])
-
- job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] != 0 else None
- job_obj.spindledir = self.app.defaults['excellon_spindledir']
- job_obj.dwell = self.options["dwell"]
- job_obj.dwelltime = float(self.options["dwelltime"])
-
- job_obj.pp_excellon_name = pp_excellon_name
-
- job_obj.toolchange_xy_type = "excellon"
- job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
- job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
-
- job_obj.options['xmin'] = xmin
- job_obj.options['ymin'] = ymin
- job_obj.options['xmax'] = xmax
- job_obj.options['ymax'] = ymax
-
- job_obj.z_pdepth = float(self.options["z_pdepth"])
- job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-
- job_obj.z_cut = float(self.options['cutz'])
- job_obj.toolchange = self.options["toolchange"]
- job_obj.xy_toolchange = self.app.defaults["excellon_toolchangexy"]
- job_obj.z_toolchange = float(self.options["toolchangez"])
- job_obj.startz = float(self.options["startz"]) if self.options["startz"] else None
- job_obj.endz = float(self.options["endz"])
- job_obj.xy_end = self.options["endxy"]
- job_obj.excellon_optimization_type = self.app.defaults["excellon_optimization_type"]
-
- tools_csv = ','.join(tools)
- ret_val = job_obj.generate_from_excellon_by_tool(self, tools_csv, use_ui=True)
-
- if ret_val == 'fail':
- return 'fail'
-
- job_obj.gcode_parse()
- job_obj.create_geometry()
-
- # To be run in separate thread
- def job_thread(a_obj):
- with self.app.proc_container.new(_("Generating CNC Code")):
- a_obj.app_obj.new_object("cncjob", job_name, job_init)
-
- # Create promise for the new name.
- self.app.collection.promise(job_name)
-
- # Send to worker
- # self.app.worker.add_task(job_thread, [self.app])
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
-
- def drilling_handler(self, obj):
- pass
-
def on_key_press(self, event):
# modifiers = QtWidgets.QApplication.keyboardModifiers()
# matplotlib_key_flag = False
@@ -1559,9 +1403,1094 @@ class ToolDrilling(AppTool, Excellon):
self.ui.exclusion_table.selectAll()
self.draw_sel_shape()
+ def on_cnc_button_click(self):
+ obj_name = self.ui.object_combo.currentText()
+
+ # Get source object.
+ try:
+ self.excellon_obj = self.app.collection.get_by_name(obj_name)
+ except Exception as e:
+ self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
+ return
+
+ if self.excellon_obj is None:
+ self.app.inform.emit('[ERROR_NOTCL] %s.' % _("Object not found"))
+ return
+
+ # Get the tools from the Tool Table
+ selected_uid = set()
+ for it in self.ui.tools_table.selectedItems():
+ uid = self.ui.tools_table.item(it.row(), 3).text()
+ selected_uid.add(uid)
+ tools = list(selected_uid)
+
+ if len(tools) == 0:
+ # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
+ # tool number) it means that there are 3 rows (1 tool and 2 totals).
+ # in this case regardless of the selection status of that tool, use it.
+ if self.ui.tools_table.rowCount() == 3:
+ tools.append(self.ui.tools_table.item(0, 0).text())
+ else:
+ self.app.inform.emit('[ERROR_NOTCL] %s' %
+ _("Please select one or more tools from the list and try again."))
+ return
+
+ xmin = self.excellon_obj.options['xmin']
+ ymin = self.excellon_obj.options['ymin']
+ xmax = self.excellon_obj.options['xmax']
+ ymax = self.excellon_obj.options['ymax']
+
+ job_name = self.excellon_obj.options["name"] + "_cnc"
+ pp_excellon_name = self.ui.pp_excellon_name_cb.get_value()
+ # z_pdepth = self.ui.pdepth_entry.get_value()
+ # feedrate_probe = self.ui.feedrate_probe_entry.get_value()
+ # toolchange = self.ui.toolchange_cb.get_value()
+ # z_toolchange = self.ui.toolchangez_entry.get_value()
+ # startz = self.ui.estartz_entry.get_value()
+ # endz = self.ui.endz_entry.get_value()
+ # xy_end = self.ui.endxy_entry.get_value()
+
+ # Object initialization function for app.app_obj.new_object()
+ def job_init(job_obj, app_obj):
+ assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
+ app_obj.inform.emit(_("Generating Excellon CNCJob..."))
+
+ # get the tool_table items in a list of row items
+ tool_table_items = self.get_selected_tools_table_items()
+ # insert an information only element in the front
+ tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+
+ # ## Add properties to the object
+ job_obj.origin_kind = 'excellon'
+
+ job_obj.options['Tools_in_use'] = tool_table_items
+ job_obj.options['type'] = 'Excellon'
+ job_obj.options['ppname_e'] = pp_excellon_name
+
+ job_obj.pp_excellon_name = pp_excellon_name
+ job_obj.toolchange_xy_type = "excellon"
+ job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
+ job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
+
+ job_obj.options['xmin'] = xmin
+ job_obj.options['ymin'] = ymin
+ job_obj.options['xmax'] = xmax
+ job_obj.options['ymax'] = ymax
+
+ # job_obj.z_pdepth = z_pdepth
+ # job_obj.feedrate_probe = feedrate_probe
+ #
+ # job_obj.toolchange = toolchange
+ # job_obj.xy_toolchange = self.app.defaults["excellon_toolchangexy"]
+ # job_obj.z_toolchange = z_toolchange
+ # job_obj.startz = startz
+ # job_obj.endz = endz
+ # job_obj.xy_end = xy_end
+ # job_obj.excellon_optimization_type = self.app.defaults["excellon_optimization_type"]
+
+ tools_csv = ','.join(tools)
+
+ job_obj.gcode = self.generate_from_excellon_by_tool(tools_csv, self.tools)
+ print(job_obj.gcode)
+ if job_obj.gcode == 'fail':
+ return 'fail'
+
+ job_obj.gcode_parse()
+ job_obj.create_geometry()
+
+ # To be run in separate thread
+ def job_thread(a_obj):
+ with self.app.proc_container.new(_("Generating CNC Code")):
+ a_obj.app_obj.new_object("cncjob", job_name, job_init)
+
+ # Create promise for the new name.
+ self.app.collection.promise(job_name)
+
+ # Send to worker
+ # self.app.worker.add_task(job_thread, [self.app])
+ self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+ def drilling_handler(self, obj):
+ pass
+
+ # Distance callback
+ class CreateDistanceCallback(object):
+ """Create callback to calculate distances between points."""
+
+ def __init__(self, locs, manager):
+ self.manager = manager
+ self.matrix = {}
+
+ if locs:
+ size = len(locs)
+
+ for from_node in range(size):
+ self.matrix[from_node] = {}
+ for to_node in range(size):
+ if from_node == to_node:
+ self.matrix[from_node][to_node] = 0
+ else:
+ x1 = locs[from_node][0]
+ y1 = locs[from_node][1]
+ x2 = locs[to_node][0]
+ y2 = locs[to_node][1]
+ self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
+
+ # def Distance(self, from_node, to_node):
+ # return int(self.matrix[from_node][to_node])
+ def Distance(self, from_index, to_index):
+ # Convert from routing variable Index to distance matrix NodeIndex.
+ from_node = self.manager.IndexToNode(from_index)
+ to_node = self.manager.IndexToNode(to_index)
+ return self.matrix[from_node][to_node]
+
+ @staticmethod
+ def create_tool_data_array(points):
+ # Create the data.
+ loc_list = []
+
+ for pt in points:
+ loc_list.append((pt.coords.xy[0][0], pt.coords.xy[1][0]))
+ return loc_list
+
+ def optimized_ortools_meta(self, locations, start=None):
+ optimized_path = []
+
+ tsp_size = len(locations)
+ num_routes = 1 # The number of routes, which is 1 in the TSP.
+ # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
+
+ depot = 0 if start is None else start
+
+ # Create routing model.
+ if tsp_size > 0:
+ manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
+ routing = pywrapcp.RoutingModel(manager)
+ search_parameters = pywrapcp.DefaultRoutingSearchParameters()
+ search_parameters.local_search_metaheuristic = (
+ routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
+
+ # Set search time limit in milliseconds.
+ if float(self.app.defaults["excellon_search_time"]) != 0:
+ search_parameters.time_limit.seconds = int(
+ float(self.app.defaults["excellon_search_time"]))
+ else:
+ search_parameters.time_limit.seconds = 3
+
+ # Callback to the distance function. The callback takes two
+ # arguments (the from and to node indices) and returns the distance between them.
+ dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager)
+
+ # if there are no distances then go to the next tool
+ if not dist_between_locations:
+ return
+
+ dist_callback = dist_between_locations.Distance
+ transit_callback_index = routing.RegisterTransitCallback(dist_callback)
+ routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
+
+ # Solve, returns a solution if any.
+ assignment = routing.SolveWithParameters(search_parameters)
+
+ if assignment:
+ # Solution cost.
+ log.info("OR-tools metaheuristics - Total distance: " + str(assignment.ObjectiveValue()))
+
+ # Inspect solution.
+ # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
+ route_number = 0
+ node = routing.Start(route_number)
+ start_node = node
+
+ while not routing.IsEnd(node):
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ optimized_path.append(node)
+ node = assignment.Value(routing.NextVar(node))
+ else:
+ log.warning('OR-tools metaheuristics - No solution found.')
+ else:
+ log.warning('OR-tools metaheuristics - Specify an instance greater than 0.')
+
+ return optimized_path
+ # ############################################# ##
+
+ def optimized_ortools_basic(self, locations, start=None):
+ optimized_path = []
+
+ tsp_size = len(locations)
+ num_routes = 1 # The number of routes, which is 1 in the TSP.
+
+ # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
+ depot = 0 if start is None else start
+
+ # Create routing model.
+ if tsp_size > 0:
+ manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
+ routing = pywrapcp.RoutingModel(manager)
+ search_parameters = pywrapcp.DefaultRoutingSearchParameters()
+
+ # Callback to the distance function. The callback takes two
+ # arguments (the from and to node indices) and returns the distance between them.
+ dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager)
+
+ # if there are no distances then go to the next tool
+ if not dist_between_locations:
+ return
+
+ dist_callback = dist_between_locations.Distance
+ transit_callback_index = routing.RegisterTransitCallback(dist_callback)
+ routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
+
+ # Solve, returns a solution if any.
+ assignment = routing.SolveWithParameters(search_parameters)
+
+ if assignment:
+ # Solution cost.
+ log.info("Total distance: " + str(assignment.ObjectiveValue()))
+
+ # Inspect solution.
+ # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
+ route_number = 0
+ node = routing.Start(route_number)
+ start_node = node
+
+ while not routing.IsEnd(node):
+ optimized_path.append(node)
+ node = assignment.Value(routing.NextVar(node))
+ else:
+ log.warning('No solution found.')
+ else:
+ log.warning('Specify an instance greater than 0.')
+
+ return optimized_path
+ # ############################################# ##
+
+ def optimized_travelling_salesman(self, points, start=None):
+ """
+ As solving the problem in the brute force way is too slow,
+ this function implements a simple heuristic: always
+ go to the nearest city.
+
+ Even if this algorithm is extremely simple, it works pretty well
+ giving a solution only about 25%% longer than the optimal one (cit. Wikipedia),
+ and runs very fast in O(N^2) time complexity.
+
+ >>> optimized_travelling_salesman([[i,j] for i in range(5) for j in range(5)])
+ [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 4], [1, 3], [1, 2], [1, 1], [1, 0], [2, 0], [2, 1], [2, 2],
+ [2, 3], [2, 4], [3, 4], [3, 3], [3, 2], [3, 1], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]]
+ >>> optimized_travelling_salesman([[0,0],[10,0],[6,0]])
+ [[0, 0], [6, 0], [10, 0]]
+
+ :param points: List of tuples with x, y coordinates
+ :type points: list
+ :param start: a tuple with a x,y coordinates of the start point
+ :type start: tuple
+ :return: List of points ordered in a optimized way
+ :rtype: list
+ """
+
+ if start is None:
+ start = points[0]
+ must_visit = points
+ path = [start]
+ # must_visit.remove(start)
+ while must_visit:
+ nearest = min(must_visit, key=lambda x: distance(path[-1], x))
+ path.append(nearest)
+ must_visit.remove(nearest)
+ return path
+
+ def check_zcut(self, zcut):
+ if zcut > 0:
+ self.app.inform.emit('[WARNING] %s' %
+ _("The Cut Z parameter has positive value. "
+ "It is the depth value to drill into material.\n"
+ "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+ "therefore the app will convert the value to negative. "
+ "Check the resulting CNC code (Gcode etc)."))
+ return -zcut
+ elif zcut == 0:
+ self.app.inform.emit('[WARNING] %s.' % _("The Cut Z parameter is zero. There will be no cut, aborting"))
+ return 'fail'
+
+ def generate_from_excellon_by_tool(self, sel_tools, tools, order='fwd'):
+ """
+ Creates Gcode for this object from an Excellon object
+ for the specified tools.
+
+ :return: Tool GCode
+ :rtype: str
+ """
+ log.debug("Creating CNC Job from Excellon...")
+
+ settings = QtCore.QSettings("Open Source", "FlatCAM")
+ if settings.contains("machinist"):
+ machinist_setting = settings.value('machinist', type=int)
+ else:
+ machinist_setting = 0
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # TOOLS
+ # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
+ # so we actually are sorting the tools by diameter
+ # #############################################################################################################
+ # #############################################################################################################
+ all_tools = []
+ for tool_as_key, v in list(tools.items()):
+ all_tools.append((int(tool_as_key), float(v['tooldia'])))
+
+ if order == 'fwd':
+ sorted_tools = sorted(all_tools, key=lambda t1: t1[1])
+ elif order == 'rev':
+ sorted_tools = sorted(all_tools, key=lambda t1: t1[1], reverse=True)
+ else:
+ sorted_tools = all_tools
+
+ if sel_tools == "all":
+ selected_tools = [i[0] for i in all_tools] # we get a array of ordered sel_tools
+ else:
+ selected_tools = eval(sel_tools)
+
+ # Create a sorted list of selected sel_tools from the sorted_tools list
+ sel_tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
+
+ log.debug("Tools sorted are: %s" % str(sel_tools))
+ # #############################################################################################################
+ # #############################################################################################################
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case of
+ # running this method from a Tcl Command
+ # #############################################################################################################
+ # #############################################################################################################
+ build_tools_in_use_list = False
+ if 'Tools_in_use' not in self.excellon_obj.options:
+ self.excellon_obj.options['Tools_in_use'] = []
+
+ # if the list is empty (either we just added the key or it was already there but empty) signal to build it
+ if not self.excellon_obj.options['Tools_in_use']:
+ build_tools_in_use_list = True
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # fill the data into the self.exc_cnc_tools dictionary
+ # #############################################################################################################
+ # #############################################################################################################
+ for it in all_tools:
+ for to_ol in sel_tools:
+ if to_ol == it[0]:
+ sol_geo = []
+
+ drill_no = 0
+ if 'drills' in tools[to_ol]:
+ drill_no = len(tools[to_ol]['drills'])
+ for drill in tools[to_ol]['drills']:
+ sol_geo.append(drill.buffer((it[1] / 2.0), resolution=self.geo_steps_per_circle))
+
+ slot_no = 0
+ if 'slots' in tools[to_ol]:
+ slot_no = len(tools[to_ol]['slots'])
+ for slot in tools[to_ol]['slots']:
+ start = (slot[0].x, slot[0].y)
+ stop = (slot[1].x, slot[1].y)
+ sol_geo.append(
+ LineString([start, stop]).buffer((it[1] / 2.0), resolution=self.geo_steps_per_circle)
+ )
+
+ if self.use_ui:
+ try:
+ z_off = float(tools[it[0]]['data']['offset']) * (-1)
+ except KeyError:
+ z_off = 0
+ else:
+ z_off = 0
+
+ default_data = {}
+ for k, v in list(self.options.items()):
+ default_data[k] = deepcopy(v)
+
+ self.exc_cnc_tools[it[1]] = {}
+ self.exc_cnc_tools[it[1]]['tool'] = it[0]
+ self.exc_cnc_tools[it[1]]['nr_drills'] = drill_no
+ self.exc_cnc_tools[it[1]]['nr_slots'] = slot_no
+ self.exc_cnc_tools[it[1]]['offset_z'] = z_off
+ self.exc_cnc_tools[it[1]]['data'] = default_data
+ self.exc_cnc_tools[it[1]]['solid_geometry'] = deepcopy(sol_geo)
+
+ # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case of
+ # running this method from a Tcl Command
+ if build_tools_in_use_list is True:
+ self.options['Tools_in_use'].append(
+ [it[0], it[1], drill_no, slot_no]
+ )
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # Points (Group by tool): a dictionary of shapely Point geo elements grouped by tool number
+ # #############################################################################################################
+ # #############################################################################################################
+ self.app.inform.emit(_("Creating a list of points to drill..."))
+
+ points = {}
+ for tool, tl_dict in tools.items():
+ if tool in sel_tools:
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ if 'drills' in tl_dict and tl_dict['drills']:
+ for drill_pt in tl_dict['drills']:
+ try:
+ points[tool].append(drill_pt)
+ except KeyError:
+ points[tool] = [drill_pt]
+ log.debug("Found %d TOOLS with drills." % len(points))
+
+ # check if there are drill points in the exclusion areas.
+ # If we find any within the exclusion areas return 'fail'
+ for tool in points:
+ for pt in points[tool]:
+ for area in self.app.exc_areas.exclusion_areas_storage:
+ pt_buf = pt.buffer(self.exc_tools[tool]['tooldia'] / 2.0)
+ if pt_buf.within(area['shape']) or pt_buf.intersects(area['shape']):
+ self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed. Drill points inside the exclusion zones."))
+ return 'fail'
+
+ # #############################################################################################################
+ # General Parameters
+ # #############################################################################################################
+ used_excellon_optimization_type = self.app.defaults["excellon_optimization_type"]
+ self.f_plunge = self.app.defaults["excellon_f_plunge"]
+ self.f_retract = self.app.defaults["excellon_f_retract"]
+
+ # Prepprocessor
+ self.pp_excellon_name = self.default_data["excellon_ppname_e"]
+ self.pp_excellon = self.app.preprocessors[self.pp_excellon_name]
+ p = self.pp_excellon
+
+ # this holds the resulting GCode
+ gcode = ''
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # Initialization
+ # #############################################################################################################
+ # #############################################################################################################
+ gcode += self.doformat(p.start_code)
+
+ if self.toolchange is False:
+ if self.xy_toolchange is not None:
+ gcode += self.doformat(p.lift_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
+ gcode += self.doformat(p.startz_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
+ else:
+ gcode += self.doformat(p.lift_code, x=0.0, y=0.0)
+ gcode += self.doformat(p.startz_code, x=0.0, y=0.0)
+
+ if self.xy_toolchange is not None:
+ self.oldx = self.xy_toolchange[0]
+ self.oldy = self.xy_toolchange[1]
+ else:
+ self.oldx = 0.0
+ self.oldy = 0.0
+
+ measured_distance = 0.0
+ measured_down_distance = 0.0
+ measured_up_to_zero_distance = 0.0
+ measured_lift_distance = 0.0
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # GCODE creation
+ # #############################################################################################################
+ # #############################################################################################################
+ self.app.inform.emit('%s...' % _("Starting G-Code"))
+
+ has_drills = None
+ for tool, tool_dict in self.exc_tools.items():
+ if 'drills' in tool_dict and tool_dict['drills']:
+ has_drills = True
+ break
+ if not has_drills:
+ log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
+ "The loaded Excellon file has no drills ...")
+ self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
+ return 'fail'
+
+ current_platform = platform.architecture()[0]
+ if current_platform != '64bit':
+ used_excellon_optimization_type = 'T'
+
+ # #############################################################################################################
+ # #############################################################################################################
+ # ################################## DRILLING !!! #########################################################
+ # #############################################################################################################
+ # #############################################################################################################
+ if used_excellon_optimization_type == 'M':
+ log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
+ elif used_excellon_optimization_type == 'B':
+ log.debug("Using OR-Tools Basic drill path optimization.")
+ elif used_excellon_optimization_type == 'T':
+ log.debug("Using Travelling Salesman drill path optimization.")
+ else:
+ log.debug("Using no path optimization.")
+
+ if self.toolchange is True:
+ for tool in sel_tools:
+
+ tool_dict = tools[tool]['data']
+
+ # check if it has drills
+ if not tools[tool]['drills']:
+ continue
+
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ self.tool = tool
+ self.tooldia = tools[tool]["tooldia"]
+ self.postdata['toolC'] = self.tooldia
+
+ self.z_feedrate = tool_dict['feedrate_z']
+ self.feedrate = tool_dict['feedrate']
+ self.z_cut = tool_dict['cutz']
+ gcode += self.doformat(p.z_feedrate_code)
+
+ # Z_cut parameter
+ if machinist_setting == 0:
+ self.z_cut = self.check_zcut(zcut=tool_dict["excellon_cutz"])
+ if self.z_cut == 'fail':
+ return 'fail'
+
+ # multidepth use this
+ old_zcut = tool_dict["excellon_cutz"]
+
+ self.z_move = tool_dict['travelz']
+ self.spindlespeed = tool_dict['spindlespeed']
+ self.dwell = tool_dict['dwell']
+ self.dwelltime = tool_dict['dwelltime']
+ self.multidepth = tool_dict['multidepth']
+ self.z_depthpercut = tool_dict['depthperpass']
+
+ # XY_toolchange parameter
+ self.xy_toolchange = tool_dict["excellon_toolchangexy"]
+ try:
+ if self.xy_toolchange == '':
+ self.xy_toolchange = None
+ else:
+ self.xy_toolchange = re.sub('[()\[\]]', '',
+ str(self.xy_toolchange)) if self.xy_toolchange else None
+
+ if self.xy_toolchange:
+ self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
+
+ if self.xy_toolchange and len(self.xy_toolchange) != 2:
+ self.app.inform.emit('[ERROR]%s' %
+ _("The Toolchange X,Y field in Edit -> Preferences has to be "
+ "in the format (x, y) \nbut now there is only one value, not two. "))
+ return 'fail'
+ except Exception as e:
+ log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
+ pass
+
+ # XY_end parameter
+ self.xy_end = tool_dict["excellon_endxy"]
+ self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
+ if self.xy_end and self.xy_end != '':
+ self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
+ if self.xy_end and len(self.xy_end) < 2:
+ self.app.inform.emit(
+ '[ERROR] %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
+ "in the format (x, y) but now there is only one value, not two."))
+ return 'fail'
+
+ # #########################################################################################################
+ # ############ Create the data. #################
+ # #########################################################################################################
+ locations = []
+ altPoints = []
+ optimized_path = []
+
+ if used_excellon_optimization_type == 'M':
+ if tool in points:
+ locations = self.create_tool_data_array(points=points[tool])
+ # if there are no locations then go to the next tool
+ if not locations:
+ continue
+ optimized_path = self.optimized_ortools_meta(locations=locations)
+ elif used_excellon_optimization_type == 'B':
+ if tool in points:
+ locations = self.create_tool_data_array(points=points[tool])
+ # if there are no locations then go to the next tool
+ if not locations:
+ continue
+ optimized_path = self.optimized_ortools_basic(locations=locations)
+ elif used_excellon_optimization_type == 'T':
+ for point in points[tool]:
+ altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
+ optimized_path = self.optimized_travelling_salesman(altPoints)
+ else:
+ # it's actually not optimized path but here we build a list of (x,y) coordinates
+ # out of the tool's drills
+ for drill in self.exc_tools[tool]['drills']:
+ unoptimized_coords = (
+ drill.x,
+ drill.y
+ )
+ optimized_path.append(unoptimized_coords)
+ # #########################################################################################################
+ # #########################################################################################################
+
+ # Only if there are locations to drill
+ if not optimized_path:
+ continue
+
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ # Tool change sequence (optional)
+ if self.toolchange:
+ gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
+ # Spindle start
+ gcode += self.doformat(p.spindle_code)
+ # Dwell time
+ if self.dwell is True:
+ gcode += self.doformat(p.dwell_code)
+
+ current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[tool]["tooldia"])))
+
+ self.app.inform.emit(
+ '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
+ str(current_tooldia),
+ str(self.units))
+ )
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ # APPLY Offset only when using the appGUI, for TclCommand this will create an error
+ # because the values for Z offset are created in build_ui()
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ try:
+ z_offset = float(tool_dict['offset']) * (-1)
+ except KeyError:
+ z_offset = 0
+ self.z_cut = z_offset + old_zcut
+
+ self.coordinates_type = self.app.defaults["cncjob_coords_type"]
+ if self.coordinates_type == "G90":
+ # Drillling! for Absolute coordinates type G90
+ # variables to display the percentage of work done
+ geo_len = len(optimized_path)
+
+ old_disp_number = 0
+ log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
+
+ loc_nr = 0
+ for point in optimized_path:
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ if used_excellon_optimization_type == 'T':
+ locx = point[0]
+ locy = point[1]
+ else:
+ locx = locations[point][0]
+ locy = locations[point][1]
+
+ travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
+ end_point=(locx, locy),
+ tooldia=current_tooldia)
+ prev_z = None
+ for travel in travels:
+ locx = travel[1][0]
+ locy = travel[1][1]
+
+ if travel[0] is not None:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # raise to safe Z (travel[0]) each time because safe Z may be different
+ self.z_move = travel[0]
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ # restore z_move
+ self.z_move = tool_dict['travelz']
+ else:
+ if prev_z is not None:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # we assume that previously the z_move was altered therefore raise to
+ # the travel_z (z_move)
+ self.z_move = tool_dict['travelz']
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+ else:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # store prev_z
+ prev_z = travel[0]
+
+ # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
+ doc = deepcopy(self.z_cut)
+ self.z_cut = 0.0
+
+ while abs(self.z_cut) < abs(doc):
+
+ self.z_cut -= self.z_depthpercut
+ if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
+ self.z_cut = doc
+ gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+ measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
+ if self.f_retract is False:
+ gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+ measured_up_to_zero_distance += abs(self.z_cut)
+ measured_lift_distance += abs(self.z_move)
+ else:
+ measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ else:
+ gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+ measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
+ if self.f_retract is False:
+ gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+ measured_up_to_zero_distance += abs(self.z_cut)
+ measured_lift_distance += abs(self.z_move)
+ else:
+ measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
+ self.oldx = locx
+ self.oldy = locy
+
+ loc_nr += 1
+ disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
+
+ if old_disp_number < disp_number <= 100:
+ self.app.proc_container.update_view_text(' %d%%' % disp_number)
+ old_disp_number = disp_number
+
+ else:
+ self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
+ return 'fail'
+ self.z_cut = deepcopy(old_zcut)
+ else:
+ # We are not using Toolchange therefore we need to decide which tool properties to use
+ one_tool = 1
+
+ all_points = []
+ for tool in points:
+ # check if it has drills
+ if not points[tool]:
+ continue
+ all_points += points[tool]
+
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ self.tool = one_tool
+ self.tooldia = self.exc_tools[one_tool]["tooldia"]
+ self.postdata['toolC'] = self.tooldia
+
+ if self.use_ui:
+ self.z_feedrate = self.exc_tools[one_tool]['data']['feedrate_z']
+ self.feedrate = self.exc_tools[one_tool]['data']['feedrate']
+ self.z_cut = self.exc_tools[one_tool]['data']['cutz']
+ gcode += self.doformat(p.z_feedrate_code)
+
+ if self.machinist_setting == 0:
+ if self.z_cut > 0:
+ self.app.inform.emit('[WARNING] %s' %
+ _("The Cut Z parameter has positive value. "
+ "It is the depth value to drill into material.\n"
+ "The Cut Z parameter needs to have a negative value, "
+ "assuming it is a typo "
+ "therefore the app will convert the value to negative. "
+ "Check the resulting CNC code (Gcode etc)."))
+ self.z_cut = -self.z_cut
+ elif self.z_cut == 0:
+ self.app.inform.emit('[WARNING] %s.' %
+ _("The Cut Z parameter is zero. There will be no cut, skipping file"))
+ return 'fail'
+
+ old_zcut = deepcopy(self.z_cut)
+
+ self.z_move = self.exc_tools[one_tool]['data']['travelz']
+ self.spindlespeed = self.exc_tools[one_tool]['data']['spindlespeed']
+ self.dwell = self.exc_tools[one_tool]['data']['dwell']
+ self.dwelltime = self.exc_tools[one_tool]['data']['dwelltime']
+ self.multidepth = self.exc_tools[one_tool]['data']['multidepth']
+ self.z_depthpercut = self.exc_tools[one_tool]['data']['depthperpass']
+ else:
+ old_zcut = deepcopy(self.z_cut)
+
+ # #########################################################################################################
+ # ############ Create the data. #################
+ # #########################################################################################################
+ locations = []
+ altPoints = []
+ optimized_path = []
+
+ if used_excellon_optimization_type == 'M':
+ if all_points:
+ locations = self.create_tool_data_array(points=all_points)
+ # if there are no locations then go to the next tool
+ if not locations:
+ return 'fail'
+ optimized_path = self.optimized_ortools_meta(locations=locations)
+ elif used_excellon_optimization_type == 'B':
+ if all_points:
+ locations = self.create_tool_data_array(points=all_points)
+ # if there are no locations then go to the next tool
+ if not locations:
+ return 'fail'
+ optimized_path = self.optimized_ortools_basic(locations=locations)
+ elif used_excellon_optimization_type == 'T':
+ for point in all_points:
+ altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
+ optimized_path = self.optimized_travelling_salesman(altPoints)
+ else:
+ # it's actually not optimized path but here we build a list of (x,y) coordinates
+ # out of the tool's drills
+ for pt in all_points:
+ unoptimized_coords = (
+ pt.x,
+ pt.y
+ )
+ optimized_path.append(unoptimized_coords)
+ # #########################################################################################################
+ # #########################################################################################################
+
+ # Only if there are locations to drill
+ if not optimized_path:
+ return 'fail'
+
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ # Spindle start
+ gcode += self.doformat(p.spindle_code)
+ # Dwell time
+ if self.dwell is True:
+ gcode += self.doformat(p.dwell_code)
+
+ current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[one_tool]["tooldia"])))
+
+ self.app.inform.emit(
+ '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
+ str(current_tooldia),
+ str(self.units))
+ )
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ # APPLY Offset only when using the appGUI, for TclCommand this will create an error
+ # because the values for Z offset are created in build_ui()
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ try:
+ z_offset = float(self.exc_tools[one_tool]['data']['offset']) * (-1)
+ except KeyError:
+ z_offset = 0
+ self.z_cut = z_offset + old_zcut
+
+ self.coordinates_type = self.app.defaults["cncjob_coords_type"]
+ if self.coordinates_type == "G90":
+ # Drillling! for Absolute coordinates type G90
+ # variables to display the percentage of work done
+ geo_len = len(optimized_path)
+
+ old_disp_number = 0
+ log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
+
+ loc_nr = 0
+ for point in optimized_path:
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
+
+ if used_excellon_optimization_type == 'T':
+ locx = point[0]
+ locy = point[1]
+ else:
+ locx = locations[point][0]
+ locy = locations[point][1]
+
+ travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
+ end_point=(locx, locy),
+ tooldia=current_tooldia)
+ prev_z = None
+ for travel in travels:
+ locx = travel[1][0]
+ locy = travel[1][1]
+
+ if travel[0] is not None:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # raise to safe Z (travel[0]) each time because safe Z may be different
+ self.z_move = travel[0]
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ # restore z_move
+ self.z_move = self.exc_tools[one_tool]['data']['travelz']
+ else:
+ if prev_z is not None:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # we assume that previously the z_move was altered therefore raise to
+ # the travel_z (z_move)
+ self.z_move = self.exc_tools[one_tool]['data']['travelz']
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+ else:
+ # move to next point
+ gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ # store prev_z
+ prev_z = travel[0]
+
+ # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+ if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
+ doc = deepcopy(self.z_cut)
+ self.z_cut = 0.0
+
+ while abs(self.z_cut) < abs(doc):
+
+ self.z_cut -= self.z_depthpercut
+ if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
+ self.z_cut = doc
+ gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+ measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
+ if self.f_retract is False:
+ gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+ measured_up_to_zero_distance += abs(self.z_cut)
+ measured_lift_distance += abs(self.z_move)
+ else:
+ measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ else:
+ gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+ measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
+ if self.f_retract is False:
+ gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+ measured_up_to_zero_distance += abs(self.z_cut)
+ measured_lift_distance += abs(self.z_move)
+ else:
+ measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
+ gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+ measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
+ self.oldx = locx
+ self.oldy = locy
+
+ loc_nr += 1
+ disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
+
+ if old_disp_number < disp_number <= 100:
+ self.app.proc_container.update_view_text(' %d%%' % disp_number)
+ old_disp_number = disp_number
+
+ else:
+ self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
+ return 'fail'
+ self.z_cut = deepcopy(old_zcut)
+
+ if used_excellon_optimization_type == 'M':
+ log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
+ elif used_excellon_optimization_type == 'B':
+ log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
+ elif used_excellon_optimization_type == 'T':
+ log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
+ else:
+ log.debug("The total travel distance with with no optimization is: %s" % str(measured_distance))
+
+ gcode += self.doformat(p.spindle_stop_code)
+ # Move to End position
+ gcode += self.doformat(p.end_code, x=0, y=0)
+
+ # #############################################################################################################
+ # ############################# Calculate DISTANCE and ESTIMATED TIME #########################################
+ # #############################################################################################################
+ measured_distance += abs(distance_euclidian(self.oldx, self.oldy, 0, 0))
+ log.debug("The total travel distance including travel to end position is: %s" %
+ str(measured_distance) + '\n')
+ self.travel_distance = measured_distance
+
+ # I use the value of self.feedrate_rapid for the feadrate in case of the measure_lift_distance and for
+ # traveled_time because it is not always possible to determine the feedrate that the CNC machine uses
+ # for G0 move (the fastest speed available to the CNC router). Although self.feedrate_rapids is used only with
+ # Marlin preprocessor and derivatives.
+ self.routing_time = (measured_down_distance + measured_up_to_zero_distance) / self.feedrate
+ lift_time = measured_lift_distance / self.feedrate_rapid
+ traveled_time = measured_distance / self.feedrate_rapid
+ self.routing_time += lift_time + traveled_time
+
+ self.app.inform.emit(_("Finished G-Code generation..."))
+ return gcode
+
def reset_fields(self):
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+ def doformat(self, fun, **kwargs):
+ return self.doformat2(fun, **kwargs) + "\n"
+
+ def doformat2(self, fun, **kwargs):
+ """
+ This method will call one of the current preprocessor methods having as parameters all the attributes of
+ current class to which will add the kwargs parameters
+
+ :param fun: One of the methods inside the preprocessor classes which get loaded here in the 'p' object
+ :type fun: class 'function'
+ :param kwargs: keyword args which will update attributes of the current class
+ :type kwargs: dict
+ :return: Gcode line
+ :rtype: str
+ """
+ attributes = AttrDict()
+ attributes.update(self.postdata)
+ attributes.update(kwargs)
+ try:
+ returnvalue = fun(attributes)
+ return returnvalue
+ except Exception:
+ self.app.log.error('Exception occurred within a preprocessor: ' + traceback.format_exc())
+ return ''
+
+ @property
+ def postdata(self):
+ """
+ This will return all the attributes of the class in the form of a dictionary
+
+ :return: Class attributes
+ :rtype: dict
+ """
+ return self.__dict__
+
class DrillingUI:
@@ -1663,7 +2592,7 @@ class DrillingUI:
self.tools_table.horizontalHeaderItem(2).setToolTip(
_("The number of Drill holes. Holes that are drilled with\n"
"a drill bit."))
- self.tools_table.horizontalHeaderItem(3).setToolTip(
+ self.tools_table.horizontalHeaderItem(4).setToolTip(
_("The number of Slot holes. Holes that are created by\n"
"milling them with an endmill bit."))
@@ -1718,61 +2647,6 @@ class DrillingUI:
self.grid1.setColumnStretch(1, 1)
self.exc_tools_box.addLayout(self.grid1)
- # Operation Type
- self.operation_label = QtWidgets.QLabel('%s:' % _('Operation'))
- self.operation_label.setToolTip(
- _("Operation type:\n"
- "- Drilling -> will drill the drills/slots associated with this tool\n"
- "- Milling -> will mill the drills/slots")
- )
- self.operation_radio = RadioSet(
- [
- {'label': _('Drilling'), 'value': 'drill'},
- {'label': _("Milling"), 'value': 'mill'}
- ]
- )
- self.operation_radio.setObjectName("e_operation")
-
- self.grid1.addWidget(self.operation_label, 0, 0)
- self.grid1.addWidget(self.operation_radio, 0, 1)
-
- # separator_line = QtWidgets.QFrame()
- # separator_line.setFrameShape(QtWidgets.QFrame.HLine)
- # separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
- # self.grid3.addWidget(separator_line, 1, 0, 1, 2)
-
- self.mill_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
- self.mill_type_label.setToolTip(
- _("Milling type:\n"
- "- Drills -> will mill the drills associated with this tool\n"
- "- Slots -> will mill the slots associated with this tool\n"
- "- Both -> will mill both drills and mills or whatever is available")
- )
- self.milling_type_radio = RadioSet(
- [
- {'label': _('Drills'), 'value': 'drills'},
- {'label': _("Slots"), 'value': 'slots'},
- {'label': _("Both"), 'value': 'both'},
- ]
- )
- self.milling_type_radio.setObjectName("e_milling_type")
-
- self.grid1.addWidget(self.mill_type_label, 2, 0)
- self.grid1.addWidget(self.milling_type_radio, 2, 1)
-
- self.mill_dia_label = QtWidgets.QLabel('%s:' % _('Milling Diameter'))
- self.mill_dia_label.setToolTip(
- _("The diameter of the tool who will do the milling")
- )
-
- self.mill_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
- self.mill_dia_entry.set_precision(self.decimals)
- self.mill_dia_entry.set_range(0.0000, 9999.9999)
- self.mill_dia_entry.setObjectName("e_milling_dia")
-
- self.grid1.addWidget(self.mill_dia_label, 3, 0)
- self.grid1.addWidget(self.mill_dia_entry, 3, 1)
-
# Cut Z
self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
self.cutzlabel.setToolTip(
@@ -1840,21 +2714,6 @@ class DrillingUI:
self.grid1.addWidget(self.travelzlabel, 6, 0)
self.grid1.addWidget(self.travelz_entry, 6, 1)
- # Feedrate X-Y
- self.frxylabel = QtWidgets.QLabel('%s:' % _('Feedrate X-Y'))
- self.frxylabel.setToolTip(
- _("Cutting speed in the XY\n"
- "plane in units per minute")
- )
- self.xyfeedrate_entry = FCDoubleSpinner(callback=self.confirmation_message)
- self.xyfeedrate_entry.set_precision(self.decimals)
- self.xyfeedrate_entry.set_range(0, 9999.9999)
- self.xyfeedrate_entry.setSingleStep(0.1)
- self.xyfeedrate_entry.setObjectName("e_feedratexy")
-
- self.grid1.addWidget(self.frxylabel, 12, 0)
- self.grid1.addWidget(self.xyfeedrate_entry, 12, 1)
-
# Excellon Feedrate Z
self.frzlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
self.frzlabel.setToolTip(
@@ -1894,34 +2753,6 @@ class DrillingUI:
self.feedrate_rapid_label.hide()
self.feedrate_rapid_entry.hide()
- # Cut over 1st point in path
- self.extracut_cb = FCCheckBox('%s:' % _('Re-cut'))
- self.extracut_cb.setToolTip(
- _("In order to remove possible\n"
- "copper leftovers where first cut\n"
- "meet with last cut, we generate an\n"
- "extended cut over the first cut section.")
- )
- self.extracut_cb.setObjectName("e_extracut")
-
- self.e_cut_entry = FCDoubleSpinner(callback=self.confirmation_message)
- self.e_cut_entry.set_range(0, 99999)
- self.e_cut_entry.set_precision(self.decimals)
- self.e_cut_entry.setSingleStep(0.1)
- self.e_cut_entry.setWrapping(True)
- self.e_cut_entry.setToolTip(
- _("In order to remove possible\n"
- "copper leftovers where first cut\n"
- "meet with last cut, we generate an\n"
- "extended cut over the first cut section.")
- )
- self.e_cut_entry.setObjectName("e_extracut_length")
-
- self.ois_recut = OptionalInputSection(self.extracut_cb, [self.e_cut_entry])
-
- self.grid1.addWidget(self.extracut_cb, 17, 0)
- self.grid1.addWidget(self.e_cut_entry, 17, 1)
-
# Spindlespeed
self.spindle_label = QtWidgets.QLabel('%s:' % _('Spindle speed'))
self.spindle_label.setToolTip(
@@ -1945,6 +2776,7 @@ class DrillingUI:
)
self.dwell_cb.setObjectName("e_dwell")
+ # Dwelltime
self.dwelltime_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.dwelltime_entry.set_precision(self.decimals)
self.dwelltime_entry.set_range(0.0, 9999.9999)
@@ -1976,38 +2808,6 @@ class DrillingUI:
self.grid1.addWidget(self.tool_offset_label, 25, 0)
self.grid1.addWidget(self.offset_entry, 25, 1)
- # #################################################################
- # ################# GRID LAYOUT 4 ###############################
- # #################################################################
-
- # self.grid4 = QtWidgets.QGridLayout()
- # self.exc_tools_box.addLayout(self.grid4)
- # self.grid4.setColumnStretch(0, 0)
- # self.grid4.setColumnStretch(1, 1)
- #
- # # choose_tools_label = QtWidgets.QLabel(
- # # _("Select from the Tools Table above the hole dias to be\n"
- # # "drilled. Use the # column to make the selection.")
- # # )
- # # grid2.addWidget(choose_tools_label, 0, 0, 1, 3)
- #
- # # ### Choose what to use for Gcode creation: Drills, Slots or Both
- # gcode_type_label = QtWidgets.QLabel('%s' % _('Gcode'))
- # gcode_type_label.setToolTip(
- # _("Choose what to use for GCode generation:\n"
- # "'Drills', 'Slots' or 'Both'.\n"
- # "When choosing 'Slots' or 'Both', slots will be\n"
- # "converted to a series of drills.")
- # )
- # self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
- # {'label': 'Slots', 'value': 'slots'},
- # {'label': 'Both', 'value': 'both'}])
- # self.grid4.addWidget(gcode_type_label, 1, 0)
- # self.grid4.addWidget(self.excellon_gcode_type_radio, 1, 1)
- # # temporary action until I finish the feature
- # self.excellon_gcode_type_radio.setVisible(False)
- # gcode_type_label.hide()
-
# #################################################################
# ################# GRID LAYOUT 5 ###############################
# #################################################################
@@ -2146,7 +2946,7 @@ class DrillingUI:
self.feedrate_probe_entry.setVisible(False)
# Preprocessor Excellon selection
- pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor E"))
+ pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
pp_excellon_label.setToolTip(
_("The preprocessor JSON file that dictates\n"
"Gcode output for Excellon Objects.")
@@ -2157,18 +2957,6 @@ class DrillingUI:
self.grid3.addWidget(pp_excellon_label, 15, 0)
self.grid3.addWidget(self.pp_excellon_name_cb, 15, 1)
- # Preprocessor Geometry selection
- pp_geo_label = QtWidgets.QLabel('%s:' % _("Preprocessor G"))
- pp_geo_label.setToolTip(
- _("The preprocessor JSON file that dictates\n"
- "Gcode output for Geometry (Milling) Objects.")
- )
- self.pp_geo_name_cb = FCComboBox()
- self.pp_geo_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
-
- self.grid3.addWidget(pp_geo_label, 16, 0)
- self.grid3.addWidget(self.pp_geo_name_cb, 16, 1)
-
# ------------------------------------------------------------------------------------------------------------
# ------------------------- EXCLUSION AREAS ------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------
@@ -2333,3 +3121,17 @@ class DrillingUI:
(_("Edited value is out of range"), minval, maxval), False)
else:
self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+
+def distance(pt1, pt2):
+ return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+
+def distance_euclidian(x1, y1, x2, y2):
+ return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
+
+
+class AttrDict(dict):
+ def __init__(self, *args, **kwargs):
+ super(AttrDict, self).__init__(*args, **kwargs)
+ self.__dict__ = self
diff --git a/camlib.py b/camlib.py
index 7e73e389..5bf74406 100644
--- a/camlib.py
+++ b/camlib.py
@@ -3040,16 +3040,17 @@ class CNCjob(Geometry):
# #############################################################################################################
points = {}
for tool, tool_dict in self.exc_tools.items():
- if self.app.abort_flag:
- # graceful abort requested by the user
- raise grace
+ if tool in tools:
+ if self.app.abort_flag:
+ # graceful abort requested by the user
+ raise grace
- if 'drills' in tool_dict and tool_dict['drills']:
- for drill_pt in tool_dict['drills']:
- try:
- points[tool].append(drill_pt)
- except KeyError:
- points[tool] = [drill_pt]
+ if 'drills' in tool_dict and tool_dict['drills']:
+ for drill_pt in tool_dict['drills']:
+ try:
+ points[tool].append(drill_pt)
+ except KeyError:
+ points[tool] = [drill_pt]
log.debug("Found %d TOOLS with drills." % len(points))
# check if there are drill points in the exclusion areas.