Merge branch 'original'

# Conflicts:
#	FlatCAMApp.py
#	FlatCAMGUI.py
#	FlatCAMObj.py
#	ObjectCollection.py
#	PlotCanvas.py
#	README.md
#	camlib.pyc
#	descartes/__init__.pyc
#	descartes/patch.pyc
This commit is contained in:
HDR
2016-07-15 14:47:02 +06:00
61 changed files with 8318 additions and 1117 deletions

View File

@@ -169,8 +169,10 @@ class DblSidedTool(FlatCAMTool):
# For now, lets limit to Gerbers and Excellons.
# assert isinstance(gerb, FlatCAMGerber)
if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
if not isinstance(fcobj, FlatCAMGerber) and \
not isinstance(fcobj, FlatCAMExcellon) and \
not isinstance(fcobj, FlatCAMGeometry):
self.info("ERROR: Only Gerber, Excellon and Geometry objects can be mirrored.")
return
axis = self.mirror_axis.get_value()

View File

@@ -2,14 +2,23 @@ import sys
from PyQt4 import QtGui
from FlatCAMApp import App
def debug_trace():
'''Set a tracepoint in the Python debugger that works with Qt'''
"""
Set a tracepoint in the Python debugger that works with Qt
:return: None
"""
from PyQt4.QtCore import pyqtRemoveInputHook
#from pdb import set_trace
pyqtRemoveInputHook()
#set_trace()
debug_trace()
# All X11 calling should be thread safe otherwise we have strange issues
# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
# NOTE: Never talk to the GUI from threads! This is why I commented the above.
app = QtGui.QApplication(sys.argv)
fc = App()
sys.exit(app.exec_())
sys.exit(app.exec_())

File diff suppressed because it is too large Load Diff

View File

@@ -23,54 +23,53 @@ class FlatCAMGUI(QtGui.QMainWindow):
# New
self.menufilenew = QtGui.QAction(QtGui.QIcon('share/file16.png'), '&New', self)
self.menufile.addAction(self.menufilenew)
# Open recent
# Recent
self.recent = self.menufile.addMenu(QtGui.QIcon('share/folder16.png'), "Open recent ...")
# Open gerber ...
self.menufileopengerber = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Gerber ...', self)
self.menufile.addAction(self.menufileopengerber)
# Open Excellon ...
self.menufileopenexcellon = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Excellon ...', self)
self.menufile.addAction(self.menufileopenexcellon)
# Open G-Code ...
self.menufileopengcode = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open G-&Code ...', self)
self.menufile.addAction(self.menufileopengcode)
# Open Project ...
self.menufileopenproject = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self)
self.menufile.addAction(self.menufileopenproject)
# Open gerber
self.menufileopengerber = QtGui.QAction('Open &Gerber ...', self)
self.menufile.addAction(self.menufileopengerber)
# Import SVG ...
self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self)
self.menufile.addAction(self.menufileimportsvg)
# Open Excellon ...
self.menufileopenexcellon = QtGui.QAction('Open &Excellon ...', self)
self.menufile.addAction(self.menufileopenexcellon)
# Open G-Code ...
self.menufileopengcode = QtGui.QAction('Open G-&Code ...', self)
self.menufile.addAction(self.menufileopengcode)
# Open recent
# Recent
self.recent = self.menufile.addMenu("Recent files")
# Separator
self.menufile.addSeparator()
# Save Defaults
self.menufilesavedefaults = QtGui.QAction('Save &Defaults', self)
self.menufile.addAction(self.menufilesavedefaults)
# Separator
self.menufile.addSeparator()
# Export SVG ...
self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self)
self.menufile.addAction(self.menufileexportsvg)
# Save Project
self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self)
self.menufile.addAction(self.menufilesaveproject)
# Save Project As ...
self.menufilesaveprojectas = QtGui.QAction('Save Project &As ...', self)
self.menufilesaveprojectas = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project &As ...', self)
self.menufile.addAction(self.menufilesaveprojectas)
# Save Project Copy ...
self.menufilesaveprojectcopy = QtGui.QAction('Save Project C&opy ...', self)
self.menufilesaveprojectcopy = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project C&opy ...', self)
self.menufile.addAction(self.menufilesaveprojectcopy)
# Separator
self.menufile.addSeparator()
# Save Defaults
self.menufilesavedefaults = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save &Defaults', self)
self.menufile.addAction(self.menufilesavedefaults)
# Quit
exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), 'E&xit', self)
exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), '&Exit', self)
# exitAction.setShortcut('Ctrl+Q')
# exitAction.setStatusTip('Exit application')
exit_action.triggered.connect(QtGui.qApp.quit)
@@ -145,9 +144,11 @@ class FlatCAMGUI(QtGui.QMainWindow):
### Notebook ###
################
self.notebook = QtGui.QTabWidget()
# self.notebook.setMinimumWidth(250)
### Projet ###
project_tab = QtGui.QWidget()
project_tab.setMinimumWidth(250) # Hack
self.project_tab_layout = QtGui.QVBoxLayout(project_tab)
self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
self.notebook.addTab(project_tab, "Project")
@@ -247,7 +248,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
self.setWindowIcon(self.app_icon)
self.setGeometry(100, 100, 1024, 650)
self.setWindowTitle('FlatCAM %s' % version)
self.setWindowTitle('FlatCAM %s - Development Version' % version)
self.show()
def closeEvent(self, event):
@@ -421,41 +422,6 @@ class GerberOptionsGroupUI(OptionsGroupUI):
)
grid1.addWidget(self.combine_passes_cb, 3, 0)
## Clear non-copper regions
self.clearcopper_label = QtGui.QLabel("<b>Clear non-copper:</b>")
self.clearcopper_label.setToolTip(
"Create a Geometry object with\n"
"toolpaths to cut all non-copper regions."
)
self.layout.addWidget(self.clearcopper_label)
grid5 = QtGui.QGridLayout()
self.layout.addLayout(grid5)
ncctdlabel = QtGui.QLabel('Tools dia:')
ncctdlabel.setToolTip(
"Diameters of the cutting tools, separated by ','"
)
grid5.addWidget(ncctdlabel, 0, 0)
self.ncc_tool_dia_entry = FCEntry()
grid5.addWidget(self.ncc_tool_dia_entry, 0, 1)
nccoverlabel = QtGui.QLabel('Overlap:')
nccoverlabel.setToolTip(
"How much (fraction of tool width)\n"
"to overlap each pass."
)
grid5.addWidget(nccoverlabel, 1, 0)
self.ncc_overlap_entry = FloatEntry()
grid5.addWidget(self.ncc_overlap_entry, 1, 1)
nccmarginlabel = QtGui.QLabel('Margin:')
nccmarginlabel.setToolTip(
"Bounding box margin."
)
grid5.addWidget(nccmarginlabel, 2, 0)
self.ncc_margin_entry = FloatEntry()
grid5.addWidget(self.ncc_margin_entry, 2, 1)
## Board cuttout
self.board_cutout_label = QtGui.QLabel("<b>Board cutout:</b>")
self.board_cutout_label.setToolTip(
@@ -624,14 +590,39 @@ class ExcellonOptionsGroupUI(OptionsGroupUI):
self.feedrate_entry = LengthEntry()
grid1.addWidget(self.feedrate_entry, 2, 1)
toolchangezlabel = QtGui.QLabel('Toolchange Z:')
toolchangezlabel.setToolTip(
"Tool Z where user can change drill bit\n"
)
grid1.addWidget(toolchangezlabel, 3, 0)
self.toolchangez_entry = LengthEntry()
grid1.addWidget(self.toolchangez_entry, 3, 1)
spdlabel = QtGui.QLabel('Spindle speed:')
spdlabel.setToolTip(
"Speed of the spindle\n"
"in RPM (optional)"
)
grid1.addWidget(spdlabel, 3, 0)
grid1.addWidget(spdlabel, 4, 0)
self.spindlespeed_entry = IntEntry(allow_empty=True)
grid1.addWidget(self.spindlespeed_entry, 3, 1)
grid1.addWidget(self.spindlespeed_entry, 4, 1)
#### Milling Holes ####
self.mill_hole_label = QtGui.QLabel('<b>Mill Holes</b>')
self.mill_hole_label.setToolTip(
"Create Geometry for milling holes."
)
self.layout.addWidget(self.mill_hole_label)
grid1 = QtGui.QGridLayout()
self.layout.addLayout(grid1)
tdlabel = QtGui.QLabel('Tool dia:')
tdlabel.setToolTip(
"Diameter of the cutting tool."
)
grid1.addWidget(tdlabel, 0, 0)
self.tooldia_entry = LengthEntry()
grid1.addWidget(self.tooldia_entry, 0, 1)
class GeometryOptionsGroupUI(OptionsGroupUI):
@@ -815,6 +806,26 @@ class CNCJobOptionsGroupUI(OptionsGroupUI):
self.append_text = FCTextArea()
self.layout.addWidget(self.append_text)
# Dwell
grid1 = QtGui.QGridLayout()
self.layout.addLayout(grid1)
dwelllabel = QtGui.QLabel('Dwell:')
dwelllabel.setToolTip(
"Pause to allow the spindle to reach its\n"
"speed before cutting."
)
dwelltime = QtGui.QLabel('Duration [sec.]:')
dwelltime.setToolTip(
"Number of second to dwell."
)
self.dwell_cb = FCCheckBox()
self.dwelltime_cb = FCEntry()
grid1.addWidget(dwelllabel, 0, 0)
grid1.addWidget(self.dwell_cb, 0, 1)
grid1.addWidget(dwelltime, 1, 0)
grid1.addWidget(self.dwelltime_cb, 1, 1)
class GlobalOptionsUI(QtGui.QWidget):
"""
@@ -870,4 +881,4 @@ class GlobalOptionsUI(QtGui.QWidget):
#
#
# if __name__ == '__main__':
# main()
# main()

View File

@@ -1,3 +1,4 @@
from cStringIO import StringIO
from PyQt4 import QtCore
from copy import copy
from ObjectUI import *
@@ -6,9 +7,7 @@ import inspect # TODO: For debugging only.
from camlib import *
from FlatCAMCommon import LoudDict
from FlatCAMDraw import FlatCAMDraw
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
TOLERANCE = 0.01
########################################
## FlatCAMObj ##
@@ -43,8 +42,6 @@ class FlatCAMObj(QtCore.QObject):
self.axes = None # Matplotlib axes
self.kind = None # Override with proper name
self.item = None # Link with project view item
self.muted_ui = False
# assert isinstance(self.ui, ObjectUI)
@@ -52,6 +49,23 @@ class FlatCAMObj(QtCore.QObject):
# self.ui.offset_button.clicked.connect(self.on_offset_button_click)
# self.ui.scale_button.clicked.connect(self.on_scale_button_click)
def from_dict(self, d):
"""
This supersedes ``from_dict`` in derived classes. Derived classes
must inherit from FlatCAMObj first, then from derivatives of Geometry.
``self.options`` is only updated, not overwritten. This ensures that
options set by the app do not vanish when reading the objects
from a project file.
"""
for attr in self.ser_attrs:
if attr == 'options':
self.options.update(d[attr])
else:
setattr(self, attr, d[attr])
def on_options_change(self, key):
self.emit(QtCore.SIGNAL("optionChanged"), key)
@@ -61,19 +75,18 @@ class FlatCAMObj(QtCore.QObject):
self.form_fields = {"name": self.ui.name_entry}
assert isinstance(self.ui, ObjectUI)
self.ui.name_entry.editingFinished.connect(self.on_name_editing_finished)
self.ui.name_entry.returnPressed.connect(self.on_name_activate)
self.ui.offset_button.clicked.connect(self.on_offset_button_click)
self.ui.scale_button.clicked.connect(self.on_scale_button_click)
def __str__(self):
return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
def on_name_editing_finished(self):
def on_name_activate(self):
old_name = copy(self.options["name"])
new_name = self.ui.name_entry.get_value()
if new_name != old_name:
self.options["name"] = new_name
self.app.info("Name changed from %s to %s" % (old_name, new_name))
self.options["name"] = self.ui.name_entry.get_value()
self.app.info("Name changed from %s to %s" % (old_name, new_name))
def on_offset_button_click(self):
self.app.report_usage("obj_on_offset_button")
@@ -90,14 +103,50 @@ class FlatCAMObj(QtCore.QObject):
self.scale(factor)
self.plot()
def setup_axes(self, figure):
"""
1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
them to figure if not part of the figure. 4) Sets transparent
background. 5) Sets 1:1 scale aspect ratio.
:param figure: A Matplotlib.Figure on which to add/configure axes.
:type figure: matplotlib.figure.Figure
:return: None
:rtype: None
"""
if self.axes is None:
FlatCAMApp.App.log.debug("setup_axes(): New axes")
self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
label=self.options["name"])
elif self.axes not in figure.axes:
FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
self.axes.cla()
figure.add_axes(self.axes)
else:
FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes")
self.axes.cla()
# Remove all decoration. The app's axes will have
# the ticks and grid.
self.axes.set_frame_on(False) # No frame
self.axes.set_xticks([]) # No tick
self.axes.set_yticks([]) # No ticks
self.axes.patch.set_visible(False) # No background
self.axes.set_aspect(1)
def to_form(self):
"""
Copies options to the UI form.
:return: None
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.to_form()")
for option in self.options:
self.set_form_item(option)
try:
self.set_form_item(option)
except:
self.app.log.warning("Unexpected error:", sys.exc_info())
def read_form(self):
"""
@@ -108,7 +157,10 @@ class FlatCAMObj(QtCore.QObject):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
for option in self.options:
self.read_form_item(option)
try:
self.read_form_item(option)
except:
self.app.log.warning("Unexpected error:", sys.exc_info())
def build_ui(self):
"""
@@ -137,8 +189,8 @@ class FlatCAMObj(QtCore.QObject):
self.app.ui.selected_scroll_area.takeWidget()
except:
self.app.log.debug("Nothing to remove")
self.app.ui.selected_scroll_area.setWidget(self.ui)
self.to_form()
self.muted_ui = False
@@ -170,6 +222,17 @@ class FlatCAMObj(QtCore.QObject):
except KeyError:
self.app.log.warning("Failed to read option from field: %s" % option)
# #try read field only when option have equivalent in form_fields
# if option in self.form_fields:
# option_type=type(self.options[option])
# try:
# value=self.form_fields[option].get_value()
# #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either.
# except (KeyError,SyntaxError):
# self.app.log.warning("Failed to read option from field: %s" % option)
# else:
# self.app.log.warning("Form fied does not exists: %s" % option)
def plot(self):
"""
Plot this object (Extend this method to implement the actual plotting).
@@ -192,7 +255,6 @@ class FlatCAMObj(QtCore.QObject):
# Clear axes or we will plot on top of them.
self.axes.cla() # TODO: Thread safe?
return True
def serialize(self):
@@ -238,9 +300,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": 0.016,
"isopasses": 1,
"isooverlap": 0.15,
"ncctools": "1.0, 0.5",
"nccoverlap": 0.4,
"nccmargin": 1,
"combine_passes": True,
"cutouttooldia": 0.07,
"cutoutmargin": 0.2,
@@ -286,9 +345,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": self.ui.iso_tool_dia_entry,
"isopasses": self.ui.iso_width_entry,
"isooverlap": self.ui.iso_overlap_entry,
"ncctools": self.ui.ncc_tool_dia_entry,
"nccoverlap": self.ui.ncc_overlap_entry,
"nccmargin": self.ui.ncc_margin_entry,
"combine_passes": self.ui.combine_passes_cb,
"cutouttooldia": self.ui.cutout_tooldia_entry,
"cutoutmargin": self.ui.cutout_margin_entry,
@@ -300,15 +356,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"bboxrounded": self.ui.bbrounded_cb
})
# Fill form fields only on object create
self.to_form()
assert isinstance(self.ui, GerberObjectUI)
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
self.ui.generate_ncc_button.clicked.connect(self.on_ncc_button_click)
self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
@@ -351,7 +403,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
name = self.options["name"] + "_cutout"
def geo_init(geo_obj, app_obj):
margin = self.options["cutoutmargin"] + self.options["cutouttooldia"] / 2
margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
minx, miny, maxx, maxy = self.bounds()
minx -= margin
@@ -392,89 +444,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.read_form()
self.isolate()
def on_ncc_button_click(self, *args):
self.app.report_usage("gerber_on_ncc_button")
# Prepare parameters
try:
tools = [float(eval(dia)) for dia in self.ui.ncc_tool_dia_entry.get_value().split(",")]
except:
FlatCAMApp.App.log.error("At least one tool diameter needed")
return
over = self.ui.ncc_overlap_entry.get_value()
margin = self.ui.ncc_margin_entry.get_value()
if over is None or margin is None:
FlatCAMApp.App.log.error("Overlap and margin values needed")
return
print "non-copper clear button clicked", tools, over, margin
# Sort tools in descending order
tools.sort(reverse=True)
# Prepare non-copper polygons
bounding_box = self.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
empty = self.get_empty_area(bounding_box)
if type(empty) is Polygon:
empty = MultiPolygon([empty])
# Main procedure
def clear_non_copper():
# Already cleared area
cleared = MultiPolygon()
# Geometry object creating callback
def geo_init(geo_obj, app_obj):
geo_obj.options["cnctooldia"] = tool
geo_obj.solid_geometry = []
for p in area.geoms:
try:
cp = self.clear_polygon(p, tool, over)
geo_obj.solid_geometry.append(list(cp.get_objects()))
except:
FlatCAMApp.App.log.warning("Polygon is ommited")
# Generate area for each tool
offset = sum(tools)
for tool in tools:
# Get remaining tools offset
offset -= tool
# Area to clear
area = empty.buffer(-offset).difference(cleared)
# Transform area to MultiPolygon
if type(area) is Polygon:
area = MultiPolygon([area])
# Check if area not empty
if len(area.geoms) > 0:
# Overall cleared area
cleared = empty.buffer(-offset * (1 + over)).buffer(-tool / 2).buffer(tool / 2)
# Create geometry object
name = self.options["name"] + "_ncc_" + repr(tool) + "D"
self.app.new_object("geometry", name, geo_init)
else:
return
# Do job in background
proc = self.app.proc_container.new("Clearing non-copper areas.")
def job_thread(app_obj):
try:
clear_non_copper()
except Exception as e:
proc.done()
raise e
proc.done()
self.app.inform.emit("Clear non-copper areas started ...")
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
def follow(self, outname=None):
"""
Creates a geometry object "following" the gerber paths.
@@ -548,7 +517,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
geo_obj.solid_geometry = []
for i in range(passes):
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
geom = generate_envelope(offset, i == 0)
geom = generate_envelope (offset, i == 0)
geo_obj.solid_geometry.append(geom)
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
@@ -558,7 +527,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else:
for i in range(passes):
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
if passes > 1:
iso_name = base_name + str(i + 1)
else:
@@ -568,7 +537,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = self.options["isotooldia"]
geo_obj.solid_geometry = generate_envelope(offset, i == 0)
geo_obj.solid_geometry = generate_envelope (offset, i == 0)
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
# TODO: Do something if this is None. Offer changing name?
@@ -637,7 +606,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
for poly in geometry:
# TODO: Too many things hardcoded.
try:
poly = poly.simplify(TOLERANCE)
patch = PolygonPatch(poly,
facecolor="#BBF268",
edgecolor="#006E20",
@@ -649,7 +617,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
FlatCAMApp.App.log.warning(str(poly))
else:
for poly in geometry:
poly = poly.simplify(TOLERANCE)
x, y = poly.exterior.xy
self.axes.plot(x, y, linespec)
for ints in poly.interiors:
@@ -699,6 +666,76 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# from predecessors.
self.ser_attrs += ['options', 'kind']
@staticmethod
def merge(exc_list, exc_final):
"""
Merge excellons in exc_list into exc_final.
Options are allways copied from source .
Tools are also merged, if name for tool is same and size differs, then as name is used next available number from both lists
if only one object is specified in exc_list then this acts as copy only
:param exc_list: List or one object of FlatCAMExcellon Objects to join.
:param exc_final: Destination FlatCAMExcellon object.
:return: None
"""
if type(exc_list) is not list:
exc_list_real= list()
exc_list_real.append(exc_list)
else:
exc_list_real=exc_list
for exc in exc_list_real:
# Expand lists
if type(exc) is list:
FlatCAMExcellon.merge(exc, exc_final)
# If not list, merge excellons
else:
# TODO: I realize forms does not save values into options , when object is deselected
# leave this here for future use
# this reinitialize options based on forms, all steps may not be necessary
# exc.app.collection.set_active(exc.options['name'])
# exc.to_form()
# exc.read_form()
for option in exc.options:
if option is not 'name':
try:
exc_final.options[option] = exc.options[option]
except:
exc.app.log.warning("Failed to copy option.",option)
#deep copy of all drills,to avoid any references
for drill in exc.drills:
point = Point(drill['point'].x,drill['point'].y)
exc_final.drills.append({"point": point, "tool": drill['tool']})
toolsrework=dict()
max_numeric_tool=0
for toolname in exc.tools.iterkeys():
numeric_tool=int(toolname)
if numeric_tool>max_numeric_tool:
max_numeric_tool=numeric_tool
toolsrework[exc.tools[toolname]['C']]=toolname
#exc_final as last because names from final tools will be used
for toolname in exc_final.tools.iterkeys():
numeric_tool=int(toolname)
if numeric_tool>max_numeric_tool:
max_numeric_tool=numeric_tool
toolsrework[exc_final.tools[toolname]['C']]=toolname
for toolvalues in toolsrework.iterkeys():
if toolsrework[toolvalues] in exc_final.tools:
if exc_final.tools[toolsrework[toolvalues]]!={"C": toolvalues}:
exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues}
else:
exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues}
#this value was not co
exc_final.zeros=exc.zeros
exc_final.create_geometry()
def build_ui(self):
FlatCAMObj.build_ui(self)
@@ -717,6 +754,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
dia.setFlags(QtCore.Qt.ItemIsEnabled)
self.ui.tools_table.setItem(i, 1, dia) # Diameter
i += 1
# sort the tool diameter column
self.ui.tools_table.sortItems(1)
# all the tools are selected by default
self.ui.tools_table.selectColumn(0)
self.ui.tools_table.resizeColumnsToContents()
self.ui.tools_table.resizeRowsToContents()
self.ui.tools_table.horizontalHeader().setStretchLastSection(True)
@@ -748,9 +791,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
"spindlespeed": self.ui.spindlespeed_entry
})
# Fill form fields
self.to_form()
assert isinstance(self.ui, ExcellonObjectUI), \
"Expected a ExcellonObjectUI, got %s" % type(self.ui)
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
@@ -959,7 +999,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"plot": True,
"tooldia": 0.4 / 25.4, # 0.4mm in inches
"append": "",
"prepend": ""
"prepend": "",
"dwell": False,
"dwelltime": 1
})
# Attributes to be included in serialization
@@ -979,12 +1021,11 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"plot": self.ui.plot_cb,
"tooldia": self.ui.tooldia_entry,
"append": self.ui.append_text,
"prepend": self.ui.prepend_text
"prepend": self.ui.prepend_text,
"dwell": self.ui.dwell_cb,
"dwelltime": self.ui.dwelltime_entry
})
# Fill form fields only on object create
self.to_form()
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
@@ -1000,6 +1041,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
def on_exportgcode_button_click(self, *args):
self.app.report_usage("cncjob_on_exportgcode_button")
self.read_form()
try:
filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...",
directory=self.app.defaults["last_folder"])
@@ -1011,16 +1054,69 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
self.export_gcode(filename, preamble=preamble, postamble=postamble)
def dwell_generator(self, lines):
"""
Inserts "G4 P..." instructions after spindle-start
instructions (M03 or M04).
"""
log.debug("dwell_generator()...")
m3m4re = re.compile(r'^\s*[mM]0[34]')
g4re = re.compile(r'^\s*[gG]4\s+([\d\.\+\-e]+)')
bufline = None
for line in lines:
# If the buffer contains a G4, yield that.
# If current line is a G4, discard it.
if bufline is not None:
yield bufline
bufline = None
if not g4re.search(line):
yield line
continue
# If start spindle, buffer a G4.
if m3m4re.search(line):
log.debug("Found M03/4")
bufline = "G4 P{}\n".format(self.options['dwelltime'])
yield line
raise StopIteration
def export_gcode(self, filename, preamble='', postamble=''):
f = open(filename, 'w')
f.write(preamble + '\n' + self.gcode + "\n" + postamble)
f.close()
lines = StringIO(self.gcode)
## Post processing
# Dwell?
if self.options['dwell']:
log.debug("Will add G04!")
lines = self.dwell_generator(lines)
## Write
with open(filename, 'w') as f:
f.write(preamble + "\n")
for line in lines:
f.write(line)
f.write(postamble)
# Just for adding it to the recent files list.
self.app.file_opened.emit("cncjob", filename)
self.app.inform.emit("Saved to: " + filename)
def get_gcode(self, preamble='', postamble=''):
#we need this to beable get_gcode separatelly for shell command export_code
return preamble + '\n' + self.gcode + "\n" + postamble
def on_plot_cb_click(self, *args):
if self.muted_ui:
return
@@ -1034,7 +1130,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
if not FlatCAMObj.plot(self):
return
self.plot2(self.axes, tooldia=self.options["tooldia"], tool_tolerance=TOLERANCE)
self.plot2(self.axes, tooldia=self.options["tooldia"])
self.app.plotcanvas.auto_adjust_axes()
@@ -1078,12 +1174,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
else:
geo_final.solid_geometry.append(geo.solid_geometry)
# try: # Iterable
# for shape in geo.solid_geometry:
# geo_final.solid_geometry.append(shape)
#
# except TypeError: # Non-iterable
# geo_final.solid_geometry.append(geo.solid_geometry)
# try: # Iterable
# for shape in geo.solid_geometry:
# geo_final.solid_geometry.append(shape)
#
# except TypeError: # Non-iterable
# geo_final.solid_geometry.append(geo.solid_geometry)
def __init__(self, name):
FlatCAMObj.__init__(self, name)
@@ -1137,15 +1233,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"depthperpass": self.ui.maxdepth_entry
})
# Fill form fields only on object create
self.to_form()
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
def on_paint_button_click(self, *args):
self.app.report_usage("geometry_on_paint_button")
self.app.info("Click inside the desired polygon.")
@@ -1168,7 +1260,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
def paint_poly(self, inside_pt, tooldia, overlap):
# Which polygon.
# poly = find_polygon(self.solid_geometry, inside_pt)
#poly = find_polygon(self.solid_geometry, inside_pt)
poly = self.find_polygon(inside_pt)
# No polygon?
@@ -1185,7 +1277,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
def gen_paintarea(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry), \
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
# assert isinstance(app_obj, App)
#assert isinstance(app_obj, App)
if self.options["paintmethod"] == "seed":
cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]),
@@ -1228,7 +1320,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
outname=None,
spindlespeed=None,
multidepth=None,
depthperpass=None):
depthperpass=None,
use_thread=True):
"""
Creates a CNCJob out of this Geometry object. The actual
work is done by the target FlatCAMCNCjob object's
@@ -1289,18 +1382,21 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
app_obj.progress.emit(80)
# To be run in separate thread
def job_thread(app_obj):
with self.app.proc_container.new("Generating CNC Job."):
app_obj.new_object("cncjob", outname, job_init)
app_obj.inform.emit("CNCjob created: %s" % outname)
app_obj.progress.emit(100)
if use_thread:
# To be run in separate thread
def job_thread(app_obj):
with self.app.proc_container.new("Generating CNC Job."):
app_obj.new_object("cncjob", outname, job_init)
app_obj.inform.emit("CNCjob created: %s" % outname)
app_obj.progress.emit(100)
# Create a promise with the name
self.app.collection.promise(outname)
# Create a promise with the name
self.app.collection.promise(outname)
# Send to worker
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
# Send to worker
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
else:
self.app.new_object("cncjob", outname, job_init)
def on_plot_cb_click(self, *args): # TODO: args not needed
if self.muted_ui:
@@ -1337,11 +1433,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
dx, dy = vect
if type(self.solid_geometry) == list:
self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
for g in self.solid_geometry]
else:
self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
def translate_recursion(geom):
if type(geom) == list:
geoms=list()
for local_geom in geom:
geoms.append(translate_recursion(local_geom))
return geoms
else:
return affinity.translate(geom, xoff=dx, yoff=dy)
self.solid_geometry=translate_recursion(self.solid_geometry)
def convert_units(self, units):
factor = Geometry.convert_units(self, units)
@@ -1363,7 +1464,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
except TypeError: # Element is not iterable...
if type(element) == Polygon:
x, y = element.simplify(TOLERANCE).exterior.coords.xy
x, y = element.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in element.interiors:
x, y = ints.coords.xy
@@ -1371,7 +1472,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
return
if type(element) == LineString or type(element) == LinearRing:
x, y = element.simplify(TOLERANCE).coords.xy
x, y = element.coords.xy
self.axes.plot(x, y, 'r-')
return

View File

@@ -1,5 +1,4 @@
from PyQt4 import QtCore
#import FlatCAMApp
class Worker(QtCore.QObject):
@@ -8,31 +7,52 @@ class Worker(QtCore.QObject):
in a single independent thread.
"""
# avoid multiple tests for debug availability
pydevd_failed = False
def __init__(self, app, name=None):
super(Worker, self).__init__()
self.app = app
self.name = name
def allow_debug(self):
"""
allow debuging/breakpoints in this threads
should work from PyCharm and PyDev
:return:
"""
if not self.pydevd_failed:
try:
import pydevd
pydevd.settrace(suspend=False, trace_only_current_thread=True)
except ImportError:
self.pydevd_failed=True
def run(self):
# FlatCAMApp.App.log.debug("Worker Started!")
self.app.log.debug("Worker Started!")
self.allow_debug()
# Tasks are queued in the event listener.
self.app.worker_task.connect(self.do_worker_task)
def do_worker_task(self, task):
# FlatCAMApp.App.log.debug("Running task: %s" % str(task))
self.app.log.debug("Running task: %s" % str(task))
# 'worker_name' property of task allows to target
# specific worker.
if 'worker_name' in task and task['worker_name'] == self.name:
task['fcn'](*task['params'])
self.allow_debug()
if ('worker_name' in task and task['worker_name'] == self.name) or \
('worker_name' not in task and self.name is None):
try:
task['fcn'](*task['params'])
except Exception as e:
self.app.thread_exception.emit(e)
raise e
return
if 'worker_name' not in task and self.name is None:
task['fcn'](*task['params'])
return
# FlatCAMApp.App.log.debug("Task ignored.")
self.app.log.debug("Task ignored.")
self.app.log.debug("Task ignored.")

