- when editing Geometry objects with multiple geometry fixed the issue with the edited geometry always being the one in the first tool
1671 lines
74 KiB
Python
1671 lines
74 KiB
Python
# ##########################################################
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
# http://flatcam.org #
|
|
# Author: Juan Pablo Caram (c) #
|
|
# Date: 2/5/2014 #
|
|
# MIT Licence #
|
|
# ##########################################################
|
|
|
|
# ##########################################################
|
|
# File modified by: Marius Stanciu #
|
|
# ##########################################################
|
|
|
|
from shapely.geometry import MultiLineString, LinearRing
|
|
import shapely.affinity as affinity
|
|
|
|
from camlib import Geometry, flatten_shapely_geometry
|
|
|
|
from appObjects.FlatCAMObj import *
|
|
|
|
import ezdxf
|
|
import numpy as np
|
|
from copy import deepcopy
|
|
import traceback
|
|
from collections import defaultdict
|
|
from functools import reduce
|
|
|
|
import gettext
|
|
import appTranslation as fcTranslate
|
|
import builtins
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
|
|
class GeometryObject(FlatCAMObj, Geometry):
|
|
"""
|
|
Geometric object not associated with a specific
|
|
format.
|
|
"""
|
|
optionChanged = QtCore.pyqtSignal(str)
|
|
builduiSig = QtCore.pyqtSignal()
|
|
launch_job = QtCore.pyqtSignal()
|
|
|
|
ui_type = GeometryObjectUI
|
|
|
|
def __init__(self, name):
|
|
self.decimals = self.app.decimals
|
|
|
|
self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
|
|
|
|
FlatCAMObj.__init__(self, name)
|
|
Geometry.__init__(self, geo_steps_per_circle=self.circle_steps)
|
|
|
|
self.kind = "geometry"
|
|
|
|
self.options.update({
|
|
"plot": True,
|
|
"multicolored": False,
|
|
|
|
"cutz": -0.002,
|
|
"vtipdia": 0.1,
|
|
"vtipangle": 30,
|
|
"travelz": 0.1,
|
|
"feedrate": 5.0,
|
|
"feedrate_z": 5.0,
|
|
"feedrate_rapid": 5.0,
|
|
"spindlespeed": 0,
|
|
"dwell": True,
|
|
"dwelltime": 1000,
|
|
"multidepth": False,
|
|
"depthperpass": 0.002,
|
|
"extracut": False,
|
|
"extracut_length": 0.1,
|
|
"endz": 2.0,
|
|
"endxy": '',
|
|
"area_exclusion": False,
|
|
"area_shape": "polygon",
|
|
"area_strategy": "over",
|
|
"area_overz": 1.0,
|
|
|
|
"startz": None,
|
|
"toolchange": False,
|
|
"toolchangez": 1.0,
|
|
"toolchangexy": "0.0, 0.0",
|
|
"ppname_g": 'default',
|
|
"z_pdepth": -0.02,
|
|
"feedrate_probe": 3.0,
|
|
})
|
|
|
|
if "tools_mill_tooldia" not in self.options:
|
|
if type(self.app.defaults["tools_mill_tooldia"]) == float:
|
|
self.options["tools_mill_tooldia"] = self.app.defaults["tools_mill_tooldia"]
|
|
else:
|
|
try:
|
|
tools_string = self.app.defaults["tools_mill_tooldia"].split(",")
|
|
tools_diameters = [eval(a) for a in tools_string if a != '']
|
|
self.options["tools_mill_tooldia"] = tools_diameters[0] if tools_diameters else 0.0
|
|
except Exception as e:
|
|
self.app.log.error("FlatCAMObj.GeometryObject.init() --> %s" % str(e))
|
|
|
|
self.options["tools_mill_startz"] = self.app.defaults["tools_mill_startz"]
|
|
|
|
# this will hold the tool unique ID that is useful when having multiple tools with same diameter
|
|
self.tooluid = 0
|
|
|
|
'''
|
|
self.tools = {}
|
|
This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the
|
|
tool_id of the tools and the value is another dict that will hold the data under the following form:
|
|
{tooluid: {
|
|
'tooldia': 1,
|
|
'data': self.default_tool_data
|
|
'solid_geometry': []
|
|
}
|
|
}
|
|
'''
|
|
self.tools = {}
|
|
|
|
# this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table
|
|
# those elements are the ones used for generating GCode
|
|
self.sel_tools = {}
|
|
|
|
self.offset_item_options = ["Path", "In", "Out", "Custom"]
|
|
self.job_item_options = [_('Roughing'), _('Finishing'), _('Isolation'), _('Polishing')]
|
|
self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
|
|
|
|
# flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
|
|
self.v_tool_type = None
|
|
|
|
# flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry
|
|
# the default value is False
|
|
self.multigeo = False
|
|
|
|
# flag to store if the geometry is part of a special group of geometries that can't be processed by the default
|
|
# engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
|
|
self.special_group = None
|
|
|
|
# self.old_pp_state = self.app.defaults["tools_mill_multidepth"]
|
|
# self.old_toolchangeg_state = self.app.defaults["tools_mill_toolchange"]
|
|
self.units_found = self.app.app_units
|
|
|
|
# this variable can be updated by the Object that generates the geometry
|
|
self.tool_type = 'C1'
|
|
|
|
# save here the old value for the Cut Z before it is changed by selecting a V-shape type tool in the tool table
|
|
self.old_cutz = self.app.defaults["tools_mill_cutz"]
|
|
|
|
self.fill_color = self.app.defaults['geometry_plot_line']
|
|
self.outline_color = self.app.defaults['geometry_plot_line']
|
|
self.alpha_level = 'FF'
|
|
|
|
self.param_fields = {}
|
|
|
|
# store here the state of the exclusion checkbox state to be restored after building the UI
|
|
self.exclusion_area_cb_is_checked = self.app.defaults["tools_mill_area_exclusion"]
|
|
|
|
# Attributes to be included in serialization
|
|
# Always append to it because it carries contents
|
|
# from predecessors.
|
|
self.ser_attrs += ['options', 'kind', 'multigeo', 'fill_color', 'outline_color', 'alpha_level']
|
|
|
|
def build_ui(self):
|
|
try:
|
|
self.ui_disconnect()
|
|
except RuntimeError:
|
|
return
|
|
|
|
FlatCAMObj.build_ui(self)
|
|
|
|
self.units = self.app.app_units
|
|
|
|
row_idx = 0
|
|
|
|
n = len(self.tools)
|
|
self.ui.geo_tools_table.setRowCount(n)
|
|
|
|
for tooluid_key, tooluid_value in self.tools.items():
|
|
|
|
# -------------------- ID ------------------------------------------ #
|
|
tool_id = QtWidgets.QTableWidgetItem('%d' % int(row_idx + 1))
|
|
tool_id.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 0, tool_id) # Tool name/id
|
|
|
|
# Make sure that the tool diameter when in MM is with no more than 2 decimals.
|
|
# There are no tool bits in MM with more than 3 decimals diameter.
|
|
# For INCH the decimals should be no more than 3. There are no tools under 10mils.
|
|
|
|
# -------------------- DIAMETER ------------------------------------- #
|
|
dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia'])))
|
|
dia_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 1, dia_item) # Diameter
|
|
|
|
# -------------------- OFFSET ------------------------------------- #
|
|
try:
|
|
offset_item_txt = self.offset_item_options[tooluid_value['data']['tools_mill_offset_type']]
|
|
except TypeError:
|
|
offset_item_txt = tooluid_value['data']['tools_mill_offset_type']
|
|
offset_item = QtWidgets.QTableWidgetItem(offset_item_txt)
|
|
offset_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 2, offset_item) # Offset Type
|
|
|
|
# -------------------- JOB ------------------------------------- #
|
|
try:
|
|
job_item_txt = self.job_item_options[tooluid_value['data']['tools_mill_job_type']]
|
|
except TypeError:
|
|
job_item_txt = tooluid_value['data']['tools_mill_job_type']
|
|
job_item = QtWidgets.QTableWidgetItem(job_item_txt)
|
|
job_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 3, job_item) # Job Type
|
|
|
|
# -------------------- TOOL SHAPE ------------------------------------- #
|
|
try:
|
|
tool_shape_item_txt = self.tool_type_item_options[tooluid_value['data']['tools_mill_tool_shape']]
|
|
except TypeError:
|
|
tool_shape_item_txt = tooluid_value['data']['tools_mill_tool_shape']
|
|
tool_shape_item = QtWidgets.QTableWidgetItem(tool_shape_item_txt)
|
|
tool_shape_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 4, tool_shape_item) # Tool Shape
|
|
|
|
# -------------------- TOOL UID ------------------------------------- #
|
|
tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
|
|
# ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
|
|
self.ui.geo_tools_table.setItem(row_idx, 5, tool_uid_item) # Tool unique ID
|
|
|
|
# -------------------- PLOT ------------------------------------- #
|
|
empty_plot_item = QtWidgets.QTableWidgetItem('')
|
|
empty_plot_item.setFlags(~QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.ui.geo_tools_table.setItem(row_idx, 6, empty_plot_item)
|
|
plot_item = FCCheckBox()
|
|
plot_item.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft)
|
|
if self.ui.plot_cb.isChecked():
|
|
plot_item.setChecked(True)
|
|
self.ui.geo_tools_table.setCellWidget(row_idx, 6, plot_item)
|
|
|
|
row_idx += 1
|
|
|
|
for row in range(row_idx):
|
|
self.ui.geo_tools_table.item(row, 0).setFlags(
|
|
self.ui.geo_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemFlag.ItemIsSelectable)
|
|
|
|
self.ui.geo_tools_table.resizeColumnsToContents()
|
|
self.ui.geo_tools_table.resizeRowsToContents()
|
|
|
|
vertical_header = self.ui.geo_tools_table.verticalHeader()
|
|
# vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
|
vertical_header.hide()
|
|
self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
horizontal_header = self.ui.geo_tools_table.horizontalHeader()
|
|
horizontal_header.setMinimumSectionSize(10)
|
|
horizontal_header.setDefaultSectionSize(70)
|
|
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed)
|
|
horizontal_header.resizeSection(0, 20)
|
|
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
|
horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeMode.Fixed)
|
|
horizontal_header.resizeSection(4, 40)
|
|
horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.ResizeMode.Fixed)
|
|
horizontal_header.resizeSection(6, 17)
|
|
# horizontal_header.setStretchLastSection(True)
|
|
self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
self.ui.geo_tools_table.setColumnWidth(0, 20)
|
|
self.ui.geo_tools_table.setColumnWidth(4, 40)
|
|
self.ui.geo_tools_table.setColumnWidth(6, 17)
|
|
|
|
# self.ui.geo_tools_table.setSortingEnabled(True)
|
|
|
|
self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight())
|
|
self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight())
|
|
|
|
# select only the first tool / row
|
|
selected_row = 0
|
|
try:
|
|
self.select_tools_table_row(selected_row, clearsel=True)
|
|
except Exception as e:
|
|
# when the tools table is empty there will be this error but once the table is populated it will go away
|
|
self.app.log.error('GeometryObject.build_ui() -> %s' % str(e))
|
|
|
|
# disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed
|
|
# and can create some problems
|
|
if self.multigeo is False:
|
|
self.ui.geo_tools_table.setColumnHidden(6, True)
|
|
else:
|
|
self.ui.geo_tools_table.setColumnHidden(6, False)
|
|
|
|
self.ui_connect()
|
|
|
|
def set_ui(self, ui):
|
|
# this one adds the 'name' key and the self.ui.name_entry widget in the self.form_fields dict
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
self.app.log.debug("GeometryObject.set_ui()")
|
|
|
|
assert isinstance(self.ui, GeometryObjectUI), \
|
|
"Expected a GeometryObjectUI, got %s" % type(self.ui)
|
|
|
|
self.units = self.app.app_units.upper()
|
|
self.units_found = self.app.app_units
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"multicolored": self.ui.multicolored_cb,
|
|
})
|
|
|
|
# Fill form fields only on object create
|
|
self.to_form()
|
|
|
|
# store here the default data for Geometry Data
|
|
self.default_data = {}
|
|
|
|
# fill in self.default_data values from self.options
|
|
self.default_data.update(self.options)
|
|
|
|
if type(self.options["tools_mill_tooldia"]) == float:
|
|
tools_list = [self.options["tools_mill_tooldia"]]
|
|
else:
|
|
try:
|
|
temp_tools = self.options["tools_mill_tooldia"].split(",")
|
|
tools_list = [
|
|
float(eval(dia)) for dia in temp_tools if dia != ''
|
|
]
|
|
except Exception as e:
|
|
self.app.log.error("GeometryObject.set_ui() -> At least one tool diameter needed. "
|
|
"Verify in Edit -> Preferences -> Geometry General -> Tool dia. %s" % str(e))
|
|
return
|
|
|
|
self.tooluid += 1
|
|
|
|
if not self.tools:
|
|
for toold in tools_list:
|
|
new_data = deepcopy(self.default_data)
|
|
self.tools.update({
|
|
self.tooluid: {
|
|
'tooldia': self.app.dec_format(float(toold), self.decimals),
|
|
'data': new_data,
|
|
'solid_geometry': self.solid_geometry
|
|
}
|
|
})
|
|
self.tooluid += 1
|
|
else:
|
|
# if self.tools is not empty then it can safely be assumed that it comes from an opened project.
|
|
# Because of the serialization the self.tools list on project save, the dict keys (members of self.tools
|
|
# are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are
|
|
# again float type; dict's don't like having keys changed when iterated through therefore the need for the
|
|
# following convoluted way of changing the keys from string to float type
|
|
temp_tools = {}
|
|
for tooluid_key in self.tools:
|
|
val = deepcopy(self.tools[int(tooluid_key)])
|
|
new_key = deepcopy(int(tooluid_key))
|
|
temp_tools[new_key] = val
|
|
|
|
self.tools.clear()
|
|
self.tools = deepcopy(temp_tools)
|
|
|
|
if not isinstance(self.ui, GeometryObjectUI):
|
|
self.app.log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui))
|
|
return
|
|
|
|
# #############################################################################################################
|
|
# ##################################### Setting Values#########################################################
|
|
# #############################################################################################################
|
|
self.ui.vertex_points_entry.set_value(0)
|
|
self.ui.geo_tol_entry.set_value(10 ** -self.decimals)
|
|
|
|
# #############################################################################################################
|
|
# ################################ Signals Connection #########################################################
|
|
# #############################################################################################################
|
|
self.ui.level.toggled.connect(self.on_level_changed)
|
|
|
|
# Plot state signals
|
|
# self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
|
|
|
|
# Editor Signal
|
|
self.ui.editor_button.clicked.connect(self.app.object2editor)
|
|
|
|
# Properties
|
|
self.ui.info_button.toggled.connect(self.on_properties)
|
|
self.calculations_finished.connect(self.update_area_chull)
|
|
self.ui.treeWidget.itemExpanded.connect(self.on_properties_expanded)
|
|
self.ui.treeWidget.itemCollapsed.connect(self.on_properties_expanded)
|
|
|
|
# # Buttons Signals
|
|
self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=True))
|
|
self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=True))
|
|
self.ui.milling_button.clicked.connect(self.on_milling_button_clicked)
|
|
|
|
self.ui.util_button.clicked.connect(lambda st: self.ui.util_frame.show() if st else self.ui.util_frame.hide())
|
|
self.ui.vertex_points_btn.clicked.connect(self.on_calculate_vertex_points)
|
|
self.ui.simplification_btn.clicked.connect(self.on_simplify_geometry)
|
|
|
|
self.set_offset_values()
|
|
|
|
self.ui.geo_tools_table.itemSelectionChanged.connect(self.on_row_changed)
|
|
|
|
# Show/Hide Advanced Options
|
|
app_mode = self.app.defaults["global_app_level"]
|
|
self.change_level(app_mode)
|
|
|
|
def on_row_changed(self):
|
|
pass
|
|
|
|
def set_offset_values(self):
|
|
xmin, ymin, xmax, ymax = self.bounds()
|
|
center_coords = (
|
|
self.app.dec_format((xmin + abs((xmax - xmin) / 2)), self.decimals),
|
|
self.app.dec_format((ymin + abs((ymax - ymin) / 2)), self.decimals)
|
|
)
|
|
self.ui.offsetvector_entry.set_value(str(center_coords))
|
|
|
|
def change_level(self, level):
|
|
"""
|
|
|
|
:param level: application level: either 'b' or 'a'
|
|
:type level: str
|
|
:return:
|
|
"""
|
|
|
|
if level == 'a':
|
|
self.ui.level.setChecked(True)
|
|
else:
|
|
self.ui.level.setChecked(False)
|
|
self.on_level_changed(self.ui.level.isChecked())
|
|
|
|
def on_level_changed(self, checked):
|
|
if not checked:
|
|
self.ui.level.setText('%s' % _('Beginner'))
|
|
self.ui.level.setStyleSheet("""
|
|
QToolButton
|
|
{
|
|
color: green;
|
|
}
|
|
""")
|
|
|
|
else:
|
|
self.ui.level.setText('%s' % _('Advanced'))
|
|
self.ui.level.setStyleSheet("""
|
|
QToolButton
|
|
{
|
|
color: red;
|
|
}
|
|
""")
|
|
|
|
def on_properties(self, state):
|
|
if state:
|
|
self.ui.info_frame.show()
|
|
else:
|
|
self.ui.info_frame.hide()
|
|
return
|
|
|
|
self.ui.treeWidget.clear()
|
|
self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
|
|
self.ui.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored,
|
|
QtWidgets.QSizePolicy.Policy.MinimumExpanding)
|
|
# make sure that the FCTree widget columns are resized to content
|
|
self.ui.treeWidget.resize_sig.emit()
|
|
|
|
def on_properties_expanded(self):
|
|
for col in range(self.treeWidget.columnCount()):
|
|
self.ui.treeWidget.resizeColumnToContents(col)
|
|
|
|
def on_milling_button_clicked(self):
|
|
self.app.milling_tool.run(toggle=True)
|
|
|
|
def on_calculate_vertex_points(self):
|
|
self.app.log.debug("GeometryObject.on_calculate_vertex_points()")
|
|
|
|
vertex_points = 0
|
|
|
|
for tool in self.tools:
|
|
geometry = self.tools[tool]['solid_geometry']
|
|
flattened_geo = self.flatten_list(obj_list=geometry)
|
|
for geo in flattened_geo:
|
|
if geo.geom_type == 'Polygon':
|
|
vertex_points += len(list(geo.exterior.coords))
|
|
for inter in geo.interiors:
|
|
vertex_points += len(list(inter.coords))
|
|
if geo.geom_type in ['LineString', 'LinearRing']:
|
|
vertex_points += len(list(geo.coords))
|
|
|
|
self.ui.vertex_points_entry.set_value(vertex_points)
|
|
self.app.inform.emit('[success] %s' % _("Vertex points calculated."))
|
|
|
|
def on_simplify_geometry(self):
|
|
self.app.log.debug("GeometryObject.on_simplify_geometry()")
|
|
|
|
tol = self.ui.geo_tol_entry.get_value()
|
|
|
|
def task_job():
|
|
with self.app.proc_container.new('%s...' % _("Simplify")):
|
|
for tool in self.tools:
|
|
new_tool_geo = []
|
|
geometry = self.tools[tool]['solid_geometry']
|
|
flattened_geo = self.flatten_list(obj_list=geometry)
|
|
for geo in flattened_geo:
|
|
new_tool_geo.append(geo.simplify(tolerance=tol))
|
|
self.tools[tool]['solid_geometry'] = deepcopy(new_tool_geo)
|
|
|
|
# update the solid_geometry
|
|
total_geo = []
|
|
for tool in self.tools:
|
|
total_geo += self.tools[tool]['solid_geometry']
|
|
|
|
self.solid_geometry = unary_union(total_geo)
|
|
|
|
# plot the new geometry
|
|
self.app.plot_all()
|
|
|
|
# update the vertex points number
|
|
self.on_calculate_vertex_points()
|
|
|
|
self.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
self.app.worker_task.emit({'fcn': task_job, 'params': []})
|
|
|
|
def ui_connect(self):
|
|
for row in range(self.ui.geo_tools_table.rowCount()):
|
|
self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
|
|
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
|
|
def ui_disconnect(self):
|
|
for row in range(self.ui.geo_tools_table.rowCount()):
|
|
try:
|
|
self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect()
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
try:
|
|
self.ui.plot_cb.stateChanged.disconnect()
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
def select_tools_table_row(self, row, clearsel=None):
|
|
if clearsel:
|
|
self.ui.geo_tools_table.clearSelection()
|
|
|
|
if self.ui.geo_tools_table.rowCount() > 0:
|
|
# self.ui.geo_tools_table.item(row, 0).setSelected(True)
|
|
self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
|
|
|
|
def export_dxf(self):
|
|
dwg = None
|
|
dxf_format = self.app.defaults['geometry_dxf_format']
|
|
|
|
try:
|
|
dwg = ezdxf.new(dxf_format)
|
|
msp = dwg.modelspace()
|
|
|
|
# add units
|
|
dwg.units = ezdxf.InsertUnits(4) if self.app.app_units.lower() == 'mm' else ezdxf.InsertUnits(1)
|
|
dwg.header['$MEASUREMENT'] = 1 if self.app.app_units.lower() == 'mm' else 0
|
|
|
|
def g2dxf(dxf_space, geo_obj):
|
|
if isinstance(geo_obj, MultiPolygon):
|
|
for poly in geo_obj.geoms:
|
|
ext_points = list(poly.exterior.coords)
|
|
dxf_space.add_lwpolyline(ext_points)
|
|
for interior in poly.interiors:
|
|
dxf_space.add_lwpolyline(list(interior.coords))
|
|
if isinstance(geo_obj, Polygon):
|
|
ext_points = list(geo_obj.exterior.coords)
|
|
dxf_space.add_lwpolyline(ext_points)
|
|
for interior in geo_obj.interiors:
|
|
dxf_space.add_lwpolyline(list(interior.coords))
|
|
if isinstance(geo_obj, MultiLineString):
|
|
for line in geo_obj.geoms:
|
|
dxf_space.add_lwpolyline(list(line.coords))
|
|
if isinstance(geo_obj, LineString) or isinstance(geo_obj, LinearRing):
|
|
dxf_space.add_lwpolyline(list(geo_obj.coords))
|
|
|
|
multigeo_solid_geometry = []
|
|
if self.multigeo:
|
|
for tool in self.tools:
|
|
multigeo_solid_geometry += self.tools[tool]['solid_geometry']
|
|
else:
|
|
multigeo_solid_geometry = self.solid_geometry
|
|
|
|
for geo in multigeo_solid_geometry:
|
|
if type(geo) == list:
|
|
for g in geo:
|
|
g2dxf(msp, g)
|
|
else:
|
|
g2dxf(msp, geo)
|
|
|
|
# points = GeometryObject.get_pts(geo)
|
|
# msp.add_lwpolyline(points)
|
|
except Exception as e:
|
|
self.app.log.error(str(e))
|
|
|
|
return dwg
|
|
|
|
def mtool_gen_cncjob(self, outname=None, tools_dict=None, segx=None, segy=None,
|
|
plot=True, use_thread=True):
|
|
"""
|
|
Creates a multi-tool CNCJob out of this Geometry object.
|
|
The actual work is done by the target CNCJobObject object's
|
|
`generate_from_geometry_2()` method.
|
|
|
|
:param outname:
|
|
:param tools_dict: a dictionary that holds the whole data needed to create the Gcode
|
|
(including the solid_geometry)
|
|
:param segx: number of segments on the X axis, for auto-levelling
|
|
:param segy: number of segments on the Y axis, for auto-levelling
|
|
:param plot: if True the generated object will be plotted; if False will not be plotted
|
|
:param use_thread: if True use threading
|
|
:return: None
|
|
"""
|
|
|
|
# use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
|
|
outname = "%s_%s" % (self.options["name"], 'cnc') if outname is None else outname
|
|
|
|
tools_dict = self.sel_tools if tools_dict is None else tools_dict
|
|
segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
|
|
segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
|
|
|
|
try:
|
|
xmin = self.options['xmin']
|
|
ymin = self.options['ymin']
|
|
xmax = self.options['xmax']
|
|
ymax = self.options['ymax']
|
|
except Exception as e:
|
|
self.app.log.error("FlatCAMObj.GeometryObject.mtool_gen_cncjob() --> %s\n" % str(e))
|
|
|
|
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
|
|
msg += '%s' % str(e)
|
|
msg += traceback.format_exc()
|
|
self.app.inform.emit(msg)
|
|
return
|
|
|
|
# force everything as MULTI-GEO
|
|
# self.multigeo = True
|
|
|
|
# Object initialization function for app.app_obj.new_object()
|
|
# RUNNING ON SEPARATE THREAD!
|
|
def job_init_single_geometry(job_obj, app_obj):
|
|
self.app.log.debug("Creating a CNCJob out of a single-geometry")
|
|
assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
|
|
|
|
job_obj.options['xmin'] = xmin
|
|
job_obj.options['ymin'] = ymin
|
|
job_obj.options['xmax'] = xmax
|
|
job_obj.options['ymax'] = ymax
|
|
|
|
# count the tools
|
|
tool_cnt = 0
|
|
|
|
# dia_cnc_dict = {}
|
|
|
|
# this turn on the FlatCAMCNCJob plot for multiple tools
|
|
job_obj.multitool = True
|
|
job_obj.multigeo = False
|
|
job_obj.tools.clear()
|
|
|
|
job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
|
|
job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
|
|
|
|
job_obj.z_pdepth = float(self.app.defaults["tools_mill_z_pdepth"])
|
|
job_obj.feedrate_probe = float(self.app.defaults["tools_mill_feedrate_probe"])
|
|
|
|
total_gcode = ''
|
|
for tooluid_key in list(tools_dict.keys()):
|
|
tool_cnt += 1
|
|
|
|
dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
|
|
tooldia_val = app_obj.dec_format(float(tools_dict[tooluid_key]['tooldia']), self.decimals)
|
|
dia_cnc_dict.update({
|
|
'tooldia': tooldia_val
|
|
})
|
|
|
|
if dia_cnc_dict['data']['tools_mill_offset_type'] == 'in':
|
|
tool_offset = -dia_cnc_dict['tooldia'] / 2
|
|
elif dia_cnc_dict['data']['tools_mill_offset_type'].lower() == 'out':
|
|
tool_offset = dia_cnc_dict['tooldia'] / 2
|
|
elif dia_cnc_dict['data']['tools_mill_offset_type'].lower() == 'custom':
|
|
try:
|
|
offset_value = float(self.ui.tool_offset_entry.get_value())
|
|
except ValueError:
|
|
# try to convert comma to decimal point. if it's still not working error message and return
|
|
try:
|
|
offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
|
|
except ValueError:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
|
|
return
|
|
if offset_value:
|
|
tool_offset = float(offset_value)
|
|
else:
|
|
app_obj.inform.emit(
|
|
'[WARNING] %s' % _("Tool Offset is selected in Tool Table but no value is provided.\n"
|
|
"Add a Tool Offset or change the Offset Type.")
|
|
)
|
|
return
|
|
else:
|
|
tool_offset = 0.0
|
|
|
|
dia_cnc_dict['data']['tools_mill_offset_type'] = tool_offset
|
|
|
|
z_cut = tools_dict[tooluid_key]['data']["tools_mill_cutz"]
|
|
z_move = tools_dict[tooluid_key]['data']["tools_mill_travelz"]
|
|
feedrate = tools_dict[tooluid_key]['data']["tools_mill_feedrate"]
|
|
feedrate_z = tools_dict[tooluid_key]['data']["tools_mill_feedrate_z"]
|
|
feedrate_rapid = tools_dict[tooluid_key]['data']["tools_mill_feedrate_rapid"]
|
|
multidepth = tools_dict[tooluid_key]['data']["tools_mill_multidepth"]
|
|
extracut = tools_dict[tooluid_key]['data']["tools_mill_extracut"]
|
|
extracut_length = tools_dict[tooluid_key]['data']["tools_mill_extracut_length"]
|
|
depthpercut = tools_dict[tooluid_key]['data']["tools_mill_depthperpass"]
|
|
toolchange = tools_dict[tooluid_key]['data']["tools_mill_toolchange"]
|
|
toolchangez = tools_dict[tooluid_key]['data']["tools_mill_toolchangez"]
|
|
toolchangexy = tools_dict[tooluid_key]['data']["tools_mill_toolchangexy"]
|
|
startz = tools_dict[tooluid_key]['data']["tools_mill_startz"]
|
|
endz = tools_dict[tooluid_key]['data']["tools_mill_endz"]
|
|
endxy = self.options["tools_mill_endxy"]
|
|
spindlespeed = tools_dict[tooluid_key]['data']["tools_mill_spindlespeed"]
|
|
dwell = tools_dict[tooluid_key]['data']["tools_mill_dwell"]
|
|
dwelltime = tools_dict[tooluid_key]['data']["tools_mill_dwelltime"]
|
|
pp_geometry_name = tools_dict[tooluid_key]['data']["tools_mill_ppname_g"]
|
|
|
|
spindledir = self.app.defaults['tools_mill_spindledir']
|
|
tool_solid_geometry = self.solid_geometry
|
|
|
|
job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
|
|
job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
|
|
|
|
# Propagate options
|
|
job_obj.options["tooldia"] = tooldia_val
|
|
job_obj.options['type'] = 'Geometry'
|
|
job_obj.options['tool_dia'] = tooldia_val
|
|
|
|
tool_lst = list(tools_dict.keys())
|
|
is_first = True if tooluid_key == tool_lst[0] else False
|
|
|
|
# it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
|
|
# to a value of 0.0005 which is 20 times less than 0.01
|
|
tol = float(self.app.defaults['global_tolerance']) / 20
|
|
res, start_gcode = job_obj.generate_from_geometry_2(
|
|
self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol,
|
|
z_cut=z_cut, z_move=z_move,
|
|
feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
|
|
spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
|
|
multidepth=multidepth, depthpercut=depthpercut,
|
|
extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
|
|
toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
|
|
pp_geometry_name=pp_geometry_name,
|
|
tool_no=tool_cnt, is_first=is_first)
|
|
|
|
if res == 'fail':
|
|
self.app.log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
|
|
return 'fail'
|
|
|
|
dia_cnc_dict['gcode'] = res
|
|
if start_gcode != '':
|
|
job_obj.gc_start = start_gcode
|
|
|
|
total_gcode += res
|
|
|
|
self.app.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
|
|
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
|
|
# for bounding box values
|
|
# dia_cnc_dict['solid_geometry'] = unary_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
|
|
try:
|
|
dia_cnc_dict['solid_geometry'] = tool_solid_geometry
|
|
app_obj.inform.emit('[success] %s...' % _("Finished G-Code processing"))
|
|
except Exception as er:
|
|
app_obj.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(er)))
|
|
|
|
job_obj.tools.update({
|
|
tooluid_key: deepcopy(dia_cnc_dict)
|
|
})
|
|
dia_cnc_dict.clear()
|
|
|
|
job_obj.source_file = job_obj.gc_start + total_gcode
|
|
|
|
# Object initialization function for app.app_obj.new_object()
|
|
# RUNNING ON SEPARATE THREAD!
|
|
def job_init_multi_geometry(job_obj, app_obj):
|
|
self.app.log.debug("Creating a CNCJob out of a multi-geometry")
|
|
assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
|
|
|
|
job_obj.options['xmin'] = xmin
|
|
job_obj.options['ymin'] = ymin
|
|
job_obj.options['xmax'] = xmax
|
|
job_obj.options['ymax'] = ymax
|
|
|
|
# count the tools
|
|
tool_cnt = 0
|
|
|
|
# dia_cnc_dict = {}
|
|
|
|
# this turn on the FlatCAMCNCJob plot for multiple tools
|
|
job_obj.multitool = True
|
|
job_obj.multigeo = True
|
|
job_obj.tools.clear()
|
|
|
|
job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
|
|
job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
|
|
|
|
job_obj.z_pdepth = float(self.app.defaults["tools_mill_z_pdepth"])
|
|
job_obj.feedrate_probe = float(self.app.defaults["tools_mill_feedrate_probe"])
|
|
|
|
# make sure that trying to make a CNCJob from an empty file is not creating an app crash
|
|
if not self.solid_geometry:
|
|
a = 0
|
|
for tooluid_key in self.tools:
|
|
if self.tools[tooluid_key]['solid_geometry'] is None:
|
|
a += 1
|
|
if a == len(self.tools):
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
|
|
return 'fail'
|
|
|
|
total_gcode = ''
|
|
for tooluid_key in list(tools_dict.keys()):
|
|
tool_cnt += 1
|
|
dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
|
|
tooldia_val = app_obj.dec_format(float(tools_dict[tooluid_key]['tooldia']), self.decimals)
|
|
dia_cnc_dict.update({
|
|
'tooldia': tooldia_val
|
|
})
|
|
if "optimization_type" not in tools_dict[tooluid_key]['data']:
|
|
tools_dict[tooluid_key]['data']["tools_mill_optimization_type"] = \
|
|
self.app.defaults["tools_mill_optimization_type"]
|
|
|
|
# find the tool_dia associated with the tooluid_key
|
|
# search in the self.tools for the sel_tool_dia and when found see what tooluid has
|
|
# on the found tooluid in self.tools we also have the solid_geometry that interest us
|
|
# for k, v in self.tools.items():
|
|
# if float('%.*f' % (self.decimals, float(v['tooldia']))) == tooldia_val:
|
|
# current_uid = int(k)
|
|
# break
|
|
|
|
if dia_cnc_dict['data']['tools_mill_offset_type'].lower() == 'in':
|
|
tool_offset = -tooldia_val / 2
|
|
elif dia_cnc_dict['data']['tools_mill_offset_type'].lower() == 'out':
|
|
tool_offset = tooldia_val / 2
|
|
elif dia_cnc_dict['data']['tools_mill_offset_type'].lower() == 'custom':
|
|
offset_value = float(self.ui.tool_offset_entry.get_value())
|
|
if offset_value:
|
|
tool_offset = float(offset_value)
|
|
else:
|
|
self.app.inform.emit('[WARNING] %s' %
|
|
_("Tool Offset is selected in Tool Table but "
|
|
"no value is provided.\n"
|
|
"Add a Tool Offset or change the Offset Type."))
|
|
return
|
|
else:
|
|
tool_offset = 0.0
|
|
|
|
dia_cnc_dict['data']['tools_mill_offset_type'] = tool_offset
|
|
|
|
# z_cut = tools_dict[tooluid_key]['data']["cutz"]
|
|
# z_move = tools_dict[tooluid_key]['data']["travelz"]
|
|
# feedrate = tools_dict[tooluid_key]['data']["feedrate"]
|
|
# feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"]
|
|
# feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
|
|
# multidepth = tools_dict[tooluid_key]['data']["multidepth"]
|
|
# extracut = tools_dict[tooluid_key]['data']["extracut"]
|
|
# extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
|
|
# depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
|
|
# toolchange = tools_dict[tooluid_key]['data']["toolchange"]
|
|
# toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
|
|
# toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"]
|
|
# startz = tools_dict[tooluid_key]['data']["startz"]
|
|
# endz = tools_dict[tooluid_key]['data']["endz"]
|
|
# endxy = self.options["endxy"]
|
|
# spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"]
|
|
# dwell = tools_dict[tooluid_key]['data']["dwell"]
|
|
# dwelltime = tools_dict[tooluid_key]['data']["dwelltime"]
|
|
# pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"]
|
|
#
|
|
# spindledir = self.app.defaults['geometry_spindledir']
|
|
tool_solid_geometry = self.tools[tooluid_key]['solid_geometry']
|
|
|
|
job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
|
|
job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
|
|
|
|
# Propagate options
|
|
job_obj.options["tooldia"] = tooldia_val
|
|
job_obj.options['type'] = 'Geometry'
|
|
job_obj.options['tool_dia'] = tooldia_val
|
|
|
|
# it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
|
|
# to a value of 0.0005 which is 20 times less than 0.01
|
|
tol = float(self.app.defaults['global_tolerance']) / 20
|
|
|
|
tool_lst = list(tools_dict.keys())
|
|
is_first = True if tooluid_key == tool_lst[0] else False
|
|
is_last = True if tooluid_key == tool_lst[-1] else False
|
|
res, start_gcode = job_obj.geometry_tool_gcode_gen(tooluid_key, tools_dict, first_pt=(0, 0),
|
|
tolerance=tol,
|
|
is_first=is_first, is_last=is_last,
|
|
toolchange=True)
|
|
if res == 'fail':
|
|
self.app.log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
|
|
return 'fail'
|
|
else:
|
|
dia_cnc_dict['gcode'] = res
|
|
total_gcode += res
|
|
|
|
if start_gcode != '':
|
|
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(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
|
|
# for bounding box values
|
|
# geo_for_bound_values = unary_union([
|
|
# geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True
|
|
# ])
|
|
try:
|
|
dia_cnc_dict['solid_geometry'] = deepcopy(tool_solid_geometry)
|
|
app_obj.inform.emit('[success] %s...' % _("Finished G-Code processing"))
|
|
except Exception as ee:
|
|
app_obj.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(ee)))
|
|
|
|
job_obj.tools.update({
|
|
tooluid_key: deepcopy(dia_cnc_dict)
|
|
})
|
|
dia_cnc_dict.clear()
|
|
|
|
job_obj.source_file = total_gcode
|
|
|
|
if use_thread:
|
|
# To be run in separate thread
|
|
def job_thread(a_obj):
|
|
if self.multigeo is False:
|
|
with self.app.proc_container.new('%s...' % _("Generating")):
|
|
ret_val = a_obj.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
|
|
if ret_val != 'fail':
|
|
a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
|
|
else:
|
|
with self.app.proc_container.new('%s...' % _("Generating")):
|
|
ret_val = a_obj.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
|
|
if ret_val != 'fail':
|
|
a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
|
|
|
|
# Create a promise with the name
|
|
self.app.collection.promise(outname)
|
|
# Send to worker
|
|
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
|
else:
|
|
if self.solid_geometry:
|
|
self.app.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
|
|
else:
|
|
self.app.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
|
|
|
|
def generatecncjob(self, outname=None, dia=None, offset=None, z_cut=None, z_move=None, feedrate=None,
|
|
feedrate_z=None, feedrate_rapid=None, spindlespeed=None, dwell=None, dwelltime=None,
|
|
multidepth=None, dpp=None, toolchange=None, toolchangez=None, toolchangexy=None,
|
|
extracut=None, extracut_length=None, startz=None, endz=None, endxy=None, pp=None,
|
|
segx=None, segy=None, use_thread=True, plot=True):
|
|
"""
|
|
Only used by the TCL Command Cncjob.
|
|
Creates a CNCJob out of this Geometry object. The actual
|
|
work is done by the target camlib.CNCjob
|
|
`generate_from_geometry_2()` method.
|
|
|
|
:param outname: Name of the new object
|
|
:param dia: Tool diameter
|
|
:param offset:
|
|
:param z_cut: Cut depth (negative value)
|
|
:param z_move: Height of the tool when travelling (not cutting)
|
|
:param feedrate: Feed rate while cutting on X - Y plane
|
|
:param feedrate_z: Feed rate while cutting on Z plane
|
|
:param feedrate_rapid: Feed rate while moving with rapids
|
|
:param spindlespeed: Spindle speed (RPM)
|
|
:param dwell:
|
|
:param dwelltime:
|
|
:param multidepth:
|
|
:param dpp: Depth for each pass when multidepth parameter is True
|
|
:param toolchange:
|
|
:param toolchangez:
|
|
:param toolchangexy: A sequence ox X,Y coordinates: a 2-length tuple or a string.
|
|
Coordinates in X,Y plane for the Toolchange event
|
|
:param extracut:
|
|
:param extracut_length:
|
|
:param startz:
|
|
:param endz:
|
|
:param endxy: A sequence ox X,Y coordinates: a 2-length tuple or a string.
|
|
Coordinates in X, Y plane for the last move after ending the job.
|
|
:param pp: Name of the preprocessor
|
|
:param segx:
|
|
:param segy:
|
|
:param use_thread:
|
|
:param plot:
|
|
:return: None
|
|
"""
|
|
|
|
tooldia = dia if dia else float(self.options["tools_mill_tooldia"])
|
|
outname = outname if outname is not None else self.options["name"]
|
|
|
|
z_cut = z_cut if z_cut is not None else float(self.options["cutz"])
|
|
z_move = z_move if z_move is not None else float(self.options["travelz"])
|
|
|
|
feedrate = feedrate if feedrate is not None else float(self.options["feedrate"])
|
|
feedrate_z = feedrate_z if feedrate_z is not None else float(self.options["feedrate_z"])
|
|
feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else float(self.options["feedrate_rapid"])
|
|
|
|
multidepth = multidepth if multidepth is not None else self.options["multidepth"]
|
|
depthperpass = dpp if dpp is not None else float(self.options["depthperpass"])
|
|
|
|
segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
|
|
segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
|
|
|
|
extracut = extracut if extracut is not None else float(self.options["extracut"])
|
|
extracut_length = extracut_length if extracut_length is not None else float(self.options["extracut_length"])
|
|
|
|
startz = startz if startz is not None else self.options["startz"]
|
|
endz = endz if endz is not None else float(self.options["endz"])
|
|
|
|
endxy = endxy if endxy else self.options["endxy"]
|
|
if isinstance(endxy, str):
|
|
endxy = re.sub('[()\[\]]', '', endxy)
|
|
if endxy and endxy != '':
|
|
endxy = [float(eval(a)) for a in endxy.split(",")]
|
|
|
|
toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"])
|
|
|
|
toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
|
|
if isinstance(toolchangexy, str):
|
|
toolchangexy = re.sub('[()\[\]]', '', toolchangexy)
|
|
if toolchangexy and toolchangexy != '':
|
|
toolchangexy = [float(eval(a)) for a in toolchangexy.split(",")]
|
|
|
|
toolchange = toolchange if toolchange else self.options["toolchange"]
|
|
|
|
offset = offset if offset else 0.0
|
|
|
|
# int or None.
|
|
spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed']
|
|
dwell = dwell if dwell else self.options["dwell"]
|
|
dwelltime = dwelltime if dwelltime else float(self.options["dwelltime"])
|
|
|
|
ppname_g = pp if pp else self.options["ppname_g"]
|
|
|
|
# Object initialization function for app.app_obj.new_object()
|
|
# RUNNING ON SEPARATE THREAD!
|
|
def job_init(job_obj, app_obj):
|
|
assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
|
|
|
|
# Propagate options
|
|
job_obj.options["tooldia"] = tooldia
|
|
|
|
job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
|
|
job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
|
|
|
|
job_obj.options['type'] = 'Geometry'
|
|
job_obj.options['tool_dia'] = tooldia
|
|
|
|
job_obj.segx = segx
|
|
job_obj.segy = segy
|
|
|
|
job_obj.z_pdepth = float(self.options["z_pdepth"])
|
|
job_obj.feedrate_probe = float(self.options["feedrate_probe"])
|
|
|
|
job_obj.options['xmin'] = self.options['xmin']
|
|
job_obj.options['ymin'] = self.options['ymin']
|
|
job_obj.options['xmax'] = self.options['xmax']
|
|
job_obj.options['ymax'] = self.options['ymax']
|
|
|
|
# it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
|
|
# to a value of 0.0005 which is 20 times less than 0.01
|
|
tol = float(self.app.defaults['global_tolerance']) / 20
|
|
res, start_gcode = job_obj.generate_from_geometry_2(
|
|
self, tooldia=tooldia, offset=offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate,
|
|
feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, dwell=dwell,
|
|
dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthperpass, toolchange=toolchange,
|
|
toolchangez=toolchangez, toolchangexy=toolchangexy, extracut=extracut, extracut_length=extracut_length,
|
|
startz=startz, endz=endz, endxy=endxy, pp_geometry_name=ppname_g, is_first=True)
|
|
|
|
if start_gcode != '':
|
|
job_obj.gc_start = start_gcode
|
|
|
|
job_obj.source_file = start_gcode + res
|
|
job_obj.gcode_parse()
|
|
app_obj.inform.emit('[success] %s...' % _("Finished G-Code processing"))
|
|
|
|
if use_thread:
|
|
# To be run in separate thread
|
|
def job_thread(app_obj):
|
|
with self.app.proc_container.new('%s...' % _("Generating")):
|
|
app_obj.app_obj.new_object("cncjob", outname, job_init, plot=plot)
|
|
app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
|
|
|
|
# Create a promise with the name
|
|
self.app.collection.promise(outname)
|
|
# Send to worker
|
|
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
|
else:
|
|
self.app.app_obj.new_object("cncjob", outname, job_init, plot=plot)
|
|
|
|
def scale(self, xfactor, yfactor=None, point=None):
|
|
"""
|
|
Scales all geometry by a given factor.
|
|
|
|
:param xfactor: Factor by which to scale the object's geometry/
|
|
:type xfactor: float
|
|
:param yfactor: Factor by which to scale the object's geometry/
|
|
:type yfactor: float
|
|
:param point: Point around which to scale
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
self.app.log.debug("FlatCAMObj.GeometryObject.scale()")
|
|
|
|
try:
|
|
xfactor = float(xfactor)
|
|
except Exception:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float."))
|
|
return
|
|
|
|
if yfactor is None:
|
|
yfactor = xfactor
|
|
else:
|
|
try:
|
|
yfactor = float(yfactor)
|
|
except Exception:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float."))
|
|
return
|
|
|
|
if xfactor == 1 and yfactor == 1:
|
|
return
|
|
|
|
if point is None:
|
|
px = 0
|
|
py = 0
|
|
else:
|
|
px, py = point
|
|
|
|
self.geo_len = 0
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
def scale_recursion(geom):
|
|
if type(geom) is list:
|
|
geoms = []
|
|
for local_geom in geom:
|
|
geoms.append(scale_recursion(local_geom))
|
|
return geoms
|
|
else:
|
|
try:
|
|
self.el_count += 1
|
|
disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
|
|
if self.old_disp_number < disp_number <= 100:
|
|
self.app.proc_container.update_view_text(' %d%%' % disp_number)
|
|
self.old_disp_number = disp_number
|
|
|
|
return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
|
|
except AttributeError:
|
|
return geom
|
|
|
|
if self.multigeo is True:
|
|
for tool in self.tools:
|
|
# variables to display the percentage of work done
|
|
self.geo_len = 0
|
|
try:
|
|
self.geo_len = len(self.tools[tool]['solid_geometry'])
|
|
except TypeError:
|
|
self.geo_len = 1
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
|
|
|
|
try:
|
|
# variables to display the percentage of work done
|
|
self.geo_len = 0
|
|
try:
|
|
self.geo_len = len(self.solid_geometry)
|
|
except TypeError:
|
|
self.geo_len = 1
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
self.solid_geometry = scale_recursion(self.solid_geometry)
|
|
except AttributeError:
|
|
self.solid_geometry = []
|
|
return
|
|
|
|
self.app.proc_container.new_text = ''
|
|
self.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
def offset(self, vect):
|
|
"""
|
|
Offsets all geometry by a given vector/
|
|
|
|
:param vect: (x, y) vector by which to offset the object's geometry.
|
|
:type vect: tuple
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
self.app.log.debug("FlatCAMObj.GeometryObject.offset()")
|
|
|
|
try:
|
|
dx, dy = vect
|
|
except TypeError:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("An (x,y) pair of values are needed. "
|
|
"Probable you entered only one value in the Offset field.")
|
|
)
|
|
return
|
|
|
|
if dx == 0 and dy == 0:
|
|
return
|
|
|
|
self.geo_len = 0
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
def translate_recursion(geom):
|
|
if type(geom) is list:
|
|
geoms = []
|
|
for local_geom in geom:
|
|
geoms.append(translate_recursion(local_geom))
|
|
return geoms
|
|
else:
|
|
try:
|
|
self.el_count += 1
|
|
disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
|
|
if self.old_disp_number < disp_number <= 100:
|
|
self.app.proc_container.update_view_text(' %d%%' % disp_number)
|
|
self.old_disp_number = disp_number
|
|
|
|
return affinity.translate(geom, xoff=dx, yoff=dy)
|
|
except AttributeError:
|
|
return geom
|
|
|
|
if self.multigeo is True:
|
|
for tool in self.tools:
|
|
# variables to display the percentage of work done
|
|
self.geo_len = 0
|
|
try:
|
|
self.geo_len = len(self.tools[tool]['solid_geometry'])
|
|
except TypeError:
|
|
self.geo_len = 1
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
|
|
|
|
# variables to display the percentage of work done
|
|
self.geo_len = 0
|
|
try:
|
|
self.geo_len = len(self.solid_geometry)
|
|
except TypeError:
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
self.el_count = 0
|
|
|
|
self.solid_geometry = translate_recursion(self.solid_geometry)
|
|
|
|
self.app.proc_container.new_text = ''
|
|
self.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
def convert_units(self, units):
|
|
self.app.log.debug("FlatCAMObj.GeometryObject.convert_units()")
|
|
|
|
self.ui_disconnect()
|
|
|
|
factor = Geometry.convert_units(self, units)
|
|
|
|
self.options['cutz'] = float(self.options['cutz']) * factor
|
|
self.options['depthperpass'] = float(self.options['depthperpass']) * factor
|
|
self.options['travelz'] = float(self.options['travelz']) * factor
|
|
self.options['feedrate'] = float(self.options['feedrate']) * factor
|
|
self.options['feedrate_z'] = float(self.options['feedrate_z']) * factor
|
|
self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
|
|
self.options['endz'] = float(self.options['endz']) * factor
|
|
# self.options['tools_mill_tooldia'] *= factor
|
|
# self.options['painttooldia'] *= factor
|
|
# self.options['paintmargin'] *= factor
|
|
# self.options['paintoverlap'] *= factor
|
|
|
|
self.options["toolchangez"] = float(self.options["toolchangez"]) * factor
|
|
|
|
if self.app.defaults["tools_mill_toolchangexy"] == '':
|
|
self.options['toolchangexy'] = "0.0, 0.0"
|
|
else:
|
|
coords_xy = [float(eval(coord)) for coord in self.app.defaults["tools_mill_toolchangexy"].split(",")]
|
|
if len(coords_xy) < 2:
|
|
self.app.inform.emit('[ERROR] %s' %
|
|
_("The Toolchange X,Y field in Edit -> Preferences "
|
|
"has to be in the format (x, y)\n"
|
|
"but now there is only one value, not two.")
|
|
)
|
|
return 'fail'
|
|
coords_xy[0] *= factor
|
|
coords_xy[1] *= factor
|
|
self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
|
|
|
|
if self.options['startz'] is not None:
|
|
self.options['startz'] = float(self.options['startz']) * factor
|
|
|
|
param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
|
|
'endz', 'toolchangez']
|
|
|
|
if isinstance(self, GeometryObject):
|
|
temp_tools_dict = {}
|
|
tool_dia_copy = {}
|
|
data_copy = {}
|
|
for tooluid_key, tooluid_value in self.tools.items():
|
|
for dia_key, dia_value in tooluid_value.items():
|
|
if dia_key == 'tooldia':
|
|
dia_value *= factor
|
|
dia_value = float('%.*f' % (self.decimals, dia_value))
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'offset':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'offset_value':
|
|
dia_value *= factor
|
|
tool_dia_copy[dia_key] = dia_value
|
|
|
|
# convert the value in the Custom Tool Offset entry in UI
|
|
custom_offset = None
|
|
try:
|
|
custom_offset = float(self.ui.tool_offset_entry.get_value())
|
|
except ValueError:
|
|
# try to convert comma to decimal point. if it's still not working error message and return
|
|
try:
|
|
custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
|
|
except ValueError:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Wrong value format entered, use a number."))
|
|
return
|
|
except TypeError:
|
|
pass
|
|
|
|
if custom_offset:
|
|
custom_offset *= factor
|
|
self.ui.tool_offset_entry.set_value(custom_offset)
|
|
|
|
if dia_key == 'tool_type':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'data':
|
|
for data_key, data_value in dia_value.items():
|
|
# convert the form fields that are convertible
|
|
for param in param_list:
|
|
if data_key == param and data_value is not None:
|
|
data_copy[data_key] = data_value * factor
|
|
# copy the other dict entries that are not convertible
|
|
if data_key not in param_list:
|
|
data_copy[data_key] = data_value
|
|
tool_dia_copy[dia_key] = deepcopy(data_copy)
|
|
data_copy.clear()
|
|
|
|
temp_tools_dict.update({
|
|
tooluid_key: deepcopy(tool_dia_copy)
|
|
})
|
|
tool_dia_copy.clear()
|
|
|
|
self.tools.clear()
|
|
self.tools = deepcopy(temp_tools_dict)
|
|
|
|
return factor
|
|
|
|
def plot_element(self, element, color=None, visible=None):
|
|
|
|
if color is None:
|
|
color = '#FF0000FF'
|
|
|
|
visible = visible if visible else self.options['plot']
|
|
try:
|
|
if isinstance(element, (MultiPolygon, MultiLineString)):
|
|
for sub_el in element.geoms:
|
|
self.plot_element(sub_el, color=color)
|
|
else:
|
|
for sub_el in element:
|
|
self.plot_element(sub_el, color=color)
|
|
except TypeError: # Element is not iterable...
|
|
# if self.app.use_3d_engine:
|
|
self.add_shape(shape=element, color=color, visible=visible, layer=0)
|
|
|
|
def plot(self, visible=None, kind=None, plot_tool=None):
|
|
"""
|
|
Plot the object.
|
|
|
|
:param visible: Controls if the added shape is visible of not
|
|
:param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob,
|
|
because CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot()
|
|
has to be rewritten
|
|
:param plot_tool: plot a specific tool for multigeo objects
|
|
:return:
|
|
"""
|
|
|
|
# Does all the required setup and returns False
|
|
# if the 'ptint' option is set to False.
|
|
if not FlatCAMObj.plot(self):
|
|
return
|
|
|
|
if self.app.use_3d_engine:
|
|
def random_color():
|
|
r_color = np.random.rand(4)
|
|
r_color[3] = 1
|
|
return r_color
|
|
else:
|
|
def random_color():
|
|
while True:
|
|
r_color = np.random.rand(4)
|
|
r_color[3] = 1
|
|
|
|
new_color = '#'
|
|
for idx in range(len(r_color)):
|
|
new_color += '%x' % int(r_color[idx] * 255)
|
|
# do it until a valid color is generated
|
|
# a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha
|
|
# for a total of 9 chars
|
|
if len(new_color) == 9:
|
|
break
|
|
return new_color
|
|
|
|
try:
|
|
# plot solid geometries found as members of self.tools attribute dict
|
|
# for MultiGeo
|
|
if self.multigeo is True: # geo multi tool usage
|
|
if plot_tool is None:
|
|
for tooluid_key in self.tools:
|
|
solid_geometry = self.tools[tooluid_key]['solid_geometry']
|
|
if 'override_color' in self.tools[tooluid_key]['data']:
|
|
color = self.tools[tooluid_key]['data']['override_color']
|
|
else:
|
|
color = random_color() if self.options['multicolored'] else \
|
|
self.app.defaults["geometry_plot_line"]
|
|
|
|
self.plot_element(solid_geometry, visible=visible, color=color)
|
|
else:
|
|
solid_geometry = self.tools[plot_tool]['solid_geometry']
|
|
if 'override_color' in self.tools[plot_tool]['data']:
|
|
color = self.tools[plot_tool]['data']['override_color']
|
|
else:
|
|
color = random_color() if self.options['multicolored'] else \
|
|
self.app.defaults["geometry_plot_line"]
|
|
|
|
self.plot_element(solid_geometry, visible=visible, color=color)
|
|
else:
|
|
# plot solid geometry that may be an direct attribute of the geometry object
|
|
# for SingleGeo
|
|
if self.solid_geometry:
|
|
solid_geometry = self.solid_geometry
|
|
color = self.app.defaults["geometry_plot_line"]
|
|
|
|
self.plot_element(solid_geometry, visible=visible, color=color)
|
|
|
|
# self.plot_element(self.solid_geometry, visible=self.options['plot'])
|
|
|
|
self.shapes.redraw()
|
|
|
|
except (ObjectDeleted, AttributeError):
|
|
self.shapes.clear(update=True)
|
|
|
|
def on_plot_cb_click(self):
|
|
if self.muted_ui:
|
|
return
|
|
|
|
self.read_form_item('plot')
|
|
self.plot()
|
|
|
|
self.ui_disconnect()
|
|
cb_flag = self.ui.plot_cb.isChecked()
|
|
for row in range(self.ui.geo_tools_table.rowCount()):
|
|
table_cb = self.ui.geo_tools_table.cellWidget(row, 6)
|
|
if cb_flag:
|
|
table_cb.setChecked(True)
|
|
else:
|
|
table_cb.setChecked(False)
|
|
self.ui_connect()
|
|
|
|
def on_plot_cb_click_table(self):
|
|
# self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
|
|
self.ui_disconnect()
|
|
# cw = self.sender()
|
|
# cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
|
|
# cw_row = cw_index.row()
|
|
check_row = 0
|
|
|
|
self.shapes.clear(update=True)
|
|
|
|
for tooluid_key in self.tools:
|
|
solid_geometry = self.tools[tooluid_key]['solid_geometry']
|
|
|
|
# find the geo_plugin_table row associated with the tooluid_key
|
|
for row in range(self.ui.geo_tools_table.rowCount()):
|
|
tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
|
|
if tooluid_item == int(tooluid_key):
|
|
check_row = row
|
|
break
|
|
|
|
if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
|
|
try:
|
|
color = self.tools[tooluid_key]['data']['override_color']
|
|
self.plot_element(element=solid_geometry, visible=True, color=color)
|
|
except KeyError:
|
|
self.plot_element(element=solid_geometry, visible=True)
|
|
self.shapes.redraw()
|
|
|
|
# make sure that the general plot is disabled if one of the row plot's are disabled and
|
|
# if all the row plot's are enabled also enable the general plot checkbox
|
|
cb_cnt = 0
|
|
total_row = self.ui.geo_tools_table.rowCount()
|
|
for row in range(total_row):
|
|
if self.ui.geo_tools_table.cellWidget(row, 6).isChecked():
|
|
cb_cnt += 1
|
|
else:
|
|
cb_cnt -= 1
|
|
if cb_cnt < total_row:
|
|
self.ui.plot_cb.setChecked(False)
|
|
else:
|
|
self.ui.plot_cb.setChecked(True)
|
|
self.ui_connect()
|
|
|
|
def on_multicolored_cb_click(self):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('multicolored')
|
|
self.plot()
|
|
|
|
@staticmethod
|
|
def merge(geo_list, geo_final, multi_geo=None, fuse_tools=None):
|
|
"""
|
|
Merges the geometry of objects in grb_list into the geometry of geo_final.
|
|
|
|
:param geo_list: List of GerberObject Objects to join.
|
|
:param geo_final: Destination GerberObject object.
|
|
:param multi_geo: if the merged geometry objects are of type MultiGeo
|
|
:param fuse_tools: If True will try to fuse tools of the same type for the Geometry objects
|
|
:return: None
|
|
"""
|
|
|
|
if geo_final.solid_geometry is None:
|
|
geo_final.solid_geometry = []
|
|
|
|
geo_final.solid_geometry = flatten_shapely_geometry(geo_final.solid_geometry)
|
|
new_solid_geometry = []
|
|
new_options = {}
|
|
new_tools = {}
|
|
|
|
for geo_obj in geo_list:
|
|
for option in geo_obj.options:
|
|
if option != 'name':
|
|
try:
|
|
new_options[option] = deepcopy(geo_obj.options[option])
|
|
except Exception as e:
|
|
log.error("Failed to copy option %s. Error: %s" % (str(option), str(e)))
|
|
|
|
# Expand lists
|
|
if type(geo_obj) is list:
|
|
GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final)
|
|
# If not list, just append
|
|
else:
|
|
if multi_geo is None or multi_geo is False:
|
|
geo_final.multigeo = False
|
|
else:
|
|
geo_final.multigeo = True
|
|
|
|
try:
|
|
new_solid_geometry += deepcopy(geo_obj.solid_geometry)
|
|
except Exception as e:
|
|
log.error("GeometryObject.merge() --> %s" % str(e))
|
|
|
|
# find the tool_uid maximum value in the geo_final
|
|
try:
|
|
max_uid = max([int(i) for i in new_tools.keys()])
|
|
except ValueError:
|
|
max_uid = 0
|
|
|
|
# add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
|
|
# to merge the obj.tools as it is likely there is none to merge.
|
|
if geo_obj.kind != 'gerber' and geo_obj.kind != 'excellon':
|
|
for tool_uid in geo_obj.tools:
|
|
max_uid += 1
|
|
new_tools[max_uid] = deepcopy(geo_obj.tools[tool_uid])
|
|
|
|
geo_final.options.update(new_options)
|
|
geo_final.solid_geometry = new_solid_geometry
|
|
|
|
if new_tools and fuse_tools is True:
|
|
# merge the geometries of the tools that share the same tool diameter and the same tool_type
|
|
# and the same type
|
|
final_tools = {}
|
|
same_dia = defaultdict(list)
|
|
same_type = defaultdict(list)
|
|
same_tool_type = defaultdict(list)
|
|
|
|
# find tools that have the same diameter and group them by diameter
|
|
for k, v in new_tools.items():
|
|
same_dia[v['tooldia']].append(k)
|
|
|
|
# find tools that have the same type (job) and group them by type
|
|
for k, v in new_tools.items():
|
|
same_type[v['data']['tools_mill_job_type']].append(k)
|
|
|
|
# find tools that have the same tool_type and group them by tool_type
|
|
for k, v in new_tools.items():
|
|
same_tool_type[v['data']['tools_mill_tool_shape']].append(k)
|
|
|
|
# find the intersections in the above groups
|
|
intersect_list = []
|
|
for dia, dia_list in same_dia.items():
|
|
for ty, type_list in same_type.items():
|
|
for t_ty, tool_type_list in same_tool_type.items():
|
|
intersection = reduce(np.intersect1d, (dia_list, type_list, tool_type_list)).tolist()
|
|
if intersection:
|
|
intersect_list.append(intersection)
|
|
|
|
new_tool_nr = 1
|
|
for i_lst in intersect_list:
|
|
new_solid_geo = []
|
|
last_tool = None
|
|
for old_tool in i_lst:
|
|
new_solid_geo += new_tools[old_tool]['solid_geometry']
|
|
last_tool = old_tool
|
|
|
|
if new_solid_geo and last_tool:
|
|
final_tools[new_tool_nr] = \
|
|
{
|
|
k: deepcopy(new_tools[last_tool][k]) for k in new_tools[last_tool] if k != 'solid_geometry'
|
|
}
|
|
final_tools[new_tool_nr]['solid_geometry'] = deepcopy(new_solid_geo)
|
|
new_tool_nr += 1
|
|
else:
|
|
final_tools = new_tools
|
|
|
|
# if not final_tools:
|
|
# return 'fail'
|
|
geo_final.tools = final_tools
|
|
|
|
@staticmethod
|
|
def get_pts(o):
|
|
"""
|
|
Returns a list of all points in the object, where
|
|
the object can be a MultiPolygon, Polygon, Not a polygon, or a list
|
|
of such. Search is done recursively.
|
|
|
|
:param: geometric object
|
|
:return: List of points
|
|
:rtype: list
|
|
"""
|
|
pts = []
|
|
|
|
# Iterable: descend into each item.
|
|
try:
|
|
for subo in o:
|
|
pts += GeometryObject.get_pts(subo)
|
|
|
|
# Non-iterable
|
|
except TypeError:
|
|
if o is not None:
|
|
if type(o) == MultiPolygon:
|
|
for poly in o:
|
|
pts += GeometryObject.get_pts(poly)
|
|
# ## Descend into .exerior and .interiors
|
|
elif type(o) == Polygon:
|
|
pts += GeometryObject.get_pts(o.exterior)
|
|
for i in o.interiors:
|
|
pts += GeometryObject.get_pts(i)
|
|
elif type(o) == MultiLineString:
|
|
for line in o:
|
|
pts += GeometryObject.get_pts(line)
|
|
# ## Has .coords: list them.
|
|
else:
|
|
pts += list(o.coords)
|
|
else:
|
|
return
|
|
return pts
|