diff --git a/FlatCAM.py b/FlatCAM.py
index eac22909..3a462461 100644
--- a/FlatCAM.py
+++ b/FlatCAM.py
@@ -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 1)")
+ 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 2)")
+ 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 3)")
+ 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()
diff --git a/FlatCAM.ui b/FlatCAM.ui
index 5428f59c..1f20ca16 100644
--- a/FlatCAM.ui
+++ b/FlatCAM.ui
@@ -6,7 +6,7 @@
5
dialog
FlatCAM
- Version Alpha 2 (2014/02) - UNSTABLE
+ Version Alpha 3 (2014/03) - UNSTABLE
(c) 2014 Juan Pablo Caram
2D Post-processing for Manufacturing specialized in
Printed Circuit Boards
@@ -33,6 +33,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+ share/flatcam_icon128.png