View File

@@ -221,6 +221,9 @@ class FCCheckBox(QtGui.QCheckBox):
def set_value(self, val):
self.setChecked(val)
def toggle(self):
self.set_value(not self.get_value())
class FCTextArea(QtGui.QPlainTextEdit):
def __init__(self, parent=None):

View File

@@ -5,18 +5,11 @@ import FlatCAMApp
from PyQt4 import Qt, QtGui, QtCore
class KeySensitiveListView(QtGui.QTreeView):
class KeySensitiveListView(QtGui.QListView):
"""
QtGui.QListView extended to emit a signal on key press.
"""
def __init__(self, parent = None):
super(KeySensitiveListView, self).__init__(parent)
self.setHeaderHidden(True)
self.setEditTriggers(QtGui.QTreeView.SelectedClicked)
# self.setRootIsDecorated(False)
# self.setExpandsOnDoubleClick(False)
keyPressed = QtCore.pyqtSignal(int)
def keyPressEvent(self, event):
@@ -24,70 +17,11 @@ class KeySensitiveListView(QtGui.QTreeView):
self.keyPressed.emit(event.key())
class TreeItem:
"""
Item of a tree model
"""
def __init__(self, data, icon = None, obj = None, parent_item = None):
self.parent_item = parent_item
self.item_data = data # Columns string data
self.icon = icon # Decoration
self.obj = obj # FlatCAMObj
self.child_items = []
if parent_item:
parent_item.append_child(self)
def append_child(self, item):
self.child_items.append(item)
item.set_parent_item(self)
def remove_child(self, item):
child = self.child_items.pop(self.child_items.index(item))
del child
def remove_children(self):
for child in self.child_items:
del child
self.child_items = []
def child(self, row):
return self.child_items[row]
def child_count(self):
return len(self.child_items)
def column_count(self):
return len(self.item_data)
def data(self, column):
return self.item_data[column]
def row(self):
return self.parent_item.child_items.index(self)
def set_parent_item(self, parent_item):
self.parent_item = parent_item
def __del__(self):
del self.obj
del self.icon
class ObjectCollection(QtCore.QAbstractItemModel):
class ObjectCollection(QtCore.QAbstractListModel):
"""
Object storage and management.
"""
groups = [
("gerber", "Gerber"),
("excellon", "Excellon"),
("geometry", "Geometry"),
("cncjob", "CNC Job")
]
classdict = {
"gerber": FlatCAMGerber,
"excellon": FlatCAMExcellon,
@@ -102,34 +36,15 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"geometry": "share/geometry16.png"
}
root_item = None
app = None
def __init__(self, parent=None):
QtCore.QAbstractItemModel.__init__(self, parent=parent)
QtCore.QAbstractListModel.__init__(self, parent=parent)
### Icons for the list view
self.icons = {}
for kind in ObjectCollection.icon_files:
self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
# Create root tree view item
self.root_item = TreeItem(["root"])
# Create group items
self.group_items = {}
for kind, title in ObjectCollection.groups:
item = TreeItem([title], self.icons[kind])
self.group_items[kind] = item
self.root_item.append_child(item)
# Create test sub-items
# for i in self.root_item.m_child_items:
# print i.data(0)
# i.append_child(TreeItem(["empty"]))
### Data ###
self.object_list = []
self.checked_indexes = []
# Names of objects that are expected to become available.
@@ -140,6 +55,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
self.promises = set()
### View
#self.view = QtGui.QListView()
self.view = KeySensitiveListView()
self.view.setSelectionMode(Qt.QAbstractItemView.ExtendedSelection)
self.view.setModel(self)
@@ -162,114 +78,41 @@ class ObjectCollection(QtCore.QAbstractItemModel):
def on_key(self, key):
# Delete
active = self.get_active()
if key == QtCore.Qt.Key_Delete and active:
if key == QtCore.Qt.Key_Delete:
# Delete via the application to
# ensure cleanup of the GUI
active.app.on_delete()
self.get_active().app.on_delete()
return
if key == QtCore.Qt.Key_Space:
self.get_active().ui.plot_cb.toggle()
return
def on_mouse_down(self, event):
FlatCAMApp.App.log.debug("Mouse button pressed on list")
def index(self, row, column = 0, parent = None, *args, **kwargs):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
def rowCount(self, parent=QtCore.QModelIndex(), *args, **kwargs):
return len(self.object_list)
if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
def columnCount(self, *args, **kwargs):
return 1
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def parent(self, index = None):
if not index.isValid():
return QtCore.QModelIndex()
parent_item = index.internalPointer().parent_item
if parent_item == self.root_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, index = None, *args, **kwargs):
if index.column() > 0:
return 0
if not index.isValid():
parent_item = self.root_item
else:
parent_item = index.internalPointer()
return parent_item.child_count()
def columnCount(self, index = None, *args, **kwargs):
if index.isValid():
return index.internalPointer().column_count()
else:
return self.root_item.column_count()
def data(self, index, role = None):
if not index.isValid():
def data(self, index, role=Qt.Qt.DisplayRole):
if not index.isValid() or not 0 <= index.row() < self.rowCount():
return QtCore.QVariant()
if role in [Qt.Qt.DisplayRole, Qt.Qt.EditRole]:
obj = index.internalPointer().obj
if obj:
return obj.options["name"]
else:
return index.internalPointer().data(index.column())
elif role == Qt.Qt.DecorationRole:
icon = index.internalPointer().icon
if icon:
return icon
else:
return Qt.QPixmap()
else:
return QtCore.QVariant()
def setData(self, index, data, role = None):
if index.isValid():
obj = index.internalPointer().obj
if obj:
obj.options["name"] = data.toString()
obj.build_ui()
def flags(self, index):
if not index.isValid():
return 0
# Prevent groups from selection
if not index.internalPointer().obj:
return Qt.Qt.ItemIsEnabled
else:
return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable | Qt.Qt.ItemIsEditable
return QtCore.QAbstractItemModel.flags(self, index)
# def data(self, index, role=Qt.Qt.DisplayRole):
# if not index.isValid() or not 0 <= index.row() < self.rowCount():
# return QtCore.QVariant()
# row = index.row()
# if role == Qt.Qt.DisplayRole:
# return self.object_list[row].options["name"]
# if role == Qt.Qt.DecorationRole:
# return self.icons[self.object_list[row].kind]
# # if role == Qt.Qt.CheckStateRole:
# # if row in self.checked_indexes:
# # return Qt.Qt.Checked
# # else:
# # return Qt.Qt.Unchecked
row = index.row()
if role == Qt.Qt.DisplayRole:
return self.object_list[row].options["name"]
if role == Qt.Qt.DecorationRole:
return self.icons[self.object_list[row].kind]
# if role == Qt.Qt.CheckStateRole:
# if row in self.checked_indexes:
# return Qt.Qt.Checked
# else:
# return Qt.Qt.Unchecked
def print_list(self):
for obj in self.get_list():
for obj in self.object_list:
print obj
def append(self, obj, active=False):
@@ -300,20 +143,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
obj.set_ui(obj.ui_type())
# Required before appending (Qt MVC)
group = self.group_items[obj.kind]
group_index = self.index(group.row(), 0, Qt.QModelIndex())
self.beginInsertRows(group_index, group.child_count(), group.child_count())
self.beginInsertRows(QtCore.QModelIndex(), len(self.object_list), len(self.object_list))
# Append new item
obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
# Simply append to the python list
self.object_list.append(obj)
# Required after appending (Qt MVC)
self.endInsertRows()
# Expand group
if group.child_count() is 1:
self.view.setExpanded(group_index, True)
def get_names(self):
"""
Gets a list of the names of all objects in the collection.
@@ -323,7 +160,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
return [x.options['name'] for x in self.get_list()]
return [x.options['name'] for x in self.object_list]
def get_bounds(self):
"""
@@ -341,8 +178,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
xmax = -Inf
ymax = -Inf
# for obj in self.object_list:
for obj in self.get_list():
for obj in self.object_list:
try:
gxmin, gymin, gxmax, gymax = obj.bounds()
xmin = min([xmin, gxmin])
@@ -365,7 +201,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
for obj in self.get_list():
for obj in self.object_list:
if obj.options['name'] == name:
return obj
return None
@@ -374,12 +210,12 @@ class ObjectCollection(QtCore.QAbstractItemModel):
selections = self.view.selectedIndexes()
if len(selections) == 0:
return
row = selections[0].row()
active = selections[0].internalPointer()
group = active.parent_item
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.object_list.pop(row)
self.beginRemoveRows(self.index(group.row(), 0, Qt.QModelIndex()), active.row(), active.row())
group.remove_child(active)
self.endRemoveRows()
def get_active(self):
@@ -391,8 +227,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
selections = self.view.selectedIndexes()
if len(selections) == 0:
return None
return selections[0].internalPointer().obj
row = selections[0].row()
return self.object_list[row]
def get_selected(self):
"""
@@ -400,7 +236,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:return: List of objects
"""
return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
return [self.object_list[sel.row()] for sel in self.view.selectedIndexes()]
def set_active(self, name):
"""
@@ -410,34 +246,40 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:param name: Name of the FlatCAM Object
:return: None
"""
obj = self.get_by_name(name)
item = obj.item
group = self.group_items[obj.kind]
iobj = self.createIndex(self.get_names().index(name), 0) # Column 0
self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select)
group_index = self.index(group.row(), 0, Qt.QModelIndex())
item_index = self.index(item.row(), 0, group_index)
def set_inactive(self, name):
"""
Unselect object by name from the project list. This triggers the
list_selection_changed event and call on_list_selection_changed.
self.view.selectionModel().select(item_index, QtGui.QItemSelectionModel.Select)
:param name: Name of the FlatCAM Object
:return: None
"""
iobj = self.createIndex(self.get_names().index(name), 0) # Column 0
self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Deselect)
def set_all_inactive(self):
"""
Unselect all objects from the project list. This triggers the
list_selection_changed event and call on_list_selection_changed.
:return: None
"""
for name in self.get_names():
self.set_inactive(name)
def on_list_selection_change(self, current, previous):
FlatCAMApp.App.log.debug("on_list_selection_change()")
FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
try:
obj = current.indexes()[0].internalPointer().obj
selection_index = current.indexes()[0].row()
except IndexError:
FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
try:
self.app.ui.selected_scroll_area.takeWidget()
except:
FlatCAMApp.App.log.debug("Nothing to remove")
self.app.setup_component_editor()
return
if obj:
obj.build_ui()
self.object_list[selection_index].build_ui()
def on_item_activated(self, index):
"""
@@ -446,23 +288,18 @@ class ObjectCollection(QtCore.QAbstractItemModel):
:param index: Index of the item in the list.
:return: None
"""
index.internalPointer().obj.build_ui()
self.object_list[index.row()].build_ui()
def delete_all(self):
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
self.beginResetModel()
self.checked_indexes = []
for group in self.root_item.child_items:
group.remove_children()
self.object_list = []
self.checked_indexes = []
self.endResetModel()
def get_list(self):
obj_list = []
for group in self.root_item.child_items:
for item in group.child_items:
obj_list.append(item.obj)
return self.object_list
return obj_list

View File

