mirror of
https://github.com/Denvi/FlatCAM.git
synced 2026-03-23 08:37:14 +01:00
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:
@@ -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()
|
||||
|
||||
13
FlatCAM.py
13
FlatCAM.py
@@ -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_())
|
||||
|
||||
1578
FlatCAMApp.py
1578
FlatCAMApp.py
File diff suppressed because it is too large
Load Diff
147
FlatCAMGUI.py
147
FlatCAMGUI.py
@@ -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()
|
||||
409
FlatCAMObj.py
409
FlatCAMObj.py
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
22
ObjectUI.py
22
ObjectUI.py
@@ -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(
|
||||
|
||||
359
PlotCanvas.py
359
PlotCanvas.py
@@ -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
|
||||
|
||||
23
README.md
23
README.md
@@ -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:
|
||||
|
||||

|
||||

|
||||

|
||||
CAD program, and create G-Code for Isolation routing.
|
||||
533
camlib.py
533
camlib.py
@@ -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
|
||||
|
||||
BIN
camlib.pyc
BIN
camlib.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
522
svgparse.py
Normal 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
398
tclCommands/TclCommand.py
Normal 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)
|
||||
61
tclCommands/TclCommandAddPolygon.py
Normal file
61
tclCommands/TclCommandAddPolygon.py
Normal 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()
|
||||
61
tclCommands/TclCommandAddPolyline.py
Normal file
61
tclCommands/TclCommandAddPolyline.py
Normal 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()
|
||||
80
tclCommands/TclCommandCncjob.py
Normal file
80
tclCommands/TclCommandCncjob.py
Normal 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)
|
||||
81
tclCommands/TclCommandDrillcncjob.py
Normal file
81
tclCommands/TclCommandDrillcncjob.py
Normal 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)
|
||||
79
tclCommands/TclCommandExportGcode.py
Normal file
79
tclCommands/TclCommandExportGcode.py
Normal 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)
|
||||
64
tclCommands/TclCommandExteriors.py
Normal file
64
tclCommands/TclCommandExteriors.py
Normal 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)
|
||||
81
tclCommands/TclCommandImportSvg.py
Normal file
81
tclCommands/TclCommandImportSvg.py
Normal 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)
|
||||
|
||||
64
tclCommands/TclCommandInteriors.py
Normal file
64
tclCommands/TclCommandInteriors.py
Normal 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)
|
||||
79
tclCommands/TclCommandIsolate.py
Normal file
79
tclCommands/TclCommandIsolate.py
Normal 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)
|
||||
40
tclCommands/TclCommandNew.py
Normal file
40
tclCommands/TclCommandNew.py
Normal 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()
|
||||
95
tclCommands/TclCommandOpenGerber.py
Normal file
95
tclCommands/TclCommandOpenGerber.py
Normal 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
54
tclCommands/__init__.py
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
95
tests/canvas/performance.py
Normal file
95
tests/canvas/performance.py
Normal 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
6
tests/canvas/prof.sh
Executable 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)"
|
||||
125
tests/excellon_files/case1.drl
Normal file
125
tests/excellon_files/case1.drl
Normal 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
|
||||
26
tests/gerber_files/detector_contour.gbr
Normal file
26
tests/gerber_files/detector_contour.gbr
Normal 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*
|
||||
2146
tests/gerber_files/detector_copper_bottom.gbr
Normal file
2146
tests/gerber_files/detector_copper_bottom.gbr
Normal file
File diff suppressed because it is too large
Load Diff
71
tests/gerber_files/detector_copper_top.gbr
Normal file
71
tests/gerber_files/detector_copper_top.gbr
Normal 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*
|
||||
46
tests/gerber_files/detector_drill.txt
Normal file
46
tests/gerber_files/detector_drill.txt
Normal 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
|
||||
34
tests/svg/7segment_9,9.svg
Normal file
34
tests/svg/7segment_9,9.svg
Normal 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 |
468
tests/svg/Arduino Nano3_pcb.svg
Normal file
468
tests/svg/Arduino Nano3_pcb.svg
Normal 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
126
tests/svg/drawing.svg
Normal 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 |
77
tests/svg/usb_connector.svg
Normal file
77
tests/svg/usb_connector.svg
Normal 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
163
tests/test_excellon_flow.py
Normal 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
|
||||
@@ -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
129
tests/test_svg_flow.py
Normal 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
|
||||
18
tests/test_tclCommands/__init__.py
Normal file
18
tests/test_tclCommands/__init__.py
Normal 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 *
|
||||
18
tests/test_tclCommands/test_TclCommandAddPolygon.py
Normal file
18
tests/test_tclCommands/test_TclCommandAddPolygon.py
Normal 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))
|
||||
18
tests/test_tclCommands/test_TclCommandAddPolyline.py
Normal file
18
tests/test_tclCommands/test_TclCommandAddPolyline.py
Normal 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))
|
||||
17
tests/test_tclCommands/test_TclCommandCncjob.py
Normal file
17
tests/test_tclCommands/test_TclCommandCncjob.py
Normal 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)))
|
||||
18
tests/test_tclCommands/test_TclCommandDrillcncjob.py
Normal file
18
tests/test_tclCommands/test_TclCommandDrillcncjob.py
Normal 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)))
|
||||
33
tests/test_tclCommands/test_TclCommandExportGcode.py
Normal file
33
tests/test_tclCommands/test_TclCommandExportGcode.py
Normal 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
|
||||
24
tests/test_tclCommands/test_TclCommandExteriors.py
Normal file
24
tests/test_tclCommands/test_TclCommandExteriors.py
Normal 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)))
|
||||
60
tests/test_tclCommands/test_TclCommandImportSvg.py
Normal file
60
tests/test_tclCommands/test_TclCommandImportSvg.py
Normal 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)))
|
||||
24
tests/test_tclCommands/test_TclCommandInteriors.py
Normal file
24
tests/test_tclCommands/test_TclCommandInteriors.py
Normal 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)))
|
||||
21
tests/test_tclCommands/test_TclCommandIsolate.py
Normal file
21
tests/test_tclCommands/test_TclCommandIsolate.py
Normal 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)))
|
||||
48
tests/test_tclCommands/test_TclCommandNew.py
Normal file
48
tests/test_tclCommands/test_TclCommandNew.py
Normal 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)))
|
||||
14
tests/test_tclCommands/test_TclCommandNewGeometry.py
Normal file
14
tests/test_tclCommands/test_TclCommandNewGeometry.py
Normal 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)))
|
||||
15
tests/test_tclCommands/test_TclCommandOpenExcellon.py
Normal file
15
tests/test_tclCommands/test_TclCommandOpenExcellon.py
Normal 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)))
|
||||
25
tests/test_tclCommands/test_TclCommandOpenGerber.py
Normal file
25
tests/test_tclCommands/test_TclCommandOpenGerber.py
Normal 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))
|
||||
25
tests/test_tclCommands/test_TclCommandPaintPolygon.py
Normal file
25
tests/test_tclCommands/test_TclCommandPaintPolygon.py
Normal 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
187
tests/test_tcl_shell.py
Normal 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
|
||||
Reference in New Issue
Block a user