Fixed bug in Excellon parser. Did not support numbers with period.

This commit is contained in:
Juan Pablo Caram
2014-03-03 22:12:07 -05:00
parent 49c8aeb723
commit ad5e989331
3 changed files with 1721 additions and 1214 deletions

View File

@@ -19,8 +19,7 @@ import simplejson as json
from matplotlib.figure import Figure
from numpy import arange, sin, pi
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
#from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
#from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
from mpl_toolkits.axes_grid.anchored_artists import AnchoredText
from camlib import *
import sys
@@ -552,11 +551,23 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
return factor
def plot(self, figure):
"""
Plots the object onto the give figure. Updates the canvas
when done.
:param figure: Matplotlib figure on which to plot.
:type figure: Matplotlib.Figure
:return: None
"""
# Sets up and clears self.axes.
# Attaches axes to the figure... Maybe we want to do that
# when plotting is complete?
FlatCAMObj.plot(self, figure)
if not self.options["plot"]:
return
# Make sure solid_geometry is iterable.
try:
_ = iter(self.solid_geometry)
except TypeError:
@@ -611,6 +622,8 @@ class App:
# Needed to interact with the GUI from other threads.
GObject.threads_init()
# GLib.log_set_handler()
#### GUI ####
self.gladefile = "FlatCAM.ui"
self.builder = Gtk.Builder()
@@ -634,6 +647,8 @@ class App:
self.setup_project_list() # The "Project" tab
self.setup_component_editor() # The "Selected" tab
self.setup_toolbar()
#### Event handling ####
self.builder.connect_signals(self)
@@ -688,7 +703,14 @@ class App:
# self.radios.update({obj.kind + "_" + option: obj.radios[option]})
# self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
## Event subscriptions ##
self.plot_click_subscribers = {}
self.plot_mousemove_subscribers = {}
## Tools ##
self.measure = Measurement(self.axes, self.plot_click_subscribers,
self.plot_mousemove_subscribers,
lambda: self.canvas.queue_draw())
#### Initialization ####
self.load_defaults()
@@ -697,13 +719,13 @@ class App:
self.units_label.set_text("[" + self.options["units"] + "]")
#### Check for updates ####
self.version = 1
self.version = 2
t1 = threading.Thread(target=self.versionCheck)
t1.daemon = True
t1.start()
#### For debugging only ###
def someThreadFunc(self):
def someThreadFunc(app_obj):
print "Hello World!"
t = threading.Thread(target=someThreadFunc, args=(self,))
@@ -717,10 +739,55 @@ class App:
self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
self.window.set_title("FlatCAM - Alpha 2 UNSTABLE - Check for updates!")
self.window.set_title("FlatCAM - Alpha 3 UNSTABLE - Check for updates!")
self.window.set_default_size(900, 600)
self.window.show_all()
def setup_toolbar(self):
toolbar = self.builder.get_object("toolbar_main")
# Zoom fit
zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
zoom_fit = Gtk.ToolButton.new(zf_ico, "")
zoom_fit.connect("clicked", self.on_zoom_fit)
zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
toolbar.insert(zoom_fit, -1)
# Zoom out
zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
zoom_out = Gtk.ToolButton.new(zo_ico, "")
zoom_out.connect("clicked", self.on_zoom_out)
zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
toolbar.insert(zoom_out, -1)
# Zoom in
zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
zoom_in = Gtk.ToolButton.new(zi_ico, "")
zoom_in.connect("clicked", self.on_zoom_in)
zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
toolbar.insert(zoom_in, -1)
# Clear plot
cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
clear_plot = Gtk.ToolButton.new(cp_ico, "")
clear_plot.connect("clicked", self.on_clear_plots)
clear_plot.set_tooltip_markup("Clear Plot")
toolbar.insert(clear_plot, -1)
# Replot
rp_ico = Gtk.Image.new_from_file('share/replot32.png')
replot = Gtk.ToolButton.new(rp_ico, "")
replot.connect("clicked", self.on_toolbar_replot)
replot.set_tooltip_markup("Re-plot all")
toolbar.insert(replot, -1)
# Delete item
del_ico = Gtk.Image.new_from_file('share/delete32.png')
delete = Gtk.ToolButton.new(del_ico, "")
delete.connect("clicked", self.on_delete)
delete.set_tooltip_markup("Delete selected\nobject.")
toolbar.insert(delete, -1)
def setup_plot(self):
"""
Sets up the main plotting area by creating a Matplotlib
@@ -1403,6 +1470,9 @@ class App:
for widget in tooltips:
self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
def do_nothing(self, param):
return
########################################
## EVENT HANDLERS ##
########################################
@@ -2284,6 +2354,7 @@ class App:
assert isinstance(app_obj, App)
cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
geo_obj.solid_geometry = cp
geo_obj.options["cnctooldia"] = tooldia
name = self.selected_item_name + "_paint"
self.new_object("geometry", name, gen_paintarea)
@@ -2628,6 +2699,10 @@ class App:
self.position_label.set_label("X: %.4f Y: %.4f" % (
event.xdata, event.ydata))
self.mouse = [event.xdata, event.ydata]
for subscriber in self.plot_mousemove_subscribers:
self.plot_mousemove_subscribers[subscriber](event)
except:
self.position_label.set_label("")
self.mouse = None
@@ -2743,6 +2818,240 @@ class App:
self.zoom(1.5, self.mouse)
return
if event.key == 'm':
if self.measure.toggle_active():
self.info("Measuring tool ON")
else:
self.info("Measuring tool OFF")
return
class Measurement:
def __init__(self, axes, click_subscibers, move_subscribers, update=None):
self.update = update
self.axes = axes
self.click_subscribers = click_subscibers
self.move_subscribers = move_subscribers
self.point1 = None
self.point2 = None
self.active = False
self.at = None # AnchoredText object on plot
def toggle_active(self):
if self.active:
self.active = False
self.move_subscribers.pop("meas")
self.click_subscribers.pop("meas")
self.at.remove()
if self.update is not None:
self.update()
return False
else:
self.active = True
self.click_subscribers["meas"] = self.on_click
self.move_subscribers["meas"] = self.on_move
return True
def on_move(self, event):
try:
self.at.remove()
except:
pass
if self.point1 is None:
self.at = AnchoredText("Click on a reference point...")
else:
dx = event.xdata - self.point1[0]
dy = event.ydata - self.point1[1]
d = sqrt(dx**2 + dy**2)
self.at = AnchoredText("D = %.4f\nD(x) = %.4f\nD(y) = %.4f" % (d, dx, dy),
loc=2, prop={'size': 14}, frameon=False)
self.axes.add_artist(self.at)
if self.update is not None:
self.update()
def on_click(self, event):
if self.point1 is None:
self.point1 = (event.xdata, event.ydata)
return
else:
self.point2 = copy.copy(self.point1)
self.point1 = (event.xdata, event.ydata)
class PlotCanvas:
"""
Class handling the plotting area in the application.
"""
def __init__(self, container):
# Options
self.x_margin = 15 # pixels
self.y_margin = 25 # Pixels
# Parent container
self.container = container
# Plots go onto a single matplotlib.figure
self.figure = Figure(dpi=50) # TODO: dpi needed?
self.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)
self.canvas = FigureCanvas(self.figure)
self.canvas.set_hexpand(1)
self.canvas.set_vexpand(1)
self.canvas.set_can_focus(True) # For key press
# Attach to parent
self.container.attach(self.canvas, 0, 0, 600, 400)
def mpl_connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the Matplotlib interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: func
:return: Nothing
"""
self.canvas.mpl_connect(event_name, callback)
def connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the native GTK interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: function
:return: Nothing
"""
self.canvas.connect(event_name, callback)
def clear(self):
"""
Clears axes and figure.
:return: None
"""
# Clear
self.axes.cla()
self.figure.clf()
# Re-build
self.figure.add_axes(self.axes)
self.axes.set_aspect(1)
self.axes.grid(True)
# Re-draw
self.canvas.queue_draw()
def adjust_axes(self, xmin, ymin, xmax, ymax):
"""
Adjusts axes of all plots while maintaining the use of the whole canvas
and an aspect ratio to 1:1 between x and y axes. The parameters are an original
request that will be modified to fit these restrictions.
:param xmin: Requested minimum value for the X axis.
:type xmin: float
:param ymin: Requested minimum value for the Y axis.
:type ymin: float
:param xmax: Requested maximum value for the X axis.
:type xmax: float
:param ymax: Requested maximum value for the Y axis.
:type ymax: float
:return: None
"""
width = xmax - xmin
height = ymax - ymin
try:
r = width / height
except:
print "ERROR: Height is", height
return
canvas_w, canvas_h = self.canvas.get_width_height()
canvas_r = float(canvas_w) / canvas_h
x_ratio = float(self.x_margin) / canvas_w
y_ratio = float(self.y_margin) / canvas_h
if r > canvas_r:
ycenter = (ymin + ymax) / 2.0
newheight = height * r / canvas_r
ymin = ycenter - newheight / 2.0
ymax = ycenter + newheight / 2.0
else:
xcenter = (xmax + ymin) / 2.0
newwidth = width * canvas_r / r
xmin = xcenter - newwidth / 2.0
xmax = xcenter + newwidth / 2.0
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
# Re-draw
self.canvas.queue_draw()
def auto_adjust_axes(self):
"""
Calls ``adjust_axes()`` using the extents of the base axes.
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
def zoom(self, factor, center=None):
"""
Zooms the plot by factor around a given
center point. Takes care of re-drawing.
:param factor: Number by which to scale the plot.
:type factor: float
:param center: Coordinates [x, y] of the point around which to scale the plot.
:type center: list
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
if center is None:
center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
# For keeping the point at the pointer location
relx = (xmax - center[0]) / width
rely = (ymax - center[1]) / height
new_width = width / factor
new_height = height / factor
xmin = center[0] - new_width * (1 - relx)
xmax = center[0] + new_width * relx
ymin = center[1] - new_height * (1 - rely)
ymax = center[1] + new_height * rely
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
# Re-draw
self.canvas.queue_draw()
app = App()
Gtk.main()

2536
FlatCAM.ui

File diff suppressed because it is too large Load Diff

View File

@@ -376,7 +376,7 @@ class Gerber (Geometry):
:rtype : None
"""
# Apertures
print "Scaling apertures..."
#print "Scaling apertures..."
for apid in self.apertures:
for param in self.apertures[apid]:
if param != "type": # All others are dimensions.
@@ -384,20 +384,20 @@ class Gerber (Geometry):
self.apertures[apid][param] *= factor
# Paths
print "Scaling paths..."
#print "Scaling paths..."
for path in self.paths:
path['linestring'] = affinity.scale(path['linestring'],
factor, factor, origin=(0, 0))
# Flashes
print "Scaling flashes..."
#print "Scaling flashes..."
for fl in self.flashes:
# TODO: Shouldn't 'loc' be a numpy.array()?
fl['loc'][0] *= factor
fl['loc'][1] *= factor
# Regions
print "Scaling regions..."
#print "Scaling regions..."
for reg in self.regions:
reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
origin=(0, 0))
@@ -424,20 +424,20 @@ class Gerber (Geometry):
dx, dy = vect
# Paths
print "Shifting paths..."
#print "Shifting paths..."
for path in self.paths:
path['linestring'] = affinity.translate(path['linestring'],
xoff=dx, yoff=dy)
# Flashes
print "Shifting flashes..."
#print "Shifting flashes..."
for fl in self.flashes:
# TODO: Shouldn't 'loc' be a numpy.array()?
fl['loc'][0] += dx
fl['loc'][1] += dy
# Regions
print "Shifting regions..."
#print "Shifting regions..."
for reg in self.regions:
reg['polygon'] = affinity.translate(reg['polygon'],
xoff=dx, yoff=dy)
@@ -808,15 +808,12 @@ class Gerber (Geometry):
:return: None
"""
# if len(self.buffered_paths) == 0:
# self.buffer_paths()
print "... buffer_paths()"
self.buffer_paths()
print "... fix_regions()"
self.fix_regions()
print "... do_flashes()"
self.do_flashes()
print "... cascaded_union()"
self.solid_geometry = cascaded_union(self.buffered_paths +
[poly['polygon'] for poly in self.regions] +
self.flash_geometry)
@@ -930,8 +927,10 @@ class Excellon(Geometry):
self.meas_re = re.compile(r'^M7([12])$')
# Coordinates
self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
#self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
#self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
self.coordsperiod_re = re.compile(r'(?=.*X(\d*\.\d*))?(?=.*Y(\d*\.\d*))?[XY]')
self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X(\d*))?(?=.*Y(\d*))?[XY]')
# R - Repeat hole (# times, X offset, Y offset)
self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(\d*\.?\d*))?(?:Y(\d*\.?\d*))?$')
@@ -962,10 +961,15 @@ class Excellon(Geometry):
:return: None
"""
# State variables
current_tool = ""
in_header = False
current_x = None
current_y = None
i = 0 # Line number
for eline in elines:
i += 1
## Header Begin/End ##
if self.hbegin_re.search(eline):
@@ -985,12 +989,46 @@ class Excellon(Geometry):
current_tool = str(int(match.group(1)))
continue
## Drill ##
indexx = eline.find("X")
indexy = eline.find("Y")
if indexx != -1 and indexy != -1:
x = float(int(eline[indexx+1:indexy])/10000.0)
y = float(int(eline[indexy+1:-1])/10000.0)
## Coordinates without period ##
match = self.coordsnoperiod_re.search(eline)
if match:
try:
x = float(match.group(1))/10000
current_x = x
except TypeError:
x = current_x
try:
y = float(match.group(2))/10000
current_y = y
except TypeError:
y = current_y
if x is None or y is None:
print "ERROR: Missing coordinates"
continue
self.drills.append({'point': Point((x, y)), 'tool': current_tool})
continue
## Coordinates with period ##
match = self.coordsperiod_re.search(eline)
if match:
try:
x = float(match.group(1))
current_x = x
except TypeError:
x = current_x
try:
y = float(match.group(2))
except TypeError:
y = current_y
if x is None or y is None:
print "ERROR: Missing coordinates"
continue
self.drills.append({'point': Point((x, y)), 'tool': current_tool})
continue