@@ -163,7 +163,9 @@ class CNCObjectUI(ObjectUI):
)
self.custom_box.addWidget(self.updateplot_button)
##################
## Export G-Code
##################
self.export_gcode_label = QtGui.QLabel("<b>Export G-Code:</b>")
self.export_gcode_label.setToolTip(
"Export and save G-Code to\n"
@@ -194,6 +196,26 @@ class CNCObjectUI(ObjectUI):
self.append_text = FCTextArea()
self.custom_box.addWidget(self.append_text)
# Dwell
grid1 = QtGui.QGridLayout()
self.custom_box.addLayout(grid1)
dwelllabel = QtGui.QLabel('Dwell:')
dwelllabel.setToolTip(
"Pause to allow the spindle to reach its\n"
"speed before cutting."
)
dwelltime = QtGui.QLabel('Duration [sec.]:')
dwelltime.setToolTip(
"Number of second to dwell."
)
self.dwell_cb = FCCheckBox()
self.dwelltime_entry = FCEntry()
grid1.addWidget(dwelllabel, 0, 0)
grid1.addWidget(self.dwell_cb, 0, 1)
grid1.addWidget(dwelltime, 1, 0)
grid1.addWidget(self.dwelltime_entry, 1, 1)
# GO Button
self.export_gcode_button = QtGui.QPushButton('Export G-Code')
self.export_gcode_button.setToolTip(

View File

@@ -12,26 +12,103 @@ from PyQt4 import QtGui, QtCore
from matplotlib import use as mpl_use
mpl_use("Qt4Agg")
import FlatCAMApp
import numpy as np
import copy
from math import ceil, floor
import math
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureOffscreenCanvas
from matplotlib.backends.backend_agg import FigureCanvasAgg
import FlatCAMApp
import logging
log = logging.getLogger('base')
class CanvasCache(QtCore.QObject):
"""
Case story #1:
1) No objects in the project.
2) Object is created (new_object() emits object_created(obj)).
on_object_created() adds (i) object to collection and emits
(ii) new_object_available() then calls (iii) object.plot()
3) object.plot() creates axes if necessary on
app.collection.figure. Then plots on it.
4) Plots on a cache-size canvas (in background).
5) Plot completes. Bitmap is generated.
6) Visible canvas is painted.
"""
# Signals:
# A bitmap is ready to be displayed.
new_screen = QtCore.pyqtSignal()
def __init__(self, plotcanvas, app, dpi=50):
super(CanvasCache, self).__init__()
self.app = app
self.plotcanvas = plotcanvas
self.dpi = dpi
self.figure = Figure(dpi=dpi)
self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
self.axes.set_frame_on(False)
self.axes.set_xticks([])
self.axes.set_yticks([])
self.canvas = FigureCanvasAgg(self.figure)
self.cache = None
def run(self):
log.debug("CanvasCache Thread Started!")
self.plotcanvas.update_screen_request.connect(self.on_update_req)
self.app.new_object_available.connect(self.on_new_object_available)
def on_update_req(self, extents):
"""
Event handler for an updated display request.
:param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
"""
log.debug("Canvas update requested: %s" % str(extents))
# Note: This information below might be out of date. Establish
# a protocol regarding when to change the canvas in the main
# thread and when to check these values here in the background,
# or pass this data in the signal (safer).
log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
# Move the requested screen portion to the main thread
# and inform about the update:
self.new_screen.emit()
# Continue to update the cache.
def on_new_object_available(self):
log.debug("A new object is available. Should plot it!")
class PlotCanvas(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
app = None
updates_queue = 0
# Signals:
# Request for new bitmap to display. The parameter
# is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
update_screen_request = QtCore.pyqtSignal(list)
image_ready = QtCore.pyqtSignal(object, tuple)
def __init__(self, container):
def __init__(self, container, app):
"""
The constructor configures the Matplotlib figure that
will contain all plots, creates the base axes and connects
@@ -43,6 +120,8 @@ class PlotCanvas(QtCore.QObject):
super(PlotCanvas, self).__init__()
self.app = app
# Options
self.x_margin = 15 # pixels
self.y_margin = 25 # Pixels
@@ -54,24 +133,17 @@ class PlotCanvas(QtCore.QObject):
self.figure = Figure(dpi=50) # TODO: dpi needed?
self.figure.patch.set_visible(False)
# Offscreen figure
self.offscreen_figure = Figure(dpi=50)
self.offscreen_figure.patch.set_visible(False)
# These axes show the ticks and grid. No plotting done here.
# New axes must have a label, otherwise mpl returns an existing one.
self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
self.axes.set_aspect(1)
self.axes.grid(True)
# The canvas is the top level container (Gtk.DrawingArea)
# The canvas is the top level container (FigureCanvasQTAgg)
self.canvas = FigureCanvas(self.figure)
# self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
# self.canvas.setFocus()
# Image
self.image = None
#self.canvas.set_hexpand(1)
#self.canvas.set_vexpand(1)
#self.canvas.set_can_focus(True) # For key press
@@ -84,6 +156,15 @@ class PlotCanvas(QtCore.QObject):
# Update every time the canvas is re-drawn.
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
### Bitmap Cache
self.cache = CanvasCache(self, self.app)
self.cache_thread = QtCore.QThread()
self.cache.moveToThread(self.cache_thread)
super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
# self.connect()
self.cache_thread.start()
self.cache.new_screen.connect(self.on_new_screen)
# Events
self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
@@ -103,11 +184,9 @@ class PlotCanvas(QtCore.QObject):
self.pan_axes = []
self.panning = False
self.reset_nonupdate_bounds()
def on_new_screen(self):
self.image_ready.connect(self.update_canvas)
# self.plots_updated.connect(self.on_plots_updated)
log.debug("Cache updated the screen!")
def on_key_down(self, event):
"""
@@ -149,7 +228,7 @@ class PlotCanvas(QtCore.QObject):
def connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the native GTK interface.
Attach an event handler to the canvas through the native Qt interface.
:param event_name: Name of the event
:type event_name: str
@@ -159,12 +238,6 @@ class PlotCanvas(QtCore.QObject):
"""
self.canvas.connect(event_name, callback)
def reset_nonupdate_bounds(self):
self.bx1 = float('-inf')
self.bx2 = float('inf')
self.by1 = self.bx1
self.by2 = self.bx2
def clear(self):
"""
Clears axes and figure.
@@ -176,7 +249,6 @@ class PlotCanvas(QtCore.QObject):
self.axes.cla()
try:
self.figure.clf()
self.offscreen_figure.clf()
except KeyError:
FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
@@ -185,32 +257,8 @@ class PlotCanvas(QtCore.QObject):
self.axes.set_aspect(1)
self.axes.grid(True)
# Prepare offscreen base axes
ax = self.offscreen_figure.add_axes([0.0, 0.0, 1.0, 1.0], label='base')
ax.set_frame_on(True)
ax.patch.set_color("white")
# Hide frame edge
for spine in ax.spines:
ax.spines[spine].set_visible(False)
ax.set_aspect(1)
# Set update bounds
self.reset_nonupdate_bounds()
# Re-draw
self.canvas.draw()
def auto_adjust_axes(self, *args):
"""
Calls ``adjust_axes()`` using the extents of the base axes.
:rtype : None
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
self.canvas.draw_idle()
def adjust_axes(self, xmin, ymin, xmax, ymax):
"""
@@ -268,135 +316,21 @@ class PlotCanvas(QtCore.QObject):
# Sync re-draw to proper paint on form resize
self.canvas.draw()
self.update()
def update(self):
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
# Get objects collection bounds
margin = 2
x1, y1, x2, y2 = self.app.collection.get_bounds()
x1, y1, x2, y2 = x1 - margin, y1 - margin, x2 + margin, y2 + margin
def auto_adjust_axes(self, *args):
"""
Calls ``adjust_axes()`` using the extents of the base axes.
:rtype : None
:return: None
"""
# Get visible bounds
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
# Truncate bounds
print "Collection bounds", x1, x2, y1, y2
print "Viewport", xmin, xmax, ymin, ymax
if x1 < xmin or x2 > xmax or y1 < ymin or y2 > ymax:
print "Truncating bounds"
width = xmax - xmin
height = ymax - ymin
x1 = xmin - width * 2
x2 = xmax + width * 2
y1 = ymin - height * 2
y2 = ymax + height * 2
# self.bx1 = x1
# self.bx2 = x2
# self.by1 = y1
# self.by2 = y2
self.bx1 = xmin - width
self.bx2 = xmax + width
self.by1 = ymin - height
self.by2 = ymax + height
else:
self.reset_nonupdate_bounds()
# Calculate bounds in screen space
points = self.axes.transData.transform([(x1, y1), (x2, y2)])
# Round bounds to integers
rounded_points = [(floor(points[0][0]), floor(points[0][1])), (ceil(points[1][0]), ceil(points[1][1]))]
# Calculate width/height of image
w, h = (rounded_points[1][0] - rounded_points[0][0]), (rounded_points[1][1] - rounded_points[0][1])
# Get bounds back in axes units
inverted_transform = self.axes.transData.inverted()
bounds = inverted_transform.transform(rounded_points)
# print "image bounds", x1, x2, y1, y2, points, rounded_points, bounds, w, h, self.axes.transData.transform(bounds)
x1, x2, y1, y2 = bounds[0][0], bounds[1][0], bounds[0][1], bounds[1][1]
# print "new image bounds", x1, x2, y1, y2
pixel = inverted_transform.transform([(0, 0), (1, 1)])
pixel_size = pixel[1][0] - pixel[0][0]
# print "pixel size", pixel, pixel_size
def update_image(figure):
# Abort update if next update in queue
if self.updates_queue > 1:
self.updates_queue -= 1
return
# Rescale axes
for ax in figure.get_axes():
ax.set_xlim(x1 + pixel_size, x2 + pixel_size)
ax.set_ylim(y1, y2)
ax.set_xticks([])
ax.set_yticks([])
# Resize figure
dpi = figure.dpi
figure.set_size_inches(w / dpi, h / dpi)
try:
# Draw to buffer
self.updates_queue -= 1
offscreen_canvas = FigureOffscreenCanvas(figure)
offscreen_canvas.draw()
# Abort drawing if next update in queue
if self.updates_queue > 0:
del offscreen_canvas
return
buf = offscreen_canvas.buffer_rgba()
ncols, nrows = offscreen_canvas.get_width_height()
image = np.frombuffer(buf, dtype=np.uint8).reshape(nrows, ncols, 4)
self.image_ready.emit(copy.deepcopy(image), (x1, x2, y1, y2))
del image
del offscreen_canvas
except Exception as e:
self.app.log.debug(e.message)
# Do job in background
proc = self.app.proc_container.new("Updating view")
def job_thread(app_obj, figure):
try:
update_image(figure)
except Exception as e:
proc.done()
raise e
proc.done()
# self.app.inform.emit("View update starting ...")
self.updates_queue += 1
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app, self.offscreen_figure]})
def update_canvas(self, image, bounds):
try:
self.image.remove()
except:
pass
self.image = self.axes.imshow(image, extent=bounds, interpolation="Nearest")
self.canvas.draw_idle()
del image
self.adjust_axes(xmin, ymin, xmax, ymax)
def zoom(self, factor, center=None):
"""
@@ -412,7 +346,6 @@ class PlotCanvas(QtCore.QObject):
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
@@ -437,10 +370,10 @@ class PlotCanvas(QtCore.QObject):
ax.set_ylim((ymin, ymax))
# Async re-draw
# self.canvas.draw_idle()
self.canvas.draw_idle()
self.update()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def pan(self, x, y):
xmin, xmax = self.axes.get_xlim()
@@ -456,6 +389,9 @@ class PlotCanvas(QtCore.QObject):
# Re-draw
self.canvas.draw_idle()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def new_axes(self, name):
"""
Creates and returns an Axes object attached to this object's Figure.
@@ -465,13 +401,7 @@ class PlotCanvas(QtCore.QObject):
:rtype: Axes
"""
ax = self.offscreen_figure.add_axes([0.0, 0.0, 1.0, 1.0], label=name)
ax.set_frame_on(False) # No frame
ax.patch.set_visible(False) # No background
ax.set_aspect(1)
return ax
return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
def on_scroll(self, event):
"""
@@ -526,8 +456,7 @@ class PlotCanvas(QtCore.QObject):
self.pan_axes.append(a)
# Set pan view flag
if len(self.pan_axes) > 0:
self.panning = True;
if len(self.pan_axes) > 0: self.panning = True;
def on_mouse_release(self, event):
@@ -553,19 +482,43 @@ class PlotCanvas(QtCore.QObject):
for a in self.pan_axes:
a.drag_pan(1, event.key, event.x, event.y)
# Update
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
# Async re-draw (redraws only on thread idle state, uses timer on backend)
self.canvas.draw_idle()
if xmin < self.bx1 or xmax > self.bx2 or ymin < self.by1 or ymax > self.by2:
# Redraw image
print "Redrawing image"
self.update()
else:
# Async re-draw (redraws only on thread idle state, uses timer on backend)
self.canvas.draw_idle()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def on_draw(self, renderer):
# Store background on canvas redraw
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
def get_axes_pixelsize(self):
"""
Axes size in pixels.
:return: Pixel width and height
:rtype: tuple
"""
bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
width *= self.figure.dpi
height *= self.figure.dpi
return width, height
def get_density(self):
"""
Returns unit length per pixel on horizontal
and vertical axes.
:return: X and Y density
:rtype: tuple
"""
xpx, ypx = self.get_axes_pixelsize()
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
return width / xpx, height / ypx

View File

