From c3260802df212c5dcc204bfddad5f7135e4200a4 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 15 Jan 2014 22:51:06 -0500 Subject: [PATCH] Centralized object creation. Cleaner notebook handling. Centralized form generation. Some threading improvement. Comments. --- cirkuix.py | 464 +++++++++++++++++++++++++++++++++-------------------- cirkuix.ui | 181 ++++++--------------- 2 files changed, 344 insertions(+), 301 deletions(-) diff --git a/cirkuix.py b/cirkuix.py index d716559c..66d76a58 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -1,8 +1,7 @@ import threading -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GLib +from gi.repository import Gtk, Gdk, GLib, GObject + from matplotlib.figure import Figure from numpy import arange, sin, pi @@ -14,6 +13,12 @@ from camlib import * 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. + """ + form_getters = {} form_setters = {} @@ -55,8 +60,42 @@ class CirkuixObj: for name in self.form_getters: self.options[name] = self.form_getters[name]() + def build_ui(self, kind): + """ + Sets up the UI/form for this object. + @param kind: Kind of object, i.e. 'gerber' + @type kind: str + @return: None + """ + + # Where to the UI for this object + 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_" + kind) # offscreenwindow + sw = self.app.builder.get_object("sw_" + kind) # scrollwindows + osw.remove(sw) # TODO: Is this needed ? + vp = self.app.builder.get_object("vp_" + 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_" + kind + "name") + entry_name.set_text(self.name) + entry_name.connect("activate", self.app.on_activate_name) + self.to_form() + sw.show() + class CirkuixGerber(CirkuixObj, Gerber): + """ + Represents Gerber code. + """ def __init__(self, name): Gerber.__init__(self) @@ -75,20 +114,10 @@ class CirkuixGerber(CirkuixObj, Gerber): } def build_ui(self): - print "cirkuixgerber.build_ui()" - osw = self.app.builder.get_object("offscrwindow_gerber") - #box1 = self.app.builder.get_object("box_gerber") - #osw.remove(box1) - sw = self.app.builder.get_object("sw_gerber") - osw.remove(sw) - vp = self.app.builder.get_object("vp_gerber") - vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) - self.app.notebook.append_page(sw, Gtk.Label("Selection")) - entry_name = self.app.builder.get_object("entry_gerbername") - entry_name.set_text(self.name) - entry_name.connect("activate", self.app.on_activate_name) - self.to_form() - sw.show() + """ + @return: None + """ + CirkuixObj.build_ui(self, "gerber") def plot(self, figure): self.setup_axes(figure) @@ -118,6 +147,9 @@ class CirkuixGerber(CirkuixObj, Gerber): class CirkuixExcellon(CirkuixObj, Excellon): + """ + Represents Excellon code. + """ def __init__(self, name): Excellon.__init__(self) @@ -130,20 +162,7 @@ class CirkuixExcellon(CirkuixObj, Excellon): } def build_ui(self): - print "build_excellon_ui()" - osw = self.app.builder.get_object("offscrwindow_excellon") - #box1 = self.app.builder.get_object("box_excellon") - #osw.remove(box1) - sw = self.app.builder.get_object("sw_excellon") - osw.remove(sw) - vp = self.app.builder.get_object("vp_excellon") - vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) - self.app.notebook.append_page(sw, Gtk.Label("Selection")) - entry_name = self.app.builder.get_object("entry_excellonname") - entry_name.set_text(self.name) - entry_name.connect("activate", self.app.on_activate_name) - self.to_form() - sw.show() + CirkuixObj.build_ui(self, "excellon") def plot(self, figure): self.setup_axes(figure) @@ -159,6 +178,9 @@ class CirkuixExcellon(CirkuixObj, Excellon): 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, @@ -172,20 +194,7 @@ class CirkuixCNCjob(CirkuixObj, CNCjob): } def build_ui(self): - print "build_cncjob_ui()" - osw = self.app.builder.get_object("offscrwindow_cncjob") - #box1 = self.app.builder.get_object("box_cncjob") - #osw.remove(box1) - sw = self.app.builder.get_object("sw_cncjob") - osw.remove(sw) - vp = self.app.builder.get_object("vp_cncjob") - vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) - self.app.notebook.append_page(sw, Gtk.Label("Selection")) - entry_name = self.app.builder.get_object("entry_cncjobname") - entry_name.set_text(self.name) - entry_name.connect("activate", self.app.on_activate_name) - self.to_form() - sw.show() + CirkuixObj.build_ui(self, "cncjob") def plot(self, figure): self.setup_axes(figure) @@ -193,6 +202,11 @@ class CirkuixCNCjob(CirkuixObj, CNCjob): class CirkuixGeometry(CirkuixObj, Geometry): + """ + Geometric object not associated with a specific + format. + """ + def __init__(self, name): CirkuixObj.__init__(self, name) self.options = {"plot": True, @@ -209,20 +223,7 @@ class CirkuixGeometry(CirkuixObj, Geometry): } def build_ui(self): - print "build_geometry_ui()" - osw = self.app.builder.get_object("offscrwindow_geometry") - #box1 = self.app.builder.get_object("box_geometry") - #osw.remove(box1) - sw = self.app.builder.get_object("sw_geometry") - osw.remove(sw) - vp = self.app.builder.get_object("vp_geometry") - vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) - self.app.notebook.append_page(sw, Gtk.Label("Selection")) - entry_name = self.app.builder.get_object("entry_geometryname") - entry_name.set_text(self.name) - entry_name.connect("activate", self.app.on_activate_name) - self.to_form() - sw.show() + CirkuixObj.build_ui(self, "geometry") def plot(self, figure): self.setup_axes(figure) @@ -244,9 +245,20 @@ class CirkuixGeometry(CirkuixObj, Geometry): class App: + """ + The main application class. The constructor starts the GUI. + """ + def __init__(self): + """ + Starts the application and the Gtk.main(). + @return: app + """ + # Needed to interact with the GUI from other threads. GLib.threads_init() + GObject.threads_init() + #Gdk.threads_init() ######################################## ## GUI ## @@ -296,9 +308,17 @@ class App: ## START ## ######################################## self.window.show_all() - Gtk.main() + #Gtk.main() 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) @@ -398,7 +418,9 @@ class App: """ 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() @@ -407,24 +429,42 @@ class App: renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Title", renderer, text=0) self.tree.append_column(column) - self.builder.get_object("notebook1").append_page(self.tree, Gtk.Label("Project")) + #self.builder.get_object("notebook1").append_page(self.tree, Gtk.Label("Project")) + 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, False, False, 1) - self.builder.get_object("notebook1").append_page(box1, Gtk.Label("Selection")) + box_selected.pack_start(box1, True, True, 0) 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() @@ -455,6 +495,11 @@ class App: self.canvas.queue_draw() def build_list(self): + """ + Clears and re-populates the list of objects in tcurrently + in the project. + @return: None + """ self.store.clear() for key in self.stuff: self.store.append([key]) @@ -463,12 +508,19 @@ class App: """ 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() for i in self.stuff: @@ -479,88 +531,148 @@ class App: self.canvas.queue_draw() def clear_plots(self): + """ + Clears self.axes and self.figure. + @return: None + """ + self.axes.cla() self.figure.clf() self.figure.add_axes(self.axes) 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) + 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: Knd of object to create. + @param name: Name for the object. + @param initilize: Function to run after the + object has been created but before attacing it + to the application. Takes the new object and the + app as parameters. + @return: The object requested + @rtype : CirkuixObj extended + """ + + # Check for existing name + if name in self.stuff: + return None + + # Create object + classdict = { + "gerber": CirkuixGerber, + "excellon": CirkuixExcellon, + "cncjob": CirkuixCNCjob, + "geometry": CirkuixGeometry + } + obj = classdict[kind](name) + + # Initialize as per user request + initialize(obj, self) + + # Add to our records + self.stuff[name] = obj + + # Update GUI list and select it + self.store.append([name]) + #self.build_list() + self.set_list_selection(name) + GLib.timeout_add(100, lambda: self.notebook.set_current_page(1)) + + # Plot + obj.plot(self.figure) + obj.axes.set_alpha(0.0) + self.on_zoom_fit(None) + + return obj + + ######################################## ## EVENT HANDLERS ## ######################################## def on_gerber_generate_noncopper(self, widget): - gerber = self.stuff[self.selected_item_name] - gerber.read_form() - name = self.selected_item_name + "_noncopper" - bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"]) - - non_copper = bounding_box.difference(gerber.solid_geometry) - - geometry = CirkuixGeometry(name) - geometry.solid_geometry = non_copper - - self.stuff[name] = geometry - self.build_list() + def geo_init(geo_obj, app_obj): + assert isinstance(geo_obj, CirkuixGeometry) + gerber = app_obj.stuff[self.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): - margin = self.get_eval("entry_gerber_cutout_margin") - gap_size = self.get_eval("entry_gerber_cutout_gapsize") - gerber = self.stuff[self.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]]]} name = self.selected_item_name + "_cutout" - geometry = CirkuixGeometry(name) - cuts = None - if self.builder.get_object("rb_2tb").get_active(): - cuts = cases["tb"] - elif self.builder.get_object("rb_2lr").get_active(): - cuts = cases["lr"] - else: - cuts = cases["4"] - geometry.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) - # Add to App and update. - self.stuff[name] = geometry - self.build_list() + def geo_init(geo_obj, app_obj): + # TODO: get from object + margin = app_obj.get_eval("entry_gerber_cutout_margin") + gap_size = app_obj.get_eval("entry_gerber_cutout_gapsize") + 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): """ @@ -573,39 +685,40 @@ class App: def on_generate_isolation(self, widget): print "Generating Isolation Geometry:" - # Get required info - tooldia = self.builder.get_object("entry_gerberisotooldia").get_text() - tooldia = eval(tooldia) - print "tooldia:", tooldia - - # Generate - iso = self.stuff[self.selected_item_name].isolation_geometry(tooldia/2.0) - # TODO: This will break if there is something with this name already iso_name = self.selected_item_name + "_iso" - geo = CirkuixGeometry(iso_name) - geo.solid_geometry = iso - # Add to App and update. - self.stuff[iso_name] = geo - self.build_list() + 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_gerberisotooldia") + geo_obj.solid_geometry = self.stuff[self.selected_item_name].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): print "Generating CNC job" - # Get required info - cutz = self.get_eval("entry_geometry_cutz") - travelz = self.get_eval("entry_geometry_travelz") - feedrate = self.get_eval("entry_geometry_feedrate") - - geometry = self.stuff[self.selected_item_name] + job_name = self.selected_item_name + "_cnc" - job = CirkuixCNCjob(job_name, z_move=travelz, z_cut=cutz, feedrate=feedrate) - job.generate_from_geometry(geometry.solid_geometry) - job.gcode_parse() - job.create_geometry() - - # Add to App and update. - self.stuff[job_name] = job - self.build_list() + + def job_init(job_obj, app_obj): + # TODO: Object must be updated on form change and the options + # TODO: read from the object. + z_cut = app_obj.get_eval("entry_geometry_cutz") + z_move = app_obj.get_eval("entry_geometry_travelz") + feedrate = app_obj.get_eval("entry_geometry_feedrate") + + geometry = app_obj.stuff[app_obj.selected_item_name] + assert isinstance(job_obj, CirkuixCNCjob) + job_obj.z_cut = z_cut + job_obj.z_move = z_move + job_obj.feedrate = feedrate + job_obj.generate_from_geometry(geometry.solid_geometry) + job_obj.gcode_parse() + job_obj.create_geometry() + + self.new_object("cncjob", job_name, job_init) def on_cncjob_tooldia_activate(self, widget): job = self.stuff[self.selected_item_name] @@ -626,7 +739,7 @@ class App: self.stuff.pop(self.selected_item_name) #self.tree.get_selection().disconnect(self.signal_id) - self.build_list() # Update the items list + self.build_list() # Update the items list #self.signal_id = self.tree.get_selection().connect( # "changed", self.on_tree_selection_changed) @@ -658,27 +771,17 @@ class App: if treeiter is not None: print "You selected", model[treeiter][0] + self.selected_item_name = model[treeiter][0] + #self.stuff[self.selected_item_name].build_ui() + GLib.timeout_add(100, lambda: self.stuff[self.selected_item_name].build_ui()) else: - return # TODO: Clear "Selected" page - - self.selected_item_name = model[treeiter][0] - # Remove the current selection page - # from the notebook - # TODO: Assuming it was last page or #2. Find the right page - self.builder.get_object("notebook1").remove_page(2) + print "Nothing selected" + self.selected_item_name = None + self.setup_component_editor() - self.stuff[self.selected_item_name].build_ui() + def on_file_new(self, param): + print "File->New not implemented yet." - # Determine the kind of item selected - #kind = self.stuff[model[treeiter][0]].kind - - # Build the UI - # builder = {"gerber": self.build_gerber_ui, - # "excellon": self.build_excellon_ui, - # "cncjob": self.build_cncjob_ui, - # "geometry": self.build_geometry_ui} - # builder[kind]() - def on_filequit(self, param): print "quit from menu" self.window.destroy() @@ -755,14 +858,18 @@ class App: self.progress_bar.set_text("Done!") self.progress_bar.set_fraction(1.0) - self.notebook.set_current_page(1) + #self.notebook.set_current_page(0) self.set_list_selection(name) + #self.notebook.set_current_page(1) + GLib.timeout_add(100, lambda: self.notebook.set_current_page(1)) def clear_bar(bar): bar.set_text("") bar.set_fraction(0.0) + return False - threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + #threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + GLib.timeout_add_seconds(1, clear_bar, self.progress_bar) self.file_chooser_action(on_success) def on_fileopenexcellon(self, param): @@ -790,10 +897,17 @@ class App: self.progress_bar.set_text("Done!") self.progress_bar.set_fraction(1.0) + #self.notebook.set_current_page(0) + self.set_list_selection(name) + #self.notebook.set_current_page(1) + GLib.timeout_add(100, lambda: self.notebook.set_current_page(1)) + def clear_bar(bar): bar.set_text("") bar.set_fraction(0.0) - threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + return False + #threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + GLib.timeout_add_seconds(1, clear_bar, self.progress_bar) self.file_chooser_action(on_success) @@ -828,10 +942,16 @@ class App: self.progress_bar.set_text("Done!") self.progress_bar.set_fraction(1.0) + #self.notebook.set_current_page(0) + self.set_list_selection(name) + #self.notebook.set_current_page(1) + def clear_bar(bar): bar.set_text("") bar.set_fraction(0.0) - threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + return False + #threading.Timer(1, clear_bar, args=(self.progress_bar,)).start() + GLib.timeout_add_seconds(1, clear_bar, self.progress_bar) self.file_chooser_action(on_success) def on_mouse_move_over_plot(self, event): @@ -846,9 +966,12 @@ class App: def on_click_over_plot(self, event): # For key presses self.canvas.grab_focus() - - print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( - event.button, event.x, event.y, event.xdata, event.ydata) + + try: + print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( + event.button, event.x, event.y, event.xdata, event.ydata) + except: + print "Outside plot!" def on_zoom_in(self, event): self.zoom(1.5) @@ -926,3 +1049,4 @@ class App: return app = App() +Gtk.main() diff --git a/cirkuix.ui b/cirkuix.ui index c07774ab..cd2415b3 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -1203,6 +1203,7 @@ False True True + @@ -1448,6 +1449,7 @@ True + 250 True True 3 @@ -1456,135 +1458,12 @@ 3 True - + True False - 3 - 3 - 3 + True + True vertical - - - True - False - 4 - GERBER - True - - - - - - False - False - 0 - - - - - Merge Polygons - True - True - False - 0 - True - True - - - False - True - 1 - - - - - Solid - True - True - False - 0 - True - - - False - True - 2 - - - - - Multi-colored - True - True - False - 0 - True - - - False - True - 3 - - - - - True - False - 4 - G-CODE - - - - - - False - True - 4 - - - - - True - False - - - True - False - Tool dia: - - - False - True - 0 - - - - - True - True - - 0.0 - - - - False - True - 1 - - - - - False - True - 5 - - - - - - - - @@ -1594,23 +1473,63 @@ True False - Defaults + Project False - + + True + False + True + True + vertical + + + + + + 1 + - + + True + False + Selected + + + 1 + False + - + + True + False + True + True + vertical + + + + + + 2 + - + + True + False + Options + + + 2 + False +