diff --git a/FlatCAM.py b/FlatCAM.py index ddeca77..cf5a3f4 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -29,609 +29,9 @@ import urllib import copy import random +from FlatCAMObj import * -######################################## -## FlatCAMObj ## -######################################## -class FlatCAMObj: - """ - Base type of objects handled in FlatCAM. These become interactive - in the GUI, can be plotted, and their options can be modified - by the user in their respective forms. - """ - - # Instance of the application to which these are related. - # The app should set this value. - app = None - - def __init__(self, name): - self.options = {"name": name} - self.form_kinds = {"name": "entry_text"} # Kind of form element for each option - self.radios = {} # Name value pairs for radio sets - self.radios_inv = {} # Inverse of self.radios - self.axes = None # Matplotlib axes - self.kind = None # Override with proper name - - def setup_axes(self, figure): - """ - 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches - them to figure if not part of the figure. 4) Sets transparent - background. 5) Sets 1:1 scale aspect ratio. - - :param figure: A Matplotlib.Figure on which to add/configure axes. - :type figure: matplotlib.figure.Figure - :return: None - :rtype: None - """ - - if self.axes is None: - print "New axes" - self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9], - label=self.options["name"]) - elif self.axes not in figure.axes: - print "Clearing and attaching axes" - self.axes.cla() - figure.add_axes(self.axes) - else: - print "Clearing Axes" - self.axes.cla() - - # Remove all decoration. The app's axes will have - # the ticks and grid. - self.axes.set_frame_on(False) # No frame - self.axes.set_xticks([]) # No tick - self.axes.set_yticks([]) # No ticks - self.axes.patch.set_visible(False) # No background - self.axes.set_aspect(1) - - def to_form(self): - """ - Copies options to the UI form. - - :return: None - """ - for option in self.options: - self.set_form_item(option) - - def read_form(self): - """ - Reads form into ``self.options``. - - :return: None - :rtype: None - """ - for option in self.options: - self.read_form_item(option) - - def build_ui(self): - """ - Sets up the UI/form for this object. - - :return: None - :rtype: None - """ - - # Where the UI for this object is drawn - box_selected = self.app.builder.get_object("box_selected") - - # Remove anything else in the box - box_children = box_selected.get_children() - for child in box_children: - box_selected.remove(child) - - osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow - sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows - osw.remove(sw) # TODO: Is this needed ? - vp = self.app.builder.get_object("vp_" + self.kind) # Viewport - vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) - - # Put in the UI - box_selected.pack_start(sw, True, True, 0) - - entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name") - entry_name.connect("activate", self.app.on_activate_name) - self.to_form() - sw.show() - - def set_form_item(self, option): - """ - Copies the specified option to the UI form. - - :param option: Name of the option (Key in ``self.options``). - :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 - - def read_form_item(self, option): - """ - Reads the specified option from the UI form into ``self.options``. - - :param option: Name of the option. - :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 - - def plot(self): - """ - Plot this object (Extend this method to implement the actual plotting). - Axes get created, appended to canvas and cleared before plotting. - Call this in descendants before doing the plotting. - - :return: Whether to continue plotting or not depending on the "plot" option. - :rtype: bool - """ - - # Axes must exist and be attached to canvas. - if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes: - self.axes = self.app.plotcanvas.new_axes(self.options['name']) - - if not self.options["plot"]: - self.axes.cla() - self.app.plotcanvas.auto_adjust_axes() - return False - - # Clear axes or we will plot on top of them. - self.axes.cla() - # GLib.idle_add(self.axes.cla) - return True - - def serialize(self): - """ - Returns a representation of the object as a dictionary so - it can be later exported as JSON. Override this method. - - :return: Dictionary representing the object - :rtype: dict - """ - return - - def deserialize(self, obj_dict): - """ - Re-builds an object from its serialized version. - - :param obj_dict: Dictionary representing a FlatCAMObj - :type obj_dict: dict - :return: None - """ - return - - -class FlatCAMGerber(FlatCAMObj, Gerber): - """ - Represents Gerber code. - """ - - def __init__(self, name): - Gerber.__init__(self) - FlatCAMObj.__init__(self, name) - - self.kind = "gerber" - - # The 'name' is already in self.options from FlatCAMObj - self.options.update({ - "plot": True, - "mergepolys": True, - "multicolored": False, - "solid": False, - "isotooldia": 0.016, - "isopasses": 1, - "isooverlap": 0.15, - "cutouttooldia": 0.07, - "cutoutmargin": 0.2, - "cutoutgapsize": 0.15, - "gaps": "tb", - "noncoppermargin": 0.0, - "noncopperrounded": False, - "bboxmargin": 0.0, - "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'] - - def convert_units(self, units): - """ - Converts the units of the object by scaling dimensions in all geometry - and options. - - :param units: Units to which to convert the object: "IN" or "MM". - :type units: str - :return: None - :rtype: None - """ - - factor = Gerber.convert_units(self, units) - - self.options['isotooldia'] *= factor - self.options['cutoutmargin'] *= factor - self.options['cutoutgapsize'] *= factor - self.options['noncoppermargin'] *= factor - self.options['bboxmargin'] *= factor - - def plot(self): - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - if self.options["mergepolys"]: - geometry = self.solid_geometry - else: - geometry = self.buffered_paths + \ - [poly['polygon'] for poly in self.regions] + \ - self.flash_geometry - - if self.options["multicolored"]: - linespec = '-' - else: - linespec = 'k-' - - if self.options["solid"]: - for poly in geometry: - # TODO: Too many things hardcoded. - patch = PolygonPatch(poly, - facecolor="#BBF268", - edgecolor="#006E20", - alpha=0.75, - zorder=2) - self.axes.add_patch(patch) - else: - for poly in geometry: - x, y = poly.exterior.xy - self.axes.plot(x, y, linespec) - for ints in poly.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, linespec) - - # self.app.plotcanvas.auto_adjust_axes() - GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) - - def serialize(self): - return { - "options": self.options, - "kind": self.kind - } - - -class FlatCAMExcellon(FlatCAMObj, Excellon): - """ - Represents Excellon/Drill code. - """ - - def __init__(self, name): - Excellon.__init__(self) - FlatCAMObj.__init__(self, name) - - self.kind = "excellon" - - self.options.update({ - "plot": True, - "solid": False, - "drillz": -0.1, - "travelz": 0.1, - "feedrate": 5.0, - "toolselection": "" - }) - - self.form_kinds.update({ - "plot": "cb", - "solid": "cb", - "drillz": "entry_eval", - "travelz": "entry_eval", - "feedrate": "entry_eval", - "toolselection": "entry_text" - }) - - # TODO: Document this. - self.tool_cbs = {} - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - - def convert_units(self, units): - factor = Excellon.convert_units(self, units) - - self.options['drillz'] *= factor - self.options['travelz'] *= factor - self.options['feedrate'] *= factor - - def plot(self): - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - try: - _ = iter(self.solid_geometry) - except TypeError: - self.solid_geometry = [self.solid_geometry] - - # Plot excellon (All polygons?) - if self.options["solid"]: - for geo in self.solid_geometry: - patch = PolygonPatch(geo, - facecolor="#C40000", - edgecolor="#750000", - alpha=0.75, - zorder=3) - self.axes.add_patch(patch) - else: - for geo in self.solid_geometry: - x, y = geo.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in geo.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'g-') - - #self.app.plotcanvas.auto_adjust_axes() - GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) - - def show_tool_chooser(self): - win = Gtk.Window() - box = Gtk.Box(spacing=2) - box.set_orientation(Gtk.Orientation(1)) - win.add(box) - for tool in self.tools: - self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool])) - box.pack_start(self.tool_cbs[tool], False, False, 1) - button = Gtk.Button(label="Accept") - box.pack_start(button, False, False, 1) - win.show_all() - - def on_accept(widget): - win.destroy() - tool_list = [] - for toolx in self.tool_cbs: - if self.tool_cbs[toolx].get_active(): - tool_list.append(toolx) - self.options["toolselection"] = ", ".join(tool_list) - self.to_form() - - button.connect("activate", on_accept) - button.connect("clicked", on_accept) - - -class FlatCAMCNCjob(FlatCAMObj, CNCjob): - """ - Represents G-Code. - """ - - def __init__(self, name, units="in", kind="generic", z_move=0.1, - feedrate=3.0, z_cut=-0.002, tooldia=0.0): - CNCjob.__init__(self, units=units, kind=kind, z_move=z_move, - feedrate=feedrate, z_cut=z_cut, tooldia=tooldia) - FlatCAMObj.__init__(self, name) - - self.kind = "cncjob" - - self.options.update({ - "plot": True, - "tooldia": 0.4 / 25.4 # 0.4mm in inches - }) - - 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'] - - def plot(self): - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - self.plot2(self.axes, tooldia=self.options["tooldia"]) - - #self.app.plotcanvas.auto_adjust_axes() - GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) - - def convert_units(self, units): - factor = CNCjob.convert_units(self, units) - print "FlatCAMCNCjob.convert_units()" - self.options["tooldia"] *= factor - - -class FlatCAMGeometry(FlatCAMObj, Geometry): - """ - Geometric object not associated with a specific - format. - """ - - def __init__(self, name): - FlatCAMObj.__init__(self, name) - Geometry.__init__(self) - - self.kind = "geometry" - - self.options.update({ - "plot": True, - "solid": False, - "multicolored": False, - "cutz": -0.002, - "travelz": 0.1, - "feedrate": 5.0, - "cnctooldia": 0.4 / 25.4, - "painttooldia": 0.0625, - "paintoverlap": 0.15, - "paintmargin": 0.01 - }) - - self.form_kinds.update({ - "plot": "cb", - "solid": "cb", - "multicolored": "cb", - "cutz": "entry_eval", - "travelz": "entry_eval", - "feedrate": "entry_eval", - "cnctooldia": "entry_eval", - "painttooldia": "entry_eval", - "paintoverlap": "entry_eval", - "paintmargin": "entry_eval" - }) - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - - def scale(self, factor): - """ - Scales all geometry by a given factor. - - :param factor: Factor by which to scale the object's geometry/ - :type factor: float - :return: None - :rtype: None - """ - - if type(self.solid_geometry) == list: - self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0)) - for g in self.solid_geometry] - else: - self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor, - origin=(0, 0)) - - def offset(self, vect): - """ - Offsets all geometry by a given vector/ - - :param vect: (x, y) vector by which to offset the object's geometry. - :type vect: tuple - :return: None - :rtype: None - """ - - dx, dy = vect - - if type(self.solid_geometry) == list: - self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy) - for g in self.solid_geometry] - else: - self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy) - - def convert_units(self, units): - factor = Geometry.convert_units(self, units) - - self.options['cutz'] *= factor - self.options['travelz'] *= factor - self.options['feedrate'] *= factor - self.options['cnctooldia'] *= factor - self.options['painttooldia'] *= factor - self.options['paintmargin'] *= factor - - return factor - - def plot(self): - """ - Plots the object into its axes. If None, of if the axes - are not part of the app's figure, it fetches new ones. - - :return: None - """ - - # Does all the required setup and returns False - # if the 'ptint' option is set to False. - if not FlatCAMObj.plot(self): - return - - # Make sure solid_geometry is iterable. - try: - _ = iter(self.solid_geometry) - except TypeError: - self.solid_geometry = [self.solid_geometry] - - for geo in self.solid_geometry: - - if type(geo) == Polygon: - x, y = geo.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in geo.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'r-') - continue - - if type(geo) == LineString or type(geo) == LinearRing: - x, y = geo.coords.xy - self.axes.plot(x, y, 'r-') - continue - - if type(geo) == MultiPolygon: - for poly in geo: - x, y = poly.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in poly.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'r-') - continue - - print "WARNING: Did not plot:", str(type(geo)) - - #self.app.plotcanvas.auto_adjust_axes() - GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) - +from FlatCAMWorker import Worker ######################################## ## App ## @@ -655,9 +55,12 @@ class App: # 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") @@ -678,6 +81,7 @@ class App: self.setup_project_list() # The "Project" tab self.setup_component_editor() # The "Selected" tab + ## Setup the toolbar. Adds buttons. self.setup_toolbar() #### Event handling #### @@ -759,11 +163,16 @@ class App: 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 #### - self.version = 3 + self.version = 4 t1 = threading.Thread(target=self.versionCheck) t1.daemon = True t1.start() + # self.worker.add_task(self.versionCheck) #### For debugging only ### def somethreadfunc(app_obj): @@ -780,7 +189,7 @@ class App: self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png') self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png') Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256]) - self.window.set_title("FlatCAM - Alpha 3 UNSTABLE - Check for updates!") + self.window.set_title("FlatCAM - Alpha 4 UNSTABLE") self.window.set_default_size(900, 600) self.window.show_all() @@ -900,11 +309,16 @@ class App: # Closure needed to create callbacks in a loop. # Otherwise late binding occurs. + # def make_callback(func, fname): + # def opener(*args): + # t = threading.Thread(target=lambda: func(fname)) + # t.daemon = True + # t.start() + # return opener + def make_callback(func, fname): def opener(*args): - t = threading.Thread(target=lambda: func(fname)) - t.daemon = True - t.start() + self.worker.add_task(func, [fname]) return opener try: @@ -1003,9 +417,10 @@ class App: GLib.idle_add(lambda: self.on_zoom_fit(None)) GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) - t = threading.Thread(target=thread_func, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=thread_func, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(thread_func, [self]) def get_eval(self, widget_name): """ @@ -1470,9 +885,10 @@ class App: GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes) GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) - t = threading.Thread(target=thread_func, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=thread_func, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(thread_func, [self]) def enable_all_plots(self, *args): self.plotcanvas.clear() @@ -1494,9 +910,10 @@ class App: GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes) GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) - t = threading.Thread(target=thread_func, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=thread_func, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(thread_func, [self]) def register_recent(self, kind, filename): record = {'kind': kind, 'filename': filename} @@ -2158,9 +1575,10 @@ class App: obj.plot() GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle")) - t = threading.Thread(target=thread_func, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=thread_func, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(thread_func, [self]) def on_generate_excellon_cncjob(self, widget): """ @@ -2205,9 +1623,10 @@ class App: GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) # Start the thread - t = threading.Thread(target=job_thread, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=job_thread, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(job_thread, [self]) def on_excellon_tool_choose(self, widget): """ @@ -2397,9 +1816,10 @@ class App: GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) # Start the thread - t = threading.Thread(target=job_thread, args=(self,)) - t.daemon = True - t.start() + # t = threading.Thread(target=job_thread, args=(self,)) + # t.daemon = True + # t.start() + self.worker.add_task(job_thread, [self]) def on_generate_paintarea(self, widget): """ @@ -2623,9 +2043,10 @@ class App: if response == Gtk.ResponseType.OK: filename = dialog.get_filename() dialog.destroy() - t = threading.Thread(target=on_success, args=(self, filename)) - t.daemon = True - t.start() + # t = threading.Thread(target=on_success, args=(self, filename)) + # t.daemon = True + # t.start() + self.worker.add_task(on_success, [self, filename]) #on_success(self, filename) elif response == Gtk.ResponseType.CANCEL: self.info("Open cancelled.") # print("Cancel clicked") @@ -3372,7 +2793,6 @@ class PlotCanvas: self.pan(0, -0.3) return - def on_mouse_move(self, event): """ Mouse movement event hadler. @@ -3382,5 +2802,24 @@ class PlotCanvas: """ self.mouse = [event.xdata, event.ydata] + +class ObjectCollection: + def __init__(self): + self.collection = [] + self.active = None + + def set_active(self, name): + for obj in self.collection: + if obj.options['name'] == name: + self.active = obj + return self.active + return None + + def get_active(self): + return self.active + + def append(self, obj): + self.collection.append(obj) + app = App() Gtk.main() diff --git a/FlatCAMObj.py b/FlatCAMObj.py new file mode 100644 index 0000000..33c5145 --- /dev/null +++ b/FlatCAMObj.py @@ -0,0 +1,616 @@ +############################################################ +# FlatCAM: 2D Post-processing for Manufacturing # +# http://caram.cl/software/flatcam # +# Author: Juan Pablo Caram (c) # +# Date: 2/5/2014 # +# MIT Licence # +############################################################ + +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib + +from camlib import * + + +######################################## +## FlatCAMObj ## +######################################## +class FlatCAMObj: + """ + Base type of objects handled in FlatCAM. These become interactive + in the GUI, can be plotted, and their options can be modified + by the user in their respective forms. + """ + + # Instance of the application to which these are related. + # The app should set this value. + app = None + + def __init__(self, name): + self.options = {"name": name} + self.form_kinds = {"name": "entry_text"} # Kind of form element for each option + self.radios = {} # Name value pairs for radio sets + self.radios_inv = {} # Inverse of self.radios + self.axes = None # Matplotlib axes + self.kind = None # Override with proper name + + def setup_axes(self, figure): + """ + 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches + them to figure if not part of the figure. 4) Sets transparent + background. 5) Sets 1:1 scale aspect ratio. + + :param figure: A Matplotlib.Figure on which to add/configure axes. + :type figure: matplotlib.figure.Figure + :return: None + :rtype: None + """ + + if self.axes is None: + print "New axes" + self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9], + label=self.options["name"]) + elif self.axes not in figure.axes: + print "Clearing and attaching axes" + self.axes.cla() + figure.add_axes(self.axes) + else: + print "Clearing Axes" + self.axes.cla() + + # Remove all decoration. The app's axes will have + # the ticks and grid. + self.axes.set_frame_on(False) # No frame + self.axes.set_xticks([]) # No tick + self.axes.set_yticks([]) # No ticks + self.axes.patch.set_visible(False) # No background + self.axes.set_aspect(1) + + def to_form(self): + """ + Copies options to the UI form. + + :return: None + """ + for option in self.options: + self.set_form_item(option) + + def read_form(self): + """ + Reads form into ``self.options``. + + :return: None + :rtype: None + """ + for option in self.options: + self.read_form_item(option) + + def build_ui(self): + """ + Sets up the UI/form for this object. + + :return: None + :rtype: None + """ + + # Where the UI for this object is drawn + box_selected = self.app.builder.get_object("box_selected") + + # Remove anything else in the box + box_children = box_selected.get_children() + for child in box_children: + box_selected.remove(child) + + osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow + sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows + osw.remove(sw) # TODO: Is this needed ? + vp = self.app.builder.get_object("vp_" + self.kind) # Viewport + vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1)) + + # Put in the UI + box_selected.pack_start(sw, True, True, 0) + + entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name") + entry_name.connect("activate", self.app.on_activate_name) + self.to_form() + sw.show() + + def set_form_item(self, option): + """ + Copies the specified option to the UI form. + + :param option: Name of the option (Key in ``self.options``). + :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 + + def read_form_item(self, option): + """ + Reads the specified option from the UI form into ``self.options``. + + :param option: Name of the option. + :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 + + def plot(self): + """ + Plot this object (Extend this method to implement the actual plotting). + Axes get created, appended to canvas and cleared before plotting. + Call this in descendants before doing the plotting. + + :return: Whether to continue plotting or not depending on the "plot" option. + :rtype: bool + """ + + # Axes must exist and be attached to canvas. + if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes: + self.axes = self.app.plotcanvas.new_axes(self.options['name']) + + if not self.options["plot"]: + self.axes.cla() + self.app.plotcanvas.auto_adjust_axes() + return False + + # Clear axes or we will plot on top of them. + self.axes.cla() + # GLib.idle_add(self.axes.cla) + return True + + def serialize(self): + """ + Returns a representation of the object as a dictionary so + it can be later exported as JSON. Override this method. + + :return: Dictionary representing the object + :rtype: dict + """ + return + + def deserialize(self, obj_dict): + """ + Re-builds an object from its serialized version. + + :param obj_dict: Dictionary representing a FlatCAMObj + :type obj_dict: dict + :return: None + """ + return + + +class FlatCAMGerber(FlatCAMObj, Gerber): + """ + Represents Gerber code. + """ + + def __init__(self, name): + Gerber.__init__(self) + FlatCAMObj.__init__(self, name) + + self.kind = "gerber" + + # The 'name' is already in self.options from FlatCAMObj + self.options.update({ + "plot": True, + "mergepolys": True, + "multicolored": False, + "solid": False, + "isotooldia": 0.016, + "isopasses": 1, + "isooverlap": 0.15, + "cutouttooldia": 0.07, + "cutoutmargin": 0.2, + "cutoutgapsize": 0.15, + "gaps": "tb", + "noncoppermargin": 0.0, + "noncopperrounded": False, + "bboxmargin": 0.0, + "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'] + + def convert_units(self, units): + """ + Converts the units of the object by scaling dimensions in all geometry + and options. + + :param units: Units to which to convert the object: "IN" or "MM". + :type units: str + :return: None + :rtype: None + """ + + factor = Gerber.convert_units(self, units) + + self.options['isotooldia'] *= factor + self.options['cutoutmargin'] *= factor + self.options['cutoutgapsize'] *= factor + self.options['noncoppermargin'] *= factor + self.options['bboxmargin'] *= factor + + def plot(self): + + # Does all the required setup and returns False + # if the 'ptint' option is set to False. + if not FlatCAMObj.plot(self): + return + + if self.options["mergepolys"]: + geometry = self.solid_geometry + else: + geometry = self.buffered_paths + \ + [poly['polygon'] for poly in self.regions] + \ + self.flash_geometry + + if self.options["multicolored"]: + linespec = '-' + else: + linespec = 'k-' + + if self.options["solid"]: + for poly in geometry: + # TODO: Too many things hardcoded. + patch = PolygonPatch(poly, + facecolor="#BBF268", + edgecolor="#006E20", + alpha=0.75, + zorder=2) + self.axes.add_patch(patch) + else: + for poly in geometry: + x, y = poly.exterior.xy + self.axes.plot(x, y, linespec) + for ints in poly.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, linespec) + + # self.app.plotcanvas.auto_adjust_axes() + GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) + + def serialize(self): + return { + "options": self.options, + "kind": self.kind + } + + +class FlatCAMExcellon(FlatCAMObj, Excellon): + """ + Represents Excellon/Drill code. + """ + + def __init__(self, name): + Excellon.__init__(self) + FlatCAMObj.__init__(self, name) + + self.kind = "excellon" + + self.options.update({ + "plot": True, + "solid": False, + "drillz": -0.1, + "travelz": 0.1, + "feedrate": 5.0, + "toolselection": "" + }) + + self.form_kinds.update({ + "plot": "cb", + "solid": "cb", + "drillz": "entry_eval", + "travelz": "entry_eval", + "feedrate": "entry_eval", + "toolselection": "entry_text" + }) + + # TODO: Document this. + self.tool_cbs = {} + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + + def convert_units(self, units): + factor = Excellon.convert_units(self, units) + + self.options['drillz'] *= factor + self.options['travelz'] *= factor + self.options['feedrate'] *= factor + + def plot(self): + + # Does all the required setup and returns False + # if the 'ptint' option is set to False. + if not FlatCAMObj.plot(self): + return + + try: + _ = iter(self.solid_geometry) + except TypeError: + self.solid_geometry = [self.solid_geometry] + + # Plot excellon (All polygons?) + if self.options["solid"]: + for geo in self.solid_geometry: + patch = PolygonPatch(geo, + facecolor="#C40000", + edgecolor="#750000", + alpha=0.75, + zorder=3) + self.axes.add_patch(patch) + else: + for geo in self.solid_geometry: + x, y = geo.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in geo.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'g-') + + #self.app.plotcanvas.auto_adjust_axes() + GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) + + def show_tool_chooser(self): + win = Gtk.Window() + box = Gtk.Box(spacing=2) + box.set_orientation(Gtk.Orientation(1)) + win.add(box) + for tool in self.tools: + self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool])) + box.pack_start(self.tool_cbs[tool], False, False, 1) + button = Gtk.Button(label="Accept") + box.pack_start(button, False, False, 1) + win.show_all() + + def on_accept(widget): + win.destroy() + tool_list = [] + for toolx in self.tool_cbs: + if self.tool_cbs[toolx].get_active(): + tool_list.append(toolx) + self.options["toolselection"] = ", ".join(tool_list) + self.to_form() + + button.connect("activate", on_accept) + button.connect("clicked", on_accept) + + +class FlatCAMCNCjob(FlatCAMObj, CNCjob): + """ + Represents G-Code. + """ + + def __init__(self, name, units="in", kind="generic", z_move=0.1, + feedrate=3.0, z_cut=-0.002, tooldia=0.0): + CNCjob.__init__(self, units=units, kind=kind, z_move=z_move, + feedrate=feedrate, z_cut=z_cut, tooldia=tooldia) + FlatCAMObj.__init__(self, name) + + self.kind = "cncjob" + + self.options.update({ + "plot": True, + "tooldia": 0.4 / 25.4 # 0.4mm in inches + }) + + 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'] + + def plot(self): + + # Does all the required setup and returns False + # if the 'ptint' option is set to False. + if not FlatCAMObj.plot(self): + return + + self.plot2(self.axes, tooldia=self.options["tooldia"]) + + #self.app.plotcanvas.auto_adjust_axes() + GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) + + def convert_units(self, units): + factor = CNCjob.convert_units(self, units) + print "FlatCAMCNCjob.convert_units()" + self.options["tooldia"] *= factor + + +class FlatCAMGeometry(FlatCAMObj, Geometry): + """ + Geometric object not associated with a specific + format. + """ + + def __init__(self, name): + FlatCAMObj.__init__(self, name) + Geometry.__init__(self) + + self.kind = "geometry" + + self.options.update({ + "plot": True, + "solid": False, + "multicolored": False, + "cutz": -0.002, + "travelz": 0.1, + "feedrate": 5.0, + "cnctooldia": 0.4 / 25.4, + "painttooldia": 0.0625, + "paintoverlap": 0.15, + "paintmargin": 0.01 + }) + + self.form_kinds.update({ + "plot": "cb", + "solid": "cb", + "multicolored": "cb", + "cutz": "entry_eval", + "travelz": "entry_eval", + "feedrate": "entry_eval", + "cnctooldia": "entry_eval", + "painttooldia": "entry_eval", + "paintoverlap": "entry_eval", + "paintmargin": "entry_eval" + }) + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + + def scale(self, factor): + """ + Scales all geometry by a given factor. + + :param factor: Factor by which to scale the object's geometry/ + :type factor: float + :return: None + :rtype: None + """ + + if type(self.solid_geometry) == list: + self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0)) + for g in self.solid_geometry] + else: + self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor, + origin=(0, 0)) + + def offset(self, vect): + """ + Offsets all geometry by a given vector/ + + :param vect: (x, y) vector by which to offset the object's geometry. + :type vect: tuple + :return: None + :rtype: None + """ + + dx, dy = vect + + if type(self.solid_geometry) == list: + self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy) + for g in self.solid_geometry] + else: + self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy) + + def convert_units(self, units): + factor = Geometry.convert_units(self, units) + + self.options['cutz'] *= factor + self.options['travelz'] *= factor + self.options['feedrate'] *= factor + self.options['cnctooldia'] *= factor + self.options['painttooldia'] *= factor + self.options['paintmargin'] *= factor + + return factor + + def plot(self): + """ + Plots the object into its axes. If None, of if the axes + are not part of the app's figure, it fetches new ones. + + :return: None + """ + + # Does all the required setup and returns False + # if the 'ptint' option is set to False. + if not FlatCAMObj.plot(self): + return + + # Make sure solid_geometry is iterable. + try: + _ = iter(self.solid_geometry) + except TypeError: + self.solid_geometry = [self.solid_geometry] + + for geo in self.solid_geometry: + + if type(geo) == Polygon: + x, y = geo.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in geo.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'r-') + continue + + if type(geo) == LineString or type(geo) == LinearRing: + x, y = geo.coords.xy + self.axes.plot(x, y, 'r-') + continue + + if type(geo) == MultiPolygon: + for poly in geo: + x, y = poly.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in poly.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'r-') + continue + + print "WARNING: Did not plot:", str(type(geo)) + + #self.app.plotcanvas.auto_adjust_axes() + GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) \ No newline at end of file diff --git a/camlib.py b/camlib.py index 2223393..bc90ff2 100644 --- a/camlib.py +++ b/camlib.py @@ -1488,7 +1488,7 @@ class Excellon(Geometry): self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' + r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + - r'(?=.*Z(-?\d*\.?\d*))?[CFSBHT]') + r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') # Tool select # Can have additional data after tool number but @@ -1513,11 +1513,11 @@ class Excellon(Geometry): # Coordinates #self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$') #self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$') - self.coordsperiod_re = re.compile(r'(?=.*X(-?\d*\.\d*))?(?=.*Y(-?\d*\.\d*))?[XY]') - self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X(-?\d*))?(?=.*Y(-?\d*))?[XY]') + self.coordsperiod_re = re.compile(r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]') + self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]') # R - Repeat hole (# times, X offset, Y offset) - self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(-?\d*\.?\d*))?(?:Y(-?\d*\.?\d*))?$') + self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$') # Various stop/pause commands self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))') diff --git a/doc/source/devman.rst b/doc/source/devman.rst index 1693620..7fdbc91 100644 --- a/doc/source/devman.rst +++ b/doc/source/devman.rst @@ -55,4 +55,8 @@ This creates a dictionary with attributes specified in the object's ``ser_attrs` } return geo -This is used in ``json.dump(d, f, default=to_dict)`` and is applied to objects that json encounters to be in a non-serialized form. \ No newline at end of file +This is used in ``json.dump(d, f, default=to_dict)`` and is applied to objects that json encounters to be in a non-serialized form. + +Geometry Processing +~~~~~~~~~~~~~~~~~~~ + diff --git a/recent.json b/recent.json index c688797..93d7f0c 100644 --- a/recent.json +++ b/recent.json @@ -1 +1 @@ -[{"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC_1303_Bottom.gbr"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles.drl"}, {"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\\Example1_copper_bottom.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1_CNC\\cutout1.gcode"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.TXT"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.DRL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.GBL"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\full.fcproj"}] \ No newline at end of file +[{"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC2003Through.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.GTL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC_1303_Bottom.gbr"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles.drl"}, {"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\\Example1_copper_bottom.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1_CNC\\cutout1.gcode"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.TXT"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.DRL"}] \ No newline at end of file