@@ -1,27 +1,8 @@
FlatCAM: 2D Computer-Aided PCB Manufacturing
============================================
(c) 2014-2015 Juan Pablo Caram
(c) 2014-2016 Juan Pablo Caram
FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
Among other things, it can take a Gerber file generated by your favorite PCB
CAD program, and create G-Code for Isolation routing.
============================================
This fork features:
- "Clear non-copper" feature, supporting multi-tool work.
- Groups in Project view.
- Pan view by dragging in visualizer window with pressed MMB.
- Basic visualizer performance tricks.
Plans for the far future:
- OpenGL-based visualizer.
Some screenshots:
![copper_clear_1.png](https://bitbucket.org/repo/zbbdpg/images/2313087322-copper_clear_1.png)
![copper_clear_cnc_job_1.png](https://bitbucket.org/repo/zbbdpg/images/1699766214-copper_clear_cnc_job_1.png)
![copper_clear_cnc_job_2.png](https://bitbucket.org/repo/zbbdpg/images/3722929164-copper_clear_cnc_job_2.png)
CAD program, and create G-Code for Isolation routing.

533
camlib.py
View File

@@ -9,6 +9,7 @@
#from scipy import optimize
#import traceback
from cStringIO import StringIO
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
transpose
from numpy.linalg import solve, norm
@@ -16,6 +17,7 @@ from matplotlib.figure import Figure
import re
import sys
import traceback
from decimal import Decimal
import collections
import numpy as np
@@ -42,6 +44,16 @@ import simplejson as json
# TODO: Commented for FlatCAM packaging with cx_freeze
#from matplotlib.pyplot import plot, subplot
import xml.etree.ElementTree as ET
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
import itertools
import xml.etree.ElementTree as ET
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
from svgparse import *
import logging
log = logging.getLogger('base2')
@@ -125,6 +137,60 @@ class Geometry(object):
log.error("Failed to run union on polygons.")
raise
def add_polyline(self, points):
"""
Adds a polyline to the object (by union)
:param points: The vertices of the polyline.
:return: None
"""
if self.solid_geometry is None:
self.solid_geometry = []
if type(self.solid_geometry) is list:
self.solid_geometry.append(LineString(points))
return
try:
self.solid_geometry = self.solid_geometry.union(LineString(points))
except:
#print "Failed to run union on polygons."
log.error("Failed to run union on polylines.")
raise
def is_empty(self):
if isinstance(self.solid_geometry, BaseGeometry):
return self.solid_geometry.is_empty
if isinstance(self.solid_geometry, list):
return len(self.solid_geometry) == 0
raise Exception("self.solid_geometry is neither BaseGeometry or list.")
def subtract_polygon(self, points):
"""
Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
:param points: The vertices of the polygon.
:return: none
"""
if self.solid_geometry is None:
self.solid_geometry = []
#pathonly should be allways True, otherwise polygons are not subtracted
flat_geometry = self.flatten(pathonly=True)
log.debug("%d paths" % len(flat_geometry))
polygon=Polygon(points)
toolgeo=cascaded_union(polygon)
diffs=[]
for target in flat_geometry:
if type(target) == LineString or type(target) == LinearRing:
diffs.append(target.difference(toolgeo))
else:
log.warning("Not implemented.")
self.solid_geometry=cascaded_union(diffs)
def bounds(self):
"""
Returns coordinates of rectangular bounds
@@ -193,7 +259,6 @@ class Geometry(object):
return interiors
def get_exteriors(self, geometry=None):
"""
Returns all exteriors of polygons in geometry. Uses
@@ -335,14 +400,39 @@ class Geometry(object):
"""
return self.solid_geometry.buffer(offset)
def is_empty(self):
def import_svg(self, filename, flip=True):
"""
Imports shapes from an SVG file into the object's geometry.
:param filename: Path to the SVG file.
:type filename: str
:return: None
"""
# Parse into list of shapely objects
svg_tree = ET.parse(filename)
svg_root = svg_tree.getroot()
# Change origin to bottom left
# h = float(svg_root.get('height'))
# w = float(svg_root.get('width'))
h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
geos = getsvggeo(svg_root)
if flip:
geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
# Add to object
if self.solid_geometry is None:
return True
self.solid_geometry = []
if type(self.solid_geometry) is list and len(self.solid_geometry) == 0:
return True
if type(self.solid_geometry) is list:
self.solid_geometry.append(cascaded_union(geos))
else: # It's shapely geometry
self.solid_geometry = cascaded_union([self.solid_geometry,
cascaded_union(geos)])
return False
return
def size(self):
"""
@@ -802,6 +892,47 @@ class Geometry(object):
"""
self.solid_geometry = [cascaded_union(self.solid_geometry)]
def export_svg(self, scale_factor=0.00):
"""
Exports the Gemoetry Object as a SVG Element
:return: SVG Element
"""
# Make sure we see a Shapely Geometry class and not a list
geom = cascaded_union(self.flatten())
# scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
# If 0 or less which is invalid then default to 0.05
# This value appears to work for zooming, and getting the output svg line width
# to match that viewed on screen with FlatCam
if scale_factor <= 0:
scale_factor = 0.05
# Convert to a SVG
svg_elem = geom.svg(scale_factor=scale_factor)
return svg_elem
def mirror(self, axis, point):
"""
Mirrors the object around a specified axis passign through
the given point.
:param axis: "X" or "Y" indicates around which axis to mirror.
:type axis: str
:param point: [x, y] point belonging to the mirror axis.
:type point: list
:return: None
"""
px, py = point
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
## solid_geometry ???
# It's a cascaded union of objects.
self.solid_geometry = affinity.scale(self.solid_geometry,
xscale, yscale, origin=(px, py))
class ApertureMacro:
"""
@@ -1125,6 +1256,8 @@ class ApertureMacro:
:param modifiers: Modifiers (parameters) for this macro
:type modifiers: list
:return: Shapely geometry
:rtype: shapely.geometry.polygon
"""
## Primitive makers
@@ -1145,11 +1278,11 @@ class ApertureMacro:
modifiers = [float(m) for m in modifiers]
self.locvars = {}
for i in range(0, len(modifiers)):
self.locvars[str(i+1)] = modifiers[i]
self.locvars[str(i + 1)] = modifiers[i]
## Parse
self.primitives = [] # Cleanup
self.geometry = None
self.geometry = Polygon()
self.parse_content()
## Make the geometry
@@ -1158,9 +1291,9 @@ class ApertureMacro:
prim_geo = makers[str(int(primitive[0]))](primitive[1:])
# Add it (according to polarity)
if self.geometry is None and prim_geo['pol'] == 1:
self.geometry = prim_geo['geometry']
continue
# if self.geometry is None and prim_geo['pol'] == 1:
# self.geometry = prim_geo['geometry']
# continue
if prim_geo['pol'] == 1:
self.geometry = self.geometry.union(prim_geo['geometry'])
continue
@@ -1263,7 +1396,9 @@ class Gerber (Geometry):
self.comm_re = re.compile(r'^G0?4(.*)$')
# AD - Aperture definition
self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.]*)(?:,(.*))?\*%$')
# Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
# NOTE: Adding "-" to support output from Upverter.
self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
# AM - Aperture Macro
# Beginning of macro (Ends with *%):
@@ -1383,35 +1518,35 @@ class Gerber (Geometry):
## Solid geometry
self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
def mirror(self, axis, point):
"""
Mirrors the object around a specified axis passign through
the given point. What is affected:
* ``buffered_paths``
* ``flash_geometry``
* ``solid_geometry``
* ``regions``
NOTE:
Does not modify the data used to create these elements. If these
are recreated, the scaling will be lost. This behavior was modified
because of the complexity reached in this class.
:param axis: "X" or "Y" indicates around which axis to mirror.
:type axis: str
:param point: [x, y] point belonging to the mirror axis.
:type point: list
:return: None
"""
px, py = point
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
## solid_geometry ???
# It's a cascaded union of objects.
self.solid_geometry = affinity.scale(self.solid_geometry,
xscale, yscale, origin=(px, py))
# def mirror(self, axis, point):
# """
# Mirrors the object around a specified axis passign through
# the given point. What is affected:
#
# * ``buffered_paths``
# * ``flash_geometry``
# * ``solid_geometry``
# * ``regions``
#
# NOTE:
# Does not modify the data used to create these elements. If these
# are recreated, the scaling will be lost. This behavior was modified
# because of the complexity reached in this class.
#
# :param axis: "X" or "Y" indicates around which axis to mirror.
# :type axis: str
# :param point: [x, y] point belonging to the mirror axis.
# :type point: list
# :return: None
# """
#
# px, py = point
# xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
#
# ## solid_geometry ???
# # It's a cascaded union of objects.
# self.solid_geometry = affinity.scale(self.solid_geometry,
# xscale, yscale, origin=(px, py))
def aperture_parse(self, apertureId, apertureType, apParameters):
"""
@@ -1521,7 +1656,8 @@ class Gerber (Geometry):
# Otherwise leave as is.
else:
yield cleanline
# yield cleanline
yield line
break
self.parse_lines(line_generator(), follow=follow)
@@ -1602,7 +1738,7 @@ class Gerber (Geometry):
match = self.am1_re.search(gline)
# Start macro if match, else not an AM, carry on.
if match:
log.info("Starting macro. Line %d: %s" % (line_num, gline))
log.debug("Starting macro. Line %d: %s" % (line_num, gline))
current_macro = match.group(1)
self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
if match.group(2): # Append
@@ -1610,13 +1746,13 @@ class Gerber (Geometry):
if match.group(3): # Finish macro
#self.aperture_macros[current_macro].parse_content()
current_macro = None
log.info("Macro complete in 1 line.")
log.debug("Macro complete in 1 line.")
continue
else: # Continue macro
log.info("Continuing macro. Line %d." % line_num)
log.debug("Continuing macro. Line %d." % line_num)
match = self.am2_re.search(gline)
if match: # Finish macro
log.info("End of macro. Line %d." % line_num)
log.debug("End of macro. Line %d." % line_num)
self.aperture_macros[current_macro].append(match.group(1))
#self.aperture_macros[current_macro].parse_content()
current_macro = None
@@ -1659,7 +1795,10 @@ class Gerber (Geometry):
## --- BUFFERED ---
if making_region:
geo = Polygon(path)
if follow:
geo = Polygon()
else:
geo = Polygon(path)
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
@@ -1669,7 +1808,9 @@ class Gerber (Geometry):
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
if not geo.is_empty: poly_buffer.append(geo)
if not geo.is_empty:
poly_buffer.append(geo)
path = [[current_x, current_y]] # Start new path
@@ -1681,17 +1822,26 @@ class Gerber (Geometry):
if len(path) > 1:
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width / 2)
if not geo.is_empty: poly_buffer.append(geo)
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
if not geo.is_empty:
poly_buffer.append(geo)
# Reset path starting point
path = [[current_x, current_y]]
# --- BUFFERED ---
# Draw the flash
if follow:
continue
flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
self.apertures[current_aperture])
if not flash.is_empty: poly_buffer.append(flash)
if not flash.is_empty:
poly_buffer.append(flash)
continue
@@ -1744,8 +1894,13 @@ class Gerber (Geometry):
# --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"]
buffered = LineString(path).buffer(width / 2)
if not buffered.is_empty: poly_buffer.append(buffered)
if follow:
buffered = LineString(path)
else:
buffered = LineString(path).buffer(width / 2)
if not buffered.is_empty:
poly_buffer.append(buffered)
current_x = x
current_y = y
@@ -1858,9 +2013,12 @@ class Gerber (Geometry):
log.debug("Bare op-code %d." % current_operation_code)
# flash = Gerber.create_flash_geometry(Point(path[-1]),
# self.apertures[current_aperture])
if follow:
continue
flash = Gerber.create_flash_geometry(Point(current_x, current_y),
self.apertures[current_aperture])
if not flash.is_empty: poly_buffer.append(flash)
if not flash.is_empty:
poly_buffer.append(flash)
except IndexError:
log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
@@ -1882,8 +2040,13 @@ class Gerber (Geometry):
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
if not geo.is_empty: poly_buffer.append(geo)
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width/2)
if not geo.is_empty:
poly_buffer.append(geo)
path = [path[-1]]
@@ -1910,10 +2073,15 @@ class Gerber (Geometry):
# "aperture": last_path_aperture})
# --- Buffered ---
region = Polygon(path)
if follow:
region = Polygon()
else:
region = Polygon(path)
if not region.is_valid:
region = region.buffer(0)
if not region.is_empty: poly_buffer.append(region)
if not follow:
region = region.buffer(0)
if not region.is_empty:
poly_buffer.append(region)
path = [[current_x, current_y]] # Start new path
continue
@@ -1946,8 +2114,14 @@ class Gerber (Geometry):
if len(path) > 1:
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width / 2)
if not geo.is_empty: poly_buffer.append(geo)
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
if not geo.is_empty:
poly_buffer.append(geo)
path = [path[-1]]
continue
@@ -1962,8 +2136,13 @@ class Gerber (Geometry):
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width / 2)
if not geo.is_empty: poly_buffer.append(geo)
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
if not geo.is_empty:
poly_buffer.append(geo)
path = [path[-1]]
@@ -2034,10 +2213,18 @@ class Gerber (Geometry):
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width / 2)
if not geo.is_empty: poly_buffer.append(geo)
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 2)
if not geo.is_empty:
poly_buffer.append(geo)
# --- Apply buffer ---
if follow:
self.solid_geometry = poly_buffer
return
log.warn("Joining %d polygons." % len(poly_buffer))
if self.use_buffer_for_union:
log.debug("Union by buffer...")
@@ -2117,8 +2304,11 @@ class Gerber (Geometry):
if aperture['type'] == 'AM': # Aperture Macro
loc = location.coords[0]
flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
if flash_geo.is_empty:
log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
log.warning("Unknown aperture type: %s" % aperture['type'])
return None
def create_geometry(self):
@@ -2604,7 +2794,8 @@ class CNCjob(Geometry):
self.feedrate = feedrate
self.tooldia = tooldia
self.unitcode = {"IN": "G20", "MM": "G21"}
self.pausecode = "G04 P1"
# TODO: G04 Does not exist. It's G4 and now we are handling in postprocessing.
#self.pausecode = "G04 P1"
self.feedminutecode = "G94"
self.absolutecode = "G90"
self.gcode = ""
@@ -2656,12 +2847,20 @@ class CNCjob(Geometry):
log.debug("Creating CNC Job from Excellon...")
# Tools
# sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
# so we actually are sorting the tools by diameter
sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
if tools == "all":
tools = [tool for tool in exobj.tools]
tools = [i[0] for i in sorted_tools] # we get a array of ordered tools
log.debug("Tools 'all' and sorted are: %s" % str(tools))
else:
tools = [x.strip() for x in tools.split(",")]
tools = filter(lambda i: i in exobj.tools, tools)
log.debug("Tools are: %s" % str(tools))
selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ','
selected_tools = filter(lambda i: i in selected_tools, selected_tools)
# Create a sorted list of selected tools from the sorted_tools list
tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
log.debug("Tools selected and sorted are: %s" % str(tools))
# Points (Group by tool)
points = {}
@@ -2678,7 +2877,8 @@ class CNCjob(Geometry):
# Basic G-Code macros
t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n"
down = "G01 Z%.4f\n" % self.z_cut
up = "G01 Z%.4f\n" % self.z_move
up = "G00 Z%.4f\n" % self.z_move
up_to_zero = "G01 Z0\n"
# Initialization
gcode = self.unitcode[self.units.upper()] + "\n"
@@ -2688,32 +2888,36 @@ class CNCjob(Geometry):
gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
if self.spindlespeed is not None:
gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
# Spindle start with configured speed
gcode += "M03 S%d\n" % int(self.spindlespeed)
else:
gcode += "M03\n" # Spindle start
gcode += self.pausecode + "\n"
#gcode += self.pausecode + "\n"
for tool in points:
for tool in tools:
# Tool change sequence (optional)
if toolchange:
gcode += "G00 Z%.4f\n" % toolchangez
gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer)
gcode += "M5\n" # Spindle Stop
gcode += "M6\n" # Tool change
gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
gcode += "M0\n" # Temporary machine stop
if self.spindlespeed is not None:
gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
else:
gcode += "M03\n" # Spindle start
# Only if tool has points.
if tool in points:
# Tool change sequence (optional)
if toolchange:
gcode += "G00 Z%.4f\n" % toolchangez
gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer)
gcode += "M5\n" # Spindle Stop
gcode += "M6\n" # Tool change
gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
gcode += "M0\n" # Temporary machine stop
if self.spindlespeed is not None:
# Spindle start with configured speed
gcode += "M03 S%d\n" % int(self.spindlespeed)
else:
gcode += "M03\n" # Spindle start
# Drillling!
for point in points[tool]:
x, y = point.coords.xy
gcode += t % (x[0], y[0])
gcode += down + up
# Drillling!
for point in points[tool]:
x, y = point.coords.xy
gcode += t % (x[0], y[0])
gcode += down + up_to_zero + up
gcode += t % (0, 0)
gcode += "M05\n" # Spindle stop
@@ -2786,7 +2990,7 @@ class CNCjob(Geometry):
self.gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
else:
self.gcode += "M03\n" # Spindle start
self.gcode += self.pausecode + "\n"
#self.gcode += self.pausecode + "\n"
## Iterate over geometry paths getting the nearest each time.
log.debug("Starting G-Code...")
@@ -2822,17 +3026,24 @@ class CNCjob(Geometry):
#--------- Multi-pass ---------
else:
if isinstance(self.z_cut, Decimal):
z_cut = self.z_cut
else:
z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001'))
if depthpercut is None:
depthpercut = self.z_cut
depthpercut = z_cut
elif not isinstance(depthpercut, Decimal):
depthpercut = Decimal(depthpercut).quantize(Decimal('0.000000001'))
depth = 0
reverse = False
while depth > self.z_cut:
while depth > z_cut:
# Increase depth. Limit to z_cut.
depth -= depthpercut
if depth < self.z_cut:
depth = self.z_cut
if depth < z_cut:
depth = z_cut
# Cut at specific depth and do not lift the tool.
# Note: linear2gcode() will use G00 to move to the
@@ -2887,65 +3098,25 @@ class CNCjob(Geometry):
self.gcode += "G00 X0Y0\n"
self.gcode += "M05\n" # Spindle stop
def pre_parse(self, gtext):
@staticmethod
def codes_split(gline):
"""
Separates parts of the G-Code text into a list of dictionaries.
Used by ``self.gcode_parse()``.
Parses a line of G-Code such as "G01 X1234 Y987" into
a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
:param gtext: A single string with g-code
:param gline: G-Code line string
:return: Dictionary with parsed line.
"""
# Units: G20-inches, G21-mm
units_re = re.compile(r'^G2([01])')
command = {}
# TODO: This has to be re-done
gcmds = []
lines = gtext.split("\n") # TODO: This is probably a lot of work!
for line in lines:
# Clean up
line = line.strip()
match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
while match:
command[match.group(1)] = float(match.group(2).replace(" ", ""))
gline = gline[match.end():]
match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
# Remove comments
# NOTE: Limited to 1 bracket pair
op = line.find("(")
cl = line.find(")")
#if op > -1 and cl > op:
if cl > op > -1:
#comment = line[op+1:cl]
line = line[:op] + line[(cl+1):]
# Units
match = units_re.match(line)
if match:
self.units = {'0': "IN", '1': "MM"}[match.group(1)]
# Parse GCode
# 0 4 12
# G01 X-0.007 Y-0.057
# --> codes_idx = [0, 4, 12]
codes = "NMGXYZIJFPST"
codes_idx = []
i = 0
for ch in line:
if ch in codes:
codes_idx.append(i)
i += 1
n_codes = len(codes_idx)
if n_codes == 0:
continue
# Separate codes in line
parts = []
for p in range(n_codes - 1):
parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
parts.append(line[codes_idx[-1]:].strip())
# Separate codes from values
cmds = {}
for part in parts:
cmds[part[0]] = float(part[1:])
gcmds.append(cmds)
return gcmds
return command
def gcode_parse(self):
"""
@@ -2958,10 +3129,7 @@ class CNCjob(Geometry):
# Results go here
geometry = []
# TODO: Merge into single parser?
gobjs = self.pre_parse(self.gcode)
# Last known instruction
current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
@@ -2970,7 +3138,14 @@ class CNCjob(Geometry):
path = [(0, 0)]
# Process every instruction
for gobj in gobjs:
for line in StringIO(self.gcode):
gobj = self.codes_split(line)
## Units
if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
continue
## Changing height
if 'Z' in gobj:
@@ -3014,7 +3189,7 @@ class CNCjob(Geometry):
center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
radius = sqrt(gobj['I']**2 + gobj['J']**2)
start = arctan2(-gobj['J'], -gobj['I'])
stop = arctan2(-center[1]+y, -center[0]+x)
stop = arctan2(-center[1] + y, -center[0] + x)
path += arc(center, radius, start, stop,
arcdir[current['G']],
self.steps_per_circ)
@@ -3226,6 +3401,54 @@ class CNCjob(Geometry):
self.create_geometry()
def export_svg(self, scale_factor=0.00):
"""
Exports the CNC Job as a SVG Element
:scale_factor: float
:return: SVG Element string
"""
# scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
# If not specified then try and use the tool diameter
# This way what is on screen will match what is outputed for the svg
# This is quite a useful feature for svg's used with visicut
if scale_factor <= 0:
scale_factor = self.options['tooldia'] / 2
# If still 0 then defailt to 0.05
# This value appears to work for zooming, and getting the output svg line width
# to match that viewed on screen with FlatCam
if scale_factor == 0:
scale_factor = 0.05
# Seperate the list of cuts and travels into 2 distinct lists
# This way we can add different formatting / colors to both
cuts = []
travels = []
for g in self.gcode_parsed:
if g['kind'][0] == 'C': cuts.append(g)
if g['kind'][0] == 'T': travels.append(g)
# Used to determine the overall board size
self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
# Convert the cuts and travels into single geometry objects we can render as svg xml
if travels:
travelsgeom = cascaded_union([geo['geom'] for geo in travels])
if cuts:
cutsgeom = cascaded_union([geo['geom'] for geo in cuts])
# Render the SVG Xml
# The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
# It's better to have the travels sitting underneath the cuts for visicut
svg_elem = ""
if travels:
svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D")
if cuts:
svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF")
return svg_elem
# def get_bounds(geometry_set):
# xmin = Inf

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -13,3 +13,4 @@ pip install --upgrade matplotlib
pip install --upgrade Shapely
apt-get install libspatialindex-dev
pip install rtree
pip install svg.path

522
svgparse.py Normal file
View File

@@ -0,0 +1,522 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 12/18/2015 #
# MIT Licence #
# #
# SVG Features supported: #
# * Groups #
# * Rectangles (w/ rounded corners) #
# * Circles #
# * Ellipses #
# * Polygons #
# * Polylines #
# * Lines #
# * Paths #
# * All transformations #
# #
# Reference: www.w3.org/TR/SVG/Overview.html #
############################################################
import xml.etree.ElementTree as ET
import re
import itertools
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
from shapely.geometry import LinearRing, LineString, Point
from shapely.affinity import translate, rotate, scale, skew, affine_transform
import numpy as np
import logging
log = logging.getLogger('base2')
def svgparselength(lengthstr):
"""
Parse an SVG length string into a float and a units
string, if any.
:param lengthstr: SVG length string.
:return: Number and units pair.
:rtype: tuple(float, str|None)
"""
integer_re_str = r'[+-]?[0-9]+'
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
match = re.search(length_re_str, lengthstr)
if match:
return float(match.group(1)), match.group(2)
raise Exception('Cannot parse SVG length: %s' % lengthstr)
def path2shapely(path, res=1.0):
"""
Converts an svg.path.Path into a Shapely
LinearRing or LinearString.
:rtype : LinearRing
:rtype : LineString
:param path: svg.path.Path instance
:param res: Resolution (minimum step along path)
:return: Shapely geometry object
"""
points = []
for component in path:
# Line
if isinstance(component, Line):
start = component.start
x, y = start.real, start.imag
if len(points) == 0 or points[-1] != (x, y):
points.append((x, y))
end = component.end
points.append((end.real, end.imag))
continue
# Arc, CubicBezier or QuadraticBezier
if isinstance(component, Arc) or \
isinstance(component, CubicBezier) or \
isinstance(component, QuadraticBezier):
# How many points to use in the dicrete representation.
length = component.length(res / 10.0)
steps = int(length / res + 0.5)
# solve error when step is below 1,
# it may cause other problems, but LineString needs at least two points
if steps == 0:
steps = 1
frac = 1.0 / steps
# print length, steps, frac
for i in range(steps):
point = component.point(i * frac)
x, y = point.real, point.imag
if len(points) == 0 or points[-1] != (x, y):
points.append((x, y))
end = component.point(1.0)
points.append((end.real, end.imag))
continue
log.warning("I don't know what this is:", component)
continue
if path.closed:
return LinearRing(points)
else:
return LineString(points)
def svgrect2shapely(rect, n_points=32):
"""
Converts an SVG rect into Shapely geometry.
:param rect: Rect Element
:type rect: xml.etree.ElementTree.Element
:return: shapely.geometry.polygon.LinearRing
"""
w = svgparselength(rect.get('width'))[0]
h = svgparselength(rect.get('height'))[0]
x_obj = rect.get('x')
if x_obj is not None:
x = svgparselength(x_obj)[0]
else:
x = 0
y_obj = rect.get('y')
if y_obj is not None:
y = svgparselength(y_obj)[0]
else:
y = 0
rxstr = rect.get('rx')
rystr = rect.get('ry')
if rxstr is None and rystr is None: # Sharp corners
pts = [
(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
]
else: # Rounded corners
rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
ry = 0.0 if rystr is None else svgparselength(rystr)[0]
n_points = int(n_points / 4 + 0.5)
t = np.arange(n_points, dtype=float) / n_points / 4
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
lower_right = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
upper_right = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
upper_left = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
lower_left = [(x_[i], y_[i]) for i in range(n_points)]
pts = [(x + rx, y), (x - rx + w, y)] + \
lower_right + \
[(x + w, y + ry), (x + w, y + h - ry)] + \
upper_right + \
[(x + w - rx, y + h), (x + rx, y + h)] + \
upper_left + \
[(x, y + h - ry), (x, y + ry)] + \
lower_left
return LinearRing(pts)
def svgcircle2shapely(circle):
"""
Converts an SVG circle into Shapely geometry.
:param circle: Circle Element
:type circle: xml.etree.ElementTree.Element
:return: Shapely representation of the circle.
:rtype: shapely.geometry.polygon.LinearRing
"""
# cx = float(circle.get('cx'))
# cy = float(circle.get('cy'))
# r = float(circle.get('r'))
cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
# TODO: No resolution specified.
return Point(cx, cy).buffer(r)
def svgellipse2shapely(ellipse, n_points=64):
"""
Converts an SVG ellipse into Shapely geometry
:param ellipse: Ellipse Element
:type ellipse: xml.etree.ElementTree.Element
:param n_points: Number of discrete points in output.
:return: Shapely representation of the ellipse.
:rtype: shapely.geometry.polygon.LinearRing
"""
cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
t = np.arange(n_points, dtype=float) / n_points
x = cx + rx * np.cos(2 * np.pi * t)
y = cy + ry * np.sin(2 * np.pi * t)
pts = [(x[i], y[i]) for i in range(n_points)]
return LinearRing(pts)
def svgline2shapely(line):
"""
:param line: Line element
:type line: xml.etree.ElementTree.Element
:return: Shapely representation on the line.
:rtype: shapely.geometry.polygon.LinearRing
"""
x1 = svgparselength(line.get('x1'))[0]
y1 = svgparselength(line.get('y1'))[0]
x2 = svgparselength(line.get('x2'))[0]
y2 = svgparselength(line.get('y2'))[0]
return LineString([(x1, y1), (x2, y2)])
def svgpolyline2shapely(polyline):
ptliststr = polyline.get('points')
points = parse_svg_point_list(ptliststr)
return LineString(points)
def svgpolygon2shapely(polygon):
ptliststr = polygon.get('points')
points = parse_svg_point_list(ptliststr)
return LinearRing(points)
def getsvggeo(node):
"""
Extracts and flattens all geometry from an SVG node
into a list of Shapely geometry.
:param node: xml.etree.ElementTree.Element
:return: List of Shapely geometry
:rtype: list
"""
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
geo = []
# Recurse
if len(node) > 0:
for child in node:
subgeo = getsvggeo(child)
if subgeo is not None:
geo += subgeo
# Parse
elif kind == 'path':
log.debug("***PATH***")
P = parse_path(node.get('d'))
P = path2shapely(P)
geo = [P]
elif kind == 'rect':
log.debug("***RECT***")
R = svgrect2shapely(node)
geo = [R]
elif kind == 'circle':
log.debug("***CIRCLE***")
C = svgcircle2shapely(node)
geo = [C]
elif kind == 'ellipse':
log.debug("***ELLIPSE***")
E = svgellipse2shapely(node)
geo = [E]
elif kind == 'polygon':
log.debug("***POLYGON***")
poly = svgpolygon2shapely(node)
geo = [poly]
elif kind == 'line':
log.debug("***LINE***")
line = svgline2shapely(node)
geo = [line]
elif kind == 'polyline':
log.debug("***POLYLINE***")
pline = svgpolyline2shapely(node)
geo = [pline]
else:
log.warning("Unknown kind: " + kind)
geo = None
# ignore transformation for unknown kind
if geo is not None:
# Transformations
if 'transform' in node.attrib:
trstr = node.get('transform')
trlist = parse_svg_transform(trstr)
#log.debug(trlist)
# Transformations are applied in reverse order
for tr in trlist[::-1]:
if tr[0] == 'translate':
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
elif tr[0] == 'scale':
geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'rotate':
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
for geoi in geo]
elif tr[0] == 'skew':
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'matrix':
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
else:
raise Exception('Unknown transformation: %s', tr)
return geo
def parse_svg_point_list(ptliststr):
"""
Returns a list of coordinate pairs extracted from the "points"
attribute in SVG polygons and polylines.
:param ptliststr: "points" attribute string in polygon or polyline.
:return: List of tuples with coordinates.
"""
pairs = []
last = None
pos = 0
i = 0
for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
val = float(ptliststr[pos:match.start()])
if i % 2 == 1:
pairs.append((last, val))
else:
last = val
pos = match.end()
i += 1
# Check for last element
val = float(ptliststr[pos:])
if i % 2 == 1:
pairs.append((last, val))
else:
log.warning("Incomplete coordinates.")
return pairs
def parse_svg_transform(trstr):
"""
Parses an SVG transform string into a list
of transform names and their parameters.
Possible transformations are:
* Translate: translate(<tx> [<ty>]), which specifies
a translation by tx and ty. If <ty> is not provided,
it is assumed to be zero. Result is
['translate', tx, ty]
* Scale: scale(<sx> [<sy>]), which specifies a scale operation
by sx and sy. If <sy> is not provided, it is assumed to be
equal to <sx>. Result is: ['scale', sx, sy]
* Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
a rotation by <rotate-angle> degrees about a given point.
If optional parameters <cx> and <cy> are not supplied,
the rotate is about the origin of the current user coordinate
system. Result is: ['rotate', rotate-angle, cx, cy]
* Skew: skewX(<skew-angle>), which specifies a skew
transformation along the x-axis. skewY(<skew-angle>), which
specifies a skew transformation along the y-axis.
Result is ['skew', angle-x, angle-y]
* Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
transformation in the form of a transformation matrix of six
values. matrix(a,b,c,d,e,f) is equivalent to applying the
transformation matrix [a b c d e f]. Result is
['matrix', a, b, c, d, e, f]
Note: All parameters to the transformations are "numbers",
i.e. no units present.
:param trstr: SVG transform string.
:type trstr: str
:return: List of transforms.
:rtype: list
"""
trlist = []
assert isinstance(trstr, str)
trstr = trstr.strip(' ')
integer_re_str = r'[+-]?[0-9]+'
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
# num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
translate_re_str = r'translate\s*\(\s*(' + \
number_re_str + r')(?:' + \
comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
scale_re_str = r'scale\s*\(\s*(' + \
number_re_str + r')' + \
r'(?:' + comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
skew_re_str = r'skew([XY])\s*\(\s*(' + \
number_re_str + r')\s*\)'
rotate_re_str = r'rotate\s*\(\s*(' + \
number_re_str + r')' + \
r'(?:' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + \
comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
matrix_re_str = r'matrix\s*\(\s*' + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')\s*\)'
while len(trstr) > 0:
match = re.search(r'^' + translate_re_str, trstr)
if match:
trlist.append([
'translate',
float(match.group(1)),
float(match.group(2)) if match.group else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + scale_re_str, trstr)
if match:
trlist.append([
'translate',
float(match.group(1)),
float(match.group(2)) if match.group else float(match.group(1))
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + skew_re_str, trstr)
if match:
trlist.append([
'skew',
float(match.group(2)) if match.group(1) == 'X' else 0.0,
float(match.group(2)) if match.group(1) == 'Y' else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + rotate_re_str, trstr)
if match:
trlist.append([
'rotate',
float(match.group(1)),
float(match.group(2)) if match.group(2) else 0.0,
float(match.group(3)) if match.group(3) else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + matrix_re_str, trstr)
if match:
trlist.append(['matrix'] + [float(x) for x in match.groups()])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
raise Exception("Don't know how to parse: %s" % trstr)
return trlist
if __name__ == "__main__":
tree = ET.parse('tests/svg/drawing.svg')
root = tree.getroot()
ns = re.search(r'\{(.*)\}', root.tag).group(1)
print ns
for geo in getsvggeo(root):
print geo

398
tclCommands/TclCommand.py Normal file
View File

@@ -0,0 +1,398 @@
import sys
import re
import FlatCAMApp
import abc
import collections
from PyQt4 import QtCore
from contextlib import contextmanager
class TclCommand(object):
# FlatCAMApp
app = None
# logger
log = None
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = []
# dictionary of types from Tcl command, needs to be ordered
# OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
# OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
option_types = collections.OrderedDict()
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
# OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
help = {
'main': "undefined help.",
'args': collections.OrderedDict([
('argumentname', 'undefined help.'),
('optionname', 'undefined help.')
]),
'examples': []
}
# original incoming arguments into command
original_args = None
def __init__(self, app):
self.app = app
if self.app is None:
raise TypeError('Expected app to be FlatCAMApp instance.')
if not isinstance(self.app, FlatCAMApp.App):
raise TypeError('Expected FlatCAMApp, got %s.' % type(app))
self.log = self.app.log
def raise_tcl_error(self, text):
"""
this method pass exception from python into TCL as error, so we get stacktrace and reason
this is only redirect to self.app.raise_tcl_error
:param text: text of error
:return: none
"""
self.app.raise_tcl_error(text)
def get_current_command(self):
"""
get current command, we are not able to get it from TCL we have to reconstruct it
:return: current command
"""
command_string = []
command_string.append(self.aliases[0])
if self.original_args is not None:
for arg in self.original_args:
command_string.append(arg)
return " ".join(command_string)
def get_decorated_help(self):
"""
Decorate help for TCL console output.
:return: decorated help from structure
"""
def get_decorated_command(alias_name):
command_string = []
for arg_key, arg_type in self.help['args'].items():
command_string.append(get_decorated_argument(arg_key, arg_type, True))
return "> " + alias_name + " " + " ".join(command_string)
def get_decorated_argument(help_key, help_text, in_command=False):
option_symbol = ''
if help_key in self.arg_names:
arg_type = self.arg_names[help_key]
type_name = str(arg_type.__name__)
in_command_name = "<" + type_name + ">"
elif help_key in self.option_types:
option_symbol = '-'
arg_type = self.option_types[help_key]
type_name = str(arg_type.__name__)
in_command_name = option_symbol + help_key + " <" + type_name + ">"
else:
option_symbol = ''
type_name = '?'
in_command_name = option_symbol + help_key + " <" + type_name + ">"
if in_command:
if help_key in self.required:
return in_command_name
else:
return '[' + in_command_name + "]"
else:
if help_key in self.required:
return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text
else:
return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]"
def get_decorated_example(example_item):
return "> "+example_item
help_string = [self.help['main']]
for alias in self.aliases:
help_string.append(get_decorated_command(alias))
for key, value in self.help['args'].items():
help_string.append(get_decorated_argument(key, value))
# timeout is unique for signaled commands (this is not best oop practice, but much easier for now)
if isinstance(self, TclCommandSignaled):
help_string.append("\t[-timeout <int>: Max wait for job timeout before error.]")
for example in self.help['examples']:
help_string.append(get_decorated_example(example))
return "\n".join(help_string)
@staticmethod
def parse_arguments(args):
"""
Pre-processes arguments to detect '-keyword value' pairs into dictionary
and standalone parameters into list.
This is copy from FlatCAMApp.setup_shell().h() just for accessibility,
original should be removed after all commands will be converted
:param args: arguments from tcl to parse
:return: arguments, options
"""
options = {}
arguments = []
n = len(args)
name = None
for i in range(n):
match = re.search(r'^-([a-zA-Z].*)', args[i])
if match:
assert name is None
name = match.group(1)
continue
if name is None:
arguments.append(args[i])
else:
options[name] = args[i]
name = None
return arguments, options
def check_args(self, args):
"""
Check arguments and options for right types
:param args: arguments from tcl to check
:return: named_args, unnamed_args
"""
arguments, options = self.parse_arguments(args)
named_args = {}
unnamed_args = []
# check arguments
idx = 0
arg_names_items = self.arg_names.items()
for argument in arguments:
if len(self.arg_names) > idx:
key, arg_type = arg_names_items[idx]
try:
named_args[key] = arg_type(argument)
except Exception, e:
self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'."
% (key, arg_type, str(e)))
else:
unnamed_args.append(argument)
idx += 1
# check options
for key in options:
if key not in self.option_types and key != 'timeout':
self.raise_tcl_error('Unknown parameter: %s' % key)
try:
if key != 'timeout':
named_args[key] = self.option_types[key](options[key])
else:
named_args[key] = int(options[key])
except Exception, e:
self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
% (key, self.option_types[key], str(e)))
# check required arguments
for key in self.required:
if key not in named_args:
self.raise_tcl_error("Missing required argument '%s'." % key)
return named_args, unnamed_args
def raise_tcl_unknown_error(self, unknownException):
"""
raise Exception if is different type than TclErrorException
this is here mainly to show unknown errors inside TCL shell console
:param unknownException:
:return:
"""
#if not isinstance(unknownException, self.TclErrorException):
# self.raise_tcl_error("Unknown error: %s" % str(unknownException))
#else:
raise unknownException
def raise_tcl_error(self, text):
"""
this method pass exception from python into TCL as error, so we get stacktrace and reason
:param text: text of error
:return: raise exception
"""
# becouse of signaling we cannot call error to TCL from here but when task is finished
# also nonsiglaned arwe handled here to better exception handling and diplay after command is finished
raise self.app.TclErrorException(text)
def execute_wrapper(self, *args):
"""
Command which is called by tcl console when current commands aliases are hit.
Main catch(except) is implemented here.
This method should be reimplemented only when initial checking sequence differs
:param args: arguments passed from tcl command console
:return: None, output text or exception
"""
#self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
try:
self.log.debug("TCL command '%s' executed." % str(self.__class__))
self.original_args=args
args, unnamed_args = self.check_args(args)
return self.execute(args, unnamed_args)
except Exception as unknown:
error_info=sys.exc_info()
self.log.error("TCL command '%s' failed." % str(self))
self.app.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)
@abc.abstractmethod
def execute(self, args, unnamed_args):
"""
Direct execute of command, this method should be implemented in each descendant.
No main catch should be implemented here.
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None, output text or exception
"""
raise NotImplementedError("Please Implement this method")
class TclCommandSignaled(TclCommand):
"""
!!! I left it here only for demonstration !!!
Go to TclCommandCncjob and into class definition put
class TclCommandCncjob(TclCommand.TclCommandSignaled):
also change
obj.generatecncjob(use_thread = False, **args)
to
obj.generatecncjob(use_thread = True, **args)
This class is child of TclCommand and is used for commands which create new objects
it handles all neccessary stuff about blocking and passing exeptions
"""
output = None
def execute_call(self, args, unnamed_args):
try:
self.output = None
self.error=None
self.error_info=None
self.output = self.execute(args, unnamed_args)
except Exception as unknown:
self.error_info = sys.exc_info()
self.error=unknown
finally:
self.app.shell_command_finished.emit(self)
def execute_wrapper(self, *args):
"""
Command which is called by tcl console when current commands aliases are hit.
Main catch(except) is implemented here.
This method should be reimplemented only when initial checking sequence differs
:param args: arguments passed from tcl command console
:return: None, output text or exception
"""
@contextmanager
def wait_signal(signal, timeout=300000):
"""Block loop until signal emitted, or timeout (ms) elapses."""
loop = QtCore.QEventLoop()
# Normal termination
signal.connect(loop.quit)
# Termination by exception in thread
self.app.thread_exception.connect(loop.quit)
status = {'timed_out': False}
def report_quit():
status['timed_out'] = True
loop.quit()
yield
# Temporarily change how exceptions are managed.
oeh = sys.excepthook
ex = []
def except_hook(type_, value, traceback_):
ex.append(value)
oeh(type_, value, traceback_)
sys.excepthook = except_hook
# Terminate on timeout
if timeout is not None:
QtCore.QTimer.singleShot(timeout, report_quit)
# Block
loop.exec_()
# Restore exception management
sys.excepthook = oeh
if ex:
raise ex[0]
if status['timed_out']:
self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout <miliseconds>' for command or 'set_sys background_timeout <miliseconds>'.")
try:
self.log.debug("TCL command '%s' executed." % str(self.__class__))
self.original_args=args
args, unnamed_args = self.check_args(args)
if 'timeout' in args:
passed_timeout=args['timeout']
del args['timeout']
else:
passed_timeout= self.app.defaults['background_timeout']
# set detail for processing, it will be there until next open or close
self.app.shell.open_proccessing(self.get_current_command())
def handle_finished(obj):
self.app.shell_command_finished.disconnect(handle_finished)
if self.error is not None:
self.raise_tcl_unknown_error(self.error)
self.app.shell_command_finished.connect(handle_finished)
with wait_signal(self.app.shell_command_finished, passed_timeout):
# every TclCommandNewObject ancestor support timeout as parameter,
# but it does not mean anything for child itself
# when operation will be really long is good to set it higher then defqault 30s
self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
return self.output
except Exception as unknown:
# if error happens inside thread execution, then pass correct error_info to display
if self.error_info is not None:
error_info = self.error_info
else:
error_info=sys.exc_info()
self.log.error("TCL command '%s' failed." % str(self))
self.app.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)

View File

@@ -0,0 +1,61 @@
from ObjectCollection import *
import TclCommand
class TclCommandAddPolygon(TclCommand.TclCommandSignaled):
"""
Tcl shell command to create a polygon in the given Geometry object
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['add_polygon', 'add_poly']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict()
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Creates a polygon in the given Geometry object.",
'args': collections.OrderedDict([
('name', 'Name of the Geometry object to which to append the polygon.'),
('xi, yi', 'Coordinates of points in the polygon.')
]),
'examples': [
'add_polygon <name> <x0> <y0> <x1> <y1> <x2> <y2> [x3 y3 [...]]'
]
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, Geometry):
self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
if len(unnamed_args) % 2 != 0:
self.raise_tcl_error("Incomplete coordinates.")
points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
obj.add_polygon(points)
obj.plot()

View File

@@ -0,0 +1,61 @@
from ObjectCollection import *
import TclCommand
class TclCommandAddPolyline(TclCommand.TclCommandSignaled):
"""
Tcl shell command to create a polyline in the given Geometry object
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['add_polyline']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict()
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Creates a polyline in the given Geometry object.",
'args': collections.OrderedDict([
('name', 'Name of the Geometry object to which to append the polyline.'),
('xi, yi', 'Coordinates of points in the polyline.')
]),
'examples': [
'add_polyline <name> <x0> <y0> <x1> <y1> <x2> <y2> [x3 y3 [...]]'
]
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, Geometry):
self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
if len(unnamed_args) % 2 != 0:
self.raise_tcl_error("Incomplete coordinates.")
points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
obj.add_polyline(points)
obj.plot()

View File

@@ -0,0 +1,80 @@
from ObjectCollection import *
import TclCommand
class TclCommandCncjob(TclCommand.TclCommandSignaled):
"""
Tcl shell command to Generates a CNC Job from a Geometry Object.
example:
set_sys units MM
new
open_gerber tests/gerber_files/simple1.gbr -outname margin
isolate margin -dia 3
cncjob margin_iso
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['cncjob']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('z_cut',float),
('z_move',float),
('feedrate',float),
('tooldia',float),
('spindlespeed',int),
('multidepth',bool),
('depthperpass',float),
('outname',str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Generates a CNC Job from a Geometry Object.",
'args': collections.OrderedDict([
('name', 'Name of the source object.'),
('z_cut', 'Z-axis cutting position.'),
('z_move', 'Z-axis moving position.'),
('feedrate', 'Moving speed when cutting.'),
('tooldia', 'Tool diameter to show on screen.'),
('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
('multidepth', 'Use or not multidepth cnccut.'),
('depthperpass', 'Height of one layer for multidepth.'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' not in args:
args['outname'] = name + "_cnc"
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, FlatCAMGeometry):
self.raise_tcl_error('Expected FlatCAMGeometry, got %s %s.' % (name, type(obj)))
del args['name']
obj.generatecncjob(use_thread = False, **args)

View File

@@ -0,0 +1,81 @@
from ObjectCollection import *
import TclCommand
class TclCommandDrillcncjob(TclCommand.TclCommandSignaled):
"""
Tcl shell command to Generates a Drill CNC Job from a Excellon Object.
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['drillcncjob']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('tools',str),
('drillz',float),
('travelz',float),
('feedrate',float),
('spindlespeed',int),
('toolchange',bool),
('outname',str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Generates a Drill CNC Job from a Excellon Object.",
'args': collections.OrderedDict([
('name', 'Name of the source object.'),
('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'),
('drillz', 'Drill depth into material (example: -2.0).'),
('travelz', 'Travel distance above material (example: 2.0).'),
('feedrate', 'Drilling feed rate.'),
('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
('toolchange', 'Enable tool changes (example: True).'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' not in args:
args['outname'] = name + "_cnc"
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, FlatCAMExcellon):
self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj)))
def job_init(job_obj, app):
job_obj.z_cut = args["drillz"]
job_obj.z_move = args["travelz"]
job_obj.feedrate = args["feedrate"]
job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
tools = args["tools"] if "tools" in args else 'all'
job_obj.generate_from_excellon_by_tool(obj, tools, toolchange)
job_obj.gcode_parse()
job_obj.create_geometry()
self.app.new_object("cncjob", args['outname'], job_init)

View File

@@ -0,0 +1,79 @@
from ObjectCollection import *
import TclCommand
class TclCommandExportGcode(TclCommand.TclCommandSignaled):
"""
Tcl shell command to export gcode as tcl output for "set X [export_gcode ...]"
Requires name to be available. It might still be in the
making at the time this function is called, so check for
promises and send to background if there are promises.
this export may be catched by tcl and past as preable to another export_gcode or write_gcode
this can be used to join GCODES
example:
set_sys units MM
new
open_gerber tests/gerber_files/simple1.gbr -outname margin
isolate margin -dia 3
cncjob margin_iso
cncjob margin_iso
set EXPORT [export_gcode margin_iso_cnc]
write_gcode margin_iso_cnc_1 /tmp/file.gcode ${EXPORT}
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['export_gcode']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str),
('preamble', str),
('postamble', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict()
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Export gcode into console output.",
'args': collections.OrderedDict([
('name', 'Name of the source Geometry object.'),
('preamble', 'Prepend GCODE.'),
('postamble', 'Append GCODE.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, CNCjob):
self.raise_tcl_error('Expected CNCjob, got %s %s.' % (name, type(obj)))
if self.app.collection.has_promises():
self.raise_tcl_error('!!!Promises exists, but should not here!!!')
del args['name']
return obj.get_gcode(**args)

View File

@@ -0,0 +1,64 @@
from ObjectCollection import *
import TclCommand
class TclCommandExteriors(TclCommand.TclCommandSignaled):
"""
Tcl shell command to get exteriors of polygons
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['exteriors', 'ext']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('outname', str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Get exteriors of polygons.",
'args': collections.OrderedDict([
('name', 'Name of the source Geometry object.'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' in args:
outname = args['outname']
else:
outname = name + "_exteriors"
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, Geometry):
self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
def geo_init(geo_obj, app_obj):
geo_obj.solid_geometry = obj_exteriors
obj_exteriors = obj.get_exteriors()
self.app.new_object('geometry', outname, geo_init)

View File

@@ -0,0 +1,81 @@
from ObjectCollection import *
import TclCommand
class TclCommandImportSvg(TclCommand.TclCommandSignaled):
"""
Tcl shell command to import an SVG file as a Geometry Object.
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['import_svg']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('filename', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('type', str),
('outname', str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['filename']
# structured help for current command, args needs to be ordered
help = {
'main': "Import an SVG file as a Geometry Object..",
'args': collections.OrderedDict([
('filename', 'Path to file to open.'),
('type', 'Import as gerber or geometry(default).'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
# How the object should be initialized
def obj_init(geo_obj, app_obj):
if not isinstance(geo_obj, Geometry):
self.raise_tcl_error('Expected Geometry or Gerber, got %s %s.' % (outname, type(geo_obj)))
geo_obj.import_svg(filename)
filename = args['filename']
if 'outname' in args:
outname = args['outname']
else:
outname = filename.split('/')[-1].split('\\')[-1]
if 'type' in args:
obj_type = args['type']
else:
obj_type = 'geometry'
if obj_type != "geometry" and obj_type != "gerber":
self.raise_tcl_error("Option type can be 'geopmetry' or 'gerber' only, got '%s'." % obj_type)
with self.app.proc_container.new("Import SVG"):
# Object creation
self.app.new_object(obj_type, outname, obj_init)
# Register recent file
self.app.file_opened.emit("svg", filename)
# GUI feedback
self.app.inform.emit("Opened: " + filename)

View File

@@ -0,0 +1,64 @@
from ObjectCollection import *
import TclCommand
class TclCommandInteriors(TclCommand.TclCommandSignaled):
"""
Tcl shell command to get interiors of polygons
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['interiors']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('outname', str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Get interiors of polygons.",
'args': collections.OrderedDict([
('name', 'Name of the source Geometry object.'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' in args:
outname = args['outname']
else:
outname = name + "_interiors"
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, Geometry):
self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
def geo_init(geo_obj, app_obj):
geo_obj.solid_geometry = obj_exteriors
obj_exteriors = obj.get_interiors()
self.app.new_object('geometry', outname, geo_init)

View File

@@ -0,0 +1,79 @@
from ObjectCollection import *
import TclCommand
class TclCommandIsolate(TclCommand.TclCommandSignaled):
"""
Tcl shell command to Creates isolation routing geometry for the given Gerber.
example:
set_sys units MM
new
open_gerber tests/gerber_files/simple1.gbr -outname margin
isolate margin -dia 3
cncjob margin_iso
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['isolate']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('dia',float),
('passes',int),
('overlap',float),
('combine',int),
('outname',str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Creates isolation routing geometry for the given Gerber.",
'args': collections.OrderedDict([
('name', 'Name of the source object.'),
('dia', 'Tool diameter.'),
('passes', 'Passes of tool width.'),
('overlap', 'Fraction of tool diameter to overlap passes.'),
('combine', 'Combine all passes into one geometry.'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' not in args:
args['outname'] = name + "_iso"
if 'timeout' in args:
timeout = args['timeout']
else:
timeout = 10000
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, FlatCAMGerber):
self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj)))
del args['name']
obj.isolate(**args)

View File

@@ -0,0 +1,40 @@
from ObjectCollection import *
from PyQt4 import QtCore
import TclCommand
class TclCommandNew(TclCommand.TclCommand):
"""
Tcl shell command to starts a new project. Clears objects from memory
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['new']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict()
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict()
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = []
# structured help for current command, args needs to be ordered
help = {
'main': "Starts a new project. Clears objects from memory.",
'args': collections.OrderedDict(),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
self.app.on_file_new()

View File

@@ -0,0 +1,95 @@
from ObjectCollection import *
import TclCommand
class TclCommandOpenGerber(TclCommand.TclCommandSignaled):
"""
Tcl shell command to opens a Gerber file
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['open_gerber']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('filename', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('follow', str),
('outname', str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['filename']
# structured help for current command, args needs to be ordered
help = {
'main': "Opens a Gerber file.",
'args': collections.OrderedDict([
('filename', 'Path to file to open.'),
('follow', 'N If 1, does not create polygons, just follows the gerber path.'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
# How the object should be initialized
def obj_init(gerber_obj, app_obj):
if not isinstance(gerber_obj, Geometry):
self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (outname, type(gerber_obj)))
# Opening the file happens here
self.app.progress.emit(30)
try:
gerber_obj.parse_file(filename, follow=follow)
except IOError:
app_obj.inform.emit("[error] Failed to open file: %s " % filename)
app_obj.progress.emit(0)
self.raise_tcl_error('Failed to open file: %s' % filename)
except ParseError, e:
app_obj.inform.emit("[error] Failed to parse file: %s, %s " % (filename, str(e)))
app_obj.progress.emit(0)
self.log.error(str(e))
raise
# Further parsing
app_obj.progress.emit(70)
filename = args['filename']
if 'outname' in args:
outname = args['outname']
else:
outname = filename.split('/')[-1].split('\\')[-1]
follow = None
if 'follow' in args:
follow = args['follow']
with self.app.proc_container.new("Opening Gerber"):
# Object creation
self.app.new_object("gerber", outname, obj_init)
# Register recent file
self.app.file_opened.emit("gerber", filename)
self.app.progress.emit(100)
# GUI feedback
self.app.inform.emit("Opened: " + filename)

54
tclCommands/__init__.py Normal file
View File

@@ -0,0 +1,54 @@
import pkgutil
import sys
# allowed command modules (please append them alphabetically ordered)
import tclCommands.TclCommandAddPolygon
import tclCommands.TclCommandAddPolyline
import tclCommands.TclCommandCncjob
import tclCommands.TclCommandDrillcncjob
import tclCommands.TclCommandExportGcode
import tclCommands.TclCommandExteriors
import tclCommands.TclCommandImportSvg
import tclCommands.TclCommandInteriors
import tclCommands.TclCommandIsolate
import tclCommands.TclCommandNew
import tclCommands.TclCommandOpenGerber
__all__ = []
for loader, name, is_pkg in pkgutil.walk_packages(__path__):
module = loader.find_module(name).load_module(name)
__all__.append(name)
def register_all_commands(app, commands):
"""
Static method which register all known commands.
Command should be for now in directory tclCommands and module should start with TCLCommand
Class have to follow same name as module.
we need import all modules in top section:
import tclCommands.TclCommandExteriors
at this stage we can include only wanted commands with this, auto loading may be implemented in future
I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc.
:param app: FlatCAMApp
:param commands: array of commands which should be modified
:return: None
"""
tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')}
for key, mod in tcl_modules.items():
if key != 'tclCommands.TclCommand':
class_name = key.split('.')[1]
class_type = getattr(mod, class_name)
command_instance = class_type(app)
for alias in command_instance.aliases:
commands[alias] = {
'fcn': command_instance.execute_wrapper,
'help': command_instance.get_decorated_help()
}

View File

@@ -4,8 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history.
"""
import cgi
from PyQt4.QtCore import pyqtSignal
from PyQt4.QtCore import pyqtSignal, Qt
from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
QSizePolicy, QTextCursor, QTextEdit, \
QVBoxLayout, QWidget
@@ -19,13 +18,13 @@ class _ExpandableTextEdit(QTextEdit):
historyNext = pyqtSignal()
historyPrev = pyqtSignal()
def __init__(self, termWidget, *args):
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._termWidget = termwidget
def sizeHint(self):
"""
@@ -39,10 +38,10 @@ class _ExpandableTextEdit(QTextEdit):
"""
Update widget height to fit all text
"""
documentSize = self.document().size().toSize()
self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height())
documentsize = self.document().size().toSize()
self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
self.setMaximumHeight(self._fittedHeight)
self.updateGeometry();
self.updateGeometry()
def keyPressEvent(self, event):
"""
@@ -55,30 +54,33 @@ class _ExpandableTextEdit(QTextEdit):
return
elif event.matches(QKeySequence.MoveToNextLine):
text = self.toPlainText()
cursorPos = self.textCursor().position()
textBeforeEnd = text[cursorPos:]
cursor_pos = self.textCursor().position()
textBeforeEnd = text[cursor_pos:]
# if len(textBeforeEnd.splitlines()) <= 1:
if len(textBeforeEnd.split('\n')) <= 1:
self.historyNext.emit()
return
elif event.matches(QKeySequence.MoveToPreviousLine):
text = self.toPlainText()
cursorPos = self.textCursor().position()
textBeforeStart = text[:cursorPos]
cursor_pos = self.textCursor().position()
text_before_start = text[:cursor_pos]
# lineCount = len(textBeforeStart.splitlines())
lineCount = len(textBeforeStart.split('\n'))
if len(textBeforeStart) > 0 and \
(textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'):
lineCount += 1
if lineCount <= 1:
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):
event.matches(QKeySequence.MoveToPreviousPage):
return self._termWidget.browser().keyPressEvent(event)
QTextEdit.keyPressEvent(self, event)
def insertFromMimeData(self, mime_data):
# Paste only plain text.
self.insertPlainText(mime_data.text())
class TermWidget(QWidget):
"""
@@ -94,8 +96,9 @@ class TermWidget(QWidget):
self._browser = QTextEdit(self)
self._browser.setStyleSheet("font: 9pt \"Courier\";")
self._browser.setReadOnly(True)
self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() +
"span {white-space:pre;}")
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)
@@ -113,6 +116,34 @@ class TermWidget(QWidget):
self._edit.setFocus()
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)
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)
def _append_to_browser(self, style, text):
"""
Convert text to HTML for inserting it to browser
@@ -120,30 +151,12 @@ class TermWidget(QWidget):
assert style in ('in', 'out', 'err')
text = cgi.escape(text)
text = text.replace('\n', '<br/>')
if style != 'out':
def_bg = self._browser.palette().color(QPalette.Base)
h, s, v, a = def_bg.getHsvF()
if style == 'in':
if v > 0.5: # white background
v = v - (v / 8) # make darker
else:
v = v + ((1 - v) / 4) # make ligher
else: # err
if v < 0.5:
v = v + ((1 - v) / 4) # make ligher
if h == -1: # make red
h = 0
s = .4
else:
h = h + ((1 - h) * 0.5) # make more red
bg = QColor.fromHsvF(h, s, v).name()
text = '<span style="background-color: %s; font-weight: bold;">%s</span>' % (str(bg), text)
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!!!
@@ -238,4 +251,3 @@ class TermWidget(QWidget):
self._historyIndex -= 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)

View File

@@ -0,0 +1,95 @@
from __future__ import division
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import cStringIO
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
import cProfile
import sys
def gen_data():
N = 100000
x = np.random.rand(N) * 10
y = np.random.rand(N) * 10
colors = np.random.rand(N)
area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radiuses
data = x, y, area, colors
return data
# @profile
def large_plot(data):
x, y, area, colors = data
fig = Figure(figsize=(10, 10), dpi=80)
axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
axes.set_frame_on(False)
axes.set_xticks([])
axes.set_yticks([])
# axes.set_xlim(0, 10)
# axes.set_ylim(0, 10)
axes.scatter(x, y, s=area, c=colors, alpha=0.5)
axes.set_xlim(0, 10)
axes.set_ylim(0, 10)
canvas = FigureCanvasAgg(fig)
canvas.draw()
# canvas = FigureCanvasQTAgg(fig)
# buf = canvas.tostring_rgb()
buf = fig.canvas.tostring_rgb()
ncols, nrows = fig.canvas.get_width_height()
img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
return img
def small_plot(data):
x, y, area, colors = data
fig = Figure(figsize=(3, 3), dpi=80)
axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
axes.set_frame_on(False)
axes.set_xticks([])
axes.set_yticks([])
# axes.set_xlim(5, 6)
# axes.set_ylim(5, 6)
axes.scatter(x, y, s=area, c=colors, alpha=0.5)
axes.set_xlim(4, 7)
axes.set_ylim(4, 7)
canvas = FigureCanvasAgg(fig)
canvas.draw()
# canvas = FigureCanvasQTAgg(fig)
# buf = canvas.tostring_rgb()
buf = fig.canvas.tostring_rgb()
ncols, nrows = fig.canvas.get_width_height()
img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
return img
def doit():
d = gen_data()
img = large_plot(d)
return img
if __name__ == "__main__":
d = gen_data()
if sys.argv[1] == 'large':
cProfile.runctx('large_plot(d)', None, locals())
else:
cProfile.runctx('small_plot(d)', None, locals())

6
tests/canvas/prof.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
echo "*** LARGE ***"
python performance.py large | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)"
echo "*** SMALL ***"
python performance.py small | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)"

View File

@@ -0,0 +1,125 @@
M48
INCH
T01C0.0200
T02C0.0800
T03C0.0600
T04C0.0300
T05C0.0650
T06C0.0450
T07C0.0400
T08C0.1181
T09C0.0500
%
T01
X-018204Y+015551
X-025842Y+015551
T02
X-000118Y+020629
X-000118Y+016889
X+012401Y+020629
X+012401Y+016889
X-010170Y+002440
X-010110Y+011470
X+018503Y+026574
T03
X+013060Y+010438
X+013110Y+000000
X-049015Y+002165
X+018378Y+010433
X+018317Y+000000
X-049015Y+010039
X-041141Y-000629
X-041181Y+012992
X-056496Y+012992
X-056496Y-000590
T04
X-037560Y+030490
X-036560Y+030490
X-035560Y+030490
X-034560Y+030490
X-033560Y+030490
X-032560Y+030490
X-031560Y+030490
X-030560Y+030490
X-029560Y+030490
X-028560Y+030490
X-027560Y+030490
X-026560Y+030490
X-025560Y+030490
X-024560Y+030490
X-024560Y+036490
X-025560Y+036490
X-026560Y+036490
X-027560Y+036490
X-028560Y+036490
X-029560Y+036490
X-030560Y+036490
X-031560Y+036490
X-032560Y+036490
X-033560Y+036490
X-034560Y+036490
X-035560Y+036490
X-036560Y+036490
X-037560Y+036490
X-014590Y+030810
X-013590Y+030810
X-012590Y+030810
X-011590Y+030810
X-011590Y+033810
X-012590Y+033810
X-013590Y+033810
X-014590Y+033810
X-021260Y+034680
X-020010Y+034680
X-008390Y+035840
X-008390Y+034590
X-008440Y+031870
X-008440Y+030620
T05
X-022504Y+019291
X-020354Y+019291
X-018204Y+019291
X-030142Y+019291
X-027992Y+019291
X-025842Y+019291
X-012779Y+019291
X-010629Y+019291
X-008479Y+019291
T06
X-028080Y+028230
X-030080Y+028230
X-034616Y+024409
X-039616Y+024409
X-045364Y+023346
X-045364Y+018346
X-045364Y+030157
X-045364Y+025157
X-008604Y+026983
X-013604Y+026983
X-016844Y+034107
X-016844Y+029107
T07
X-041655Y+026456
X-040655Y+026456
X-039655Y+026456
X-041640Y+022047
X-040640Y+022047
X-039640Y+022047
X-049760Y+029430
X-048760Y+029430
X-047760Y+029430
X-019220Y+037380
X-020220Y+037380
X-021220Y+037380
T08
X-024212Y+007751
X-024212Y+004011
X-035629Y+007874
X-035629Y+004133
T09
X+007086Y+030708
X+007086Y+032874
X-000787Y+031889
X-000787Y+035826
X-000787Y+027952
M30

View File

@@ -0,0 +1,26 @@
G04 MADE WITH FRITZING*
G04 WWW.FRITZING.ORG*
G04 DOUBLE SIDED*
G04 HOLES PLATED*
G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
%ASAXBY*%
%FSLAX23Y23*%
%MOIN*%
%OFA0B0*%
%SFA1.0B1.0*%
%ADD10R,1.771650X1.181100*%
%ADD11C,0.008000*%
%ADD10C,0.008*%
%LNCONTOUR*%
G90*
G70*
G54D10*
G54D11*
X4Y1177D02*
X1768Y1177D01*
X1768Y4D01*
X4Y4D01*
X4Y1177D01*
D02*
G04 End of contour*
M02*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
G04 MADE WITH FRITZING*
G04 WWW.FRITZING.ORG*
G04 DOUBLE SIDED*
G04 HOLES PLATED*
G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
%ASAXBY*%
%FSLAX23Y23*%
%MOIN*%
%OFA0B0*%
%SFA1.0B1.0*%
%ADD10C,0.075000*%
%ADD11C,0.099055*%
%ADD12C,0.078740*%
%ADD13R,0.075000X0.075000*%
%ADD14C,0.024000*%
%ADD15C,0.020000*%
%LNCOPPER1*%
G90*
G70*
G54D10*
X1149Y872D03*
X1349Y872D03*
X749Y722D03*
X749Y522D03*
X1149Y522D03*
X1449Y522D03*
X1149Y422D03*
X1449Y422D03*
X1149Y322D03*
X1449Y322D03*
X1149Y222D03*
X1449Y222D03*
X949Y472D03*
X949Y72D03*
G54D11*
X749Y972D03*
X599Y972D03*
X349Y322D03*
X349Y472D03*
X349Y672D03*
X349Y822D03*
G54D10*
X699Y122D03*
X699Y322D03*
G54D12*
X699Y222D03*
X949Y972D03*
X749Y622D03*
X1049Y222D03*
X1249Y872D03*
G54D13*
X1149Y872D03*
X1149Y522D03*
G54D14*
X952Y946D02*
X1045Y249D01*
G54D15*
X776Y695D02*
X721Y695D01*
X721Y750D01*
X776Y750D01*
X776Y695D01*
D02*
X671Y150D02*
X726Y150D01*
X726Y95D01*
X671Y95D01*
X671Y150D01*
D02*
G04 End of Copper1*
M02*

View File

@@ -0,0 +1,46 @@
; NON-PLATED HOLES START AT T1
; THROUGH (PLATED) HOLES START AT T100
M48
INCH
T1C0.125984
T100C0.031496
T101C0.035000
T102C0.059055
%
T1
X001488Y010223
X001488Y001223
X016488Y001223
X016488Y010223
T100
X009488Y009723
X007488Y006223
X012488Y008723
X010488Y002223
X006988Y002223
T101
X014488Y004223
X006988Y003223
X013488Y008723
X011488Y008723
X007488Y005223
X014488Y003223
X014488Y002223
X011488Y005223
X009488Y000723
X011488Y004223
X006988Y001223
X009488Y004723
X007488Y007223
X011488Y003223
X014488Y005223
X011488Y002223
T102
X003488Y008223
X003488Y004723
X007488Y009723
X003488Y006723
X005988Y009723
X003488Y003223
T00
M30

View File

@@ -0,0 +1,34 @@
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE svg>
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In -->
<svg xmlns="http://www.w3.org/2000/svg" width="0.402778in" xml:space="preserve" xmlns:xml="http://www.w3.org/XML/1998/namespace" x="0px" version="1.2" y="0px" height="0.527778in" viewBox="0 0 29 38" baseProfile="tiny">
<g id="copper0">
<g id="copper1">
<rect x="1.362" y="2.091" fill="none" stroke="#F7BD13" stroke-width="1.224" width="4.104" height="4.104"/>
<circle fill="none" cx="3.362" cy="4.091" stroke="#F7BD13" id="connector1pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="11.29" stroke="#F7BD13" id="connector2pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="18.491" stroke="#F7BD13" id="connector3pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="25.69" stroke="#F7BD13" id="connector4pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.365" cy="32.81" stroke="#F7BD13" id="connector5pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="25.001" cy="32.821" stroke="#F7BD13" id="connector6pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="25.702" stroke="#F7BD13" id="connector7pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="18.503" stroke="#F7BD13" id="connector8pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="11.302" stroke="#F7BD13" id="connector9pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="25.029" cy="4.077" stroke="#F7BD13" id="connector10pin" r="2.052" stroke-width="1.224"/>
</g>
<circle fill="none" cx="3.362" cy="4.091" stroke="#F7BD13" id="connector1pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="11.29" stroke="#F7BD13" id="connector2pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="18.491" stroke="#F7BD13" id="connector3pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.362" cy="25.69" stroke="#F7BD13" id="connector4pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="3.365" cy="32.81" stroke="#F7BD13" id="connector5pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="25.001" cy="32.821" stroke="#F7BD13" id="connector6pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="25.702" stroke="#F7BD13" id="connector7pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="18.503" stroke="#F7BD13" id="connector8pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="24.999" cy="11.302" stroke="#F7BD13" id="connector9pin" r="2.052" stroke-width="1.224"/>
<circle fill="none" cx="25.029" cy="4.077" stroke="#F7BD13" id="connector10pin" r="2.052" stroke-width="1.224"/>
</g>
<g id="silkscreen">
<rect width="28.347" x="0.024" y="0.024" height="36.851" fill="none" stroke="#FFFFFF" stroke-width="0.7087"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,468 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="50.4px" height="122.448px" viewBox="0 0 50.4 122.448" enable-background="new 0 0 50.4 122.448" xml:space="preserve">
<desc>Fritzing footprint generated by brd2svg</desc>
<g id="silkscreen">
<path fill="none" stroke="#FFFFFF" stroke-width="0.576" d="M0.288,122.136h49.824V0.288H0.288V122.136"/>
<g>
<title>element:J1</title>
<g>
<title>package:HEAD15-NOSS</title>
</g>
</g>
<g>
<title>element:J2</title>
<g>
<title>package:HEAD15-NOSS-1</title>
</g>
</g>
<g>
<title>element:U2</title>
<g>
<title>package:SSOP28</title>
</g>
</g>
<g>
<title>element:U3</title>
<g>
<title>package:SOT223</title>
</g>
</g>
</g>
<g id="copper1">
<g id="copper0">
<circle id="connector16pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="10.8" r="1.908"/>
<rect x="1.692" y="8.892" fill="none" stroke="#F7BD13" stroke-width="1.224" width="3.814" height="3.816"/>
<circle id="connector17pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="18" r="1.908"/>
<circle id="connector18pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="25.2" r="1.908"/>
<circle id="connector19pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="32.4" r="1.908"/>
<circle id="connector20pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="39.6" r="1.908"/>
<circle id="connector21pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="46.8" r="1.908"/>
<circle id="connector22pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="54" r="1.908"/>
<circle id="connector23pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="61.2" r="1.908"/>
<circle id="connector24pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="68.4" r="1.908"/>
<circle id="connector25pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="75.6" r="1.908"/>
<circle id="connector26pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="82.8" r="1.908"/>
<circle id="connector27pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="90" r="1.908"/>
<circle id="connector28pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="97.2" r="1.908"/>
<circle id="connector29pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="104.4" r="1.908"/>
<circle id="connector30pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="3.6" cy="111.6" r="1.908"/>
<circle id="connector31pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="10.8" r="1.908"/>
<circle id="connector32pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="18" r="1.908"/>
<circle id="connector33pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="25.2" r="1.908"/>
<circle id="connector34pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="32.4" r="1.908"/>
<circle id="connector35pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="39.6" r="1.908"/>
<circle id="connector36pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="46.8" r="1.908"/>
<circle id="connector37pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="54" r="1.908"/>
<circle id="connector38pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="61.2" r="1.908"/>
<circle id="connector39pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="68.4" r="1.908"/>
<circle id="connector40pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="75.6" r="1.908"/>
<circle id="connector41pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="82.8" r="1.908"/>
<circle id="connector42pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="90" r="1.908"/>
<circle id="connector43pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="97.2" r="1.908"/>
<circle id="connector44pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="104.4" r="1.908"/>
<circle id="connector45pad" fill="none" stroke="#F7BD13" stroke-width="1.224" cx="46.8" cy="111.6" r="1.908"/>
</g>
</g>
<g>
<title>layer 21</title>
<g>
<title>text:TX1</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 13.68)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -6.713867e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">TX1</text>
</g>
</g>
</g>
<g>
<title>text:RX0</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 20.88)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 1.220703e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">RX0</text>
</g>
</g>
</g>
<g>
<title>text:RST</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 28.008)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -7.934570e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">RST</text>
</g>
</g>
</g>
<g>
<title>text:GND</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 35.208)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 0 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">GND</text>
</g>
</g>
</g>
<g>
<title>text:D2</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 41.544)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 6.103516e-005 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D2</text>
</g>
</g>
</g>
<g>
<title>text:D3</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 48.6)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -5.798340e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D3</text>
</g>
</g>
</g>
<g>
<title>text:D4</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 55.872)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -6.103516e-005 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D4</text>
</g>
</g>
</g>
<g>
<title>text:D5</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 62.928)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -7.019043e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D5</text>
</g>
</g>
</g>
<g>
<title>text:D6</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 70.272)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -4.577637e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D6</text>
</g>
</g>
</g>
<g>
<title>text:D7</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 77.544)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 6.103516e-005 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D7</text>
</g>
</g>
</g>
<g>
<title>text:D8</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 84.6)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -5.798340e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D8</text>
</g>
</g>
</g>
<g>
<title>text:D9</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 91.872)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -6.103516e-005 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D9</text>
</g>
</g>
</g>
<g>
<title>text:D10</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 100.224)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0016 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D10</text>
</g>
</g>
</g>
<g>
<title>text:D11</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 107.352)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -5.493164e-004 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D11</text>
</g>
</g>
</g>
<g>
<title>text:D12</title>
<g transform="matrix(1, 0, 0, 1, 9.216, 114.552)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0017 -4.272461e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D12</text>
</g>
</g>
</g>
<g>
<title>text:D13</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 114.552)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0017 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">D13</text>
</g>
</g>
</g>
<g>
<title>text:3V3</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 107.28)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -2.746582e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">3V3</text>
</g>
</g>
</g>
<g>
<title>text:REF</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 100.152)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0013 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">REF</text>
</g>
</g>
</g>
<g>
<title>text:A0</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 91.944)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -3.356934e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A0</text>
</g>
</g>
</g>
<g>
<title>text:A1</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 84.672)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -8.544922e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A1</text>
</g>
</g>
</g>
<g>
<title>text:A2</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 77.544)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 6.103516e-005 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A2</text>
</g>
</g>
</g>
<g>
<title>text:A3</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 70.344)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -7.324219e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A3</text>
</g>
</g>
</g>
<g>
<title>text:A4</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 63.216)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 1.831055e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A4</text>
</g>
</g>
</g>
<g>
<title>text:A5</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 55.944)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -3.356934e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A5</text>
</g>
</g>
</g>
<g>
<title>text:A6</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 48.744)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0011 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A6</text>
</g>
</g>
</g>
<g>
<title>text:A7</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 41.616)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -2.136230e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">A7</text>
</g>
</g>
</g>
<g>
<title>text:5V</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 34.416)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -9.765625e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">5V</text>
</g>
</g>
</g>
<g>
<title>text:RST</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 27.216)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 1.831055e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">RST</text>
</g>
</g>
</g>
<g>
<title>text:GND</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 19.8)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 1.831055e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">GND</text>
</g>
</g>
</g>
<g>
<title>text:VIN</title>
<g transform="matrix(1, 0, 0, 1, 43.488, 12.672)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -8.544922e-004 -9.460449e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="2.6726">VIN</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 92.664)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.001 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 99.936)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -5.187988e-004 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 107.064)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0014 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 71.064)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 -0.0014 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 63.792)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 0 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>text:*</title>
<g transform="matrix(1, 0, 0, 1, 7.056, 49.464)">
<g transform="rotate(270)">
<text transform="matrix(1 0 0 1 1.220703e-004 -5.798340e-004)" fill="#FFFFFF" font-family="'OCRA'" font-size="1.6704">*</text>
</g>
</g>
</g>
<g>
<title>element:C1</title>
<g>
<title>package:CAP0805-NP</title>
</g>
</g>
<g>
<title>element:C2</title>
<g>
<title>package:TAN-A</title>
</g>
</g>
<g>
<title>element:C3</title>
<g>
<title>package:CAP0805-NP</title>
</g>
</g>
<g>
<title>element:C4</title>
<g>
<title>package:CAP0805-NP</title>
</g>
</g>
<g>
<title>element:C7</title>
<g>
<title>package:CAP0805-NP</title>
</g>
</g>
<g>
<title>element:C8</title>
<g>
<title>package:TAN-A</title>
</g>
</g>
<g>
<title>element:C9</title>
<g>
<title>package:CAP0805-NP</title>
</g>
</g>
<g>
<title>element:D1</title>
<g>
<title>package:SOD-123</title>
</g>
</g>
<g>
<title>element:J1</title>
<g>
<title>package:HEAD15-NOSS</title>
</g>
</g>
<g>
<title>element:J2</title>
<g>
<title>package:HEAD15-NOSS-1</title>
</g>
</g>
<g>
<title>element:RP1</title>
<g>
<title>package:RES4NT</title>
</g>
</g>
<g>
<title>element:RP2</title>
<g>
<title>package:RES4NT</title>
</g>
</g>
<g>
<title>element:U$4</title>
<g>
<title>package:FIDUCIAL-1X2</title>
</g>
</g>
<g>
<title>element:U$37</title>
<g>
<title>package:FIDUCIAL-1X2</title>
</g>
</g>
<g>
<title>element:U$53</title>
<g>
<title>package:FIDUCIAL-1X2</title>
</g>
</g>
<g>
<title>element:U$54</title>
<g>
<title>package:FIDUCIAL-1X2</title>
</g>
</g>
<g>
<title>element:U2</title>
<g>
<title>package:SSOP28</title>
</g>
</g>
<g>
<title>element:U3</title>
<g>
<title>package:SOT223</title>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

