flatcam/cirkuix.py

1501 lines
50 KiB
Python

import threading
from gi.repository import Gtk, Gdk, GLib, GObject
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 camlib import *
########################################
## CirkuixObj ##
########################################
class CirkuixObj:
"""
Base type of objects handled in Cirkuix. 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
"""
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 set_options(self, options):
for name in options:
self.options[name] = options[name]
return
def to_form(self):
for option in self.options:
self.set_form_item(option)
def read_form(self):
"""
Reads form into self.options
@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):
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):
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, figure):
"""
Extend this method! Sets up axes if needed and
clears them. Descendants must do the actual plotting.
"""
# Creates the axes if necessary and sets them up.
self.setup_axes(figure)
# Clear axes.
# self.axes.cla()
# return
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 CirkuixObj
@type obj_dict: dict
@return None
"""
return
class CirkuixGerber(CirkuixObj, Gerber):
"""
Represents Gerber code.
"""
def __init__(self, name):
Gerber.__init__(self)
CirkuixObj.__init__(self, name)
self.kind = "gerber"
# The 'name' is already in self.options from CirkuixObj
self.options.update({
"plot": True,
"mergepolys": True,
"multicolored": False,
"solid": False,
"isotooldia": 0.4/25.4,
"cutoutmargin": 0.2,
"cutoutgapsize": 0.15,
"gaps": "tb",
"noncoppermargin": 0.0,
"bboxmargin": 0.0,
"bboxrounded": False
})
# The 'name' is already in self.form_kinds from CirkuixObj
self.form_kinds.update({
"plot": "cb",
"mergepolys": "cb",
"multicolored": "cb",
"solid": "cb",
"isotooldia": "entry_eval",
"cutoutmargin": "entry_eval",
"cutoutgapsize": "entry_eval",
"gaps": "radio",
"noncoppermargin": "entry_eval",
"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"}}
def convert_units(self, units):
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, figure):
CirkuixObj.plot(self, figure)
self.create_geometry()
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-'
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.canvas.queue_draw()
def serialize(self):
return {
"options": self.options,
"kind": self.kind
}
class CirkuixExcellon(CirkuixObj, Excellon):
"""
Represents Excellon code.
"""
def __init__(self, name):
Excellon.__init__(self)
CirkuixObj.__init__(self, name)
self.kind = "excellon"
self.options.update({
"plot": True,
"solid": False,
"multicolored": False,
"drillz": -0.1,
"travelz": 0.1,
"feedrate": 5.0,
"toolselection": ""
})
self.form_kinds.update({
"plot": "cb",
"solid": "cb",
"multicolored": "cb",
"drillz": "entry_eval",
"travelz": "entry_eval",
"feedrate": "entry_eval",
"toolselection": "entry_text"
})
self.tool_cbs = {}
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, figure):
CirkuixObj.plot(self, figure)
#self.setup_axes(figure)
self.create_geometry()
# Plot excellon
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.on_zoom_fit(None)
self.app.canvas.queue_draw()
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+": "+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 tool in self.tool_cbs:
if self.tool_cbs[tool].get_active():
tool_list.append(tool)
self.options["toolselection"] = ", ".join(tool_list)
self.to_form()
button.connect("activate", on_accept)
button.connect("clicked", on_accept)
# def parse_lines(self, elines):
# Excellon.parse_lines(self, elines)
# self.options["units"] = self.units
class CirkuixCNCjob(CirkuixObj, 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)
CirkuixObj.__init__(self, name)
self.kind = "cncjob"
self.options.update({
"plot": True,
"solid": False,
"multicolored": False,
"tooldia": 0.4/25.4 # 0.4mm in inches
})
self.form_kinds.update({
"plot": "cb",
"solid": "cb",
"multicolored": "cb",
"tooldia": "entry_eval"
})
def plot(self, figure):
CirkuixObj.plot(self, figure)
#self.setup_axes(figure)
self.plot2(self.axes, tooldia=self.options["tooldia"])
self.app.on_zoom_fit(None)
self.app.canvas.queue_draw()
class CirkuixGeometry(CirkuixObj, Geometry):
"""
Geometric object not associated with a specific
format.
"""
def __init__(self, name):
CirkuixObj.__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"
})
# def convert_units(self, units):
# factor = Geometry.convert_units(self, units)
def scale(self, factor):
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 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, figure):
CirkuixObj.plot(self, figure)
#self.setup_axes(figure)
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.on_zoom_fit(None)
self.app.canvas.queue_draw()
########################################
## App ##
########################################
class App:
"""
The main application class. The constructor starts the GUI.
"""
def __init__(self):
"""
Starts the application and the Gtk.main().
@return: app
@rtype: App
"""
# Needed to interact with the GUI from other threads.
GObject.threads_init()
## GUI ##
self.gladefile = "cirkuix.ui"
self.builder = Gtk.Builder()
self.builder.add_from_file(self.gladefile)
self.window = self.builder.get_object("window1")
self.window.set_title("Cirkuix")
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")
## Event handling ##
self.builder.connect_signals(self)
## Make plot area ##
self.figure = None
self.axes = None
self.canvas = None
self.setup_plot()
self.setup_component_viewer()
self.setup_component_editor()
## DATA ##
self.setup_obj_classes()
self.stuff = {} # CirkuixObj'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
self.defaults = {
"units": "in"
} # Application defaults
self.options = {} # Project options
self.plot_click_subscribers = {}
# Initialization
self.load_defaults()
self.options.update(self.defaults)
self.units_label.set_text("[" + self.options["units"] + "]")
# For debugging only
def someThreadFunc(self):
print "Hello World!"
t = threading.Thread(target=someThreadFunc, args=(self,))
t.start()
########################################
## START ##
########################################
self.window.set_default_size(900, 600)
self.window.show_all()
def setup_plot(self):
"""
Sets up the main plotting area by creating a matplotlib
figure in self.canvas, adding axes and configuring them.
These axes should not be ploted on and are just there to
display the axes ticks and grid.
@return: None
"""
self.figure = Figure(dpi=50)
self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
self.axes.set_aspect(1)
#t = arange(0.0,5.0,0.01)
#s = sin(2*pi*t)
#self.axes.plot(t,s)
self.axes.grid(True)
self.figure.patch.set_visible(False)
self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
self.canvas.set_hexpand(1)
self.canvas.set_vexpand(1)
# Events
self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
self.canvas.set_can_focus(True) # For key press
self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
#self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
self.canvas.connect("configure-event", self.on_canvas_configure)
self.grid.attach(self.canvas, 0, 0, 600, 400)
def setup_obj_classes(self):
CirkuixObj.app = self
def setup_component_viewer(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("Title", 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.
@return: None
"""
self.info_label.set_text(text)
def zoom(self, factor, center=None):
"""
Zooms the plot by factor around a given
center point. Takes care of re-drawing.
@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
for name in self.stuff:
self.stuff[name].axes.set_xlim((xmin, xmax))
self.stuff[name].axes.set_ylim((ymin, ymax))
self.axes.set_xlim((xmin, xmax))
self.axes.set_ylim((ymin, ymax))
self.canvas.queue_draw()
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] if the radiobutton
whose name is key is active.
@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.clear_plots()
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(self.figure)
percentage += delta
GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
self.on_zoom_fit(None)
self.axes.grid(True)
self.canvas.queue_draw()
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 clear_plots(self):
"""
Clears self.axes and self.figure.
@return: None
"""
# TODO: Create a setup_axes method that gets called here and in setup_plot?
self.axes.cla()
self.figure.clf()
self.figure.add_axes(self.axes)
self.axes.set_aspect(1)
self.axes.grid(True)
self.canvas.queue_draw()
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
@return: Depends on contents of the entry text.
"""
value = self.builder.get_object(widget_name).get_text()
return eval(value)
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.
@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 repear.
return False
def new_object(self, kind, name, initialize):
"""
Creates a new specalized CirkuixObj 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:
self.info("Rename " + name + " in project first.")
return None
# Create object
classdict = {
"gerber": CirkuixGerber,
"excellon": CirkuixExcellon,
"cncjob": CirkuixCNCjob,
"geometry": CirkuixGeometry
}
obj = classdict[kind](name)
# 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(self.figure)
obj.axes.set_alpha(0.0)
self.on_zoom_fit(None)
return obj
def set_progress_bar(self, percentage, text=""):
self.progress_bar.set_text(text)
self.progress_bar.set_fraction(percentage)
return False
def save_project(self):
return
def get_current(self):
"""
Returns the currently selected CirkuixObj in the application.
@return: Currently selected CirkuixObj in the application.
@rtype: CirkuixObj
"""
try:
return self.stuff[self.selected_item_name]
except:
return None
def adjust_axes(self, xmin, ymin, xmax, ymax):
m_x = 15 # pixels
m_y = 25 # pixels
width = xmax-xmin
height = ymax-ymin
r = width/height
Fw, Fh = self.canvas.get_width_height()
Fr = float(Fw)/Fh
x_ratio = float(m_x)/Fw
y_ratio = float(m_y)/Fh
if r > Fr:
ycenter = (ymin+ymax)/2.0
newheight = height*r/Fr
ymin = ycenter-newheight/2.0
ymax = ycenter+newheight/2.0
else:
xcenter = (xmax+ymin)/2.0
newwidth = width*Fr/r
xmin = xcenter-newwidth/2.0
xmax = xcenter+newwidth/2.0
for name in self.stuff:
if self.stuff[name].axes is None:
continue
self.stuff[name].axes.set_xlim((xmin, xmax))
self.stuff[name].axes.set_ylim((ymin, ymax))
self.stuff[name].axes.set_position([x_ratio, y_ratio,
1-2*x_ratio, 1-2*y_ratio])
self.axes.set_xlim((xmin, xmax))
self.axes.set_ylim((ymin, ymax))
self.axes.set_position([x_ratio, y_ratio,
1-2*x_ratio, 1-2*y_ratio])
self.canvas.queue_draw()
def load_defaults(self):
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:
self.info("ERROR: Failed to parse defaults file.")
return
self.defaults.update(defaults)
########################################
## EVENT HANDLERS ##
########################################
def on_scale_object(self, widget):
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):
print "on_canvas_configure()"
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
def on_row_activated(self, widget, path, col):
self.notebook.set_current_page(1)
def on_generate_gerber_bounding_box(self, widget):
gerber = self.get_current()
gerber.read_form()
name = self.selected_item_name + "_bbox"
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, CirkuixGeometry)
bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
if not gerber.options["bboxrounded"]:
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-plot the current object only.
@param widget: The widget from which this was called.
@return: None
"""
print "Re-plotting"
self.get_current().read_form()
self.set_progress_bar(0.5, "Plotting...")
#GLib.idle_add(lambda: 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))
app_obj.get_current().plot(app_obj.figure)
GLib.idle_add(lambda: app_obj.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 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: The widget from which this was called.
@return: None
"""
job_name = self.selected_item_name + "_cnc"
excellon = self.get_current()
assert isinstance(excellon, CirkuixExcellon)
excellon.read_form()
# Object initialization function for app.new_object()
def job_init(job_obj, app_obj):
excellon_ = self.get_current()
assert isinstance(excellon_, CirkuixExcellon)
assert isinstance(job_obj, CirkuixCNCjob)
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, CirkuixExcellon)
excellon.show_tool_chooser()
def on_entry_eval_activate(self, widget):
self.on_eval_update(widget)
obj = self.get_current()
assert isinstance(obj, CirkuixObj)
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
"""
name = self.selected_item_name + "_noncopper"
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, CirkuixGeometry)
gerber = app_obj.stuff[app_obj.selected_item_name]
assert isinstance(gerber, CirkuixGerber)
gerber.read_form()
bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
non_copper = bounding_box.difference(gerber.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
"""
name = self.selected_item_name + "_cutout"
def geo_init(geo_obj, app_obj):
# TODO: get from object
margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
gerber = app_obj.stuff[app_obj.selected_item_name]
minx, miny, maxx, maxy = gerber.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
"""
print "Generating Isolation Geometry:"
iso_name = self.selected_item_name + "_iso"
def iso_init(geo_obj, app_obj):
# TODO: Object must be updated on form change and the options
# TODO: read from the object.
tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia/2.0)
# 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
"""
print "Generating CNC job"
job_name = self.selected_item_name + "_cnc"
# Object initialization function for app.new_object()
def job_init(job_obj, app_obj):
assert isinstance(job_obj, CirkuixCNCjob)
geometry = app_obj.stuff[app_obj.selected_item_name]
assert isinstance(geometry, CirkuixGeometry)
geometry.read_form()
GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
job_obj.z_cut = geometry.options["cutz"]
job_obj.z_move = geometry.options["travelz"]
job_obj.feedrate = geometry.options["feedrate"]
job_obj.options["tooldia"] = geometry.options["cnctooldia"]
GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
job_obj.generate_from_geometry(geometry)
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_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 CirkuixGeometry object.
"""
self.info("Click inside the desired polygon.")
geo = self.get_current()
geo.read_form()
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, CirkuixGeometry)
assert isinstance(app_obj, App)
cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
geo_obj.solid_geometry = cp
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):
def on_success(self, filename):
cncjob = self.get_current()
f = open(filename, 'w')
f.write(cncjob.gcode)
f.close()
print "Saved to:", filename
self.file_chooser_save_action(on_success)
def on_delete(self, widget):
"""
Delete the currently selected CirkuixObj.
@param widget: The widget from which this was called.
@return:
"""
print "on_delete():", self.selected_item_name
# Remove plot
self.figure.delaxes(self.get_current().axes)
self.canvas.queue_draw()
# Remove from dictionary
self.stuff.pop(self.selected_item_name)
# Update UI
self.build_list() # Update the items list
def on_replot(self, widget):
self.plot_all()
def on_clear_plots(self, widget):
self.clear_plots()
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.
"""
# 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 CirkuixObj.
@param selection: Selection associated to the project tree or list
@type selection: Gtk.TreeSelection
@return: None
"""
print "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 "You selected", model[treeiter][0]
self.selected_item_name = model[treeiter][0]
GLib.idle_add(lambda: self.get_current().build_ui())
else:
print "Nothing selected"
self.selected_item_name = None
self.setup_component_editor()
def on_file_new(self, param):
# Remove everythong from memory
# Clear plot
self.clear_plots()
# Clear object editor
#self.setup_component_editor()
# Clear data
self.stuff = {}
# Clear list
#self.tree_select.unselect_all()
self.build_list()
#print "File->New not implemented yet."
def on_filequit(self, param):
print "quit from menu"
self.window.destroy()
Gtk.main_quit()
def on_closewindow(self, param):
print "quit from X"
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.
"""
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:
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.
"""
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:
print("Cancel clicked")
dialog.destroy()
def on_fileopengerber(self, param):
# 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):
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.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):
# 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)
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):
# 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):
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):
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]
except:
self.position_label.set_label("")
self.mouse = None
def on_click_over_plot(self, event):
# For key presses
self.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)
for subscriber in self.plot_click_subscribers:
self.plot_click_subscribers[subscriber](event)
except Exception, e:
print "Outside plot!"
def on_zoom_in(self, event):
self.zoom(1.5)
return
def on_zoom_out(self, event):
self.zoom(1/1.5)
def on_zoom_fit(self, event):
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.adjust_axes(xmin, ymin, xmax, ymax)
# def on_scroll_over_plot(self, event):
# print "Scroll"
# center = [event.xdata, event.ydata]
# if sign(event.step):
# self.zoom(1.5, center=center)
# else:
# self.zoom(1/1.5, center=center)
#
# def on_window_scroll(self, event):
# print "Scroll"
def on_key_over_plot(self, event):
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.zoom(1/1.5, self.mouse)
return
if event.key == '3': # 3
self.zoom(1.5, self.mouse)
return
app = App()
Gtk.main()