flatcam/FlatCAM.py

3111 lines
104 KiB
Python

############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://caram.cl/software/flatcam #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
import threading
# TODO: Bundle together. This is just for debugging.
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GLib
from gi.repository import GObject
import simplejson as json
import matplotlib
from matplotlib.figure import Figure
from numpy import arange, sin, pi
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
#from mpl_toolkits.axes_grid.anchored_artists import AnchoredText
from camlib import *
import sys
import urllib
import copy
import random
########################################
## FlatCAMObj ##
########################################
class FlatCAMObj:
"""
Base type of objects handled in FlatCAM. These become interactive
in the GUI, can be plotted, and their options can be modified
by the user in their respective forms.
"""
# Instance of the application to which these are related.
# The app should set this value.
app = None
def __init__(self, name):
self.options = {"name": name}
self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
self.radios = {} # Name value pairs for radio sets
self.radios_inv = {} # Inverse of self.radios
self.axes = None # Matplotlib axes
self.kind = None # Override with proper name
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:
print "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:
print "Clearing and attaching axes"
self.axes.cla()
figure.add_axes(self.axes)
else:
print "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
"""
for option in self.options:
self.set_form_item(option)
def read_form(self):
"""
Reads form into ``self.options``.
:return: None
:rtype: None
"""
for option in self.options:
self.read_form_item(option)
def build_ui(self):
"""
Sets up the UI/form for this object.
:return: None
:rtype: None
"""
# Where the UI for this object is drawn
box_selected = self.app.builder.get_object("box_selected")
# Remove anything else in the box
box_children = box_selected.get_children()
for child in box_children:
box_selected.remove(child)
osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
osw.remove(sw) # TODO: Is this needed ?
vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
# Put in the UI
box_selected.pack_start(sw, True, True, 0)
entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
entry_name.connect("activate", self.app.on_activate_name)
self.to_form()
sw.show()
def set_form_item(self, option):
"""
Copies the specified option to the UI form.
:param option: Name of the option (Key in ``self.options``).
:type option: str
:return: None
"""
fkind = self.form_kinds[option]
fname = fkind + "_" + self.kind + "_" + option
if fkind == 'entry_eval' or fkind == 'entry_text':
self.app.builder.get_object(fname).set_text(str(self.options[option]))
return
if fkind == 'cb':
self.app.builder.get_object(fname).set_active(self.options[option])
return
if fkind == 'radio':
self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
return
print "Unknown kind of form item:", fkind
def read_form_item(self, option):
"""
Reads the specified option from the UI form into ``self.options``.
:param option: Name of the option.
:type option: str
:return: None
"""
fkind = self.form_kinds[option]
fname = fkind + "_" + self.kind + "_" + option
if fkind == 'entry_text':
self.options[option] = self.app.builder.get_object(fname).get_text()
return
if fkind == 'entry_eval':
self.options[option] = self.app.get_eval(fname)
return
if fkind == 'cb':
self.options[option] = self.app.builder.get_object(fname).get_active()
return
if fkind == 'radio':
self.options[option] = self.app.get_radio_value(self.radios[option])
return
print "Unknown kind of form item:", fkind
def plot(self):
"""
Plot this object (Extend this method to implement the actual plotting).
Axes get created, appended to canvas and cleared before plotting.
Call this in descendants before doing the plotting.
:return: Whether to continue plotting or not depending on the "plot" option.
:rtype: bool
"""
# Axes must exist and be attached to canvas.
if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
self.axes = self.app.plotcanvas.new_axes(self.options['name'])
if not self.options["plot"]:
self.axes.cla()
self.app.plotcanvas.auto_adjust_axes()
return False
# Clear axes or we will plot on top of them.
self.axes.cla()
# GLib.idle_add(self.axes.cla)
return True
def serialize(self):
"""
Returns a representation of the object as a dictionary so
it can be later exported as JSON. Override this method.
:return: Dictionary representing the object
:rtype: dict
"""
return
def deserialize(self, obj_dict):
"""
Re-builds an object from its serialized version.
:param obj_dict: Dictionary representing a FlatCAMObj
:type obj_dict: dict
:return: None
"""
return
class FlatCAMGerber(FlatCAMObj, Gerber):
"""
Represents Gerber code.
"""
def __init__(self, name):
Gerber.__init__(self)
FlatCAMObj.__init__(self, name)
self.kind = "gerber"
# The 'name' is already in self.options from FlatCAMObj
self.options.update({
"plot": True,
"mergepolys": True,
"multicolored": False,
"solid": False,
"isotooldia": 0.016,
"isopasses": 1,
"isooverlap": 0.15,
"cutouttooldia": 0.07,
"cutoutmargin": 0.2,
"cutoutgapsize": 0.15,
"gaps": "tb",
"noncoppermargin": 0.0,
"noncopperrounded": False,
"bboxmargin": 0.0,
"bboxrounded": False
})
# The 'name' is already in self.form_kinds from FlatCAMObj
self.form_kinds.update({
"plot": "cb",
"mergepolys": "cb",
"multicolored": "cb",
"solid": "cb",
"isotooldia": "entry_eval",
"isopasses": "entry_eval",
"isooverlap": "entry_eval",
"cutouttooldia": "entry_eval",
"cutoutmargin": "entry_eval",
"cutoutgapsize": "entry_eval",
"gaps": "radio",
"noncoppermargin": "entry_eval",
"noncopperrounded": "cb",
"bboxmargin": "entry_eval",
"bboxrounded": "cb"
})
self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
def convert_units(self, units):
"""
Converts the units of the object by scaling dimensions in all geometry
and options.
:param units: Units to which to convert the object: "IN" or "MM".
:type units: str
:return: None
:rtype: None
"""
factor = Gerber.convert_units(self, units)
self.options['isotooldia'] *= factor
self.options['cutoutmargin'] *= factor
self.options['cutoutgapsize'] *= factor
self.options['noncoppermargin'] *= factor
self.options['bboxmargin'] *= factor
def plot(self):
# Does all the required setup and returns False
# if the 'ptint' option is set to False.
if not FlatCAMObj.plot(self):
return
if self.options["mergepolys"]:
geometry = self.solid_geometry
else:
geometry = self.buffered_paths + \
[poly['polygon'] for poly in self.regions] + \
self.flash_geometry
if self.options["multicolored"]:
linespec = '-'
else:
linespec = 'k-'
if self.options["solid"]:
for poly in geometry:
# TODO: Too many things hardcoded.
patch = PolygonPatch(poly,
facecolor="#BBF268",
edgecolor="#006E20",
alpha=0.75,
zorder=2)
self.axes.add_patch(patch)
else:
for poly in geometry:
x, y = poly.exterior.xy
self.axes.plot(x, y, linespec)
for ints in poly.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, linespec)
# self.app.plotcanvas.auto_adjust_axes()
GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
def serialize(self):
return {
"options": self.options,
"kind": self.kind
}
class FlatCAMExcellon(FlatCAMObj, Excellon):
"""
Represents Excellon/Drill code.
"""
def __init__(self, name):
Excellon.__init__(self)
FlatCAMObj.__init__(self, name)
self.kind = "excellon"
self.options.update({
"plot": True,
"solid": False,
"drillz": -0.1,
"travelz": 0.1,
"feedrate": 5.0,
"toolselection": ""
})
self.form_kinds.update({
"plot": "cb",
"solid": "cb",
"drillz": "entry_eval",
"travelz": "entry_eval",
"feedrate": "entry_eval",
"toolselection": "entry_text"
})
# TODO: Document this.
self.tool_cbs = {}
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
def convert_units(self, units):
factor = Excellon.convert_units(self, units)
self.options['drillz'] *= factor
self.options['travelz'] *= factor
self.options['feedrate'] *= factor
def plot(self):
# Does all the required setup and returns False
# if the 'ptint' option is set to False.
if not FlatCAMObj.plot(self):
return
try:
_ = iter(self.solid_geometry)
except TypeError:
self.solid_geometry = [self.solid_geometry]
# Plot excellon (All polygons?)
if self.options["solid"]:
for geo in self.solid_geometry:
patch = PolygonPatch(geo,
facecolor="#C40000",
edgecolor="#750000",
alpha=0.75,
zorder=3)
self.axes.add_patch(patch)
else:
for geo in self.solid_geometry:
x, y = geo.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in geo.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'g-')
#self.app.plotcanvas.auto_adjust_axes()
GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
def show_tool_chooser(self):
win = Gtk.Window()
box = Gtk.Box(spacing=2)
box.set_orientation(Gtk.Orientation(1))
win.add(box)
for tool in self.tools:
self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
box.pack_start(self.tool_cbs[tool], False, False, 1)
button = Gtk.Button(label="Accept")
box.pack_start(button, False, False, 1)
win.show_all()
def on_accept(widget):
win.destroy()
tool_list = []
for toolx in self.tool_cbs:
if self.tool_cbs[toolx].get_active():
tool_list.append(toolx)
self.options["toolselection"] = ", ".join(tool_list)
self.to_form()
button.connect("activate", on_accept)
button.connect("clicked", on_accept)
class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"""
Represents G-Code.
"""
def __init__(self, name, units="in", kind="generic", z_move=0.1,
feedrate=3.0, z_cut=-0.002, tooldia=0.0):
CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
FlatCAMObj.__init__(self, name)
self.kind = "cncjob"
self.options.update({
"plot": True,
"tooldia": 0.4 / 25.4 # 0.4mm in inches
})
self.form_kinds.update({
"plot": "cb",
"tooldia": "entry_eval"
})
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
def plot(self):
# Does all the required setup and returns False
# if the 'ptint' option is set to False.
if not FlatCAMObj.plot(self):
return
self.plot2(self.axes, tooldia=self.options["tooldia"])
#self.app.plotcanvas.auto_adjust_axes()
GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
def convert_units(self, units):
factor = CNCjob.convert_units(self, units)
print "FlatCAMCNCjob.convert_units()"
self.options["tooldia"] *= factor
class FlatCAMGeometry(FlatCAMObj, Geometry):
"""
Geometric object not associated with a specific
format.
"""
def __init__(self, name):
FlatCAMObj.__init__(self, name)
Geometry.__init__(self)
self.kind = "geometry"
self.options.update({
"plot": True,
"solid": False,
"multicolored": False,
"cutz": -0.002,
"travelz": 0.1,
"feedrate": 5.0,
"cnctooldia": 0.4 / 25.4,
"painttooldia": 0.0625,
"paintoverlap": 0.15,
"paintmargin": 0.01
})
self.form_kinds.update({
"plot": "cb",
"solid": "cb",
"multicolored": "cb",
"cutz": "entry_eval",
"travelz": "entry_eval",
"feedrate": "entry_eval",
"cnctooldia": "entry_eval",
"painttooldia": "entry_eval",
"paintoverlap": "entry_eval",
"paintmargin": "entry_eval"
})
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
def scale(self, factor):
"""
Scales all geometry by a given factor.
:param factor: Factor by which to scale the object's geometry/
:type factor: float
:return: None
:rtype: None
"""
if type(self.solid_geometry) == list:
self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
for g in self.solid_geometry]
else:
self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
origin=(0, 0))
def offset(self, vect):
"""
Offsets all geometry by a given vector/
:param vect: (x, y) vector by which to offset the object's geometry.
:type vect: tuple
:return: None
:rtype: None
"""
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 convert_units(self, units):
factor = Geometry.convert_units(self, units)
self.options['cutz'] *= factor
self.options['travelz'] *= factor
self.options['feedrate'] *= factor
self.options['cnctooldia'] *= factor
self.options['painttooldia'] *= factor
self.options['paintmargin'] *= factor
return factor
def plot(self):
"""
Plots the object into its axes. If None, of if the axes
are not part of the app's figure, it fetches new ones.
:return: None
"""
# Does all the required setup and returns False
# if the 'ptint' option is set to False.
if not FlatCAMObj.plot(self):
return
# Make sure solid_geometry is iterable.
try:
_ = iter(self.solid_geometry)
except TypeError:
self.solid_geometry = [self.solid_geometry]
for geo in self.solid_geometry:
if type(geo) == Polygon:
x, y = geo.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in geo.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'r-')
continue
if type(geo) == LineString or type(geo) == LinearRing:
x, y = geo.coords.xy
self.axes.plot(x, y, 'r-')
continue
if type(geo) == MultiPolygon:
for poly in geo:
x, y = poly.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in poly.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'r-')
continue
print "WARNING: Did not plot:", str(type(geo))
#self.app.plotcanvas.auto_adjust_axes()
GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
########################################
## App ##
########################################
class App:
"""
The main application class. The constructor starts the GUI.
"""
def __init__(self):
"""
Starts the application. Takes no parameters.
:return: app
:rtype: 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()
self.builder.add_from_file(self.gladefile)
self.window = self.builder.get_object("window1")
self.position_label = self.builder.get_object("label3")
self.grid = self.builder.get_object("grid1")
self.notebook = self.builder.get_object("notebook1")
self.info_label = self.builder.get_object("label_status")
self.progress_bar = self.builder.get_object("progressbar")
self.progress_bar.set_show_text(True)
self.units_label = self.builder.get_object("label_units")
self.toolbar = self.builder.get_object("toolbar_main")
# White (transparent) background on the "Options" tab.
self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
Gdk.RGBA(1, 1, 1, 1))
# Combo box to choose between project and application options.
self.combo_options = self.builder.get_object("combo_options")
self.combo_options.set_active(1)
self.setup_project_list() # The "Project" tab
self.setup_component_editor() # The "Selected" tab
self.setup_toolbar()
#### Event handling ####
self.builder.connect_signals(self)
#### Make plot area ####
self.plotcanvas = PlotCanvas(self.grid)
self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
self.setup_tooltips()
#### DATA ####
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.setup_obj_classes()
self.stuff = {} # FlatCAMObj's by name
self.mouse = None # Mouse coordinates over plot
# What is selected by the user. It is
# a key if self.stuff
self.selected_item_name = None
# Used to inhibit the on_options_update callback when
# the options are being changed by the program and not the user.
self.options_update_ignore = False
self.toggle_units_ignore = False
self.defaults = {
"units": "in"
} # Application defaults
## Current Project ##
self.options = {} # Project options
self.project_filename = None
self.form_kinds = {
"units": "radio"
}
self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
"gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
"gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
# Options for each kind of FlatCAMObj.
# Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
obj = FlatCAMClass("no_name")
for option in obj.form_kinds:
self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
# if obj.form_kinds[option] == "radio":
# 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.builder.get_object("box39"), self.plotcanvas.axes,
self.plot_click_subscribers, self.plot_mousemove_subscribers)
# Toolbar icon
# TODO: Where should I put this? Tool should have a method to add to toolbar?
meas_ico = Gtk.Image.new_from_file('share/measure32.png')
measure = Gtk.ToolButton.new(meas_ico, "")
measure.connect("clicked", self.measure.toggle_active)
measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
"Click on point to set reference.\n" +
"(Click on plot and hit <b>m</b>)")
self.toolbar.insert(measure, -1)
#### Initialization ####
self.load_defaults()
self.options.update(self.defaults) # Copy app defaults to project options
self.options2form() # Populate the app defaults form
self.units_label.set_text("[" + self.options["units"] + "]")
#### Check for updates ####
self.version = 2
t1 = threading.Thread(target=self.versionCheck)
t1.daemon = True
t1.start()
#### For debugging only ###
def somethreadfunc(app_obj):
print "Hello World!"
t = threading.Thread(target=somethreadfunc, args=(self,))
t.daemon = True
t.start()
########################################
## START ##
########################################
self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
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 3 UNSTABLE - Check for updates!")
self.window.set_default_size(900, 600)
self.window.show_all()
def setup_toolbar(self):
# 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>)")
self.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>)")
self.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>)")
self.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")
self.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")
self.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.")
self.toolbar.insert(delete, -1)
def setup_obj_classes(self):
"""
Sets up application specifics on the FlatCAMObj class.
:return: None
"""
FlatCAMObj.app = self
def setup_project_list(self):
"""
Sets up list or Tree where whatever has been loaded or created is
displayed.
:return: None
"""
self.store = Gtk.ListStore(str)
self.tree = Gtk.TreeView(self.store)
#self.list = Gtk.ListBox()
self.tree.connect("row_activated", self.on_row_activated)
self.tree_select = self.tree.get_selection()
self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Objects", renderer, text=0)
self.tree.append_column(column)
self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
def setup_component_editor(self):
"""
Initial configuration of the component editor. Creates
a page titled "Selection" on the notebook on the left
side of the main window.
:return: None
"""
box_selected = self.builder.get_object("box_selected")
# Remove anything else in the box
box_children = box_selected.get_children()
for child in box_children:
box_selected.remove(child)
box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
label1 = Gtk.Label("Choose an item from Project")
box1.pack_start(label1, True, False, 1)
box_selected.pack_start(box1, True, True, 0)
#box_selected.show()
box1.show()
label1.show()
def info(self, text):
"""
Show text on the status bar.
:param text: Text to display.
:type text: str
:return: None
"""
self.info_label.set_text(text)
def build_list(self):
"""
Clears and re-populates the list of objects in currently
in the project.
:return: None
"""
print "build_list(): clearing"
self.tree_select.unselect_all()
self.store.clear()
print "repopulating...",
for key in self.stuff:
print key,
self.store.append([key])
print
def get_radio_value(self, radio_set):
"""
Returns the radio_set[key] of the radiobutton
whose name is key is active.
:param radio_set: A dictionary containing widget_name: value pairs.
:type radio_set: dict
:return: radio_set[key]
"""
for name in radio_set:
if self.builder.get_object(name).get_active():
return radio_set[name]
def plot_all(self):
"""
Re-generates all plots from all objects.
:return: None
"""
self.plotcanvas.clear()
self.set_progress_bar(0.1, "Re-plotting...")
def thread_func(app_obj):
percentage = 0.1
try:
delta = 0.9 / len(self.stuff)
except ZeroDivisionError:
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
return
for i in self.stuff:
self.stuff[i].plot()
percentage += delta
GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
#app_obj.plotcanvas.auto_adjust_axes()
#self.on_zoom_fit(None)
GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
GLib.idle_add(lambda: self.on_zoom_fit(None))
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
t = threading.Thread(target=thread_func, args=(self,))
t.daemon = True
t.start()
def get_eval(self, widget_name):
"""
Runs eval() on the on the text entry of name 'widget_name'
and returns the results.
:param widget_name: Name of Gtk.Entry
:type widget_name: str
:return: Depends on contents of the entry text.
"""
value = self.builder.get_object(widget_name).get_text()
if value == "":
value = "None"
try:
evald = eval(value)
return evald
except:
self.info("Could not evaluate: " + value)
return None
def set_list_selection(self, name):
"""
Marks a given object as selected in the list ob objects
in the GUI. This selection will in turn trigger
``self.on_tree_selection_changed()``.
:param name: Name of the object.
:type name: str
:return: None
"""
iter = self.store.get_iter_first()
while iter is not None and self.store[iter][0] != name:
iter = self.store.iter_next(iter)
self.tree_select.unselect_all()
self.tree_select.select_iter(iter)
# Need to return False such that GLib.idle_add
# or .timeout_add do not repeat.
return False
def new_object(self, kind, name, initialize):
"""
Creates a new specalized FlatCAMObj and attaches it to the application,
this is, updates the GUI accordingly, any other records and plots it.
:param kind: The kind of object to create. One of 'gerber',
'excellon', 'cncjob' and 'geometry'.
:type kind: str
:param name: Name for the object.
:type name: str
:param initialize: Function to run after creation of the object
but before it is attached to the application. The function is
called with 2 parameters: the new object and the App instance.
:type initialize: function
:return: None
:rtype: None
"""
### Check for existing name
if name in self.stuff:
## Create a new name
# Ends with number?
match = re.search(r'(.*[^\d])?(\d+)$', name)
if match: # Yes: Increment the number!
base = match.group(1) or ''
num = int(match.group(2))
name = base + str(num + 1)
else: # No: add a number!
name += "_1"
# Create object
classdict = {
"gerber": FlatCAMGerber,
"excellon": FlatCAMExcellon,
"cncjob": FlatCAMCNCjob,
"geometry": FlatCAMGeometry
}
obj = classdict[kind](name)
obj.units = self.options["units"] # TODO: The constructor should look at defaults.
# Initialize as per user request
# User must take care to implement initialize
# in a thread-safe way as is is likely that we
# have been invoked in a separate thread.
#initialize(obj, self)
# Set default options from self.options
for option in self.options:
if option.find(kind + "_") == 0:
oname = option[len(kind)+1:]
obj.options[oname] = self.options[option]
# Initialize as per user request
# User must take care to implement initialize
# in a thread-safe way as is is likely that we
# have been invoked in a separate thread.
initialize(obj, self)
# Check units and convert if necessary
if self.options["units"].upper() != obj.units.upper():
GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
obj.convert_units(self.options["units"])
# Add to our records
self.stuff[name] = obj
# Update GUI list and select it (Thread-safe?)
self.store.append([name])
#self.build_list()
GLib.idle_add(lambda: self.set_list_selection(name))
# TODO: Gtk.notebook.set_current_page is not known to
# TODO: return False. Fix this??
GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
# Plot
# TODO: (Thread-safe?)
obj.plot()
# TODO: Threading dissaster!
GLib.idle_add(lambda: self.on_zoom_fit(None))
return obj
def set_progress_bar(self, percentage, text=""):
"""
Sets the application's progress bar to a given frac_digits and text.
:param percentage: The frac_digits (0.0-1.0) of the progress.
:type percentage: float
:param text: Text to display on the progress bar.
:type text: str
:return: None
"""
self.progress_bar.set_text(text)
self.progress_bar.set_fraction(percentage)
return False
def get_current(self):
"""
Returns the currently selected FlatCAMObj in the application.
:return: Currently selected FlatCAMObj in the application.
:rtype: FlatCAMObj or None
"""
# TODO: Could possibly read the form into the object here.
# But there are some cases when the form for the object
# is not up yet. See on_tree_selection_changed.
try:
return self.stuff[self.selected_item_name]
except:
return None
def load_defaults(self):
"""
Loads the aplication's default settings from defaults.json into
``self.defaults``.
:return: None
"""
try:
f = open("defaults.json")
options = f.read()
f.close()
except:
self.info("ERROR: Could not load defaults file.")
return
try:
defaults = json.loads(options)
except:
e = sys.exc_info()[0]
print e
self.info("ERROR: Failed to parse defaults file.")
return
self.defaults.update(defaults)
def read_form(self):
"""
Reads the options form into self.defaults/self.options.
:return: None
:rtype: None
"""
combo_sel = self.combo_options.get_active()
options_set = [self.options, self.defaults][combo_sel]
for option in options_set:
self.read_form_item(option, options_set)
def read_form_item(self, name, dest):
"""
Reads the value of a form item in the defaults/options form and
saves it to the corresponding dictionary.
:param name: Name of the form item. A key in ``self.defaults`` or
``self.options``.
:type name: str
:param dest: Dictionary to which to save the value.
:type dest: dict
:return: None
"""
fkind = self.form_kinds[name]
fname = fkind + "_" + "app" + "_" + name
if fkind == 'entry_text':
dest[name] = self.builder.get_object(fname).get_text()
return
if fkind == 'entry_eval':
dest[name] = self.get_eval(fname)
return
if fkind == 'cb':
dest[name] = self.builder.get_object(fname).get_active()
return
if fkind == 'radio':
dest[name] = self.get_radio_value(self.radios[name])
return
print "Unknown kind of form item:", fkind
def options2form(self):
"""
Sets the 'Project Options' or 'Application Defaults' form with values from
``self.options`` or ``self.defaults``.
:return: None
:rtype: None
"""
# Set the on-change callback to do nothing while we do the changes.
self.options_update_ignore = True
self.toggle_units_ignore = True
combo_sel = self.combo_options.get_active()
options_set = [self.options, self.defaults][combo_sel]
for option in options_set:
self.set_form_item(option, options_set[option])
self.options_update_ignore = False
self.toggle_units_ignore = False
def set_form_item(self, name, value):
"""
Sets a form item 'name' in the GUI with the given 'value'. The syntax of
form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
whatever name it's been given. For self.defaults, name is a key in the dictionary.
:param name: Name of the form field.
:type name: str
:param value: The value to set the form field to.
:type value: Depends on field kind.
:return: None
"""
if name not in self.form_kinds:
print "WARNING: Tried to set unknown option/form item:", name
return
fkind = self.form_kinds[name]
fname = fkind + "_" + "app" + "_" + name
if fkind == 'entry_eval' or fkind == 'entry_text':
try:
self.builder.get_object(fname).set_text(str(value))
except:
print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
return
if fkind == 'cb':
try:
self.builder.get_object(fname).set_active(value)
except:
print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
return
if fkind == 'radio':
try:
self.builder.get_object(self.radios_inv[name][value]).set_active(True)
except:
print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
return
print "Unknown kind of form item:", fkind
def save_project(self, filename):
"""
Saves the current project to the specified file.
:param filename: Name of the file in which to save.
:type filename: str
:return: None
"""
# Capture the latest changes
try:
self.get_current().read_form()
except:
pass
d = {"objs": [self.stuff[o].to_dict() for o in self.stuff],
"options": self.options}
try:
f = open(filename, 'w')
except:
print "ERROR: Failed to open file for saving:", filename
return
try:
json.dump(d, f, default=to_dict)
except:
print "ERROR: File open but failed to write:", filename
f.close()
return
f.close()
def open_project(self, filename):
"""
Loads a project from the specified file.
:param filename: Name of the file from which to load.
:type filename: str
:return: None
"""
try:
f = open(filename, 'r')
except:
#print "WARNING: Failed to open project file:", filename
self.info("ERROR: Failed to open project file: %s" % filename)
return
try:
d = json.load(f, object_hook=dict2obj)
except:
#print sys.exc_info()
#print "WARNING: Failed to parse project file:", filename
self.info("ERROR: Failed to parse project file: %s" % filename)
f.close()
return
# Clear the current project
self.on_file_new(None)
# Project options
self.options.update(d['options'])
self.project_filename = filename
self.units_label.set_text(self.options["units"])
# Re create objects
for obj in d['objs']:
def obj_init(obj_inst, app_inst):
obj_inst.from_dict(obj)
self.new_object(obj['kind'], obj['options']['name'], obj_init)
self.info("Project loaded from: " + filename)
def populate_objects_combo(self, combo):
"""
Populates a Gtk.Comboboxtext with the list of the object in the project.
:param combo: Name or instance of the comboboxtext.
:type combo: str or Gtk.ComboBoxText
:return: None
"""
print "Populating combo!"
if type(combo) == str:
combo = self.builder.get_object(combo)
combo.remove_all()
for obj in self.stuff:
combo.append_text(obj)
def versionCheck(self, *args):
"""
Checks for the latest version of the program. Alerts the
user if theirs is outdated. This method is meant to be run
in a saeparate thread.
:return: None
"""
try:
f = urllib.urlopen("http://caram.cl/flatcam/VERSION") # TODO: Hardcoded.
except:
GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
return
try:
data = json.load(f)
except:
GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
f.close()
return
f.close()
if self.version >= data["version"]:
GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
return
label = Gtk.Label("There is a newer version of FlatCAM\n" +
"available for download:\n\n" +
data["name"] + "\n\n" + data["message"])
dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK))
dialog.set_default_size(150, 100)
dialog.set_modal(True)
box = dialog.get_content_area()
box.set_border_width(10)
box.add(label)
def do_dialog():
dialog.show_all()
response = dialog.run()
dialog.destroy()
GLib.idle_add(lambda: do_dialog())
return
def setup_tooltips(self):
tooltips = {
"cb_gerber_plot": "Plot this object on the main window.",
"cb_gerber_mergepolys": "Show overlapping polygons as single.",
"cb_gerber_solid": "Paint inside polygons.",
"cb_gerber_multicolored": "Draw polygons with different colors."
}
for widget in tooltips:
self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
def do_nothing(self, param):
return
########################################
## EVENT HANDLERS ##
########################################
def on_offset_object(self, widget):
"""
Offsets the object's geometry by the vector specified
in the form. Re-plots.
:param widget: Ignored
:return: None
"""
obj = self.get_current()
obj.read_form()
assert isinstance(obj, FlatCAMObj)
try:
vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
except:
self.info("ERROR: Vector is not in (x, y) format.")
return
assert isinstance(obj, Geometry)
obj.offset(vect)
obj.plot()
return
def on_cb_plot_toggled(self, widget):
"""
Callback for toggling the "Plot" checkbox. Re-plots.
:param widget: Ignored.
:return: None
"""
self.get_current().read_form()
self.get_current().plot()
def on_about(self, widget):
"""
Opens the 'About' dialog box.
:param widget: Ignored.
:return: None
"""
about = self.builder.get_object("aboutdialog")
response = about.run()
#about.destroy()
about.hide()
def on_create_mirror(self, widget):
"""
Creates a mirror image of an object to be used as a bottom layer.
:param widget: Ignored.
:return: None
"""
# TODO: Move (some of) this to camlib!
# Object to mirror
try:
obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
fcobj = self.stuff[obj_name]
except KeyError:
self.info("WARNING: Cannot mirror that object.")
return
# 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.")
return
# Mirror axis "X" or "Y
axis = self.get_radio_value({"rb_mirror_x": "X",
"rb_mirror_y": "Y"})
mode = self.get_radio_value({"rb_mirror_box": "box",
"rb_mirror_point": "point"})
if mode == "point": # A single point defines the mirror axis
# TODO: Error handling
px, py = eval(self.point_entry.get_text())
else: # The axis is the line dividing the box in the middle
name = self.box_combo.get_active_text()
bb_obj = self.stuff[name]
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5*(xmin+xmax)
py = 0.5*(ymin+ymax)
# Do the mirroring
# xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
# mirrored = affinity.scale(fcobj.solid_geometry, xscale, yscale, origin=(px, py))
#
# def obj_init(obj_inst, app_inst):
# obj_inst.solid_geometry = mirrored
#
# self.new_object("gerber", fcobj.options["name"] + "_mirror", obj_init)
fcobj.mirror(axis, [px, py])
fcobj.plot()
#self.on_update_plot(None)
def on_create_aligndrill(self, widget):
"""
Creates alignment holes Excellon object. Creates mirror duplicates
of the specified holes around the specified axis.
:param widget: Ignored.
:return: None
"""
# Mirror axis. Same as in on_create_mirror.
axis = self.get_radio_value({"rb_mirror_x": "X",
"rb_mirror_y": "Y"})
# TODO: Error handling
mode = self.get_radio_value({"rb_mirror_box": "box",
"rb_mirror_point": "point"})
if mode == "point":
px, py = eval(self.point_entry.get_text())
else:
name = self.box_combo.get_active_text()
bb_obj = self.stuff[name]
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5*(xmin+xmax)
py = 0.5*(ymin+ymax)
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
# Tools
dia = self.get_eval("entry_dblsided_alignholediam")
tools = {"1": {"C": dia}}
# Parse hole list
# TODO: Better parsing
holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
holes = eval("[" + holes + "]")
drills = []
for hole in holes:
point = Point(hole)
point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
drills.append({"point": point, "tool": "1"})
drills.append({"point": point_mirror, "tool": "1"})
def obj_init(obj_inst, app_inst):
obj_inst.tools = tools
obj_inst.drills = drills
obj_inst.create_geometry()
self.new_object("excellon", "Alignment Drills", obj_init)
def on_toggle_pointbox(self, widget):
"""
Callback for radio selection change between point and box in the
Double-sided PCB tool. Updates the UI accordingly.
:param widget: Ignored.
:return: None
"""
# Where the entry or combo go
box = self.builder.get_object("box_pointbox")
# Clear contents
children = box.get_children()
for child in children:
box.remove(child)
choice = self.get_radio_value({"rb_mirror_point": "point",
"rb_mirror_box": "box"})
if choice == "point":
self.point_entry = Gtk.Entry()
self.builder.get_object("box_pointbox").pack_start(self.point_entry,
False, False, 1)
self.point_entry.show()
else:
self.box_combo = Gtk.ComboBoxText()
self.builder.get_object("box_pointbox").pack_start(self.box_combo,
False, False, 1)
self.populate_objects_combo(self.box_combo)
self.box_combo.show()
def on_tools_doublesided(self, param):
"""
Callback for menu item Tools->Double Sided PCB Tool. Launches the
tool placing its UI in the "Tool" tab in the notebook.
:param param: Ignored.
:return: None
"""
# Were are we drawing the UI
box_tool = self.builder.get_object("box_tool")
# Remove anything else in the box
box_children = box_tool.get_children()
for child in box_children:
box_tool.remove(child)
# Get the UI
osw = self.builder.get_object("offscreenwindow_dblsided")
sw = self.builder.get_object("sw_dblsided")
osw.remove(sw)
vp = self.builder.get_object("vp_dblsided")
vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
# Put in the UI
box_tool.pack_start(sw, True, True, 0)
# INITIALIZATION
# Populate combo box
self.populate_objects_combo("comboboxtext_bottomlayer")
# Point entry
self.point_entry = Gtk.Entry()
box = self.builder.get_object("box_pointbox")
for child in box.get_children():
box.remove(child)
box.pack_start(self.point_entry, False, False, 1)
# Show the "Tool" tab
self.notebook.set_current_page(3)
sw.show_all()
def on_toggle_units(self, widget):
"""
Callback for the Units radio-button change in the Options tab.
Changes the application's default units or the current project's units.
If changing the project's units, the change propagates to all of
the objects in the project.
:param widget: Ignored.
:return: None
"""
if self.toggle_units_ignore:
return
combo_sel = self.combo_options.get_active()
options_set = [self.options, self.defaults][combo_sel]
# Options to scale
dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
'geometry_paintmargin']
def scale_options(factor):
for dim in dimensions:
options_set[dim] *= factor
# The scaling factor depending on choice of units.
factor = 1/25.4
if self.builder.get_object('rb_mm').get_active():
factor = 25.4
# App units. Convert without warning.
if combo_sel == 1:
self.read_form()
scale_options(factor)
self.options2form()
return
# Changing project units. Warn user.
label = Gtk.Label("Changing the units of the project causes all geometrical \n" + \
"properties of all objects to be scaled accordingly. Continue?")
dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK))
dialog.set_default_size(150, 100)
dialog.set_modal(True)
box = dialog.get_content_area()
box.set_border_width(10)
box.add(label)
dialog.show_all()
response = dialog.run()
dialog.destroy()
if response == Gtk.ResponseType.OK:
#print "Converting units..."
#print "Converting options..."
self.read_form()
scale_options(factor)
self.options2form()
for obj in self.stuff:
units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
#print "Converting ", obj, " to ", units
self.stuff[obj].convert_units(units)
current = self.get_current()
if current is not None:
current.to_form()
self.plot_all()
else:
# Undo toggling
self.toggle_units_ignore = True
if self.builder.get_object('rb_mm').get_active():
self.builder.get_object('rb_inch').set_active(True)
else:
self.builder.get_object('rb_mm').set_active(True)
self.toggle_units_ignore = False
self.read_form()
self.info("Converted units to %s" % self.options["units"])
self.units_label.set_text("[" + self.options["units"] + "]")
def on_file_openproject(self, param):
"""
Callback for menu item File->Open Project. Opens a file chooser and calls
``self.open_project()`` after successful selection of a filename.
:param param: Ignored.
:return: None
"""
def on_success(app_obj, filename):
app_obj.open_project(filename)
self.file_chooser_action(on_success)
def on_file_saveproject(self, param):
"""
Callback for menu item File->Save Project. Saves the project to
``self.project_filename`` or calls ``self.on_file_saveprojectas()``
if set to None. The project is saved by calling ``self.save_project()``.
:param param: Ignored.
:return: None
"""
if self.project_filename is None:
self.on_file_saveprojectas(None)
else:
self.save_project(self.project_filename)
self.info("Project saved to: " + self.project_filename)
def on_file_saveprojectas(self, param):
"""
Callback for menu item File->Save Project As... Opens a file
chooser and saves the project to the given file via
``self.save_project()``.
:param param: Ignored.
:return: None
"""
def on_success(app_obj, filename):
assert isinstance(app_obj, App)
app_obj.save_project(filename)
self.project_filename = filename
app_obj.info("Project saved to: " + filename)
self.file_chooser_save_action(on_success)
def on_file_saveprojectcopy(self, param):
"""
Callback for menu item File->Save Project Copy... Opens a file
chooser and saves the project to the given file via
``self.save_project``. It does not update ``self.project_filename`` so
subsequent save requests are done on the previous known filename.
:param param: Ignore.
:return: None
"""
def on_success(app_obj, filename):
assert isinstance(app_obj, App)
app_obj.save_project(filename)
app_obj.info("Project copy saved to: " + filename)
self.file_chooser_save_action(on_success)
def on_options_app2project(self, param):
"""
Callback for Options->Transfer Options->App=>Project. Copies options
from application defaults to project defaults.
:param param: Ignored.
:return: None
"""
self.options.update(self.defaults)
self.options2form() # Update UI
def on_options_project2app(self, param):
"""
Callback for Options->Transfer Options->Project=>App. Copies options
from project defaults to application defaults.
:param param: Ignored.
:return: None
"""
self.defaults.update(self.options)
self.options2form() # Update UI
def on_options_project2object(self, param):
"""
Callback for Options->Transfer Options->Project=>Object. Copies options
from project defaults to the currently selected object.
:param param: Ignored.
:return: None
"""
obj = self.get_current()
if obj is None:
self.info("WARNING: No object selected.")
return
for option in self.options:
if option.find(obj.kind + "_") == 0:
oname = option[len(obj.kind)+1:]
obj.options[oname] = self.options[option]
obj.to_form() # Update UI
def on_options_object2project(self, param):
"""
Callback for Options->Transfer Options->Object=>Project. Copies options
from the currently selected object to project defaults.
:param param: Ignored.
:return: None
"""
obj = self.get_current()
if obj is None:
self.info("WARNING: No object selected.")
return
obj.read_form()
for option in obj.options:
if option in ['name']: # TODO: Handle this better...
continue
self.options[obj.kind + "_" + option] = obj.options[option]
self.options2form() # Update UI
def on_options_object2app(self, param):
"""
Callback for Options->Transfer Options->Object=>App. Copies options
from the currently selected object to application defaults.
:param param: Ignored.
:return: None
"""
obj = self.get_current()
if obj is None:
self.info("WARNING: No object selected.")
return
obj.read_form()
for option in obj.options:
if option in ['name']: # TODO: Handle this better...
continue
self.defaults[obj.kind + "_" + option] = obj.options[option]
self.options2form() # Update UI
def on_options_app2object(self, param):
"""
Callback for Options->Transfer Options->App=>Object. Copies options
from application defaults to the currently selected object.
:param param: Ignored.
:return: None
"""
obj = self.get_current()
if obj is None:
self.info("WARNING: No object selected.")
return
for option in self.defaults:
if option.find(obj.kind + "_") == 0:
oname = option[len(obj.kind)+1:]
obj.options[oname] = self.defaults[option]
obj.to_form() # Update UI
def on_file_savedefaults(self, param):
"""
Callback for menu item File->Save Defaults. Saves application default options
``self.defaults`` to defaults.json.
:param param: Ignored.
:return: None
"""
# Read options from file
try:
f = open("defaults.json")
options = f.read()
f.close()
except:
self.info("ERROR: Could not load defaults file.")
return
try:
defaults = json.loads(options)
except:
e = sys.exc_info()[0]
print e
self.info("ERROR: Failed to parse defaults file.")
return
# Update options
assert isinstance(defaults, dict)
defaults.update(self.defaults)
# Save update options
try:
f = open("defaults.json", "w")
json.dump(defaults, f)
f.close()
except:
self.info("ERROR: Failed to write defaults to file.")
return
self.info("Defaults saved.")
def on_options_combo_change(self, widget):
"""
Called when the combo box to choose between application defaults and
project option changes value. The corresponding variables are
copied to the UI.
:param widget: The widget from which this was called. Ignore.
:return: None
"""
#combo_sel = self.combo_options.get_active()
#print "Options --> ", combo_sel
self.options2form()
def on_options_update(self, widget):
"""
Called whenever a value in the options/defaults form changes.
All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
which may be necessary when updating the UI from code and not by the user.
:param widget: The widget from which this was called. Ignore.
:return: None
"""
if self.options_update_ignore:
return
self.read_form()
def on_scale_object(self, widget):
"""
Callback for request to change an objects geometry scale. The object
is re-scaled and replotted.
:param widget: Ignored.
:return: None
"""
obj = self.get_current()
factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
obj.scale(factor)
obj.to_form()
self.on_update_plot(None)
def on_canvas_configure(self, widget, event):
"""
Called whenever the canvas changes size. The axes are updated such
as to use the whole canvas.
:param widget: Ignored.
:param event: Ignored.
:return: None
"""
self.plotcanvas.auto_adjust_axes()
def on_row_activated(self, widget, path, col):
"""
Callback for selection activation (Enter or double-click) on the Project list.
Switches the notebook page to the object properties form. Calls
``self.notebook.set_current_page(1)``.
:param widget: Ignored.
:param path: Ignored.
:param col: Ignored.
:return: None
"""
self.notebook.set_current_page(1)
def on_generate_gerber_bounding_box(self, widget):
"""
Callback for request from the Gerber form to generate a bounding box for the
geometry in the object. Creates a FlatCAMGeometry with the bounding box.
The box can have rounded corners if specified in the form.
:param widget: Ignored.
:return: None
"""
# TODO: Use Gerber.get_bounding_box(...)
gerber = self.get_current()
gerber.read_form()
name = gerber.options["name"] + "_bbox"
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
# Bounding box with rounded corners
bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
if not gerber.options["bboxrounded"]: # Remove rounded corners
bounding_box = bounding_box.envelope
geo_obj.solid_geometry = bounding_box
self.new_object("geometry", name, geo_init)
def on_update_plot(self, widget):
"""
Callback for button on form for all kinds of objects.
Re-plots the current object only.
:param widget: The widget from which this was called. Ignored.
:return: None
"""
obj = self.get_current()
obj.read_form()
self.set_progress_bar(0.5, "Plotting...")
def thread_func(app_obj):
assert isinstance(app_obj, App)
#GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
#GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
obj.plot()
#GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
t = threading.Thread(target=thread_func, args=(self,))
t.daemon = True
t.start()
def on_generate_excellon_cncjob(self, widget):
"""
Callback for button active/click on Excellon form to
create a CNC Job for the Excellon file.
:param widget: Ignored
:return: None
"""
excellon = self.get_current()
excellon.read_form()
job_name = excellon.options["name"] + "_cnc"
# Object initialization function for app.new_object()
def job_init(job_obj, app_obj):
# excellon_ = self.get_current()
# assert isinstance(excellon_, FlatCAMExcellon)
assert isinstance(job_obj, FlatCAMCNCjob)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
job_obj.z_cut = excellon.options["drillz"]
job_obj.z_move = excellon.options["travelz"]
job_obj.feedrate = excellon.options["feedrate"]
# There could be more than one drill size...
# job_obj.tooldia = # TODO: duplicate variable!
# job_obj.options["tooldia"] =
job_obj.generate_from_excellon_by_tool(excellon, excellon.options["toolselection"])
GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
job_obj.gcode_parse()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
job_obj.create_geometry()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
# To be run in separate thread
def job_thread(app_obj):
app_obj.new_object("cncjob", job_name, job_init)
GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
# Start the thread
t = threading.Thread(target=job_thread, args=(self,))
t.daemon = True
t.start()
def on_excellon_tool_choose(self, widget):
"""
Callback for button on Excellon form to open up a window for
selecting tools.
:param widget: The widget from which this was called.
:return: None
"""
excellon = self.get_current()
assert isinstance(excellon, FlatCAMExcellon)
excellon.show_tool_chooser()
def on_entry_eval_activate(self, widget):
"""
Called when an entry is activated (eg. by hitting enter) if
set to do so. Its text is eval()'d and set to the returned value.
The current object is updated.
:param widget:
:return:
"""
self.on_eval_update(widget)
obj = self.get_current()
assert isinstance(obj, FlatCAMObj)
obj.read_form()
def on_gerber_generate_noncopper(self, widget):
"""
Callback for button on Gerber form to create a geometry object
with polygons covering the area without copper or negative of the
Gerber.
:param widget: The widget from which this was called.
:return: None
"""
gerb = self.get_current()
gerb.read_form()
name = gerb.options["name"] + "_noncopper"
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
if not gerb.options["noncopperrounded"]:
bounding_box = bounding_box.envelope
non_copper = bounding_box.difference(gerb.solid_geometry)
geo_obj.solid_geometry = non_copper
# TODO: Check for None
self.new_object("geometry", name, geo_init)
def on_gerber_generate_cutout(self, widget):
"""
Callback for button on Gerber form to create geometry with lines
for cutting off the board.
:param widget: The widget from which this was called.
:return: None
"""
gerb = self.get_current()
gerb.read_form()
name = gerb.options["name"] + "_cutout"
def geo_init(geo_obj, app_obj):
margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
minx, miny, maxx, maxy = gerb.bounds()
minx -= margin
maxx += margin
miny -= margin
maxy += margin
midx = 0.5 * (minx + maxx)
midy = 0.5 * (miny + maxy)
hgap = 0.5 * gap_size
pts = [[midx - hgap, maxy],
[minx, maxy],
[minx, midy + hgap],
[minx, midy - hgap],
[minx, miny],
[midx - hgap, miny],
[midx + hgap, miny],
[maxx, miny],
[maxx, midy - hgap],
[maxx, midy + hgap],
[maxx, maxy],
[midx + hgap, maxy]]
cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
[pts[6], pts[7], pts[10], pts[11]]],
"lr": [[pts[9], pts[10], pts[1], pts[2]],
[pts[3], pts[4], pts[7], pts[8]]],
"4": [[pts[0], pts[1], pts[2]],
[pts[3], pts[4], pts[5]],
[pts[6], pts[7], pts[8]],
[pts[9], pts[10], pts[11]]]}
cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
# TODO: Check for None
self.new_object("geometry", name, geo_init)
def on_eval_update(self, widget):
"""
Modifies the content of a Gtk.Entry by running
eval() on its contents and puting it back as a
string.
:param widget: The widget from which this was called.
:return: None
"""
# TODO: error handling here
widget.set_text(str(eval(widget.get_text())))
def on_generate_isolation(self, widget):
"""
Callback for button on Gerber form to create isolation routing geometry.
:param widget: The widget from which this was called.
:return: None
"""
gerb = self.get_current()
gerb.read_form()
dia = gerb.options["isotooldia"]
passes = int(gerb.options["isopasses"])
overlap = gerb.options["isooverlap"] * dia
for i in range(passes):
offset = (2*i + 1)/2.0 * dia - i*overlap
iso_name = gerb.options["name"] + "_iso%d" % (i+1)
# TODO: This is ugly. Create way to pass data into init function.
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = gerb.options["isotooldia"]
geo_obj.solid_geometry = gerb.isolation_geometry(offset)
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
# TODO: Do something if this is None. Offer changing name?
self.new_object("geometry", iso_name, iso_init)
def on_generate_cncjob(self, widget):
"""
Callback for button on geometry form to generate CNC job.
:param widget: The widget from which this was called.
:return: None
"""
source_geo = self.get_current()
source_geo.read_form()
job_name = source_geo.options["name"] + "_cnc"
# Object initialization function for app.new_object()
# RUNNING ON SEPARATE THREAD!
def job_init(job_obj, app_obj):
assert isinstance(job_obj, FlatCAMCNCjob)
# Propagate options
job_obj.options["tooldia"] = source_geo.options["cnctooldia"]
GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
job_obj.z_cut = source_geo.options["cutz"]
job_obj.z_move = source_geo.options["travelz"]
job_obj.feedrate = source_geo.options["feedrate"]
GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
# TODO: The tolerance should not be hard coded. Just for testing.
job_obj.generate_from_geometry(source_geo, tolerance=0.0005)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
job_obj.gcode_parse()
# TODO: job_obj.create_geometry creates stuff that is not used.
#GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
#job_obj.create_geometry()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
# To be run in separate thread
def job_thread(app_obj):
app_obj.new_object("cncjob", job_name, job_init)
GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
# Start the thread
t = threading.Thread(target=job_thread, args=(self,))
t.daemon = True
t.start()
def on_generate_paintarea(self, widget):
"""
Callback for button on geometry form.
Subscribes to the "Click on plot" event and continues
after the click. Finds the polygon containing
the clicked point and runs clear_poly() on it, resulting
in a new FlatCAMGeometry object.
:param widget: The widget from which this was called.
:return: None
"""
self.info("Click inside the desired polygon.")
geo = self.get_current()
geo.read_form()
assert isinstance(geo, FlatCAMGeometry)
tooldia = geo.options["painttooldia"]
overlap = geo.options["paintoverlap"]
# To be called after clicking on the plot.
def doit(event):
self.plot_click_subscribers.pop("generate_paintarea")
self.info("")
point = [event.xdata, event.ydata]
poly = find_polygon(geo.solid_geometry, point)
# Initializes the new geometry object
def gen_paintarea(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
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)
self.plot_click_subscribers["generate_paintarea"] = doit
def on_cncjob_exportgcode(self, widget):
"""
Called from button on CNCjob form to save the G-Code from the object.
:param widget: The widget from which this was called.
:return: None
"""
def on_success(app_obj, filename):
cncjob = app_obj.get_current()
f = open(filename, 'w')
f.write(cncjob.gcode)
f.close()
app_obj.info("Saved to: " + filename)
self.file_chooser_save_action(on_success)
def on_delete(self, widget):
"""
Delete the currently selected FlatCAMObj.
:param widget: The widget from which this was called.
:return: None
"""
# Keep this for later
name = copy.copy(self.selected_item_name)
# Remove plot
self.plotcanvas.figure.delaxes(self.get_current().axes)
self.plotcanvas.auto_adjust_axes()
# Remove from dictionary
self.stuff.pop(self.selected_item_name)
# Update UI
self.build_list() # Update the items list
self.info("Object deleted: %s" % name)
def on_toolbar_replot(self, widget):
"""
Callback for toolbar button. Re-plots all objects.
:param widget: The widget from which this was called.
:return: None
"""
self.get_current().read_form()
self.plot_all()
def on_clear_plots(self, widget):
"""
Callback for toolbar button. Clears all plots.
:param widget: The widget from which this was called.
:return: None
"""
self.plotcanvas.clear()
def on_activate_name(self, entry):
"""
Hitting 'Enter' after changing the name of an item
updates the item dictionary and re-builds the item list.
:param entry: The widget from which this was called.
:return: None
"""
# Disconnect event listener
self.tree.get_selection().disconnect(self.signal_id)
new_name = entry.get_text() # Get from form
self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
self.stuff[new_name].options["name"] = new_name # update object
self.info('Name change: ' + self.selected_item_name + " to " + new_name)
self.selected_item_name = new_name # Update selection name
self.build_list() # Update the items list
# Reconnect event listener
self.signal_id = self.tree.get_selection().connect(
"changed", self.on_tree_selection_changed)
def on_tree_selection_changed(self, selection):
"""
Callback for selection change in the project list. This changes
the currently selected FlatCAMObj.
:param selection: Selection associated to the project tree or list
:type selection: Gtk.TreeSelection
:return: None
"""
print "DEBUG: on_tree_selection_change(): ",
model, treeiter = selection.get_selected()
if treeiter is not None:
# Save data for previous selection
obj = self.get_current()
if obj is not None:
obj.read_form()
print "DEBUG: You selected", model[treeiter][0]
self.selected_item_name = model[treeiter][0]
obj_new = self.get_current()
if obj_new is not None:
GLib.idle_add(lambda: obj_new.build_ui())
else:
print "DEBUG: Nothing selected"
self.selected_item_name = None
self.setup_component_editor()
def on_file_new(self, param):
"""
Callback for menu item File->New. Returns the application to its
startup state.
:param param: Whatever is passed by the event. Ignore.
:return: None
"""
# Remove everythong from memory
# Clear plot
self.plotcanvas.clear()
# Clear object editor
#self.setup_component_editor()
# Clear data
self.stuff = {}
# Clear list
#self.tree_select.unselect_all()
self.build_list()
# Clear project filename
self.project_filename = None
# Re-fresh project options
self.on_options_app2project(None)
def on_filequit(self, param):
"""
Callback for menu item File->Quit. Closes the application.
:param param: Whatever is passed by the event. Ignore.
:return: None
"""
self.window.destroy()
Gtk.main_quit()
def on_closewindow(self, param):
"""
Callback for closing the main window.
:param param: Whatever is passed by the event. Ignore.
:return: None
"""
self.window.destroy()
Gtk.main_quit()
def file_chooser_action(self, on_success):
"""
Opens the file chooser and runs on_success on a separate thread
upon completion of valid file choice.
:param on_success: A function to run upon completion of a valid file
selection. Takes 2 parameters: The app instance and the filename.
Note that it is run on a separate thread, therefore it must take the
appropriate precautions when accessing shared resources.
:type on_success: func
:return: None
"""
dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
response = dialog.run()
if response == Gtk.ResponseType.OK:
filename = dialog.get_filename()
dialog.destroy()
t = threading.Thread(target=on_success, args=(self, filename))
t.daemon = True
t.start()
#on_success(self, filename)
elif response == Gtk.ResponseType.CANCEL:
self.info("Open cancelled.") # print("Cancel clicked")
dialog.destroy()
def file_chooser_save_action(self, on_success):
"""
Opens the file chooser and runs on_success upon completion of valid file choice.
:param on_success: A function to run upon selection of a filename. Takes 2
parameters: The instance of the application (App) and the chosen filename. This
gets run immediately in the same thread.
:return: None
"""
dialog = Gtk.FileChooserDialog("Save file", self.window,
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
dialog.set_current_name("Untitled")
response = dialog.run()
if response == Gtk.ResponseType.OK:
filename = dialog.get_filename()
dialog.destroy()
on_success(self, filename)
elif response == Gtk.ResponseType.CANCEL:
self.info("Save cancelled.") # print("Cancel clicked")
dialog.destroy()
def on_fileopengerber(self, param):
"""
Callback for menu item File->Open Gerber. Defines a function that is then passed
to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
and updates the progress bar throughout the process.
:param param: Ignore
:return: None
"""
# IMPORTANT: on_success will run on a separate thread. Use
# GLib.idle_add(function, **kwargs) to launch actions that will
# updata the GUI.
def on_success(app_obj, filename):
assert isinstance(app_obj, App)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
def obj_init(gerber_obj, app_obj):
assert isinstance(gerber_obj, FlatCAMGerber)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
gerber_obj.parse_file(filename)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
gerber_obj.create_geometry()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
name = filename.split('/')[-1].split('\\')[-1]
app_obj.new_object("gerber", name, obj_init)
GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
# on_success gets run on a separate thread
self.file_chooser_action(on_success)
def on_fileopenexcellon(self, param):
"""
Callback for menu item File->Open Excellon. Defines a function that is then passed
to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
and updates the progress bar throughout the process.
:param param: Ignore
:return: None
"""
# IMPORTANT: on_success will run on a separate thread. Use
# GLib.idle_add(function, **kwargs) to launch actions that will
# updata the GUI.
def on_success(app_obj, filename):
assert isinstance(app_obj, App)
GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
def obj_init(excellon_obj, app_obj):
GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
excellon_obj.parse_file(filename)
excellon_obj.create_geometry()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
name = filename.split('/')[-1].split('\\')[-1]
app_obj.new_object("excellon", name, obj_init)
GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
# on_success gets run on a separate thread
self.file_chooser_action(on_success)
def on_fileopengcode(self, param):
"""
Callback for menu item File->Open G-Code. Defines a function that is then passed
to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
and updates the progress bar throughout the process.
:param param: Ignore
:return: None
"""
# IMPORTANT: on_success will run on a separate thread. Use
# GLib.idle_add(function, **kwargs) to launch actions that will
# updata the GUI.
def on_success(app_obj, filename):
assert isinstance(app_obj, App)
def obj_init(job_obj, app_obj_):
"""
:type app_obj_: App
"""
assert isinstance(app_obj_, App)
GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
f = open(filename)
gcode = f.read()
f.close()
job_obj.gcode = gcode
GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
job_obj.gcode_parse()
GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
job_obj.create_geometry()
GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
name = filename.split('/')[-1].split('\\')[-1]
app_obj.new_object("cncjob", name, obj_init)
GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
# on_success gets run on a separate thread
self.file_chooser_action(on_success)
def on_mouse_move_over_plot(self, event):
"""
Callback for the mouse motion event over the plot. This event is generated
by the Matplotlib backend and has been registered in ``self.__init__()``.
For details, see: http://matplotlib.org/users/event_handling.html
:param event: Contains information about the event.
:return: None
"""
try: # May fail in case mouse not within axes
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
def on_click_over_plot(self, event):
"""
Callback for the mouse click event over the plot. This event is generated
by the Matplotlib backend and has been registered in ``self.__init__()``.
For details, see: http://matplotlib.org/users/event_handling.html
Default actions are:
* Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
:param event: Contains information about the event, like which button
was clicked, the pixel coordinates and the axes coordinates.
:return: None
"""
# For key presses
self.plotcanvas.canvas.grab_focus()
try:
print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
event.button, event.x, event.y, event.xdata, event.ydata)
# TODO: This custom subscription mechanism is probably not necessary.
for subscriber in self.plot_click_subscribers:
self.plot_click_subscribers[subscriber](event)
self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
except Exception, e:
print "Outside plot!"
def on_zoom_in(self, event):
"""
Callback for zoom-in request. This can be either from the corresponding
toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
:param event: Ignored.
:return: None
"""
self.plotcanvas.zoom(1.5)
return
def on_zoom_out(self, event):
"""
Callback for zoom-out request. This can be either from the corresponding
toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
:param event: Ignored.
:return: None
"""
self.plotcanvas.zoom(1 / 1.5)
def on_zoom_fit(self, event):
"""
Callback for zoom-out request. This can be either from the corresponding
toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
with axes limits from the geometry bounds of all objects.
:param event: Ignored.
:return: None
"""
xmin, ymin, xmax, ymax = get_bounds(self.stuff)
width = xmax - xmin
height = ymax - ymin
xmin -= 0.05 * width
xmax += 0.05 * width
ymin -= 0.05 * height
ymax += 0.05 * height
self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
def on_key_over_plot(self, event):
"""
Callback for the key pressed event when the canvas is focused. Keyboard
shortcuts are handled here. So far, these are the shortcuts:
========== ============================================
Key Action
========== ============================================
'1' Zoom-fit. Fits the axes limits to the data.
'2' Zoom-out.
'3' Zoom-in.
========== ============================================
:param event: Ignored.
:return: None
"""
#print 'you pressed', event.key, event.xdata, event.ydata
if event.key == '1': # 1
self.on_zoom_fit(None)
return
if event.key == '2': # 2
self.plotcanvas.zoom(1 / 1.5, self.mouse)
return
if event.key == '3': # 3
self.plotcanvas.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 BaseDraw:
def __init__(self, plotcanvas, name=None):
"""
:param plotcanvas: The PlotCanvas where the drawing tool will operate.
:type plotcanvas: PlotCanvas
"""
self.plotcanvas = plotcanvas
# Must have unique axes
charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
self.name = name or [random.choice(charset) for i in range(20)]
self.axes = self.plotcanvas.new_axes(self.name)
class DrawingObject(BaseDraw):
def __init__(self, plotcanvas, name=None):
"""
Possible objects are:
* Point
* Line
* Rectangle
* Circle
* Polygon
"""
BaseDraw.__init__(self, plotcanvas)
self.properties = {}
def plot(self):
return
def update_plot(self):
self.axes.cla()
self.plot()
self.plotcanvas.auto_adjust_axes()
class DrawingPoint(DrawingObject):
def __init__(self, plotcanvas, name=None, coord=None):
DrawingObject.__init__(self, plotcanvas)
self.properties.update({
"coordinate": coord
})
def plot(self):
x, y = self.properties["coordinate"]
self.axes.plot(x, y, 'o')
class Measurement:
def __init__(self, container, axes, click_subscibers, move_subscribers, update=None):
self.update = update
self.container = container
self.frame = None
self.label = None
self.click_subscribers = click_subscibers
self.move_subscribers = move_subscribers
self.point1 = None
self.point2 = None
self.active = False
def toggle_active(self, *args):
if self.active: # Deactivate
self.active = False
self.move_subscribers.pop("meas")
self.click_subscribers.pop("meas")
self.container.remove(self.frame)
if self.update is not None:
self.update()
return False
else: # Activate
print "DEBUG: Activating Measurement Tool..."
self.active = True
self.click_subscribers["meas"] = self.on_click
self.move_subscribers["meas"] = self.on_move
self.frame = Gtk.Frame()
self.frame.set_margin_right(5)
self.frame.set_margin_top(3)
align = Gtk.Alignment()
align.set(0, 0.5, 0, 0)
align.set_padding(4, 4, 4, 4)
self.label = Gtk.Label()
self.label.set_label("Click on a reference point...")
abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
abox.pack_start(self.label, False, False, 0)
align.add(abox)
self.frame.add(align)
self.container.pack_end(self.frame, False, True, 1)
self.frame.show_all()
return True
def on_move(self, event):
if self.point1 is None:
self.label.set_label("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.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
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)
self.on_move(event)
class PlotCanvas:
"""
Class handling the plotting area in the application.
"""
def __init__(self, container):
"""
The constructor configures the Matplotlib figure that
will contain all plots, creates the base axes and connects
events to the plotting area.
:param container: The parent container in which to draw plots.
:rtype: PlotCanvas
"""
# 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) # TODO: Height and width are num. columns??
# Events
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
self.canvas.connect('configure-event', self.auto_adjust_axes)
self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
self.canvas.connect("scroll-event", self.on_scroll)
self.mouse = [0,0]
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 all axes 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():
if ax._label != 'base':
ax.set_frame_on(False) # No frame
ax.set_xticks([]) # No tick
ax.set_yticks([]) # No ticks
ax.patch.set_visible(False) # No background
ax.set_aspect(1)
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, *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)
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 or center == [None, 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()
def new_axes(self, name):
"""
Creates and returns an Axes object attached to this object's Figure.
:param name: Unique label for the axes.
:return: Axes attached to the figure.
:rtype: Axes
"""
return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
# def plot_axes(self, axes):
#
# if axes not in self.figure.axes:
# self.figure.add_axes(axes)
#
# # Basic configuration
# axes.set_frame_on(False) # No frame
# axes.set_xticks([]) # No tick
# axes.set_yticks([]) # No ticks
# axes.patch.set_visible(False) # No background
# axes.set_aspect(1)
#
# # Adjust limits
# self.auto_adjust_axes()
def on_scroll(self, canvas, event):
"""
Scroll event handler.
:param canvas: The widget generating the event. Ignored.
:param event: Event object containing the event information.
:return: None
"""
z, direction = event.get_scroll_direction()
if direction is Gdk.ScrollDirection.UP:
self.zoom(1.5, self.mouse)
else:
self.zoom(1/1.5, self.mouse)
def on_mouse_move(self, event):
"""
Mouse movement event hadler.
:param event: Contains information about the event.
:return: None
"""
self.mouse = [event.xdata, event.ydata]
app = App()
Gtk.main()