126
tests/svg/drawing.svg Normal file
View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819"
height="1052.3622047"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="drawing.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="436.65332"
inkscape:cy="798.58794"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="968"
inkscape:window-height="759"
inkscape:window-x="1949"
inkscape:window-y="142"
inkscape:window-maximized="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="arc"
style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="path2985"
sodipodi:cx="210.11172"
sodipodi:cy="201.81374"
sodipodi:rx="70.710678"
sodipodi:ry="70.710678"
d="m 280.8224,201.81374 a 70.710678,70.710678 0 1 1 -141.42135,0 70.710678,70.710678 0 1 1 141.42135,0 z" />
<rect
style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect2987"
width="82.832512"
height="72.73098"
x="343.45187"
y="127.06245" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 261.70701,300.10548 137.14286,-68.57143 -13.57143,104.28572 105.71429,2.85714 -232.14286,29.28572 z"
id="path2991"
inkscape:connector-curvature="0" />
<g
id="g3018"
transform="translate(-37.142857,-103.57143)">
<rect
y="222.01678"
x="508.10672"
height="72.73098"
width="82.832512"
id="rect2987-8"
style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
<rect
y="177.86595"
x="534.49329"
height="72.73098"
width="82.832512"
id="rect2987-4"
style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
</g>
<path
style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 550.71875 258.84375 L 550.71875 286 L 513.59375 286 L 513.59375 358.71875 L 596.40625 358.71875 L 596.40625 331.59375 L 633.5625 331.59375 L 633.5625 258.84375 L 550.71875 258.84375 z "
id="rect2987-5" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 276.42857,98.076465 430.71429,83.076464"
id="path3037"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 164.28571,391.64789 c 12.85715,-54.28571 55.00001,21.42858 84.28572,22.85715 29.28571,1.42857 30.71429,-14.28572 30.71429,-14.28572"
id="path3039"
inkscape:connector-curvature="0" />
<g
id="g3018-3"
transform="matrix(0.54511991,0,0,0.54511991,308.96645,74.66094)">
<rect
y="222.01678"
x="508.10672"
height="72.73098"
width="82.832512"
id="rect2987-8-5"
style="fill:none;stroke:#999999;stroke-width:1.28412116;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
<rect
y="177.86595"
x="534.49329"
height="72.73098"
width="82.832512"
id="rect2987-4-6"
style="fill:none;stroke:#999999;stroke-width:1.28412116;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1 Basic//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd'><svg baseProfile="basic" height="0.59in" id="svg" version="1.1" viewBox="0 0 67.502 42.52" width="0.94in" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">
<g id="breadboard">
<rect fill="none" height="5.372" id="connector0pin" width="2.16" x="60.644" y="32.178"/>
<rect fill="none" height="5.309" id="connector1pin" width="2.16" x="53.496" y="5.002"/>
<rect fill="none" height="5.309" id="connector2pin" width="2.16" x="60.644" y="5.002"/>
<rect fill="none" height="5.372" id="connector3pin" width="2.159" x="53.506" y="32.178"/>
<rect fill="none" height="5.372" id="connector5pin" width="2.159" x="53.506" y="32.178"/>
<rect fill="none" height="5.372" id="connector4pin" width="2.159" x="53.506" y="32.178"/>
<rect fill="none" height="3.714" id="connector0terminal" width="2.16" x="60.644" y="33.836"/>
<rect fill="none" height="3.669" id="connector1terminal" width="2.16" x="53.496" y="5.002"/>
<rect fill="none" height="3.669" id="connector2terminal" width="2.16" x="60.644" y="5.002"/>
<rect fill="none" height="3.714" id="connector3terminal" width="2.159" x="53.506" y="33.836"/>
<rect fill="none" height="3.714" id="connector5terminal" width="2.159" x="53.506" y="33.836"/>
<rect fill="none" height="3.714" id="connector4terminal" width="2.159" x="53.506" y="33.836"/>
<g>
<polygon fill="#1F7A34" points="49.528,34.417 67.502,34.417 67.502,8.102 49.528,8.102 49.528,0 20.776,0 20.776,42.52 49.528,42.52 "/>
</g>
<g>
<g>
<path d="M30.783,4.96c0-1.988-1.609-3.598-3.598-3.598c-1.985,0-3.598,1.609-3.598,3.598 c0,1.985,1.612,3.598,3.598,3.598C29.173,8.558,30.783,6.945,30.783,4.96z" fill="#9A916C"/>
</g>
<g>
<circle cx="27.182" cy="4.96" fill="#3A3A3A" r="2.708"/>
</g>
</g>
<g>
<g>
<path d="M30.783,37.56c0-1.988-1.609-3.598-3.598-3.598c-1.985,0-3.598,1.609-3.598,3.598 c0,1.985,1.612,3.598,3.598,3.598C29.173,41.157,30.783,39.545,30.783,37.56z" fill="#9A916C"/>
</g>
<g>
<circle cx="27.182" cy="37.56" fill="#3A3A3A" r="2.708"/>
</g>
</g>
<g>
<rect fill="#898989" height="34.016" width="45.355" x="0.001" y="4.252"/>
</g>
<g>
<rect fill="#DDDDDD" height="0.743" width="45.355" x="0.001" y="4.252"/>
</g>
<g>
<rect fill="#C6C6C6" height="0.889" width="45.355" x="0.001" y="4.991"/>
</g>
<g>
<rect fill="#ADADAD" height="31.342" width="45.356" y="5.88"/>
</g>
<g>
<line fill="#919191" stroke="#4D4D4D" stroke-width="0.1" x1="34.173" x2="34.173" y1="4.252" y2="38.268"/>
</g>
<g>
<rect fill="#8C8C8C" height="5.349" width="4.667" x="52.252" y="4.961"/>
</g>
<g>
<rect fill="#8C8C8C" height="5.349" width="4.668" x="59.418" y="4.961"/>
</g>
<g>
<rect fill="#8C8C8C" height="5.349" width="4.667" x="52.252" y="32.177"/>
</g>
<g>
<rect fill="#8C8C8C" height="5.349" width="4.668" x="59.418" y="32.177"/>
</g>
<g>
<path d="M30.074,21.386l-2.64-1.524v1.134H13.468c0.515-0.416,1.008-0.965,1.493-1.505 c0.802-0.894,1.631-1.819,2.338-1.819h2.277c0.141,0.521,0.597,0.913,1.163,0.913c0.677,0,1.226-0.548,1.226-1.225 s-0.549-1.226-1.226-1.226c-0.566,0-1.022,0.392-1.163,0.914h-2.277c-0.985,0-1.868,0.984-2.803,2.026 c-0.744,0.83-1.509,1.675-2.255,1.922h-1.82c-0.185-1.02-1.073-1.794-2.145-1.794c-1.206,0-2.184,0.978-2.184,2.184 c0,1.207,0.978,2.184,2.184,2.184c1.072,0,1.96-0.774,2.145-1.794h5.196c0.746,0.247,1.511,1.093,2.254,1.922 c0.934,1.043,1.817,2.026,2.802,2.026h2.142v0.985h2.595v-2.595h-2.595v0.985h-2.142c-0.707,0-1.536-0.925-2.337-1.818 c-0.485-0.541-0.978-1.09-1.493-1.506h10.592v1.134L30.074,21.386z" fill="#4D4D4D"/>
</g>
<g>
<polyline fill="none" points="54.586,10.31 54.586,17.006 45.357,17.006 " stroke="#8C8C8C" stroke-width="1"/>
</g>
<g>
<polyline fill="none" points="61.732,10.31 61.732,19.841 45.357,19.841 " stroke="#8C8C8C" stroke-width="1"/>
</g>
<g>
<polyline fill="none" points="54.586,32.177 54.586,25.479 45.357,25.479 " stroke="#8C8C8C" stroke-width="1"/>
</g>
<g>
<polyline fill="none" points="61.732,32.177 61.732,22.646 45.357,22.646 " stroke="#8C8C8C" stroke-width="1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

