-clean-up before merge

This commit is contained in:
Marius Stanciu
2019-01-03 21:25:08 +02:00
committed by Marius S
parent 421e9766ea
commit e48d2d2f49
266 changed files with 42425 additions and 0 deletions

View 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
View 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()))

View 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
View 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
View 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")

View 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
View 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

View 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

File diff suppressed because it is too large Load Diff

View 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()))

View 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>&nbsp;%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
View 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)

View 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
View 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