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()