163
tests/test_excellon_flow.py Normal file
View File

@@ -0,0 +1,163 @@
import unittest
from PyQt4 import QtGui
import sys
from FlatCAMApp import App
from FlatCAMObj import FlatCAMExcellon, FlatCAMCNCjob
from ObjectUI import ExcellonObjectUI
import tempfile
import os
from time import sleep
class ExcellonFlowTestCase(unittest.TestCase):
"""
This is a top-level test covering the Excellon-to-GCode
generation workflow.
THIS IS A REQUIRED TEST FOR ANY UPDATES.
"""
filename = 'case1.drl'
def setUp(self):
self.app = QtGui.QApplication(sys.argv)
# Create App, keep app defaults (do not load
# user-defined defaults).
self.fc = App(user_defaults=False)
self.fc.open_excellon('tests/excellon_files/' + self.filename)
def tearDown(self):
del self.fc
del self.app
def test_flow(self):
# Names of available objects.
names = self.fc.collection.get_names()
print names
#--------------------------------------
# Total of 1 objects.
#--------------------------------------
self.assertEquals(len(names), 1,
"Expected 1 object, found %d" % len(names))
#--------------------------------------
# Object's name matches the file name.
#--------------------------------------
self.assertEquals(names[0], self.filename,
"Expected name == %s, got %s" % (self.filename, names[0]))
#---------------------------------------
# Get object by that name, make sure it's a FlatCAMExcellon.
#---------------------------------------
excellon_name = names[0]
excellon_obj = self.fc.collection.get_by_name(excellon_name)
self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),
"Expected FlatCAMExcellon, instead, %s is %s" %
(excellon_name, type(excellon_obj)))
#----------------------------------------
# Object's GUI matches Object's options
#----------------------------------------
# TODO: Open GUI with double-click on object.
# Opens the Object's GUI, populates it.
excellon_obj.build_ui()
for option, value in excellon_obj.options.iteritems():
try:
form_field = excellon_obj.form_fields[option]
except KeyError:
print ("**********************************************************\n"
"* WARNING: Option '{}' has no form field\n"
"**********************************************************"
"".format(option))
continue
self.assertEqual(value, form_field.get_value(),
"Option '{}' == {} but form has {}".format(
option, value, form_field.get_value()
))
#--------------------------------------------------
# Changes in the GUI should be read in when
# running any process. Changing something here.
#--------------------------------------------------
form_field = excellon_obj.form_fields['feedrate']
value = form_field.get_value()
form_field.set_value(value * 1.1) # Increase by 10%
print "'feedrate' == {}".format(value)
#--------------------------------------------------
# Create GCode using all tools.
#--------------------------------------------------
assert isinstance(excellon_obj, FlatCAMExcellon) # Just for the IDE
ui = excellon_obj.ui
assert isinstance(ui, ExcellonObjectUI)
ui.tools_table.selectAll() # Select All
ui.generate_cnc_button.click() # Click
# Work is done in a separate thread and results are
# passed via events to the main event loop which is
# not running. Run only for pending events.
#
# I'm not sure why, but running it only once does
# not catch the new object. Might be a timing issue.
# http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details
for _ in range(2):
sleep(0.1)
self.app.processEvents()
#---------------------------------------------
# Check that GUI has been read in.
#---------------------------------------------
value = excellon_obj.options['feedrate']
form_value = form_field.get_value()
self.assertEqual(value, form_value,
"Form value for '{}' == {} was not read into options"
"which has {}".format('feedrate', form_value, value))
print "'feedrate' == {}".format(value)
#---------------------------------------------
# Check that only 1 object has been created.
#---------------------------------------------
names = self.fc.collection.get_names()
self.assertEqual(len(names), 2,
"Expected 2 objects, found %d" % len(names))
#-------------------------------------------------------
# Make sure the CNCJob Object has the correct name
#-------------------------------------------------------
cncjob_name = excellon_name + "_cnc"
self.assertTrue(cncjob_name in names,
"Object named %s not found." % cncjob_name)
#-------------------------------------------------------
# Get the object make sure it's a cncjob object
#-------------------------------------------------------
cncjob_obj = self.fc.collection.get_by_name(cncjob_name)
self.assertTrue(isinstance(cncjob_obj, FlatCAMCNCjob),
"Expected a FlatCAMCNCjob, got %s" % type(cncjob_obj))
#-----------------------------------------
# Export G-Code, check output
#-----------------------------------------
assert isinstance(cncjob_obj, FlatCAMCNCjob) # For IDE
# get system temporary file(try create it and delete)
with tempfile.NamedTemporaryFile(prefix='unittest.',
suffix="." + cncjob_name + '.gcode',
delete=True) as tmp_file:
output_filename = tmp_file.name
cncjob_obj.export_gcode(output_filename)
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
print names

