From 0bdc3b19f0e46a0ce7902fcf51e122801f913448 Mon Sep 17 00:00:00 2001
From: Juan Pablo Caram
Date: Fri, 25 Apr 2014 01:24:03 -0400
Subject: [PATCH] Major modifications to data/gui interactions. In progress.
---
FlatCAM.py | 2965 +----------------------------
FlatCAM.ui | 4011 +--------------------------------------
FlatCAMApp.py | 3025 +++++++++++++++++++++++++++++
FlatCAMObj.py | 495 ++++-
GUIElements.py | 143 ++
ObjectCollection.py | 254 +++
ObjectUI.py | 375 ++++
camlib.py | 118 +-
defaults.json | 2 +-
doc/build/app.html | 4 +-
doc/build/genindex.html | 2 +-
recent.json | 2 +-
12 files changed, 4260 insertions(+), 7136 deletions(-)
create mode 100644 FlatCAMApp.py
create mode 100644 GUIElements.py
create mode 100644 ObjectCollection.py
create mode 100644 ObjectUI.py
diff --git a/FlatCAM.py b/FlatCAM.py
index 5810fef1..7f4ebefc 100644
--- a/FlatCAM.py
+++ b/FlatCAM.py
@@ -6,2969 +6,8 @@
# MIT Licence #
############################################################
-import threading
-import traceback
-import sys
-import urllib
-import copy
-import random
-
-from gi.repository import Gtk, GdkPixbuf
-from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
-from shapely import speedups
-
-
-########################################
-## Imports part of FlatCAM ##
-########################################
-from FlatCAMObj import *
-from FlatCAMWorker import Worker
-
-
-########################################
-## App ##
-########################################
-class App:
- """
- The main application class. The constructor starts the GUI.
- """
-
- version_url = "http://caram.cl/flatcam/VERSION"
-
- def __init__(self):
- """
- Starts the application. Takes no parameters.
-
- :return: app
- :rtype: App
- """
-
- if speedups.available:
- speedups.enable()
-
- # Needed to interact with the GUI from other threads.
- GObject.threads_init()
-
- # GLib.log_set_handler()
-
- #### GUI ####
- # Glade init
- self.gladefile = "FlatCAM.ui"
- self.builder = Gtk.Builder()
- self.builder.add_from_file(self.gladefile)
-
- # References to UI widgets
- self.window = self.builder.get_object("window1")
- self.position_label = self.builder.get_object("label3")
- self.grid = self.builder.get_object("grid1")
- self.notebook = self.builder.get_object("notebook1")
- self.info_label = self.builder.get_object("label_status")
- self.progress_bar = self.builder.get_object("progressbar")
- self.progress_bar.set_show_text(True)
- self.units_label = self.builder.get_object("label_units")
- self.toolbar = self.builder.get_object("toolbar_main")
-
- # White (transparent) background on the "Options" tab.
- self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
- Gdk.RGBA(1, 1, 1, 1))
- # Combo box to choose between project and application options.
- self.combo_options = self.builder.get_object("combo_options")
- self.combo_options.set_active(1)
-
- #self.setup_project_list() # The "Project" tab
- self.setup_component_editor() # The "Selected" tab
-
- ## Setup the toolbar. Adds buttons.
- self.setup_toolbar()
-
- #### Event handling ####
- self.builder.connect_signals(self)
-
- #### Make plot area ####
- self.plotcanvas = PlotCanvas(self.grid)
- self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
- self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
- self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
-
- self.setup_tooltips()
-
- # TODO: Hardcoded list
- for kind in ['gerber', 'excellon', 'geometry', 'cncjob']:
- entry_name = self.builder.get_object("entry_text_" + kind + "_name")
- entry_name.connect("activate", self.on_activate_name)
-
- #### DATA ####
- self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- self.setup_obj_classes()
- self.mouse = None # Mouse coordinates over plot
- self.recent = []
- self.collection = ObjectCollection()
- self.builder.get_object("box_project").pack_start(self.collection.view, False, False, 1)
- # TODO: Do this different
- self.collection.view.connect("row_activated", self.on_row_activated)
-
- # Used to inhibit the on_options_update callback when
- # the options are being changed by the program and not the user.
- self.options_update_ignore = False
-
- self.toggle_units_ignore = False
-
- self.defaults = {
- "units": "in"
- } # Application defaults
-
- ## Current Project ##
- self.options = {} # Project options
- self.project_filename = None
-
- self.form_kinds = {
- "units": "radio"
- }
-
- self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
- "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
- self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
- "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
-
- # Options for each kind of FlatCAMObj.
- # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
- for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
- obj = FlatCAMClass("no_name")
- for option in obj.form_kinds:
- self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
- # if obj.form_kinds[option] == "radio":
- # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
- # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
-
- ## Event subscriptions ##
-
- ## Tools ##
- self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas)
- # Toolbar icon
- # TODO: Where should I put this? Tool should have a method to add to toolbar?
- meas_ico = Gtk.Image.new_from_file('share/measure32.png')
- measure = Gtk.ToolButton.new(meas_ico, "")
- measure.connect("clicked", self.measure.toggle_active)
- measure.set_tooltip_markup("Measure Tool: Enable/disable tool.\n" +
- "Click on point to set reference.\n" +
- "(Click on plot and hit m)")
- 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()
-
- self.worker = Worker()
- self.worker.daemon = True
- self.worker.start()
-
- #### Check for updates ####
- # Separate thread (Not worker)
- self.version = 4
- t1 = threading.Thread(target=self.versionCheck)
- t1.daemon = True
- t1.start()
-
- #### For debugging only ###
- def somethreadfunc(app_obj):
- print "Hello World!"
-
- t = threading.Thread(target=somethreadfunc, args=(self,))
- t.daemon = True
- t.start()
-
- ########################################
- ## START ##
- ########################################
- self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
- self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
- self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
- Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
- self.window.set_title("FlatCAM - Alpha 4 UNSTABLE")
- self.window.set_default_size(900, 600)
- self.window.show_all()
-
- def message_dialog(self, title, message, kind="info"):
- types = {"info": Gtk.MessageType.INFO,
- "warn": Gtk.MessageType.WARNING,
- "error": Gtk.MessageType.ERROR}
- dlg = Gtk.MessageDialog(self.window, 0, types[kind], Gtk.ButtonsType.OK, title)
- dlg.format_secondary_text(message)
-
- def lifecycle():
- dlg.run()
- dlg.destroy()
-
- GLib.idle_add(lifecycle)
-
- def question_dialog(self, title, message):
- label = Gtk.Label(message)
- dialog = Gtk.Dialog(title, self.window, 0,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK))
- dialog.set_default_size(150, 100)
- dialog.set_modal(True)
- box = dialog.get_content_area()
- box.set_border_width(10)
- box.add(label)
- dialog.show_all()
- response = dialog.run()
- dialog.destroy()
- return response
-
- def setup_toolbar(self):
-
- # Zoom fit
- zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
- zoom_fit = Gtk.ToolButton.new(zf_ico, "")
- zoom_fit.connect("clicked", self.on_zoom_fit)
- zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit 1)")
- self.toolbar.insert(zoom_fit, -1)
-
- # Zoom out
- zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
- zoom_out = Gtk.ToolButton.new(zo_ico, "")
- zoom_out.connect("clicked", self.on_zoom_out)
- zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit 2)")
- self.toolbar.insert(zoom_out, -1)
-
- # Zoom in
- zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
- zoom_in = Gtk.ToolButton.new(zi_ico, "")
- zoom_in.connect("clicked", self.on_zoom_in)
- zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit 3)")
- self.toolbar.insert(zoom_in, -1)
-
- # Clear plot
- cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
- clear_plot = Gtk.ToolButton.new(cp_ico, "")
- clear_plot.connect("clicked", self.on_clear_plots)
- clear_plot.set_tooltip_markup("Clear Plot")
- self.toolbar.insert(clear_plot, -1)
-
- # Replot
- rp_ico = Gtk.Image.new_from_file('share/replot32.png')
- replot = Gtk.ToolButton.new(rp_ico, "")
- replot.connect("clicked", self.on_toolbar_replot)
- replot.set_tooltip_markup("Re-plot all")
- self.toolbar.insert(replot, -1)
-
- # Delete item
- del_ico = Gtk.Image.new_from_file('share/delete32.png')
- delete = Gtk.ToolButton.new(del_ico, "")
- delete.connect("clicked", self.on_delete)
- delete.set_tooltip_markup("Delete selected\nobject.")
- self.toolbar.insert(delete, -1)
-
- def setup_obj_classes(self):
- """
- Sets up application specifics on the FlatCAMObj class.
-
- :return: None
- """
- FlatCAMObj.app = self
-
- def setup_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 setup_recent_items(self):
-
- # TODO: Move this to constructor
- icons = {
- "gerber": "share/flatcam_icon16.png",
- "excellon": "share/drill16.png",
- "cncjob": "share/cnc16.png",
- "project": "share/project16.png"
- }
-
- openers = {
- 'gerber': self.open_gerber,
- 'excellon': self.open_excellon,
- 'cncjob': self.open_gcode,
- 'project': self.open_project
- }
-
- # Closure needed to create callbacks in a loop.
- # Otherwise late binding occurs.
- def make_callback(func, fname):
- def opener(*args):
- self.worker.add_task(func, [fname])
- return opener
-
- try:
- f = open('recent.json')
- except:
- print "ERROR: Failed to load recent item list."
- self.info("Failed to load recent item list.")
- return
-
- try:
- self.recent = json.load(f)
- except:
- print "ERROR: Failed to parse recent item list."
- self.info("Failed to parse recent item list.")
- f.close()
- return
- f.close()
-
- recent_menu = Gtk.Menu()
- for recent in self.recent:
- filename = recent['filename'].split('/')[-1].split('\\')[-1]
- item = Gtk.ImageMenuItem.new_with_label(filename)
- im = Gtk.Image.new_from_file(icons[recent["kind"]])
- item.set_image(im)
-
- o = make_callback(openers[recent["kind"]], recent['filename'])
-
- item.connect('activate', o)
- recent_menu.append(item)
-
- self.builder.get_object('open_recent').set_submenu(recent_menu)
- recent_menu.show_all()
-
- def info(self, text):
- """
- Show text on the status bar. This method is thread safe.
-
- :param text: Text to display.
- :type text: str
- :return: None
- """
- GLib.idle_add(lambda: self.info_label.set_text(text))
-
- def get_radio_value(self, radio_set):
- """
- Returns the radio_set[key] of the radiobutton
- whose name is key is active.
-
- :param radio_set: A dictionary containing widget_name: value pairs.
- :type radio_set: dict
- :return: radio_set[key]
- """
-
- for name in radio_set:
- if self.builder.get_object(name).get_active():
- return radio_set[name]
-
- def plot_all(self):
- """
- Re-generates all plots from all objects.
-
- :return: None
- """
- self.plotcanvas.clear()
- self.set_progress_bar(0.1, "Re-plotting...")
-
- def worker_task(app_obj):
- percentage = 0.1
- try:
- delta = 0.9 / len(self.collection.get_list())
- except ZeroDivisionError:
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
- return
- for obj in self.collection.get_list():
- obj.plot()
- percentage += delta
- GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
-
- GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
- GLib.idle_add(lambda: self.on_zoom_fit(None))
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
-
- # Send to worker
- self.worker.add_task(worker_task, [self])
-
- def get_eval(self, widget_name):
- """
- Runs eval() on the on the text entry of name 'widget_name'
- and returns the results.
-
- :param widget_name: Name of Gtk.Entry
- :type widget_name: str
- :return: Depends on contents of the entry text.
- """
-
- value = self.builder.get_object(widget_name).get_text()
- if value == "":
- value = "None"
- try:
- evald = eval(value)
- return evald
- except:
- self.info("Could not evaluate: " + value)
- return None
-
- def new_object(self, kind, name, initialize):
- """
- Creates a new specalized FlatCAMObj and attaches it to the application,
- this is, updates the GUI accordingly, any other records and plots it.
- This method is thread-safe.
-
- :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
- """
-
- print "new_object()"
-
- ### Check for existing name
- if name in self.collection.get_names():
- ## Create a new name
- # Ends with number?
- match = re.search(r'(.*[^\d])?(\d+)$', name)
- if match: # Yes: Increment the number!
- base = match.group(1) or ''
- num = int(match.group(2))
- name = base + str(num + 1)
- else: # No: add a number!
- name += "_1"
-
- # Create object
- classdict = {
- "gerber": FlatCAMGerber,
- "excellon": FlatCAMExcellon,
- "cncjob": FlatCAMCNCjob,
- "geometry": FlatCAMGeometry
- }
- obj = classdict[kind](name)
- obj.units = self.options["units"] # TODO: The constructor should look at defaults.
-
- # Set default options from self.options
- for option in self.options:
- if option.find(kind + "_") == 0:
- oname = option[len(kind)+1:]
- obj.options[oname] = self.options[option]
-
- # Initialize as per user request
- # User must take care to implement initialize
- # in a thread-safe way as is is likely that we
- # have been invoked in a separate thread.
- initialize(obj, self)
-
- # Check units and convert if necessary
- if self.options["units"].upper() != obj.units.upper():
- GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
- obj.convert_units(self.options["units"])
-
- # Add to our records
- self.collection.append(obj, active=True)
-
- # Show object details now.
- GLib.idle_add(lambda: self.notebook.set_current_page(1))
-
- # Plot
- # TODO: (Thread-safe?)
- obj.plot()
-
- GLib.idle_add(lambda: self.on_zoom_fit(None))
- #self.on_zoom_fit(None)
-
- return obj
-
- def set_progress_bar(self, percentage, text=""):
- """
- Sets the application's progress bar to a given frac_digits and text.
-
- :param percentage: The frac_digits (0.0-1.0) of the progress.
- :type percentage: float
- :param text: Text to display on the progress bar.
- :type text: str
- :return: None
- """
- self.progress_bar.set_text(text)
- self.progress_bar.set_fraction(percentage)
- return False
-
- def load_defaults(self):
- """
- Loads the aplication's default settings from defaults.json into
- ``self.defaults``.
-
- :return: None
- """
- try:
- f = open("defaults.json")
- options = f.read()
- f.close()
- except:
- self.info("ERROR: Could not load defaults file.")
- return
-
- try:
- defaults = json.loads(options)
- except:
- e = sys.exc_info()[0]
- print e
- self.info("ERROR: Failed to parse defaults file.")
- return
- self.defaults.update(defaults)
-
- def read_form(self):
- """
- Reads the options form into self.defaults/self.options.
-
- :return: None
- :rtype: None
- """
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
- for option in options_set:
- self.read_form_item(option, options_set)
-
- def read_form_item(self, name, dest):
- """
- Reads the value of a form item in the defaults/options form and
- saves it to the corresponding dictionary.
-
- :param name: Name of the form item. A key in ``self.defaults`` or
- ``self.options``.
- :type name: str
- :param dest: Dictionary to which to save the value.
- :type dest: dict
- :return: None
- """
- fkind = self.form_kinds[name]
- fname = fkind + "_" + "app" + "_" + name
-
- if fkind == 'entry_text':
- dest[name] = self.builder.get_object(fname).get_text()
- return
- if fkind == 'entry_eval':
- dest[name] = self.get_eval(fname)
- return
- if fkind == 'cb':
- dest[name] = self.builder.get_object(fname).get_active()
- return
- if fkind == 'radio':
- dest[name] = self.get_radio_value(self.radios[name])
- return
- print "Unknown kind of form item:", fkind
-
- def options2form(self):
- """
- Sets the 'Project Options' or 'Application Defaults' form with values from
- ``self.options`` or ``self.defaults``.
-
- :return: None
- :rtype: None
- """
-
- # Set the on-change callback to do nothing while we do the changes.
- self.options_update_ignore = True
- self.toggle_units_ignore = True
-
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
- for option in options_set:
- self.set_form_item(option, options_set[option])
-
- self.options_update_ignore = False
- self.toggle_units_ignore = False
-
- def set_form_item(self, name, value):
- """
- Sets a form item 'name' in the GUI with the given 'value'. The syntax of
- form names in the GUI is _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 save_project(self, filename):
- """
- Saves the current project to the specified file.
-
- :param filename: Name of the file in which to save.
- :type filename: str
- :return: None
- """
-
- # Capture the latest changes
- try:
- self.collection.get_active().read_form()
- except:
- pass
-
- # Serialize the whole project
- d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
- "options": self.options}
-
- try:
- f = open(filename, 'w')
- except:
- print "ERROR: Failed to open file for saving:", filename
- return
-
- try:
- json.dump(d, f, default=to_dict)
- except:
- print "ERROR: File open but failed to write:", filename
- f.close()
- return
-
- f.close()
-
- def open_project(self, filename):
- """
- Loads a project from the specified file.
-
- :param filename: Name of the file from which to load.
- :type filename: str
- :return: None
- """
-
- try:
- f = open(filename, 'r')
- except:
- self.info("ERROR: Failed to open project file: %s" % filename)
- return
-
- try:
- d = json.load(f, object_hook=dict2obj)
- except:
- self.info("ERROR: Failed to parse project file: %s" % filename)
- f.close()
- return
-
- self.register_recent("project", filename)
-
- # Clear the current project
- self.on_file_new(None)
-
- # Project options
- self.options.update(d['options'])
- self.project_filename = filename
- GLib.idle_add(lambda: self.units_label.set_text(self.options["units"]))
-
- # Re create objects
- for obj in d['objs']:
- def obj_init(obj_inst, app_inst):
- obj_inst.from_dict(obj)
- self.new_object(obj['kind'], obj['options']['name'], obj_init)
-
- self.info("Project loaded from: " + filename)
-
- def populate_objects_combo(self, combo):
- """
- Populates a Gtk.Comboboxtext with the list of the object in the project.
-
- :param combo: Name or instance of the comboboxtext.
- :type combo: str or Gtk.ComboBoxText
- :return: None
- """
- print "Populating combo!"
- if type(combo) == str:
- combo = self.builder.get_object(combo)
-
- combo.remove_all()
- for name in self.collection.get_names():
- combo.append_text(name)
-
- def versionCheck(self, *args):
- """
- Checks for the latest version of the program. Alerts the
- user if theirs is outdated. This method is meant to be run
- in a saeparate thread.
-
- :return: None
- """
-
- try:
- f = urllib.urlopen(App.version_url)
- except:
- GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
- return
-
- try:
- data = json.load(f)
- except:
- GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
- f.close()
- return
-
- f.close()
-
- if self.version >= data["version"]:
- GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
- return
-
- label = Gtk.Label("There is a newer version of FlatCAM\n" +
- "available for download:\n\n" +
- data["name"] + "\n\n" + data["message"])
- dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK))
- dialog.set_default_size(150, 100)
- dialog.set_modal(True)
- box = dialog.get_content_area()
- box.set_border_width(10)
- box.add(label)
-
- def do_dialog():
- dialog.show_all()
- response = dialog.run()
- dialog.destroy()
-
- GLib.idle_add(lambda: do_dialog())
-
- return
-
- def setup_tooltips(self):
- tooltips = {
- "cb_gerber_plot": "Plot this object on the main window.",
- "cb_gerber_mergepolys": "Show overlapping polygons as single.",
- "cb_gerber_solid": "Paint inside polygons.",
- "cb_gerber_multicolored": "Draw polygons with different colors."
- }
-
- for widget in tooltips:
- self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
-
- def do_nothing(self, param):
- return
-
- def disable_plots(self, except_current=False):
- """
- Disables all plots with exception of the current object if specified.
-
- :param except_current: Wether to skip the current object.
- :rtype except_current: boolean
- :return: None
- """
- # TODO: This method is very similar to replot_all. Try to merge.
-
- self.set_progress_bar(0.1, "Re-plotting...")
-
- def worker_task(app_obj):
- percentage = 0.1
- try:
- delta = 0.9 / len(self.collection.get_list())
- except ZeroDivisionError:
- 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()
- percentage += delta
- GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
-
- GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
-
- # Send to worker
- self.worker.add_task(worker_task, [self])
-
- def enable_all_plots(self, *args):
- self.plotcanvas.clear()
- self.set_progress_bar(0.1, "Re-plotting...")
-
- def worker_task(app_obj):
- percentage = 0.1
- try:
- delta = 0.9 / len(self.collection.get_list())
- except ZeroDivisionError:
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
- return
- for obj in self.collection.get_list():
- obj.options['plot'] = True
- obj.plot()
- percentage += delta
- GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
-
- GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
-
- # Send to worker
- self.worker.add_task(worker_task, [self])
-
- def register_recent(self, kind, filename):
- record = {'kind': kind, 'filename': filename}
-
- if record in self.recent:
- return
-
- self.recent.insert(0, record)
-
- if len(self.recent) > 10: # Limit reached
- self.recent.pop()
-
- try:
- f = open('recent.json', 'w')
- except:
- print "ERROR: Failed to open recent items file for writing."
- self.info('Failed to open recent files file for writing.')
- return
-
- try:
- json.dump(self.recent, f)
- except:
- print "ERROR: Failed to write to recent items file."
- self.info('Failed to write to recent items file.')
- f.close()
-
- f.close()
-
- def open_gerber(self, filename):
- """
- Opens a Gerber file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param filename: Gerber file filename
- :type filename: str
- :return: None
- """
- GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Gerber ..."))
-
- # How the object should be initialized
- def obj_init(gerber_obj, app_obj):
- assert isinstance(gerber_obj, FlatCAMGerber)
-
- # Opening the file happens here
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
- gerber_obj.parse_file(filename)
-
- # Further parsing
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
- #gerber_obj.create_geometry()
- #gerber_obj.solid_geometry = gerber_obj.otf_geometry
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
-
- # Object name
- name = filename.split('/')[-1].split('\\')[-1]
-
- self.new_object("gerber", name, obj_init)
-
- # New object creation and file processing
- # try:
- # self.new_object("gerber", name, obj_init)
- # except:
- # e = sys.exc_info()
- # print "ERROR:", e[0]
- # traceback.print_exc()
- # self.message_dialog("Failed to create Gerber Object",
- # "Attempting to create a FlatCAM Gerber Object from " +
- # "Gerber file failed during processing:\n" +
- # str(e[0]) + " " + str(e[1]), kind="error")
- # GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
- # self.collection.delete_active()
- # return
-
- # Register recent file
- self.register_recent("gerber", filename)
-
- # GUI feedback
- self.info("Opened: " + filename)
- GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
-
- def open_excellon(self, filename):
- """
- Opens an Excellon file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param filename: Excellon file filename
- :type filename: str
- :return: None
- """
- GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Excellon ..."))
-
- # How the object should be initialized
- def obj_init(excellon_obj, app_obj):
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
- excellon_obj.parse_file(filename)
- excellon_obj.create_geometry()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
-
- # Object name
- name = filename.split('/')[-1].split('\\')[-1]
-
- # New object creation and file processing
- try:
- self.new_object("excellon", name, obj_init)
- except:
- e = sys.exc_info()
- print "ERROR:", e[0]
- self.message_dialog("Failed to create Excellon Object",
- "Attempting to create a FlatCAM Excellon Object from " +
- "Excellon file failed during processing:\n" +
- str(e[0]) + " " + str(e[1]), kind="error")
- GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
- self.collection.delete_active()
- return
-
- # Register recent file
- self.register_recent("excellon", filename)
-
- # GUI feedback
- self.info("Opened: " + filename)
- GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
-
- def open_gcode(self, filename):
- """
- Opens a G-gcode file, parses it and creates a new object for
- it in the program. Thread-safe.
-
- :param filename: G-code file filename
- :type filename: str
- :return: None
- """
-
- # How the object should be initialized
- def obj_init(job_obj, app_obj_):
- """
-
- :type app_obj_: App
- """
- assert isinstance(app_obj_, App)
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
-
- f = open(filename)
- gcode = f.read()
- f.close()
-
- job_obj.gcode = gcode
-
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
- job_obj.gcode_parse()
-
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
- job_obj.create_geometry()
-
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
-
- # Object name
- name = filename.split('/')[-1].split('\\')[-1]
-
- # New object creation and file processing
- try:
- self.new_object("cncjob", name, obj_init)
- except:
- e = sys.exc_info()
- print "ERROR:", e[0]
- self.message_dialog("Failed to create CNCJob Object",
- "Attempting to create a FlatCAM CNCJob Object from " +
- "G-Code file failed during processing:\n" +
- str(e[0]) + " " + str(e[1]), kind="error")
- GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
- self.collection.delete_active()
- return
-
- # Register recent file
- self.register_recent("cncjob", filename)
-
- # GUI feedback
- self.info("Opened: " + filename)
- GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
-
- ########################################
- ## EVENT HANDLERS ##
- ########################################
- def on_debug_printlist(self, *args):
- self.collection.print_list()
-
- def on_disable_all_plots(self, widget):
- self.disable_plots()
-
- 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.
-
- :param widget: Ignored.
- :return: None
- """
-
- about = self.builder.get_object("aboutdialog")
- response = about.run()
- about.hide()
-
- def on_create_mirror(self, widget):
- """
- Creates a mirror image of an object to be used as a bottom layer.
-
- :param widget: Ignored.
- :return: None
- """
- # TODO: Move (some of) this to camlib!
-
- # Object to mirror
- obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
- fcobj = self.collection.get_by_name(obj_name)
-
- # For now, lets limit to Gerbers and Excellons.
- # assert isinstance(gerb, FlatCAMGerber)
- if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
- self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
- return
-
- # Mirror axis "X" or "Y
- axis = self.get_radio_value({"rb_mirror_x": "X",
- "rb_mirror_y": "Y"})
- mode = self.get_radio_value({"rb_mirror_box": "box",
- "rb_mirror_point": "point"})
- if mode == "point": # A single point defines the mirror axis
- # TODO: Error handling
- px, py = eval(self.point_entry.get_text())
- else: # The axis is the line dividing the box in the middle
- name = self.box_combo.get_active_text()
- bb_obj = self.collection.get_by_name(name)
- xmin, ymin, xmax, ymax = bb_obj.bounds()
- px = 0.5*(xmin+xmax)
- py = 0.5*(ymin+ymax)
-
- fcobj.mirror(axis, [px, py])
- fcobj.plot()
-
- def on_create_aligndrill(self, widget):
- """
- Creates alignment holes Excellon object. Creates mirror duplicates
- of the specified holes around the specified axis.
-
- :param widget: Ignored.
- :return: None
- """
-
- # Mirror axis. Same as in on_create_mirror.
- axis = self.get_radio_value({"rb_mirror_x": "X",
- "rb_mirror_y": "Y"})
- # TODO: Error handling
- mode = self.get_radio_value({"rb_mirror_box": "box",
- "rb_mirror_point": "point"})
- if mode == "point":
- px, py = eval(self.point_entry.get_text())
- else:
- name = self.box_combo.get_active_text()
- bb_obj = self.collection.get_by_name(name)
- xmin, ymin, xmax, ymax = bb_obj.bounds()
- px = 0.5*(xmin+xmax)
- py = 0.5*(ymin+ymax)
- xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
-
- # Tools
- dia = self.get_eval("entry_dblsided_alignholediam")
- tools = {"1": {"C": dia}}
-
- # Parse hole list
- # TODO: Better parsing
- holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
- holes = eval("[" + holes + "]")
- drills = []
- for hole in holes:
- point = Point(hole)
- point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
- drills.append({"point": point, "tool": "1"})
- drills.append({"point": point_mirror, "tool": "1"})
-
- def obj_init(obj_inst, app_inst):
- obj_inst.tools = tools
- obj_inst.drills = drills
- obj_inst.create_geometry()
-
- self.new_object("excellon", "Alignment Drills", obj_init)
-
- def on_toggle_pointbox(self, widget):
- """
- Callback for radio selection change between point and box in the
- Double-sided PCB tool. Updates the UI accordingly.
-
- :param widget: Ignored.
- :return: None
- """
-
- # Where the entry or combo go
- box = self.builder.get_object("box_pointbox")
-
- # Clear contents
- children = box.get_children()
- for child in children:
- box.remove(child)
-
- choice = self.get_radio_value({"rb_mirror_point": "point",
- "rb_mirror_box": "box"})
-
- if choice == "point":
- self.point_entry = Gtk.Entry()
- self.builder.get_object("box_pointbox").pack_start(self.point_entry,
- False, False, 1)
- self.point_entry.show()
- else:
- self.box_combo = Gtk.ComboBoxText()
- self.builder.get_object("box_pointbox").pack_start(self.box_combo,
- False, False, 1)
- self.populate_objects_combo(self.box_combo)
- self.box_combo.show()
-
-
- def on_tools_doublesided(self, param):
- """
- Callback for menu item Tools->Double Sided PCB Tool. Launches the
- tool placing its UI in the "Tool" tab in the notebook.
-
- :param param: Ignored.
- :return: None
- """
-
- # Were are we drawing the UI
- box_tool = self.builder.get_object("box_tool")
-
- # Remove anything else in the box
- box_children = box_tool.get_children()
- for child in box_children:
- box_tool.remove(child)
-
- # Get the UI
- osw = self.builder.get_object("offscreenwindow_dblsided")
- sw = self.builder.get_object("sw_dblsided")
- osw.remove(sw)
- vp = self.builder.get_object("vp_dblsided")
- vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
-
- # Put in the UI
- box_tool.pack_start(sw, True, True, 0)
-
- # INITIALIZATION
- # Populate combo box
- self.populate_objects_combo("comboboxtext_bottomlayer")
-
- # Point entry
- self.point_entry = Gtk.Entry()
- box = self.builder.get_object("box_pointbox")
- for child in box.get_children():
- box.remove(child)
- box.pack_start(self.point_entry, False, False, 1)
-
- # Show the "Tool" tab
- self.notebook.set_current_page(3)
- sw.show_all()
-
- def on_toggle_units(self, widget):
- """
- Callback for the Units radio-button change in the Options tab.
- Changes the application's default units or the current project's units.
- If changing the project's units, the change propagates to all of
- the objects in the project.
-
- :param widget: Ignored.
- :return: None
- """
-
- if self.toggle_units_ignore:
- return
-
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
-
- # Options to scale
- dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
- 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
- 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
- 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
- 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
- 'geometry_paintmargin']
-
- def scale_options(factor):
- for dim in dimensions:
- options_set[dim] *= factor
-
- # The scaling factor depending on choice of units.
- factor = 1/25.4
- if self.builder.get_object('rb_mm').get_active():
- factor = 25.4
-
- # App units. Convert without warning.
- if combo_sel == 1:
- self.read_form()
- scale_options(factor)
- self.options2form()
- return
-
- # Changing project units. Warn user.
- label = Gtk.Label("Changing the units of the project causes all geometrical \n" + \
- "properties of all objects to be scaled accordingly. Continue?")
- dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK))
- dialog.set_default_size(150, 100)
- dialog.set_modal(True)
- box = dialog.get_content_area()
- box.set_border_width(10)
- box.add(label)
- dialog.show_all()
- response = dialog.run()
- dialog.destroy()
-
- if response == Gtk.ResponseType.OK:
- #print "Converting units..."
- #print "Converting options..."
- self.read_form()
- scale_options(factor)
- self.options2form()
- for obj in self.collection.get_list():
- units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
- obj.convert_units(units)
- current = self.collection.get_active()
- if current is not None:
- current.to_form()
- self.plot_all()
- else:
- # Undo toggling
- self.toggle_units_ignore = True
- if self.builder.get_object('rb_mm').get_active():
- self.builder.get_object('rb_inch').set_active(True)
- else:
- self.builder.get_object('rb_mm').set_active(True)
- self.toggle_units_ignore = False
-
- self.read_form()
- self.info("Converted units to %s" % self.options["units"])
- self.units_label.set_text("[" + self.options["units"] + "]")
-
- def on_file_openproject(self, param):
- """
- Callback for menu item File->Open Project. Opens a file chooser and calls
- ``self.open_project()`` after successful selection of a filename.
-
- :param param: Ignored.
- :return: None
- """
-
- def on_success(app_obj, filename):
- app_obj.open_project(filename)
-
- self.file_chooser_action(on_success)
-
- def on_file_saveproject(self, param):
- """
- Callback for menu item File->Save Project. Saves the project to
- ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
- if set to None. The project is saved by calling ``self.save_project()``.
-
- :param param: Ignored.
- :return: None
- """
-
- if self.project_filename is None:
- self.on_file_saveprojectas(None)
- else:
- self.save_project(self.project_filename)
- self.register_recent("project", self.project_filename)
- self.info("Project saved to: " + self.project_filename)
-
- def on_file_saveprojectas(self, param):
- """
- Callback for menu item File->Save Project As... Opens a file
- chooser and saves the project to the given file via
- ``self.save_project()``.
-
- :param param: Ignored.
- :return: None
- """
-
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
-
- try:
- f = open(filename, 'r')
- f.close()
- exists = True
- except IOError:
- exists = False
-
- msg = "File exists. Overwrite?"
- if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
- return
-
- app_obj.save_project(filename)
- self.project_filename = filename
- self.register_recent("project", filename)
- app_obj.info("Project saved to: " + filename)
-
- self.file_chooser_save_action(on_success)
-
- def on_file_saveprojectcopy(self, param):
- """
- Callback for menu item File->Save Project Copy... Opens a file
- chooser and saves the project to the given file via
- ``self.save_project``. It does not update ``self.project_filename`` so
- subsequent save requests are done on the previous known filename.
-
- :param param: Ignore.
- :return: None
- """
-
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
-
- try:
- f = open(filename, 'r')
- f.close()
- exists = True
- except IOError:
- exists = False
-
- msg = "File exists. Overwrite?"
- if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
- return
-
- app_obj.save_project(filename)
- self.register_recent("project", filename)
- app_obj.info("Project copy saved to: " + filename)
-
- self.file_chooser_save_action(on_success)
-
- def on_options_app2project(self, param):
- """
- Callback for Options->Transfer Options->App=>Project. Copies options
- from application defaults to project defaults.
-
- :param param: Ignored.
- :return: None
- """
-
- self.options.update(self.defaults)
- self.options2form() # Update UI
-
- def on_options_project2app(self, param):
- """
- Callback for Options->Transfer Options->Project=>App. Copies options
- from project defaults to application defaults.
-
- :param param: Ignored.
- :return: None
- """
-
- self.defaults.update(self.options)
- self.options2form() # Update UI
-
- def on_options_project2object(self, param):
- """
- Callback for Options->Transfer Options->Project=>Object. Copies options
- from project defaults to the currently selected object.
-
- :param param: Ignored.
- :return: None
- """
-
- obj = self.collection.get_active()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- for option in self.options:
- if option.find(obj.kind + "_") == 0:
- oname = option[len(obj.kind)+1:]
- obj.options[oname] = self.options[option]
- obj.to_form() # Update UI
-
- def on_options_object2project(self, param):
- """
- Callback for Options->Transfer Options->Object=>Project. Copies options
- from the currently selected object to project defaults.
-
- :param param: Ignored.
- :return: None
- """
-
- obj = self.collection.get_active()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- obj.read_form()
- for option in obj.options:
- if option in ['name']: # TODO: Handle this better...
- continue
- self.options[obj.kind + "_" + option] = obj.options[option]
- self.options2form() # Update UI
-
- def on_options_object2app(self, param):
- """
- Callback for Options->Transfer Options->Object=>App. Copies options
- from the currently selected object to application defaults.
-
- :param param: Ignored.
- :return: None
- """
- obj = self.collection.get_active()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- obj.read_form()
- for option in obj.options:
- if option in ['name']: # TODO: Handle this better...
- continue
- self.defaults[obj.kind + "_" + option] = obj.options[option]
- self.options2form() # Update UI
-
- def on_options_app2object(self, param):
- """
- Callback for Options->Transfer Options->App=>Object. Copies options
- from application defaults to the currently selected object.
-
- :param param: Ignored.
- :return: None
- """
-
- obj = self.collection.get_active()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- for option in self.defaults:
- if option.find(obj.kind + "_") == 0:
- oname = option[len(obj.kind)+1:]
- obj.options[oname] = self.defaults[option]
- obj.to_form() # Update UI
-
- def on_file_savedefaults(self, param):
- """
- Callback for menu item File->Save Defaults. Saves application default options
- ``self.defaults`` to defaults.json.
-
- :param param: Ignored.
- :return: None
- """
-
- # Read options from file
- try:
- f = open("defaults.json")
- options = f.read()
- f.close()
- except:
- self.info("ERROR: Could not load defaults file.")
- return
-
- try:
- defaults = json.loads(options)
- except:
- e = sys.exc_info()[0]
- print e
- self.info("ERROR: Failed to parse defaults file.")
- return
-
- # Update options
- assert isinstance(defaults, dict)
- defaults.update(self.defaults)
-
- # Save update options
- try:
- f = open("defaults.json", "w")
- json.dump(defaults, f)
- f.close()
- except:
- self.info("ERROR: Failed to write defaults to file.")
- return
-
- self.info("Defaults saved.")
-
- def on_options_combo_change(self, widget):
- """
- Called when the combo box to choose between application defaults and
- project option changes value. The corresponding variables are
- copied to the UI.
-
- :param widget: The widget from which this was called. Ignore.
- :return: None
- """
-
- #combo_sel = self.combo_options.get_active()
- #print "Options --> ", combo_sel
- self.options2form()
-
- def on_options_update(self, widget):
- """
- Called whenever a value in the options/defaults form changes.
- All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
- which may be necessary when updating the UI from code and not by the user.
-
- :param widget: The widget from which this was called. Ignore.
- :return: None
- """
-
- if self.options_update_ignore:
- return
- self.read_form()
-
- def on_scale_object(self, widget):
- """
- Callback for request to change an objects geometry scale. The object
- is re-scaled and replotted.
-
- :param widget: Ignored.
- :return: None
- """
-
- obj = self.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
- as to use the whole canvas.
-
- :param widget: Ignored.
- :param event: Ignored.
- :return: None
- """
-
- self.plotcanvas.auto_adjust_axes()
-
- def on_row_activated(self, widget, path, col):
- """
- Callback for selection activation (Enter or double-click) on the Project list.
- Switches the notebook page to the object properties form. Calls
- ``self.notebook.set_current_page(1)``.
-
- :param widget: Ignored.
- :param path: Ignored.
- :param col: Ignored.
- :return: None
- """
- self.notebook.set_current_page(1)
-
- def on_generate_gerber_bounding_box(self, widget):
- """
- Callback for request from the Gerber form to generate a bounding box for the
- geometry in the object. Creates a FlatCAMGeometry with the bounding box.
- The box can have rounded corners if specified in the form.
-
- :param widget: Ignored.
- :return: None
- """
- # TODO: Use Gerber.get_bounding_box(...)
- gerber = self.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.
- Re-plots the current object only.
-
- :param widget: The widget from which this was called. Ignored.
- :return: None
- """
-
- obj = self.collection.get_active()
- obj.read_form()
-
- self.set_progress_bar(0.5, "Plotting...")
-
- def thread_func(app_obj):
- assert isinstance(app_obj, App)
- obj.plot()
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
-
- # 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
- selecting tools.
-
- :param widget: The widget from which this was called.
- :return: None
- """
- excellon = self.collection.get_active()
- assert isinstance(excellon, FlatCAMExcellon)
- excellon.show_tool_chooser()
-
- def on_entry_eval_activate(self, widget):
- """
- Called when an entry is activated (eg. by hitting enter) if
- set to do so. Its text is eval()'d and set to the returned value.
- The current object is updated.
-
- :param widget:
- :return:
- """
- self.on_eval_update(widget)
- obj = self.collection.get_active()
- 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
- eval() on its contents and puting it back as a
- string.
-
- :param widget: The widget from which this was called.
- :return: None
- """
- # TODO: error handling here
- widget.set_text(str(eval(widget.get_text())))
-
- def on_generate_isolation(self, widget):
- """
- Callback for button on Gerber form to create isolation routing geometry.
-
- :param widget: The widget from which this was called.
- :return: None
- """
-
- gerb = self.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)
-
- def on_delete(self, widget):
- """
- Delete the currently selected FlatCAMObj.
-
- :param widget: The widget from which this was called. Ignored.
- :return: None
- """
-
- # Keep this for later
- name = copy.copy(self.collection.get_active().options["name"])
-
- # Remove plot
- self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
- self.plotcanvas.auto_adjust_axes()
-
- # Clear form
- self.setup_component_editor()
-
- # Remove from dictionary
- self.collection.delete_active()
-
- self.info("Object deleted: %s" % name)
-
- def on_toolbar_replot(self, widget):
- """
- Callback for toolbar button. Re-plots all objects.
-
- :param widget: The widget from which this was called.
- :return: None
- """
-
- self.collection.get_active().read_form()
-
- self.plot_all()
-
- def on_clear_plots(self, widget):
- """
- Callback for toolbar button. Clears all plots.
-
- :param widget: The widget from which this was called.
- :return: None
- """
- self.plotcanvas.clear()
-
- def on_activate_name(self, entry):
- """
- Hitting 'Enter' after changing the name of an item
- updates the item dictionary and re-builds the item list.
-
- :param entry: The widget from which this was called.
- :return: None
- """
-
- 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
- startup state. This method is thread-safe.
-
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
- # Remove everything from memory
-
- # GUI things
- def task():
- # Clear plot
- self.plotcanvas.clear()
-
- # Delete data
- self.collection.delete_all()
-
- # Clear object editor
- self.setup_component_editor()
-
- GLib.idle_add(task)
-
- # Clear project filename
- self.project_filename = None
-
- # Re-fresh project options
- self.on_options_app2project(None)
-
- def on_filequit(self, param):
- """
- Callback for menu item File->Quit. Closes the application.
-
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
-
- self.window.destroy()
- Gtk.main_quit()
-
- def on_closewindow(self, param):
- """
- Callback for closing the main window.
-
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
-
- self.window.destroy()
- Gtk.main_quit()
-
- def file_chooser_action(self, on_success):
- """
- Opens the file chooser and runs on_success on a separate thread
- upon completion of valid file choice.
-
- :param on_success: A function to run upon completion of a valid file
- selection. Takes 2 parameters: The app instance and the filename.
- Note that it is run on a separate thread, therefore it must take the
- appropriate precautions when accessing shared resources.
- :type on_success: func
- :return: None
- """
- dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
- Gtk.FileChooserAction.OPEN,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
- response = dialog.run()
- if response == Gtk.ResponseType.OK:
- filename = dialog.get_filename()
- dialog.destroy()
- # Send to worker.
- self.worker.add_task(on_success, [self, filename])
- elif response == Gtk.ResponseType.CANCEL:
- self.info("Open cancelled.")
- dialog.destroy()
-
- def file_chooser_save_action(self, on_success):
- """
- Opens the file chooser and runs on_success upon completion of valid file choice.
-
- :param on_success: A function to run upon selection of a filename. Takes 2
- parameters: The instance of the application (App) and the chosen filename. This
- gets run immediately in the same thread.
- :return: None
- """
- dialog = Gtk.FileChooserDialog("Save file", self.window,
- Gtk.FileChooserAction.SAVE,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
- dialog.set_current_name("Untitled")
- response = dialog.run()
- if response == Gtk.ResponseType.OK:
- filename = dialog.get_filename()
- dialog.destroy()
- on_success(self, filename)
- elif response == Gtk.ResponseType.CANCEL:
- self.info("Save cancelled.") # print("Cancel clicked")
- dialog.destroy()
-
- def on_fileopengerber(self, param):
- """
- Callback for menu item File->Open Gerber. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
- and updates the progress bar throughout the process.
-
- :param param: Ignore
- :return: None
- """
-
- self.file_chooser_action(lambda ao, filename: self.open_gerber(filename))
-
- def on_fileopenexcellon(self, param):
- """
- Callback for menu item File->Open Excellon. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
- and updates the progress bar throughout the process.
-
- :param param: Ignore
- :return: None
- """
-
- self.file_chooser_action(lambda ao, filename: self.open_excellon(filename))
-
- def on_fileopengcode(self, param):
- """
- Callback for menu item File->Open G-Code. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
- and updates the progress bar throughout the process.
-
- :param param: Ignore
- :return: None
- """
-
- self.file_chooser_action(lambda ao, filename: self.open_gcode(filename))
-
- def on_mouse_move_over_plot(self, event):
- """
- Callback for the mouse motion event over the plot. This event is generated
- by the Matplotlib backend and has been registered in ``self.__init__()``.
- For details, see: http://matplotlib.org/users/event_handling.html
-
- :param event: Contains information about the event.
- :return: None
- """
-
- try: # May fail in case mouse not within axes
- self.position_label.set_label("X: %.4f Y: %.4f" % (
- event.xdata, event.ydata))
- self.mouse = [event.xdata, event.ydata]
-
- # for subscriber in self.plot_mousemove_subscribers:
- # self.plot_mousemove_subscribers[subscriber](event)
-
- except:
- self.position_label.set_label("")
- self.mouse = None
-
- def on_click_over_plot(self, event):
- """
- Callback for the mouse click event over the plot. This event is generated
- by the Matplotlib backend and has been registered in ``self.__init__()``.
- For details, see: http://matplotlib.org/users/event_handling.html
-
- Default actions are:
-
- * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
-
- :param event: Contains information about the event, like which button
- was clicked, the pixel coordinates and the axes coordinates.
- :return: None
- """
-
- # So it can receive key presses
- self.plotcanvas.canvas.grab_focus()
-
- try:
- print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
- event.button, event.x, event.y, event.xdata, event.ydata)
-
- # TODO: This custom subscription mechanism is probably not necessary.
- # for subscriber in self.plot_click_subscribers:
- # self.plot_click_subscribers[subscriber](event)
-
- self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
-
- except Exception, e:
- print "Outside plot!"
-
- def on_zoom_in(self, event):
- """
- Callback for zoom-in request. This can be either from the corresponding
- toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
-
- :param event: Ignored.
- :return: None
- """
- self.plotcanvas.zoom(1.5)
- return
-
- def on_zoom_out(self, event):
- """
- Callback for zoom-out request. This can be either from the corresponding
- toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
-
- :param event: Ignored.
- :return: None
- """
- self.plotcanvas.zoom(1 / 1.5)
-
- def on_zoom_fit(self, event):
- """
- Callback for zoom-out request. This can be either from the corresponding
- toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
- with axes limits from the geometry bounds of all objects.
-
- :param event: Ignored.
- :return: None
- """
- xmin, ymin, xmax, ymax = self.collection.get_bounds()
- width = xmax - xmin
- height = ymax - ymin
- xmin -= 0.05 * width
- xmax += 0.05 * width
- ymin -= 0.05 * height
- ymax += 0.05 * height
- self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
-
- def on_key_over_plot(self, event):
- """
- Callback for the key pressed event when the canvas is focused. Keyboard
- shortcuts are handled here. So far, these are the shortcuts:
-
- ========== ============================================
- Key Action
- ========== ============================================
- '1' Zoom-fit. Fits the axes limits to the data.
- '2' Zoom-out.
- '3' Zoom-in.
- 'm' Toggle on-off the measuring tool.
- ========== ============================================
-
- :param event: Ignored.
- :return: None
- """
-
- if event.key == '1': # 1
- self.on_zoom_fit(None)
- return
-
- if event.key == '2': # 2
- self.plotcanvas.zoom(1 / 1.5, self.mouse)
- return
-
- if event.key == '3': # 3
- self.plotcanvas.zoom(1.5, self.mouse)
- return
-
- if event.key == 'm':
- if self.measure.toggle_active():
- self.info("Measuring tool ON")
- else:
- self.info("Measuring tool OFF")
- return
-
-
-class BaseDraw:
- def __init__(self, plotcanvas, name=None):
- """
-
- :param plotcanvas: The PlotCanvas where the drawing tool will operate.
- :type plotcanvas: PlotCanvas
- """
-
- self.plotcanvas = plotcanvas
-
- # Must have unique axes
- charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
- self.name = name or [random.choice(charset) for i in range(20)]
- self.axes = self.plotcanvas.new_axes(self.name)
-
-
-class DrawingObject(BaseDraw):
- def __init__(self, plotcanvas, name=None):
- """
- Possible objects are:
-
- * Point
- * Line
- * Rectangle
- * Circle
- * Polygon
- """
-
- BaseDraw.__init__(self, plotcanvas)
- self.properties = {}
-
- def plot(self):
- return
-
- def update_plot(self):
- self.axes.cla()
- self.plot()
- self.plotcanvas.auto_adjust_axes()
-
-
-class DrawingPoint(DrawingObject):
- def __init__(self, plotcanvas, name=None, coord=None):
- DrawingObject.__init__(self, plotcanvas)
-
- self.properties.update({
- "coordinate": coord
- })
-
- def plot(self):
- x, y = self.properties["coordinate"]
- self.axes.plot(x, y, 'o')
-
-
-class Measurement:
- def __init__(self, container, plotcanvas, update=None):
- self.update = update
- self.container = container
- self.frame = None
- self.label = None
- self.point1 = None
- self.point2 = None
- self.active = False
- self.plotcanvas = plotcanvas
- self.click_subscription = None
- self.move_subscription = None
-
- def toggle_active(self, *args):
- if self.active: # Deactivate
- self.active = False
- self.container.remove(self.frame)
- if self.update is not None:
- self.update()
- self.plotcanvas.mpl_disconnect(self.click_subscription)
- self.plotcanvas.mpl_disconnect(self.move_subscription)
- return False
- else: # Activate
- print "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)
- self.frame = Gtk.Frame()
- self.frame.set_margin_right(5)
- self.frame.set_margin_top(3)
- align = Gtk.Alignment()
- align.set(0, 0.5, 0, 0)
- align.set_padding(4, 4, 4, 4)
- self.label = Gtk.Label()
- self.label.set_label("Click on a reference point...")
- abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
- abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
- abox.pack_start(self.label, False, False, 0)
- align.add(abox)
- self.frame.add(align)
- self.container.pack_end(self.frame, False, True, 1)
- self.frame.show_all()
- return True
-
- def on_move(self, event):
- if self.point1 is None:
- self.label.set_label("Click on a reference point...")
- else:
- try:
- dx = event.xdata - self.point1[0]
- dy = event.ydata - self.point1[1]
- d = sqrt(dx**2 + dy**2)
- self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
- except TypeError:
- pass
- if self.update is not None:
- self.update()
-
- def on_click(self, event):
- if self.point1 is None:
- self.point1 = (event.xdata, event.ydata)
- else:
- self.point2 = copy.copy(self.point1)
- self.point1 = (event.xdata, event.ydata)
- self.on_move(event)
-
-
-class PlotCanvas:
- """
- Class handling the plotting area in the application.
- """
-
- def __init__(self, container):
- """
- The constructor configures the Matplotlib figure that
- will contain all plots, creates the base axes and connects
- events to the plotting area.
-
- :param container: The parent container in which to draw plots.
- :rtype: PlotCanvas
- """
- # Options
- self.x_margin = 15 # pixels
- self.y_margin = 25 # Pixels
-
- # Parent container
- self.container = container
-
- # Plots go onto a single matplotlib.figure
- self.figure = Figure(dpi=50) # TODO: dpi needed?
- self.figure.patch.set_visible(False)
-
- # These axes show the ticks and grid. No plotting done here.
- # New axes must have a label, otherwise mpl returns an existing one.
- self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
- self.axes.set_aspect(1)
- self.axes.grid(True)
-
- # The canvas is the top level container (Gtk.DrawingArea)
- self.canvas = FigureCanvas(self.figure)
- self.canvas.set_hexpand(1)
- self.canvas.set_vexpand(1)
- self.canvas.set_can_focus(True) # For key press
-
- # Attach to parent
- self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
-
- # Events
- self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
- self.canvas.connect('configure-event', self.auto_adjust_axes)
- self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
- self.canvas.connect("scroll-event", self.on_scroll)
- self.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]
-
-
-class ObjectCollection:
-
- classdict = {
- "gerber": FlatCAMGerber,
- "excellon": FlatCAMExcellon,
- "cncjob": FlatCAMCNCjob,
- "geometry": FlatCAMGeometry
- }
-
- icon_files = {
- "gerber": "share/flatcam_icon16.png",
- "excellon": "share/drill16.png",
- "cncjob": "share/cnc16.png",
- "geometry": "share/geometry16.png"
- }
-
- def __init__(self):
-
- ### Icons for the list view
- self.icons = {}
- for kind in ObjectCollection.icon_files:
- self.icons[kind] = GdkPixbuf.Pixbuf.new_from_file(ObjectCollection.icon_files[kind])
-
- ### GUI List components
- ## Model
- self.store = Gtk.ListStore(FlatCAMObj)
-
- ## View
- self.view = Gtk.TreeView(model=self.store)
- #self.view.connect("row_activated", self.on_row_activated)
- self.tree_selection = self.view.get_selection()
- self.change_subscription = self.tree_selection.connect("changed", self.on_list_selection_change)
-
- ## Renderers
- # Icon
- renderer_pixbuf = Gtk.CellRendererPixbuf()
- column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf)
-
- def _set_cell_icon(column, cell, model, it, data):
- obj = model.get_value(it, 0)
- cell.set_property('pixbuf', self.icons[obj.kind])
-
- column_pixbuf.set_cell_data_func(renderer_pixbuf, _set_cell_icon)
- self.view.append_column(column_pixbuf)
-
- # Name
- renderer_text = Gtk.CellRendererText()
- column_text = Gtk.TreeViewColumn("Name", renderer_text)
-
- def _set_cell_text(column, cell, model, it, data):
- obj = model.get_value(it, 0)
- cell.set_property('text', obj.options["name"])
-
- column_text.set_cell_data_func(renderer_text, _set_cell_text)
- self.view.append_column(column_text)
-
- def print_list(self):
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- print obj
- iterat = self.store.iter_next(iterat)
-
- def delete_all(self):
- print "OC.delete_all()"
- # self.collection = []
- # self.active = None
- self.store.clear()
-
- def delete_active(self):
- print "OC.delete_active()"
- try:
- model, treeiter = self.tree_selection.get_selected()
- self.store.remove(treeiter)
- except:
- pass
-
- def on_row_activated(self, *args):
- """
- Does nothing right now.
- :param args: Ignored.
- :return: None
- """
- print "OC.on_row_activated()"
- return
-
- def on_list_selection_change(self, selection):
- """
- Callback for change in selection on the objects' list.
- Instructs the new selection to build the UI for its options.
-
- :param selection: Ignored.
- :return: None
- """
- print "OC.on_list_selection_change()"
- try:
- self.get_active().build_ui()
- except:
- pass
-
- # TODO: Now we don't have a reference to the previously
- # TODO: active, so cannot read form.
-
- def set_active(self, name):
- """
- Sets an object as the active object in the program. Same
- as `set_list_selection()`.
-
- :param name: Name of the object.
- :type name: str
- :return: None
- """
- print "OC.set_active()"
- self.set_list_selection(name)
-
- def get_active(self):
- print "OC.get_active()"
- try:
- model, treeiter = self.tree_selection.get_selected()
- return model[treeiter][0]
- except (TypeError, ValueError):
- return None
-
- def set_list_selection(self, name):
- """
- Sets which object should be selected in the list.
-
- :param name: Name of the object.
- :rtype name: str
- :return: None
- """
- print "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)
- self.tree_selection.select_iter(iterat)
-
- def append(self, obj, active=False):
- """
- Add a FlatCAMObj the the collection. This method is thread-safe.
-
- :param obj: FlatCAMObj to append
- :type obj: FlatCAMObj
- :param active: If it is to become the active object after appending
- :type active: bool
- :return: None
- """
- print "OC.append()"
-
- def guitask():
- self.store.append([obj])
- if active:
- self.set_list_selection(obj.options["name"])
- GLib.idle_add(guitask)
-
- def get_names(self):
- """
- Gets a list of the names of all objects in the collection.
-
- :return: List of names.
- :rtype: list
- """
- print "OC.get_names()"
- names = []
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- names.append(obj.options["name"])
- iterat = self.store.iter_next(iterat)
- return names
-
- def get_bounds(self):
- """
- Finds coordinates bounding all objects in the collection.
-
- :return: [xmin, ymin, xmax, ymax]
- :rtype: list
- """
- print "OC.get_bounds()"
-
- # TODO: Move the operation out of here.
-
- xmin = Inf
- ymin = Inf
- xmax = -Inf
- ymax = -Inf
-
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- try:
- gxmin, gymin, gxmax, gymax = obj.bounds()
- xmin = min([xmin, gxmin])
- ymin = min([ymin, gymin])
- xmax = max([xmax, gxmax])
- ymax = max([ymax, gymax])
- except:
- print "DEV WARNING: Tried to get bounds of empty geometry."
- iterat = self.store.iter_next(iterat)
- return [xmin, ymin, xmax, ymax]
-
- def get_list(self):
- """
- Returns a list with all FlatCAMObj.
-
- :return: List with all FlatCAMObj.
- :rtype: list
- """
- collection_list = []
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- collection_list.append(obj)
- iterat = self.store.iter_next(iterat)
- return collection_list
-
- def get_by_name(self, name):
- """
- Fetches the FlatCAMObj with the given `name`.
-
- :param name: The name of the object.
- :type name: str
- :return: The requested object or None if no such object.
- :rtype: FlatCAMObj or None
- """
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- if obj.options["name"] == name:
- return obj
- iterat = self.store.iter_next(iterat)
- return None
-
- def change_name(self, old_name, new_name):
- """
- Changes the name of `FlatCAMObj` named `old_name` to `new_name`.
-
- :param old_name: Name of the object to change.
- :type old_name: str
- :param new_name: New name.
- :type new_name: str
- :return: True if name change succeeded, False otherwise. Will fail
- if no object with `old_name` is found.
- :rtype: bool
- """
- iterat = self.store.get_iter_first()
- while iterat is not None:
- obj = self.store[iterat][0]
- if obj.options["name"] == old_name:
- obj.options["name"] = new_name
- self.store.row_changed(0, iterat)
- return True
- iterat = self.store.iter_next(iterat)
- return False
-
+from gi.repository import Gtk
+from FlatCAMApp import *
app = App()
Gtk.main()
diff --git a/FlatCAM.ui b/FlatCAM.ui
index 85521316..664bff42 100644
--- a/FlatCAM.ui
+++ b/FlatCAM.ui
@@ -535,2594 +535,6 @@ specified axis.
-
-
- False
-
-
- True
- True
- never
- in
-
-
- True
- False
-
-
- True
- False
- 5
- 5
- 5
- 5
- vertical
-
-
- True
- False
-
-
- True
- False
- share/drill32.png
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 3
- Excellon Object
-
-
-
-
-
- True
- True
- 1
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
-
-
- True
- False
- 3
- Name:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 2
-
-
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 3
-
-
-
-
- Solid
- True
- True
- False
- 0
- True
-
-
- False
- True
- 4
-
-
-
-
-
-
-
- Update Plot
- True
- True
- True
-
-
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- 5
- 0
- 3
- Create CNC Job:
-
-
-
-
-
- False
- True
- 7
-
-
-
-
- True
- False
- 2
- 4
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Drill Z:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- False
- 1
- Travel Z:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- 1
- Feed rate:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Tools:
-
-
- 0
- 3
- 1
- 1
-
-
-
-
- True
- False
-
-
- True
- True
- ●
- True
-
-
- False
- True
- 0
-
-
-
-
- Choose
- True
- True
- True
-
-
-
-
- False
- True
- 1
-
-
-
-
- 1
- 3
- 1
- 1
-
-
-
-
- False
- True
- 8
-
-
-
-
- Generate
- True
- True
- True
-
-
-
-
- False
- True
- 9
-
-
-
-
- True
- False
- 3
- 0
- 3
- Scale:
-
-
-
-
-
- False
- True
- 10
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- 1
- Factor:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 1.0
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 11
-
-
-
-
- Scale
- True
- True
- True
-
-
-
-
- False
- True
- 12
-
-
-
-
- True
- False
- 3
- 0
- 3
- Offset:
-
-
-
-
-
- False
- True
- 13
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- 1
- Offset Vector:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- (0.0, 0.0)
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 14
-
-
-
-
- Offset
- True
- True
- True
-
-
-
-
- False
- True
- 15
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- False
-
-
- True
- True
- never
- in
-
-
- True
- False
-
-
- True
- False
- 5
- 5
- 5
- 5
- vertical
-
-
- True
- False
-
-
- True
- False
- share/geometry32.png
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 3
- Geometry Object
-
-
-
-
-
- True
- True
- 1
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
-
-
- True
- False
- The Object's name. Only gets changed
-after hitting Enter in the text box.
- 3
- Name:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 2
-
-
-
-
- Plot
- True
- True
- False
- Plot this object on the main window.
- 0
- True
- True
-
-
-
- False
- True
- 3
-
-
-
-
- Solid
- True
- True
- False
- Show overlapping polygons as single.
- 0
- True
-
-
- False
- True
- 4
-
-
-
-
- Multi-colored
- True
- True
- False
- Draw polygons with different colors.
- 0
- True
-
-
- False
- True
- 5
-
-
-
-
- Update Plot
- True
- True
- True
-
-
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- <b>Create CNC Job:</b> CNC Jobs from Geometry
-Objects are cutting toolpaths along object contours.
- 5
- 0
- 3
- Create CNC Job:
-
-
-
-
-
- False
- True
- 7
-
-
-
-
- True
- False
- 2
- 4
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- The contours are cut at the given Z-axis
-position. Typically, the surface of the
-workpiece is at z=0.0, and <b>Cut Z</b>
-is specified as a negative number.
- 1
- Cut Z:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- False
- This specifies the Z-axis position of the
-tool for XY-plane motions when not
-cutting.
- 1
- Travel Z:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- The feed rate is the speed in <b>units/minute</b>
-when cutting. When not cutting, movement is
-done at maximum speed.
- 1
- Feed rate:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- False
- The tool diameter specified here is for
-visual purposes only and does not affect
-the resulting job.
- 1
- Tool diam:
-
-
- 0
- 3
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 3
- 1
- 1
-
-
-
-
- False
- True
- 8
-
-
-
-
- Generate
- True
- True
- True
- Creates the CNC Job oobject from this
-Geometry object with the specified options.
-
-
-
-
- False
- True
- 9
-
-
-
-
- True
- False
- <b>Paint Area:</b> Creates contour
-inside a polygon for a tool to cover
-its entire surface. Use for clearing
-large copper areas.
- 5
- 0
- 3
- Paint Area:
-
-
-
-
-
- False
- True
- 10
-
-
-
-
- True
- False
- 2
- 5
-
-
- True
- False
- Tool diameter for painting
-inside a polygon.
- 1
- Tool diam:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- False
- How much (fraction of tool diameter)
-to overlap each toolpath.
- 1
- Overlap:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- False
- Margin from the edge of the polygon.
-Use this to stay away from edges
-cut or to be cut with a finer, more
-accurate tool.
- 1
- Margin:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- False
- True
- 11
-
-
-
-
- Generate
- True
- True
- True
- Creates a Geometry Object with toolpaths
-to cover the inside of a polygon.
-
-<b>Note:</b> After clicking generate
-you have to click inside the desired
-polygon.
-
-
-
-
- False
- True
- 12
-
-
-
-
- True
- False
- <b>Scale:</b> Resizes the geometry
-of the object. All sizes and coordinates
-are multiplied by the given factor.
- 3
- 0
- 3
- Scale:
-
-
-
-
-
- False
- True
- 13
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- Factor by which to multiply all
-geometrical dimensions.
- 1
- Factor:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 1.0
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 14
-
-
-
-
- Scale
- True
- True
- True
- Scales the geometry
-of this object.
-
-
-
-
- False
- True
- 15
-
-
-
-
- True
- False
- <b>Offset:</b> Shift the geometry of
-this object by the specified (x, y) vector.
- 3
- 0
- 3
- Offset:
-
-
-
-
-
- False
- True
- 16
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- Vector by which to offset the geometry.
-Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are
-decimal numbers representing the
-distance in the corresponding axis.
- 1
- Offset Vector:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- (0.0, 0.0)
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 17
-
-
-
-
- Offset
- True
- True
- True
- Offset the geometry of
-this object.
-
-
-
-
- False
- True
- 18
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- False
-
-
- True
- True
- never
- in
-
-
- True
- False
-
-
- True
- False
- 5
- 5
- 5
- 5
- vertical
-
-
- True
- False
-
-
- True
- False
- share/flatcam_icon32.png
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 3
- Gerber Object
-
-
-
-
-
- True
- True
- 1
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
-
-
- True
- False
- The Object's name. Only gets changed
-after hitting Enter in the text box.
- 3
- Name:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 5
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 2
-
-
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 3
-
-
-
-
- Merge Polygons
- True
- True
- False
- 0
- True
- True
-
-
- False
- True
- 4
-
-
-
-
- Solid
- True
- True
- False
- 0
- True
-
-
- False
- True
- 5
-
-
-
-
- Multi-colored
- True
- True
- False
- 0
- True
-
-
- False
- True
- 6
-
-
-
-
- Update Plot
- True
- True
- True
-
-
-
-
- False
- True
- 7
-
-
-
-
- True
- False
- <b>Isolation Routing:</b> Cutting copper
-around traces for electrical isolation.
- 5
- 0
- 3
- Isolation Routing:
-
-
-
-
-
- False
- True
- 8
-
-
-
-
- True
- False
- 3
- 2
-
-
- True
- False
- Tool paths for isolation routing are drawn
-at 1/2 of the tool diameter away from
-polygons defined in Gerber.
- 1
- 3
- Tool diam:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- False
- True
- The number of toolwidths defining
-the total width of the isolation.
- 1
- 3
- Width (# passes):
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- True
- How much (fraction of the tool diameter)
-to overlap each pass.
- 1
- 3
- Pass overlap:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- 0.15
- True
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- 1
- True
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- False
- True
- 9
-
-
-
-
- Generate Geometry
- True
- True
- True
- Creates a geometry object with tool
-paths for isolation routing. If more than
-1 pass, wil create one object for each.
-
-
-
-
- False
- True
- 10
-
-
-
-
- True
- False
- <b>Board cutout:</b> Cut around
-the edge of the board.
- 5
- 0
- 3
- Board cutout:
-
-
-
-
-
- False
- True
- 11
-
-
-
-
- True
- False
- 3
-
-
- True
- False
- Distance away from the rectangular edge
-enclosing the board to generate the
-cutting tool path.
- 1
- Margin:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- Length of the gaps along the toolpath.
-These gaps are needed to hold the
-board in place until the job is complete.
- 1
- Gap size:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Gaps:
-
-
- 0
- 3
- 1
- 1
-
-
-
-
- True
- False
-
-
- 2 (T/B)
- True
- True
- False
- Gaps are placed on the top and
-bottom edges of the board cutout.
- 8
- 0
- True
- rb_2lr
-
-
- False
- True
- 0
-
-
-
-
- 2 (L/R)
- True
- True
- False
- Gaps are placed on the left and right
-edges of the board cutout.
- 8
- 0
- True
- True
-
-
- False
- True
- 1
-
-
-
-
- 4
- True
- True
- False
- Gaps are placed on the four edges
-of the board cutout.
- 0
- True
- rb_2lr
-
-
- False
- True
- 2
-
-
-
-
- 1
- 3
- 1
- 1
-
-
-
-
- True
- False
- True
- Tool diameter to be used to cut out
-the baord.
- 1
- Tool diam.:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- False
- True
- 12
-
-
-
-
- Generate Geometry
- True
- True
- True
- Generates the Geometry object with
-toolpaths for board cutout.
-
-
-
-
- False
- True
- 13
-
-
-
-
- True
- False
- <b>Non-copper regions</b>: Use to create
-geometry with polygons covering areas
-without copper on the board.
-
-This can be used, for example, to clear all
-copper from certain regions.
- 5
- 0
- 3
- Non-copper regions:
-
-
-
-
-
- False
- True
- 14
-
-
-
-
- True
- False
- 4
- 4
- 1
-
-
- True
- False
- Defines how far from the rectangular box
-enclosing the contents of the board the
-edge of the board is.
-
-Copper-free areas will be located within
-this boundary.
- 1
- Boundary margin:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 14
- True
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 15
-
-
-
-
- Rounded corners
- True
- True
- False
- True
- Whether to draw rounded corners on the bounding
-box. The radius is the Boundary margin.
-
- Whether to draw rounded corners on the bounding
-box. The radius is the Boundary margin.
-
- 0
- True
-
-
- False
- True
- 16
-
-
-
-
- Generate Geometry
- True
- True
- True
- Creates a Geometry Object covering
-areas without copper.
-
-
-
-
- False
- True
- 17
-
-
-
-
- True
- False
- <b>Bounding Box:</b> A simple rectangular
-boundary around the PCB contents.
- 5
- 0
- 3
- Bounding box:
-
-
-
-
-
- False
- True
- 18
-
-
-
-
- True
- False
- 4
- 4
- 1
-
-
- True
- False
- Distance from the rectangular box enclosing
-the PCB contents at which to draw the
-bounding box.
- 1
- Boundary margin:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 14
- True
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 19
-
-
-
-
- Rounded corners
- True
- True
- False
- Whether to draw rounded corners on the bounding
-box. The radius is the Boundary margin.
-
- 0
- True
-
-
- False
- True
- 20
-
-
-
-
- Generate Bounding Box
- True
- True
- True
- Generates a Geometry object
-with the bounding box.
-
-
-
-
- False
- True
- 21
-
-
-
-
- True
- False
- <b>Scale:</b> Resizes the geometry
-of the object. All sizes and coordinates
-are multiplied by the given factor.
- 3
- 0
- 3
- Scale:
-
-
-
-
-
- False
- True
- 22
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- Factor by which to multiply all
-geometrical dimensions.
- 1
- Factor:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 1.0
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 23
-
-
-
-
- Scale
- True
- True
- True
- Scales the geometry
-of this object.
-
-
-
-
- False
- True
- 24
-
-
-
-
- True
- False
- <b>Offset:</b> Shift the geometry of
-this object by the specified (x, y) vector.
- 3
- 0
- 3
- Offset:
-
-
-
-
-
- False
- True
- 25
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- Vector by which to offset the geometry.
-Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are
-decimal numbers representing the
-distance in the corresponding axis.
- 1
- Offset Vector:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- (0.0, 0.0)
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 26
-
-
-
-
- Offset
- True
- True
- True
- Offset the geometry of
-this object.
-
-
-
-
- False
- True
- 27
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
600
400
@@ -3540,7 +952,7 @@ to application defaults.
False
image16
False
-
+
@@ -3611,7 +1023,27 @@ to application defaults.
True
vertical
-
+
+ True
+ True
+ True
+ never
+ in
+
+
+ True
+ False
+
+
+
+
+
+
+
+ False
+ True
+ 0
+
@@ -3651,7 +1083,7 @@ for the current object.
True
False
-
+
True
False
5
@@ -3724,1407 +1156,18 @@ project.
-
-
-
-
-
-
-
+
True
False
- 5
+ vertical
-
- True
- False
- 1
- Units:
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 8
-
-
- inch
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 0
-
-
-
-
- mm
- True
- True
- False
- 0
- True
- rb_inch
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
+
False
True
- 3
-
-
-
-
- True
- False
- 8
- 0
-
-
- True
- False
- 6
- 6
- 12
- 6
-
-
- True
- False
- vertical
-
-
- True
- False
- 5
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 1
-
-
-
-
- Merge Polygons
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 2
-
-
-
-
- Solid
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 3
-
-
-
-
- Multi-colored
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 4
-
-
-
-
- True
- False
- 5
- 0
- 3
- Isolation Routing:
-
-
-
-
-
- False
- True
- 5
-
-
-
-
- True
- False
- 3
- 2
-
-
- True
- False
- True
- Tool paths for isolation routing are drawn
-at 1/2 of the tool diameter away from
-polygons defined in Gerber.
- Tool paths for isolation routing are drawn
-at 1/2 of the tool diameter away from
-polygons defined in Gerber.
- 1
- 3
- Tool diam:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- False
- True
- The number of toolwidths defining
-the total width of the isolation.
- The number of toolwidths defining
-the total width of the isolation.
- 1
- 3
- Width (# passes):
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- True
- How much (fraction of the tool diameter)
-to overlap each pass.
- How much (fraction of the tool diameter)
-to overlap each pass.
- 1
- 3
- Pass overlap:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- 2
- 2
- ●
- 16
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- 5
- 0
- 3
- Board cutout:
-
-
-
-
-
- False
- True
- 7
-
-
-
-
- True
- False
- 3
-
-
- True
- False
- 1
- Margin:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Gap size:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Gaps:
-
-
- 0
- 3
- 1
- 1
-
-
-
-
- True
- False
-
-
- 2 (T/B)
- True
- True
- False
- 8
- 0
- True
- True
-
-
-
- False
- True
- 0
-
-
-
-
- 2 (L/R)
- True
- True
- False
- 8
- 0
- True
- rb_app_2tb
-
-
-
- False
- True
- 1
-
-
-
-
- 4
- True
- True
- False
- 0
- True
- rb_app_2tb
-
-
- False
- True
- 2
-
-
-
-
- 1
- 3
- 1
- 1
-
-
-
-
- True
- False
- 1
- Tool diam.:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- 12
- True
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- False
- True
- 8
-
-
-
-
- True
- False
- 5
- 0
- 3
- Non-copper regions:
-
-
-
-
-
- False
- True
- 9
-
-
-
-
- True
- False
- 4
- 4
- 1
-
-
- True
- False
- 1
- Boundary margin:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 14
- True
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 10
-
-
-
-
- Rounded corners
- True
- True
- False
- 0
- True
-
-
- False
- True
- 11
-
-
-
-
- True
- False
- 5
- 0
- 3
- Bounding box:
-
-
-
-
-
- False
- True
- 12
-
-
-
-
- True
- False
- 4
- 4
- 1
-
-
- True
- False
- 1
- Boundary margin:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- 14
- True
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 13
-
-
-
-
- Rounded corners
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 14
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- False
- 5
- <b>Gerber Objects</b>
- True
-
-
-
-
- False
- True
- 4
-
-
-
-
-
-
-
-
-
-
- True
- False
- 8
- 0
-
-
- True
- False
- 6
- 6
- 12
- 6
-
-
- True
- False
- vertical
-
-
- True
- False
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 1
-
-
-
-
- Solid
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 2
-
-
-
-
-
-
-
- True
- False
- 5
- 0
- 3
- Create CNC Job:
-
-
-
-
-
- False
- True
- 4
-
-
-
-
- True
- False
- 2
- 4
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Drill Z:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- False
- 1
- Travel Z:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- 1
- Feed rate:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- False
- True
- 5
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- False
- 5
- <b>Excellon Objects</b>
- True
-
-
-
-
- False
- True
- 7
-
-
-
-
- True
- False
- 8
- 0
-
-
- True
- False
- 6
- 6
- 12
- 6
-
-
- True
- False
- vertical
-
-
- True
- False
- 0
- 3
- Plot Options:
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 1
-
-
-
-
- Solid
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 2
-
-
-
-
- Multi-colored
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 3
-
-
-
-
- True
- False
- 5
- 0
- 3
- Create CNC Job:
-
-
-
-
-
- False
- True
- 4
-
-
-
-
- True
- False
- 2
- 4
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Cut Z:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- False
- 1
- Travel Z:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- False
- 1
- Feed rate:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- False
- 1
- Tool diam:
-
-
- 0
- 3
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 3
- 1
- 1
-
-
-
-
- False
- True
- 5
-
-
-
-
- True
- False
- 5
- 0
- 3
- Paint Area:
-
-
-
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- 2
- 5
-
-
- True
- False
- 1
- Tool diam:
-
-
- 0
- 0
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 0
- 1
- 1
-
-
-
-
- True
- False
- 1
- Overlap:
-
-
- 0
- 1
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 1
- 1
- 1
-
-
-
-
- True
- False
- 1
- Margin:
-
-
- 0
- 2
- 1
- 1
-
-
-
-
- True
- True
- ●
- True
-
-
-
- 1
- 2
- 1
- 1
-
-
-
-
- False
- True
- 7
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- False
- 5
- <b>Geometry Objects</b>
- True
-
-
-
-
- False
- True
- 8
-
-
-
-
- True
- False
- 8
- 0
-
-
- True
- False
- 6
- 6
- 12
- 6
-
-
- True
- False
- vertical
-
-
- Plot
- True
- True
- False
- 0
- True
- True
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 3
- 3
- 3
-
-
- True
- False
- 1
- Tool diam:
-
-
- False
- True
- 0
-
-
-
-
- True
- True
- ●
- True
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- False
- 5
- <b>CNC Job Objects</b>
- True
-
-
-
-
- False
- True
- 9
+ 1
diff --git a/FlatCAMApp.py b/FlatCAMApp.py
new file mode 100644
index 00000000..86b8d708
--- /dev/null
+++ b/FlatCAMApp.py
@@ -0,0 +1,3025 @@
+import threading
+import traceback
+import sys
+import urllib
+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 shapely import speedups
+
+
+########################################
+## Imports part of FlatCAM ##
+########################################
+from FlatCAMWorker import Worker
+from ObjectCollection import *
+from FlatCAMObj import *
+
+
+class GerberOptionsGroupUI(Gtk.VBox):
+ def __init__(self):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ ## 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)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ # Solid CB
+ self.solid_cb = FCCheckBox(label='Solid')
+ grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+ # Multicolored CB
+ self.multicolored_cb = FCCheckBox(label='Multicolored')
+ grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+
+ ## Isolation Routing
+ self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.isolation_routing_label.set_markup("Isolation Routing:")
+ self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+ grid = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid, expand=True, fill=False, padding=2)
+
+ l1 = Gtk.Label('Tool diam:', xalign=1)
+ grid.attach(l1, 0, 0, 1, 1)
+ self.iso_tool_dia_entry = LengthEntry()
+ grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
+
+ l2 = Gtk.Label('Width (# passes):', xalign=1)
+ grid.attach(l2, 0, 1, 1, 1)
+ self.iso_width_entry = IntEntry()
+ grid.attach(self.iso_width_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Pass overlap:', xalign=1)
+ grid.attach(l3, 0, 2, 1, 1)
+ self.iso_overlap_entry = FloatEntry()
+ grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
+
+ ## Board cuttout
+ self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.isolation_routing_label.set_markup("Board cutout:")
+ self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+ grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid2, expand=True, fill=False, padding=2)
+
+ l4 = Gtk.Label('Tool dia:', xalign=1)
+ grid2.attach(l4, 0, 0, 1, 1)
+ self.cutout_tooldia_entry = LengthEntry()
+ grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
+
+ l5 = Gtk.Label('Margin:', xalign=1)
+ grid2.attach(l5, 0, 1, 1, 1)
+ self.cutout_margin_entry = LengthEntry()
+ grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
+
+ l6 = Gtk.Label('Gap size:', xalign=1)
+ grid2.attach(l6, 0, 2, 1, 1)
+ self.cutout_gap_entry = LengthEntry()
+ grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
+
+ l7 = Gtk.Label('Gaps:', xalign=1)
+ grid2.attach(l7, 0, 3, 1, 1)
+ self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+ {'label': '2 (L/R)', 'value': 'lr'},
+ {'label': '4', 'value': '4'}])
+ grid2.attach(self.gaps_radio, 1, 3, 1, 1)
+
+ ## Non-copper regions
+ self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.noncopper_label.set_markup("Non-copper regions:")
+ self.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
+
+ grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid3, expand=True, fill=False, padding=2)
+
+ l8 = Gtk.Label('Boundary margin:', xalign=1)
+ grid3.attach(l8, 0, 0, 1, 1)
+ self.noncopper_margin_entry = LengthEntry()
+ grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
+
+ self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+ grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
+
+ ## Bounding box
+ self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.boundingbox_label.set_markup('Bounding Box:')
+ self.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
+
+ grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid4, expand=True, fill=False, padding=2)
+
+ l9 = Gtk.Label('Boundary Margin:', xalign=1)
+ grid4.attach(l9, 0, 0, 1, 1)
+ self.bbmargin_entry = LengthEntry()
+ grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
+
+ self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+ grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
+
+
+class ExcellonOptionsGroupUI(Gtk.VBox):
+ def __init__(self):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ ## 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)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ self.solid_cb = FCCheckBox(label='Solid')
+ grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+ ## Create CNC Job
+ self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.cncjob_label.set_markup('Create CNC Job')
+ self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+ grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid1, expand=True, fill=False, padding=2)
+
+ l1 = Gtk.Label('Cut Z:', xalign=1)
+ grid1.attach(l1, 0, 0, 1, 1)
+ self.cutz_entry = LengthEntry()
+ grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+ l2 = Gtk.Label('Travel Z:', xalign=1)
+ grid1.attach(l2, 0, 1, 1, 1)
+ self.travelz_entry = LengthEntry()
+ grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Feed rate:', xalign=1)
+ grid1.attach(l3, 0, 2, 1, 1)
+ self.feedrate_entry = LengthEntry()
+ grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
+
+
+class GeometryOptionsGroupUI(Gtk.VBox):
+ def __init__(self):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ ## 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)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ ## Create CNC Job
+ self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.cncjob_label.set_markup('Create CNC Job:')
+ self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+ grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid1, expand=True, fill=False, padding=2)
+
+ # Cut Z
+ l1 = Gtk.Label('Cut Z:', xalign=1)
+ grid1.attach(l1, 0, 0, 1, 1)
+ self.cutz_entry = LengthEntry()
+ grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+ # Travel Z
+ l2 = Gtk.Label('Travel Z:', xalign=1)
+ grid1.attach(l2, 0, 1, 1, 1)
+ self.travelz_entry = LengthEntry()
+ grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Feed rate:', xalign=1)
+ grid1.attach(l3, 0, 2, 1, 1)
+ self.cncfeedrate_entry = LengthEntry()
+ grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
+
+ l4 = Gtk.Label('Tool dia:', xalign=1)
+ grid1.attach(l4, 0, 3, 1, 1)
+ self.cnctooldia_entry = LengthEntry()
+ grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
+
+ ## Paint Area
+ self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.paint_label.set_markup('Paint Area:')
+ self.pack_start(self.paint_label, expand=True, fill=False, padding=2)
+
+ grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid2, expand=True, fill=False, padding=2)
+
+ # Tool dia
+ l5 = Gtk.Label('Tool dia:', xalign=1)
+ grid2.attach(l5, 0, 0, 1, 1)
+ self.painttooldia_entry = LengthEntry()
+ grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+
+ # Overlap
+ l6 = Gtk.Label('Overlap:', xalign=1)
+ grid2.attach(l6, 0, 1, 1, 1)
+ self.paintoverlap_entry = LengthEntry()
+ grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+
+ # Margin
+ l7 = Gtk.Label('Margin:', xalign=1)
+ grid2.attach(l7, 0, 2, 1, 1)
+ self.paintmargin_entry = LengthEntry()
+ grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+
+
+class CNCJobOptionsGroupUI(Gtk.VBox):
+ def __init__(self):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ ## 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)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 2, 1)
+
+ # Tool dia for plot
+ l1 = Gtk.Label('Tool dia:', xalign=1)
+ grid0.attach(l1, 0, 1, 1, 1)
+ self.tooldia_entry = LengthEntry()
+ grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+
+
+class GlobalOptionsUI(Gtk.VBox):
+ def __init__(self):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ box1 = Gtk.Box()
+ self.pack_start(box1, expand=False, fill=False, padding=2)
+ l1 = Gtk.Label('Units:')
+ box1.pack_start(l1, expand=False, fill=False, padding=2)
+ self.units_radio = RadioSet([{'label': 'inch', 'value': 'IN'},
+ {'label': 'mm', 'value': 'MM'}])
+ box1.pack_start(self.units_radio, expand=False, fill=False, padding=2)
+
+ ####### Gerber #######
+ l2 = Gtk.Label(margin=5)
+ l2.set_markup('Gerber Options')
+ frame1 = Gtk.Frame(label_widget=l2)
+ self.pack_start(frame1, expand=False, fill=False, padding=2)
+ self.gerber_group = GerberOptionsGroupUI()
+ frame1.add(self.gerber_group)
+
+ ######## Excellon #########
+ l3 = Gtk.Label(margin=5)
+ l3.set_markup('Excellon Options')
+ frame2 = Gtk.Frame(label_widget=l3)
+ self.pack_start(frame2, expand=False, fill=False, padding=2)
+ self.excellon_group = ExcellonOptionsGroupUI()
+ frame2.add(self.excellon_group)
+
+ ########## Geometry ##########
+ l4 = Gtk.Label(margin=5)
+ l4.set_markup('Geometry Options')
+ frame3 = Gtk.Frame(label_widget=l4)
+ self.pack_start(frame3, expand=False, fill=False, padding=2)
+ self.geometry_group = GeometryOptionsGroupUI()
+ frame3.add(self.geometry_group)
+
+ ########## CNC ############
+ l5 = Gtk.Label(margin=5)
+ l5.set_markup('CNC Job Options')
+ frame4 = Gtk.Frame(label_widget=l5)
+ self.pack_start(frame4, expand=False, fill=False, padding=2)
+ self.cncjob_group = CNCJobOptionsGroupUI()
+ frame4.add(self.cncjob_group)
+
+
+########################################
+## App ##
+########################################
+class App:
+ """
+ The main application class. The constructor starts the GUI.
+ """
+
+ log = logging.getLogger('base')
+ log.setLevel(logging.DEBUG)
+ formatter = logging.Formatter('[%(levelname)s] %(message)s')
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+ log.addHandler(handler)
+
+ version_url = "http://caram.cl/flatcam/VERSION"
+
+ def __init__(self):
+ """
+ Starts the application. Takes no parameters.
+
+ :return: app
+ :rtype: App
+ """
+
+ App.log.info("FlatCAM Starting...")
+
+ if speedups.available:
+ App.log.info("Enabling geometry speedups...")
+ speedups.enable()
+
+ # Needed to interact with the GUI from other threads.
+ GObject.threads_init()
+
+ # GLib.log_set_handler()
+
+ #### GUI ####
+ # Glade init
+ self.gladefile = "FlatCAM.ui"
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(self.gladefile)
+
+ # References to UI widgets
+ self.window = self.builder.get_object("window1")
+ self.position_label = self.builder.get_object("label3")
+ self.grid = self.builder.get_object("grid1")
+ self.notebook = self.builder.get_object("notebook1")
+ self.info_label = self.builder.get_object("label_status")
+ self.progress_bar = self.builder.get_object("progressbar")
+ self.progress_bar.set_show_text(True)
+ self.units_label = self.builder.get_object("label_units")
+ self.toolbar = self.builder.get_object("toolbar_main")
+
+ # White (transparent) background on the "Options" tab.
+ self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
+ Gdk.RGBA(1, 1, 1, 1))
+ # Combo box to choose between project and application options.
+ self.combo_options = self.builder.get_object("combo_options")
+ self.combo_options.set_active(1)
+
+ #self.setup_project_list() # The "Project" tab
+ self.setup_component_editor() # The "Selected" tab
+
+ ## Setup the toolbar. Adds buttons.
+ self.setup_toolbar()
+
+ #### Event handling ####
+ self.builder.connect_signals(self)
+
+ #### Make plot area ####
+ self.plotcanvas = PlotCanvas(self.grid)
+ self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
+ self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
+ self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
+
+ #### DATA ####
+ self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ self.setup_obj_classes()
+ self.mouse = None # Mouse coordinates over plot
+ self.recent = []
+ self.collection = ObjectCollection()
+ self.builder.get_object("box_project").pack_start(self.collection.view, False, False, 1)
+ # TODO: Do this different
+ self.collection.view.connect("row_activated", self.on_row_activated)
+
+ # Used to inhibit the on_options_update callback when
+ # the options are being changed by the program and not the user.
+ self.options_update_ignore = False
+
+ self.toggle_units_ignore = False
+
+ self.options_box = self.builder.get_object('options_box')
+ ## Application defaults ##
+ self.defaults = {
+ "units": "in"
+ }
+ self.defaults_form = GlobalOptionsUI()
+
+ ## Current Project ##
+ self.options = {} # Project options
+ self.project_filename = None
+ self.options_form = GlobalOptionsUI()
+
+ self.options_box.pack_start(self.defaults_form, False, False, 1)
+
+ # self.form_kinds = {
+ # "units": "radio"
+ # }
+
+ # self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
+ # "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
+ # self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
+ # "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
+
+ # Options for each kind of FlatCAMObj.
+ # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
+ # for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
+ # obj = FlatCAMClass("no_name")
+ # for option in obj.form_kinds:
+ # self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
+ # # if obj.form_kinds[option] == "radio":
+ # # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
+ # # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
+
+ ## Event subscriptions ##
+
+ ## Tools ##
+ self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas)
+ # Toolbar icon
+ # TODO: Where should I put this? Tool should have a method to add to toolbar?
+ meas_ico = Gtk.Image.new_from_file('share/measure32.png')
+ measure = Gtk.ToolButton.new(meas_ico, "")
+ measure.connect("clicked", self.measure.toggle_active)
+ measure.set_tooltip_markup("Measure Tool: Enable/disable tool.\n" +
+ "Click on point to set reference.\n" +
+ "(Click on plot and hit m)")
+ 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()
+
+ App.log.info("Starting Worker...")
+ self.worker = Worker()
+ self.worker.daemon = True
+ self.worker.start()
+
+ #### Check for updates ####
+ # Separate thread (Not worker)
+ self.version = 4
+ 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
+ t1.start()
+
+ #### For debugging only ###
+ def somethreadfunc(app_obj):
+ App.log.info("Hello World!")
+
+ t = threading.Thread(target=somethreadfunc, args=(self,))
+ t.daemon = True
+ t.start()
+
+ ########################################
+ ## START ##
+ ########################################
+ self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
+ self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
+ self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
+ Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
+ self.window.set_title("FlatCAM - Alpha 4 UNSTABLE")
+ self.window.set_default_size(900, 600)
+ self.window.show_all()
+ App.log.info("END of constructor. Releasing control.")
+
+ def message_dialog(self, title, message, kind="info"):
+ types = {"info": Gtk.MessageType.INFO,
+ "warn": Gtk.MessageType.WARNING,
+ "error": Gtk.MessageType.ERROR}
+ dlg = Gtk.MessageDialog(self.window, 0, types[kind], Gtk.ButtonsType.OK, title)
+ dlg.format_secondary_text(message)
+
+ def lifecycle():
+ dlg.run()
+ dlg.destroy()
+
+ GLib.idle_add(lifecycle)
+
+ def question_dialog(self, title, message):
+ label = Gtk.Label(message)
+ dialog = Gtk.Dialog(title, self.window, 0,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OK, Gtk.ResponseType.OK))
+ dialog.set_default_size(150, 100)
+ dialog.set_modal(True)
+ box = dialog.get_content_area()
+ box.set_border_width(10)
+ box.add(label)
+ dialog.show_all()
+ response = dialog.run()
+ dialog.destroy()
+ return response
+
+ def setup_toolbar(self):
+
+ # Zoom fit
+ zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
+ zoom_fit = Gtk.ToolButton.new(zf_ico, "")
+ zoom_fit.connect("clicked", self.on_zoom_fit)
+ zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit 1)")
+ self.toolbar.insert(zoom_fit, -1)
+
+ # Zoom out
+ zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
+ zoom_out = Gtk.ToolButton.new(zo_ico, "")
+ zoom_out.connect("clicked", self.on_zoom_out)
+ zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit 2)")
+ self.toolbar.insert(zoom_out, -1)
+
+ # Zoom in
+ zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
+ zoom_in = Gtk.ToolButton.new(zi_ico, "")
+ zoom_in.connect("clicked", self.on_zoom_in)
+ zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit 3)")
+ self.toolbar.insert(zoom_in, -1)
+
+ # Clear plot
+ cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
+ clear_plot = Gtk.ToolButton.new(cp_ico, "")
+ clear_plot.connect("clicked", self.on_clear_plots)
+ clear_plot.set_tooltip_markup("Clear Plot")
+ self.toolbar.insert(clear_plot, -1)
+
+ # Replot
+ rp_ico = Gtk.Image.new_from_file('share/replot32.png')
+ replot = Gtk.ToolButton.new(rp_ico, "")
+ replot.connect("clicked", self.on_toolbar_replot)
+ replot.set_tooltip_markup("Re-plot all")
+ self.toolbar.insert(replot, -1)
+
+ # Delete item
+ del_ico = Gtk.Image.new_from_file('share/delete32.png')
+ delete = Gtk.ToolButton.new(del_ico, "")
+ delete.connect("clicked", self.on_delete)
+ delete.set_tooltip_markup("Delete selected\nobject.")
+ self.toolbar.insert(delete, -1)
+
+ def setup_obj_classes(self):
+ """
+ Sets up application specifics on the FlatCAMObj class.
+
+ :return: None
+ """
+ FlatCAMObj.app = self
+
+ def setup_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("vp_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.add(box1)
+ box1.show()
+ label1.show()
+
+ def setup_recent_items(self):
+
+ # TODO: Move this to constructor
+ icons = {
+ "gerber": "share/flatcam_icon16.png",
+ "excellon": "share/drill16.png",
+ "cncjob": "share/cnc16.png",
+ "project": "share/project16.png"
+ }
+
+ openers = {
+ 'gerber': self.open_gerber,
+ 'excellon': self.open_excellon,
+ 'cncjob': self.open_gcode,
+ 'project': self.open_project
+ }
+
+ # Closure needed to create callbacks in a loop.
+ # Otherwise late binding occurs.
+ def make_callback(func, fname):
+ def opener(*args):
+ self.worker.add_task(func, [fname])
+ return opener
+
+ try:
+ f = open('recent.json')
+ except IOError:
+ App.log.error("Failed to load recent item list.")
+ self.info("ERROR: Failed to load recent item list.")
+ return
+
+ try:
+ self.recent = json.load(f)
+ except:
+ App.log.error("Failed to parse recent item list.")
+ self.info("ERROR: Failed to parse recent item list.")
+ f.close()
+ return
+ f.close()
+
+ recent_menu = Gtk.Menu()
+ for recent in self.recent:
+ filename = recent['filename'].split('/')[-1].split('\\')[-1]
+ item = Gtk.ImageMenuItem.new_with_label(filename)
+ im = Gtk.Image.new_from_file(icons[recent["kind"]])
+ item.set_image(im)
+
+ o = make_callback(openers[recent["kind"]], recent['filename'])
+
+ item.connect('activate', o)
+ recent_menu.append(item)
+
+ self.builder.get_object('open_recent').set_submenu(recent_menu)
+ recent_menu.show_all()
+
+ def info(self, text):
+ """
+ Show text on the status bar. This method is thread safe.
+
+ :param text: Text to display.
+ :type text: str
+ :return: None
+ """
+ GLib.idle_add(lambda: self.info_label.set_text(text))
+
+ def get_radio_value(self, radio_set):
+ """
+ Returns the radio_set[key] of the radiobutton
+ whose name is key is active.
+
+ :param radio_set: A dictionary containing widget_name: value pairs.
+ :type radio_set: dict
+ :return: radio_set[key]
+ """
+
+ for name in radio_set:
+ if self.builder.get_object(name).get_active():
+ return radio_set[name]
+
+ def plot_all(self):
+ """
+ Re-generates all plots from all objects.
+
+ :return: None
+ """
+ self.plotcanvas.clear()
+ self.set_progress_bar(0.1, "Re-plotting...")
+
+ def worker_task(app_obj):
+ percentage = 0.1
+ try:
+ delta = 0.9 / len(self.collection.get_list())
+ except ZeroDivisionError:
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+ return
+ for obj in self.collection.get_list():
+ obj.plot()
+ percentage += delta
+ GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+ GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+ GLib.idle_add(lambda: self.on_zoom_fit(None))
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+
+ # Send to worker
+ self.worker.add_task(worker_task, [self])
+
+ def get_eval(self, widget_name):
+ """
+ Runs eval() on the on the text entry of name 'widget_name'
+ and returns the results.
+
+ :param widget_name: Name of Gtk.Entry
+ :type widget_name: str
+ :return: Depends on contents of the entry text.
+ """
+
+ value = self.builder.get_object(widget_name).get_text()
+ if value == "":
+ value = "None"
+ try:
+ evald = eval(value)
+ return evald
+ except:
+ self.info("Could not evaluate: " + value)
+ return None
+
+ def new_object(self, kind, name, initialize):
+ """
+ Creates a new specalized FlatCAMObj and attaches it to the application,
+ this is, updates the GUI accordingly, any other records and plots it.
+ This method is thread-safe.
+
+ :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
+ """
+
+ App.log.debug("new_object()")
+
+ ### Check for existing name
+ if name in self.collection.get_names():
+ ## Create a new name
+ # Ends with number?
+ match = re.search(r'(.*[^\d])?(\d+)$', name)
+ if match: # Yes: Increment the number!
+ base = match.group(1) or ''
+ num = int(match.group(2))
+ name = base + str(num + 1)
+ else: # No: add a number!
+ name += "_1"
+
+ # Create object
+ classdict = {
+ "gerber": FlatCAMGerber,
+ "excellon": FlatCAMExcellon,
+ "cncjob": FlatCAMCNCjob,
+ "geometry": FlatCAMGeometry
+ }
+ obj = classdict[kind](name)
+ obj.units = self.options["units"] # TODO: The constructor should look at defaults.
+
+ # Set default options from self.options
+ for option in self.options:
+ if option.find(kind + "_") == 0:
+ oname = option[len(kind)+1:]
+ obj.options[oname] = self.options[option]
+
+ # Initialize as per user request
+ # User must take care to implement initialize
+ # in a thread-safe way as is is likely that we
+ # have been invoked in a separate thread.
+ initialize(obj, self)
+
+ # Check units and convert if necessary
+ if self.options["units"].upper() != obj.units.upper():
+ GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
+ obj.convert_units(self.options["units"])
+
+ # Add to our records
+ self.collection.append(obj, active=True)
+
+ # Show object details now.
+ GLib.idle_add(lambda: self.notebook.set_current_page(1))
+
+ # Plot
+ # TODO: (Thread-safe?)
+ obj.plot()
+
+ GLib.idle_add(lambda: self.on_zoom_fit(None))
+ #self.on_zoom_fit(None)
+
+ return obj
+
+ def set_progress_bar(self, percentage, text=""):
+ """
+ Sets the application's progress bar to a given frac_digits and text.
+
+ :param percentage: The frac_digits (0.0-1.0) of the progress.
+ :type percentage: float
+ :param text: Text to display on the progress bar.
+ :type text: str
+ :return: None
+ """
+ self.progress_bar.set_text(text)
+ self.progress_bar.set_fraction(percentage)
+ return False
+
+ def load_defaults(self):
+ """
+ Loads the aplication's default settings from defaults.json into
+ ``self.defaults``.
+
+ :return: None
+ """
+ try:
+ f = open("defaults.json")
+ options = f.read()
+ f.close()
+ except IOError:
+ App.log.error("Could not load defaults file.")
+ self.info("ERROR: Could not load defaults file.")
+ return
+
+ try:
+ defaults = json.loads(options)
+ except:
+ e = sys.exc_info()[0]
+ App.log.error(str(e))
+ self.info("ERROR: Failed to parse defaults file.")
+ return
+ self.defaults.update(defaults)
+
+ def read_form(self):
+ """
+ Reads the options form into self.defaults/self.options.
+
+ :return: None
+ :rtype: None
+ """
+ combo_sel = self.combo_options.get_active()
+ options_set = [self.options, self.defaults][combo_sel]
+ for option in options_set:
+ self.read_form_item(option, options_set)
+
+ def read_form_item(self, name, dest):
+ """
+ Reads the value of a form item in the defaults/options form and
+ saves it to the corresponding dictionary.
+
+ :param name: Name of the form item. A key in ``self.defaults`` or
+ ``self.options``.
+ :type name: str
+ :param dest: Dictionary to which to save the value.
+ :type dest: dict
+ :return: None
+ """
+ fkind = self.form_kinds[name]
+ fname = fkind + "_" + "app" + "_" + name
+
+ if fkind == 'entry_text':
+ dest[name] = self.builder.get_object(fname).get_text()
+ return
+ if fkind == 'entry_eval':
+ dest[name] = self.get_eval(fname)
+ return
+ if fkind == 'cb':
+ dest[name] = self.builder.get_object(fname).get_active()
+ return
+ if fkind == 'radio':
+ dest[name] = self.get_radio_value(self.radios[name])
+ return
+ print "Unknown kind of form item:", fkind
+
+ # def options2form(self):
+ # """
+ # Sets the 'Project Options' or 'Application Defaults' form with values from
+ # ``self.options`` or ``self.defaults``.
+ #
+ # :return: None
+ # :rtype: None
+ # """
+ #
+ # # Set the on-change callback to do nothing while we do the changes.
+ # self.options_update_ignore = True
+ # self.toggle_units_ignore = True
+ #
+ # combo_sel = self.combo_options.get_active()
+ # options_set = [self.options, self.defaults][combo_sel]
+ # for option in options_set:
+ # self.set_form_item(option, options_set[option])
+ #
+ # self.options_update_ignore = False
+ # self.toggle_units_ignore = False
+
+ def set_form_item(self, name, value):
+ """
+ Sets a form item 'name' in the GUI with the given 'value'. The syntax of
+ form names in the GUI is _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 save_project(self, filename):
+ """
+ Saves the current project to the specified file.
+
+ :param filename: Name of the file in which to save.
+ :type filename: str
+ :return: None
+ """
+
+ # Capture the latest changes
+ try:
+ self.collection.get_active().read_form()
+ except:
+ pass
+
+ # Serialize the whole project
+ d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
+ "options": self.options}
+
+ try:
+ f = open(filename, 'w')
+ except:
+ print "ERROR: Failed to open file for saving:", filename
+ return
+
+ try:
+ json.dump(d, f, default=to_dict)
+ except:
+ print "ERROR: File open but failed to write:", filename
+ f.close()
+ return
+
+ f.close()
+
+ def open_project(self, filename):
+ """
+ Loads a project from the specified file.
+
+ :param filename: Name of the file from which to load.
+ :type filename: str
+ :return: None
+ """
+
+ try:
+ f = open(filename, 'r')
+ except IOError:
+ App.log.error("Failed to open project file: %s" % filename)
+ self.info("ERROR: Failed to open project file: %s" % filename)
+ return
+
+ try:
+ d = json.load(f, object_hook=dict2obj)
+ except:
+ App.log.error("Failed to parse project file: %s" % filename)
+ self.info("ERROR: Failed to parse project file: %s" % filename)
+ f.close()
+ return
+
+ self.register_recent("project", filename)
+
+ # Clear the current project
+ self.on_file_new(None)
+
+ # Project options
+ self.options.update(d['options'])
+ self.project_filename = filename
+ GLib.idle_add(lambda: self.units_label.set_text(self.options["units"]))
+
+ # Re create objects
+ for obj in d['objs']:
+ def obj_init(obj_inst, app_inst):
+ obj_inst.from_dict(obj)
+ self.new_object(obj['kind'], obj['options']['name'], obj_init)
+
+ self.info("Project loaded from: " + filename)
+
+ def populate_objects_combo(self, combo):
+ """
+ Populates a Gtk.Comboboxtext with the list of the object in the project.
+
+ :param combo: Name or instance of the comboboxtext.
+ :type combo: str or Gtk.ComboBoxText
+ :return: None
+ """
+ print "Populating combo!"
+ if type(combo) == str:
+ combo = self.builder.get_object(combo)
+
+ combo.remove_all()
+ for name in self.collection.get_names():
+ combo.append_text(name)
+
+ def version_check(self, *args):
+ """
+ Checks for the latest version of the program. Alerts the
+ user if theirs is outdated. This method is meant to be run
+ in a saeparate thread.
+
+ :return: None
+ """
+
+ try:
+ f = urllib.urlopen(App.version_url)
+ except:
+ App.log.warning("Failed checking for latest version. Could not connect.")
+ GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+ return
+
+ try:
+ data = json.load(f)
+ except:
+ App.log.error("Could nor parse information about latest version.")
+ GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+ f.close()
+ return
+
+ f.close()
+
+ if self.version >= data["version"]:
+ GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
+ return
+
+ label = Gtk.Label("There is a newer version of FlatCAM\n" +
+ "available for download:\n\n" +
+ data["name"] + "\n\n" + data["message"])
+ dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OK, Gtk.ResponseType.OK))
+ dialog.set_default_size(150, 100)
+ dialog.set_modal(True)
+ box = dialog.get_content_area()
+ box.set_border_width(10)
+ box.add(label)
+
+ def do_dialog():
+ dialog.show_all()
+ response = dialog.run()
+ dialog.destroy()
+
+ GLib.idle_add(lambda: do_dialog())
+
+ return
+
+ # def setup_tooltips(self):
+ # tooltips = {
+ # "cb_gerber_plot": "Plot this object on the main window.",
+ # # "cb_gerber_mergepolys": "Show overlapping polygons as single.",
+ # "cb_gerber_solid": "Paint inside polygons.",
+ # "cb_gerber_multicolored": "Draw polygons with different colors."
+ # }
+ #
+ # for widget in tooltips:
+ # self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
+
+ def do_nothing(self, param):
+ return
+
+ def disable_plots(self, except_current=False):
+ """
+ Disables all plots with exception of the current object if specified.
+
+ :param except_current: Wether to skip the current object.
+ :rtype except_current: boolean
+ :return: None
+ """
+ # TODO: This method is very similar to replot_all. Try to merge.
+
+ self.set_progress_bar(0.1, "Re-plotting...")
+
+ def worker_task(app_obj):
+ percentage = 0.1
+ try:
+ delta = 0.9 / len(self.collection.get_list())
+ except ZeroDivisionError:
+ 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()
+ percentage += delta
+ GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+ GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+
+ # Send to worker
+ self.worker.add_task(worker_task, [self])
+
+ def enable_all_plots(self, *args):
+ self.plotcanvas.clear()
+ self.set_progress_bar(0.1, "Re-plotting...")
+
+ def worker_task(app_obj):
+ percentage = 0.1
+ try:
+ delta = 0.9 / len(self.collection.get_list())
+ except ZeroDivisionError:
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+ return
+ for obj in self.collection.get_list():
+ obj.options['plot'] = True
+ obj.plot()
+ percentage += delta
+ GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+ GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+
+ # Send to worker
+ self.worker.add_task(worker_task, [self])
+
+ def register_recent(self, kind, filename):
+ record = {'kind': kind, 'filename': filename}
+
+ if record in self.recent:
+ return
+
+ self.recent.insert(0, record)
+
+ if len(self.recent) > 10: # Limit reached
+ self.recent.pop()
+
+ try:
+ f = open('recent.json', 'w')
+ except IOError:
+ App.log.error("Failed to open recent items file for writing.")
+ self.info('Failed to open recent files file for writing.')
+ return
+
+ try:
+ json.dump(self.recent, f)
+ except:
+ App.log.error("Failed to write to recent items file.")
+ self.info('ERROR: Failed to write to recent items file.')
+ f.close()
+
+ f.close()
+
+ def open_gerber(self, filename):
+ """
+ Opens a Gerber file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param filename: Gerber file filename
+ :type filename: str
+ :return: None
+ """
+ GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Gerber ..."))
+
+ # How the object should be initialized
+ def obj_init(gerber_obj, app_obj):
+ assert isinstance(gerber_obj, FlatCAMGerber)
+
+ # Opening the file happens here
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
+ gerber_obj.parse_file(filename)
+
+ # Further parsing
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
+
+ # Object name
+ name = filename.split('/')[-1].split('\\')[-1]
+
+ self.new_object("gerber", name, obj_init)
+
+ # New object creation and file processing
+ # try:
+ # self.new_object("gerber", name, obj_init)
+ # except:
+ # e = sys.exc_info()
+ # print "ERROR:", e[0]
+ # traceback.print_exc()
+ # self.message_dialog("Failed to create Gerber Object",
+ # "Attempting to create a FlatCAM Gerber Object from " +
+ # "Gerber file failed during processing:\n" +
+ # str(e[0]) + " " + str(e[1]), kind="error")
+ # GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+ # self.collection.delete_active()
+ # return
+
+ # Register recent file
+ self.register_recent("gerber", filename)
+
+ # GUI feedback
+ self.info("Opened: " + filename)
+ GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+ GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+
+ def open_excellon(self, filename):
+ """
+ Opens an Excellon file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param filename: Excellon file filename
+ :type filename: str
+ :return: None
+ """
+ GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Excellon ..."))
+
+ # How the object should be initialized
+ def obj_init(excellon_obj, app_obj):
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
+ excellon_obj.parse_file(filename)
+ excellon_obj.create_geometry()
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
+
+ # Object name
+ name = filename.split('/')[-1].split('\\')[-1]
+
+ # New object creation and file processing
+ try:
+ self.new_object("excellon", name, obj_init)
+ except:
+ e = sys.exc_info()
+ App.log.error(str(e))
+ self.message_dialog("Failed to create Excellon Object",
+ "Attempting to create a FlatCAM Excellon Object from " +
+ "Excellon file failed during processing:\n" +
+ str(e[0]) + " " + str(e[1]), kind="error")
+ GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+ self.collection.delete_active()
+ return
+
+ # Register recent file
+ self.register_recent("excellon", filename)
+
+ # GUI feedback
+ self.info("Opened: " + filename)
+ GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+ GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
+
+ def open_gcode(self, filename):
+ """
+ Opens a G-gcode file, parses it and creates a new object for
+ it in the program. Thread-safe.
+
+ :param filename: G-code file filename
+ :type filename: str
+ :return: None
+ """
+
+ # How the object should be initialized
+ def obj_init(job_obj, app_obj_):
+ """
+
+ :type app_obj_: App
+ """
+ assert isinstance(app_obj_, App)
+ GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
+
+ f = open(filename)
+ gcode = f.read()
+ f.close()
+
+ job_obj.gcode = gcode
+
+ GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
+ job_obj.gcode_parse()
+
+ GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
+ job_obj.create_geometry()
+
+ GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
+
+ # Object name
+ name = filename.split('/')[-1].split('\\')[-1]
+
+ # New object creation and file processing
+ try:
+ self.new_object("cncjob", name, obj_init)
+ except:
+ e = sys.exc_info()
+ App.log.error(str(e))
+ self.message_dialog("Failed to create CNCJob Object",
+ "Attempting to create a FlatCAM CNCJob Object from " +
+ "G-Code file failed during processing:\n" +
+ str(e[0]) + " " + str(e[1]), kind="error")
+ GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+ self.collection.delete_active()
+ return
+
+ # Register recent file
+ self.register_recent("cncjob", filename)
+
+ # GUI feedback
+ self.info("Opened: " + filename)
+ GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+ GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
+
+ ########################################
+ ## EVENT HANDLERS ##
+ ########################################
+ def on_debug_printlist(self, *args):
+ self.collection.print_list()
+
+ def on_disable_all_plots(self, widget):
+ self.disable_plots()
+
+ 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.
+
+ :param widget: Ignored.
+ :return: None
+ """
+
+ about = self.builder.get_object("aboutdialog")
+ about.run()
+ about.hide()
+
+ def on_create_mirror(self, widget):
+ """
+ Creates a mirror image of an object to be used as a bottom layer.
+
+ :param widget: Ignored.
+ :return: None
+ """
+ # TODO: Move (some of) this to camlib!
+
+ # Object to mirror
+ obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
+ fcobj = self.collection.get_by_name(obj_name)
+
+ # For now, lets limit to Gerbers and Excellons.
+ # assert isinstance(gerb, FlatCAMGerber)
+ if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
+ self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
+ return
+
+ # Mirror axis "X" or "Y
+ axis = self.get_radio_value({"rb_mirror_x": "X",
+ "rb_mirror_y": "Y"})
+ mode = self.get_radio_value({"rb_mirror_box": "box",
+ "rb_mirror_point": "point"})
+ if mode == "point": # A single point defines the mirror axis
+ # TODO: Error handling
+ px, py = eval(self.point_entry.get_text())
+ else: # The axis is the line dividing the box in the middle
+ name = self.box_combo.get_active_text()
+ bb_obj = self.collection.get_by_name(name)
+ xmin, ymin, xmax, ymax = bb_obj.bounds()
+ px = 0.5*(xmin+xmax)
+ py = 0.5*(ymin+ymax)
+
+ fcobj.mirror(axis, [px, py])
+ fcobj.plot()
+
+ def on_create_aligndrill(self, widget):
+ """
+ Creates alignment holes Excellon object. Creates mirror duplicates
+ of the specified holes around the specified axis.
+
+ :param widget: Ignored.
+ :return: None
+ """
+
+ # Mirror axis. Same as in on_create_mirror.
+ axis = self.get_radio_value({"rb_mirror_x": "X",
+ "rb_mirror_y": "Y"})
+ # TODO: Error handling
+ mode = self.get_radio_value({"rb_mirror_box": "box",
+ "rb_mirror_point": "point"})
+ if mode == "point":
+ px, py = eval(self.point_entry.get_text())
+ else:
+ name = self.box_combo.get_active_text()
+ bb_obj = self.collection.get_by_name(name)
+ xmin, ymin, xmax, ymax = bb_obj.bounds()
+ px = 0.5*(xmin+xmax)
+ py = 0.5*(ymin+ymax)
+ xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+ # Tools
+ dia = self.get_eval("entry_dblsided_alignholediam")
+ tools = {"1": {"C": dia}}
+
+ # Parse hole list
+ # TODO: Better parsing
+ holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
+ holes = eval("[" + holes + "]")
+ drills = []
+ for hole in holes:
+ point = Point(hole)
+ point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+ drills.append({"point": point, "tool": "1"})
+ drills.append({"point": point_mirror, "tool": "1"})
+
+ def obj_init(obj_inst, app_inst):
+ obj_inst.tools = tools
+ obj_inst.drills = drills
+ obj_inst.create_geometry()
+
+ self.new_object("excellon", "Alignment Drills", obj_init)
+
+ def on_toggle_pointbox(self, widget):
+ """
+ Callback for radio selection change between point and box in the
+ Double-sided PCB tool. Updates the UI accordingly.
+
+ :param widget: Ignored.
+ :return: None
+ """
+
+ # Where the entry or combo go
+ box = self.builder.get_object("box_pointbox")
+
+ # Clear contents
+ children = box.get_children()
+ for child in children:
+ box.remove(child)
+
+ choice = self.get_radio_value({"rb_mirror_point": "point",
+ "rb_mirror_box": "box"})
+
+ if choice == "point":
+ self.point_entry = Gtk.Entry()
+ self.builder.get_object("box_pointbox").pack_start(self.point_entry,
+ False, False, 1)
+ self.point_entry.show()
+ else:
+ self.box_combo = Gtk.ComboBoxText()
+ self.builder.get_object("box_pointbox").pack_start(self.box_combo,
+ False, False, 1)
+ self.populate_objects_combo(self.box_combo)
+ self.box_combo.show()
+
+ def on_tools_doublesided(self, param):
+ """
+ Callback for menu item Tools->Double Sided PCB Tool. Launches the
+ tool placing its UI in the "Tool" tab in the notebook.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ # Were are we drawing the UI
+ box_tool = self.builder.get_object("box_tool")
+
+ # Remove anything else in the box
+ box_children = box_tool.get_children()
+ for child in box_children:
+ box_tool.remove(child)
+
+ # Get the UI
+ osw = self.builder.get_object("offscreenwindow_dblsided")
+ sw = self.builder.get_object("sw_dblsided")
+ osw.remove(sw)
+ vp = self.builder.get_object("vp_dblsided")
+ vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
+
+ # Put in the UI
+ box_tool.pack_start(sw, True, True, 0)
+
+ # INITIALIZATION
+ # Populate combo box
+ self.populate_objects_combo("comboboxtext_bottomlayer")
+
+ # Point entry
+ self.point_entry = Gtk.Entry()
+ box = self.builder.get_object("box_pointbox")
+ for child in box.get_children():
+ box.remove(child)
+ box.pack_start(self.point_entry, False, False, 1)
+
+ # Show the "Tool" tab
+ self.notebook.set_current_page(3)
+ sw.show_all()
+
+ def on_toggle_units(self, widget):
+ """
+ Callback for the Units radio-button change in the Options tab.
+ Changes the application's default units or the current project's units.
+ If changing the project's units, the change propagates to all of
+ the objects in the project.
+
+ :param widget: Ignored.
+ :return: None
+ """
+
+ if self.toggle_units_ignore:
+ return
+
+ combo_sel = self.combo_options.get_active()
+ options_set = [self.options, self.defaults][combo_sel]
+
+ # Options to scale
+ dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
+ 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
+ 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
+ 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
+ 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
+ 'geometry_paintmargin']
+
+ def scale_options(sfactor):
+ for dim in dimensions:
+ options_set[dim] *= sfactor
+
+ # The scaling factor depending on choice of units.
+ factor = 1/25.4
+ if self.builder.get_object('rb_mm').get_active():
+ factor = 25.4
+
+ # App units. Convert without warning.
+ if combo_sel == 1:
+ self.read_form()
+ scale_options(factor)
+ self.options2form()
+ return
+
+ # Changing project units. Warn user.
+ label = Gtk.Label("Changing the units of the project causes all geometrical \n" +
+ "properties of all objects to be scaled accordingly. Continue?")
+ dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OK, Gtk.ResponseType.OK))
+ dialog.set_default_size(150, 100)
+ dialog.set_modal(True)
+ box = dialog.get_content_area()
+ box.set_border_width(10)
+ box.add(label)
+ dialog.show_all()
+ response = dialog.run()
+ dialog.destroy()
+
+ if response == Gtk.ResponseType.OK:
+ #print "Converting units..."
+ #print "Converting options..."
+ self.read_form()
+ scale_options(factor)
+ self.options2form()
+ for obj in self.collection.get_list():
+ units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
+ obj.convert_units(units)
+ current = self.collection.get_active()
+ if current is not None:
+ current.to_form()
+ self.plot_all()
+ else:
+ # Undo toggling
+ self.toggle_units_ignore = True
+ if self.builder.get_object('rb_mm').get_active():
+ self.builder.get_object('rb_inch').set_active(True)
+ else:
+ self.builder.get_object('rb_mm').set_active(True)
+ self.toggle_units_ignore = False
+
+ self.read_form()
+ self.info("Converted units to %s" % self.options["units"])
+ self.units_label.set_text("[" + self.options["units"] + "]")
+
+ def on_file_openproject(self, param):
+ """
+ Callback for menu item File->Open Project. Opens a file chooser and calls
+ ``self.open_project()`` after successful selection of a filename.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ def on_success(app_obj, filename):
+ app_obj.open_project(filename)
+
+ self.file_chooser_action(on_success)
+
+ def on_file_saveproject(self, param):
+ """
+ Callback for menu item File->Save Project. Saves the project to
+ ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
+ if set to None. The project is saved by calling ``self.save_project()``.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ if self.project_filename is None:
+ self.on_file_saveprojectas(None)
+ else:
+ self.save_project(self.project_filename)
+ self.register_recent("project", self.project_filename)
+ self.info("Project saved to: " + self.project_filename)
+
+ def on_file_saveprojectas(self, param):
+ """
+ Callback for menu item File->Save Project As... Opens a file
+ chooser and saves the project to the given file via
+ ``self.save_project()``.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ def on_success(app_obj, filename):
+ assert isinstance(app_obj, App)
+
+ try:
+ f = open(filename, 'r')
+ f.close()
+ exists = True
+ except IOError:
+ exists = False
+
+ msg = "File exists. Overwrite?"
+ if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
+ return
+
+ app_obj.save_project(filename)
+ self.project_filename = filename
+ self.register_recent("project", filename)
+ app_obj.info("Project saved to: " + filename)
+
+ self.file_chooser_save_action(on_success)
+
+ def on_file_saveprojectcopy(self, param):
+ """
+ Callback for menu item File->Save Project Copy... Opens a file
+ chooser and saves the project to the given file via
+ ``self.save_project``. It does not update ``self.project_filename`` so
+ subsequent save requests are done on the previous known filename.
+
+ :param param: Ignore.
+ :return: None
+ """
+
+ def on_success(app_obj, filename):
+ assert isinstance(app_obj, App)
+
+ try:
+ f = open(filename, 'r')
+ f.close()
+ exists = True
+ except IOError:
+ exists = False
+
+ msg = "File exists. Overwrite?"
+ if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
+ return
+
+ app_obj.save_project(filename)
+ self.register_recent("project", filename)
+ app_obj.info("Project copy saved to: " + filename)
+
+ self.file_chooser_save_action(on_success)
+
+ def on_options_app2project(self, param):
+ """
+ Callback for Options->Transfer Options->App=>Project. Copies options
+ from application defaults to project defaults.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ self.options.update(self.defaults)
+ self.options2form() # Update UI
+
+ def on_options_project2app(self, param):
+ """
+ Callback for Options->Transfer Options->Project=>App. Copies options
+ from project defaults to application defaults.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ self.defaults.update(self.options)
+ self.options2form() # Update UI
+
+ def on_options_project2object(self, param):
+ """
+ Callback for Options->Transfer Options->Project=>Object. Copies options
+ from project defaults to the currently selected object.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ obj = self.collection.get_active()
+ if obj is None:
+ self.info("WARNING: No object selected.")
+ return
+ for option in self.options:
+ if option.find(obj.kind + "_") == 0:
+ oname = option[len(obj.kind)+1:]
+ obj.options[oname] = self.options[option]
+ obj.to_form() # Update UI
+
+ def on_options_object2project(self, param):
+ """
+ Callback for Options->Transfer Options->Object=>Project. Copies options
+ from the currently selected object to project defaults.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ obj = self.collection.get_active()
+ if obj is None:
+ self.info("WARNING: No object selected.")
+ return
+ obj.read_form()
+ for option in obj.options:
+ if option in ['name']: # TODO: Handle this better...
+ continue
+ self.options[obj.kind + "_" + option] = obj.options[option]
+ self.options2form() # Update UI
+
+ def on_options_object2app(self, param):
+ """
+ Callback for Options->Transfer Options->Object=>App. Copies options
+ from the currently selected object to application defaults.
+
+ :param param: Ignored.
+ :return: None
+ """
+ obj = self.collection.get_active()
+ if obj is None:
+ self.info("WARNING: No object selected.")
+ return
+ obj.read_form()
+ for option in obj.options:
+ if option in ['name']: # TODO: Handle this better...
+ continue
+ self.defaults[obj.kind + "_" + option] = obj.options[option]
+ self.options2form() # Update UI
+
+ def on_options_app2object(self, param):
+ """
+ Callback for Options->Transfer Options->App=>Object. Copies options
+ from application defaults to the currently selected object.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ obj = self.collection.get_active()
+ if obj is None:
+ self.info("WARNING: No object selected.")
+ return
+ for option in self.defaults:
+ if option.find(obj.kind + "_") == 0:
+ oname = option[len(obj.kind)+1:]
+ obj.options[oname] = self.defaults[option]
+ obj.to_form() # Update UI
+
+ def on_file_savedefaults(self, param):
+ """
+ Callback for menu item File->Save Defaults. Saves application default options
+ ``self.defaults`` to defaults.json.
+
+ :param param: Ignored.
+ :return: None
+ """
+
+ # Read options from file
+ try:
+ f = open("defaults.json")
+ options = f.read()
+ f.close()
+ except:
+ self.info("ERROR: Could not load defaults file.")
+ return
+
+ try:
+ defaults = json.loads(options)
+ except:
+ e = sys.exc_info()[0]
+ print e
+ self.info("ERROR: Failed to parse defaults file.")
+ return
+
+ # Update options
+ assert isinstance(defaults, dict)
+ defaults.update(self.defaults)
+
+ # Save update options
+ try:
+ f = open("defaults.json", "w")
+ json.dump(defaults, f)
+ f.close()
+ except:
+ self.info("ERROR: Failed to write defaults to file.")
+ return
+
+ self.info("Defaults saved.")
+
+ def on_options_combo_change(self, widget):
+ """
+ Called when the combo box to choose between application defaults and
+ project option changes value. The corresponding variables are
+ copied to the UI.
+
+ :param widget: The widget from which this was called. Ignore.
+ :return: None
+ """
+
+ combo_sel = self.combo_options.get_active()
+ print "Options --> ", combo_sel
+
+ # Remove anything else in the box
+ box_children = self.options_box.get_children()
+ for child in box_children:
+ self.options_box.remove(child)
+
+ form = [self.options_form, self.defaults_form][combo_sel]
+ self.options_box.pack_start(form, False, False, 1)
+ form.show_all()
+
+ # 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
+ as to use the whole canvas.
+
+ :param widget: Ignored.
+ :param event: Ignored.
+ :return: None
+ """
+
+ self.plotcanvas.auto_adjust_axes()
+
+ def on_row_activated(self, widget, path, col):
+ """
+ Callback for selection activation (Enter or double-click) on the Project list.
+ Switches the notebook page to the object properties form. Calls
+ ``self.notebook.set_current_page(1)``.
+
+ :param widget: Ignored.
+ :param path: Ignored.
+ :param col: Ignored.
+ :return: None
+ """
+ self.notebook.set_current_page(1)
+
+ # def on_generate_gerber_bounding_box(self, widget):
+ # """
+ # Callback for request from the Gerber form to generate a bounding box for the
+ # geometry in the object. Creates a FlatCAMGeometry with the bounding box.
+ # The box can have rounded corners if specified in the form.
+ #
+ # :param widget: Ignored.
+ # :return: None
+ # """
+ # # TODO: Use Gerber.get_bounding_box(...)
+ # gerber = self.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.
+ Re-plots the current object only.
+
+ :param widget: The widget from which this was called. Ignored.
+ :return: None
+ """
+
+ obj = self.collection.get_active()
+ obj.read_form()
+
+ self.set_progress_bar(0.5, "Plotting...")
+
+ def thread_func(app_obj):
+ assert isinstance(app_obj, App)
+ obj.plot()
+ GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+
+ # 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
+ selecting tools.
+
+ :param widget: The widget from which this was called.
+ :return: None
+ """
+ excellon = self.collection.get_active()
+ assert isinstance(excellon, FlatCAMExcellon)
+ excellon.show_tool_chooser()
+
+ def on_entry_eval_activate(self, widget):
+ """
+ Called when an entry is activated (eg. by hitting enter) if
+ set to do so. Its text is eval()'d and set to the returned value.
+ The current object is updated.
+
+ :param widget:
+ :return:
+ """
+ self.on_eval_update(widget)
+ obj = self.collection.get_active()
+ 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
+ eval() on its contents and puting it back as a
+ string.
+
+ :param widget: The widget from which this was called.
+ :return: None
+ """
+ # TODO: error handling here
+ widget.set_text(str(eval(widget.get_text())))
+
+ # def on_generate_isolation(self, widget):
+ # """
+ # Callback for button on Gerber form to create isolation routing geometry.
+ #
+ # :param widget: The widget from which this was called.
+ # :return: None
+ # """
+ #
+ # gerb = self.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)
+
+ def on_delete(self, widget):
+ """
+ Delete the currently selected FlatCAMObj.
+
+ :param widget: The widget from which this was called. Ignored.
+ :return: None
+ """
+
+ # Keep this for later
+ name = copy.copy(self.collection.get_active().options["name"])
+
+ # Remove plot
+ self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
+ self.plotcanvas.auto_adjust_axes()
+
+ # Clear form
+ self.setup_component_editor()
+
+ # Remove from dictionary
+ self.collection.delete_active()
+
+ self.info("Object deleted: %s" % name)
+
+ def on_toolbar_replot(self, widget):
+ """
+ Callback for toolbar button. Re-plots all objects.
+
+ :param widget: The widget from which this was called.
+ :return: None
+ """
+
+ self.collection.get_active().read_form()
+
+ self.plot_all()
+
+ def on_clear_plots(self, widget):
+ """
+ Callback for toolbar button. Clears all plots.
+
+ :param widget: The widget from which this was called.
+ :return: None
+ """
+ self.plotcanvas.clear()
+
+ # def on_activate_name(self, entry):
+ # """
+ # Hitting 'Enter' after changing the name of an item
+ # updates the item dictionary and re-builds the item list.
+ #
+ # :param entry: The widget from which this was called.
+ # :return: None
+ # """
+ #
+ # 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
+ startup state. This method is thread-safe.
+
+ :param param: Whatever is passed by the event. Ignore.
+ :return: None
+ """
+ # Remove everything from memory
+
+ # GUI things
+ def task():
+ # Clear plot
+ self.plotcanvas.clear()
+
+ # Delete data
+ self.collection.delete_all()
+
+ # Clear object editor
+ self.setup_component_editor()
+
+ GLib.idle_add(task)
+
+ # Clear project filename
+ self.project_filename = None
+
+ # Re-fresh project options
+ self.on_options_app2project(None)
+
+ def on_filequit(self, param):
+ """
+ Callback for menu item File->Quit. Closes the application.
+
+ :param param: Whatever is passed by the event. Ignore.
+ :return: None
+ """
+
+ self.window.destroy()
+ Gtk.main_quit()
+
+ def on_closewindow(self, param):
+ """
+ Callback for closing the main window.
+
+ :param param: Whatever is passed by the event. Ignore.
+ :return: None
+ """
+
+ self.window.destroy()
+ Gtk.main_quit()
+
+ def file_chooser_action(self, on_success):
+ """
+ Opens the file chooser and runs on_success on a separate thread
+ upon completion of valid file choice.
+
+ :param on_success: A function to run upon completion of a valid file
+ selection. Takes 2 parameters: The app instance and the filename.
+ Note that it is run on a separate thread, therefore it must take the
+ appropriate precautions when accessing shared resources.
+ :type on_success: func
+ :return: None
+ """
+ dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
+ Gtk.FileChooserAction.OPEN,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+ response = dialog.run()
+ if response == Gtk.ResponseType.OK:
+ filename = dialog.get_filename()
+ dialog.destroy()
+ # Send to worker.
+ self.worker.add_task(on_success, [self, filename])
+ elif response == Gtk.ResponseType.CANCEL:
+ self.info("Open cancelled.")
+ dialog.destroy()
+
+ def file_chooser_save_action(self, on_success):
+ """
+ Opens the file chooser and runs on_success upon completion of valid file choice.
+
+ :param on_success: A function to run upon selection of a filename. Takes 2
+ parameters: The instance of the application (App) and the chosen filename. This
+ gets run immediately in the same thread.
+ :return: None
+ """
+ dialog = Gtk.FileChooserDialog("Save file", self.window,
+ Gtk.FileChooserAction.SAVE,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+ dialog.set_current_name("Untitled")
+ response = dialog.run()
+ if response == Gtk.ResponseType.OK:
+ filename = dialog.get_filename()
+ dialog.destroy()
+ on_success(self, filename)
+ elif response == Gtk.ResponseType.CANCEL:
+ self.info("Save cancelled.") # print("Cancel clicked")
+ dialog.destroy()
+
+ def on_fileopengerber(self, param):
+ """
+ Callback for menu item File->Open Gerber. Defines a function that is then passed
+ to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
+ and updates the progress bar throughout the process.
+
+ :param param: Ignore
+ :return: None
+ """
+
+ self.file_chooser_action(lambda ao, filename: self.open_gerber(filename))
+
+ def on_fileopenexcellon(self, param):
+ """
+ Callback for menu item File->Open Excellon. Defines a function that is then passed
+ to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
+ and updates the progress bar throughout the process.
+
+ :param param: Ignore
+ :return: None
+ """
+
+ self.file_chooser_action(lambda ao, filename: self.open_excellon(filename))
+
+ def on_fileopengcode(self, param):
+ """
+ Callback for menu item File->Open G-Code. Defines a function that is then passed
+ to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
+ and updates the progress bar throughout the process.
+
+ :param param: Ignore
+ :return: None
+ """
+
+ self.file_chooser_action(lambda ao, filename: self.open_gcode(filename))
+
+ def on_mouse_move_over_plot(self, event):
+ """
+ Callback for the mouse motion event over the plot. This event is generated
+ by the Matplotlib backend and has been registered in ``self.__init__()``.
+ For details, see: http://matplotlib.org/users/event_handling.html
+
+ :param event: Contains information about the event.
+ :return: None
+ """
+
+ try: # May fail in case mouse not within axes
+ self.position_label.set_label("X: %.4f Y: %.4f" % (
+ event.xdata, event.ydata))
+ self.mouse = [event.xdata, event.ydata]
+
+ # for subscriber in self.plot_mousemove_subscribers:
+ # self.plot_mousemove_subscribers[subscriber](event)
+
+ except:
+ self.position_label.set_label("")
+ self.mouse = None
+
+ def on_click_over_plot(self, event):
+ """
+ Callback for the mouse click event over the plot. This event is generated
+ by the Matplotlib backend and has been registered in ``self.__init__()``.
+ For details, see: http://matplotlib.org/users/event_handling.html
+
+ Default actions are:
+
+ * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
+
+ :param event: Contains information about the event, like which button
+ was clicked, the pixel coordinates and the axes coordinates.
+ :return: None
+ """
+
+ # So it can receive key presses
+ self.plotcanvas.canvas.grab_focus()
+
+ try:
+ print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
+ event.button, event.x, event.y, event.xdata, event.ydata)
+
+ # TODO: This custom subscription mechanism is probably not necessary.
+ # for subscriber in self.plot_click_subscribers:
+ # self.plot_click_subscribers[subscriber](event)
+
+ self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
+
+ except Exception, e:
+ print "Outside plot!"
+
+ def on_zoom_in(self, event):
+ """
+ Callback for zoom-in request. This can be either from the corresponding
+ toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
+
+ :param event: Ignored.
+ :return: None
+ """
+ self.plotcanvas.zoom(1.5)
+ return
+
+ def on_zoom_out(self, event):
+ """
+ Callback for zoom-out request. This can be either from the corresponding
+ toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
+
+ :param event: Ignored.
+ :return: None
+ """
+ self.plotcanvas.zoom(1 / 1.5)
+
+ def on_zoom_fit(self, event):
+ """
+ Callback for zoom-out request. This can be either from the corresponding
+ toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
+ with axes limits from the geometry bounds of all objects.
+
+ :param event: Ignored.
+ :return: None
+ """
+ xmin, ymin, xmax, ymax = self.collection.get_bounds()
+ width = xmax - xmin
+ height = ymax - ymin
+ xmin -= 0.05 * width
+ xmax += 0.05 * width
+ ymin -= 0.05 * height
+ ymax += 0.05 * height
+ self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
+
+ def on_key_over_plot(self, event):
+ """
+ Callback for the key pressed event when the canvas is focused. Keyboard
+ shortcuts are handled here. So far, these are the shortcuts:
+
+ ========== ============================================
+ Key Action
+ ========== ============================================
+ '1' Zoom-fit. Fits the axes limits to the data.
+ '2' Zoom-out.
+ '3' Zoom-in.
+ 'm' Toggle on-off the measuring tool.
+ ========== ============================================
+
+ :param event: Ignored.
+ :return: None
+ """
+
+ if event.key == '1': # 1
+ self.on_zoom_fit(None)
+ return
+
+ if event.key == '2': # 2
+ self.plotcanvas.zoom(1 / 1.5, self.mouse)
+ return
+
+ if event.key == '3': # 3
+ self.plotcanvas.zoom(1.5, self.mouse)
+ return
+
+ if event.key == 'm':
+ if self.measure.toggle_active():
+ self.info("Measuring tool ON")
+ else:
+ self.info("Measuring tool OFF")
+ return
+
+
+class BaseDraw:
+ def __init__(self, plotcanvas, name=None):
+ """
+
+ :param plotcanvas: The PlotCanvas where the drawing tool will operate.
+ :type plotcanvas: PlotCanvas
+ """
+
+ self.plotcanvas = plotcanvas
+
+ # Must have unique axes
+ charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
+ self.name = name or [random.choice(charset) for i in range(20)]
+ self.axes = self.plotcanvas.new_axes(self.name)
+
+
+class DrawingObject(BaseDraw):
+ def __init__(self, plotcanvas, name=None):
+ """
+ Possible objects are:
+
+ * Point
+ * Line
+ * Rectangle
+ * Circle
+ * Polygon
+ """
+
+ BaseDraw.__init__(self, plotcanvas)
+ self.properties = {}
+
+ def plot(self):
+ return
+
+ def update_plot(self):
+ self.axes.cla()
+ self.plot()
+ self.plotcanvas.auto_adjust_axes()
+
+
+class DrawingPoint(DrawingObject):
+ def __init__(self, plotcanvas, name=None, coord=None):
+ DrawingObject.__init__(self, plotcanvas)
+
+ self.properties.update({
+ "coordinate": coord
+ })
+
+ def plot(self):
+ x, y = self.properties["coordinate"]
+ self.axes.plot(x, y, 'o')
+
+
+class Measurement:
+ def __init__(self, container, plotcanvas, update=None):
+ self.update = update
+ self.container = container
+ self.frame = None
+ self.label = None
+ self.point1 = None
+ self.point2 = None
+ self.active = False
+ self.plotcanvas = plotcanvas
+ self.click_subscription = None
+ self.move_subscription = None
+
+ def toggle_active(self, *args):
+ if self.active: # Deactivate
+ self.active = False
+ self.container.remove(self.frame)
+ if self.update is not None:
+ self.update()
+ self.plotcanvas.mpl_disconnect(self.click_subscription)
+ self.plotcanvas.mpl_disconnect(self.move_subscription)
+ return False
+ else: # Activate
+ print "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)
+ self.frame = Gtk.Frame()
+ self.frame.set_margin_right(5)
+ self.frame.set_margin_top(3)
+ align = Gtk.Alignment()
+ align.set(0, 0.5, 0, 0)
+ align.set_padding(4, 4, 4, 4)
+ self.label = Gtk.Label()
+ self.label.set_label("Click on a reference point...")
+ abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
+ abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
+ abox.pack_start(self.label, False, False, 0)
+ align.add(abox)
+ self.frame.add(align)
+ self.container.pack_end(self.frame, False, True, 1)
+ self.frame.show_all()
+ return True
+
+ def on_move(self, event):
+ if self.point1 is None:
+ self.label.set_label("Click on a reference point...")
+ else:
+ try:
+ dx = event.xdata - self.point1[0]
+ dy = event.ydata - self.point1[1]
+ d = sqrt(dx**2 + dy**2)
+ self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
+ except TypeError:
+ pass
+ if self.update is not None:
+ self.update()
+
+ def on_click(self, event):
+ if self.point1 is None:
+ self.point1 = (event.xdata, event.ydata)
+ else:
+ self.point2 = copy.copy(self.point1)
+ self.point1 = (event.xdata, event.ydata)
+ self.on_move(event)
+
+
+class PlotCanvas:
+ """
+ Class handling the plotting area in the application.
+ """
+
+ def __init__(self, container):
+ """
+ The constructor configures the Matplotlib figure that
+ will contain all plots, creates the base axes and connects
+ events to the plotting area.
+
+ :param container: The parent container in which to draw plots.
+ :rtype: PlotCanvas
+ """
+ # Options
+ self.x_margin = 15 # pixels
+ self.y_margin = 25 # Pixels
+
+ # Parent container
+ self.container = container
+
+ # Plots go onto a single matplotlib.figure
+ self.figure = Figure(dpi=50) # TODO: dpi needed?
+ self.figure.patch.set_visible(False)
+
+ # These axes show the ticks and grid. No plotting done here.
+ # New axes must have a label, otherwise mpl returns an existing one.
+ self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
+ self.axes.set_aspect(1)
+ self.axes.grid(True)
+
+ # The canvas is the top level container (Gtk.DrawingArea)
+ self.canvas = FigureCanvas(self.figure)
+ self.canvas.set_hexpand(1)
+ self.canvas.set_vexpand(1)
+ self.canvas.set_can_focus(True) # For key press
+
+ # Attach to parent
+ self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
+
+ # Events
+ self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
+ self.canvas.connect('configure-event', self.auto_adjust_axes)
+ self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
+ self.canvas.connect("scroll-event", self.on_scroll)
+ self.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 1500fffa..0e565b72 100644
--- a/FlatCAMObj.py
+++ b/FlatCAMObj.py
@@ -11,7 +11,32 @@ from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import GObject
+import inspect # TODO: Remove
+
+from FlatCAMApp import *
from camlib import *
+from ObjectUI import *
+
+
+class LoudDict(dict):
+ def __init__(self, *args, **kwargs):
+ super(LoudDict, self).__init__(*args, **kwargs)
+ self.callback = lambda x: None
+ self.silence = False
+
+ def set_change_callback(self, callback):
+ if self.silence:
+ return
+ self.callback = callback
+
+ def __setitem__(self, key, value):
+ super(LoudDict, self).__setitem__(key, value)
+ try:
+ if self.__getitem__(key) == value:
+ return
+ except KeyError:
+ pass
+ self.callback(key)
########################################
@@ -28,19 +53,63 @@ class FlatCAMObj(GObject.GObject, object):
# The app should set this value.
app = None
- def __init__(self, name):
+ # name = GObject.property(type=str)
+
+ def __init__(self, name, ui):
+ """
+
+ :param name: Name of the object given by the user.
+ :param ui: User interface to interact with the object.
+ :type ui: ObjectUI
+ :return: FlatCAMObj
+ """
GObject.GObject.__init__(self)
- self.options = {"name": name}
- self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
+ # View
+ self.ui = ui
+
+ self.options = LoudDict(name=name)
+ self.options.set_change_callback(self.on_options_change)
+
+ self.form_fields = {"name": self.ui.name_entry}
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
+ self.muted_ui = False
+
+ self.ui.name_entry.connect('activate', self.on_name_activate)
+ self.ui.offset_button.connect('clicked', self.on_offset_button_click)
+ self.ui.offset_button.connect('activate', self.on_offset_button_click)
+ self.ui.scale_button.connect('clicked', self.on_scale_button_click)
+ self.ui.scale_button.connect('activate', self.on_scale_button_click)
+
def __str__(self):
return "".format(self.kind, self.options["name"])
+ def on_name_activate(self, *args):
+ old_name = copy(self.options["name"])
+ new_name = self.ui.name_entry.get_text()
+ self.options["name"] = self.ui.name_entry.get_text()
+ self.app.info("Name changed from %s to %s" % (old_name, new_name))
+
+ def on_offset_button_click(self, *args):
+ self.read_form()
+ vect = self.ui.offsetvector_entry.get_value()
+ self.offset(vect)
+ self.plot()
+
+ def on_scale_button_click(self, *args):
+ self.read_form()
+ factor = self.ui.scale_entry.get_value()
+ self.scale(factor)
+ self.plot()
+
+ def on_options_change(self, key):
+ self.form_fields[key].set_value(self.options[key])
+ return
+
def setup_axes(self, figure):
"""
1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
@@ -89,6 +158,7 @@ class FlatCAMObj(GObject.GObject, object):
:return: None
:rtype: None
"""
+ print inspect.stack()[1][3], "--> FlatCAMObj.read_form()"
for option in self.options:
self.read_form_item(option)
@@ -100,27 +170,25 @@ class FlatCAMObj(GObject.GObject, object):
:rtype: None
"""
+ self.muted_ui = True
+ print inspect.stack()[1][3], "--> FlatCAMObj.build_ui()"
+
# Where the UI for this object is drawn
- box_selected = self.app.builder.get_object("box_selected")
+ # box_selected = self.app.builder.get_object("box_selected")
+ box_selected = self.app.builder.get_object("vp_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)
+ # box_selected.pack_start(sw, True, True, 0)
+ box_selected.add(self.ui)
self.to_form()
- sw.show()
+ box_selected.show_all()
+ self.ui.show()
+ self.muted_ui = False
def set_form_item(self, option):
"""
@@ -130,19 +198,11 @@ class FlatCAMObj(GObject.GObject, object):
:type option: str
:return: None
"""
- fkind = self.form_kinds[option]
- fname = fkind + "_" + self.kind + "_" + option
- if fkind == 'entry_eval' or fkind == 'entry_text':
- self.app.builder.get_object(fname).set_text(str(self.options[option]))
- return
- if fkind == 'cb':
- self.app.builder.get_object(fname).set_active(self.options[option])
- return
- if fkind == 'radio':
- self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
- return
- print "Unknown kind of form item:", fkind
+ 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)
def read_form_item(self, option):
"""
@@ -152,22 +212,11 @@ class FlatCAMObj(GObject.GObject, object):
:type option: str
:return: None
"""
- fkind = self.form_kinds[option]
- fname = fkind + "_" + self.kind + "_" + option
- if fkind == 'entry_text':
- self.options[option] = self.app.builder.get_object(fname).get_text()
- return
- if fkind == 'entry_eval':
- self.options[option] = self.app.get_eval(fname)
- return
- if fkind == 'cb':
- self.options[option] = self.app.builder.get_object(fname).get_active()
- return
- if fkind == 'radio':
- self.options[option] = self.app.get_radio_value(self.radios[option])
- return
- print "Unknown kind of form item:", fkind
+ try:
+ self.options[option] = self.form_fields[option].get_value()
+ except KeyError:
+ App.log.warning("Failed to read option from field: %s" % option)
def plot(self):
"""
@@ -221,14 +270,31 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def __init__(self, name):
Gerber.__init__(self)
- FlatCAMObj.__init__(self, name)
+ FlatCAMObj.__init__(self, name, GerberObjectUI())
self.kind = "gerber"
+ self.form_fields.update({
+ "plot": self.ui.plot_cb,
+ "multicolored": self.ui.multicolored_cb,
+ "solid": self.ui.solid_cb,
+ "isotooldia": self.ui.iso_tool_dia_entry,
+ "isopasses": self.ui.iso_width_entry,
+ "isooverlap": self.ui.iso_overlap_entry,
+ "cutouttooldia": self.ui.cutout_tooldia_entry,
+ "cutoutmargin": self.ui.cutout_margin_entry,
+ "cutoutgapsize": self.ui.cutout_gap_entry,
+ "gaps": self.ui.gaps_radio,
+ "noncoppermargin": self.ui.noncopper_margin_entry,
+ "noncopperrounded": self.ui.noncopper_rounded_cb,
+ "bboxmargin": self.ui.bbmargin_entry,
+ "bboxrounded": self.ui.bbrounded_cb
+ })
+
# The 'name' is already in self.options from FlatCAMObj
+ # Automatically updates the UI
self.options.update({
"plot": True,
- "mergepolys": True,
"multicolored": False,
"solid": False,
"isotooldia": 0.016,
@@ -244,33 +310,137 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"bboxrounded": False
})
- # The 'name' is already in self.form_kinds from FlatCAMObj
- self.form_kinds.update({
- "plot": "cb",
- "mergepolys": "cb",
- "multicolored": "cb",
- "solid": "cb",
- "isotooldia": "entry_eval",
- "isopasses": "entry_eval",
- "isooverlap": "entry_eval",
- "cutouttooldia": "entry_eval",
- "cutoutmargin": "entry_eval",
- "cutoutgapsize": "entry_eval",
- "gaps": "radio",
- "noncoppermargin": "entry_eval",
- "noncopperrounded": "cb",
- "bboxmargin": "entry_eval",
- "bboxrounded": "cb"
- })
-
- self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
- self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
-
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
+ assert isinstance(self.ui, GerberObjectUI)
+ 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.multicolored_cb.connect('clicked', self.on_multicolored_cb_click)
+ self.ui.multicolored_cb.connect('activate', self.on_multicolored_cb_click)
+ self.ui.generate_iso_button.connect('clicked', self.on_iso_button_click)
+ self.ui.generate_iso_button.connect('activate', self.on_iso_button_click)
+ self.ui.generate_cutout_button.connect('clicked', self.on_generatecutout_button_click)
+ self.ui.generate_cutout_button.connect('activate', self.on_generatecutout_button_click)
+ self.ui.generate_bb_button.connect('clicked', self.on_generatebb_button_click)
+ self.ui.generate_bb_button.connect('activate', self.on_generatebb_button_click)
+ self.ui.generate_noncopper_button.connect('clicked', self.on_generatenoncopper_button_click)
+ self.ui.generate_noncopper_button.connect('activate', self.on_generatenoncopper_button_click)
+
+ def on_generatenoncopper_button_click(self, *args):
+ self.read_form()
+ name = self.options["name"] + "_noncopper"
+
+ def geo_init(geo_obj, app_obj):
+ assert isinstance(geo_obj, FlatCAMGeometry)
+ bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
+ if not self.options["noncopperrounded"]:
+ bounding_box = bounding_box.envelope
+ non_copper = bounding_box.difference(self.solid_geometry)
+ geo_obj.solid_geometry = non_copper
+
+ # TODO: Check for None
+ self.app.new_object("geometry", name, geo_init)
+
+ def on_generatebb_button_click(self, *args):
+ self.read_form()
+ name = self.options["name"] + "_bbox"
+
+ def geo_init(geo_obj, app_obj):
+ assert isinstance(geo_obj, FlatCAMGeometry)
+ # Bounding box with rounded corners
+ bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
+ if not self.options["bboxrounded"]: # Remove rounded corners
+ bounding_box = bounding_box.envelope
+ geo_obj.solid_geometry = bounding_box
+
+ self.app.new_object("geometry", name, geo_init)
+
+ def on_generatecutout_button_click(self, *args):
+ self.read_form()
+ name = self.options["name"] + "_cutout"
+
+ def geo_init(geo_obj, app_obj):
+ margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
+ gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
+ minx, miny, maxx, maxy = self.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[self.options['gaps']]
+ geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+
+ # TODO: Check for None
+ self.app.new_object("geometry", name, geo_init)
+
+ def on_iso_button_click(self, *args):
+ self.read_form()
+ dia = self.options["isotooldia"]
+ passes = int(self.options["isopasses"])
+ overlap = self.options["isooverlap"] * dia
+
+ for i in range(passes):
+
+ offset = (2*i + 1)/2.0 * dia - i*overlap
+ iso_name = self.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"] = self.options["isotooldia"]
+
+ geo_obj.solid_geometry = self.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.app.new_object("geometry", iso_name, iso_init)
+
+ def on_plot_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('plot')
+ self.plot()
+
+ def on_solid_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('solid')
+ self.plot()
+
+ def on_multicolored_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('multicolored')
+ self.plot()
+
def convert_units(self, units):
"""
Converts the units of the object by scaling dimensions in all geometry
@@ -354,10 +524,20 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
def __init__(self, name):
Excellon.__init__(self)
- FlatCAMObj.__init__(self, name)
+ FlatCAMObj.__init__(self, name, ExcellonObjectUI())
self.kind = "excellon"
+ self.form_fields.update({
+ "name": self.ui.name_entry,
+ "plot": self.ui.plot_cb,
+ "solid": self.ui.solid_cb,
+ "drillz": self.ui.cutz_entry,
+ "travelz": self.ui.travelz_entry,
+ "feedrate": self.ui.feedrate_entry,
+ "toolselection": self.ui.tools_entry
+ })
+
self.options.update({
"plot": True,
"solid": False,
@@ -367,14 +547,14 @@ 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"
- })
+ # 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 = {}
@@ -384,6 +564,23 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# from predecessors.
self.ser_attrs += ['options', 'kind']
+ 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)
+
+ def on_plot_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('plot')
+ self.plot()
+
+ def on_solid_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('solid')
+ self.plot()
+
def convert_units(self, units):
factor = Excellon.convert_units(self, units)
@@ -457,7 +654,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
feedrate=3.0, z_cut=-0.002, tooldia=0.0):
CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
- FlatCAMObj.__init__(self, name)
+ FlatCAMObj.__init__(self, name, CNCObjectUI())
self.kind = "cncjob"
@@ -466,16 +663,31 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"tooldia": 0.4 / 25.4 # 0.4mm in inches
})
- self.form_kinds.update({
- "plot": "cb",
- "tooldia": "entry_eval"
+ self.form_fields.update({
+ "name": self.ui.name_entry,
+ "plot": self.ui.plot_cb,
+ "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.
self.ser_attrs += ['options', 'kind']
+ self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+ self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+
+ def on_plot_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('plot')
+ self.plot()
+
def plot(self):
# Does all the required setup and returns False
@@ -501,15 +713,29 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"""
def __init__(self, name):
- FlatCAMObj.__init__(self, name)
+ FlatCAMObj.__init__(self, name, GeometryObjectUI())
Geometry.__init__(self)
self.kind = "geometry"
+ self.form_fields.update({
+ "name": self.ui.name_entry,
+ "plot": self.ui.plot_cb,
+ # "solid": self.ui.sol,
+ # "multicolored": self.ui.,
+ "cutz": self.ui.cutz_entry,
+ "travelz": self.ui.travelz_entry,
+ "feedrate": self.ui.cncfeedrate_entry,
+ "cnctooldia": self.ui.cnctooldia_entry,
+ "painttooldia": self.ui.painttooldia_entry,
+ "paintoverlap": self.ui.paintoverlap_entry,
+ "paintmargin": self.ui.paintmargin_entry
+ })
+
self.options.update({
"plot": True,
- "solid": False,
- "multicolored": False,
+ # "solid": False,
+ # "multicolored": False,
"cutz": -0.002,
"travelz": 0.1,
"feedrate": 5.0,
@@ -519,24 +745,105 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"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"
- })
+ # self.form_kinds.update({
+ # "plot": "cb",
+ # "solid": "cb",
+ # "multicolored": "cb",
+ # "cutz": "entry_eval",
+ # "travelz": "entry_eval",
+ # "feedrate": "entry_eval",
+ # "cnctooldia": "entry_eval",
+ # "painttooldia": "entry_eval",
+ # "paintoverlap": "entry_eval",
+ # "paintmargin": "entry_eval"
+ # })
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
+ assert isinstance(self.ui, GeometryObjectUI)
+ self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+ self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+ self.ui.generate_cnc_button.connect('clicked', self.on_generatecnc_button_click)
+ self.ui.generate_cnc_button.connect('activate', self.on_generatecnc_button_click)
+ self.ui.generate_paint_button.connect('clicked', self.on_paint_button_click)
+ self.ui.generate_paint_button.connect('activate', self.on_paint_button_click)
+
+ def on_paint_button_click(self, *args):
+ self.app.info("Click inside the desired polygon.")
+ self.read_form()
+ tooldia = self.options["painttooldia"]
+ overlap = self.options["paintoverlap"]
+
+ # Connection ID for the click event
+ subscription = None
+
+ # To be called after clicking on the plot.
+ def doit(event):
+ self.app.plotcanvas.mpl_disconnect(subscription)
+ point = [event.xdata, event.ydata]
+ poly = find_polygon(self.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(-self.options["paintmargin"]), tooldia, overlap)
+ geo_obj.solid_geometry = cp
+ geo_obj.options["cnctooldia"] = tooldia
+
+ name = self.options["name"] + "_paint"
+ self.new_object("geometry", name, gen_paintarea)
+
+ subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
+
+ def on_generatecnc_button_click(self, *args):
+ self.read_form()
+ job_name = self.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"] = self.options["cnctooldia"]
+
+ GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+ job_obj.z_cut = self.options["cutz"]
+ job_obj.z_move = self.options["travelz"]
+ job_obj.feedrate = self.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(self, 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, "Idle"))
+
+ # Send to worker
+ self.app.worker.add_task(job_thread, [self.app])
+
+ def on_plot_cb_click(self, *args):
+ if self.muted_ui:
+ return
+ self.read_form_item('plot')
+ self.plot()
+
def scale(self, factor):
"""
Scales all geometry by a given factor.
diff --git a/GUIElements.py b/GUIElements.py
new file mode 100644
index 00000000..f067677e
--- /dev/null
+++ b/GUIElements.py
@@ -0,0 +1,143 @@
+from gi.repository import Gtk
+import re
+from copy import copy
+
+
+class RadioSet(Gtk.Box):
+ def __init__(self, choices):
+ """
+ The choices are specified as a list of dictionaries containing:
+
+ * 'label': Shown in the UI
+ * 'value': The value returned is selected
+
+ :param choices: List of choices. See description.
+ :type choices: list
+ """
+ Gtk.Box.__init__(self)
+ self.choices = copy(choices)
+ self.group = None
+ for choice in self.choices:
+ if self.group is None:
+ choice['radio'] = Gtk.RadioButton.new_with_label(None, choice['label'])
+ self.group = choice['radio']
+ 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)
+
+ # def on_toggle(self, *args):
+ # 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."
+ return None
+
+ def set_value(self, val):
+ for choice in self.choices:
+ if choice['value'] == val:
+ choice['radio'].set_active(True)
+ return
+ print "ERROR: Value given is not part of this RadioSet:", val
+
+
+class LengthEntry(Gtk.Entry):
+ def __init__(self, output_units='IN'):
+ Gtk.Entry.__init__(self)
+ self.output_units = output_units
+ self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
+
+ # Unit conversion table OUTPUT-INPUT
+ self.scales = {
+ 'IN': {'MM': 1/25.4},
+ 'MM': {'IN': 25.4}
+ }
+
+ self.connect('activate', self.on_activate)
+
+ def on_activate(self, *args):
+ val = self.get_value()
+ if val is not None:
+ self.set_text(str(val))
+ else:
+ print "WARNING: Could not interpret entry:", self.get_text()
+
+ def get_value(self):
+ raw = self.get_text().strip(' ')
+ match = self.format_re.search(raw)
+ if not match:
+ return None
+ try:
+ if match.group(2) is not None and match.group(2).upper() in self.scales:
+ return float(match.group(1))*self.scales[self.output_units][match.group(2).upper()]
+ else:
+ return float(match.group(1))
+ except:
+ print "ERROR: Could not parse value in entry:", raw
+ return None
+
+ def set_value(self, val):
+ self.set_text(str(val))
+
+
+class FloatEntry(Gtk.Entry):
+ def __init__(self):
+ Gtk.Entry.__init__(self)
+
+ self.connect('activate', self.on_activate)
+
+ def on_activate(self, *args):
+ val = self.get_value()
+ if val is not None:
+ self.set_text(str(val))
+ else:
+ print "WARNING: Could not interpret entry:", self.get_text()
+
+ def get_value(self):
+ raw = self.get_text().strip(' ')
+ try:
+ evaled = eval(raw)
+ except:
+ print "ERROR: Could not evaluate:", raw
+ return None
+
+ return float(evaled)
+
+ def set_value(self, val):
+ self.set_text(str(val))
+
+
+class IntEntry(Gtk.Entry):
+ def __init__(self):
+ Gtk.Entry.__init__(self)
+
+ def get_value(self):
+ return int(self.get_text())
+
+ def set_value(self, val):
+ self.set_text(str(val))
+
+
+class FCEntry(Gtk.Entry):
+ def __init__(self):
+ Gtk.Entry.__init__(self)
+
+ def get_value(self):
+ return self.get_text()
+
+ def set_value(self, val):
+ self.set_text(str(val))
+
+
+class FCCheckBox(Gtk.CheckButton):
+ def __init__(self, label=''):
+ Gtk.CheckButton.__init__(self, label=label)
+
+ def get_value(self):
+ return self.get_active()
+
+ def set_value(self, val):
+ self.set_active(val)
\ No newline at end of file
diff --git a/ObjectCollection.py b/ObjectCollection.py
new file mode 100644
index 00000000..4aefc32c
--- /dev/null
+++ b/ObjectCollection.py
@@ -0,0 +1,254 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing #
+# http://caram.cl/software/flatcam #
+# Author: Juan Pablo Caram (c) #
+# Date: 4/20/2014 #
+# MIT Licence #
+############################################################
+
+from FlatCAMObj import *
+from gi.repository import Gtk, GdkPixbuf
+import inspect # TODO: Remove
+
+
+class ObjectCollection:
+
+ classdict = {
+ "gerber": FlatCAMGerber,
+ "excellon": FlatCAMExcellon,
+ "cncjob": FlatCAMCNCjob,
+ "geometry": FlatCAMGeometry
+ }
+
+ icon_files = {
+ "gerber": "share/flatcam_icon16.png",
+ "excellon": "share/drill16.png",
+ "cncjob": "share/cnc16.png",
+ "geometry": "share/geometry16.png"
+ }
+
+ def __init__(self):
+
+ ### Icons for the list view
+ self.icons = {}
+ for kind in ObjectCollection.icon_files:
+ self.icons[kind] = GdkPixbuf.Pixbuf.new_from_file(ObjectCollection.icon_files[kind])
+
+ ### GUI List components
+ ## Model
+ self.store = Gtk.ListStore(FlatCAMObj)
+
+ ## View
+ self.view = Gtk.TreeView(model=self.store)
+ #self.view.connect("row_activated", self.on_row_activated)
+ self.tree_selection = self.view.get_selection()
+ self.change_subscription = self.tree_selection.connect("changed", self.on_list_selection_change)
+
+ ## Renderers
+ # Icon
+ renderer_pixbuf = Gtk.CellRendererPixbuf()
+ column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf)
+
+ def _set_cell_icon(column, cell, model, it, data):
+ obj = model.get_value(it, 0)
+ cell.set_property('pixbuf', self.icons[obj.kind])
+
+ column_pixbuf.set_cell_data_func(renderer_pixbuf, _set_cell_icon)
+ self.view.append_column(column_pixbuf)
+
+ # Name
+ renderer_text = Gtk.CellRendererText()
+ column_text = Gtk.TreeViewColumn("Name", renderer_text)
+
+ def _set_cell_text(column, cell, model, it, data):
+ obj = model.get_value(it, 0)
+ cell.set_property('text', obj.options["name"])
+
+ column_text.set_cell_data_func(renderer_text, _set_cell_text)
+ self.view.append_column(column_text)
+
+ def print_list(self):
+ iterat = self.store.get_iter_first()
+ while iterat is not None:
+ obj = self.store[iterat][0]
+ print obj
+ iterat = self.store.iter_next(iterat)
+
+ def delete_all(self):
+ print "OC.delete_all()"
+ self.store.clear()
+
+ def delete_active(self):
+ print "OC.delete_active()"
+ try:
+ model, treeiter = self.tree_selection.get_selected()
+ self.store.remove(treeiter)
+ except:
+ pass
+
+ def on_list_selection_change(self, selection):
+ """
+ Callback for change in selection on the objects' list.
+ Instructs the new selection to build the UI for its options.
+
+ :param selection: Ignored.
+ :return: None
+ """
+ print inspect.stack()[1][3], "--> OC.on_list_selection_change()"
+ try:
+ self.get_active().build_ui()
+ except AttributeError: # For None being active
+ pass
+
+ def set_active(self, name):
+ """
+ Sets an object as the active object in the program. Same
+ as `set_list_selection()`.
+
+ :param name: Name of the object.
+ :type name: str
+ :return: None
+ """
+ print "OC.set_active()"
+ self.set_list_selection(name)
+
+ def get_active(self):
+ print inspect.stack()[1][3], "--> OC.get_active()"
+ try:
+ model, treeiter = self.tree_selection.get_selected()
+ return model[treeiter][0]
+ except (TypeError, ValueError):
+ return None
+
+ def set_list_selection(self, name):
+ """
+ Sets which object should be selected in the list.
+
+ :param name: Name of the object.
+ :rtype name: str
+ :return: None
+ """
+ print 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)
+ self.tree_selection.select_iter(iterat)
+
+ def append(self, obj, active=False):
+ """
+ Add a FlatCAMObj the the collection. This method is thread-safe.
+
+ :param obj: FlatCAMObj to append
+ :type obj: FlatCAMObj
+ :param active: If it is to become the active object after appending
+ :type active: bool
+ :return: None
+ """
+ print inspect.stack()[1][3], "--> OC.append()"
+
+ def guitask():
+ self.store.append([obj])
+ if active:
+ self.set_list_selection(obj.options["name"])
+ GLib.idle_add(guitask)
+
+ def get_names(self):
+ """
+ Gets a list of the names of all objects in the collection.
+
+ :return: List of names.
+ :rtype: list
+ """
+ print "OC.get_names()"
+ names = []
+ iterat = self.store.get_iter_first()
+ while iterat is not None:
+ obj = self.store[iterat][0]
+ names.append(obj.options["name"])
+ iterat = self.store.iter_next(iterat)
+ return names
+
+ def get_bounds(self):
+ """
+ Finds coordinates bounding all objects in the collection.
+
+ :return: [xmin, ymin, xmax, ymax]
+ :rtype: list
+ """
+ print "OC.get_bounds()"
+
+ # TODO: Move the operation out of here.
+
+ xmin = Inf
+ ymin = Inf
+ xmax = -Inf
+ ymax = -Inf
+
+ iterat = self.store.get_iter_first()
+ while iterat is not None:
+ obj = self.store[iterat][0]
+ try:
+ gxmin, gymin, gxmax, gymax = obj.bounds()
+ xmin = min([xmin, gxmin])
+ ymin = min([ymin, gymin])
+ xmax = max([xmax, gxmax])
+ ymax = max([ymax, gymax])
+ except:
+ print "DEV WARNING: Tried to get bounds of empty geometry."
+ iterat = self.store.iter_next(iterat)
+ return [xmin, ymin, xmax, ymax]
+
+ def get_list(self):
+ """
+ Returns a list with all FlatCAMObj.
+
+ :return: List with all FlatCAMObj.
+ :rtype: list
+ """
+ collection_list = []
+ iterat = self.store.get_iter_first()
+ while iterat is not None:
+ obj = self.store[iterat][0]
+ collection_list.append(obj)
+ iterat = self.store.iter_next(iterat)
+ return collection_list
+
+ def get_by_name(self, name):
+ """
+ Fetches the FlatCAMObj with the given `name`.
+
+ :param name: The name of the object.
+ :type name: str
+ :return: The requested object or None if no such object.
+ :rtype: FlatCAMObj or None
+ """
+ iterat = self.store.get_iter_first()
+ while iterat is not None:
+ obj = self.store[iterat][0]
+ if obj.options["name"] == name:
+ return obj
+ iterat = self.store.iter_next(iterat)
+ return None
+
+ # def change_name(self, old_name, new_name):
+ # """
+ # Changes the name of `FlatCAMObj` named `old_name` to `new_name`.
+ #
+ # :param old_name: Name of the object to change.
+ # :type old_name: str
+ # :param new_name: New name.
+ # :type new_name: str
+ # :return: True if name change succeeded, False otherwise. Will fail
+ # if no object with `old_name` is found.
+ # :rtype: bool
+ # """
+ # print inspect.stack()[1][3], "--> OC.change_name()"
+ # iterat = self.store.get_iter_first()
+ # while iterat is not None:
+ # obj = self.store[iterat][0]
+ # if obj.options["name"] == old_name:
+ # obj.options["name"] = new_name
+ # self.store.row_changed(0, iterat)
+ # return True
+ # iterat = self.store.iter_next(iterat)
+ # return False
\ No newline at end of file
diff --git a/ObjectUI.py b/ObjectUI.py
new file mode 100644
index 00000000..7b6fa15c
--- /dev/null
+++ b/ObjectUI.py
@@ -0,0 +1,375 @@
+
+from gi.repository import Gtk
+import re
+from copy import copy
+
+from GUIElements import *
+
+
+class ObjectUI(Gtk.VBox):
+ def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object'):
+ Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+ ## Page Title box (spacing between children)
+ self.title_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
+ self.pack_start(self.title_box, expand=False, fill=False, padding=2)
+
+ ## Page Title icon
+ self.icon = Gtk.Image.new_from_file(icon_file)
+ self.title_box.pack_start(self.icon, expand=False, fill=False, padding=2)
+
+ ## Title label
+ self.title_label = Gtk.Label()
+ self.title_label.set_markup("" + title + "")
+ self.title_label.set_justify(Gtk.Justification.CENTER)
+ self.title_box.pack_start(self.title_label, expand=False, fill=False, padding=2)
+
+ ## Object name
+ self.name_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
+ self.pack_start(self.name_box, expand=True, fill=False, padding=2)
+ name_label = Gtk.Label('Name:')
+ name_label.set_justify(Gtk.Justification.RIGHT)
+ self.name_box.pack_start(name_label,
+ expand=False, fill=False, padding=2)
+ self.name_entry = FCEntry()
+ self.name_box.pack_start(self.name_entry, expand=True, fill=False, padding=2)
+
+ ## Box box for custom widgets
+ self.custom_box = Gtk.VBox(spacing=3, margin=0, vexpand=False)
+ self.pack_start(self.custom_box, expand=False, fill=False, padding=0)
+
+ ## Common to all objects
+ ## 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)
+
+ grid5 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid5, expand=True, fill=False, padding=2)
+
+ # Factor
+ l10 = Gtk.Label('Factor:', xalign=1)
+ grid5.attach(l10, 0, 0, 1, 1)
+ self.scale_entry = FloatEntry()
+ self.scale_entry.set_text("1.0")
+ grid5.attach(self.scale_entry, 1, 0, 1, 1)
+
+ # GO Button
+ self.scale_button = Gtk.Button(label='Scale')
+ self.pack_start(self.scale_button, expand=True, 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)
+
+ grid6 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid6, expand=True, 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.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)
+
+ def set_field(self, name, value):
+ getattr(self, name).set_value(value)
+
+ def get_field(self, name):
+ return getattr(self, name).get_value()
+
+
+class CNCObjectUI(ObjectUI):
+ 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)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 2, 1)
+
+ # Tool dia for plot
+ l1 = Gtk.Label('Tool dia:', xalign=1)
+ grid0.attach(l1, 0, 1, 1, 1)
+ self.tooldia_entry = LengthEntry()
+ grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+
+ # Update plot button
+ self.updateplot_button = Gtk.Button(label='Update Plot')
+ self.pack_start(self.updateplot_button, expand=True, 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)
+
+ # 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)
+
+
+class GeometryObjectUI(ObjectUI):
+ def __init__(self):
+ ObjectUI.__init__(self, title='Geometry Object', icon_file='share/geometry32.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.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ ## Create CNC Job
+ self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.cncjob_label.set_markup('Create CNC Job:')
+ self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+ grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+
+ # Cut Z
+ l1 = Gtk.Label('Cut Z:', xalign=1)
+ grid1.attach(l1, 0, 0, 1, 1)
+ self.cutz_entry = LengthEntry()
+ grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+ # Travel Z
+ l2 = Gtk.Label('Travel Z:', xalign=1)
+ grid1.attach(l2, 0, 1, 1, 1)
+ self.travelz_entry = LengthEntry()
+ grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Feed rate:', xalign=1)
+ grid1.attach(l3, 0, 2, 1, 1)
+ self.cncfeedrate_entry = LengthEntry()
+ grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
+
+ l4 = Gtk.Label('Tool dia:', xalign=1)
+ grid1.attach(l4, 0, 3, 1, 1)
+ self.cnctooldia_entry = LengthEntry()
+ grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
+
+ self.generate_cnc_button = Gtk.Button(label='Generate')
+ self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+
+ ## Paint Area
+ self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.paint_label.set_markup('Paint Area:')
+ self.custom_box.pack_start(self.paint_label, expand=True, fill=False, padding=2)
+
+ grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+
+ # Tool dia
+ l5 = Gtk.Label('Tool dia:', xalign=1)
+ grid2.attach(l5, 0, 0, 1, 1)
+ self.painttooldia_entry = LengthEntry()
+ grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+
+ # Overlap
+ l6 = Gtk.Label('Overlap:', xalign=1)
+ grid2.attach(l6, 0, 1, 1, 1)
+ self.paintoverlap_entry = LengthEntry()
+ grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+
+ # Margin
+ l7 = Gtk.Label('Margin:', xalign=1)
+ grid2.attach(l7, 0, 2, 1, 1)
+ self.paintmargin_entry = LengthEntry()
+ grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+
+ # GO Button
+ self.generate_paint_button = Gtk.Button(label='Generate')
+ self.custom_box.pack_start(self.generate_paint_button, expand=True, fill=False, padding=2)
+
+
+class ExcellonObjectUI(ObjectUI):
+ def __init__(self):
+ ObjectUI.__init__(self, title='Excellon Object', icon_file='share/drill32.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.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ self.solid_cb = FCCheckBox(label='Solid')
+ grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+ ## Create CNC Job
+ self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.cncjob_label.set_markup('Create CNC Job')
+ self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+ grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+
+ l1 = Gtk.Label('Cut Z:', xalign=1)
+ grid1.attach(l1, 0, 0, 1, 1)
+ self.cutz_entry = LengthEntry()
+ grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+ l2 = Gtk.Label('Travel Z:', xalign=1)
+ grid1.attach(l2, 0, 1, 1, 1)
+ self.travelz_entry = LengthEntry()
+ grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Feed rate:', xalign=1)
+ grid1.attach(l3, 0, 2, 1, 1)
+ self.feedrate_entry = LengthEntry()
+ grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
+
+ l4 = Gtk.Label('Tools:', xalign=1)
+ grid1.attach(l4, 0, 3, 1, 1)
+ boxt = Gtk.Box()
+ grid1.attach(boxt, 1, 3, 1, 1)
+ self.tools_entry = FCEntry()
+ boxt.pack_start(self.tools_entry, expand=True, fill=False, padding=2)
+ self.choose_tools_button = Gtk.Button(label='Choose...')
+ boxt.pack_start(self.choose_tools_button, expand=True, fill=False, padding=2)
+
+ self.generate_cnc_button = Gtk.Button(label='Generate')
+ self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+
+
+class GerberObjectUI(ObjectUI):
+ def __init__(self):
+ ObjectUI.__init__(self, title='Gerber Object')
+
+ ## 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.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+ grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+ # Plot CB
+ self.plot_cb = FCCheckBox(label='Plot')
+ grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+ # Solid CB
+ self.solid_cb = FCCheckBox(label='Solid')
+ grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+ # Multicolored CB
+ self.multicolored_cb = FCCheckBox(label='Multicolored')
+ grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+
+ ## Isolation Routing
+ self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.isolation_routing_label.set_markup("Isolation Routing:")
+ self.custom_box.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+ grid = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid, expand=True, fill=False, padding=2)
+
+ l1 = Gtk.Label('Tool diam:', xalign=1)
+ grid.attach(l1, 0, 0, 1, 1)
+ self.iso_tool_dia_entry = LengthEntry()
+ grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
+
+ l2 = Gtk.Label('Width (# passes):', xalign=1)
+ grid.attach(l2, 0, 1, 1, 1)
+ self.iso_width_entry = IntEntry()
+ grid.attach(self.iso_width_entry, 1, 1, 1, 1)
+
+ l3 = Gtk.Label('Pass overlap:', xalign=1)
+ grid.attach(l3, 0, 2, 1, 1)
+ self.iso_overlap_entry = FloatEntry()
+ grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
+
+ self.generate_iso_button = Gtk.Button(label='Generate Geometry')
+ self.custom_box.pack_start(self.generate_iso_button, expand=True, fill=False, padding=2)
+
+ ## Board cuttout
+ self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.isolation_routing_label.set_markup("Board cutout:")
+ self.custom_box.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+ grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+
+ l4 = Gtk.Label('Tool dia:', xalign=1)
+ grid2.attach(l4, 0, 0, 1, 1)
+ self.cutout_tooldia_entry = LengthEntry()
+ grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
+
+ l5 = Gtk.Label('Margin:', xalign=1)
+ grid2.attach(l5, 0, 1, 1, 1)
+ self.cutout_margin_entry = LengthEntry()
+ grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
+
+ l6 = Gtk.Label('Gap size:', xalign=1)
+ grid2.attach(l6, 0, 2, 1, 1)
+ self.cutout_gap_entry = LengthEntry()
+ grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
+
+ l7 = Gtk.Label('Gaps:', xalign=1)
+ grid2.attach(l7, 0, 3, 1, 1)
+ self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+ {'label': '2 (L/R)', 'value': 'lr'},
+ {'label': '4', 'value': '4'}])
+ grid2.attach(self.gaps_radio, 1, 3, 1, 1)
+
+ self.generate_cutout_button = Gtk.Button(label='Generate Geometry')
+ self.custom_box.pack_start(self.generate_cutout_button, expand=True, fill=False, padding=2)
+
+ ## Non-copper regions
+ self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.noncopper_label.set_markup("Non-copper regions:")
+ self.custom_box.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
+
+ grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid3, expand=True, fill=False, padding=2)
+
+ l8 = Gtk.Label('Boundary margin:', xalign=1)
+ grid3.attach(l8, 0, 0, 1, 1)
+ self.noncopper_margin_entry = LengthEntry()
+ grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
+
+ self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+ grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
+
+ self.generate_noncopper_button = Gtk.Button(label='Generate Geometry')
+ self.custom_box.pack_start(self.generate_noncopper_button, expand=True, fill=False, padding=2)
+
+ ## Bounding box
+ self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+ self.boundingbox_label.set_markup('Bounding Box:')
+ self.custom_box.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
+
+ grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
+ self.custom_box.pack_start(grid4, expand=True, fill=False, padding=2)
+
+ l9 = Gtk.Label('Boundary Margin:', xalign=1)
+ grid4.attach(l9, 0, 0, 1, 1)
+ self.bbmargin_entry = LengthEntry()
+ grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
+
+ self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+ grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
+
+ self.generate_bb_button = Gtk.Button(label='Generate Geometry')
+ self.custom_box.pack_start(self.generate_bb_button, expand=True, fill=False, padding=2)
\ No newline at end of file
diff --git a/camlib.py b/camlib.py
index 8aa2e085..630cf18f 100644
--- a/camlib.py
+++ b/camlib.py
@@ -554,40 +554,6 @@ class Gerber (Geometry):
| others | Depend on ``type`` |
+-----------+-----------------------------------+
- * ``paths`` (list): A path is described by a line an aperture that follows that
- line. Each paths[i] is a dictionary:
-
- +------------+------------------------------------------------+
- | Key | Value |
- +============+================================================+
- | linestring | (Shapely.LineString) The actual path. |
- +------------+------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+------------------------------------------------+
-
- * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
- is a dictionary:
-
- +------------+------------------------------------------------+
- | Key | Value |
- +============+================================================+
- | loc | (Point) Shapely Point indicating location. |
- +------------+------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+------------------------------------------------+
-
- * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
- which have an exterior and zero or more interiors. An aperture is also
- associated with a region. Each is a dictionary:
-
- +------------+-----------------------------------------------------+
- | Key | Value |
- +============+=====================================================+
- | polygon | (Shapely.Polygon) The polygon defining the region. |
- +------------+-----------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+-----------------------------------------------------+
-
* ``aperture_macros`` (dictionary): Are predefined geometrical structures
that can be instanciated with different parameters in an aperture
definition. See ``apertures`` above. The key is the name of the macro,
@@ -635,23 +601,6 @@ class Gerber (Geometry):
# ['size':float], ['width':float],
# ['height':float]}, ...}
self.apertures = {}
-
- # Paths [{'linestring':LineString, 'aperture':str}]
- # self.paths = []
-
- # Buffered Paths [Polygon]
- # Paths transformed into Polygons by
- # offsetting the aperture size/2
- # self.buffered_paths = []
-
- # Polygon regions [{'polygon':Polygon, 'aperture':str}]
- # self.regions = []
-
- # Flashes [{'loc':[float,float], 'aperture':str}]
- # self.flashes = []
-
- # Geometry from flashes
- # self.flash_geometry = []
# Aperture Macros
self.aperture_macros = {}
@@ -659,9 +608,8 @@ class Gerber (Geometry):
# Attributes to be included in serialization
# Always append to it because it carries contents
# from Geometry.
- self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
- 'buffered_paths', 'regions', 'flashes',
- 'flash_geometry', 'aperture_macros']
+ self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
+ 'aperture_macros', 'solid_geometry']
#### Parser patterns ####
# FS - Format Specification
@@ -1432,8 +1380,8 @@ class Excellon(Geometry):
self.drills = []
- # Trailing "T" or leading "L"
- self.zeros = ""
+ # Trailing "T" or leading "L" (default)
+ self.zeros = "L"
# Attributes to be included in serialization
# Always append to it because it carries contents
@@ -1504,6 +1452,9 @@ class Excellon(Geometry):
# Various stop/pause commands
self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
+
+ # Parse coordinates
+ self.leadingzeros_re = re.compile(r'^(0*)(\d*)')
def parse_file(self, filename):
"""
@@ -1538,7 +1489,7 @@ class Excellon(Geometry):
for eline in elines:
line_num += 1
- ### Cleanup
+ ### Cleanup lines
eline = eline.strip(' \r\n')
## Header Begin/End ##
@@ -1563,13 +1514,15 @@ class Excellon(Geometry):
match = self.coordsnoperiod_re.search(eline)
if match:
try:
- x = float(match.group(1))/10000
+ #x = float(match.group(1))/10000
+ x = self.parse_number(match.group(1))
current_x = x
except TypeError:
x = current_x
try:
- y = float(match.group(2))/10000
+ #y = float(match.group(2))/10000
+ y = self.parse_number(match.group(2))
current_y = y
except TypeError:
y = current_y
@@ -1581,7 +1534,7 @@ class Excellon(Geometry):
self.drills.append({'point': Point((x, y)), 'tool': current_tool})
continue
- ## Coordinates with period ##
+ ## Coordinates with period: Use literally. ##
match = self.coordsperiod_re.search(eline)
if match:
try:
@@ -1630,6 +1583,21 @@ class Excellon(Geometry):
print "WARNING: Line ignored:", eline
+ def parse_number(self, number_str):
+ """
+ Parses coordinate numbers without period.
+
+ :param number_str: String representing the numerical value.
+ :type number_str: str
+ :return: Floating point representation of the number
+ :rtype: foat
+ """
+ if self.zeros == "L":
+ match = self.leadingzeros_re.search(number_str)
+ return float(number_str)/(10**(len(match.group(2))-2+len(match.group(1))))
+ else: # Trailing
+ return float(number_str)/10000
+
def create_geometry(self):
"""
Creates circles of the tool diameter at every point
@@ -2443,33 +2411,3 @@ def parse_gerber_number(strnumber, frac_digits):
"""
return int(strnumber)*(10**(-frac_digits))
-
-def parse_gerber_coords(gstr, int_digits, frac_digits):
- """
- Parse Gerber coordinates
-
- :param gstr: Line of G-Code containing coordinates.
- :type gstr: str
- :param int_digits: Number of digits in integer part of a number.
- :type int_digits: int
- :param frac_digits: Number of digits in frac_digits part of a number.
- :type frac_digits: int
- :return: [x, y] coordinates.
- :rtype: list
- """
- global gerbx, gerby
- xindex = gstr.find("X")
- yindex = gstr.find("Y")
- index = gstr.find("D")
- if xindex == -1:
- x = gerbx
- y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
- elif yindex == -1:
- y = gerby
- x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
- else:
- x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
- y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
- gerbx = x
- gerby = y
- return [x, y]
diff --git a/defaults.json b/defaults.json
index 78934a07..9c29c686 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, "gerber_mergepolys": 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, "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
diff --git a/doc/build/app.html b/doc/build/app.html
index b3a5767a..1b463b34 100644
--- a/doc/build/app.html
+++ b/doc/build/app.html
@@ -1434,8 +1434,8 @@ side of the main window.
--
-versionCheck(*args)
+-
+version_check(*args)
Checks for the latest version of the program. Alerts the
user if theirs is outdated. This method is meant to be run
in a saeparate thread.
diff --git a/doc/build/genindex.html b/doc/build/genindex.html
index 767195ae..25e8b0bd 100644
--- a/doc/build/genindex.html
+++ b/doc/build/genindex.html
@@ -1021,7 +1021,7 @@
- - versionCheck() (FlatCAM.App method)
+
- version_check() (FlatCAM.App method)
|
diff --git a/recent.json b/recent.json
index 3a578ee9..73729efe 100644
--- a/recent.json
+++ b/recent.json
@@ -1 +1 @@
-[{"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"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\bedini 7 coils capacitor discharge.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Gerbers\\AVR_Transistor_Tester_copper_bottom.GBL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Gerbers\\AVR_Transistor_Tester_copper_top.GTL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\maitest.gtl"}]
\ No newline at end of file
+[{"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