diff --git a/FlatCAM.ui b/FlatCAM.ui index 664bff42..36c298de 100644 --- a/FlatCAM.ui +++ b/FlatCAM.ui @@ -6,7 +6,7 @@ 5 dialog FlatCAM - Version Alpha 3 (2014/03) - UNSTABLE + Version Alpha 5 (2014/05) (c) 2014 Juan Pablo Caram 2D Post-processing for Manufacturing specialized in Printed Circuit Boards diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 86b8d708..b2b725d2 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1,14 +1,20 @@ +############################################################ +# FlatCAM: 2D Post-processing for Manufacturing # +# http://caram.cl/software/flatcam # +# Author: Juan Pablo Caram (c) # +# Date: 2/5/2014 # +# MIT Licence # +############################################################ + import threading import traceback import sys import urllib -import copy +from copy import copy import random import logging -from gi.repository import Gtk, GdkPixbuf, GObject, Gdk -from matplotlib.figure import Figure -from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas +from gi.repository import Gtk, GdkPixbuf, GObject, Gdk, GLib from shapely import speedups @@ -18,6 +24,7 @@ from shapely import speedups from FlatCAMWorker import Worker from ObjectCollection import * from FlatCAMObj import * +from PlotCanvas import * class GerberOptionsGroupUI(Gtk.VBox): @@ -347,8 +354,6 @@ class App: # Needed to interact with the GUI from other threads. GObject.threads_init() - # GLib.log_set_handler() - #### GUI #### # Glade init self.gladefile = "FlatCAM.ui" @@ -406,36 +411,153 @@ class App: self.options_box = self.builder.get_object('options_box') ## Application defaults ## - self.defaults = { - "units": "in" - } self.defaults_form = GlobalOptionsUI() + self.defaults_form_fields = { + "units": self.defaults_form.units_radio, + "gerber_plot": self.defaults_form.gerber_group.plot_cb, + "gerber_solid": self.defaults_form.gerber_group.solid_cb, + "gerber_multicolored": self.defaults_form.gerber_group.multicolored_cb, + "gerber_isotooldia": self.defaults_form.gerber_group.iso_tool_dia_entry, + "gerber_isopasses": self.defaults_form.gerber_group.iso_width_entry, + "gerber_isooverlap": self.defaults_form.gerber_group.iso_overlap_entry, + "gerber_cutouttooldia": self.defaults_form.gerber_group.cutout_tooldia_entry, + "gerber_cutoutmargin": self.defaults_form.gerber_group.cutout_margin_entry, + "gerber_cutoutgapsize": self.defaults_form.gerber_group.cutout_gap_entry, + "gerber_gaps": self.defaults_form.gerber_group.gaps_radio, + "gerber_noncoppermargin": self.defaults_form.gerber_group.noncopper_margin_entry, + "gerber_noncopperrounded": self.defaults_form.gerber_group.noncopper_rounded_cb, + "gerber_bboxmargin": self.defaults_form.gerber_group.bbmargin_entry, + "gerber_bboxrounded": self.defaults_form.gerber_group.bbrounded_cb, + "excellon_plot": self.defaults_form.excellon_group.plot_cb, + "excellon_solid": self.defaults_form.excellon_group.solid_cb, + "excellon_drillz": self.defaults_form.excellon_group.cutz_entry, + "excellon_travelz": self.defaults_form.excellon_group.travelz_entry, + "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry, + "geometry_plot": self.defaults_form.geometry_group.plot_cb, + "geometry_cutz": self.defaults_form.geometry_group.cutz_entry, + "geometry_travelz": self.defaults_form.geometry_group.travelz_entry, + "geometry_feedrate": self.defaults_form.geometry_group.cncfeedrate_entry, + "geometry_cnctooldia": self.defaults_form.geometry_group.cnctooldia_entry, + "geometry_painttooldia": self.defaults_form.geometry_group.painttooldia_entry, + "geometry_paintoverlap": self.defaults_form.geometry_group.paintoverlap_entry, + "geometry_paintmargin": self.defaults_form.geometry_group.paintmargin_entry, + "cncjob_plot": self.defaults_form.cncjob_group.plot_cb, + "cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry + } + + self.defaults = { + "units": "IN", + "gerber_plot": True, + "gerber_solid": True, + "gerber_multicolored": False, + "gerber_isotooldia": 0.016, + "gerber_isopasses": 1, + "gerber_isooverlap": 0.15, + "gerber_cutouttooldia": 0.07, + "gerber_cutoutmargin": 0.1, + "gerber_cutoutgapsize": 0.15, + "gerber_gaps": "4", + "gerber_noncoppermargin": 0.0, + "gerber_noncopperrounded": False, + "gerber_bboxmargin": 0.0, + "gerber_bboxrounded": False, + "excellon_plot": True, + "excellon_solid": False, + "excellon_drillz": -0.1, + "excellon_travelz": 0.1, + "excellon_feedrate": 3.0, + "geometry_plot": True, + "geometry_cutz": -0.002, + "geometry_travelz": 0.1, + "geometry_feedrate": 3.0, + "geometry_cnctooldia": 0.016, + "geometry_painttooldia": 0.07, + "geometry_paintoverlap": 0.15, + "geometry_paintmargin": 0.0, + "cncjob_plot": True, + "cncjob_tooldia": 0.016 + } + self.load_defaults() + self.defaults_write_form() ## Current Project ## - self.options = {} # Project options - self.project_filename = None self.options_form = GlobalOptionsUI() + self.options_form_fields = { + "units": self.options_form.units_radio, + "gerber_plot": self.options_form.gerber_group.plot_cb, + "gerber_solid": self.options_form.gerber_group.solid_cb, + "gerber_multicolored": self.options_form.gerber_group.multicolored_cb, + "gerber_isotooldia": self.options_form.gerber_group.iso_tool_dia_entry, + "gerber_isopasses": self.options_form.gerber_group.iso_width_entry, + "gerber_isooverlap": self.options_form.gerber_group.iso_overlap_entry, + "gerber_cutouttooldia": self.options_form.gerber_group.cutout_tooldia_entry, + "gerber_cutoutmargin": self.options_form.gerber_group.cutout_margin_entry, + "gerber_cutoutgapsize": self.options_form.gerber_group.cutout_gap_entry, + "gerber_gaps": self.options_form.gerber_group.gaps_radio, + "gerber_noncoppermargin": self.options_form.gerber_group.noncopper_margin_entry, + "gerber_noncopperrounded": self.options_form.gerber_group.noncopper_rounded_cb, + "gerber_bboxmargin": self.options_form.gerber_group.bbmargin_entry, + "gerber_bboxrounded": self.options_form.gerber_group.bbrounded_cb, + "excellon_plot": self.options_form.excellon_group.plot_cb, + "excellon_solid": self.options_form.excellon_group.solid_cb, + "excellon_drillz": self.options_form.excellon_group.cutz_entry, + "excellon_travelz": self.options_form.excellon_group.travelz_entry, + "excellon_feedrate": self.options_form.excellon_group.feedrate_entry, + "geometry_plot": self.options_form.geometry_group.plot_cb, + "geometry_cutz": self.options_form.geometry_group.cutz_entry, + "geometry_travelz": self.options_form.geometry_group.travelz_entry, + "geometry_feedrate": self.options_form.geometry_group.cncfeedrate_entry, + "geometry_cnctooldia": self.options_form.geometry_group.cnctooldia_entry, + "geometry_painttooldia": self.options_form.geometry_group.painttooldia_entry, + "geometry_paintoverlap": self.options_form.geometry_group.paintoverlap_entry, + "geometry_paintmargin": self.options_form.geometry_group.paintmargin_entry, + "cncjob_plot": self.options_form.cncjob_group.plot_cb, + "cncjob_tooldia": self.options_form.cncjob_group.tooldia_entry + } - self.options_box.pack_start(self.defaults_form, False, False, 1) + # Project options + self.options = { + "units": "IN", + "gerber_plot": True, + "gerber_solid": True, + "gerber_multicolored": False, + "gerber_isotooldia": 0.016, + "gerber_isopasses": 1, + "gerber_isooverlap": 0.15, + "gerber_cutouttooldia": 0.07, + "gerber_cutoutmargin": 0.1, + "gerber_cutoutgapsize": 0.15, + "gerber_gaps": "4", + "gerber_noncoppermargin": 0.0, + "gerber_noncopperrounded": False, + "gerber_bboxmargin": 0.0, + "gerber_bboxrounded": False, + "excellon_plot": True, + "excellon_solid": False, + "excellon_drillz": -0.1, + "excellon_travelz": 0.1, + "excellon_feedrate": 3.0, + "geometry_plot": True, + "geometry_cutz": -0.002, + "geometry_travelz": 0.1, + "geometry_feedrate": 3.0, + "geometry_cnctooldia": 0.016, + "geometry_painttooldia": 0.07, + "geometry_paintoverlap": 0.15, + "geometry_paintmargin": 0.0, + "cncjob_plot": True, + "cncjob_tooldia": 0.016 + } + self.options.update(self.defaults) # Copy app defaults to project options + self.options_write_form() - # self.form_kinds = { - # "units": "radio" - # } + self.project_filename = None - # self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"}, - # "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}} - # self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"}, - # "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}} + # Where we draw the options/defaults forms. + self.on_options_combo_change(None) + #self.options_box.pack_start(self.defaults_form, False, False, 1) - # Options for each kind of FlatCAMObj. - # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot' - # for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]: - # obj = FlatCAMClass("no_name") - # for option in obj.form_kinds: - # self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option] - # # if obj.form_kinds[option] == "radio": - # # self.radios.update({obj.kind + "_" + option: obj.radios[option]}) - # # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]}) + self.options_form.units_radio.group_toggle_fn = lambda x, y: self.on_toggle_units(x) ## Event subscriptions ## @@ -452,9 +574,6 @@ class App: self.toolbar.insert(measure, -1) #### Initialization #### - self.load_defaults() - self.options.update(self.defaults) # Copy app defaults to project options - # self.options2form() # Populate the app defaults form self.units_label.set_text("[" + self.options["units"] + "]") self.setup_recent_items() @@ -465,7 +584,7 @@ class App: #### Check for updates #### # Separate thread (Not worker) - self.version = 4 + self.version = 5 App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version)) t1 = threading.Thread(target=self.version_check) t1.daemon = True @@ -486,7 +605,7 @@ class App: self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png') self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png') Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256]) - self.window.set_title("FlatCAM - Alpha 4 UNSTABLE") + self.window.set_title("FlatCAM - Alpha 5") self.window.set_default_size(900, 600) self.window.show_all() App.log.info("END of constructor. Releasing control.") @@ -582,6 +701,10 @@ class App: box_selected = self.builder.get_object("vp_selected") + # White background + box_selected.override_background_color(Gtk.StateType.NORMAL, + Gdk.RGBA(1, 1, 1, 1)) + # Remove anything else in the box box_children = box_selected.get_children() for child in box_children: @@ -721,7 +844,7 @@ class App: self.info("Could not evaluate: " + value) return None - def new_object(self, kind, name, initialize): + def new_object(self, kind, name, initialize, active=True, fit=True, plot=True): """ Creates a new specalized FlatCAMObj and attaches it to the application, this is, updates the GUI accordingly, any other records and plots it. @@ -782,17 +905,18 @@ class App: obj.convert_units(self.options["units"]) # Add to our records - self.collection.append(obj, active=True) + self.collection.append(obj, active=active) # Show object details now. GLib.idle_add(lambda: self.notebook.set_current_page(1)) # Plot # TODO: (Thread-safe?) - obj.plot() + if plot: + obj.plot() - GLib.idle_add(lambda: self.on_zoom_fit(None)) - #self.on_zoom_fit(None) + if fit: + GLib.idle_add(lambda: self.on_zoom_fit(None)) return obj @@ -835,105 +959,21 @@ class App: return self.defaults.update(defaults) - def read_form(self): - """ - Reads the options form into self.defaults/self.options. + def defaults_read_form(self): + for option in self.defaults_form_fields: + self.defaults[option] = self.defaults_form_fields[option].get_value() - :return: None - :rtype: None - """ - combo_sel = self.combo_options.get_active() - options_set = [self.options, self.defaults][combo_sel] - for option in options_set: - self.read_form_item(option, options_set) + def options_read_form(self): + for option in self.options_form_fields: + self.options[option] = self.options_form_fields[option].get_value() - def read_form_item(self, name, dest): - """ - Reads the value of a form item in the defaults/options form and - saves it to the corresponding dictionary. + def defaults_write_form(self): + for option in self.defaults_form_fields: + self.defaults_form_fields[option].set_value(self.defaults[option]) - :param name: Name of the form item. A key in ``self.defaults`` or - ``self.options``. - :type name: str - :param dest: Dictionary to which to save the value. - :type dest: dict - :return: None - """ - fkind = self.form_kinds[name] - fname = fkind + "_" + "app" + "_" + name - - if fkind == 'entry_text': - dest[name] = self.builder.get_object(fname).get_text() - return - if fkind == 'entry_eval': - dest[name] = self.get_eval(fname) - return - if fkind == 'cb': - dest[name] = self.builder.get_object(fname).get_active() - return - if fkind == 'radio': - dest[name] = self.get_radio_value(self.radios[name]) - return - print "Unknown kind of form item:", fkind - - # def options2form(self): - # """ - # Sets the 'Project Options' or 'Application Defaults' form with values from - # ``self.options`` or ``self.defaults``. - # - # :return: None - # :rtype: None - # """ - # - # # Set the on-change callback to do nothing while we do the changes. - # self.options_update_ignore = True - # self.toggle_units_ignore = True - # - # combo_sel = self.combo_options.get_active() - # options_set = [self.options, self.defaults][combo_sel] - # for option in options_set: - # self.set_form_item(option, options_set[option]) - # - # self.options_update_ignore = False - # self.toggle_units_ignore = False - - def set_form_item(self, name, value): - """ - Sets a form item 'name' in the GUI with the given 'value'. The syntax of - form names in the GUI is _app_, where kind is one of: rb (radio button), - cb (check button), entry_eval or entry_text (entry), combo (combo box). name is - whatever name it's been given. For self.defaults, name is a key in the dictionary. - - :param name: Name of the form field. - :type name: str - :param value: The value to set the form field to. - :type value: Depends on field kind. - :return: None - """ - if name not in self.form_kinds: - print "WARNING: Tried to set unknown option/form item:", name - return - fkind = self.form_kinds[name] - fname = fkind + "_" + "app" + "_" + name - if fkind == 'entry_eval' or fkind == 'entry_text': - try: - self.builder.get_object(fname).set_text(str(value)) - except: - print "ERROR: Failed to set value of %s to %s" % (fname, str(value)) - return - if fkind == 'cb': - try: - self.builder.get_object(fname).set_active(value) - except: - print "ERROR: Failed to set value of %s to %s" % (fname, str(value)) - return - if fkind == 'radio': - try: - self.builder.get_object(self.radios_inv[name][value]).set_active(True) - except: - print "ERROR: Failed to set value of %s to %s" % (fname, str(value)) - return - print "Unknown kind of form item:", fkind + def options_write_form(self): + for option in self.options_form_fields: + self.options_form_fields[option].set_value(self.options[option]) def save_project(self, filename): """ @@ -956,14 +996,14 @@ class App: try: f = open(filename, 'w') - except: - print "ERROR: Failed to open file for saving:", filename + except IOError: + App.log.error("ERROR: Failed to open file for saving:", filename) return try: json.dump(d, f, default=to_dict) except: - print "ERROR: File open but failed to write:", filename + App.log.error("ERROR: File open but failed to write:", filename) f.close() return @@ -977,6 +1017,7 @@ class App: :type filename: str :return: None """ + App.log.debug("Opening project: " + filename) try: f = open(filename, 'r') @@ -1004,12 +1045,16 @@ class App: GLib.idle_add(lambda: self.units_label.set_text(self.options["units"])) # Re create objects + App.log.debug("Re-creating objects...") for obj in d['objs']: def obj_init(obj_inst, app_inst): obj_inst.from_dict(obj) - self.new_object(obj['kind'], obj['options']['name'], obj_init) + App.log.debug(obj['kind'] + ": " + obj['options']['name']) + self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=False) + self.plot_all() self.info("Project loaded from: " + filename) + App.log.debug("Project loaded") def populate_objects_combo(self, combo): """ @@ -1019,7 +1064,7 @@ class App: :type combo: str or Gtk.ComboBoxText :return: None """ - print "Populating combo!" + App.log.debug("Populating combo!") if type(combo) == str: combo = self.builder.get_object(combo) @@ -1078,17 +1123,6 @@ class App: return - # def setup_tooltips(self): - # tooltips = { - # "cb_gerber_plot": "Plot this object on the main window.", - # # "cb_gerber_mergepolys": "Show overlapping polygons as single.", - # "cb_gerber_solid": "Paint inside polygons.", - # "cb_gerber_multicolored": "Draw polygons with different colors." - # } - # - # for widget in tooltips: - # self.builder.get_object(widget).set_tooltip_markup(tooltips[widget]) - def do_nothing(self, param): return @@ -1112,7 +1146,6 @@ class App: GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) return for obj in self.collection.get_list(): - #if i != app_obj.selected_item_name or not except_current: if obj != self.collection.get_active() or not except_current: obj.options['plot'] = False obj.plot() @@ -1339,39 +1372,6 @@ class App: def on_disable_all_plots_not_current(self, widget): self.disable_plots(except_current=True) - # def on_offset_object(self, widget): - # """ - # Offsets the object's geometry by the vector specified - # in the form. Re-plots. - # - # :param widget: Ignored - # :return: None - # """ - # - # obj = self.collection.get_active() - # obj.read_form() - # assert isinstance(obj, FlatCAMObj) - # try: - # vect = self.get_eval("entry_eval_" + obj.kind + "_offset") - # except: - # self.info("ERROR: Vector is not in (x, y) format.") - # return - # assert isinstance(obj, Geometry) - # obj.offset(vect) - # obj.plot() - # return - - # def on_cb_plot_toggled(self, widget): - # """ - # Callback for toggling the "Plot" checkbox. Re-plots. - # - # :param widget: Ignored. - # :return: None - # """ - # - # self.collection.get_active().read_form() - # self.collection.get_active().plot() - def on_about(self, widget): """ Opens the 'About' dialog box. @@ -1556,9 +1556,6 @@ class App: if self.toggle_units_ignore: return - combo_sel = self.combo_options.get_active() - options_set = [self.options, self.defaults][combo_sel] - # Options to scale dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize', 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz', @@ -1569,20 +1566,13 @@ class App: def scale_options(sfactor): for dim in dimensions: - options_set[dim] *= sfactor + self.options[dim] *= sfactor # The scaling factor depending on choice of units. factor = 1/25.4 - if self.builder.get_object('rb_mm').get_active(): + if self.options_form.units_radio.get_value().upper() == 'MM': factor = 25.4 - # App units. Convert without warning. - if combo_sel == 1: - self.read_form() - scale_options(factor) - self.options2form() - return - # Changing project units. Warn user. label = Gtk.Label("Changing the units of the project causes all geometrical \n" + "properties of all objects to be scaled accordingly. Continue?") @@ -1599,13 +1589,11 @@ class App: dialog.destroy() if response == Gtk.ResponseType.OK: - #print "Converting units..." - #print "Converting options..." - self.read_form() + self.options_read_form() scale_options(factor) - self.options2form() + self.options_write_form() for obj in self.collection.get_list(): - units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"}) + units = self.options_form.units_radio.get_value().upper() obj.convert_units(units) current = self.collection.get_active() if current is not None: @@ -1614,13 +1602,13 @@ class App: else: # Undo toggling self.toggle_units_ignore = True - if self.builder.get_object('rb_mm').get_active(): - self.builder.get_object('rb_inch').set_active(True) + if self.options_form.units_radio.get_value().upper() == 'MM': + self.options_form.units_radio.set_value('IN') else: - self.builder.get_object('rb_mm').set_active(True) + self.options_form.units_radio.set_value('MM') self.toggle_units_ignore = False - self.read_form() + self.options_read_form() self.info("Converted units to %s" % self.options["units"]) self.units_label.set_text("[" + self.options["units"] + "]") @@ -1636,6 +1624,7 @@ class App: def on_success(app_obj, filename): app_obj.open_project(filename) + # Runs on_success on worker self.file_chooser_action(on_success) def on_file_saveproject(self, param): @@ -1726,8 +1715,9 @@ class App: :return: None """ + self.defaults_read_form() self.options.update(self.defaults) - self.options2form() # Update UI + self.options_write_form() def on_options_project2app(self, param): """ @@ -1738,8 +1728,9 @@ class App: :return: None """ + self.options_read_form() self.defaults.update(self.options) - self.options2form() # Update UI + self.defaults_write_form() def on_options_project2object(self, param): """ @@ -1750,6 +1741,7 @@ class App: :return: None """ + self.options_read_form() obj = self.collection.get_active() if obj is None: self.info("WARNING: No object selected.") @@ -1778,7 +1770,7 @@ class App: if option in ['name']: # TODO: Handle this better... continue self.options[obj.kind + "_" + option] = obj.options[option] - self.options2form() # Update UI + self.options_write_form() def on_options_object2app(self, param): """ @@ -1797,7 +1789,7 @@ class App: if option in ['name']: # TODO: Handle this better... continue self.defaults[obj.kind + "_" + option] = obj.options[option] - self.options2form() # Update UI + self.defaults_write_form() def on_options_app2object(self, param): """ @@ -1808,6 +1800,7 @@ class App: :return: None """ + self.defaults_read_form() obj = self.collection.get_active() if obj is None: self.info("WARNING: No object selected.") @@ -1833,6 +1826,7 @@ class App: options = f.read() f.close() except: + App.log.error("Could not load defaults file.") self.info("ERROR: Could not load defaults file.") return @@ -1840,12 +1834,13 @@ class App: defaults = json.loads(options) except: e = sys.exc_info()[0] - print e + App.log.error("Failed to parse defaults file.") + App.log.error(str(e)) self.info("ERROR: Failed to parse defaults file.") return # Update options - assert isinstance(defaults, dict) + self.defaults_read_form() defaults.update(self.defaults) # Save update options @@ -1870,7 +1865,7 @@ class App: """ combo_sel = self.combo_options.get_active() - print "Options --> ", combo_sel + App.log.debug("Options --> %s" % combo_sel) # Remove anything else in the box box_children = self.options_box.get_children() @@ -1883,35 +1878,6 @@ class App: # self.options2form() - def on_options_update(self, widget): - """ - Called whenever a value in the options/defaults form changes. - All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``, - which may be necessary when updating the UI from code and not by the user. - - :param widget: The widget from which this was called. Ignore. - :return: None - """ - - if self.options_update_ignore: - return - self.read_form() - - # def on_scale_object(self, widget): - # """ - # Callback for request to change an objects geometry scale. The object - # is re-scaled and replotted. - # - # :param widget: Ignored. - # :return: None - # """ - # - # obj = self.collection.get_active() - # factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor") - # obj.scale(factor) - # obj.to_form() - # self.on_update_plot(None) - def on_canvas_configure(self, widget, event): """ Called whenever the canvas changes size. The axes are updated such @@ -1937,30 +1903,6 @@ class App: """ self.notebook.set_current_page(1) - # def on_generate_gerber_bounding_box(self, widget): - # """ - # Callback for request from the Gerber form to generate a bounding box for the - # geometry in the object. Creates a FlatCAMGeometry with the bounding box. - # The box can have rounded corners if specified in the form. - # - # :param widget: Ignored. - # :return: None - # """ - # # TODO: Use Gerber.get_bounding_box(...) - # gerber = self.collection.get_active() - # gerber.read_form() - # name = gerber.options["name"] + "_bbox" - # - # def geo_init(geo_obj, app_obj): - # assert isinstance(geo_obj, FlatCAMGeometry) - # # Bounding box with rounded corners - # bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"]) - # if not gerber.options["bboxrounded"]: # Remove rounded corners - # bounding_box = bounding_box.envelope - # geo_obj.solid_geometry = bounding_box - # - # self.new_object("geometry", name, geo_init) - def on_update_plot(self, widget): """ Callback for button on form for all kinds of objects. @@ -1983,51 +1925,6 @@ class App: # Send to worker self.worker.add_task(thread_func, [self]) - def on_generate_excellon_cncjob(self, widget): - """ - Callback for button active/click on Excellon form to - create a CNC Job for the Excellon file. - - :param widget: Ignored - :return: None - """ - - excellon = self.collection.get_active() - excellon.read_form() - job_name = excellon.options["name"] + "_cnc" - - # Object initialization function for app.new_object() - def job_init(job_obj, app_obj): - # excellon_ = self.get_current() - # assert isinstance(excellon_, FlatCAMExcellon) - assert isinstance(job_obj, FlatCAMCNCjob) - - GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job...")) - job_obj.z_cut = excellon.options["drillz"] - job_obj.z_move = excellon.options["travelz"] - job_obj.feedrate = excellon.options["feedrate"] - # There could be more than one drill size... - # job_obj.tooldia = # TODO: duplicate variable! - # job_obj.options["tooldia"] = - job_obj.generate_from_excellon_by_tool(excellon, excellon.options["toolselection"]) - - GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code...")) - job_obj.gcode_parse() - - GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry...")) - job_obj.create_geometry() - - GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting...")) - - # To be run in separate thread - def job_thread(app_obj): - app_obj.new_object("cncjob", job_name, job_init) - GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!")) - GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) - - # Send to worker - self.worker.add_task(job_thread, [self]) - def on_excellon_tool_choose(self, widget): """ Callback for button on Excellon form to open up a window for @@ -2054,81 +1951,6 @@ class App: assert isinstance(obj, FlatCAMObj) obj.read_form() - # def on_gerber_generate_noncopper(self, widget): - # """ - # Callback for button on Gerber form to create a geometry object - # with polygons covering the area without copper or negative of the - # Gerber. - # - # :param widget: The widget from which this was called. - # :return: None - # """ - # - # gerb = self.collection.get_active() - # gerb.read_form() - # name = gerb.options["name"] + "_noncopper" - # - # def geo_init(geo_obj, app_obj): - # assert isinstance(geo_obj, FlatCAMGeometry) - # bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"]) - # if not gerb.options["noncopperrounded"]: - # bounding_box = bounding_box.envelope - # non_copper = bounding_box.difference(gerb.solid_geometry) - # geo_obj.solid_geometry = non_copper - # - # # TODO: Check for None - # self.new_object("geometry", name, geo_init) - - # def on_gerber_generate_cutout(self, widget): - # """ - # Callback for button on Gerber form to create geometry with lines - # for cutting off the board. - # - # :param widget: The widget from which this was called. - # :return: None - # """ - # - # gerb = self.collection.get_active() - # gerb.read_form() - # name = gerb.options["name"] + "_cutout" - # - # def geo_init(geo_obj, app_obj): - # margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2 - # gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"] - # minx, miny, maxx, maxy = gerb.bounds() - # minx -= margin - # maxx += margin - # miny -= margin - # maxy += margin - # midx = 0.5 * (minx + maxx) - # midy = 0.5 * (miny + maxy) - # hgap = 0.5 * gap_size - # pts = [[midx - hgap, maxy], - # [minx, maxy], - # [minx, midy + hgap], - # [minx, midy - hgap], - # [minx, miny], - # [midx - hgap, miny], - # [midx + hgap, miny], - # [maxx, miny], - # [maxx, midy - hgap], - # [maxx, midy + hgap], - # [maxx, maxy], - # [midx + hgap, maxy]] - # cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]], - # [pts[6], pts[7], pts[10], pts[11]]], - # "lr": [[pts[9], pts[10], pts[1], pts[2]], - # [pts[3], pts[4], pts[7], pts[8]]], - # "4": [[pts[0], pts[1], pts[2]], - # [pts[3], pts[4], pts[5]], - # [pts[6], pts[7], pts[8]], - # [pts[9], pts[10], pts[11]]]} - # cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})] - # geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) - # - # # TODO: Check for None - # self.new_object("geometry", name, geo_init) - def on_eval_update(self, widget): """ Modifies the content of a Gtk.Entry by running @@ -2141,143 +1963,21 @@ class App: # TODO: error handling here widget.set_text(str(eval(widget.get_text()))) - # def on_generate_isolation(self, widget): + # def on_cncjob_exportgcode(self, widget): # """ - # Callback for button on Gerber form to create isolation routing geometry. + # Called from button on CNCjob form to save the G-Code from the object. # # :param widget: The widget from which this was called. # :return: None # """ + # def on_success(app_obj, filename): + # cncjob = app_obj.collection.get_active() + # f = open(filename, 'w') + # f.write(cncjob.gcode) + # f.close() + # app_obj.info("Saved to: " + filename) # - # gerb = self.collection.get_active() - # gerb.read_form() - # dia = gerb.options["isotooldia"] - # passes = int(gerb.options["isopasses"]) - # overlap = gerb.options["isooverlap"] * dia - # - # for i in range(passes): - # - # offset = (2*i + 1)/2.0 * dia - i*overlap - # iso_name = gerb.options["name"] + "_iso%d" % (i+1) - # - # # TODO: This is ugly. Create way to pass data into init function. - # def iso_init(geo_obj, app_obj): - # # Propagate options - # geo_obj.options["cnctooldia"] = gerb.options["isotooldia"] - # - # geo_obj.solid_geometry = gerb.isolation_geometry(offset) - # app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"]) - # - # # TODO: Do something if this is None. Offer changing name? - # self.new_object("geometry", iso_name, iso_init) - - # def on_generate_cncjob(self, widget): - # """ - # Callback for button on geometry form to generate CNC job. - # - # :param widget: The widget from which this was called. - # :return: None - # """ - # - # source_geo = self.collection.get_active() - # source_geo.read_form() - # job_name = source_geo.options["name"] + "_cnc" - # - # # Object initialization function for app.new_object() - # # RUNNING ON SEPARATE THREAD! - # def job_init(job_obj, app_obj): - # assert isinstance(job_obj, FlatCAMCNCjob) - # # Propagate options - # job_obj.options["tooldia"] = source_geo.options["cnctooldia"] - # - # GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job...")) - # job_obj.z_cut = source_geo.options["cutz"] - # job_obj.z_move = source_geo.options["travelz"] - # job_obj.feedrate = source_geo.options["feedrate"] - # - # GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry...")) - # # TODO: The tolerance should not be hard coded. Just for testing. - # job_obj.generate_from_geometry(source_geo, tolerance=0.0005) - # - # GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code...")) - # job_obj.gcode_parse() - # - # # TODO: job_obj.create_geometry creates stuff that is not used. - # #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry...")) - # #job_obj.create_geometry() - # - # GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting...")) - # - # # To be run in separate thread - # def job_thread(app_obj): - # app_obj.new_object("cncjob", job_name, job_init) - # GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name)) - # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!")) - # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) - # - # # Send to worker - # self.worker.add_task(job_thread, [self]) - - # def on_generate_paintarea(self, widget): - # """ - # Callback for button on geometry form. - # Subscribes to the "Click on plot" event and continues - # after the click. Finds the polygon containing - # the clicked point and runs clear_poly() on it, resulting - # in a new FlatCAMGeometry object. - # - # :param widget: The widget from which this was called. - # :return: None - # """ - # - # self.info("Click inside the desired polygon.") - # geo = self.collection.get_active() - # geo.read_form() - # assert isinstance(geo, FlatCAMGeometry) - # tooldia = geo.options["painttooldia"] - # overlap = geo.options["paintoverlap"] - # - # # Connection ID for the click event - # subscription = None - # - # # To be called after clicking on the plot. - # def doit(event): - # #self.plot_click_subscribers.pop("generate_paintarea") - # self.plotcanvas.mpl_disconnect(subscription) - # self.info("Painting") - # point = [event.xdata, event.ydata] - # poly = find_polygon(geo.solid_geometry, point) - # - # # Initializes the new geometry object - # def gen_paintarea(geo_obj, app_obj): - # assert isinstance(geo_obj, FlatCAMGeometry) - # assert isinstance(app_obj, App) - # cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap) - # geo_obj.solid_geometry = cp - # geo_obj.options["cnctooldia"] = tooldia - # - # #name = self.selected_item_name + "_paint" - # name = geo.options["name"] + "_paint" - # self.new_object("geometry", name, gen_paintarea) - # - # #self.plot_click_subscribers["generate_paintarea"] = doit - # subscription = self.plotcanvas.mpl_connect('button_press_event', doit) - - def on_cncjob_exportgcode(self, widget): - """ - Called from button on CNCjob form to save the G-Code from the object. - - :param widget: The widget from which this was called. - :return: None - """ - def on_success(app_obj, filename): - cncjob = app_obj.collection.get_active() - f = open(filename, 'w') - f.write(cncjob.gcode) - f.close() - app_obj.info("Saved to: " + filename) - - self.file_chooser_save_action(on_success) + # self.file_chooser_save_action(on_success) def on_delete(self, widget): """ @@ -2288,7 +1988,7 @@ class App: """ # Keep this for later - name = copy.copy(self.collection.get_active().options["name"]) + name = copy(self.collection.get_active().options["name"]) # Remove plot self.plotcanvas.figure.delaxes(self.collection.get_active().axes) @@ -2310,7 +2010,10 @@ class App: :return: None """ - self.collection.get_active().read_form() + try: + self.collection.get_active().read_form() + except AttributeError: + pass self.plot_all() @@ -2323,20 +2026,6 @@ class App: """ self.plotcanvas.clear() - # def on_activate_name(self, entry): - # """ - # Hitting 'Enter' after changing the name of an item - # updates the item dictionary and re-builds the item list. - # - # :param entry: The widget from which this was called. - # :return: None - # """ - # - # old_name = copy.copy(self.collection.get_active().options["name"]) - # new_name = entry.get_text() - # self.collection.change_name(old_name, new_name) - # self.info("Name changed from %s to %s" % (old_name, new_name)) - def on_file_new(self, param): """ Callback for menu item File->New. Returns the application to its @@ -2346,16 +2035,20 @@ class App: :return: None """ # Remove everything from memory + App.log.debug("on_file_bew()") # GUI things def task(): # Clear plot + App.log.debug(" self.plotcanvas.clear()") self.plotcanvas.clear() # Delete data + App.log.debug(" self.collection.delete_all()") self.collection.delete_all() # Clear object editor + App.log.debug(" self.setup_component_editor()") self.setup_component_editor() GLib.idle_add(task) @@ -2514,17 +2207,14 @@ class App: self.plotcanvas.canvas.grab_focus() try: - print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % ( - event.button, event.x, event.y, event.xdata, event.ydata) - - # TODO: This custom subscription mechanism is probably not necessary. - # for subscriber in self.plot_click_subscribers: - # self.plot_click_subscribers[subscriber](event) + App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % ( + event.button, event.x, event.y, event.xdata, event.ydata)) self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1) except Exception, e: - print "Outside plot!" + App.log.debug("Outside plot?") + App.log.debug(str(e)) def on_zoom_in(self, event): """ @@ -2679,7 +2369,7 @@ class Measurement: self.plotcanvas.mpl_disconnect(self.move_subscription) return False else: # Activate - print "DEBUG: Activating Measurement Tool..." + App.log.debug("DEBUG: Activating Measurement Tool...") self.active = True self.click_subscription = self.plotcanvas.mpl_connect("button_press_event", self.on_click) self.move_subscription = self.plotcanvas.mpl_connect('motion_notify_event', self.on_move) @@ -2718,308 +2408,6 @@ class Measurement: if self.point1 is None: self.point1 = (event.xdata, event.ydata) else: - self.point2 = copy.copy(self.point1) + self.point2 = copy(self.point1) self.point1 = (event.xdata, event.ydata) self.on_move(event) - - -class PlotCanvas: - """ - Class handling the plotting area in the application. - """ - - def __init__(self, container): - """ - The constructor configures the Matplotlib figure that - will contain all plots, creates the base axes and connects - events to the plotting area. - - :param container: The parent container in which to draw plots. - :rtype: PlotCanvas - """ - # Options - self.x_margin = 15 # pixels - self.y_margin = 25 # Pixels - - # Parent container - self.container = container - - # Plots go onto a single matplotlib.figure - self.figure = Figure(dpi=50) # TODO: dpi needed? - self.figure.patch.set_visible(False) - - # These axes show the ticks and grid. No plotting done here. - # New axes must have a label, otherwise mpl returns an existing one. - self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) - self.axes.set_aspect(1) - self.axes.grid(True) - - # The canvas is the top level container (Gtk.DrawingArea) - self.canvas = FigureCanvas(self.figure) - self.canvas.set_hexpand(1) - self.canvas.set_vexpand(1) - self.canvas.set_can_focus(True) # For key press - - # Attach to parent - self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? - - # Events - self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) - self.canvas.connect('configure-event', self.auto_adjust_axes) - self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) - self.canvas.connect("scroll-event", self.on_scroll) - self.canvas.mpl_connect('key_press_event', self.on_key_down) - self.canvas.mpl_connect('key_release_event', self.on_key_up) - - self.mouse = [0, 0] - self.key = None - - def on_key_down(self, event): - """ - - :param event: - :return: - """ - self.key = event.key - - def on_key_up(self, event): - """ - - :param event: - :return: - """ - self.key = None - - def mpl_connect(self, event_name, callback): - """ - Attach an event handler to the canvas through the Matplotlib interface. - - :param event_name: Name of the event - :type event_name: str - :param callback: Function to call - :type callback: func - :return: Connection id - :rtype: int - """ - return self.canvas.mpl_connect(event_name, callback) - - def mpl_disconnect(self, cid): - """ - Disconnect callback with the give id. - :param cid: Callback id. - :return: None - """ - self.canvas.mpl_disconnect(cid) - - def connect(self, event_name, callback): - """ - Attach an event handler to the canvas through the native GTK interface. - - :param event_name: Name of the event - :type event_name: str - :param callback: Function to call - :type callback: function - :return: Nothing - """ - self.canvas.connect(event_name, callback) - - def clear(self): - """ - Clears axes and figure. - - :return: None - """ - - # Clear - self.axes.cla() - self.figure.clf() - - # Re-build - self.figure.add_axes(self.axes) - self.axes.set_aspect(1) - self.axes.grid(True) - - # Re-draw - self.canvas.queue_draw() - - def adjust_axes(self, xmin, ymin, xmax, ymax): - """ - Adjusts all axes while maintaining the use of the whole canvas - and an aspect ratio to 1:1 between x and y axes. The parameters are an original - request that will be modified to fit these restrictions. - - :param xmin: Requested minimum value for the X axis. - :type xmin: float - :param ymin: Requested minimum value for the Y axis. - :type ymin: float - :param xmax: Requested maximum value for the X axis. - :type xmax: float - :param ymax: Requested maximum value for the Y axis. - :type ymax: float - :return: None - """ - - print "PC.adjust_axes()" - - width = xmax - xmin - height = ymax - ymin - try: - r = width / height - except: - print "ERROR: Height is", height - return - canvas_w, canvas_h = self.canvas.get_width_height() - canvas_r = float(canvas_w) / canvas_h - x_ratio = float(self.x_margin) / canvas_w - y_ratio = float(self.y_margin) / canvas_h - - if r > canvas_r: - ycenter = (ymin + ymax) / 2.0 - newheight = height * r / canvas_r - ymin = ycenter - newheight / 2.0 - ymax = ycenter + newheight / 2.0 - else: - xcenter = (xmax + ymin) / 2.0 - newwidth = width * canvas_r / r - xmin = xcenter - newwidth / 2.0 - xmax = xcenter + newwidth / 2.0 - - # Adjust axes - for ax in self.figure.get_axes(): - if ax._label != 'base': - ax.set_frame_on(False) # No frame - ax.set_xticks([]) # No tick - ax.set_yticks([]) # No ticks - ax.patch.set_visible(False) # No background - ax.set_aspect(1) - ax.set_xlim((xmin, xmax)) - ax.set_ylim((ymin, ymax)) - ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) - - # Re-draw - self.canvas.queue_draw() - - def auto_adjust_axes(self, *args): - """ - Calls ``adjust_axes()`` using the extents of the base axes. - - :rtype : None - :return: None - """ - - xmin, xmax = self.axes.get_xlim() - ymin, ymax = self.axes.get_ylim() - self.adjust_axes(xmin, ymin, xmax, ymax) - - def zoom(self, factor, center=None): - """ - Zooms the plot by factor around a given - center point. Takes care of re-drawing. - - :param factor: Number by which to scale the plot. - :type factor: float - :param center: Coordinates [x, y] of the point around which to scale the plot. - :type center: list - :return: None - """ - - xmin, xmax = self.axes.get_xlim() - ymin, ymax = self.axes.get_ylim() - width = xmax - xmin - height = ymax - ymin - - if center is None or center == [None, None]: - center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] - - # For keeping the point at the pointer location - relx = (xmax - center[0]) / width - rely = (ymax - center[1]) / height - - new_width = width / factor - new_height = height / factor - - xmin = center[0] - new_width * (1 - relx) - xmax = center[0] + new_width * relx - ymin = center[1] - new_height * (1 - rely) - ymax = center[1] + new_height * rely - - # Adjust axes - for ax in self.figure.get_axes(): - ax.set_xlim((xmin, xmax)) - ax.set_ylim((ymin, ymax)) - - # Re-draw - self.canvas.queue_draw() - - def pan(self, x, y): - xmin, xmax = self.axes.get_xlim() - ymin, ymax = self.axes.get_ylim() - width = xmax - xmin - height = ymax - ymin - - # Adjust axes - for ax in self.figure.get_axes(): - ax.set_xlim((xmin + x*width, xmax + x*width)) - ax.set_ylim((ymin + y*height, ymax + y*height)) - - # Re-draw - self.canvas.queue_draw() - - def new_axes(self, name): - """ - Creates and returns an Axes object attached to this object's Figure. - - :param name: Unique label for the axes. - :return: Axes attached to the figure. - :rtype: Axes - """ - - return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) - - def on_scroll(self, canvas, event): - """ - Scroll event handler. - - :param canvas: The widget generating the event. Ignored. - :param event: Event object containing the event information. - :return: None - """ - - # So it can receive key presses - self.canvas.grab_focus() - - # Event info - z, direction = event.get_scroll_direction() - - if self.key is None: - - if direction is Gdk.ScrollDirection.UP: - self.zoom(1.5, self.mouse) - else: - self.zoom(1/1.5, self.mouse) - return - - if self.key == 'shift': - - if direction is Gdk.ScrollDirection.UP: - self.pan(0.3, 0) - else: - self.pan(-0.3, 0) - return - - if self.key == 'ctrl+control': - - if direction is Gdk.ScrollDirection.UP: - self.pan(0, 0.3) - else: - self.pan(0, -0.3) - return - - def on_mouse_move(self, event): - """ - Mouse movement event hadler. Stores the coordinates. - - :param event: Contains information about the event. - :return: None - """ - self.mouse = [event.xdata, event.ydata] \ No newline at end of file diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 0e565b72..09fb699c 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -13,7 +13,7 @@ from gi.repository import GObject import inspect # TODO: Remove -from FlatCAMApp import * +import FlatCAMApp from camlib import * from ObjectUI import * @@ -123,15 +123,15 @@ class FlatCAMObj(GObject.GObject, object): """ if self.axes is None: - print "New axes" + FlatCAMApp.App.log.debug("setup_axes(): 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" + FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes") self.axes.cla() figure.add_axes(self.axes) else: - print "Clearing Axes" + FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes") self.axes.cla() # Remove all decoration. The app's axes will have @@ -158,7 +158,7 @@ class FlatCAMObj(GObject.GObject, object): :return: None :rtype: None """ - print inspect.stack()[1][3], "--> FlatCAMObj.read_form()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()") for option in self.options: self.read_form_item(option) @@ -171,7 +171,7 @@ class FlatCAMObj(GObject.GObject, object): """ self.muted_ui = True - print inspect.stack()[1][3], "--> FlatCAMObj.build_ui()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()") # Where the UI for this object is drawn # box_selected = self.app.builder.get_object("box_selected") @@ -186,8 +186,8 @@ class FlatCAMObj(GObject.GObject, object): # box_selected.pack_start(sw, True, True, 0) box_selected.add(self.ui) self.to_form() - box_selected.show_all() - self.ui.show() + GLib.idle_add(box_selected.show_all) + GLib.idle_add(self.ui.show_all) self.muted_ui = False def set_form_item(self, option): @@ -202,7 +202,7 @@ class FlatCAMObj(GObject.GObject, object): try: self.form_fields[option].set_value(self.options[option]) except KeyError: - App.log.warn("Tried to set an option or field that does not exist: %s" % option) + self.app.log.warn("Tried to set an option or field that does not exist: %s" % option) def read_form_item(self, option): """ @@ -216,7 +216,7 @@ class FlatCAMObj(GObject.GObject, object): try: self.options[option] = self.form_fields[option].get_value() except KeyError: - App.log.warning("Failed to read option from field: %s" % option) + self.app.log.warning("Failed to read option from field: %s" % option) def plot(self): """ @@ -497,8 +497,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): zorder=2) self.axes.add_patch(patch) except AssertionError: - print "WARNING: A geometry component was not a polygon:" - print poly + FlatCAMApp.App.log.warning("A geometry component was not a polygon:") + FlatCAMApp.App.log.warning(str(poly)) else: for poly in geometry: x, y = poly.exterior.xy @@ -547,15 +547,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): "toolselection": "" }) - # self.form_kinds.update({ - # "plot": "cb", - # "solid": "cb", - # "drillz": "entry_eval", - # "travelz": "entry_eval", - # "feedrate": "entry_eval", - # "toolselection": "entry_text" - # }) - # TODO: Document this. self.tool_cbs = {} @@ -564,10 +555,49 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # from predecessors. self.ser_attrs += ['options', 'kind'] + assert isinstance(self.ui, ExcellonObjectUI) self.ui.plot_cb.connect('clicked', self.on_plot_cb_click) self.ui.plot_cb.connect('activate', self.on_plot_cb_click) self.ui.solid_cb.connect('clicked', self.on_solid_cb_click) self.ui.solid_cb.connect('activate', self.on_solid_cb_click) + self.ui.choose_tools_button.connect('clicked', lambda args: self.show_tool_chooser()) + self.ui.choose_tools_button.connect('activate', lambda args: self.show_tool_chooser()) + self.ui.generate_cnc_button.connect('clicked', self.on_create_cncjob_button_click) + self.ui.generate_cnc_button.connect('activate', self.on_create_cncjob_button_click) + + def on_create_cncjob_button_click(self, *args): + self.read_form() + job_name = self.options["name"] + "_cnc" + + # Object initialization function for app.new_object() + def job_init(job_obj, app_obj): + assert isinstance(job_obj, FlatCAMCNCjob) + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job...")) + job_obj.z_cut = self.options["drillz"] + job_obj.z_move = self.options["travelz"] + job_obj.feedrate = self.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(self, self.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, "")) + + # Send to worker + self.app.worker.add_task(job_thread, [self.app]) def on_plot_cb_click(self, *args): if self.muted_ui: @@ -669,11 +699,6 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): "tooldia": self.ui.tooldia_entry }) - # self.form_kinds.update({ - # "plot": "cb", - # "tooldia": "entry_eval" - # }) - # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. @@ -682,6 +707,18 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.ui.plot_cb.connect('clicked', self.on_plot_cb_click) self.ui.plot_cb.connect('activate', self.on_plot_cb_click) + self.ui.export_gcode_button.connect('clicked', self.on_exportgcode_button_click) + self.ui.export_gcode_button.connect('activate', self.on_exportgcode_button_click) + + def on_exportgcode_button_click(self, *args): + def on_success(app_obj, filename): + f = open(filename, 'w') + f.write(self.gcode) + f.close() + app_obj.info("Saved to: " + filename) + + self.app.file_chooser_save_action(on_success) + def on_plot_cb_click(self, *args): if self.muted_ui: return @@ -702,7 +739,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): def convert_units(self, units): factor = CNCjob.convert_units(self, units) - print "FlatCAMCNCjob.convert_units()" + FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()") self.options["tooldia"] *= factor @@ -795,7 +832,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): geo_obj.options["cnctooldia"] = tooldia name = self.options["name"] + "_paint" - self.new_object("geometry", name, gen_paintarea) + self.app.new_object("geometry", name, gen_paintarea) subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit) @@ -934,7 +971,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.axes.plot(x, y, 'r-') continue - print "WARNING: Did not plot:", str(type(geo)) + FlatCAMApp.App.log.warning("Did not plot:", str(type(geo))) #self.app.plotcanvas.auto_adjust_axes() GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) \ No newline at end of file diff --git a/GUIElements.py b/GUIElements.py index f067677e..d8b55ef1 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -1,6 +1,15 @@ +############################################################ +# FlatCAM: 2D Post-processing for Manufacturing # +# http://caram.cl/software/flatcam # +# Author: Juan Pablo Caram (c) # +# Date: 2/5/2014 # +# MIT Licence # +############################################################ + from gi.repository import Gtk import re from copy import copy +import FlatCAMApp class RadioSet(Gtk.Box): @@ -24,16 +33,20 @@ class RadioSet(Gtk.Box): else: choice['radio'] = Gtk.RadioButton.new_with_label_from_widget(self.group, choice['label']) self.pack_start(choice['radio'], expand=True, fill=False, padding=2) - # choice['radio'].connect('toggled', self.on_toggle) + choice['radio'].connect('toggled', self.on_toggle) - # def on_toggle(self, *args): - # return + self.group_toggle_fn = lambda x, y: None + + def on_toggle(self, btn): + if btn.get_active(): + self.group_toggle_fn(btn, self.get_value) + return def get_value(self): for choice in self.choices: if choice['radio'].get_active(): return choice['value'] - print "ERROR: No button was toggled in RadioSet." + FlatCAMApp.App.log.error("No button was toggled in RadioSet.") return None def set_value(self, val): @@ -41,7 +54,7 @@ class RadioSet(Gtk.Box): if choice['value'] == val: choice['radio'].set_active(True) return - print "ERROR: Value given is not part of this RadioSet:", val + FlatCAMApp.App.log.error("Value given is not part of this RadioSet: %s" % str(val)) class LengthEntry(Gtk.Entry): @@ -63,7 +76,7 @@ class LengthEntry(Gtk.Entry): if val is not None: self.set_text(str(val)) else: - print "WARNING: Could not interpret entry:", self.get_text() + FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text()) def get_value(self): raw = self.get_text().strip(' ') @@ -76,7 +89,7 @@ class LengthEntry(Gtk.Entry): else: return float(match.group(1)) except: - print "ERROR: Could not parse value in entry:", raw + FlatCAMApp.App.log.error("Could not parse value in entry: %s" % str(raw)) return None def set_value(self, val): @@ -94,14 +107,14 @@ class FloatEntry(Gtk.Entry): if val is not None: self.set_text(str(val)) else: - print "WARNING: Could not interpret entry:", self.get_text() + FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text()) def get_value(self): raw = self.get_text().strip(' ') try: evaled = eval(raw) except: - print "ERROR: Could not evaluate:", raw + FlatCAMApp.App.log.error("Could not evaluate: %s" % str(raw)) return None return float(evaled) @@ -132,6 +145,29 @@ class FCEntry(Gtk.Entry): self.set_text(str(val)) +class EvalEntry(Gtk.Entry): + def __init__(self): + Gtk.Entry.__init__(self) + + def on_activate(self, *args): + val = self.get_value() + if val is not None: + self.set_text(str(val)) + else: + FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text()) + + def get_value(self): + raw = self.get_text().strip(' ') + try: + return eval(raw) + except: + FlatCAMApp.App.log.error("Could not evaluate: %s" % str(raw)) + return None + + def set_value(self, val): + self.set_text(str(val)) + + class FCCheckBox(Gtk.CheckButton): def __init__(self, label=''): Gtk.CheckButton.__init__(self, label=label) diff --git a/ObjectCollection.py b/ObjectCollection.py index 4aefc32c..00674560 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -9,6 +9,7 @@ from FlatCAMObj import * from gi.repository import Gtk, GdkPixbuf import inspect # TODO: Remove +import FlatCAMApp class ObjectCollection: @@ -75,11 +76,11 @@ class ObjectCollection: iterat = self.store.iter_next(iterat) def delete_all(self): - print "OC.delete_all()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()") self.store.clear() def delete_active(self): - print "OC.delete_active()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_active()") try: model, treeiter = self.tree_selection.get_selected() self.store.remove(treeiter) @@ -94,7 +95,7 @@ class ObjectCollection: :param selection: Ignored. :return: None """ - print inspect.stack()[1][3], "--> OC.on_list_selection_change()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.on_list_selection_change()") try: self.get_active().build_ui() except AttributeError: # For None being active @@ -109,11 +110,11 @@ class ObjectCollection: :type name: str :return: None """ - print "OC.set_active()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_active()") self.set_list_selection(name) def get_active(self): - print inspect.stack()[1][3], "--> OC.get_active()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_active()") try: model, treeiter = self.tree_selection.get_selected() return model[treeiter][0] @@ -128,7 +129,7 @@ class ObjectCollection: :rtype name: str :return: None """ - print inspect.stack()[1][3], "--> OC.set_list_selection()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_list_selection()") iterat = self.store.get_iter_first() while iterat is not None and self.store[iterat][0].options["name"] != name: iterat = self.store.iter_next(iterat) @@ -144,7 +145,7 @@ class ObjectCollection: :type active: bool :return: None """ - print inspect.stack()[1][3], "--> OC.append()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.append()") def guitask(): self.store.append([obj]) @@ -159,7 +160,7 @@ class ObjectCollection: :return: List of names. :rtype: list """ - print "OC.get_names()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_names()") names = [] iterat = self.store.get_iter_first() while iterat is not None: @@ -175,7 +176,7 @@ class ObjectCollection: :return: [xmin, ymin, xmax, ymax] :rtype: list """ - print "OC.get_bounds()" + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()") # TODO: Move the operation out of here. @@ -194,7 +195,7 @@ class ObjectCollection: xmax = max([xmax, gxmax]) ymax = max([ymax, gymax]) except: - print "DEV WARNING: Tried to get bounds of empty geometry." + FlatCAMApp.App.log.waring("DEV WARNING: Tried to get bounds of empty geometry.") iterat = self.store.iter_next(iterat) return [xmin, ymin, xmax, ymax] @@ -205,6 +206,7 @@ class ObjectCollection: :return: List with all FlatCAMObj. :rtype: list """ + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_list()") collection_list = [] iterat = self.store.get_iter_first() while iterat is not None: @@ -222,6 +224,8 @@ class ObjectCollection: :return: The requested object or None if no such object. :rtype: FlatCAMObj or None """ + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()") + iterat = self.store.get_iter_first() while iterat is not None: obj = self.store[iterat][0] diff --git a/ObjectUI.py b/ObjectUI.py index 7b6fa15c..ae87033d 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -1,12 +1,20 @@ +############################################################ +# FlatCAM: 2D Post-processing for Manufacturing # +# http://caram.cl/software/flatcam # +# Author: Juan Pablo Caram (c) # +# Date: 2/5/2014 # +# MIT Licence # +############################################################ from gi.repository import Gtk -import re -from copy import copy - from GUIElements import * class ObjectUI(Gtk.VBox): + """ + Base class for the UI of FlatCAM objects. + """ + def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object'): Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False) @@ -26,7 +34,7 @@ class ObjectUI(Gtk.VBox): ## Object name self.name_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2) - self.pack_start(self.name_box, expand=True, fill=False, padding=2) + self.pack_start(self.name_box, expand=False, fill=False, padding=2) name_label = Gtk.Label('Name:') name_label.set_justify(Gtk.Justification.RIGHT) self.name_box.pack_start(name_label, @@ -42,10 +50,10 @@ class ObjectUI(Gtk.VBox): ## Scale self.scale_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5) self.scale_label.set_markup('Scale:') - self.pack_start(self.scale_label, expand=True, fill=False, padding=2) + self.pack_start(self.scale_label, expand=False, fill=False, padding=2) grid5 = Gtk.Grid(column_spacing=3, row_spacing=2) - self.pack_start(grid5, expand=True, fill=False, padding=2) + self.pack_start(grid5, expand=False, fill=False, padding=2) # Factor l10 = Gtk.Label('Factor:', xalign=1) @@ -56,25 +64,25 @@ class ObjectUI(Gtk.VBox): # GO Button self.scale_button = Gtk.Button(label='Scale') - self.pack_start(self.scale_button, expand=True, fill=False, padding=2) + self.pack_start(self.scale_button, expand=False, fill=False, padding=2) ## Offset self.offset_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5) self.offset_label.set_markup('Offset:') - self.pack_start(self.offset_label, expand=True, fill=False, padding=2) + self.pack_start(self.offset_label, expand=False, fill=False, padding=2) grid6 = Gtk.Grid(column_spacing=3, row_spacing=2) - self.pack_start(grid6, expand=True, fill=False, padding=2) + self.pack_start(grid6, expand=False, fill=False, padding=2) # Vector l11 = Gtk.Label('Offset Vector:', xalign=1) grid6.attach(l11, 0, 0, 1, 1) - self.offsetvector_entry = FCEntry() + self.offsetvector_entry = EvalEntry() self.offsetvector_entry.set_text("(0.0, 0.0)") grid6.attach(self.offsetvector_entry, 1, 0, 1, 1) self.offset_button = Gtk.Button(label='Scale') - self.pack_start(self.offset_button, expand=True, fill=False, padding=2) + self.pack_start(self.offset_button, expand=False, fill=False, padding=2) def set_field(self, name, value): getattr(self, name).set_value(value) @@ -84,16 +92,20 @@ class ObjectUI(Gtk.VBox): class CNCObjectUI(ObjectUI): + """ + User interface for CNCJob objects. + """ + def __init__(self): ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png') ## Plot options self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5) self.plot_options_label.set_markup("Plot Options:") - self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2) + self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2) grid0 = Gtk.Grid(column_spacing=3, row_spacing=2) - self.pack_start(grid0, expand=True, fill=False, padding=2) + self.custom_box.pack_start(grid0, expand=False, fill=False, padding=2) # Plot CB self.plot_cb = FCCheckBox(label='Plot') @@ -107,19 +119,23 @@ class CNCObjectUI(ObjectUI): # Update plot button self.updateplot_button = Gtk.Button(label='Update Plot') - self.pack_start(self.updateplot_button, expand=True, fill=False, padding=2) + self.custom_box.pack_start(self.updateplot_button, expand=False, fill=False, padding=2) ## Export G-Code self.export_gcode_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5) self.export_gcode_label.set_markup("Export G-Code:") - self.pack_start(self.export_gcode_label, expand=True, fill=False, padding=2) + self.custom_box.pack_start(self.export_gcode_label, expand=False, fill=False, padding=2) # GO Button self.export_gcode_button = Gtk.Button(label='Export G-Code') - self.pack_start(self.export_gcode_button, expand=True, fill=False, padding=2) + self.custom_box.pack_start(self.export_gcode_button, expand=False, fill=False, padding=2) class GeometryObjectUI(ObjectUI): + """ + User interface for Geometry objects. + """ + def __init__(self): ObjectUI.__init__(self, title='Geometry Object', icon_file='share/geometry32.png') @@ -200,6 +216,10 @@ class GeometryObjectUI(ObjectUI): class ExcellonObjectUI(ObjectUI): + """ + User interface for Excellon objects. + """ + def __init__(self): ObjectUI.__init__(self, title='Excellon Object', icon_file='share/drill32.png') @@ -254,6 +274,9 @@ class ExcellonObjectUI(ObjectUI): class GerberObjectUI(ObjectUI): + """ + User interface for Gerber objects. + """ def __init__(self): ObjectUI.__init__(self, title='Gerber Object') diff --git a/PlotCanvas.py b/PlotCanvas.py new file mode 100644 index 00000000..0aef35e4 --- /dev/null +++ b/PlotCanvas.py @@ -0,0 +1,310 @@ +from gi.repository import Gdk +from matplotlib.figure import Figure +from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas +#from FlatCAMApp import * +import FlatCAMApp + + +class PlotCanvas: + """ + Class handling the plotting area in the application. + """ + + def __init__(self, container): + """ + The constructor configures the Matplotlib figure that + will contain all plots, creates the base axes and connects + events to the plotting area. + + :param container: The parent container in which to draw plots. + :rtype: PlotCanvas + """ + # Options + self.x_margin = 15 # pixels + self.y_margin = 25 # Pixels + + # Parent container + self.container = container + + # Plots go onto a single matplotlib.figure + self.figure = Figure(dpi=50) # TODO: dpi needed? + self.figure.patch.set_visible(False) + + # These axes show the ticks and grid. No plotting done here. + # New axes must have a label, otherwise mpl returns an existing one. + self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) + self.axes.set_aspect(1) + self.axes.grid(True) + + # The canvas is the top level container (Gtk.DrawingArea) + self.canvas = FigureCanvas(self.figure) + self.canvas.set_hexpand(1) + self.canvas.set_vexpand(1) + self.canvas.set_can_focus(True) # For key press + + # Attach to parent + self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? + + # Events + self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) + self.canvas.connect('configure-event', self.auto_adjust_axes) + self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) + self.canvas.connect("scroll-event", self.on_scroll) + self.canvas.mpl_connect('key_press_event', self.on_key_down) + self.canvas.mpl_connect('key_release_event', self.on_key_up) + + self.mouse = [0, 0] + self.key = None + + def on_key_down(self, event): + """ + + :param event: + :return: + """ + self.key = event.key + + def on_key_up(self, event): + """ + + :param event: + :return: + """ + self.key = None + + def mpl_connect(self, event_name, callback): + """ + Attach an event handler to the canvas through the Matplotlib interface. + + :param event_name: Name of the event + :type event_name: str + :param callback: Function to call + :type callback: func + :return: Connection id + :rtype: int + """ + return self.canvas.mpl_connect(event_name, callback) + + def mpl_disconnect(self, cid): + """ + Disconnect callback with the give id. + :param cid: Callback id. + :return: None + """ + self.canvas.mpl_disconnect(cid) + + def connect(self, event_name, callback): + """ + Attach an event handler to the canvas through the native GTK interface. + + :param event_name: Name of the event + :type event_name: str + :param callback: Function to call + :type callback: function + :return: Nothing + """ + self.canvas.connect(event_name, callback) + + def clear(self): + """ + Clears axes and figure. + + :return: None + """ + + # Clear + self.axes.cla() + try: + self.figure.clf() + except KeyError: + FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") + + # Re-build + self.figure.add_axes(self.axes) + self.axes.set_aspect(1) + self.axes.grid(True) + + # Re-draw + self.canvas.queue_draw() + + def adjust_axes(self, xmin, ymin, xmax, ymax): + """ + Adjusts all axes while maintaining the use of the whole canvas + and an aspect ratio to 1:1 between x and y axes. The parameters are an original + request that will be modified to fit these restrictions. + + :param xmin: Requested minimum value for the X axis. + :type xmin: float + :param ymin: Requested minimum value for the Y axis. + :type ymin: float + :param xmax: Requested maximum value for the X axis. + :type xmax: float + :param ymax: Requested maximum value for the Y axis. + :type ymax: float + :return: None + """ + + FlatCAMApp.App.log.debug("PC.adjust_axes()") + + width = xmax - xmin + height = ymax - ymin + try: + r = width / height + except ZeroDivisionError: + FlatCAMApp.App.log.error("Height is %f" % height) + return + canvas_w, canvas_h = self.canvas.get_width_height() + canvas_r = float(canvas_w) / canvas_h + x_ratio = float(self.x_margin) / canvas_w + y_ratio = float(self.y_margin) / canvas_h + + if r > canvas_r: + ycenter = (ymin + ymax) / 2.0 + newheight = height * r / canvas_r + ymin = ycenter - newheight / 2.0 + ymax = ycenter + newheight / 2.0 + else: + xcenter = (xmax + xmin) / 2.0 + newwidth = width * canvas_r / r + xmin = xcenter - newwidth / 2.0 + xmax = xcenter + newwidth / 2.0 + + # Adjust axes + for ax in self.figure.get_axes(): + if ax._label != 'base': + ax.set_frame_on(False) # No frame + ax.set_xticks([]) # No tick + ax.set_yticks([]) # No ticks + ax.patch.set_visible(False) # No background + ax.set_aspect(1) + ax.set_xlim((xmin, xmax)) + ax.set_ylim((ymin, ymax)) + ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) + + # Re-draw + self.canvas.queue_draw() + + def auto_adjust_axes(self, *args): + """ + Calls ``adjust_axes()`` using the extents of the base axes. + + :rtype : None + :return: None + """ + + xmin, xmax = self.axes.get_xlim() + ymin, ymax = self.axes.get_ylim() + self.adjust_axes(xmin, ymin, xmax, ymax) + + def zoom(self, factor, center=None): + """ + Zooms the plot by factor around a given + center point. Takes care of re-drawing. + + :param factor: Number by which to scale the plot. + :type factor: float + :param center: Coordinates [x, y] of the point around which to scale the plot. + :type center: list + :return: None + """ + + xmin, xmax = self.axes.get_xlim() + ymin, ymax = self.axes.get_ylim() + width = xmax - xmin + height = ymax - ymin + + if center is None or center == [None, None]: + center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] + + # For keeping the point at the pointer location + relx = (xmax - center[0]) / width + rely = (ymax - center[1]) / height + + new_width = width / factor + new_height = height / factor + + xmin = center[0] - new_width * (1 - relx) + xmax = center[0] + new_width * relx + ymin = center[1] - new_height * (1 - rely) + ymax = center[1] + new_height * rely + + # Adjust axes + for ax in self.figure.get_axes(): + ax.set_xlim((xmin, xmax)) + ax.set_ylim((ymin, ymax)) + + # Re-draw + self.canvas.queue_draw() + + def pan(self, x, y): + xmin, xmax = self.axes.get_xlim() + ymin, ymax = self.axes.get_ylim() + width = xmax - xmin + height = ymax - ymin + + # Adjust axes + for ax in self.figure.get_axes(): + ax.set_xlim((xmin + x*width, xmax + x*width)) + ax.set_ylim((ymin + y*height, ymax + y*height)) + + # Re-draw + self.canvas.queue_draw() + + def new_axes(self, name): + """ + Creates and returns an Axes object attached to this object's Figure. + + :param name: Unique label for the axes. + :return: Axes attached to the figure. + :rtype: Axes + """ + + return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) + + def on_scroll(self, canvas, event): + """ + Scroll event handler. + + :param canvas: The widget generating the event. Ignored. + :param event: Event object containing the event information. + :return: None + """ + + # So it can receive key presses + self.canvas.grab_focus() + + # Event info + z, direction = event.get_scroll_direction() + + if self.key is None: + + if direction is Gdk.ScrollDirection.UP: + self.zoom(1.5, self.mouse) + else: + self.zoom(1/1.5, self.mouse) + return + + if self.key == 'shift': + + if direction is Gdk.ScrollDirection.UP: + self.pan(0.3, 0) + else: + self.pan(-0.3, 0) + return + + if self.key == 'ctrl+control': + + if direction is Gdk.ScrollDirection.UP: + self.pan(0, 0.3) + else: + self.pan(0, -0.3) + return + + def on_mouse_move(self, event): + """ + Mouse movement event hadler. Stores the coordinates. + + :param event: Contains information about the event. + :return: None + """ + self.mouse = [event.xdata, event.ydata] diff --git a/camlib.py b/camlib.py index 630cf18f..c9fdef4f 100644 --- a/camlib.py +++ b/camlib.py @@ -27,6 +27,15 @@ import simplejson as json # TODO: Commented for FlatCAM packaging with cx_freeze #from matplotlib.pyplot import plot +import logging + +log = logging.getLogger('base2') +log.setLevel(logging.DEBUG) +formatter = logging.Formatter('[%(levelname)s] %(message)s') +handler = logging.StreamHandler() +handler.setFormatter(formatter) +log.addHandler(handler) + class Geometry(object): def __init__(self): @@ -57,7 +66,7 @@ class Geometry(object): of geometry: (xmin, ymin, xmax, ymax). """ if self.solid_geometry is None: - print "Warning: solid_geometry not computed yet." + log.warning("solid_geometry not computed yet.") return (0, 0, 0, 0) if type(self.solid_geometry) == list: @@ -72,7 +81,7 @@ class Geometry(object): bounds of geometry. """ if self.solid_geometry is None: - print "Warning: solid_geometry not computed yet." + log.warning("Solid_geometry not computed yet.") return 0 bounds = self.bounds() return (bounds[2]-bounds[0], bounds[3]-bounds[1]) @@ -133,7 +142,7 @@ class Geometry(object): :return: Scaling factor resulting from unit change. :rtype: float """ - print "Geometry.convert_units()" + log.debug("Geometry.convert_units()") if units.upper() == self.units.upper(): return 1.0 @@ -143,7 +152,7 @@ class Geometry(object): elif units.upper() == "IN": factor = 1/25.4 else: - print "Unsupported units:", units + log.error("Unsupported units: %s" % str(units)) return 1.0 self.units = units @@ -293,7 +302,7 @@ class ApertureMacro: self.primitives.append([eval(x) for x in elements]) continue - print "WARNING: Unknown syntax of aperture macro part:", part + log.warning("Unknown syntax of aperture macro part: %s" % str(part)) def append(self, data): """ @@ -834,7 +843,7 @@ class Gerber (Geometry): "modifiers": paramList} return apid - print "WARNING: Aperture not implemented:", apertureType + log.warning("Aperture not implemented: %s" % str(apertureType)) return None def parse_file(self, filename): @@ -975,7 +984,7 @@ class Gerber (Geometry): geo = Polygon(path) else: if last_path_aperture is None: - print "Warning: No aperture defined for curent path. (%d)" % line_num + log.warning("No aperture defined for curent path. (%d)" % line_num) width = self.apertures[last_path_aperture]["size"] geo = LineString(path).buffer(width/2) poly_buffer.append(geo) @@ -1016,13 +1025,13 @@ class Gerber (Geometry): j = 0 if quadrant_mode is None: - print "ERROR: Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num - print gline + log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num) + log.error(gline) continue if mode is None and current_interpolation_mode not in [2, 3]: - print "ERROR: Found arc without circular interpolation mode defined. (%d)" % line_num - print gline + log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) + log.error(gline) continue elif mode is not None: current_interpolation_mode = int(mode) @@ -1033,10 +1042,10 @@ class Gerber (Geometry): # Nothing created! Pen Up. if current_operation_code == 2: - print "Warning: Arc with D2. (%d)" % line_num + log.warning("Arc with D2. (%d)" % line_num) if len(path) > 1: if last_path_aperture is None: - print "Warning: No aperture defined for curent path. (%d)" % line_num + log.warning("No aperture defined for curent path. (%d)" % line_num) # --- BUFFERED --- width = self.apertures[last_path_aperture]["size"] @@ -1050,7 +1059,7 @@ class Gerber (Geometry): # Flash should not happen here if current_operation_code == 3: - print "ERROR: Trying to flash within arc. (%d)" % line_num + log.error("Trying to flash within arc. (%d)" % line_num) continue if quadrant_mode == 'MULTI': @@ -1075,7 +1084,7 @@ class Gerber (Geometry): continue if quadrant_mode == 'SINGLE': - print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num + log.warning("Single quadrant arc are not implemented yet. (%d)" % line_num) ### Operation code alone match = self.opcode_re.search(gline) @@ -1228,7 +1237,7 @@ class Gerber (Geometry): continue ### Line did not match any pattern. Warn user. - print "WARNING: Line ignored (%d):" % line_num, gline + log.warning("Line ignored (%d): %s" % (line_num, gline)) if len(path) > 1: # EOF, create shapely LineString if something still in path @@ -1528,7 +1537,7 @@ class Excellon(Geometry): y = current_y if x is None or y is None: - print "ERROR: Missing coordinates" + log.error("Missing coordinates") continue self.drills.append({'point': Point((x, y)), 'tool': current_tool}) @@ -1550,7 +1559,7 @@ class Excellon(Geometry): y = current_y if x is None or y is None: - print "ERROR: Missing coordinates" + log.error("Missing coordinates") continue self.drills.append({'point': Point((x, y)), 'tool': current_tool}) @@ -1581,7 +1590,7 @@ class Excellon(Geometry): self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)] continue - print "WARNING: Line ignored:", eline + log.warning("Line ignored: %s" % eline) def parse_number(self, number_str): """ @@ -1724,7 +1733,7 @@ class CNCjob(Geometry): def convert_units(self, units): factor = Geometry.convert_units(self, units) - print "CNCjob.convert_units()" + log.debug("CNCjob.convert_units()") self.z_cut *= factor self.z_move *= factor @@ -1783,20 +1792,20 @@ class CNCjob(Geometry): :return: None :rtype: None """ - print "Creating CNC Job from Excellon..." + log.debug("Creating CNC Job from Excellon...") if tools == "all": tools = [tool for tool in exobj.tools] else: tools = [x.strip() for x in tools.split(",")] tools = filter(lambda i: i in exobj.tools, tools) - print "Tools are:", tools + log.debug("Tools are: %s" % str(tools)) points = [] for drill in exobj.drills: if drill['tool'] in tools: points.append(drill['point']) - print "Found %d drills." % len(points) + log.debug("Found %d drills." % len(points)) #self.kind = "drill" self.gcode = [] @@ -1873,8 +1882,8 @@ class CNCjob(Geometry): self.gcode += self.polygon2gcode(poly, tolerance=tolerance) continue - print "WARNING: G-code generation not implemented for %s" % (str(type(geo))) - + log.warning("G-code generation not implemented for %s" % (str(type(geo)))) + self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting self.gcode += "G00 X0Y0\n" self.gcode += "M05\n" # Spindle stop @@ -1967,8 +1976,8 @@ class CNCjob(Geometry): ## Changing height if 'Z' in gobj: if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']: - print "WARNING: Non-orthogonal motion: From", current - print " To:", gobj + log.warning("Non-orthogonal motion: From %s" % str(current)) + log.warning(" To: %s" % str(gobj)) current['Z'] = gobj['Z'] # Store the path into geometry and reset path if len(path) > 1: @@ -2239,7 +2248,7 @@ def get_bounds(geometry_list): xmax = max([xmax, gxmax]) ymax = max([ymax, gymax]) except: - print "DEV WARNING: Tried to get bounds of empty geometry." + log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.") return [xmin, ymin, xmax, ymax] @@ -2392,7 +2401,7 @@ def plotg(geo): _ = iter(g) plotg(g) except: - print "Cannot plot:", str(type(g)) + log.error("Cannot plot: " + str(type(g))) continue diff --git a/defaults.json b/defaults.json index 9c29c686..10c197bd 100644 --- a/defaults.json +++ b/defaults.json @@ -1 +1 @@ -{"gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "gerber_cutouttooldia": 0.07, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false} \ No newline at end of file +{"gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "gerber_bboxmargin": 0.0, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "gerber_cutouttooldia": 0.07, "gerber_gaps": "4", "geometry_painttooldia": 0.0625, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "gerber_noncoppermargin": 0.0} \ No newline at end of file diff --git a/recent.json b/recent.json index 73729efe..3816678f 100644 --- a/recent.json +++ b/recent.json @@ -1 +1 @@ -[{"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\Bridge2.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.TXT"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.GTL"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\KiCad_Bridge2.drl"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\LockController_v1.0_pcb-RoundHoles.TXT\\LockController_v1.0_pcb-RoundHoles.TXT"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\KiCad_Bridge2-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\WindMills - Bottom Copper 2.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane_modified.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\CC_LOAD_7000164-00_REV_A_copper_top.gbr"}] \ No newline at end of file +[{"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\bedini 7 coils capacitor discharge.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\test.cnc"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\Bridge2_test.fcproj"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\Controller_Board_Shield\\CBS-B_Cu.gbl"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\squarerpcb\\KiCad_Squarer.gcode"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Squarer\\KiCad_Squarer.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Squarer\\KiCad_Squarer-F_Cu.gtl"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\Bridge2.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.TXT"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.GTL"}] \ No newline at end of file