View File

@@ -1,14 +1,22 @@
import sys
import unittest
from PyQt4 import QtGui
from FlatCAMApp import App
from FlatCAMApp import App, tclCommands
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob
from ObjectUI import GerberObjectUI, GeometryObjectUI
from time import sleep
import os
import tempfile
class GerberFlowTestCase(unittest.TestCase):
"""
This is a top-level test covering the Gerber-to-GCode
generation workflow.
THIS IS A REQUIRED TEST FOR ANY UPDATES.
"""
filename = 'simple1.gbr'
@@ -51,6 +59,36 @@ class GerberFlowTestCase(unittest.TestCase):
"Expected FlatCAMGerber, instead, %s is %s" %
(gerber_name, type(gerber_obj)))
#----------------------------------------
# Object's GUI matches Object's options
#----------------------------------------
# TODO: Open GUI with double-click on object.
# Opens the Object's GUI, populates it.
gerber_obj.build_ui()
for option, value in gerber_obj.options.iteritems():
try:
form_field = gerber_obj.form_fields[option]
except KeyError:
print ("**********************************************************\n"
"* WARNING: Option '{}' has no form field\n"
"**********************************************************"
"".format(option))
continue
self.assertEqual(value, form_field.get_value(),
"Option '{}' == {} but form has {}".format(
option, value, form_field.get_value()
))
#--------------------------------------------------
# Changes in the GUI should be read in when
# running any process. Changing something here.
#--------------------------------------------------
form_field = gerber_obj.form_fields['isotooldia']
value = form_field.get_value()
form_field.set_value(value * 1.1) # Increase by 10%
print "'isotooldia' == {}".format(value)
#--------------------------------------------------
# Create isolation routing using default values
# and by clicking on the button.
@@ -58,11 +96,22 @@ class GerberFlowTestCase(unittest.TestCase):
# Get the object's GUI and click on "Generate Geometry" under
# "Isolation Routing"
assert isinstance(gerber_obj, FlatCAMGerber) # Just for the IDE
gerber_obj.build_ui() # Open the object's UI.
# Changed: UI has been build already
#gerber_obj.build_ui() # Open the object's UI.
ui = gerber_obj.ui
assert isinstance(ui, GerberObjectUI)
ui.generate_iso_button.click() # Click
#---------------------------------------------
# Check that GUI has been read in.
#---------------------------------------------
value = gerber_obj.options['isotooldia']
form_value = form_field.get_value()
self.assertEqual(value, form_value,
"Form value for '{}' == {} was not read into options"
"which has {}".format('isotooldia', form_value, value))
print "'isotooldia' == {}".format(value)
#---------------------------------------------
# Check that only 1 object has been created.
#---------------------------------------------
@@ -128,9 +177,14 @@ class GerberFlowTestCase(unittest.TestCase):
# Export G-Code, check output
#-----------------------------------------
assert isinstance(cnc_obj, FlatCAMCNCjob)
output_filename = "tests/tmp/" + cnc_name + ".gcode"
output_filename = ""
# get system temporary file(try create it and delete also)
with tempfile.NamedTemporaryFile(prefix='unittest.',
suffix="." + cnc_name + '.gcode',
delete=True) as tmp_file:
output_filename = tmp_file.name
cnc_obj.export_gcode(output_filename)
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
print names
print names

