-clean-up before merge
This commit is contained in:
169
flatcamTools/ToolCalculators.py
Normal file
169
flatcamTools/ToolCalculators.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from PyQt5 import QtGui
|
||||
from GUIElements import FCEntry
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
import math
|
||||
|
||||
|
||||
class ToolCalculator(FlatCAMTool):
|
||||
|
||||
toolName = "Calculators"
|
||||
v_shapeName = "V-Shape Tool Calculator"
|
||||
unitsName = "Units Calculator"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.app = app
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
## V-shape Tool Calculator
|
||||
|
||||
self.v_shape_spacer_label = QtWidgets.QLabel(" ")
|
||||
self.layout.addWidget(self.v_shape_spacer_label)
|
||||
|
||||
## Title of the V-shape Tools Calculator
|
||||
v_shape_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.v_shapeName)
|
||||
self.layout.addWidget(v_shape_title_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout)
|
||||
|
||||
self.tipDia_label = QtWidgets.QLabel("Tip Diameter:")
|
||||
self.tipDia_entry = FCEntry()
|
||||
self.tipDia_entry.setFixedWidth(70)
|
||||
self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.tipDia_entry.setToolTip('This is the diameter of the tool tip.\n'
|
||||
'The manufacturer specifies it.')
|
||||
|
||||
self.tipAngle_label = QtWidgets.QLabel("Tip Angle:")
|
||||
self.tipAngle_entry = FCEntry()
|
||||
self.tipAngle_entry.setFixedWidth(70)
|
||||
self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.tipAngle_entry.setToolTip("This is the angle of the tip of the tool.\n"
|
||||
"It is specified by manufacturer.")
|
||||
|
||||
self.cutDepth_label = QtWidgets.QLabel("Cut Z:")
|
||||
self.cutDepth_entry = FCEntry()
|
||||
self.cutDepth_entry.setFixedWidth(70)
|
||||
self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.cutDepth_entry.setToolTip("This is the depth to cut into the material.\n"
|
||||
"In the CNCJob is the CutZ parameter.")
|
||||
|
||||
self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:")
|
||||
self.effectiveToolDia_entry = FCEntry()
|
||||
self.effectiveToolDia_entry.setFixedWidth(70)
|
||||
self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.effectiveToolDia_entry.setToolTip("This is the tool diameter to be entered into\n"
|
||||
"FlatCAM Gerber section.\n"
|
||||
"In the CNCJob section it is called >Tool dia<.")
|
||||
# self.effectiveToolDia_entry.setEnabled(False)
|
||||
|
||||
|
||||
form_layout.addRow(self.tipDia_label, self.tipDia_entry)
|
||||
form_layout.addRow(self.tipAngle_label, self.tipAngle_entry)
|
||||
form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
|
||||
form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
|
||||
|
||||
|
||||
## Buttons
|
||||
self.calculate_button = QtWidgets.QPushButton("Calculate")
|
||||
self.calculate_button.setFixedWidth(70)
|
||||
self.calculate_button.setToolTip(
|
||||
"Calculate either the Cut Z or the effective tool diameter,\n "
|
||||
"depending on which is desired and which is known. "
|
||||
)
|
||||
self.empty_label = QtWidgets.QLabel(" ")
|
||||
|
||||
form_layout.addRow(self.empty_label, self.calculate_button)
|
||||
|
||||
## Units Calculator
|
||||
self.unists_spacer_label = QtWidgets.QLabel(" ")
|
||||
self.layout.addWidget(self.unists_spacer_label)
|
||||
|
||||
## Title of the Units Calculator
|
||||
units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
|
||||
self.layout.addWidget(units_label)
|
||||
|
||||
#Form Layout
|
||||
form_units_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_units_layout)
|
||||
|
||||
inch_label = QtWidgets.QLabel("INCH")
|
||||
mm_label = QtWidgets.QLabel("MM")
|
||||
|
||||
self.inch_entry = FCEntry()
|
||||
self.inch_entry.setFixedWidth(70)
|
||||
self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM")
|
||||
|
||||
self.mm_entry = FCEntry()
|
||||
self.mm_entry.setFixedWidth(70)
|
||||
self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH")
|
||||
|
||||
form_units_layout.addRow(mm_label, inch_label)
|
||||
form_units_layout.addRow(self.mm_entry, self.inch_entry)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Signals
|
||||
self.cutDepth_entry.textChanged.connect(self.on_calculate_tool_dia)
|
||||
self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia)
|
||||
self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia)
|
||||
self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia)
|
||||
self.calculate_button.clicked.connect(self.on_calculate_tool_dia)
|
||||
|
||||
self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
|
||||
self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
|
||||
|
||||
|
||||
## Initialize form
|
||||
if self.app.defaults["units"] == 'MM':
|
||||
self.tipDia_entry.set_value('0.2')
|
||||
self.tipAngle_entry.set_value('45')
|
||||
self.cutDepth_entry.set_value('0.25')
|
||||
self.effectiveToolDia_entry.set_value('0.39')
|
||||
else:
|
||||
self.tipDia_entry.set_value('7.87402')
|
||||
self.tipAngle_entry.set_value('45')
|
||||
self.cutDepth_entry.set_value('9.84252')
|
||||
self.effectiveToolDia_entry.set_value('15.35433')
|
||||
|
||||
self.mm_entry.set_value('0')
|
||||
self.inch_entry.set_value('0')
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Calc. Tool")
|
||||
|
||||
def on_calculate_tool_dia(self):
|
||||
# Calculation:
|
||||
# Manufacturer gives total angle of the the tip but we need only half of it
|
||||
# tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
|
||||
# effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
|
||||
# tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
|
||||
# effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
|
||||
# effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
|
||||
|
||||
try:
|
||||
tip_diameter = float(self.tipDia_entry.get_value())
|
||||
half_tip_angle = float(self.tipAngle_entry.get_value()) / 2
|
||||
cut_depth = float(self.cutDepth_entry.get_value())
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
|
||||
self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
|
||||
|
||||
def on_calculate_inch_units(self):
|
||||
self.inch_entry.set_value('%.6f' % (float(self.mm_entry.get_value()) / 25.4))
|
||||
|
||||
def on_calculate_mm_units(self):
|
||||
self.mm_entry.set_value('%.6f' % (float(self.inch_entry.get_value()) * 25.4))
|
||||
|
||||
# end of file
|
||||
390
flatcamTools/ToolCutout.py
Normal file
390
flatcamTools/ToolCutout.py
Normal file
@@ -0,0 +1,390 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from copy import copy,deepcopy
|
||||
from ObjectCollection import *
|
||||
from FlatCAMApp import *
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from GUIElements import IntEntry, RadioSet, LengthEntry
|
||||
|
||||
from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
|
||||
|
||||
class ToolCutout(FlatCAMTool):
|
||||
|
||||
toolName = "Cutout PCB Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout)
|
||||
|
||||
## Type of object to be cutout
|
||||
self.type_obj_combo = QtWidgets.QComboBox()
|
||||
self.type_obj_combo.addItem("Gerber")
|
||||
self.type_obj_combo.addItem("Excellon")
|
||||
self.type_obj_combo.addItem("Geometry")
|
||||
|
||||
# we get rid of item1 ("Excellon") as it is not suitable for creating film
|
||||
self.type_obj_combo.view().setRowHidden(1, True)
|
||||
self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
# self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
|
||||
self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
|
||||
self.type_obj_combo_label.setToolTip(
|
||||
"Specify the type of object to be cutout.\n"
|
||||
"It can be of type: Gerber or Geometry.\n"
|
||||
"What is selected here will dictate the kind\n"
|
||||
"of objects that will populate the 'Object' combobox."
|
||||
)
|
||||
form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
|
||||
|
||||
## Object to be cutout
|
||||
self.obj_combo = QtWidgets.QComboBox()
|
||||
self.obj_combo.setModel(self.app.collection)
|
||||
self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.obj_combo.setCurrentIndex(1)
|
||||
self.object_label = QtWidgets.QLabel("Object:")
|
||||
self.object_label.setToolTip(
|
||||
"Object to be cutout. "
|
||||
)
|
||||
form_layout.addRow(self.object_label, self.obj_combo)
|
||||
|
||||
# Tool Diameter
|
||||
self.dia = FCEntry()
|
||||
self.dia_label = QtWidgets.QLabel("Tool Dia:")
|
||||
self.dia_label.setToolTip(
|
||||
"Diameter of the tool used to cutout\n"
|
||||
"the PCB shape out of the surrounding material."
|
||||
)
|
||||
form_layout.addRow(self.dia_label, self.dia)
|
||||
|
||||
# Margin
|
||||
self.margin = FCEntry()
|
||||
self.margin_label = QtWidgets.QLabel("Margin:")
|
||||
self.margin_label.setToolTip(
|
||||
"Margin over bounds. A positive value here\n"
|
||||
"will make the cutout of the PCB further from\n"
|
||||
"the actual PCB border"
|
||||
)
|
||||
form_layout.addRow(self.margin_label, self.margin)
|
||||
|
||||
# Gapsize
|
||||
self.gapsize = FCEntry()
|
||||
self.gapsize_label = QtWidgets.QLabel("Gap size:")
|
||||
self.gapsize_label.setToolTip(
|
||||
"The size of the gaps in the cutout\n"
|
||||
"used to keep the board connected to\n"
|
||||
"the surrounding material (the one \n"
|
||||
"from which the PCB is cutout)."
|
||||
)
|
||||
form_layout.addRow(self.gapsize_label, self.gapsize)
|
||||
|
||||
## Title2
|
||||
title_ff_label = QtWidgets.QLabel("<font size=4><b>FreeForm Cutout</b></font>")
|
||||
self.layout.addWidget(title_ff_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout_2 = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout_2)
|
||||
|
||||
# How gaps wil be rendered:
|
||||
# lr - left + right
|
||||
# tb - top + bottom
|
||||
# 4 - left + right +top + bottom
|
||||
# 2lr - 2*left + 2*right
|
||||
# 2tb - 2*top + 2*bottom
|
||||
# 8 - 2*left + 2*right +2*top + 2*bottom
|
||||
|
||||
# Gaps
|
||||
self.gaps = FCEntry()
|
||||
self.gaps_label = QtWidgets.QLabel("Type of gaps: ")
|
||||
self.gaps_label.setToolTip(
|
||||
"Number of gaps used for the cutout.\n"
|
||||
"There can be maximum 8 bridges/gaps.\n"
|
||||
"The choices are:\n"
|
||||
"- lr - left + right\n"
|
||||
"- tb - top + bottom\n"
|
||||
"- 4 - left + right +top + bottom\n"
|
||||
"- 2lr - 2*left + 2*right\n"
|
||||
"- 2tb - 2*top + 2*bottom\n"
|
||||
"- 8 - 2*left + 2*right +2*top + 2*bottom"
|
||||
)
|
||||
form_layout_2.addRow(self.gaps_label, self.gaps)
|
||||
|
||||
## Buttons
|
||||
hlay = QtWidgets.QHBoxLayout()
|
||||
self.layout.addLayout(hlay)
|
||||
|
||||
hlay.addStretch()
|
||||
self.ff_cutout_object_btn = QtWidgets.QPushButton(" FreeForm Cutout Object ")
|
||||
self.ff_cutout_object_btn.setToolTip(
|
||||
"Cutout the selected object.\n"
|
||||
"The cutout shape can be any shape.\n"
|
||||
"Useful when the PCB has a non-rectangular shape.\n"
|
||||
"But if the object to be cutout is of Gerber Type,\n"
|
||||
"it needs to be an outline of the actual board shape."
|
||||
)
|
||||
hlay.addWidget(self.ff_cutout_object_btn)
|
||||
|
||||
## Title3
|
||||
title_rct_label = QtWidgets.QLabel("<font size=4><b>Rectangular Cutout</b></font>")
|
||||
self.layout.addWidget(title_rct_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout_3 = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout_3)
|
||||
|
||||
gapslabel_rect = QtWidgets.QLabel('Type of gaps:')
|
||||
gapslabel_rect.setToolTip(
|
||||
"Where to place the gaps:\n"
|
||||
"- one gap Top / one gap Bottom\n"
|
||||
"- one gap Left / one gap Right\n"
|
||||
"- one gap on each of the 4 sides."
|
||||
)
|
||||
self.gaps_rect_radio = RadioSet([{'label': 'T/B', 'value': 'tb'},
|
||||
{'label': 'L/R', 'value': 'lr'},
|
||||
{'label': '4', 'value': '4'}])
|
||||
form_layout_3.addRow(gapslabel_rect, self.gaps_rect_radio)
|
||||
|
||||
hlay2 = QtWidgets.QHBoxLayout()
|
||||
self.layout.addLayout(hlay2)
|
||||
|
||||
hlay2.addStretch()
|
||||
self.rect_cutout_object_btn = QtWidgets.QPushButton("Rectangular Cutout Object")
|
||||
self.rect_cutout_object_btn.setToolTip(
|
||||
"Cutout the selected object.\n"
|
||||
"The resulting cutout shape is\n"
|
||||
"always of a rectangle form and it will be\n"
|
||||
"the bounding box of the Object."
|
||||
)
|
||||
hlay2.addWidget(self.rect_cutout_object_btn)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Init GUI
|
||||
self.dia.set_value(1)
|
||||
self.margin.set_value(0)
|
||||
self.gapsize.set_value(1)
|
||||
self.gaps.set_value(4)
|
||||
self.gaps_rect_radio.set_value("4")
|
||||
|
||||
## Signals
|
||||
self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
|
||||
self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
|
||||
|
||||
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
|
||||
|
||||
def on_type_obj_index_changed(self, index):
|
||||
obj_type = self.type_obj_combo.currentIndex()
|
||||
self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.obj_combo.setCurrentIndex(0)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Cutout Tool")
|
||||
|
||||
def on_freeform_cutout(self):
|
||||
|
||||
def subtract_rectangle(obj_, x0, y0, x1, y1):
|
||||
pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||||
obj_.subtract_polygon(pts)
|
||||
|
||||
name = self.obj_combo.currentText()
|
||||
|
||||
# Get source object.
|
||||
try:
|
||||
cutout_obj = self.app.collection.get_by_name(str(name))
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
|
||||
return "Could not retrieve object: %s" % name
|
||||
|
||||
if cutout_obj is None:
|
||||
self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
|
||||
|
||||
try:
|
||||
dia = float(self.dia.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
margin = float(self.margin.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
gapsize = float(self.gapsize.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
gaps = self.gaps.get_value()
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
|
||||
return
|
||||
|
||||
if 0 in {dia}:
|
||||
self.app.inform.emit("[warning_notcl]Tool Diameter is zero value. Change it to a positive integer.")
|
||||
return "Tool Diameter is zero value. Change it to a positive integer."
|
||||
|
||||
if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']:
|
||||
self.app.inform.emit("[warning_notcl] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
|
||||
"Fill in a correct value and retry. ")
|
||||
return
|
||||
|
||||
# Get min and max data for each object as we just cut rectangles across X or Y
|
||||
xmin, ymin, xmax, ymax = cutout_obj.bounds()
|
||||
px = 0.5 * (xmin + xmax) + margin
|
||||
py = 0.5 * (ymin + ymax) + margin
|
||||
lenghtx = (xmax - xmin) + (margin * 2)
|
||||
lenghty = (ymax - ymin) + (margin * 2)
|
||||
|
||||
gapsize = gapsize + (dia / 2)
|
||||
|
||||
if isinstance(cutout_obj,FlatCAMGeometry):
|
||||
# rename the obj name so it can be identified as cutout
|
||||
cutout_obj.options["name"] += "_cutout"
|
||||
else:
|
||||
cutout_obj.isolate(dia=dia, passes=1, overlap=1, combine=False, outname="_temp")
|
||||
ext_obj = self.app.collection.get_by_name("_temp")
|
||||
|
||||
def geo_init(geo_obj, app_obj):
|
||||
geo_obj.solid_geometry = obj_exteriors
|
||||
|
||||
outname = cutout_obj.options["name"] + "_cutout"
|
||||
|
||||
obj_exteriors = ext_obj.get_exteriors()
|
||||
self.app.new_object('geometry', outname, geo_init)
|
||||
|
||||
self.app.collection.set_all_inactive()
|
||||
self.app.collection.set_active("_temp")
|
||||
self.app.on_delete()
|
||||
|
||||
cutout_obj = self.app.collection.get_by_name(outname)
|
||||
|
||||
if int(gaps) == 8 or gaps == '2lr':
|
||||
subtract_rectangle(cutout_obj,
|
||||
xmin - gapsize, # botleft_x
|
||||
py - gapsize + lenghty / 4, # botleft_y
|
||||
xmax + gapsize, # topright_x
|
||||
py + gapsize + lenghty / 4) # topright_y
|
||||
subtract_rectangle(cutout_obj,
|
||||
xmin - gapsize,
|
||||
py - gapsize - lenghty / 4,
|
||||
xmax + gapsize,
|
||||
py + gapsize - lenghty / 4)
|
||||
|
||||
if int(gaps) == 8 or gaps == '2tb':
|
||||
subtract_rectangle(cutout_obj,
|
||||
px - gapsize + lenghtx / 4,
|
||||
ymin - gapsize,
|
||||
px + gapsize + lenghtx / 4,
|
||||
ymax + gapsize)
|
||||
subtract_rectangle(cutout_obj,
|
||||
px - gapsize - lenghtx / 4,
|
||||
ymin - gapsize,
|
||||
px + gapsize - lenghtx / 4,
|
||||
ymax + gapsize)
|
||||
|
||||
if int(gaps) == 4 or gaps == 'lr':
|
||||
subtract_rectangle(cutout_obj,
|
||||
xmin - gapsize,
|
||||
py - gapsize,
|
||||
xmax + gapsize,
|
||||
py + gapsize)
|
||||
|
||||
if int(gaps) == 4 or gaps == 'tb':
|
||||
subtract_rectangle(cutout_obj,
|
||||
px - gapsize,
|
||||
ymin - gapsize,
|
||||
px + gapsize,
|
||||
ymax + gapsize)
|
||||
|
||||
cutout_obj.plot()
|
||||
self.app.inform.emit("[success] Any form CutOut operation finished.")
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
||||
|
||||
def on_rectangular_cutout(self):
|
||||
name = self.obj_combo.currentText()
|
||||
|
||||
# Get source object.
|
||||
try:
|
||||
cutout_obj = self.app.collection.get_by_name(str(name))
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
|
||||
return "Could not retrieve object: %s" % name
|
||||
|
||||
if cutout_obj is None:
|
||||
self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
|
||||
|
||||
try:
|
||||
dia = float(self.dia.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
margin = float(self.margin.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
gapsize = float(self.gapsize.get_value())
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
|
||||
return
|
||||
try:
|
||||
gaps = self.gaps_rect_radio.get_value()
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
|
||||
return
|
||||
|
||||
if 0 in {dia}:
|
||||
self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.")
|
||||
return "Tool Diameter is zero value. Change it to a positive integer."
|
||||
|
||||
def geo_init(geo_obj, app_obj):
|
||||
real_margin = margin + (dia / 2)
|
||||
real_gap_size = gapsize + dia
|
||||
|
||||
minx, miny, maxx, maxy = cutout_obj.bounds()
|
||||
minx -= real_margin
|
||||
maxx += real_margin
|
||||
miny -= real_margin
|
||||
maxy += real_margin
|
||||
midx = 0.5 * (minx + maxx)
|
||||
midy = 0.5 * (miny + maxy)
|
||||
hgap = 0.5 * real_gap_size
|
||||
pts = [[midx - hgap, maxy],
|
||||
[minx, maxy],
|
||||
[minx, midy + hgap],
|
||||
[minx, midy - hgap],
|
||||
[minx, miny],
|
||||
[midx - hgap, miny],
|
||||
[midx + hgap, miny],
|
||||
[maxx, miny],
|
||||
[maxx, midy - hgap],
|
||||
[maxx, midy + hgap],
|
||||
[maxx, maxy],
|
||||
[midx + hgap, maxy]]
|
||||
cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
|
||||
[pts[6], pts[7], pts[10], pts[11]]],
|
||||
"lr": [[pts[9], pts[10], pts[1], pts[2]],
|
||||
[pts[3], pts[4], pts[7], pts[8]]],
|
||||
"4": [[pts[0], pts[1], pts[2]],
|
||||
[pts[3], pts[4], pts[5]],
|
||||
[pts[6], pts[7], pts[8]],
|
||||
[pts[9], pts[10], pts[11]]]}
|
||||
cuts = cases[gaps]
|
||||
geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
|
||||
|
||||
# TODO: Check for None
|
||||
self.app.new_object("geometry", name + "_cutout", geo_init)
|
||||
self.app.inform.emit("[success] Rectangular CutOut operation finished.")
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
||||
|
||||
def reset_fields(self):
|
||||
self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
467
flatcamTools/ToolDblSided.py
Normal file
467
flatcamTools/ToolDblSided.py
Normal file
@@ -0,0 +1,467 @@
|
||||
from PyQt5 import QtGui
|
||||
from GUIElements import RadioSet, EvalEntry, LengthEntry
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
from shapely.geometry import Point
|
||||
from shapely import affinity
|
||||
from PyQt5 import QtCore
|
||||
|
||||
class DblSidedTool(FlatCAMTool):
|
||||
|
||||
toolName = "Double-Sided PCB Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
self.empty_lb = QtWidgets.QLabel("")
|
||||
self.layout.addWidget(self.empty_lb)
|
||||
|
||||
## Grid Layout
|
||||
grid_lay = QtWidgets.QGridLayout()
|
||||
self.layout.addLayout(grid_lay)
|
||||
|
||||
## Gerber Object to mirror
|
||||
self.gerber_object_combo = QtWidgets.QComboBox()
|
||||
self.gerber_object_combo.setModel(self.app.collection)
|
||||
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.gerber_object_combo.setCurrentIndex(1)
|
||||
|
||||
self.botlay_label = QtWidgets.QLabel("<b>GERBER:</b>")
|
||||
self.botlay_label.setToolTip(
|
||||
"Gerber to be mirrored."
|
||||
)
|
||||
|
||||
self.mirror_gerber_button = QtWidgets.QPushButton("Mirror")
|
||||
self.mirror_gerber_button.setToolTip(
|
||||
"Mirrors (flips) the specified object around \n"
|
||||
"the specified axis. Does not create a new \n"
|
||||
"object, but modifies it."
|
||||
)
|
||||
self.mirror_gerber_button.setFixedWidth(40)
|
||||
|
||||
# grid_lay.addRow("Bottom Layer:", self.object_combo)
|
||||
grid_lay.addWidget(self.botlay_label, 0, 0)
|
||||
grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
|
||||
grid_lay.addWidget(self.mirror_gerber_button, 1, 3)
|
||||
|
||||
## Excellon Object to mirror
|
||||
self.exc_object_combo = QtWidgets.QComboBox()
|
||||
self.exc_object_combo.setModel(self.app.collection)
|
||||
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
|
||||
self.exc_object_combo.setCurrentIndex(1)
|
||||
|
||||
self.excobj_label = QtWidgets.QLabel("<b>EXCELLON:</b>")
|
||||
self.excobj_label.setToolTip(
|
||||
"Excellon Object to be mirrored."
|
||||
)
|
||||
|
||||
self.mirror_exc_button = QtWidgets.QPushButton("Mirror")
|
||||
self.mirror_exc_button.setToolTip(
|
||||
"Mirrors (flips) the specified object around \n"
|
||||
"the specified axis. Does not create a new \n"
|
||||
"object, but modifies it."
|
||||
)
|
||||
self.mirror_exc_button.setFixedWidth(40)
|
||||
|
||||
# grid_lay.addRow("Bottom Layer:", self.object_combo)
|
||||
grid_lay.addWidget(self.excobj_label, 2, 0)
|
||||
grid_lay.addWidget(self.exc_object_combo, 3, 0, 1, 2)
|
||||
grid_lay.addWidget(self.mirror_exc_button, 3, 3)
|
||||
|
||||
## Geometry Object to mirror
|
||||
self.geo_object_combo = QtWidgets.QComboBox()
|
||||
self.geo_object_combo.setModel(self.app.collection)
|
||||
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
|
||||
self.geo_object_combo.setCurrentIndex(1)
|
||||
|
||||
self.geoobj_label = QtWidgets.QLabel("<b>GEOMETRY</b>:")
|
||||
self.geoobj_label.setToolTip(
|
||||
"Geometry Obj to be mirrored."
|
||||
)
|
||||
|
||||
self.mirror_geo_button = QtWidgets.QPushButton("Mirror")
|
||||
self.mirror_geo_button.setToolTip(
|
||||
"Mirrors (flips) the specified object around \n"
|
||||
"the specified axis. Does not create a new \n"
|
||||
"object, but modifies it."
|
||||
)
|
||||
self.mirror_geo_button.setFixedWidth(40)
|
||||
|
||||
# grid_lay.addRow("Bottom Layer:", self.object_combo)
|
||||
grid_lay.addWidget(self.geoobj_label, 4, 0)
|
||||
grid_lay.addWidget(self.geo_object_combo, 5, 0, 1, 2)
|
||||
grid_lay.addWidget(self.mirror_geo_button, 5, 3)
|
||||
|
||||
## Axis
|
||||
self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
|
||||
{'label': 'Y', 'value': 'Y'}])
|
||||
self.mirax_label = QtWidgets.QLabel("Mirror Axis:")
|
||||
self.mirax_label.setToolTip(
|
||||
"Mirror vertically (X) or horizontally (Y)."
|
||||
)
|
||||
# grid_lay.addRow("Mirror Axis:", self.mirror_axis)
|
||||
self.empty_lb1 = QtWidgets.QLabel("")
|
||||
grid_lay.addWidget(self.empty_lb1, 6, 0)
|
||||
grid_lay.addWidget(self.mirax_label, 7, 0)
|
||||
grid_lay.addWidget(self.mirror_axis, 7, 1)
|
||||
|
||||
## Axis Location
|
||||
self.axis_location = RadioSet([{'label': 'Point', 'value': 'point'},
|
||||
{'label': 'Box', 'value': 'box'}])
|
||||
self.axloc_label = QtWidgets.QLabel("Axis Ref:")
|
||||
self.axloc_label.setToolTip(
|
||||
"The axis should pass through a <b>point</b> or cut\n "
|
||||
"a specified <b>box</b> (in a Geometry object) in \n"
|
||||
"the middle."
|
||||
)
|
||||
# grid_lay.addRow("Axis Location:", self.axis_location)
|
||||
grid_lay.addWidget(self.axloc_label, 8, 0)
|
||||
grid_lay.addWidget(self.axis_location, 8, 1)
|
||||
|
||||
self.empty_lb2 = QtWidgets.QLabel("")
|
||||
grid_lay.addWidget(self.empty_lb2, 9, 0)
|
||||
|
||||
## Point/Box
|
||||
self.point_box_container = QtWidgets.QVBoxLayout()
|
||||
self.pb_label = QtWidgets.QLabel("<b>Point/Box:</b>")
|
||||
self.pb_label.setToolTip(
|
||||
"Specify the point (x, y) through which the mirror axis \n "
|
||||
"passes or the Geometry object containing a rectangle \n"
|
||||
"that the mirror axis cuts in half."
|
||||
)
|
||||
# grid_lay.addRow("Point/Box:", self.point_box_container)
|
||||
|
||||
self.add_point_button = QtWidgets.QPushButton("Add")
|
||||
self.add_point_button.setToolTip(
|
||||
"Add the <b>point (x, y)</b> through which the mirror axis \n "
|
||||
"passes or the Object containing a rectangle \n"
|
||||
"that the mirror axis cuts in half.\n"
|
||||
"The point is captured by pressing SHIFT key\n"
|
||||
"and left mouse clicking on canvas or you can enter them manually."
|
||||
)
|
||||
self.add_point_button.setFixedWidth(40)
|
||||
|
||||
grid_lay.addWidget(self.pb_label, 10, 0)
|
||||
grid_lay.addLayout(self.point_box_container, 11, 0, 1, 3)
|
||||
grid_lay.addWidget(self.add_point_button, 11, 3)
|
||||
|
||||
self.point_entry = EvalEntry()
|
||||
|
||||
self.point_box_container.addWidget(self.point_entry)
|
||||
self.box_combo = QtWidgets.QComboBox()
|
||||
self.box_combo.setModel(self.app.collection)
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setCurrentIndex(1)
|
||||
|
||||
self.box_combo_type = QtWidgets.QComboBox()
|
||||
self.box_combo_type.addItem("Gerber Reference Box Object")
|
||||
self.box_combo_type.addItem("Excellon Reference Box Object")
|
||||
self.box_combo_type.addItem("Geometry Reference Box Object")
|
||||
|
||||
self.point_box_container.addWidget(self.box_combo_type)
|
||||
self.point_box_container.addWidget(self.box_combo)
|
||||
self.box_combo.hide()
|
||||
self.box_combo_type.hide()
|
||||
|
||||
|
||||
## Alignment holes
|
||||
self.ah_label = QtWidgets.QLabel("<b>Alignment Drill Coordinates:</b>")
|
||||
self.ah_label.setToolTip(
|
||||
"Alignment holes (x1, y1), (x2, y2), ... "
|
||||
"on one side of the mirror axis. For each set of (x, y) coordinates\n"
|
||||
"entered here, a pair of drills will be created: one on the\n"
|
||||
"coordinates entered and one in mirror position over the axis\n"
|
||||
"selected above in the 'Mirror Axis'."
|
||||
)
|
||||
self.layout.addWidget(self.ah_label)
|
||||
|
||||
grid_lay1 = QtWidgets.QGridLayout()
|
||||
self.layout.addLayout(grid_lay1)
|
||||
|
||||
self.alignment_holes = EvalEntry()
|
||||
|
||||
self.add_drill_point_button = QtWidgets.QPushButton("Add")
|
||||
self.add_drill_point_button.setToolTip(
|
||||
"Add alignment drill holes coords (x1, y1), (x2, y2), ... \n"
|
||||
"on one side of the mirror axis.\n"
|
||||
"The point(s) can be captured by pressing SHIFT key\n"
|
||||
"and left mouse clicking on canvas. Or you can enter them manually."
|
||||
)
|
||||
self.add_drill_point_button.setFixedWidth(40)
|
||||
|
||||
grid_lay1.addWidget(self.alignment_holes, 0, 0, 1, 2)
|
||||
grid_lay1.addWidget(self.add_drill_point_button, 0, 3)
|
||||
|
||||
## Drill diameter for alignment holes
|
||||
self.dt_label = QtWidgets.QLabel("<b>Alignment Drill Creation</b>:")
|
||||
self.dt_label.setToolTip(
|
||||
"Create a set of alignment drill holes\n"
|
||||
"with the specified diameter,\n"
|
||||
"at the specified coordinates."
|
||||
)
|
||||
self.layout.addWidget(self.dt_label)
|
||||
|
||||
grid_lay2 = QtWidgets.QGridLayout()
|
||||
self.layout.addLayout(grid_lay2)
|
||||
|
||||
self.drill_dia = LengthEntry()
|
||||
self.dd_label = QtWidgets.QLabel("Drill diam.:")
|
||||
self.dd_label.setToolTip(
|
||||
"Diameter of the drill for the "
|
||||
"alignment holes."
|
||||
)
|
||||
grid_lay2.addWidget(self.dd_label, 0, 0)
|
||||
grid_lay2.addWidget(self.drill_dia, 0, 1)
|
||||
|
||||
## Buttons
|
||||
self.create_alignment_hole_button = QtWidgets.QPushButton("Create Excellon Object")
|
||||
self.create_alignment_hole_button.setToolTip(
|
||||
"Creates an Excellon Object containing the\n"
|
||||
"specified alignment holes and their mirror\n"
|
||||
"images.")
|
||||
# self.create_alignment_hole_button.setFixedWidth(40)
|
||||
grid_lay2.addWidget(self.create_alignment_hole_button, 1,0, 1, 2)
|
||||
|
||||
self.reset_button = QtWidgets.QPushButton("Reset")
|
||||
self.reset_button.setToolTip(
|
||||
"Resets all the fields.")
|
||||
self.reset_button.setFixedWidth(40)
|
||||
grid_lay2.addWidget(self.reset_button, 1, 2)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Signals
|
||||
self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
|
||||
self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
|
||||
self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
|
||||
self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
|
||||
self.add_point_button.clicked.connect(self.on_point_add)
|
||||
self.add_drill_point_button.clicked.connect(self.on_drill_add)
|
||||
self.reset_button.clicked.connect(self.reset_fields)
|
||||
|
||||
self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
|
||||
|
||||
self.axis_location.group_toggle_fn = self.on_toggle_pointbox
|
||||
|
||||
self.drill_values = ""
|
||||
|
||||
## Initialize form
|
||||
self.mirror_axis.set_value('X')
|
||||
self.axis_location.set_value('point')
|
||||
self.drill_dia.set_value(1)
|
||||
|
||||
def on_combo_box_type(self):
|
||||
obj_type = self.box_combo_type.currentIndex()
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setCurrentIndex(0)
|
||||
|
||||
def on_create_alignment_holes(self):
|
||||
axis = self.mirror_axis.get_value()
|
||||
mode = self.axis_location.get_value()
|
||||
|
||||
if mode == "point":
|
||||
try:
|
||||
px, py = self.point_entry.get_value()
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] 'Point' reference is selected and 'Point' coordinates "
|
||||
"are missing. Add them and retry.")
|
||||
return
|
||||
else:
|
||||
selection_index = self.box_combo.currentIndex()
|
||||
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
|
||||
bb_obj = model_index.internalPointer().obj
|
||||
xmin, ymin, xmax, ymax = bb_obj.bounds()
|
||||
px = 0.5 * (xmin + xmax)
|
||||
py = 0.5 * (ymin + ymax)
|
||||
|
||||
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
|
||||
|
||||
dia = self.drill_dia.get_value()
|
||||
tools = {"1": {"C": dia}}
|
||||
|
||||
# holes = self.alignment_holes.get_value()
|
||||
holes = eval('[{}]'.format(self.alignment_holes.text()))
|
||||
if not holes:
|
||||
self.app.inform.emit("[warning_notcl] There are no Alignment Drill Coordinates to use. Add them and retry.")
|
||||
return
|
||||
|
||||
drills = []
|
||||
|
||||
for hole in holes:
|
||||
point = Point(hole)
|
||||
point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
|
||||
drills.append({"point": point, "tool": "1"})
|
||||
drills.append({"point": point_mirror, "tool": "1"})
|
||||
|
||||
def obj_init(obj_inst, app_inst):
|
||||
obj_inst.tools = tools
|
||||
obj_inst.drills = drills
|
||||
obj_inst.create_geometry()
|
||||
|
||||
self.app.new_object("excellon", "Alignment Drills", obj_init)
|
||||
self.drill_values = ''
|
||||
|
||||
def on_mirror_gerber(self):
|
||||
selection_index = self.gerber_object_combo.currentIndex()
|
||||
# fcobj = self.app.collection.object_list[selection_index]
|
||||
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
|
||||
try:
|
||||
fcobj = model_index.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Gerber object loaded ...")
|
||||
return
|
||||
|
||||
if not isinstance(fcobj, FlatCAMGerber):
|
||||
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
|
||||
return
|
||||
|
||||
axis = self.mirror_axis.get_value()
|
||||
mode = self.axis_location.get_value()
|
||||
|
||||
if mode == "point":
|
||||
try:
|
||||
px, py = self.point_entry.get_value()
|
||||
except TypeError:
|
||||
self.app.inform.emit("[warning_notcl] 'Point' coordinates missing. "
|
||||
"Using Origin (0, 0) as mirroring reference.")
|
||||
px, py = (0, 0)
|
||||
|
||||
else:
|
||||
selection_index_box = self.box_combo.currentIndex()
|
||||
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
|
||||
try:
|
||||
bb_obj = model_index_box.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
|
||||
return
|
||||
|
||||
xmin, ymin, xmax, ymax = bb_obj.bounds()
|
||||
px = 0.5 * (xmin + xmax)
|
||||
py = 0.5 * (ymin + ymax)
|
||||
|
||||
fcobj.mirror(axis, [px, py])
|
||||
self.app.object_changed.emit(fcobj)
|
||||
fcobj.plot()
|
||||
|
||||
def on_mirror_exc(self):
|
||||
selection_index = self.exc_object_combo.currentIndex()
|
||||
# fcobj = self.app.collection.object_list[selection_index]
|
||||
model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
|
||||
try:
|
||||
fcobj = model_index.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Excellon object loaded ...")
|
||||
return
|
||||
|
||||
if not isinstance(fcobj, FlatCAMExcellon):
|
||||
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
|
||||
return
|
||||
|
||||
axis = self.mirror_axis.get_value()
|
||||
mode = self.axis_location.get_value()
|
||||
|
||||
if mode == "point":
|
||||
px, py = self.point_entry.get_value()
|
||||
else:
|
||||
selection_index_box = self.box_combo.currentIndex()
|
||||
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
|
||||
try:
|
||||
bb_obj = model_index_box.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
|
||||
return
|
||||
|
||||
xmin, ymin, xmax, ymax = bb_obj.bounds()
|
||||
px = 0.5 * (xmin + xmax)
|
||||
py = 0.5 * (ymin + ymax)
|
||||
|
||||
fcobj.mirror(axis, [px, py])
|
||||
self.app.object_changed.emit(fcobj)
|
||||
fcobj.plot()
|
||||
|
||||
def on_mirror_geo(self):
|
||||
selection_index = self.geo_object_combo.currentIndex()
|
||||
# fcobj = self.app.collection.object_list[selection_index]
|
||||
model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex())
|
||||
try:
|
||||
fcobj = model_index.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Geometry object loaded ...")
|
||||
return
|
||||
|
||||
if not isinstance(fcobj, FlatCAMGeometry):
|
||||
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
|
||||
return
|
||||
|
||||
axis = self.mirror_axis.get_value()
|
||||
mode = self.axis_location.get_value()
|
||||
|
||||
if mode == "point":
|
||||
px, py = self.point_entry.get_value()
|
||||
else:
|
||||
selection_index_box = self.box_combo.currentIndex()
|
||||
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
|
||||
try:
|
||||
bb_obj = model_index_box.internalPointer().obj
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
|
||||
return
|
||||
|
||||
xmin, ymin, xmax, ymax = bb_obj.bounds()
|
||||
px = 0.5 * (xmin + xmax)
|
||||
py = 0.5 * (ymin + ymax)
|
||||
|
||||
fcobj.mirror(axis, [px, py])
|
||||
self.app.object_changed.emit(fcobj)
|
||||
fcobj.plot()
|
||||
|
||||
def on_point_add(self):
|
||||
val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
|
||||
self.point_entry.set_value(val)
|
||||
|
||||
def on_drill_add(self):
|
||||
self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
|
||||
(self.app.pos[0], self.app.pos[1])) + ','
|
||||
self.alignment_holes.set_value(self.drill_values)
|
||||
|
||||
def on_toggle_pointbox(self):
|
||||
if self.axis_location.get_value() == "point":
|
||||
self.point_entry.show()
|
||||
self.box_combo.hide()
|
||||
self.box_combo_type.hide()
|
||||
self.add_point_button.setDisabled(False)
|
||||
else:
|
||||
self.point_entry.hide()
|
||||
self.box_combo.show()
|
||||
self.box_combo_type.show()
|
||||
self.add_point_button.setDisabled(True)
|
||||
|
||||
def reset_fields(self):
|
||||
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
|
||||
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
|
||||
self.gerber_object_combo.setCurrentIndex(0)
|
||||
self.exc_object_combo.setCurrentIndex(0)
|
||||
self.geo_object_combo.setCurrentIndex(0)
|
||||
self.box_combo.setCurrentIndex(0)
|
||||
self.box_combo_type.setCurrentIndex(0)
|
||||
|
||||
|
||||
self.drill_values = ""
|
||||
self.point_entry.set_value("")
|
||||
self.alignment_holes.set_value("")
|
||||
## Initialize form
|
||||
self.mirror_axis.set_value('X')
|
||||
self.axis_location.set_value('point')
|
||||
self.drill_dia.set_value(1)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "2-Sided Tool")
|
||||
self.reset_fields()
|
||||
206
flatcamTools/ToolFilm.py
Normal file
206
flatcamTools/ToolFilm.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
|
||||
from GUIElements import RadioSet, FloatEntry
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
|
||||
class Film(FlatCAMTool):
|
||||
|
||||
toolName = "Film PCB Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
# Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
# Form Layout
|
||||
tf_form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(tf_form_layout)
|
||||
|
||||
# Type of object for which to create the film
|
||||
self.tf_type_obj_combo = QtWidgets.QComboBox()
|
||||
self.tf_type_obj_combo.addItem("Gerber")
|
||||
self.tf_type_obj_combo.addItem("Excellon")
|
||||
self.tf_type_obj_combo.addItem("Geometry")
|
||||
|
||||
# we get rid of item1 ("Excellon") as it is not suitable for creating film
|
||||
self.tf_type_obj_combo.view().setRowHidden(1, True)
|
||||
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
|
||||
self.tf_type_obj_combo_label.setToolTip(
|
||||
"Specify the type of object for which to create the film.\n"
|
||||
"The object can be of type: Gerber or Geometry.\n"
|
||||
"The selection here decide the type of objects that will be\n"
|
||||
"in the Film Object combobox."
|
||||
)
|
||||
tf_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
|
||||
|
||||
# List of objects for which we can create the film
|
||||
self.tf_object_combo = QtWidgets.QComboBox()
|
||||
self.tf_object_combo.setModel(self.app.collection)
|
||||
self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.tf_object_combo.setCurrentIndex(1)
|
||||
self.tf_object_label = QtWidgets.QLabel("Film Object:")
|
||||
self.tf_object_label.setToolTip(
|
||||
"Object for which to create the film."
|
||||
)
|
||||
tf_form_layout.addRow(self.tf_object_label, self.tf_object_combo)
|
||||
|
||||
# Type of Box Object to be used as an envelope for film creation
|
||||
# Within this we can create negative
|
||||
self.tf_type_box_combo = QtWidgets.QComboBox()
|
||||
self.tf_type_box_combo.addItem("Gerber")
|
||||
self.tf_type_box_combo.addItem("Excellon")
|
||||
self.tf_type_box_combo.addItem("Geometry")
|
||||
|
||||
# we get rid of item1 ("Excellon") as it is not suitable for box when creating film
|
||||
self.tf_type_box_combo.view().setRowHidden(1, True)
|
||||
self.tf_type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
self.tf_type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.tf_type_box_combo_label = QtWidgets.QLabel("Box Type:")
|
||||
self.tf_type_box_combo_label.setToolTip(
|
||||
"Specify the type of object to be used as an container for\n"
|
||||
"film creation. It can be: Gerber or Geometry type."
|
||||
"The selection here decide the type of objects that will be\n"
|
||||
"in the Box Object combobox."
|
||||
)
|
||||
tf_form_layout.addRow(self.tf_type_box_combo_label, self.tf_type_box_combo)
|
||||
|
||||
# Box
|
||||
self.tf_box_combo = QtWidgets.QComboBox()
|
||||
self.tf_box_combo.setModel(self.app.collection)
|
||||
self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.tf_box_combo.setCurrentIndex(1)
|
||||
|
||||
self.tf_box_combo_label = QtWidgets.QLabel("Box Object:")
|
||||
self.tf_box_combo_label.setToolTip(
|
||||
"The actual object that is used a container for the\n "
|
||||
"selected object for which we create the film.\n"
|
||||
"Usually it is the PCB outline but it can be also the\n"
|
||||
"same object for which the film is created.")
|
||||
tf_form_layout.addRow(self.tf_box_combo_label, self.tf_box_combo)
|
||||
|
||||
# Film Type
|
||||
self.film_type = RadioSet([{'label': 'Positive', 'value': 'pos'},
|
||||
{'label': 'Negative', 'value': 'neg'}])
|
||||
self.film_type_label = QtWidgets.QLabel("Film Type:")
|
||||
self.film_type_label.setToolTip(
|
||||
"Generate a Positive black film or a Negative film.\n"
|
||||
"Positive means that it will print the features\n"
|
||||
"with black on a white canvas.\n"
|
||||
"Negative means that it will print the features\n"
|
||||
"with white on a black canvas.\n"
|
||||
"The Film format is SVG."
|
||||
)
|
||||
tf_form_layout.addRow(self.film_type_label, self.film_type)
|
||||
|
||||
# Boundary for negative film generation
|
||||
|
||||
self.boundary_entry = FloatEntry()
|
||||
self.boundary_label = QtWidgets.QLabel("Border:")
|
||||
self.boundary_label.setToolTip(
|
||||
"Specify a border around the object.\n"
|
||||
"Only for negative film.\n"
|
||||
"It helps if we use as a Box Object the same \n"
|
||||
"object as in Film Object. It will create a thick\n"
|
||||
"black bar around the actual print allowing for a\n"
|
||||
"better delimitation of the outline features which are of\n"
|
||||
"white color like the rest and which may confound with the\n"
|
||||
"surroundings if not for this border."
|
||||
)
|
||||
tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
|
||||
|
||||
# Buttons
|
||||
hlay = QtWidgets.QHBoxLayout()
|
||||
self.layout.addLayout(hlay)
|
||||
hlay.addStretch()
|
||||
|
||||
self.film_object_button = QtWidgets.QPushButton("Save Film")
|
||||
self.film_object_button.setToolTip(
|
||||
"Create a Film for the selected object, within\n"
|
||||
"the specified box. Does not create a new \n "
|
||||
"FlatCAM object, but directly save it in SVG format\n"
|
||||
"which can be opened with Inkscape."
|
||||
)
|
||||
hlay.addWidget(self.film_object_button)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Signals
|
||||
self.film_object_button.clicked.connect(self.on_film_creation)
|
||||
self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
|
||||
self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
|
||||
|
||||
## Initialize form
|
||||
self.film_type.set_value('neg')
|
||||
self.boundary_entry.set_value(0.0)
|
||||
|
||||
def on_type_obj_index_changed(self, index):
|
||||
obj_type = self.tf_type_obj_combo.currentIndex()
|
||||
self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.tf_object_combo.setCurrentIndex(0)
|
||||
|
||||
def on_type_box_index_changed(self, index):
|
||||
obj_type = self.tf_type_box_combo.currentIndex()
|
||||
self.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.tf_box_combo.setCurrentIndex(0)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Film Tool")
|
||||
|
||||
def on_film_creation(self):
|
||||
try:
|
||||
name = self.tf_object_combo.currentText()
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl] No Film object selected. Load a Film object and retry.")
|
||||
return
|
||||
try:
|
||||
boxname = self.tf_box_combo.currentText()
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl] No Box object selected. Load a Box object and retry.")
|
||||
return
|
||||
|
||||
border = float(self.boundary_entry.get_value())
|
||||
if border is None:
|
||||
border = 0
|
||||
|
||||
self.app.inform.emit("Generating Film ...")
|
||||
|
||||
if self.film_type.get_value() == "pos":
|
||||
try:
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive",
|
||||
directory=self.app.get_last_save_folder(), filter="*.svg")
|
||||
except TypeError:
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive")
|
||||
|
||||
filename = str(filename)
|
||||
|
||||
if str(filename) == "":
|
||||
self.app.inform.emit("Export SVG positive cancelled.")
|
||||
return
|
||||
else:
|
||||
self.app.export_svg_black(name, boxname, filename)
|
||||
else:
|
||||
try:
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative",
|
||||
directory=self.app.get_last_save_folder(), filter="*.svg")
|
||||
except TypeError:
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative")
|
||||
|
||||
filename = str(filename)
|
||||
|
||||
if str(filename) == "":
|
||||
self.app.inform.emit("Export SVG negative cancelled.")
|
||||
return
|
||||
else:
|
||||
self.app.export_svg_negative(name, boxname, filename, border)
|
||||
|
||||
def reset_fields(self):
|
||||
self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
172
flatcamTools/ToolImage.py
Normal file
172
flatcamTools/ToolImage.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
|
||||
from GUIElements import RadioSet, FloatEntry, FCComboBox, IntEntry
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
|
||||
class ToolImage(FlatCAMTool):
|
||||
|
||||
toolName = "Image as Object"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
# Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>IMAGE to PCB</b></font>")
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
# Form Layout
|
||||
ti_form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(ti_form_layout)
|
||||
|
||||
# Type of object to create for the image
|
||||
self.tf_type_obj_combo = FCComboBox()
|
||||
self.tf_type_obj_combo.addItem("Gerber")
|
||||
self.tf_type_obj_combo.addItem("Geometry")
|
||||
|
||||
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
|
||||
self.tf_type_obj_combo_label.setToolTip(
|
||||
"Specify the type of object to create from the image.\n"
|
||||
"It can be of type: Gerber or Geometry."
|
||||
|
||||
)
|
||||
ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
|
||||
|
||||
# DPI value of the imported image
|
||||
self.dpi_entry = IntEntry()
|
||||
self.dpi_label = QtWidgets.QLabel("DPI value:")
|
||||
self.dpi_label.setToolTip(
|
||||
"Specify a DPI value for the image."
|
||||
)
|
||||
ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
|
||||
|
||||
self.emty_lbl = QtWidgets.QLabel("")
|
||||
self.layout.addWidget(self.emty_lbl)
|
||||
|
||||
self.detail_label = QtWidgets.QLabel("<font size=4><b>Level of detail:</b>")
|
||||
self.layout.addWidget(self.detail_label)
|
||||
|
||||
ti2_form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(ti2_form_layout)
|
||||
|
||||
# Type of image interpretation
|
||||
self.image_type = RadioSet([{'label': 'B/W', 'value': 'black'},
|
||||
{'label': 'Color', 'value': 'color'}])
|
||||
self.image_type_label = QtWidgets.QLabel("<b>Image type:</b>")
|
||||
self.image_type_label.setToolTip(
|
||||
"Choose a method for the image interpretation.\n"
|
||||
"B/W means a black & white image. Color means a colored image."
|
||||
)
|
||||
ti2_form_layout.addRow(self.image_type_label, self.image_type)
|
||||
|
||||
# Mask value of the imported image when image monochrome
|
||||
self.mask_bw_entry = IntEntry()
|
||||
self.mask_bw_label = QtWidgets.QLabel("Mask value <b>B/W</b>:")
|
||||
self.mask_bw_label.setToolTip(
|
||||
"Mask for monochrome image.\n"
|
||||
"Takes values between [0 ... 255].\n"
|
||||
"Decides the level of details to include\n"
|
||||
"in the resulting geometry.\n"
|
||||
"0 means no detail and 255 means everything \n"
|
||||
"(which is totally black)."
|
||||
)
|
||||
ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
|
||||
|
||||
# Mask value of the imported image for RED color when image color
|
||||
self.mask_r_entry = IntEntry()
|
||||
self.mask_r_label = QtWidgets.QLabel("Mask value <b>R:</b>")
|
||||
self.mask_r_label.setToolTip(
|
||||
"Mask for RED color.\n"
|
||||
"Takes values between [0 ... 255].\n"
|
||||
"Decides the level of details to include\n"
|
||||
"in the resulting geometry."
|
||||
)
|
||||
ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
|
||||
|
||||
# Mask value of the imported image for GREEN color when image color
|
||||
self.mask_g_entry = IntEntry()
|
||||
self.mask_g_label = QtWidgets.QLabel("Mask value <b>G:</b>")
|
||||
self.mask_g_label.setToolTip(
|
||||
"Mask for GREEN color.\n"
|
||||
"Takes values between [0 ... 255].\n"
|
||||
"Decides the level of details to include\n"
|
||||
"in the resulting geometry."
|
||||
)
|
||||
ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
|
||||
|
||||
# Mask value of the imported image for BLUE color when image color
|
||||
self.mask_b_entry = IntEntry()
|
||||
self.mask_b_label = QtWidgets.QLabel("Mask value <b>B:</b>")
|
||||
self.mask_b_label.setToolTip(
|
||||
"Mask for BLUE color.\n"
|
||||
"Takes values between [0 ... 255].\n"
|
||||
"Decides the level of details to include\n"
|
||||
"in the resulting geometry."
|
||||
)
|
||||
ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
|
||||
|
||||
# Buttons
|
||||
hlay = QtWidgets.QHBoxLayout()
|
||||
self.layout.addLayout(hlay)
|
||||
hlay.addStretch()
|
||||
|
||||
self.import_button = QtWidgets.QPushButton("Import image")
|
||||
self.import_button.setToolTip(
|
||||
"Open a image of raster type and then import it in FlatCAM."
|
||||
)
|
||||
hlay.addWidget(self.import_button)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Signals
|
||||
self.import_button.clicked.connect(self.on_file_importimage)
|
||||
|
||||
## Initialize form
|
||||
self.dpi_entry.set_value(96)
|
||||
self.image_type.set_value('black')
|
||||
self.mask_bw_entry.set_value(250)
|
||||
self.mask_r_entry.set_value(250)
|
||||
self.mask_g_entry.set_value(250)
|
||||
self.mask_b_entry.set_value(250)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Image Tool")
|
||||
|
||||
def on_file_importimage(self):
|
||||
"""
|
||||
Callback for menu item File->Import IMAGE.
|
||||
:param type_of_obj: to import the IMAGE as Geometry or as Gerber
|
||||
:type type_of_obj: str
|
||||
:return: None
|
||||
"""
|
||||
mask = []
|
||||
self.app.log.debug("on_file_importimage()")
|
||||
|
||||
filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
|
||||
"Bitmap File (*.BMP);;" \
|
||||
"PNG File (*.PNG);;" \
|
||||
"Jpeg File (*.JPG);;" \
|
||||
"All Files (*.*)"
|
||||
try:
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE",
|
||||
directory=self.app.get_last_folder(), filter=filter)
|
||||
except TypeError:
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE", filter=filter)
|
||||
|
||||
filename = str(filename)
|
||||
type = self.tf_type_obj_combo.get_value().lower()
|
||||
dpi = self.dpi_entry.get_value()
|
||||
mode = self.image_type.get_value()
|
||||
mask = [self.mask_bw_entry.get_value(), self.mask_r_entry.get_value(),self.mask_g_entry.get_value(),
|
||||
self.mask_b_entry.get_value()]
|
||||
|
||||
if filename == "":
|
||||
self.app.inform.emit("Open cancelled.")
|
||||
else:
|
||||
self.app.worker_task.emit({'fcn': self.app.import_image,
|
||||
'params': [filename, type, dpi, mode, mask]})
|
||||
# self.import_svg(filename, "geometry")
|
||||
352
flatcamTools/ToolMeasurement.py
Normal file
352
flatcamTools/ToolMeasurement.py
Normal file
@@ -0,0 +1,352 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
from VisPyVisuals import *
|
||||
|
||||
from copy import copy
|
||||
from math import sqrt
|
||||
|
||||
class Measurement(FlatCAMTool):
|
||||
|
||||
toolName = "Measurement Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout)
|
||||
|
||||
form_layout_child_1 = QtWidgets.QFormLayout()
|
||||
form_layout_child_1_1 = QtWidgets.QFormLayout()
|
||||
form_layout_child_1_2 = QtWidgets.QFormLayout()
|
||||
form_layout_child_2 = QtWidgets.QFormLayout()
|
||||
form_layout_child_3 = QtWidgets.QFormLayout()
|
||||
|
||||
self.start_label = QtWidgets.QLabel("<b>Start</b> Coords:")
|
||||
self.start_label.setToolTip("This is measuring Start point coordinates.")
|
||||
|
||||
self.stop_label = QtWidgets.QLabel("<b>Stop</b> Coords:")
|
||||
self.stop_label.setToolTip("This is the measuring Stop point coordinates.")
|
||||
|
||||
self.distance_x_label = QtWidgets.QLabel("Dx:")
|
||||
self.distance_x_label.setToolTip("This is the distance measured over the X axis.")
|
||||
|
||||
self.distance_y_label = QtWidgets.QLabel("Dy:")
|
||||
self.distance_y_label.setToolTip("This is the distance measured over the Y axis.")
|
||||
|
||||
self.total_distance_label = QtWidgets.QLabel("<b>DISTANCE:</b>")
|
||||
self.total_distance_label.setToolTip("This is the point to point Euclidian distance.")
|
||||
|
||||
self.units_entry_1 = FCEntry()
|
||||
self.units_entry_1.setToolTip("Those are the units in which the distance is measured.")
|
||||
self.units_entry_1.setDisabled(True)
|
||||
self.units_entry_1.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.units_entry_1.setFrame(False)
|
||||
self.units_entry_1.setFixedWidth(30)
|
||||
|
||||
self.units_entry_2 = FCEntry()
|
||||
self.units_entry_2.setToolTip("Those are the units in which the distance is measured.")
|
||||
self.units_entry_2.setDisabled(True)
|
||||
self.units_entry_2.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.units_entry_2.setFrame(False)
|
||||
self.units_entry_2.setFixedWidth(30)
|
||||
|
||||
self.units_entry_3 = FCEntry()
|
||||
self.units_entry_3.setToolTip("Those are the units in which the distance is measured.")
|
||||
self.units_entry_3.setDisabled(True)
|
||||
self.units_entry_3.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.units_entry_3.setFrame(False)
|
||||
self.units_entry_3.setFixedWidth(30)
|
||||
|
||||
self.units_entry_4 = FCEntry()
|
||||
self.units_entry_4.setToolTip("Those are the units in which the distance is measured.")
|
||||
self.units_entry_4.setDisabled(True)
|
||||
self.units_entry_4.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.units_entry_4.setFrame(False)
|
||||
self.units_entry_4.setFixedWidth(30)
|
||||
|
||||
self.units_entry_5 = FCEntry()
|
||||
self.units_entry_5.setToolTip("Those are the units in which the distance is measured.")
|
||||
self.units_entry_5.setDisabled(True)
|
||||
self.units_entry_5.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.units_entry_5.setFrame(False)
|
||||
self.units_entry_5.setFixedWidth(30)
|
||||
|
||||
self.start_entry = FCEntry()
|
||||
self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.start_entry.setToolTip("This is measuring Start point coordinates.")
|
||||
self.start_entry.setFixedWidth(100)
|
||||
|
||||
self.stop_entry = FCEntry()
|
||||
self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.stop_entry.setToolTip("This is the measuring Stop point coordinates.")
|
||||
self.stop_entry.setFixedWidth(100)
|
||||
|
||||
self.distance_x_entry = FCEntry()
|
||||
self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.distance_x_entry.setToolTip("This is the distance measured over the X axis.")
|
||||
self.distance_x_entry.setFixedWidth(100)
|
||||
|
||||
|
||||
self.distance_y_entry = FCEntry()
|
||||
self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.distance_y_entry.setToolTip("This is the distance measured over the Y axis.")
|
||||
self.distance_y_entry.setFixedWidth(100)
|
||||
|
||||
|
||||
self.total_distance_entry = FCEntry()
|
||||
self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.total_distance_entry.setToolTip("This is the point to point Euclidian distance.")
|
||||
self.total_distance_entry.setFixedWidth(100)
|
||||
|
||||
self.measure_btn = QtWidgets.QPushButton("Measure")
|
||||
self.measure_btn.setFixedWidth(70)
|
||||
self.layout.addWidget(self.measure_btn)
|
||||
|
||||
|
||||
form_layout_child_1.addRow(self.start_entry, self.units_entry_1)
|
||||
form_layout_child_1_1.addRow(self.stop_entry, self.units_entry_2)
|
||||
form_layout_child_1_2.addRow(self.distance_x_entry, self.units_entry_3)
|
||||
form_layout_child_2.addRow(self.distance_y_entry, self.units_entry_4)
|
||||
form_layout_child_3.addRow(self.total_distance_entry, self.units_entry_5)
|
||||
|
||||
form_layout.addRow(self.start_label, form_layout_child_1)
|
||||
form_layout.addRow(self.stop_label, form_layout_child_1_1)
|
||||
form_layout.addRow(self.distance_x_label, form_layout_child_1_2)
|
||||
form_layout.addRow(self.distance_y_label, form_layout_child_2)
|
||||
form_layout.addRow(self.total_distance_label, form_layout_child_3)
|
||||
|
||||
# initial view of the layout
|
||||
self.start_entry.set_value('(0, 0)')
|
||||
self.stop_entry.set_value('(0, 0)')
|
||||
self.distance_x_entry.set_value('0')
|
||||
self.distance_y_entry.set_value('0')
|
||||
self.total_distance_entry.set_value('0')
|
||||
self.units_entry_1.set_value(str(self.units))
|
||||
self.units_entry_2.set_value(str(self.units))
|
||||
self.units_entry_3.set_value(str(self.units))
|
||||
self.units_entry_4.set_value(str(self.units))
|
||||
self.units_entry_5.set_value(str(self.units))
|
||||
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
self.clicked_meas = 0
|
||||
|
||||
self.point1 = None
|
||||
self.point2 = None
|
||||
|
||||
# the default state is disabled for the Move command
|
||||
# self.setVisible(False)
|
||||
self.active = False
|
||||
|
||||
# VisPy visuals
|
||||
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
|
||||
|
||||
self.measure_btn.clicked.connect(self.toggle)
|
||||
|
||||
def run(self):
|
||||
if self.app.tool_tab_locked is True:
|
||||
return
|
||||
|
||||
self.toggle()
|
||||
|
||||
# Remove anything else in the GUI
|
||||
self.app.ui.tool_scroll_area.takeWidget()
|
||||
|
||||
# Put ourself in the GUI
|
||||
self.app.ui.tool_scroll_area.setWidget(self)
|
||||
|
||||
# Switch notebook to tool page
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
|
||||
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
|
||||
self.show()
|
||||
self.app.ui.notebook.setTabText(2, "Meas. Tool")
|
||||
|
||||
def on_key_release_meas(self, event):
|
||||
if event.key == 'escape':
|
||||
# abort the measurement action
|
||||
self.toggle()
|
||||
return
|
||||
|
||||
if event.key == 'G':
|
||||
# toggle grid status
|
||||
self.app.ui.grid_snap_btn.trigger()
|
||||
return
|
||||
|
||||
def toggle(self):
|
||||
# the self.active var is doing the 'toggle'
|
||||
if self.active is True:
|
||||
# DISABLE the Measuring TOOL
|
||||
self.active = False
|
||||
# disconnect the mouse/key events from functions of measurement tool
|
||||
self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move_meas)
|
||||
self.app.plotcanvas.vis_disconnect('mouse_press', self.on_click_meas)
|
||||
self.app.plotcanvas.vis_disconnect('key_release', self.on_key_release_meas)
|
||||
|
||||
# reconnect the mouse/key events to the functions from where the tool was called
|
||||
if self.app.call_source == 'app':
|
||||
self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
|
||||
self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
|
||||
self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
|
||||
self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
|
||||
elif self.app.call_source == 'geo_editor':
|
||||
self.app.geo_editor.canvas.vis_connect('mouse_move', self.app.geo_editor.on_canvas_move)
|
||||
self.app.geo_editor.canvas.vis_connect('mouse_press', self.app.geo_editor.on_canvas_click)
|
||||
self.app.geo_editor.canvas.vis_connect('key_press', self.app.geo_editor.on_canvas_key)
|
||||
self.app.geo_editor.canvas.vis_connect('key_release', self.app.geo_editor.on_canvas_key_release)
|
||||
self.app.geo_editor.canvas.vis_connect('mouse_release', self.app.geo_editor.on_canvas_click_release)
|
||||
elif self.app.call_source == 'exc_editor':
|
||||
self.app.exc_editor.canvas.vis_connect('mouse_move', self.app.exc_editor.on_canvas_move)
|
||||
self.app.exc_editor.canvas.vis_connect('mouse_press', self.app.exc_editor.on_canvas_click)
|
||||
self.app.exc_editor.canvas.vis_connect('key_press', self.app.exc_editor.on_canvas_key)
|
||||
self.app.exc_editor.canvas.vis_connect('key_release', self.app.exc_editor.on_canvas_key_release)
|
||||
self.app.exc_editor.canvas.vis_connect('mouse_release', self.app.exc_editor.on_canvas_click_release)
|
||||
|
||||
self.clicked_meas = 0
|
||||
self.app.command_active = None
|
||||
# delete the measuring line
|
||||
self.delete_shape()
|
||||
return
|
||||
else:
|
||||
# ENABLE the Measuring TOOL
|
||||
self.active = True
|
||||
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
|
||||
|
||||
# we disconnect the mouse/key handlers from wherever the measurement tool was called
|
||||
if self.app.call_source == 'app':
|
||||
self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
|
||||
self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
|
||||
self.app.plotcanvas.vis_disconnect('key_press', self.app.on_key_over_plot)
|
||||
self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
|
||||
elif self.app.call_source == 'geo_editor':
|
||||
self.app.geo_editor.canvas.vis_disconnect('mouse_move', self.app.geo_editor.on_canvas_move)
|
||||
self.app.geo_editor.canvas.vis_disconnect('mouse_press', self.app.geo_editor.on_canvas_click)
|
||||
self.app.geo_editor.canvas.vis_disconnect('key_press', self.app.geo_editor.on_canvas_key)
|
||||
self.app.geo_editor.canvas.vis_disconnect('key_release', self.app.geo_editor.on_canvas_key_release)
|
||||
self.app.geo_editor.canvas.vis_disconnect('mouse_release', self.app.geo_editor.on_canvas_click_release)
|
||||
elif self.app.call_source == 'exc_editor':
|
||||
self.app.exc_editor.canvas.vis_disconnect('mouse_move', self.app.exc_editor.on_canvas_move)
|
||||
self.app.exc_editor.canvas.vis_disconnect('mouse_press', self.app.exc_editor.on_canvas_click)
|
||||
self.app.exc_editor.canvas.vis_disconnect('key_press', self.app.exc_editor.on_canvas_key)
|
||||
self.app.exc_editor.canvas.vis_disconnect('key_release', self.app.exc_editor.on_canvas_key_release)
|
||||
self.app.exc_editor.canvas.vis_disconnect('mouse_release', self.app.exc_editor.on_canvas_click_release)
|
||||
|
||||
# we can safely connect the app mouse events to the measurement tool
|
||||
self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_meas)
|
||||
self.app.plotcanvas.vis_connect('mouse_press', self.on_click_meas)
|
||||
self.app.plotcanvas.vis_connect('key_release', self.on_key_release_meas)
|
||||
|
||||
self.app.command_active = "Measurement"
|
||||
|
||||
# initial view of the layout
|
||||
self.start_entry.set_value('(0, 0)')
|
||||
self.stop_entry.set_value('(0, 0)')
|
||||
|
||||
self.distance_x_entry.set_value('0')
|
||||
self.distance_y_entry.set_value('0')
|
||||
self.total_distance_entry.set_value('0')
|
||||
|
||||
self.units_entry_1.set_value(str(self.units))
|
||||
self.units_entry_2.set_value(str(self.units))
|
||||
self.units_entry_3.set_value(str(self.units))
|
||||
self.units_entry_4.set_value(str(self.units))
|
||||
self.units_entry_5.set_value(str(self.units))
|
||||
|
||||
self.app.inform.emit("MEASURING: Click on the Start point ...")
|
||||
|
||||
def on_click_meas(self, event):
|
||||
# mouse click will be accepted only if the left button is clicked
|
||||
# this is necessary because right mouse click and middle mouse click
|
||||
# are used for panning on the canvas
|
||||
|
||||
if event.button == 1:
|
||||
if self.clicked_meas == 0:
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
else:
|
||||
pos = pos_canvas[0], pos_canvas[1]
|
||||
self.point1 = pos
|
||||
self.start_entry.set_value("(%.4f, %.4f)" % pos)
|
||||
self.app.inform.emit("MEASURING: Click on the Destination point ...")
|
||||
|
||||
if self.clicked_meas == 1:
|
||||
try:
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# delete the selection bounding box
|
||||
self.delete_shape()
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
else:
|
||||
pos = pos_canvas[0], pos_canvas[1]
|
||||
|
||||
dx = pos[0] - self.point1[0]
|
||||
dy = pos[1] - self.point1[1]
|
||||
d = sqrt(dx**2 + dy**2)
|
||||
|
||||
self.stop_entry.set_value("(%.4f, %.4f)" % pos)
|
||||
|
||||
self.app.inform.emit("MEASURING: Result D(x) = %.4f | D(y) = %.4f | Distance = %.4f" %
|
||||
(abs(dx), abs(dy), abs(d)))
|
||||
|
||||
self.distance_x_entry.set_value('%.4f' % abs(dx))
|
||||
self.distance_y_entry.set_value('%.4f' % abs(dy))
|
||||
self.total_distance_entry.set_value('%.4f' % abs(d))
|
||||
|
||||
self.clicked_meas = 0
|
||||
self.toggle()
|
||||
|
||||
# delete the measuring line
|
||||
self.delete_shape()
|
||||
return
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.clicked_meas = 1
|
||||
|
||||
def on_mouse_move_meas(self, event):
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
self.app.app_cursor.enabled = True
|
||||
# Update cursor
|
||||
self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
|
||||
else:
|
||||
pos = pos_canvas
|
||||
self.app.app_enabled = False
|
||||
|
||||
self.point2 = (pos[0], pos[1])
|
||||
|
||||
if self.clicked_meas == 1:
|
||||
self.update_meas_shape([self.point2, self.point1])
|
||||
|
||||
def update_meas_shape(self, pos):
|
||||
self.delete_shape()
|
||||
self.draw_shape(pos)
|
||||
|
||||
def delete_shape(self):
|
||||
self.sel_shapes.clear()
|
||||
self.sel_shapes.redraw()
|
||||
|
||||
def draw_shape(self, coords):
|
||||
self.meas_line = LineString(coords)
|
||||
self.sel_shapes.add(self.meas_line, color='black', update=True, layer=0, tolerance=None)
|
||||
|
||||
def set_meas_units(self, units):
|
||||
self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
|
||||
|
||||
# end of file
|
||||
238
flatcamTools/ToolMove.py
Normal file
238
flatcamTools/ToolMove.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
from VisPyVisuals import *
|
||||
|
||||
from io import StringIO
|
||||
from copy import copy
|
||||
|
||||
|
||||
class ToolMove(FlatCAMTool):
|
||||
|
||||
toolName = "Move"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.layout.setContentsMargins(0, 0, 3, 0)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
|
||||
|
||||
self.clicked_move = 0
|
||||
|
||||
self.point1 = None
|
||||
self.point2 = None
|
||||
|
||||
# the default state is disabled for the Move command
|
||||
self.setVisible(False)
|
||||
|
||||
self.sel_rect = None
|
||||
self.old_coords = []
|
||||
|
||||
# VisPy visuals
|
||||
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.install(self, icon, separator, **kwargs)
|
||||
|
||||
def run(self):
|
||||
if self.app.tool_tab_locked is True:
|
||||
return
|
||||
self.toggle()
|
||||
|
||||
def on_left_click(self, event):
|
||||
# mouse click will be accepted only if the left button is clicked
|
||||
# this is necessary because right mouse click and middle mouse click
|
||||
# are used for panning on the canvas
|
||||
|
||||
if event.button == 1:
|
||||
if self.clicked_move == 0:
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
else:
|
||||
pos = pos_canvas
|
||||
|
||||
if self.point1 is None:
|
||||
self.point1 = pos
|
||||
else:
|
||||
self.point2 = copy(self.point1)
|
||||
self.point1 = pos
|
||||
self.app.inform.emit("MOVE: Click on the Destination point ...")
|
||||
|
||||
if self.clicked_move == 1:
|
||||
try:
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# delete the selection bounding box
|
||||
self.delete_shape()
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
else:
|
||||
pos = pos_canvas
|
||||
|
||||
dx = pos[0] - self.point1[0]
|
||||
dy = pos[1] - self.point1[1]
|
||||
|
||||
proc = self.app.proc_container.new("Moving ...")
|
||||
|
||||
def job_move(app_obj):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
|
||||
try:
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object(s) selected.")
|
||||
return "fail"
|
||||
else:
|
||||
for sel_obj in obj_list:
|
||||
|
||||
sel_obj.offset((dx, dy))
|
||||
sel_obj.plot()
|
||||
# Update the object bounding box options
|
||||
a,b,c,d = sel_obj.bounds()
|
||||
sel_obj.options['xmin'] = a
|
||||
sel_obj.options['ymin'] = b
|
||||
sel_obj.options['xmax'] = c
|
||||
sel_obj.options['ymax'] = d
|
||||
# self.app.collection.set_active(sel_obj.options['name'])
|
||||
except Exception as e:
|
||||
proc.done()
|
||||
self.app.inform.emit('[error_notcl] '
|
||||
'ToolMove.on_left_click() --> %s' % str(e))
|
||||
return "fail"
|
||||
proc.done()
|
||||
# delete the selection bounding box
|
||||
self.delete_shape()
|
||||
|
||||
self.app.worker_task.emit({'fcn': job_move, 'params': [self]})
|
||||
|
||||
self.clicked_move = 0
|
||||
self.toggle()
|
||||
self.app.inform.emit("[success]Object was moved ...")
|
||||
return
|
||||
|
||||
except TypeError:
|
||||
self.app.inform.emit('[error_notcl] '
|
||||
'ToolMove.on_left_click() --> Error when mouse left click.')
|
||||
return
|
||||
|
||||
self.clicked_move = 1
|
||||
|
||||
def on_move(self, event):
|
||||
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
|
||||
|
||||
# if GRID is active we need to get the snapped positions
|
||||
if self.app.grid_status() == True:
|
||||
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
else:
|
||||
pos = pos_canvas
|
||||
|
||||
if self.point1 is None:
|
||||
dx = pos[0]
|
||||
dy = pos[1]
|
||||
else:
|
||||
dx = pos[0] - self.point1[0]
|
||||
dy = pos[1] - self.point1[1]
|
||||
|
||||
if self.clicked_move == 1:
|
||||
self.update_sel_bbox((dx, dy))
|
||||
|
||||
def on_key_press(self, event):
|
||||
if event.key == 'escape':
|
||||
# abort the move action
|
||||
self.app.inform.emit("[warning_notcl]Move action cancelled.")
|
||||
self.toggle()
|
||||
return
|
||||
|
||||
def toggle(self):
|
||||
if self.isVisible():
|
||||
self.setVisible(False)
|
||||
|
||||
self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move)
|
||||
self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click)
|
||||
self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press)
|
||||
self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
|
||||
|
||||
self.clicked_move = 0
|
||||
|
||||
# signal that there is no command active
|
||||
self.app.command_active = None
|
||||
|
||||
# delete the selection box
|
||||
self.delete_shape()
|
||||
return
|
||||
else:
|
||||
self.setVisible(True)
|
||||
# signal that there is a command active and it is 'Move'
|
||||
self.app.command_active = "Move"
|
||||
|
||||
if self.app.collection.get_selected():
|
||||
self.app.inform.emit("MOVE: Click on the Start point ...")
|
||||
# draw the selection box
|
||||
self.draw_sel_bbox()
|
||||
else:
|
||||
self.setVisible(False)
|
||||
# signal that there is no command active
|
||||
self.app.command_active = None
|
||||
self.app.inform.emit("[warning_notcl]MOVE action cancelled. No object(s) to move.")
|
||||
|
||||
def draw_sel_bbox(self):
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
obj_list = self.app.collection.get_selected()
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl]Object(s) not selected")
|
||||
self.toggle()
|
||||
else:
|
||||
# if we have an object selected then we can safely activate the mouse events
|
||||
self.app.plotcanvas.vis_connect('mouse_move', self.on_move)
|
||||
self.app.plotcanvas.vis_connect('mouse_press', self.on_left_click)
|
||||
self.app.plotcanvas.vis_connect('key_release', self.on_key_press)
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
xmaxlist.append(xmax)
|
||||
ymaxlist.append(ymax)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
xmaximal = max(xmaxlist)
|
||||
ymaximal = max(ymaxlist)
|
||||
|
||||
p1 = (xminimal, yminimal)
|
||||
p2 = (xmaximal, yminimal)
|
||||
p3 = (xmaximal, ymaximal)
|
||||
p4 = (xminimal, ymaximal)
|
||||
self.old_coords = [p1, p2, p3, p4]
|
||||
self.draw_shape(self.old_coords)
|
||||
|
||||
def update_sel_bbox(self, pos):
|
||||
self.delete_shape()
|
||||
|
||||
pt1 = (self.old_coords[0][0] + pos[0], self.old_coords[0][1] + pos[1])
|
||||
pt2 = (self.old_coords[1][0] + pos[0], self.old_coords[1][1] + pos[1])
|
||||
pt3 = (self.old_coords[2][0] + pos[0], self.old_coords[2][1] + pos[1])
|
||||
pt4 = (self.old_coords[3][0] + pos[0], self.old_coords[3][1] + pos[1])
|
||||
|
||||
self.draw_shape([pt1, pt2, pt3, pt4])
|
||||
|
||||
def delete_shape(self):
|
||||
self.sel_shapes.clear()
|
||||
self.sel_shapes.redraw()
|
||||
|
||||
def draw_shape(self, coords):
|
||||
self.sel_rect = Polygon(coords)
|
||||
|
||||
blue_t = Color('blue')
|
||||
blue_t.alpha = 0.2
|
||||
self.sel_shapes.add(self.sel_rect, color='blue', face_color=blue_t, update=True, layer=0, tolerance=None)
|
||||
|
||||
# end of file
|
||||
882
flatcamTools/ToolNonCopperClear.py
Normal file
882
flatcamTools/ToolNonCopperClear.py
Normal file
@@ -0,0 +1,882 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from copy import copy,deepcopy
|
||||
# from GUIElements import IntEntry, RadioSet, FCEntry
|
||||
# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
|
||||
from ObjectCollection import *
|
||||
import time
|
||||
|
||||
class NonCopperClear(FlatCAMTool, Gerber):
|
||||
|
||||
toolName = "Non-Copper Clearing Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
FlatCAMTool.__init__(self, app)
|
||||
Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
|
||||
|
||||
self.tools_frame = QtWidgets.QFrame()
|
||||
self.tools_frame.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.tools_frame)
|
||||
self.tools_box = QtWidgets.QVBoxLayout()
|
||||
self.tools_box.setContentsMargins(0, 0, 0, 0)
|
||||
self.tools_frame.setLayout(self.tools_box)
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.tools_box.addWidget(title_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.tools_box.addLayout(form_layout)
|
||||
|
||||
## Object
|
||||
self.object_combo = QtWidgets.QComboBox()
|
||||
self.object_combo.setModel(self.app.collection)
|
||||
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.object_combo.setCurrentIndex(1)
|
||||
self.object_label = QtWidgets.QLabel("Gerber:")
|
||||
self.object_label.setToolTip(
|
||||
"Gerber object to be cleared of excess copper. "
|
||||
)
|
||||
e_lab_0 = QtWidgets.QLabel('')
|
||||
|
||||
form_layout.addRow(self.object_label, self.object_combo)
|
||||
form_layout.addRow(e_lab_0)
|
||||
|
||||
#### Tools ####
|
||||
self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
|
||||
self.tools_table_label.setToolTip(
|
||||
"Tools pool from which the algorithm\n"
|
||||
"will pick the ones used for copper clearing."
|
||||
)
|
||||
self.tools_box.addWidget(self.tools_table_label)
|
||||
|
||||
self.tools_table = FCTable()
|
||||
self.tools_box.addWidget(self.tools_table)
|
||||
|
||||
self.tools_table.setColumnCount(4)
|
||||
self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'TT', ''])
|
||||
self.tools_table.setColumnHidden(3, True)
|
||||
self.tools_table.setSortingEnabled(False)
|
||||
# self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
|
||||
self.tools_table.horizontalHeaderItem(0).setToolTip(
|
||||
"This is the Tool Number.\n"
|
||||
"Non copper clearing will start with the tool with the biggest \n"
|
||||
"diameter, continuing until there are no more tools.\n"
|
||||
"Only tools that create NCC clearing geometry will still be present\n"
|
||||
"in the resulting geometry. This is because with some tools\n"
|
||||
"this function will not be able to create painting geometry."
|
||||
)
|
||||
self.tools_table.horizontalHeaderItem(1).setToolTip(
|
||||
"Tool Diameter. It's value (in current FlatCAM units) \n"
|
||||
"is the cut width into the material.")
|
||||
|
||||
self.tools_table.horizontalHeaderItem(2).setToolTip(
|
||||
"The Tool Type (TT) can be:<BR>"
|
||||
"- <B>Circular</B> with 1 ... 4 teeth -> it is informative only. Being circular, <BR>"
|
||||
"the cut width in material is exactly the tool diameter.<BR>"
|
||||
"- <B>Ball</B> -> informative only and make reference to the Ball type endmill.<BR>"
|
||||
"- <B>V-Shape</B> -> it will disable de Z-Cut parameter in the resulting geometry UI form "
|
||||
"and enable two additional UI form fields in the resulting geometry: V-Tip Dia and "
|
||||
"V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such "
|
||||
"as the cut width into material will be equal with the value in the Tool Diameter "
|
||||
"column of this table.<BR>"
|
||||
"Choosing the <B>V-Shape</B> Tool Type automatically will select the Operation Type "
|
||||
"in the resulting geometry as Isolation.")
|
||||
|
||||
self.empty_label = QtWidgets.QLabel('')
|
||||
self.tools_box.addWidget(self.empty_label)
|
||||
|
||||
#### Add a new Tool ####
|
||||
hlay = QtWidgets.QHBoxLayout()
|
||||
self.tools_box.addLayout(hlay)
|
||||
|
||||
self.addtool_entry_lbl = QtWidgets.QLabel('<b>Tool Dia:</b>')
|
||||
self.addtool_entry_lbl.setToolTip(
|
||||
"Diameter for the new tool to add in the Tool Table"
|
||||
)
|
||||
self.addtool_entry = FloatEntry()
|
||||
|
||||
# hlay.addWidget(self.addtool_label)
|
||||
# hlay.addStretch()
|
||||
hlay.addWidget(self.addtool_entry_lbl)
|
||||
hlay.addWidget(self.addtool_entry)
|
||||
|
||||
grid2 = QtWidgets.QGridLayout()
|
||||
self.tools_box.addLayout(grid2)
|
||||
|
||||
self.addtool_btn = QtWidgets.QPushButton('Add')
|
||||
self.addtool_btn.setToolTip(
|
||||
"Add a new tool to the Tool Table\n"
|
||||
"with the diameter specified above."
|
||||
)
|
||||
|
||||
# self.copytool_btn = QtWidgets.QPushButton('Copy')
|
||||
# self.copytool_btn.setToolTip(
|
||||
# "Copy a selection of tools in the Tool Table\n"
|
||||
# "by first selecting a row in the Tool Table."
|
||||
# )
|
||||
|
||||
self.deltool_btn = QtWidgets.QPushButton('Delete')
|
||||
self.deltool_btn.setToolTip(
|
||||
"Delete a selection of tools in the Tool Table\n"
|
||||
"by first selecting a row(s) in the Tool Table."
|
||||
)
|
||||
|
||||
grid2.addWidget(self.addtool_btn, 0, 0)
|
||||
# grid2.addWidget(self.copytool_btn, 0, 1)
|
||||
grid2.addWidget(self.deltool_btn, 0,2)
|
||||
|
||||
self.empty_label_0 = QtWidgets.QLabel('')
|
||||
self.tools_box.addWidget(self.empty_label_0)
|
||||
|
||||
grid3 = QtWidgets.QGridLayout()
|
||||
self.tools_box.addLayout(grid3)
|
||||
|
||||
e_lab_1 = QtWidgets.QLabel('')
|
||||
grid3.addWidget(e_lab_1, 0, 0)
|
||||
|
||||
nccoverlabel = QtWidgets.QLabel('Overlap:')
|
||||
nccoverlabel.setToolTip(
|
||||
"How much (fraction) of the tool width to overlap each tool pass.\n"
|
||||
"Example:\n"
|
||||
"A value here of 0.25 means 25% from the tool diameter found above.\n\n"
|
||||
"Adjust the value starting with lower values\n"
|
||||
"and increasing it if areas that should be cleared are still \n"
|
||||
"not cleared.\n"
|
||||
"Lower values = faster processing, faster execution on PCB.\n"
|
||||
"Higher values = slow processing and slow execution on CNC\n"
|
||||
"due of too many paths."
|
||||
)
|
||||
grid3.addWidget(nccoverlabel, 1, 0)
|
||||
self.ncc_overlap_entry = FloatEntry()
|
||||
grid3.addWidget(self.ncc_overlap_entry, 1, 1)
|
||||
|
||||
nccmarginlabel = QtWidgets.QLabel('Margin:')
|
||||
nccmarginlabel.setToolTip(
|
||||
"Bounding box margin."
|
||||
)
|
||||
grid3.addWidget(nccmarginlabel, 2, 0)
|
||||
self.ncc_margin_entry = FloatEntry()
|
||||
grid3.addWidget(self.ncc_margin_entry, 2, 1)
|
||||
|
||||
# Method
|
||||
methodlabel = QtWidgets.QLabel('Method:')
|
||||
methodlabel.setToolTip(
|
||||
"Algorithm for non-copper clearing:<BR>"
|
||||
"<B>Standard</B>: Fixed step inwards.<BR>"
|
||||
"<B>Seed-based</B>: Outwards from seed.<BR>"
|
||||
"<B>Line-based</B>: Parallel lines."
|
||||
)
|
||||
grid3.addWidget(methodlabel, 3, 0)
|
||||
self.ncc_method_radio = RadioSet([
|
||||
{"label": "Standard", "value": "standard"},
|
||||
{"label": "Seed-based", "value": "seed"},
|
||||
{"label": "Straight lines", "value": "lines"}
|
||||
], orientation='vertical', stretch=False)
|
||||
grid3.addWidget(self.ncc_method_radio, 3, 1)
|
||||
|
||||
# Connect lines
|
||||
pathconnectlabel = QtWidgets.QLabel("Connect:")
|
||||
pathconnectlabel.setToolTip(
|
||||
"Draw lines between resulting\n"
|
||||
"segments to minimize tool lifts."
|
||||
)
|
||||
grid3.addWidget(pathconnectlabel, 4, 0)
|
||||
self.ncc_connect_cb = FCCheckBox()
|
||||
grid3.addWidget(self.ncc_connect_cb, 4, 1)
|
||||
|
||||
contourlabel = QtWidgets.QLabel("Contour:")
|
||||
contourlabel.setToolTip(
|
||||
"Cut around the perimeter of the polygon\n"
|
||||
"to trim rough edges."
|
||||
)
|
||||
grid3.addWidget(contourlabel, 5, 0)
|
||||
self.ncc_contour_cb = FCCheckBox()
|
||||
grid3.addWidget(self.ncc_contour_cb, 5, 1)
|
||||
|
||||
restlabel = QtWidgets.QLabel("Rest M.:")
|
||||
restlabel.setToolTip(
|
||||
"If checked, use 'rest machining'.\n"
|
||||
"Basically it will clear copper outside PCB features,\n"
|
||||
"using the biggest tool and continue with the next tools,\n"
|
||||
"from bigger to smaller, to clear areas of copper that\n"
|
||||
"could not be cleared by previous tool, until there is\n"
|
||||
"no more copper to clear or there are no more tools.\n"
|
||||
"If not checked, use the standard algorithm."
|
||||
)
|
||||
grid3.addWidget(restlabel, 6, 0)
|
||||
self.ncc_rest_cb = FCCheckBox()
|
||||
grid3.addWidget(self.ncc_rest_cb, 6, 1)
|
||||
|
||||
self.generate_ncc_button = QtWidgets.QPushButton('Generate Geometry')
|
||||
self.generate_ncc_button.setToolTip(
|
||||
"Create the Geometry Object\n"
|
||||
"for non-copper routing."
|
||||
)
|
||||
self.tools_box.addWidget(self.generate_ncc_button)
|
||||
|
||||
self.units = ''
|
||||
self.ncc_tools = {}
|
||||
self.tooluid = 0
|
||||
# store here the default data for Geometry Data
|
||||
self.default_data = {}
|
||||
|
||||
self.obj_name = ""
|
||||
self.ncc_obj = None
|
||||
|
||||
self.tools_box.addStretch()
|
||||
|
||||
self.addtool_btn.clicked.connect(self.on_tool_add)
|
||||
self.deltool_btn.clicked.connect(self.on_tool_delete)
|
||||
self.generate_ncc_button.clicked.connect(self.on_ncc)
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.install(self, icon, separator, **kwargs)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.tools_frame.show()
|
||||
self.set_ui()
|
||||
self.build_ui()
|
||||
self.app.ui.notebook.setTabText(2, "NCC Tool")
|
||||
|
||||
def set_ui(self):
|
||||
self.ncc_overlap_entry.set_value(self.app.defaults["gerber_nccoverlap"])
|
||||
self.ncc_margin_entry.set_value(self.app.defaults["gerber_nccmargin"])
|
||||
self.ncc_method_radio.set_value(self.app.defaults["gerber_nccmethod"])
|
||||
self.ncc_connect_cb.set_value(self.app.defaults["gerber_nccconnect"])
|
||||
self.ncc_contour_cb.set_value(self.app.defaults["gerber_ncccontour"])
|
||||
self.ncc_rest_cb.set_value(self.app.defaults["gerber_nccrest"])
|
||||
|
||||
self.tools_table.setupContextMenu()
|
||||
self.tools_table.addContextMenu(
|
||||
"Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
|
||||
self.tools_table.addContextMenu(
|
||||
"Delete", lambda:
|
||||
self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
|
||||
|
||||
# init the working variables
|
||||
self.default_data.clear()
|
||||
self.default_data.update({
|
||||
"name": '_ncc',
|
||||
"plot": self.app.defaults["geometry_plot"],
|
||||
"tooldia": self.app.defaults["geometry_painttooldia"],
|
||||
"cutz": self.app.defaults["geometry_cutz"],
|
||||
"vtipdia": 0.1,
|
||||
"vtipangle": 30,
|
||||
"travelz": self.app.defaults["geometry_travelz"],
|
||||
"feedrate": self.app.defaults["geometry_feedrate"],
|
||||
"feedrate_z": self.app.defaults["geometry_feedrate_z"],
|
||||
"feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
|
||||
"dwell": self.app.defaults["geometry_dwell"],
|
||||
"dwelltime": self.app.defaults["geometry_dwelltime"],
|
||||
"multidepth": self.app.defaults["geometry_multidepth"],
|
||||
"ppname_g": self.app.defaults["geometry_ppname_g"],
|
||||
"depthperpass": self.app.defaults["geometry_depthperpass"],
|
||||
"extracut": self.app.defaults["geometry_extracut"],
|
||||
"toolchange": self.app.defaults["geometry_toolchange"],
|
||||
"toolchangez": self.app.defaults["geometry_toolchangez"],
|
||||
"endz": self.app.defaults["geometry_endz"],
|
||||
"spindlespeed": self.app.defaults["geometry_spindlespeed"],
|
||||
"toolchangexy": self.app.defaults["geometry_toolchangexy"],
|
||||
"startz": self.app.defaults["geometry_startz"],
|
||||
"paintmargin": self.app.defaults["geometry_paintmargin"],
|
||||
"paintmethod": self.app.defaults["geometry_paintmethod"],
|
||||
"selectmethod": self.app.defaults["geometry_selectmethod"],
|
||||
"pathconnect": self.app.defaults["geometry_pathconnect"],
|
||||
"paintcontour": self.app.defaults["geometry_paintcontour"],
|
||||
"paintoverlap": self.app.defaults["geometry_paintoverlap"],
|
||||
"nccoverlap": self.app.defaults["gerber_nccoverlap"],
|
||||
"nccmargin": self.app.defaults["gerber_nccmargin"],
|
||||
"nccmethod": self.app.defaults["gerber_nccmethod"],
|
||||
"nccconnect": self.app.defaults["gerber_nccconnect"],
|
||||
"ncccontour": self.app.defaults["gerber_ncccontour"],
|
||||
"nccrest": self.app.defaults["gerber_nccrest"]
|
||||
})
|
||||
|
||||
try:
|
||||
dias = [float(eval(dia)) for dia in self.app.defaults["gerber_ncctools"].split(",")]
|
||||
except:
|
||||
log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Gerber Object -> NCC Tools.")
|
||||
return
|
||||
|
||||
self.tooluid = 0
|
||||
|
||||
self.ncc_tools.clear()
|
||||
for tool_dia in dias:
|
||||
self.tooluid += 1
|
||||
self.ncc_tools.update({
|
||||
int(self.tooluid): {
|
||||
'tooldia': float('%.4f' % tool_dia),
|
||||
'offset': 'Path',
|
||||
'offset_value': 0.0,
|
||||
'type': 'Iso',
|
||||
'tool_type': 'V',
|
||||
'data': dict(self.default_data),
|
||||
'solid_geometry': []
|
||||
}
|
||||
})
|
||||
self.obj_name = ""
|
||||
self.ncc_obj = None
|
||||
self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
|
||||
self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
|
||||
|
||||
def build_ui(self):
|
||||
self.ui_disconnect()
|
||||
|
||||
# updated units
|
||||
self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
|
||||
|
||||
if self.units == "IN":
|
||||
self.addtool_entry.set_value(0.039)
|
||||
else:
|
||||
self.addtool_entry.set_value(1)
|
||||
|
||||
sorted_tools = []
|
||||
for k, v in self.ncc_tools.items():
|
||||
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
|
||||
sorted_tools.sort()
|
||||
|
||||
n = len(sorted_tools)
|
||||
self.tools_table.setRowCount(n)
|
||||
tool_id = 0
|
||||
|
||||
for tool_sorted in sorted_tools:
|
||||
for tooluid_key, tooluid_value in self.ncc_tools.items():
|
||||
if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
|
||||
tool_id += 1
|
||||
id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
|
||||
id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
row_no = tool_id - 1
|
||||
self.tools_table.setItem(row_no, 0, id) # Tool name/id
|
||||
|
||||
# Make sure that the drill diameter when in MM is with no more than 2 decimals
|
||||
# There are no drill bits in MM with more than 3 decimals diameter
|
||||
# For INCH the decimals should be no more than 3. There are no drills under 10mils
|
||||
if self.units == 'MM':
|
||||
dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia'])
|
||||
else:
|
||||
dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia'])
|
||||
|
||||
dia.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
|
||||
tool_type_item = QtWidgets.QComboBox()
|
||||
for item in self.tool_type_item_options:
|
||||
tool_type_item.addItem(item)
|
||||
tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
|
||||
idx = tool_type_item.findText(tooluid_value['tool_type'])
|
||||
tool_type_item.setCurrentIndex(idx)
|
||||
|
||||
tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
|
||||
|
||||
self.tools_table.setItem(row_no, 1, dia) # Diameter
|
||||
self.tools_table.setCellWidget(row_no, 2, tool_type_item)
|
||||
|
||||
### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
|
||||
self.tools_table.setItem(row_no, 3, tool_uid_item) # Tool unique ID
|
||||
|
||||
# make the diameter column editable
|
||||
for row in range(tool_id):
|
||||
self.tools_table.item(row, 1).setFlags(
|
||||
QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
|
||||
# all the tools are selected by default
|
||||
self.tools_table.selectColumn(0)
|
||||
#
|
||||
self.tools_table.resizeColumnsToContents()
|
||||
self.tools_table.resizeRowsToContents()
|
||||
|
||||
vertical_header = self.tools_table.verticalHeader()
|
||||
vertical_header.hide()
|
||||
self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
horizontal_header = self.tools_table.horizontalHeader()
|
||||
horizontal_header.setMinimumSectionSize(10)
|
||||
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
|
||||
horizontal_header.resizeSection(0, 20)
|
||||
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
||||
|
||||
# self.tools_table.setSortingEnabled(True)
|
||||
# sort by tool diameter
|
||||
# self.tools_table.sortItems(1)
|
||||
|
||||
self.tools_table.setMinimumHeight(self.tools_table.getHeight())
|
||||
self.tools_table.setMaximumHeight(self.tools_table.getHeight())
|
||||
|
||||
self.app.report_usage("gerber_on_ncc_button")
|
||||
self.ui_connect()
|
||||
|
||||
def ui_connect(self):
|
||||
self.tools_table.itemChanged.connect(self.on_tool_edit)
|
||||
|
||||
def ui_disconnect(self):
|
||||
try:
|
||||
# if connected, disconnect the signal from the slot on item_changed as it creates issues
|
||||
self.tools_table.itemChanged.disconnect(self.on_tool_edit)
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_tool_add(self, dia=None, muted=None):
|
||||
|
||||
self.ui_disconnect()
|
||||
|
||||
if dia:
|
||||
tool_dia = dia
|
||||
else:
|
||||
tool_dia = self.addtool_entry.get_value()
|
||||
if tool_dia is None:
|
||||
self.build_ui()
|
||||
self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
|
||||
return
|
||||
|
||||
# construct a list of all 'tooluid' in the self.tools
|
||||
tool_uid_list = []
|
||||
for tooluid_key in self.ncc_tools:
|
||||
tool_uid_item = int(tooluid_key)
|
||||
tool_uid_list.append(tool_uid_item)
|
||||
|
||||
# find maximum from the temp_uid, add 1 and this is the new 'tooluid'
|
||||
if not tool_uid_list:
|
||||
max_uid = 0
|
||||
else:
|
||||
max_uid = max(tool_uid_list)
|
||||
self.tooluid = int(max_uid + 1)
|
||||
|
||||
tool_dias = []
|
||||
for k, v in self.ncc_tools.items():
|
||||
for tool_v in v.keys():
|
||||
if tool_v == 'tooldia':
|
||||
tool_dias.append(float('%.4f' % v[tool_v]))
|
||||
|
||||
if float('%.4f' % tool_dia) in tool_dias:
|
||||
if muted is None:
|
||||
self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
|
||||
self.tools_table.itemChanged.connect(self.on_tool_edit)
|
||||
return
|
||||
else:
|
||||
if muted is None:
|
||||
self.app.inform.emit("[success] New tool added to Tool Table.")
|
||||
self.ncc_tools.update({
|
||||
int(self.tooluid): {
|
||||
'tooldia': float('%.4f' % tool_dia),
|
||||
'offset': 'Path',
|
||||
'offset_value': 0.0,
|
||||
'type': 'Iso',
|
||||
'tool_type': 'V',
|
||||
'data': dict(self.default_data),
|
||||
'solid_geometry': []
|
||||
}
|
||||
})
|
||||
|
||||
self.build_ui()
|
||||
|
||||
def on_tool_edit(self):
|
||||
self.ui_disconnect()
|
||||
|
||||
tool_dias = []
|
||||
for k, v in self.ncc_tools.items():
|
||||
for tool_v in v.keys():
|
||||
if tool_v == 'tooldia':
|
||||
tool_dias.append(float('%.4f' % v[tool_v]))
|
||||
|
||||
for row in range(self.tools_table.rowCount()):
|
||||
new_tool_dia = float(self.tools_table.item(row, 1).text())
|
||||
tooluid = int(self.tools_table.item(row, 3).text())
|
||||
|
||||
# identify the tool that was edited and get it's tooluid
|
||||
if new_tool_dia not in tool_dias:
|
||||
self.ncc_tools[tooluid]['tooldia'] = new_tool_dia
|
||||
self.app.inform.emit("[success] Tool from Tool Table was edited.")
|
||||
self.build_ui()
|
||||
return
|
||||
else:
|
||||
# identify the old tool_dia and restore the text in tool table
|
||||
for k, v in self.ncc_tools.items():
|
||||
if k == tooluid:
|
||||
old_tool_dia = v['tooldia']
|
||||
break
|
||||
restore_dia_item = self.tools_table.item(row, 1)
|
||||
restore_dia_item.setText(str(old_tool_dia))
|
||||
self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
|
||||
self.build_ui()
|
||||
|
||||
def on_tool_delete(self, rows_to_delete=None, all=None):
|
||||
self.ui_disconnect()
|
||||
|
||||
deleted_tools_list = []
|
||||
|
||||
if all:
|
||||
self.paint_tools.clear()
|
||||
self.build_ui()
|
||||
return
|
||||
|
||||
if rows_to_delete:
|
||||
try:
|
||||
for row in rows_to_delete:
|
||||
tooluid_del = int(self.tools_table.item(row, 3).text())
|
||||
deleted_tools_list.append(tooluid_del)
|
||||
except TypeError:
|
||||
deleted_tools_list.append(rows_to_delete)
|
||||
|
||||
for t in deleted_tools_list:
|
||||
self.ncc_tools.pop(t, None)
|
||||
self.build_ui()
|
||||
return
|
||||
|
||||
try:
|
||||
if self.tools_table.selectedItems():
|
||||
for row_sel in self.tools_table.selectedItems():
|
||||
row = row_sel.row()
|
||||
if row < 0:
|
||||
continue
|
||||
tooluid_del = int(self.tools_table.item(row, 3).text())
|
||||
deleted_tools_list.append(tooluid_del)
|
||||
|
||||
for t in deleted_tools_list:
|
||||
self.ncc_tools.pop(t, None)
|
||||
|
||||
except AttributeError:
|
||||
self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
|
||||
return
|
||||
except Exception as e:
|
||||
log.debug(str(e))
|
||||
|
||||
self.app.inform.emit("[success] Tool(s) deleted from Tool Table.")
|
||||
self.build_ui()
|
||||
|
||||
def on_ncc(self):
|
||||
|
||||
over = self.ncc_overlap_entry.get_value()
|
||||
over = over if over else self.app.defaults["gerber_nccoverlap"]
|
||||
|
||||
margin = self.ncc_margin_entry.get_value()
|
||||
margin = margin if margin else self.app.defaults["gerber_nccmargin"]
|
||||
|
||||
connect = self.ncc_connect_cb.get_value()
|
||||
connect = connect if connect else self.app.defaults["gerber_nccconnect"]
|
||||
|
||||
contour = self.ncc_contour_cb.get_value()
|
||||
contour = contour if contour else self.app.defaults["gerber_ncccontour"]
|
||||
|
||||
clearing_method = self.ncc_rest_cb.get_value()
|
||||
clearing_method = clearing_method if clearing_method else self.app.defaults["gerber_nccrest"]
|
||||
|
||||
pol_method = self.ncc_method_radio.get_value()
|
||||
pol_method = pol_method if pol_method else self.app.defaults["gerber_nccmethod"]
|
||||
|
||||
self.obj_name = self.object_combo.currentText()
|
||||
# Get source object.
|
||||
try:
|
||||
self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
|
||||
return "Could not retrieve object: %s" % self.obj_name
|
||||
|
||||
|
||||
# Prepare non-copper polygons
|
||||
try:
|
||||
bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
|
||||
except AttributeError:
|
||||
self.app.inform.emit("[error_notcl]No Gerber file available.")
|
||||
return
|
||||
|
||||
# calculate the empty area by substracting the solid_geometry from the object bounding box geometry
|
||||
empty = self.ncc_obj.get_empty_area(bounding_box)
|
||||
if type(empty) is Polygon:
|
||||
empty = MultiPolygon([empty])
|
||||
|
||||
# clear non copper using standard algorithm
|
||||
if clearing_method == False:
|
||||
self.clear_non_copper(
|
||||
empty=empty,
|
||||
over=over,
|
||||
pol_method=pol_method,
|
||||
connect=connect,
|
||||
contour=contour
|
||||
)
|
||||
# clear non copper using rest machining algorithm
|
||||
else:
|
||||
self.clear_non_copper_rest(
|
||||
empty=empty,
|
||||
over=over,
|
||||
pol_method=pol_method,
|
||||
connect=connect,
|
||||
contour=contour
|
||||
)
|
||||
|
||||
def clear_non_copper(self, empty, over, pol_method, outname=None, connect=True, contour=True):
|
||||
|
||||
name = outname if outname else self.obj_name + "_ncc"
|
||||
|
||||
# Sort tools in descending order
|
||||
sorted_tools = []
|
||||
for k, v in self.ncc_tools.items():
|
||||
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
|
||||
sorted_tools.sort(reverse=True)
|
||||
|
||||
# Do job in background
|
||||
proc = self.app.proc_container.new("Clearing Non-Copper areas.")
|
||||
|
||||
def initialize(geo_obj, app_obj):
|
||||
assert isinstance(geo_obj, FlatCAMGeometry), \
|
||||
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
|
||||
|
||||
cleared_geo = []
|
||||
# Already cleared area
|
||||
cleared = MultiPolygon()
|
||||
|
||||
# flag for polygons not cleared
|
||||
app_obj.poly_not_cleared = False
|
||||
|
||||
# Generate area for each tool
|
||||
offset = sum(sorted_tools)
|
||||
current_uid = int(1)
|
||||
|
||||
for tool in sorted_tools:
|
||||
self.app.inform.emit('[success] Non-Copper Clearing with ToolDia = %s started.' % str(tool))
|
||||
cleared_geo[:] = []
|
||||
|
||||
# Get remaining tools offset
|
||||
offset -= (tool - 1e-12)
|
||||
|
||||
# Area to clear
|
||||
area = empty.buffer(-offset)
|
||||
try:
|
||||
area = area.difference(cleared)
|
||||
except:
|
||||
continue
|
||||
|
||||
# Transform area to MultiPolygon
|
||||
if type(area) is Polygon:
|
||||
area = MultiPolygon([area])
|
||||
|
||||
if area.geoms:
|
||||
if len(area.geoms) > 0:
|
||||
for p in area.geoms:
|
||||
try:
|
||||
if pol_method == 'standard':
|
||||
cp = self.clear_polygon(p, tool, self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
elif pol_method == 'seed':
|
||||
cp = self.clear_polygon2(p, tool, self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
else:
|
||||
cp = self.clear_polygon3(p, tool, self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
if cp:
|
||||
cleared_geo += list(cp.get_objects())
|
||||
except:
|
||||
log.warning("Polygon can not be cleared.")
|
||||
app_obj.poly_not_cleared = True
|
||||
continue
|
||||
|
||||
# check if there is a geometry at all in the cleared geometry
|
||||
if cleared_geo:
|
||||
# Overall cleared area
|
||||
cleared = empty.buffer(-offset * (1 + over)).buffer(-tool / 1.999999).buffer(
|
||||
tool / 1.999999)
|
||||
|
||||
# clean-up cleared geo
|
||||
cleared = cleared.buffer(0)
|
||||
|
||||
# find the tooluid associated with the current tool_dia so we know where to add the tool
|
||||
# solid_geometry
|
||||
for k, v in self.ncc_tools.items():
|
||||
if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
|
||||
current_uid = int(k)
|
||||
|
||||
# add the solid_geometry to the current too in self.paint_tools dictionary
|
||||
# and then reset the temporary list that stored that solid_geometry
|
||||
v['solid_geometry'] = deepcopy(cleared_geo)
|
||||
v['data']['name'] = name
|
||||
break
|
||||
geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
|
||||
else:
|
||||
log.debug("There are no geometries in the cleared polygon.")
|
||||
|
||||
geo_obj.options["cnctooldia"] = tool
|
||||
geo_obj.multigeo = True
|
||||
|
||||
def job_thread(app_obj):
|
||||
try:
|
||||
app_obj.new_object("geometry", name, initialize)
|
||||
except Exception as e:
|
||||
proc.done()
|
||||
self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper() --> %s' % str(e))
|
||||
return
|
||||
proc.done()
|
||||
|
||||
if app_obj.poly_not_cleared is False:
|
||||
self.app.inform.emit('[success] NCC Tool finished.')
|
||||
else:
|
||||
self.app.inform.emit('[warning_notcl] NCC Tool finished but some PCB features could not be cleared. '
|
||||
'Check the result.')
|
||||
# reset the variable for next use
|
||||
app_obj.poly_not_cleared = False
|
||||
|
||||
# focus on Selected Tab
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
|
||||
self.tools_frame.hide()
|
||||
self.app.ui.notebook.setTabText(2, "Tools")
|
||||
|
||||
# Promise object with the new name
|
||||
self.app.collection.promise(name)
|
||||
|
||||
# Background
|
||||
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
||||
|
||||
# clear copper with 'rest-machining' algorithm
|
||||
def clear_non_copper_rest(self, empty, over, pol_method, outname=None, connect=True, contour=True):
|
||||
|
||||
name = outname if outname is not None else self.obj_name + "_ncc_rm"
|
||||
|
||||
# Sort tools in descending order
|
||||
sorted_tools = []
|
||||
for k, v in self.ncc_tools.items():
|
||||
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
|
||||
sorted_tools.sort(reverse=True)
|
||||
|
||||
# Do job in background
|
||||
proc = self.app.proc_container.new("Clearing Non-Copper areas.")
|
||||
|
||||
def initialize_rm(geo_obj, app_obj):
|
||||
assert isinstance(geo_obj, FlatCAMGeometry), \
|
||||
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
|
||||
|
||||
cleared_geo = []
|
||||
cleared_by_last_tool = []
|
||||
rest_geo = []
|
||||
current_uid = 1
|
||||
|
||||
# repurposed flag for final object, geo_obj. True if it has any solid_geometry, False if not.
|
||||
app_obj.poly_not_cleared = True
|
||||
|
||||
area = empty.buffer(0)
|
||||
# Generate area for each tool
|
||||
while sorted_tools:
|
||||
tool = sorted_tools.pop(0)
|
||||
self.app.inform.emit('[success] Non-Copper Rest Clearing with ToolDia = %s started.' % str(tool))
|
||||
|
||||
tool_used = tool - 1e-12
|
||||
cleared_geo[:] = []
|
||||
|
||||
# Area to clear
|
||||
for poly in cleared_by_last_tool:
|
||||
try:
|
||||
area = area.difference(poly)
|
||||
except:
|
||||
pass
|
||||
cleared_by_last_tool[:] = []
|
||||
|
||||
# Transform area to MultiPolygon
|
||||
if type(area) is Polygon:
|
||||
area = MultiPolygon([area])
|
||||
|
||||
# add the rest that was not able to be cleared previously; area is a MultyPolygon
|
||||
# and rest_geo it's a list
|
||||
allparts = [p.buffer(0) for p in area.geoms]
|
||||
allparts += deepcopy(rest_geo)
|
||||
rest_geo[:] = []
|
||||
area = MultiPolygon(deepcopy(allparts))
|
||||
allparts[:] = []
|
||||
|
||||
if area.geoms:
|
||||
if len(area.geoms) > 0:
|
||||
for p in area.geoms:
|
||||
try:
|
||||
if pol_method == 'standard':
|
||||
cp = self.clear_polygon(p, tool_used, self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
elif pol_method == 'seed':
|
||||
cp = self.clear_polygon2(p, tool_used,
|
||||
self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
else:
|
||||
cp = self.clear_polygon3(p, tool_used,
|
||||
self.app.defaults["gerber_circle_steps"],
|
||||
overlap=over, contour=contour, connect=connect)
|
||||
cleared_geo.append(list(cp.get_objects()))
|
||||
except:
|
||||
log.warning("Polygon can't be cleared.")
|
||||
# this polygon should be added to a list and then try clear it with a smaller tool
|
||||
rest_geo.append(p)
|
||||
|
||||
# check if there is a geometry at all in the cleared geometry
|
||||
if cleared_geo:
|
||||
# Overall cleared area
|
||||
cleared_area = list(self.flatten_list(cleared_geo))
|
||||
|
||||
# cleared = MultiPolygon([p.buffer(tool_used / 2).buffer(-tool_used / 2)
|
||||
# for p in cleared_area])
|
||||
|
||||
# here we store the poly's already processed in the original geometry by the current tool
|
||||
# into cleared_by_last_tool list
|
||||
# this will be sustracted from the original geometry_to_be_cleared and make data for
|
||||
# the next tool
|
||||
buffer_value = tool_used / 2
|
||||
for p in cleared_area:
|
||||
poly = p.buffer(buffer_value)
|
||||
cleared_by_last_tool.append(poly)
|
||||
|
||||
# find the tooluid associated with the current tool_dia so we know
|
||||
# where to add the tool solid_geometry
|
||||
for k, v in self.ncc_tools.items():
|
||||
if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
|
||||
current_uid = int(k)
|
||||
|
||||
# add the solid_geometry to the current too in self.paint_tools dictionary
|
||||
# and then reset the temporary list that stored that solid_geometry
|
||||
v['solid_geometry'] = deepcopy(cleared_area)
|
||||
v['data']['name'] = name
|
||||
cleared_area[:] = []
|
||||
break
|
||||
|
||||
geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
|
||||
else:
|
||||
log.debug("There are no geometries in the cleared polygon.")
|
||||
|
||||
geo_obj.multigeo = True
|
||||
geo_obj.options["cnctooldia"] = tool
|
||||
|
||||
# check to see if geo_obj.tools is empty
|
||||
# it will be updated only if there is a solid_geometry for tools
|
||||
if geo_obj.tools:
|
||||
return
|
||||
else:
|
||||
# I will use this variable for this purpose although it was meant for something else
|
||||
# signal that we have no geo in the object therefore don't create it
|
||||
app_obj.poly_not_cleared = False
|
||||
return "fail"
|
||||
|
||||
def job_thread(app_obj):
|
||||
try:
|
||||
app_obj.new_object("geometry", name, initialize_rm)
|
||||
except Exception as e:
|
||||
proc.done()
|
||||
self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper_rest() --> %s' % str(e))
|
||||
return
|
||||
|
||||
if app_obj.poly_not_cleared is True:
|
||||
self.app.inform.emit('[success] NCC Tool finished.')
|
||||
# focus on Selected Tab
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
|
||||
else:
|
||||
self.app.inform.emit('[error_notcl] NCC Tool finished but could not clear the object '
|
||||
'with current settings.')
|
||||
# focus on Project Tab
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
||||
proc.done()
|
||||
# reset the variable for next use
|
||||
app_obj.poly_not_cleared = False
|
||||
|
||||
self.tools_frame.hide()
|
||||
self.app.ui.notebook.setTabText(2, "Tools")
|
||||
|
||||
# Promise object with the new name
|
||||
self.app.collection.promise(name)
|
||||
|
||||
# Background
|
||||
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
||||
1106
flatcamTools/ToolPaint.py
Normal file
1106
flatcamTools/ToolPaint.py
Normal file
File diff suppressed because it is too large
Load Diff
369
flatcamTools/ToolPanelize.py
Normal file
369
flatcamTools/ToolPanelize.py
Normal file
@@ -0,0 +1,369 @@
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from copy import copy, deepcopy
|
||||
from ObjectCollection import *
|
||||
import time
|
||||
|
||||
|
||||
class Panelize(FlatCAMTool):
|
||||
|
||||
toolName = "Panelize PCB Tool"
|
||||
|
||||
def __init__(self, app):
|
||||
super(Panelize, self).__init__(self)
|
||||
self.app = app
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
## Form Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.layout.addLayout(form_layout)
|
||||
|
||||
## Type of object to be panelized
|
||||
self.type_obj_combo = QtWidgets.QComboBox()
|
||||
self.type_obj_combo.addItem("Gerber")
|
||||
self.type_obj_combo.addItem("Excellon")
|
||||
self.type_obj_combo.addItem("Geometry")
|
||||
|
||||
self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
|
||||
self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
|
||||
self.type_obj_combo_label.setToolTip(
|
||||
"Specify the type of object to be panelized\n"
|
||||
"It can be of type: Gerber, Excellon or Geometry.\n"
|
||||
"The selection here decide the type of objects that will be\n"
|
||||
"in the Object combobox."
|
||||
)
|
||||
form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
|
||||
|
||||
## Object to be panelized
|
||||
self.object_combo = QtWidgets.QComboBox()
|
||||
self.object_combo.setModel(self.app.collection)
|
||||
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.object_combo.setCurrentIndex(1)
|
||||
self.object_label = QtWidgets.QLabel("Object:")
|
||||
self.object_label.setToolTip(
|
||||
"Object to be panelized. This means that it will\n"
|
||||
"be duplicated in an array of rows and columns."
|
||||
)
|
||||
form_layout.addRow(self.object_label, self.object_combo)
|
||||
|
||||
## Type of Box Object to be used as an envelope for panelization
|
||||
self.type_box_combo = QtWidgets.QComboBox()
|
||||
self.type_box_combo.addItem("Gerber")
|
||||
self.type_box_combo.addItem("Excellon")
|
||||
self.type_box_combo.addItem("Geometry")
|
||||
|
||||
# we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
|
||||
self.type_box_combo.view().setRowHidden(1, True)
|
||||
self.type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
|
||||
self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
|
||||
|
||||
self.type_box_combo_label = QtWidgets.QLabel("Box Type:")
|
||||
self.type_box_combo_label.setToolTip(
|
||||
"Specify the type of object to be used as an container for\n"
|
||||
"panelization. It can be: Gerber or Geometry type.\n"
|
||||
"The selection here decide the type of objects that will be\n"
|
||||
"in the Box Object combobox."
|
||||
)
|
||||
form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
|
||||
|
||||
## Box
|
||||
self.box_combo = QtWidgets.QComboBox()
|
||||
self.box_combo.setModel(self.app.collection)
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setCurrentIndex(1)
|
||||
self.box_combo_label = QtWidgets.QLabel("Box Object:")
|
||||
self.box_combo_label.setToolTip(
|
||||
"The actual object that is used a container for the\n "
|
||||
"selected object that is to be panelized."
|
||||
)
|
||||
form_layout.addRow(self.box_combo_label, self.box_combo)
|
||||
|
||||
## Spacing Columns
|
||||
self.spacing_columns = FloatEntry()
|
||||
self.spacing_columns.set_value(0.0)
|
||||
self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:")
|
||||
self.spacing_columns_label.setToolTip(
|
||||
"Spacing between columns of the desired panel.\n"
|
||||
"In current units."
|
||||
)
|
||||
form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
|
||||
|
||||
## Spacing Rows
|
||||
self.spacing_rows = FloatEntry()
|
||||
self.spacing_rows.set_value(0.0)
|
||||
self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:")
|
||||
self.spacing_rows_label.setToolTip(
|
||||
"Spacing between rows of the desired panel.\n"
|
||||
"In current units."
|
||||
)
|
||||
form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
|
||||
|
||||
## Columns
|
||||
self.columns = IntEntry()
|
||||
self.columns.set_value(1)
|
||||
self.columns_label = QtWidgets.QLabel("Columns:")
|
||||
self.columns_label.setToolTip(
|
||||
"Number of columns of the desired panel"
|
||||
)
|
||||
form_layout.addRow(self.columns_label, self.columns)
|
||||
|
||||
## Rows
|
||||
self.rows = IntEntry()
|
||||
self.rows.set_value(1)
|
||||
self.rows_label = QtWidgets.QLabel("Rows:")
|
||||
self.rows_label.setToolTip(
|
||||
"Number of rows of the desired panel"
|
||||
)
|
||||
form_layout.addRow(self.rows_label, self.rows)
|
||||
|
||||
## Constrains
|
||||
self.constrain_cb = FCCheckBox("Constrain panel within:")
|
||||
self.constrain_cb.setToolTip(
|
||||
"Area define by DX and DY within to constrain the panel.\n"
|
||||
"DX and DY values are in current units.\n"
|
||||
"Regardless of how many columns and rows are desired,\n"
|
||||
"the final panel will have as many columns and rows as\n"
|
||||
"they fit completely within selected area."
|
||||
)
|
||||
form_layout.addRow(self.constrain_cb)
|
||||
|
||||
self.x_width_entry = FloatEntry()
|
||||
self.x_width_entry.set_value(0.0)
|
||||
self.x_width_lbl = QtWidgets.QLabel("Width (DX):")
|
||||
self.x_width_lbl.setToolTip(
|
||||
"The width (DX) within which the panel must fit.\n"
|
||||
"In current units."
|
||||
)
|
||||
form_layout.addRow(self.x_width_lbl, self.x_width_entry)
|
||||
|
||||
self.y_height_entry = FloatEntry()
|
||||
self.y_height_entry.set_value(0.0)
|
||||
self.y_height_lbl = QtWidgets.QLabel("Height (DY):")
|
||||
self.y_height_lbl.setToolTip(
|
||||
"The height (DY)within which the panel must fit.\n"
|
||||
"In current units."
|
||||
)
|
||||
form_layout.addRow(self.y_height_lbl, self.y_height_entry)
|
||||
|
||||
self.constrain_sel = OptionalInputSection(
|
||||
self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
|
||||
|
||||
|
||||
## Buttons
|
||||
hlay_2 = QtWidgets.QHBoxLayout()
|
||||
self.layout.addLayout(hlay_2)
|
||||
|
||||
hlay_2.addStretch()
|
||||
self.panelize_object_button = QtWidgets.QPushButton("Panelize Object")
|
||||
self.panelize_object_button.setToolTip(
|
||||
"Panelize the specified object around the specified box.\n"
|
||||
"In other words it creates multiple copies of the source object,\n"
|
||||
"arranged in a 2D array of rows and columns."
|
||||
)
|
||||
hlay_2.addWidget(self.panelize_object_button)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
## Signals
|
||||
self.panelize_object_button.clicked.connect(self.on_panelize)
|
||||
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
|
||||
self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
|
||||
|
||||
# list to hold the temporary objects
|
||||
self.objs = []
|
||||
|
||||
# final name for the panel object
|
||||
self.outname = ""
|
||||
|
||||
# flag to signal the constrain was activated
|
||||
self.constrain_flag = False
|
||||
|
||||
def on_type_obj_index_changed(self):
|
||||
obj_type = self.type_obj_combo.currentIndex()
|
||||
self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.object_combo.setCurrentIndex(0)
|
||||
|
||||
def on_type_box_index_changed(self):
|
||||
obj_type = self.type_box_combo.currentIndex()
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setCurrentIndex(0)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Panel. Tool")
|
||||
|
||||
def on_panelize(self):
|
||||
name = self.object_combo.currentText()
|
||||
|
||||
# Get source object.
|
||||
try:
|
||||
obj = self.app.collection.get_by_name(str(name))
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
|
||||
return "Could not retrieve object: %s" % name
|
||||
|
||||
panel_obj = obj
|
||||
|
||||
if panel_obj is None:
|
||||
self.app.inform.emit("[error_notcl]Object not found: %s" % panel_obj)
|
||||
return "Object not found: %s" % panel_obj
|
||||
|
||||
boxname = self.box_combo.currentText()
|
||||
|
||||
try:
|
||||
box = self.app.collection.get_by_name(boxname)
|
||||
except:
|
||||
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % boxname)
|
||||
return "Could not retrieve object: %s" % boxname
|
||||
|
||||
if box is None:
|
||||
self.app.inform.emit("[warning]No object Box. Using instead %s" % panel_obj)
|
||||
box = panel_obj
|
||||
|
||||
self.outname = name + '_panelized'
|
||||
|
||||
spacing_columns = self.spacing_columns.get_value()
|
||||
spacing_columns = spacing_columns if spacing_columns is not None else 0
|
||||
|
||||
spacing_rows = self.spacing_rows.get_value()
|
||||
spacing_rows = spacing_rows if spacing_rows is not None else 0
|
||||
|
||||
rows = self.rows.get_value()
|
||||
rows = rows if rows is not None else 1
|
||||
|
||||
columns = self.columns.get_value()
|
||||
columns = columns if columns is not None else 1
|
||||
|
||||
constrain_dx = self.x_width_entry.get_value()
|
||||
constrain_dy = self.y_height_entry.get_value()
|
||||
|
||||
if 0 in {columns, rows}:
|
||||
self.app.inform.emit("[error_notcl]Columns or Rows are zero value. Change them to a positive integer.")
|
||||
return "Columns or Rows are zero value. Change them to a positive integer."
|
||||
|
||||
xmin, ymin, xmax, ymax = box.bounds()
|
||||
lenghtx = xmax - xmin + spacing_columns
|
||||
lenghty = ymax - ymin + spacing_rows
|
||||
|
||||
# check if constrain within an area is desired
|
||||
if self.constrain_cb.isChecked():
|
||||
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
|
||||
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
|
||||
|
||||
# adjust the number of columns and/or rows so the panel will fit within the panel constraint area
|
||||
if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
|
||||
self.constrain_flag = True
|
||||
|
||||
while panel_lengthx > constrain_dx:
|
||||
columns -= 1
|
||||
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
|
||||
while panel_lengthy > constrain_dy:
|
||||
rows -= 1
|
||||
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
|
||||
|
||||
def clean_temp():
|
||||
# deselect all to avoid delete selected object when run delete from shell
|
||||
self.app.collection.set_all_inactive()
|
||||
|
||||
for del_obj in self.objs:
|
||||
self.app.collection.set_active(del_obj.options['name'])
|
||||
self.app.on_delete()
|
||||
|
||||
self.objs[:] = []
|
||||
|
||||
def panelize():
|
||||
if panel_obj is not None:
|
||||
self.app.inform.emit("Generating panel ... Please wait.")
|
||||
|
||||
self.app.progress.emit(10)
|
||||
|
||||
if isinstance(panel_obj, FlatCAMExcellon):
|
||||
currenty = 0.0
|
||||
self.app.progress.emit(0)
|
||||
|
||||
def initialize_local_excellon(obj_init, app):
|
||||
obj_init.tools = panel_obj.tools
|
||||
# drills are offset, so they need to be deep copied
|
||||
obj_init.drills = deepcopy(panel_obj.drills)
|
||||
obj_init.offset([float(currentx), float(currenty)])
|
||||
obj_init.create_geometry()
|
||||
self.objs.append(obj_init)
|
||||
|
||||
self.app.progress.emit(0)
|
||||
for row in range(rows):
|
||||
currentx = 0.0
|
||||
for col in range(columns):
|
||||
local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
|
||||
self.app.new_object("excellon", local_outname, initialize_local_excellon, plot=False,
|
||||
autoselected=False)
|
||||
currentx += lenghtx
|
||||
currenty += lenghty
|
||||
else:
|
||||
currenty = 0
|
||||
self.app.progress.emit(0)
|
||||
|
||||
def initialize_local_geometry(obj_init, app):
|
||||
obj_init.solid_geometry = panel_obj.solid_geometry
|
||||
obj_init.offset([float(currentx), float(currenty)]),
|
||||
self.objs.append(obj_init)
|
||||
|
||||
self.app.progress.emit(0)
|
||||
for row in range(rows):
|
||||
currentx = 0
|
||||
|
||||
for col in range(columns):
|
||||
local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
|
||||
self.app.new_object("geometry", local_outname, initialize_local_geometry, plot=False,
|
||||
autoselected=False)
|
||||
currentx += lenghtx
|
||||
currenty += lenghty
|
||||
|
||||
def job_init_geometry(obj_fin, app_obj):
|
||||
FlatCAMGeometry.merge(self.objs, obj_fin)
|
||||
|
||||
def job_init_excellon(obj_fin, app_obj):
|
||||
# merge expects tools to exist in the target object
|
||||
obj_fin.tools = panel_obj.tools.copy()
|
||||
FlatCAMExcellon.merge(self.objs, obj_fin)
|
||||
|
||||
if isinstance(panel_obj, FlatCAMExcellon):
|
||||
self.app.progress.emit(50)
|
||||
self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
|
||||
else:
|
||||
self.app.progress.emit(50)
|
||||
self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
|
||||
|
||||
else:
|
||||
self.app.inform.emit("[error_notcl] Obj is None")
|
||||
return "ERROR: Obj is None"
|
||||
|
||||
panelize()
|
||||
clean_temp()
|
||||
if self.constrain_flag is False:
|
||||
self.app.inform.emit("[success]Panel done...")
|
||||
else:
|
||||
self.constrain_flag = False
|
||||
self.app.inform.emit("[warning] Too big for the constrain area. Final panel has %s columns and %s rows" %
|
||||
(columns, rows))
|
||||
|
||||
# proc = self.app.proc_container.new("Generating panel ... Please wait.")
|
||||
#
|
||||
# def job_thread(app_obj):
|
||||
# try:
|
||||
# panelize()
|
||||
# except Exception as e:
|
||||
# proc.done()
|
||||
# raise e
|
||||
# proc.done()
|
||||
#
|
||||
# self.app.collection.promise(self.outname)
|
||||
# self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
||||
|
||||
def reset_fields(self):
|
||||
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
123
flatcamTools/ToolProperties.py
Normal file
123
flatcamTools/ToolProperties.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
|
||||
|
||||
class Properties(FlatCAMTool):
|
||||
|
||||
toolName = "Properties"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||
|
||||
# this way I can hide/show the frame
|
||||
self.properties_frame = QtWidgets.QFrame()
|
||||
self.properties_frame.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.properties_frame)
|
||||
self.properties_box = QtWidgets.QVBoxLayout()
|
||||
self.properties_box.setContentsMargins(0, 0, 0, 0)
|
||||
self.properties_frame.setLayout(self.properties_box)
|
||||
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b> %s</b></font>" % self.toolName)
|
||||
self.properties_box.addWidget(title_label)
|
||||
|
||||
# self.layout.setMargin(0) # PyQt4
|
||||
self.properties_box.setContentsMargins(0, 0, 0, 0) # PyQt5
|
||||
|
||||
self.vlay = QtWidgets.QVBoxLayout()
|
||||
|
||||
self.properties_box.addLayout(self.vlay)
|
||||
|
||||
self.treeWidget = QtWidgets.QTreeWidget()
|
||||
self.treeWidget.setColumnCount(2)
|
||||
self.treeWidget.setHeaderHidden(True)
|
||||
self.treeWidget.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
||||
self.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
|
||||
|
||||
self.vlay.addWidget(self.treeWidget)
|
||||
self.vlay.setStretch(0,0)
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.app.tool_tab_locked is True:
|
||||
return
|
||||
|
||||
# this reset the TreeWidget
|
||||
self.treeWidget.clear()
|
||||
self.properties_frame.show()
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.properties()
|
||||
|
||||
def properties(self):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[error_notcl] Properties Tool was not displayed. No object selected.")
|
||||
self.app.ui.notebook.setTabText(2, "Tools")
|
||||
self.properties_frame.hide()
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
||||
return
|
||||
for obj in obj_list:
|
||||
self.addItems(obj)
|
||||
self.app.inform.emit("[success] Object Properties are displayed.")
|
||||
self.app.ui.notebook.setTabText(2, "Properties Tool")
|
||||
|
||||
def addItems(self, obj):
|
||||
parent = self.treeWidget.invisibleRootItem()
|
||||
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
obj_type = self.addParent(parent, 'TYPE', expanded=True, color=QtGui.QColor("#000000"), font=font)
|
||||
obj_name = self.addParent(parent, 'NAME', expanded=True, color=QtGui.QColor("#000000"), font=font)
|
||||
dims = self.addParent(parent, 'Dimensions', expanded=True, color=QtGui.QColor("#000000"), font=font)
|
||||
options = self.addParent(parent, 'Options', color=QtGui.QColor("#000000"), font=font)
|
||||
separator = self.addParent(parent, '')
|
||||
|
||||
self.addChild(obj_type, [obj.kind.upper()])
|
||||
self.addChild(obj_name, [obj.options['name']])
|
||||
|
||||
# calculate physical dimensions
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
length = abs(xmax - xmin)
|
||||
width = abs(ymax - ymin)
|
||||
|
||||
self.addChild(dims, ['Length:', '%.4f %s' % (
|
||||
length, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
|
||||
self.addChild(dims, ['Width:', '%.4f %s' % (
|
||||
width, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
|
||||
if self.app.general_options_form.general_group.units_radio.get_value().lower() == 'mm':
|
||||
area = (length * width) / 100
|
||||
self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'cm2')], True)
|
||||
else:
|
||||
area = length * width
|
||||
self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'in2')], True)
|
||||
|
||||
for option in obj.options:
|
||||
if option is 'name':
|
||||
continue
|
||||
self.addChild(options, [str(option), str(obj.options[option])], True)
|
||||
|
||||
self.addChild(separator, [''])
|
||||
|
||||
def addParent(self, parent, title, expanded=False, color=None, font=None):
|
||||
item = QtWidgets.QTreeWidgetItem(parent, [title])
|
||||
item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
|
||||
item.setExpanded(expanded)
|
||||
if color is not None:
|
||||
# item.setTextColor(0, color) # PyQt4
|
||||
item.setForeground(0, QtGui.QBrush(color))
|
||||
if font is not None:
|
||||
item.setFont(0, font)
|
||||
return item
|
||||
|
||||
def addChild(self, parent, title, column1=None):
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
item.setText(0, str(title[0]))
|
||||
if column1 is not None:
|
||||
item.setText(1, str(title[1]))
|
||||
|
||||
# end of file
|
||||
361
flatcamTools/ToolShell.py
Normal file
361
flatcamTools/ToolShell.py
Normal file
@@ -0,0 +1,361 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# Author: Juan Pablo Caram (c) #
|
||||
# Date: 2/5/2014 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
import html
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtCore import Qt, QStringListModel
|
||||
from PyQt5.QtGui import QColor, QKeySequence, QPalette, QTextCursor
|
||||
from PyQt5.QtWidgets import QLineEdit, QSizePolicy, QTextEdit, QVBoxLayout, QWidget, QCompleter, QAction
|
||||
|
||||
class _BrowserTextEdit(QTextEdit):
|
||||
|
||||
def __init__(self):
|
||||
QTextEdit.__init__(self)
|
||||
self.menu = None
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.menu = self.createStandardContextMenu(event.pos())
|
||||
clear_action = QAction("Clear", self)
|
||||
clear_action.setShortcut(QKeySequence(Qt.Key_Delete)) # it's not working, the shortcut
|
||||
self.menu.addAction(clear_action)
|
||||
clear_action.triggered.connect(self.clear)
|
||||
self.menu.exec_(event.globalPos())
|
||||
|
||||
|
||||
def clear(self):
|
||||
QTextEdit.clear(self)
|
||||
text = "FlatCAM 3000\n(c) 2014-2019 Juan Pablo Caram\n\nType help to get started.\n\n"
|
||||
text = html.escape(text)
|
||||
text = text.replace('\n', '<br/>')
|
||||
self.moveCursor(QTextCursor.End)
|
||||
self.insertHtml(text)
|
||||
|
||||
class _ExpandableTextEdit(QTextEdit):
|
||||
"""
|
||||
Class implements edit line, which expands themselves automatically
|
||||
"""
|
||||
|
||||
historyNext = pyqtSignal()
|
||||
historyPrev = pyqtSignal()
|
||||
|
||||
def __init__(self, termwidget, *args):
|
||||
QTextEdit.__init__(self, *args)
|
||||
self.setStyleSheet("font: 9pt \"Courier\";")
|
||||
self._fittedHeight = 1
|
||||
self.textChanged.connect(self._fit_to_document)
|
||||
self._fit_to_document()
|
||||
self._termWidget = termwidget
|
||||
|
||||
self.completer = MyCompleter()
|
||||
|
||||
self.model = QStringListModel()
|
||||
self.completer.setModel(self.model)
|
||||
self.set_model_data(keyword_list=[])
|
||||
self.completer.insertText.connect(self.insertCompletion)
|
||||
|
||||
def set_model_data(self, keyword_list):
|
||||
self.model.setStringList(keyword_list)
|
||||
|
||||
def insertCompletion(self, completion):
|
||||
tc = self.textCursor()
|
||||
extra = (len(completion) - len(self.completer.completionPrefix()))
|
||||
tc.movePosition(QTextCursor.Left)
|
||||
tc.movePosition(QTextCursor.EndOfWord)
|
||||
tc.insertText(completion[-extra:])
|
||||
self.setTextCursor(tc)
|
||||
self.completer.popup().hide()
|
||||
|
||||
def focusInEvent(self, event):
|
||||
if self.completer:
|
||||
self.completer.setWidget(self)
|
||||
QTextEdit.focusInEvent(self, event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Catch keyboard events. Process Enter, Up, Down
|
||||
"""
|
||||
if event.matches(QKeySequence.InsertParagraphSeparator):
|
||||
text = self.toPlainText()
|
||||
if self._termWidget.is_command_complete(text):
|
||||
self._termWidget.exec_current_command()
|
||||
return
|
||||
elif event.matches(QKeySequence.MoveToNextLine):
|
||||
text = self.toPlainText()
|
||||
cursor_pos = self.textCursor().position()
|
||||
textBeforeEnd = text[cursor_pos:]
|
||||
|
||||
if len(textBeforeEnd.split('\n')) <= 1:
|
||||
self.historyNext.emit()
|
||||
return
|
||||
elif event.matches(QKeySequence.MoveToPreviousLine):
|
||||
text = self.toPlainText()
|
||||
cursor_pos = self.textCursor().position()
|
||||
text_before_start = text[:cursor_pos]
|
||||
# lineCount = len(textBeforeStart.splitlines())
|
||||
line_count = len(text_before_start.split('\n'))
|
||||
if len(text_before_start) > 0 and \
|
||||
(text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
|
||||
line_count += 1
|
||||
if line_count <= 1:
|
||||
self.historyPrev.emit()
|
||||
return
|
||||
elif event.matches(QKeySequence.MoveToNextPage) or \
|
||||
event.matches(QKeySequence.MoveToPreviousPage):
|
||||
return self._termWidget.browser().keyPressEvent(event)
|
||||
|
||||
tc = self.textCursor()
|
||||
if event.key() == Qt.Key_Tab and self.completer.popup().isVisible():
|
||||
self.completer.insertText.emit(self.completer.getSelected())
|
||||
self.completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
return
|
||||
|
||||
QTextEdit.keyPressEvent(self, event)
|
||||
tc.select(QTextCursor.WordUnderCursor)
|
||||
cr = self.cursorRect()
|
||||
|
||||
if len(tc.selectedText()) > 0:
|
||||
self.completer.setCompletionPrefix(tc.selectedText())
|
||||
popup = self.completer.popup()
|
||||
popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
|
||||
|
||||
cr.setWidth(self.completer.popup().sizeHintForColumn(0)
|
||||
+ self.completer.popup().verticalScrollBar().sizeHint().width())
|
||||
self.completer.complete(cr)
|
||||
else:
|
||||
self.completer.popup().hide()
|
||||
|
||||
def sizeHint(self):
|
||||
"""
|
||||
QWidget sizeHint impelemtation
|
||||
"""
|
||||
hint = QTextEdit.sizeHint(self)
|
||||
hint.setHeight(self._fittedHeight)
|
||||
return hint
|
||||
|
||||
def _fit_to_document(self):
|
||||
"""
|
||||
Update widget height to fit all text
|
||||
"""
|
||||
documentsize = self.document().size().toSize()
|
||||
self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
|
||||
self.setMaximumHeight(self._fittedHeight)
|
||||
self.updateGeometry()
|
||||
|
||||
def insertFromMimeData(self, mime_data):
|
||||
# Paste only plain text.
|
||||
self.insertPlainText(mime_data.text())
|
||||
|
||||
|
||||
class MyCompleter(QCompleter):
|
||||
insertText = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QCompleter.__init__(self)
|
||||
self.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.highlighted.connect(self.setHighlighted)
|
||||
|
||||
def setHighlighted(self, text):
|
||||
self.lastSelected = text
|
||||
|
||||
def getSelected(self):
|
||||
return self.lastSelected
|
||||
|
||||
|
||||
class TermWidget(QWidget):
|
||||
"""
|
||||
Widget wich represents terminal. It only displays text and allows to enter text.
|
||||
All highlevel logic should be implemented by client classes
|
||||
|
||||
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
QWidget.__init__(self, *args)
|
||||
|
||||
self._browser = _BrowserTextEdit()
|
||||
self._browser.setStyleSheet("font: 9pt \"Courier\";")
|
||||
self._browser.setReadOnly(True)
|
||||
self._browser.document().setDefaultStyleSheet(
|
||||
self._browser.document().defaultStyleSheet() +
|
||||
"span {white-space:pre;}")
|
||||
|
||||
self._edit = _ExpandableTextEdit(self, self)
|
||||
self._edit.historyNext.connect(self._on_history_next)
|
||||
self._edit.historyPrev.connect(self._on_history_prev)
|
||||
self._edit.setFocus()
|
||||
self.setFocusProxy(self._edit)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._browser)
|
||||
layout.addWidget(self._edit)
|
||||
|
||||
self._history = [''] # current empty line
|
||||
self._historyIndex = 0
|
||||
|
||||
def open_proccessing(self, detail=None):
|
||||
"""
|
||||
Open processing and disable using shell commands again until all commands are finished
|
||||
|
||||
:param detail: text detail about what is currently called from TCL to python
|
||||
:return: None
|
||||
"""
|
||||
|
||||
self._edit.setTextColor(Qt.white)
|
||||
self._edit.setTextBackgroundColor(Qt.darkGreen)
|
||||
if detail is None:
|
||||
self._edit.setPlainText("...proccessing...")
|
||||
else:
|
||||
self._edit.setPlainText("...proccessing... [%s]" % detail)
|
||||
|
||||
self._edit.setDisabled(True)
|
||||
self._edit.setFocus()
|
||||
|
||||
def close_proccessing(self):
|
||||
"""
|
||||
Close processing and enable using shell commands again
|
||||
:return:
|
||||
"""
|
||||
|
||||
self._edit.setTextColor(Qt.black)
|
||||
self._edit.setTextBackgroundColor(Qt.white)
|
||||
self._edit.setPlainText('')
|
||||
self._edit.setDisabled(False)
|
||||
self._edit.setFocus()
|
||||
|
||||
def _append_to_browser(self, style, text):
|
||||
"""
|
||||
Convert text to HTML for inserting it to browser
|
||||
"""
|
||||
assert style in ('in', 'out', 'err')
|
||||
|
||||
text = html.escape(text)
|
||||
text = text.replace('\n', '<br/>')
|
||||
|
||||
if style == 'in':
|
||||
text = '<span style="font-weight: bold;">%s</span>' % text
|
||||
elif style == 'err':
|
||||
text = '<span style="font-weight: bold; color: red;">%s</span>' % text
|
||||
else:
|
||||
text = '<span>%s</span>' % text # without span <br/> is ignored!!!
|
||||
|
||||
scrollbar = self._browser.verticalScrollBar()
|
||||
old_value = scrollbar.value()
|
||||
scrollattheend = old_value == scrollbar.maximum()
|
||||
|
||||
self._browser.moveCursor(QTextCursor.End)
|
||||
self._browser.insertHtml(text)
|
||||
|
||||
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
|
||||
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
|
||||
To fix it correctly, srcoll to the bottom, if before intput has been resized,
|
||||
scrollbar was in the bottom, and remove next lien
|
||||
"""
|
||||
scrollattheend = True
|
||||
|
||||
if scrollattheend:
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
else:
|
||||
scrollbar.setValue(old_value)
|
||||
|
||||
def exec_current_command(self):
|
||||
"""
|
||||
Save current command in the history. Append it to the log. Clear edit line
|
||||
Reimplement in the child classes to actually execute command
|
||||
"""
|
||||
text = str(self._edit.toPlainText())
|
||||
self._append_to_browser('in', '> ' + text + '\n')
|
||||
|
||||
if len(self._history) < 2 or\
|
||||
self._history[-2] != text: # don't insert duplicating items
|
||||
if text[-1] == '\n':
|
||||
self._history.insert(-1, text[:-1])
|
||||
else:
|
||||
self._history.insert(-1, text)
|
||||
|
||||
self._historyIndex = len(self._history) - 1
|
||||
|
||||
self._history[-1] = ''
|
||||
self._edit.clear()
|
||||
|
||||
if not text[-1] == '\n':
|
||||
text += '\n'
|
||||
|
||||
self.child_exec_command(text)
|
||||
|
||||
def child_exec_command(self, text):
|
||||
"""
|
||||
Reimplement in the child classes
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_line_break_to_input(self):
|
||||
self._edit.textCursor().insertText('\n')
|
||||
|
||||
def append_output(self, text):
|
||||
"""Appent text to output widget
|
||||
"""
|
||||
self._append_to_browser('out', text)
|
||||
|
||||
def append_error(self, text):
|
||||
"""Appent error text to output widget. Text is drawn with red background
|
||||
"""
|
||||
self._append_to_browser('err', text)
|
||||
|
||||
def is_command_complete(self, text):
|
||||
"""
|
||||
Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
|
||||
"""
|
||||
return True
|
||||
|
||||
def browser(self):
|
||||
return self._browser
|
||||
|
||||
def _on_history_next(self):
|
||||
"""
|
||||
Down pressed, show next item from the history
|
||||
"""
|
||||
if (self._historyIndex + 1) < len(self._history):
|
||||
self._historyIndex += 1
|
||||
self._edit.setPlainText(self._history[self._historyIndex])
|
||||
self._edit.moveCursor(QTextCursor.End)
|
||||
|
||||
def _on_history_prev(self):
|
||||
"""
|
||||
Up pressed, show previous item from the history
|
||||
"""
|
||||
if self._historyIndex > 0:
|
||||
if self._historyIndex == (len(self._history) - 1):
|
||||
self._history[-1] = self._edit.toPlainText()
|
||||
self._historyIndex -= 1
|
||||
self._edit.setPlainText(self._history[self._historyIndex])
|
||||
self._edit.moveCursor(QTextCursor.End)
|
||||
|
||||
class FCShell(TermWidget):
|
||||
def __init__(self, sysShell, *args):
|
||||
TermWidget.__init__(self, *args)
|
||||
self._sysShell = sysShell
|
||||
|
||||
def is_command_complete(self, text):
|
||||
def skipQuotes(text):
|
||||
quote = text[0]
|
||||
text = text[1:]
|
||||
endIndex = str(text).index(quote)
|
||||
return text[endIndex:]
|
||||
while text:
|
||||
if text[0] in ('"', "'"):
|
||||
try:
|
||||
text = skipQuotes(text)
|
||||
except ValueError:
|
||||
return False
|
||||
text = text[1:]
|
||||
return True
|
||||
|
||||
def child_exec_command(self, text):
|
||||
self._sysShell.exec_command(text)
|
||||
756
flatcamTools/ToolTransform.py
Normal file
756
flatcamTools/ToolTransform.py
Normal file
@@ -0,0 +1,756 @@
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
from GUIElements import FCEntry, FCButton, OptionalInputSection
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMObj import *
|
||||
|
||||
|
||||
class ToolTransform(FlatCAMTool):
|
||||
|
||||
toolName = "Object Transform"
|
||||
rotateName = "Rotate"
|
||||
skewName = "Skew/Shear"
|
||||
scaleName = "Scale"
|
||||
flipName = "Mirror (Flip)"
|
||||
offsetName = "Offset"
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.transform_lay = QtWidgets.QVBoxLayout()
|
||||
self.layout.addLayout(self.transform_lay)
|
||||
## Title
|
||||
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
|
||||
self.transform_lay.addWidget(title_label)
|
||||
|
||||
self.empty_label = QtWidgets.QLabel("")
|
||||
self.empty_label.setFixedWidth(50)
|
||||
|
||||
self.empty_label1 = QtWidgets.QLabel("")
|
||||
self.empty_label1.setFixedWidth(70)
|
||||
self.empty_label2 = QtWidgets.QLabel("")
|
||||
self.empty_label2.setFixedWidth(70)
|
||||
self.empty_label3 = QtWidgets.QLabel("")
|
||||
self.empty_label3.setFixedWidth(70)
|
||||
self.empty_label4 = QtWidgets.QLabel("")
|
||||
self.empty_label4.setFixedWidth(70)
|
||||
self.transform_lay.addWidget(self.empty_label)
|
||||
|
||||
## Rotate Title
|
||||
rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
|
||||
self.transform_lay.addWidget(rotate_title_label)
|
||||
|
||||
## Layout
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
self.transform_lay.addLayout(form_layout)
|
||||
form_child = QtWidgets.QFormLayout()
|
||||
|
||||
self.rotate_label = QtWidgets.QLabel("Angle:")
|
||||
self.rotate_label.setToolTip(
|
||||
"Angle for Rotation action, in degrees.\n"
|
||||
"Float number between -360 and 359.\n"
|
||||
"Positive numbers for CW motion.\n"
|
||||
"Negative numbers for CCW motion."
|
||||
)
|
||||
self.rotate_label.setFixedWidth(50)
|
||||
|
||||
self.rotate_entry = FCEntry()
|
||||
self.rotate_entry.setFixedWidth(60)
|
||||
self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
|
||||
self.rotate_button = FCButton()
|
||||
self.rotate_button.set_value("Rotate")
|
||||
self.rotate_button.setToolTip(
|
||||
"Rotate the selected object(s).\n"
|
||||
"The point of reference is the middle of\n"
|
||||
"the bounding box for all selected objects."
|
||||
)
|
||||
self.rotate_button.setFixedWidth(60)
|
||||
|
||||
form_child.addRow(self.rotate_entry, self.rotate_button)
|
||||
form_layout.addRow(self.rotate_label, form_child)
|
||||
|
||||
self.transform_lay.addWidget(self.empty_label1)
|
||||
|
||||
## Skew Title
|
||||
skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
|
||||
self.transform_lay.addWidget(skew_title_label)
|
||||
|
||||
## Form Layout
|
||||
form1_layout = QtWidgets.QFormLayout()
|
||||
self.transform_lay.addLayout(form1_layout)
|
||||
form1_child_1 = QtWidgets.QFormLayout()
|
||||
form1_child_2 = QtWidgets.QFormLayout()
|
||||
|
||||
self.skewx_label = QtWidgets.QLabel("Angle X:")
|
||||
self.skewx_label.setToolTip(
|
||||
"Angle for Skew action, in degrees.\n"
|
||||
"Float number between -360 and 359."
|
||||
)
|
||||
self.skewx_label.setFixedWidth(50)
|
||||
self.skewx_entry = FCEntry()
|
||||
self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.skewx_entry.setFixedWidth(60)
|
||||
|
||||
self.skewx_button = FCButton()
|
||||
self.skewx_button.set_value("Skew X")
|
||||
self.skewx_button.setToolTip(
|
||||
"Skew/shear the selected object(s).\n"
|
||||
"The point of reference is the middle of\n"
|
||||
"the bounding box for all selected objects.")
|
||||
self.skewx_button.setFixedWidth(60)
|
||||
|
||||
self.skewy_label = QtWidgets.QLabel("Angle Y:")
|
||||
self.skewy_label.setToolTip(
|
||||
"Angle for Skew action, in degrees.\n"
|
||||
"Float number between -360 and 359."
|
||||
)
|
||||
self.skewy_label.setFixedWidth(50)
|
||||
self.skewy_entry = FCEntry()
|
||||
self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.skewy_entry.setFixedWidth(60)
|
||||
|
||||
self.skewy_button = FCButton()
|
||||
self.skewy_button.set_value("Skew Y")
|
||||
self.skewy_button.setToolTip(
|
||||
"Skew/shear the selected object(s).\n"
|
||||
"The point of reference is the middle of\n"
|
||||
"the bounding box for all selected objects.")
|
||||
self.skewy_button.setFixedWidth(60)
|
||||
|
||||
form1_child_1.addRow(self.skewx_entry, self.skewx_button)
|
||||
form1_child_2.addRow(self.skewy_entry, self.skewy_button)
|
||||
form1_layout.addRow(self.skewx_label, form1_child_1)
|
||||
form1_layout.addRow(self.skewy_label, form1_child_2)
|
||||
|
||||
self.transform_lay.addWidget(self.empty_label2)
|
||||
|
||||
## Scale Title
|
||||
scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
|
||||
self.transform_lay.addWidget(scale_title_label)
|
||||
|
||||
## Form Layout
|
||||
form2_layout = QtWidgets.QFormLayout()
|
||||
self.transform_lay.addLayout(form2_layout)
|
||||
form2_child_1 = QtWidgets.QFormLayout()
|
||||
form2_child_2 = QtWidgets.QFormLayout()
|
||||
|
||||
self.scalex_label = QtWidgets.QLabel("Factor X:")
|
||||
self.scalex_label.setToolTip(
|
||||
"Factor for Scale action over X axis."
|
||||
)
|
||||
self.scalex_label.setFixedWidth(50)
|
||||
self.scalex_entry = FCEntry()
|
||||
self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.scalex_entry.setFixedWidth(60)
|
||||
|
||||
self.scalex_button = FCButton()
|
||||
self.scalex_button.set_value("Scale X")
|
||||
self.scalex_button.setToolTip(
|
||||
"Scale the selected object(s).\n"
|
||||
"The point of reference depends on \n"
|
||||
"the Scale reference checkbox state.")
|
||||
self.scalex_button.setFixedWidth(60)
|
||||
|
||||
self.scaley_label = QtWidgets.QLabel("Factor Y:")
|
||||
self.scaley_label.setToolTip(
|
||||
"Factor for Scale action over Y axis."
|
||||
)
|
||||
self.scaley_label.setFixedWidth(50)
|
||||
self.scaley_entry = FCEntry()
|
||||
self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.scaley_entry.setFixedWidth(60)
|
||||
|
||||
self.scaley_button = FCButton()
|
||||
self.scaley_button.set_value("Scale Y")
|
||||
self.scaley_button.setToolTip(
|
||||
"Scale the selected object(s).\n"
|
||||
"The point of reference depends on \n"
|
||||
"the Scale reference checkbox state.")
|
||||
self.scaley_button.setFixedWidth(60)
|
||||
|
||||
self.scale_link_cb = FCCheckBox()
|
||||
self.scale_link_cb.set_value(True)
|
||||
self.scale_link_cb.setText("Link")
|
||||
self.scale_link_cb.setToolTip(
|
||||
"Scale the selected object(s)\n"
|
||||
"using the Scale Factor X for both axis.")
|
||||
self.scale_link_cb.setFixedWidth(50)
|
||||
|
||||
self.scale_zero_ref_cb = FCCheckBox()
|
||||
self.scale_zero_ref_cb.set_value(True)
|
||||
self.scale_zero_ref_cb.setText("Scale Reference")
|
||||
self.scale_zero_ref_cb.setToolTip(
|
||||
"Scale the selected object(s)\n"
|
||||
"using the origin reference when checked,\n"
|
||||
"and the center of the biggest bounding box\n"
|
||||
"of the selected objects when unchecked.")
|
||||
|
||||
form2_child_1.addRow(self.scalex_entry, self.scalex_button)
|
||||
form2_child_2.addRow(self.scaley_entry, self.scaley_button)
|
||||
form2_layout.addRow(self.scalex_label, form2_child_1)
|
||||
form2_layout.addRow(self.scaley_label, form2_child_2)
|
||||
form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb)
|
||||
self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
|
||||
|
||||
self.transform_lay.addWidget(self.empty_label3)
|
||||
|
||||
## Offset Title
|
||||
offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
|
||||
self.transform_lay.addWidget(offset_title_label)
|
||||
|
||||
## Form Layout
|
||||
form3_layout = QtWidgets.QFormLayout()
|
||||
self.transform_lay.addLayout(form3_layout)
|
||||
form3_child_1 = QtWidgets.QFormLayout()
|
||||
form3_child_2 = QtWidgets.QFormLayout()
|
||||
|
||||
self.offx_label = QtWidgets.QLabel("Value X:")
|
||||
self.offx_label.setToolTip(
|
||||
"Value for Offset action on X axis."
|
||||
)
|
||||
self.offx_label.setFixedWidth(50)
|
||||
self.offx_entry = FCEntry()
|
||||
self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.offx_entry.setFixedWidth(60)
|
||||
|
||||
self.offx_button = FCButton()
|
||||
self.offx_button.set_value("Offset X")
|
||||
self.offx_button.setToolTip(
|
||||
"Offset the selected object(s).\n"
|
||||
"The point of reference is the middle of\n"
|
||||
"the bounding box for all selected objects.\n")
|
||||
self.offx_button.setFixedWidth(60)
|
||||
|
||||
self.offy_label = QtWidgets.QLabel("Value Y:")
|
||||
self.offy_label.setToolTip(
|
||||
"Value for Offset action on Y axis."
|
||||
)
|
||||
self.offy_label.setFixedWidth(50)
|
||||
self.offy_entry = FCEntry()
|
||||
self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.offy_entry.setFixedWidth(60)
|
||||
|
||||
self.offy_button = FCButton()
|
||||
self.offy_button.set_value("Offset Y")
|
||||
self.offy_button.setToolTip(
|
||||
"Offset the selected object(s).\n"
|
||||
"The point of reference is the middle of\n"
|
||||
"the bounding box for all selected objects.\n")
|
||||
self.offy_button.setFixedWidth(60)
|
||||
|
||||
form3_child_1.addRow(self.offx_entry, self.offx_button)
|
||||
form3_child_2.addRow(self.offy_entry, self.offy_button)
|
||||
form3_layout.addRow(self.offx_label, form3_child_1)
|
||||
form3_layout.addRow(self.offy_label, form3_child_2)
|
||||
|
||||
self.transform_lay.addWidget(self.empty_label4)
|
||||
|
||||
## Flip Title
|
||||
flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
|
||||
self.transform_lay.addWidget(flip_title_label)
|
||||
|
||||
## Form Layout
|
||||
form4_layout = QtWidgets.QFormLayout()
|
||||
self.transform_lay.addLayout(form4_layout)
|
||||
form4_child = QtWidgets.QFormLayout()
|
||||
form4_child_1 = QtWidgets.QFormLayout()
|
||||
|
||||
self.flipx_button = FCButton()
|
||||
self.flipx_button.set_value("Flip on X")
|
||||
self.flipx_button.setToolTip(
|
||||
"Flip the selected object(s) over the X axis.\n"
|
||||
"Does not create a new object.\n "
|
||||
)
|
||||
self.flipx_button.setFixedWidth(60)
|
||||
|
||||
self.flipy_button = FCButton()
|
||||
self.flipy_button.set_value("Flip on Y")
|
||||
self.flipy_button.setToolTip(
|
||||
"Flip the selected object(s) over the X axis.\n"
|
||||
"Does not create a new object.\n "
|
||||
)
|
||||
self.flipy_button.setFixedWidth(60)
|
||||
|
||||
self.flip_ref_cb = FCCheckBox()
|
||||
self.flip_ref_cb.set_value(True)
|
||||
self.flip_ref_cb.setText("Ref Pt")
|
||||
self.flip_ref_cb.setToolTip(
|
||||
"Flip the selected object(s)\n"
|
||||
"around the point in Point Entry Field.\n"
|
||||
"\n"
|
||||
"The point coordinates can be captured by\n"
|
||||
"left click on canvas together with pressing\n"
|
||||
"SHIFT key. \n"
|
||||
"Then click Add button to insert coordinates.\n"
|
||||
"Or enter the coords in format (x, y) in the\n"
|
||||
"Point Entry field and click Flip on X(Y)")
|
||||
self.flip_ref_cb.setFixedWidth(50)
|
||||
|
||||
self.flip_ref_label = QtWidgets.QLabel("Point:")
|
||||
self.flip_ref_label.setToolTip(
|
||||
"Coordinates in format (x, y) used as reference for mirroring.\n"
|
||||
"The 'x' in (x, y) will be used when using Flip on X and\n"
|
||||
"the 'y' in (x, y) will be used when using Flip on Y and"
|
||||
)
|
||||
self.flip_ref_label.setFixedWidth(50)
|
||||
self.flip_ref_entry = EvalEntry2("(0, 0)")
|
||||
self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
self.flip_ref_entry.setFixedWidth(60)
|
||||
|
||||
self.flip_ref_button = FCButton()
|
||||
self.flip_ref_button.set_value("Add")
|
||||
self.flip_ref_button.setToolTip(
|
||||
"The point coordinates can be captured by\n"
|
||||
"left click on canvas together with pressing\n"
|
||||
"SHIFT key. Then click Add button to insert.")
|
||||
self.flip_ref_button.setFixedWidth(60)
|
||||
|
||||
form4_child.addRow(self.flipx_button, self.flipy_button)
|
||||
form4_child_1.addRow(self.flip_ref_entry, self.flip_ref_button)
|
||||
|
||||
form4_layout.addRow(self.empty_label, form4_child)
|
||||
form4_layout.addRow(self.flip_ref_cb)
|
||||
form4_layout.addRow(self.flip_ref_label, form4_child_1)
|
||||
self.ois_flip = OptionalInputSection(self.flip_ref_cb,
|
||||
[self.flip_ref_entry, self.flip_ref_button], logic=True)
|
||||
|
||||
self.transform_lay.addStretch()
|
||||
|
||||
## Signals
|
||||
self.rotate_button.clicked.connect(self.on_rotate)
|
||||
self.skewx_button.clicked.connect(self.on_skewx)
|
||||
self.skewy_button.clicked.connect(self.on_skewy)
|
||||
self.scalex_button.clicked.connect(self.on_scalex)
|
||||
self.scaley_button.clicked.connect(self.on_scaley)
|
||||
self.offx_button.clicked.connect(self.on_offx)
|
||||
self.offy_button.clicked.connect(self.on_offy)
|
||||
self.flipx_button.clicked.connect(self.on_flipx)
|
||||
self.flipy_button.clicked.connect(self.on_flipy)
|
||||
self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
|
||||
|
||||
self.rotate_entry.returnPressed.connect(self.on_rotate)
|
||||
self.skewx_entry.returnPressed.connect(self.on_skewx)
|
||||
self.skewy_entry.returnPressed.connect(self.on_skewy)
|
||||
self.scalex_entry.returnPressed.connect(self.on_scalex)
|
||||
self.scaley_entry.returnPressed.connect(self.on_scaley)
|
||||
self.offx_entry.returnPressed.connect(self.on_offx)
|
||||
self.offy_entry.returnPressed.connect(self.on_offy)
|
||||
|
||||
|
||||
## Initialize form
|
||||
self.rotate_entry.set_value('0')
|
||||
self.skewx_entry.set_value('0')
|
||||
self.skewy_entry.set_value('0')
|
||||
self.scalex_entry.set_value('1')
|
||||
self.scaley_entry.set_value('1')
|
||||
self.offx_entry.set_value('0')
|
||||
self.offy_entry.set_value('0')
|
||||
self.flip_ref_cb.setChecked(False)
|
||||
|
||||
def run(self):
|
||||
FlatCAMTool.run(self)
|
||||
self.app.ui.notebook.setTabText(2, "Transform Tool")
|
||||
|
||||
def on_rotate(self):
|
||||
try:
|
||||
value = float(self.rotate_entry.get_value())
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error] Failed to rotate due of: %s" % str(e))
|
||||
return
|
||||
self.app.worker_task.emit({'fcn': self.on_rotate_action,
|
||||
'params': [value]})
|
||||
# self.on_rotate_action(value)
|
||||
return
|
||||
|
||||
def on_flipx(self):
|
||||
# self.on_flip("Y")
|
||||
axis = 'Y'
|
||||
self.app.worker_task.emit({'fcn': self.on_flip,
|
||||
'params': [axis]})
|
||||
return
|
||||
|
||||
def on_flipy(self):
|
||||
# self.on_flip("X")
|
||||
axis = 'X'
|
||||
self.app.worker_task.emit({'fcn': self.on_flip,
|
||||
'params': [axis]})
|
||||
return
|
||||
|
||||
def on_flip_add_coords(self):
|
||||
val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
|
||||
self.flip_ref_entry.set_value(val)
|
||||
|
||||
def on_skewx(self):
|
||||
try:
|
||||
value = float(self.skewx_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Skew!")
|
||||
return
|
||||
# self.on_skew("X", value)
|
||||
axis = 'X'
|
||||
self.app.worker_task.emit({'fcn': self.on_skew,
|
||||
'params': [axis, value]})
|
||||
return
|
||||
|
||||
def on_skewy(self):
|
||||
try:
|
||||
value = float(self.skewy_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Skew!")
|
||||
return
|
||||
# self.on_skew("Y", value)
|
||||
axis = 'Y'
|
||||
self.app.worker_task.emit({'fcn': self.on_skew,
|
||||
'params': [axis, value]})
|
||||
return
|
||||
|
||||
def on_scalex(self):
|
||||
try:
|
||||
xvalue = float(self.scalex_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Scale!")
|
||||
return
|
||||
# scaling to zero has no sense so we remove it, because scaling with 1 does nothing
|
||||
if xvalue == 0:
|
||||
xvalue = 1
|
||||
if self.scale_link_cb.get_value():
|
||||
yvalue = xvalue
|
||||
else:
|
||||
yvalue = 1
|
||||
|
||||
axis = 'X'
|
||||
point = (0, 0)
|
||||
if self.scale_zero_ref_cb.get_value():
|
||||
self.app.worker_task.emit({'fcn': self.on_scale,
|
||||
'params': [axis, xvalue, yvalue, point]})
|
||||
# self.on_scale("X", xvalue, yvalue, point=(0,0))
|
||||
else:
|
||||
# self.on_scale("X", xvalue, yvalue)
|
||||
self.app.worker_task.emit({'fcn': self.on_scale,
|
||||
'params': [axis, xvalue, yvalue]})
|
||||
|
||||
return
|
||||
|
||||
def on_scaley(self):
|
||||
xvalue = 1
|
||||
try:
|
||||
yvalue = float(self.scaley_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Scale!")
|
||||
return
|
||||
# scaling to zero has no sense so we remove it, because scaling with 1 does nothing
|
||||
if yvalue == 0:
|
||||
yvalue = 1
|
||||
|
||||
axis = 'Y'
|
||||
point = (0, 0)
|
||||
if self.scale_zero_ref_cb.get_value():
|
||||
self.app.worker_task.emit({'fcn': self.on_scale,
|
||||
'params': [axis, xvalue, yvalue, point]})
|
||||
# self.on_scale("Y", xvalue, yvalue, point=(0,0))
|
||||
else:
|
||||
# self.on_scale("Y", xvalue, yvalue)
|
||||
self.app.worker_task.emit({'fcn': self.on_scale,
|
||||
'params': [axis, xvalue, yvalue]})
|
||||
|
||||
return
|
||||
|
||||
def on_offx(self):
|
||||
try:
|
||||
value = float(self.offx_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Offset!")
|
||||
return
|
||||
# self.on_offset("X", value)
|
||||
axis = 'X'
|
||||
self.app.worker_task.emit({'fcn': self.on_offset,
|
||||
'params': [axis, value]})
|
||||
return
|
||||
|
||||
def on_offy(self):
|
||||
try:
|
||||
value = float(self.offy_entry.get_value())
|
||||
except:
|
||||
self.app.inform.emit("[warning_notcl] No value for Offset!")
|
||||
return
|
||||
# self.on_offset("Y", value)
|
||||
axis = 'Y'
|
||||
self.app.worker_task.emit({'fcn': self.on_offset,
|
||||
'params': [axis, value]})
|
||||
return
|
||||
|
||||
def on_rotate_action(self, num):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to rotate!")
|
||||
return
|
||||
else:
|
||||
with self.app.proc_container.new("Appying Rotate"):
|
||||
try:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
xmaxlist.append(xmax)
|
||||
ymaxlist.append(ymax)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
xmaximal = max(xmaxlist)
|
||||
ymaximal = max(ymaxlist)
|
||||
|
||||
self.app.progress.emit(20)
|
||||
|
||||
for sel_obj in obj_list:
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
if isinstance(sel_obj, FlatCAMCNCjob):
|
||||
self.app.inform.emit("CNCJob objects can't be rotated.")
|
||||
else:
|
||||
sel_obj.rotate(-num, point=(px, py))
|
||||
sel_obj.plot()
|
||||
self.app.object_changed.emit(sel_obj)
|
||||
|
||||
# add information to the object that it was changed and how much
|
||||
sel_obj.options['rotate'] = num
|
||||
|
||||
self.app.inform.emit('Object(s) were rotated ...')
|
||||
self.app.progress.emit(100)
|
||||
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e))
|
||||
return
|
||||
|
||||
def on_flip(self, axis):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to flip!")
|
||||
return
|
||||
else:
|
||||
with self.app.proc_container.new("Applying Flip"):
|
||||
try:
|
||||
# get mirroring coords from the point entry
|
||||
if self.flip_ref_cb.isChecked():
|
||||
px, py = eval('{}'.format(self.flip_ref_entry.text()))
|
||||
# get mirroing coords from the center of an all-enclosing bounding box
|
||||
else:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
xmaxlist.append(xmax)
|
||||
ymaxlist.append(ymax)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
xmaximal = max(xmaxlist)
|
||||
ymaximal = max(ymaxlist)
|
||||
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
|
||||
self.app.progress.emit(20)
|
||||
|
||||
# execute mirroring
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
self.app.inform.emit("CNCJob objects can't be mirrored/flipped.")
|
||||
else:
|
||||
if axis is 'X':
|
||||
obj.mirror('X', (px, py))
|
||||
# add information to the object that it was changed and how much
|
||||
# the axis is reversed because of the reference
|
||||
if 'mirror_y' in obj.options:
|
||||
obj.options['mirror_y'] = not obj.options['mirror_y']
|
||||
else:
|
||||
obj.options['mirror_y'] = True
|
||||
obj.plot()
|
||||
self.app.inform.emit('Flipped on the Y axis ...')
|
||||
elif axis is 'Y':
|
||||
obj.mirror('Y', (px, py))
|
||||
# add information to the object that it was changed and how much
|
||||
# the axis is reversed because of the reference
|
||||
if 'mirror_x' in obj.options:
|
||||
obj.options['mirror_x'] = not obj.options['mirror_x']
|
||||
else:
|
||||
obj.options['mirror_x'] = True
|
||||
obj.plot()
|
||||
self.app.inform.emit('Flipped on the X axis ...')
|
||||
self.app.object_changed.emit(obj)
|
||||
|
||||
self.app.progress.emit(100)
|
||||
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
|
||||
return
|
||||
|
||||
def on_skew(self, axis, num):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to shear/skew!")
|
||||
return
|
||||
else:
|
||||
with self.app.proc_container.new("Applying Skew"):
|
||||
try:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
|
||||
self.app.progress.emit(20)
|
||||
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
self.app.inform.emit("CNCJob objects can't be skewed.")
|
||||
else:
|
||||
if axis is 'X':
|
||||
obj.skew(num, 0, point=(xminimal, yminimal))
|
||||
# add information to the object that it was changed and how much
|
||||
obj.options['skew_x'] = num
|
||||
elif axis is 'Y':
|
||||
obj.skew(0, num, point=(xminimal, yminimal))
|
||||
# add information to the object that it was changed and how much
|
||||
obj.options['skew_y'] = num
|
||||
obj.plot()
|
||||
self.app.object_changed.emit(obj)
|
||||
self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis))
|
||||
self.app.progress.emit(100)
|
||||
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error_notcl] Due of %s, Skew action was not executed." % str(e))
|
||||
return
|
||||
|
||||
def on_scale(self, axis, xfactor, yfactor, point=None):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to scale!")
|
||||
return
|
||||
else:
|
||||
with self.app.proc_container.new("Applying Scale"):
|
||||
try:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
xmaxlist.append(xmax)
|
||||
ymaxlist.append(ymax)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
xmaximal = max(xmaxlist)
|
||||
ymaximal = max(ymaxlist)
|
||||
|
||||
self.app.progress.emit(20)
|
||||
|
||||
if point is None:
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
else:
|
||||
px = 0
|
||||
py = 0
|
||||
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
self.app.inform.emit("CNCJob objects can't be scaled.")
|
||||
else:
|
||||
obj.scale(xfactor, yfactor, point=(px, py))
|
||||
# add information to the object that it was changed and how much
|
||||
obj.options['scale_x'] = xfactor
|
||||
obj.options['scale_y'] = yfactor
|
||||
obj.plot()
|
||||
self.app.object_changed.emit(obj)
|
||||
self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
|
||||
self.app.progress.emit(100)
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error_notcl] Due of %s, Scale action was not executed." % str(e))
|
||||
return
|
||||
|
||||
def on_offset(self, axis, num):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
|
||||
if not obj_list:
|
||||
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to offset!")
|
||||
return
|
||||
else:
|
||||
with self.app.proc_container.new("Applying Offset"):
|
||||
try:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
self.app.progress.emit(20)
|
||||
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, FlatCAMCNCjob):
|
||||
self.app.inform.emit("CNCJob objects can't be offseted.")
|
||||
else:
|
||||
if axis is 'X':
|
||||
obj.offset((num, 0))
|
||||
# add information to the object that it was changed and how much
|
||||
obj.options['offset_x'] = num
|
||||
elif axis is 'Y':
|
||||
obj.offset((0, num))
|
||||
# add information to the object that it was changed and how much
|
||||
obj.options['offset_y'] = num
|
||||
obj.plot()
|
||||
self.app.object_changed.emit(obj)
|
||||
self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis))
|
||||
self.app.progress.emit(100)
|
||||
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[error_notcl] Due of %s, Offset action was not executed." % str(e))
|
||||
return
|
||||
|
||||
# end of file
|
||||
16
flatcamTools/__init__.py
Normal file
16
flatcamTools/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import sys
|
||||
|
||||
from flatcamTools.ToolMeasurement import Measurement
|
||||
from flatcamTools.ToolPanelize import Panelize
|
||||
from flatcamTools.ToolFilm import Film
|
||||
from flatcamTools.ToolMove import ToolMove
|
||||
from flatcamTools.ToolDblSided import DblSidedTool
|
||||
from flatcamTools.ToolCutout import ToolCutout
|
||||
from flatcamTools.ToolCalculators import ToolCalculator
|
||||
from flatcamTools.ToolProperties import Properties
|
||||
from flatcamTools.ToolImage import ToolImage
|
||||
from flatcamTools.ToolPaint import ToolPaint
|
||||
from flatcamTools.ToolNonCopperClear import NonCopperClear
|
||||
from flatcamTools.ToolTransform import ToolTransform
|
||||
|
||||
from flatcamTools.ToolShell import FCShell
|
||||
Reference in New Issue
Block a user