129
tests/test_svg_flow.py Normal file
View File

@@ -0,0 +1,129 @@
import sys
import unittest
from PyQt4 import QtGui
from FlatCAMApp import App
from FlatCAMObj import FlatCAMGeometry, FlatCAMCNCjob
from ObjectUI import GerberObjectUI, GeometryObjectUI
from time import sleep
import os
import tempfile
class SVGFlowTestCase(unittest.TestCase):
def setUp(self):
self.app = QtGui.QApplication(sys.argv)
# Create App, keep app defaults (do not load
# user-defined defaults).
self.fc = App(user_defaults=False)
self.filename = 'drawing.svg'
def tearDown(self):
del self.fc
del self.app
def test_flow(self):
self.fc.import_svg('tests/svg/' + self.filename)
names = self.fc.collection.get_names()
print names
#--------------------------------------
# Total of 1 objects.
#--------------------------------------
self.assertEquals(len(names), 1,
"Expected 1 object, found %d" % len(names))
#--------------------------------------
# Object's name matches the file name.
#--------------------------------------
self.assertEquals(names[0], self.filename,
"Expected name == %s, got %s" % (self.filename, names[0]))
#---------------------------------------
# Get object by that name, make sure it's a FlatCAMGerber.
#---------------------------------------
geo_name = names[0]
geo_obj = self.fc.collection.get_by_name(geo_name)
self.assertTrue(isinstance(geo_obj, FlatCAMGeometry),
"Expected FlatCAMGeometry, instead, %s is %s" %
(geo_name, type(geo_obj)))
#----------------------------------------
# Object's GUI matches Object's options
#----------------------------------------
# TODO: Open GUI with double-click on object.
# Opens the Object's GUI, populates it.
geo_obj.build_ui()
for option, value in geo_obj.options.iteritems():
try:
form_field = geo_obj.form_fields[option]
except KeyError:
print ("**********************************************************\n"
"* WARNING: Option '{}' has no form field\n"
"**********************************************************"
"".format(option))
continue
self.assertEqual(value, form_field.get_value(),
"Option '{}' == {} but form has {}".format(
option, value, form_field.get_value()
))
#------------------------------------
# Open the UI, make CNCObject
#------------------------------------
geo_obj.build_ui()
ui = geo_obj.ui
assert isinstance(ui, GeometryObjectUI) # Just for the IDE
ui.generate_cnc_button.click() # Click
# Work is done in a separate thread and results are
# passed via events to the main event loop which is
# not running. Run only for pending events.
#
# I'm not sure why, but running it only once does
# not catch the new object. Might be a timing issue.
# http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details
for _ in range(2):
sleep(0.1)
self.app.processEvents()
#---------------------------------------------
# Check that only 1 object has been created.
#---------------------------------------------
names = self.fc.collection.get_names()
self.assertEqual(len(names), 2,
"Expected 2 objects, found %d" % len(names))
#-------------------------------------------------------
# Make sure the CNC Job Object has the correct name
#-------------------------------------------------------
cnc_name = geo_name + "_cnc"
self.assertTrue(cnc_name in names,
"Object named %s not found." % geo_name)
#-------------------------------------------------------
# Get the object make sure it's a CNC Job object
#-------------------------------------------------------
cnc_obj = self.fc.collection.get_by_name(cnc_name)
self.assertTrue(isinstance(cnc_obj, FlatCAMCNCjob),
"Expected a FlatCAMCNCJob, got %s" % type(geo_obj))
#-----------------------------------------
# Export G-Code, check output
#-----------------------------------------
assert isinstance(cnc_obj, FlatCAMCNCjob)
output_filename = ""
# get system temporary file(try create it and delete also)
with tempfile.NamedTemporaryFile(prefix='unittest.',
suffix="." + cnc_name + '.gcode',
delete=True) as tmp_file:
output_filename = tmp_file.name
cnc_obj.export_gcode(output_filename)
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
print names

View File

@@ -0,0 +1,18 @@
import pkgutil
import sys
# allowed command tests (please append them alphabetically ordered)
from test_TclCommandAddPolygon import *
from test_TclCommandAddPolyline import *
from test_TclCommandCncjob import *
from test_TclCommandDrillcncjob import *
from test_TclCommandExportGcode import *
from test_TclCommandExteriors import *
from test_TclCommandImportSvg import *
from test_TclCommandInteriors import *
from test_TclCommandIsolate import *
from test_TclCommandNew import *
from test_TclCommandNewGeometry import *
from test_TclCommandOpenExcellon import *
from test_TclCommandOpenGerber import *
from test_TclCommandPaintPolygon import *

View File

@@ -0,0 +1,18 @@
from FlatCAMObj import FlatCAMGeometry
def test_add_polygon(self):
"""
Test add polygon into geometry
:param self:
:return:
"""
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
points = '0 0 20 0 10 10 0 10'
self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points))

View File

@@ -0,0 +1,18 @@
from FlatCAMObj import FlatCAMGeometry
def test_add_polyline(self):
"""
Test add polyline into geometry
:param self:
:return:
"""
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
points = '0 0 20 0 10 10 0 10 33 33'
self.fc.exec_command_test('add_polyline "%s" %s' % (self.geometry_name, points))

View File

@@ -0,0 +1,17 @@
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMObj
from test_TclCommandIsolate import *
def test_cncjob(self):
"""
Test cncjob
:param self:
:return:
"""
# reuse isolate tests
test_isolate(self)
self.fc.exec_command_test('cncjob %s_iso -tooldia 0.5 -z_cut 0.05 -z_move 3 -feedrate 300' % self.gerber_top_name)
cam_top_obj = self.fc.collection.get_by_name(self.gerber_top_name + '_iso_cnc')
self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s"
% (self.gerber_top_name + '_iso_cnc', type(cam_top_obj)))

View File

@@ -0,0 +1,18 @@
from FlatCAMObj import FlatCAMObj
from test_TclCommandOpenExcellon import *
def test_drillcncjob(self):
"""
Test cncjob
:param self:
:return:
"""
# reuse open excellontests
test_open_excellon(self)
self.fc.exec_command_test('drillcncjob %s -tools all -drillz 0.5 -travelz 3 -feedrate 300'
% self.excellon_name)
cam_top_obj = self.fc.collection.get_by_name(self.excellon_name + '_cnc')
self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s"
% (self.excellon_name + '_cnc', type(cam_top_obj)))

View File

@@ -0,0 +1,33 @@
import os
import tempfile
from test_TclCommandCncjob import *
from test_TclCommandDrillcncjob import *
def test_export_gcodecncjob(self):
"""
Test cncjob
:param self:
:return:
"""
# reuse tests
test_cncjob(self)
test_drillcncjob(self)
with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.excellon_name + '.gcode', delete=True)\
as tmp_file:
output_filename = tmp_file.name
self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.excellon_name + '_cnc', output_filename))
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.gerber_top_name + '.gcode', delete=True)\
as tmp_file:
output_filename = tmp_file.name
self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.gerber_top_name + '_iso_cnc', output_filename))
self.assertTrue(os.path.isfile(output_filename))
os.remove(output_filename)
# TODO check what is inside files , it should be same every time

View File

@@ -0,0 +1,24 @@
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
def test_exteriors(self):
"""
Test exteriors
:param self:
:return:
"""
self.fc.exec_command_test('open_gerber %s/%s -outname %s'
% (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s"
% (self.gerber_cutout_name, type(gerber_cutout_obj)))
# exteriors interiors and delete isolated traces
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
self.fc.exec_command_test('exteriors %s -outname %s'
% (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.gerber_cutout_name + '_iso_exterior', type(obj)))

View File

@@ -0,0 +1,60 @@
from os import listdir
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
def test_import_svg(self):
"""
Test all SVG files inside svg directory.
Problematic SVG files shold be put there as test reference.
:param self:
:return:
"""
file_list = listdir(self.svg_files)
for svg_file in file_list:
# import without outname
self.fc.exec_command_test('import_svg "%s/%s"' % (self.svg_files, svg_file))
obj = self.fc.collection.get_by_name(svg_file)
self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (svg_file, type(obj)))
# import with outname
outname = '%s-%s' % (self.geometry_name, svg_file)
self.fc.exec_command_test('import_svg "%s/%s" -outname "%s"' % (self.svg_files, svg_file, outname))
obj = self.fc.collection.get_by_name(outname)
self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (outname, type(obj)))
names = self.fc.collection.get_names()
self.assertEqual(len(names), len(file_list)*2,
"Expected %d objects, found %d" % (len(file_list)*2, len(file_list)))
def test_import_svg_as_geometry(self):
self.fc.exec_command_test('import_svg "%s/%s" -type geometry -outname "%s"'
% (self.svg_files, self.svg_filename, self.geometry_name))
obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber),
"Expected FlatCAMGeometry, instead, %s is %s" % (self.geometry_name, type(obj)))
def test_import_svg_as_gerber(self):
self.fc.exec_command_test('import_svg "%s/%s" -type gerber -outname "%s"'
% (self.svg_files, self.svg_filename, self.gerber_name))
obj = self.fc.collection.get_by_name(self.gerber_name)
self.assertTrue(isinstance(obj, FlatCAMGerber),
"Expected FlatCAMGerber, instead, %s is %s" % (self.gerber_name, type(obj)))
self.fc.exec_command_test('isolate "%s"' % self.gerber_name)
obj = self.fc.collection.get_by_name(self.gerber_name+'_iso')
self.assertTrue(isinstance(obj, FlatCAMGeometry),
"Expected FlatCAMGeometry, instead, %s is %s" % (self.gerber_name+'_iso', type(obj)))

View File

@@ -0,0 +1,24 @@
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
def test_interiors(self):
"""
Test interiors
:param self:
:return:
"""
self.fc.exec_command_test('open_gerber %s/%s -outname %s'
% (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s"
% (self.gerber_cutout_name, type(gerber_cutout_obj)))
# interiors and delete isolated traces
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
self.fc.exec_command_test('interiors %s -outname %s'
% (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_interior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_interior')
self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.gerber_cutout_name + '_iso_interior', type(obj)))

View File

@@ -0,0 +1,21 @@
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry
def test_isolate(self):
"""
Test isolate gerber
:param self:
:return:
"""
self.fc.exec_command_test('open_gerber %s/%s -outname %s'
% (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s"
% (self.gerber_top_name, type(gerber_top_obj)))
# isolate traces
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter))
geometry_top_obj = self.fc.collection.get_by_name(self.gerber_top_name+'_iso')
self.assertTrue(isinstance(geometry_top_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.gerber_top_name+'_iso', type(geometry_top_obj)))

View File

@@ -0,0 +1,48 @@
from FlatCAMObj import FlatCAMGeometry
def test_new(self):
"""
Test new project
:param self:
:return:
"""
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
self.fc.exec_command_test('proc testproc {} { puts "testresult" }')
result = self.fc.exec_command_test('testproc')
self.assertEqual(result, "testresult",'testproc should return "testresult"')
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
# object should not exists anymore
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
# TODO after new it should delete all procedures and variables, we need to make sure "testproc" does not exists
# Test it again with same names
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
# object should not exists anymore
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))

View File

@@ -0,0 +1,14 @@
from FlatCAMObj import FlatCAMGeometry
def test_new_geometry(self):
"""
Test create new geometry
:param self:
:return:
"""
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))

View File

@@ -0,0 +1,15 @@
from FlatCAMObj import FlatCAMExcellon
def test_open_excellon(self):
"""
Test open excellon file
:param self:
:return:
"""
self.fc.exec_command_test('open_excellon %s/%s -outname %s'
% (self.gerber_files, self.excellon_filename, self.excellon_name))
excellon_obj = self.fc.collection.get_by_name(self.excellon_name)
self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), "Expected FlatCAMExcellon, instead, %s is %s"
% (self.excellon_name, type(excellon_obj)))

View File

@@ -0,0 +1,25 @@
from FlatCAMObj import FlatCAMGerber
def test_open_gerber(self):
"""
Test open gerber file
:param self:
:return:
"""
self.fc.exec_command_test('open_gerber %s/%s -outname %s'
% (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s"
% (self.gerber_top_name, type(gerber_top_obj)))
self.fc.exec_command_test('open_gerber %s/%s -outname %s'
% (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name))
gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s"
% (self.gerber_bottom_name, type(gerber_bottom_obj)))
#just read with original name
self.fc.exec_command_test('open_gerber %s/%s'
% (self.gerber_files, self.copper_top_filename))

View File

@@ -0,0 +1,25 @@
from FlatCAMObj import FlatCAMGeometry
def test_paint_polygon(self):
"""
Test create paint polygon geometry
:param self:
:return:
"""
self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name)
geometry_obj = self.fc.collection.get_by_name(self.geometry_name)
self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
% (self.geometry_name, type(geometry_obj)))
points = '0 0 20 0 10 10 0 10'
self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points))
# TODO rename to paint_polygon in future oop command implementation
self.fc.exec_command_test('paint_poly "%s" 5 5 2 0.5' % (self.geometry_name))
geometry_obj = self.fc.collection.get_by_name(self.geometry_name+'_paint')
# TODO uncoment check after oop implementation, because of threading inside paint poly
#self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s"
# % (self.geometry_name+'_paint', type(geometry_obj)))

187
tests/test_tcl_shell.py Normal file
View File

@@ -0,0 +1,187 @@
import sys
import unittest
from PyQt4 import QtGui
from PyQt4.QtCore import QThread
from FlatCAMApp import App
from os import listdir
from os.path import isfile
from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon
from ObjectUI import GerberObjectUI, GeometryObjectUI
from time import sleep
import os
import tempfile
class TclShellTest(unittest.TestCase):
svg_files = 'tests/svg'
svg_filename = 'Arduino Nano3_pcb.svg'
gerber_files = 'tests/gerber_files'
copper_bottom_filename = 'detector_copper_bottom.gbr'
copper_top_filename = 'detector_copper_top.gbr'
cutout_filename = 'detector_contour.gbr'
excellon_filename = 'detector_drill.txt'
gerber_name = "gerber"
geometry_name = "geometry"
excellon_name = "excellon"
gerber_top_name = "top"
gerber_bottom_name = "bottom"
gerber_cutout_name = "cutout"
engraver_diameter = 0.3
cutout_diameter = 3
drill_diameter = 0.8
# load test methods to split huge test file into smaller pieces
# reason for this is reuse one test window only,
# CANNOT DO THIS HERE!!!
#from tests.test_tclCommands import *
@classmethod
def setUpClass(cls):
cls.setup = True
cls.app = QtGui.QApplication(sys.argv)
# Create App, keep app defaults (do not load
# user-defined defaults).
cls.fc = App(user_defaults=False)
cls.fc.ui.shell_dock.show()
def setUp(self):
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
@classmethod
def tearDownClass(cls):
cls.fc.tcl = None
cls.app.closeAllWindows()
del cls.fc
del cls.app
pass
def test_set_get_units(self):
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
self.fc.exec_command_test('set_sys units IN')
self.fc.exec_command_test('new')
units=self.fc.exec_command_test('get_sys units')
self.assertEquals(units, "IN")
self.fc.exec_command_test('set_sys units MM')
self.fc.exec_command_test('new')
units=self.fc.exec_command_test('get_sys units')
self.assertEquals(units, "MM")
def test_gerber_flow(self):
# open gerber files top, bottom and cutout
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
"Expected FlatCAMGerber, instead, %s is %s" %
(self.gerber_top_name, type(gerber_top_obj)))
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name))
gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber),
"Expected FlatCAMGerber, instead, %s is %s" %
(self.gerber_bottom_name, type(gerber_bottom_obj)))
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber),
"Expected FlatCAMGerber, instead, %s is %s" %
(self.gerber_cutout_name, type(gerber_cutout_obj)))
# exteriors delete and join geometries for top layer
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
self.assertTrue(isinstance(obj, FlatCAMGeometry),
"Expected FlatCAMGeometry, instead, %s is %s" %
(self.gerber_cutout_name + '_iso_exterior', type(obj)))
# mirror bottom gerbers
self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_bottom_name, self.gerber_cutout_name))
self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_cutout_name, self.gerber_cutout_name))
# exteriors delete and join geometries for bottom layer
self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.engraver_diameter, self.gerber_cutout_name + '_bottom_iso'))
self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
self.assertTrue(isinstance(obj, FlatCAMGeometry),
"Expected FlatCAMGeometry, instead, %s is %s" %
(self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
# at this stage we should have 5 objects
names = self.fc.collection.get_names()
self.assertEqual(len(names), 5,
"Expected 5 objects, found %d" % len(names))
# isolate traces
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter))
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_bottom_name, self.engraver_diameter))
# join isolated geometries for top and bottom
self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_top_name + '_join_iso', self.gerber_top_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_bottom_name + '_join_iso', self.gerber_bottom_name + '_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
# at this stage we should have 9 objects
names = self.fc.collection.get_names()
self.assertEqual(len(names), 9,
"Expected 9 objects, found %d" % len(names))
# clean unused isolations
self.fc.exec_command_test('delete %s' % (self.gerber_bottom_name + '_iso'))
self.fc.exec_command_test('delete %s' % (self.gerber_top_name + '_iso'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso_exterior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso_exterior'))
# at this stage we should have 5 objects again
names = self.fc.collection.get_names()
self.assertEqual(len(names), 5,
"Expected 5 objects, found %d" % len(names))
# geocutout bottom test (it cuts to same object)
self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.cutout_diameter, self.gerber_cutout_name + '_bottom_iso'))
self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior'))
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso'))
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior')
self.assertTrue(isinstance(obj, FlatCAMGeometry),
"Expected FlatCAMGeometry, instead, %s is %s" %
(self.gerber_cutout_name + '_bottom_iso_exterior', type(obj)))
self.fc.exec_command_test('geocutout %s -dia %f -gapsize 0.3 -gaps 4' % (self.gerber_cutout_name + '_bottom_iso_exterior', self.cutout_diameter))
# at this stage we should have 6 objects
names = self.fc.collection.get_names()
self.assertEqual(len(names), 6,
"Expected 6 objects, found %d" % len(names))
# TODO: tests for tcl
def test_open_gerber(self):
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
"Expected FlatCAMGerber, instead, %s is %s" %
(self.gerber_top_name, type(gerber_top_obj)))
def test_excellon_flow(self):
self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name))
excellon_obj = self.fc.collection.get_by_name(self.excellon_name)
self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon),
"Expected FlatCAMExcellon, instead, %s is %s" %
(self.excellon_name, type(excellon_obj)))
# mirror bottom excellon
self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.excellon_name, self.gerber_cutout_name))
# TODO: tests for tcl