diff --git a/camlib.py b/camlib.py index b8740b5b..1ec0dd9a 100644 --- a/camlib.py +++ b/camlib.py @@ -8,10 +8,14 @@ from shapely.geometry import MultiPoint, MultiPolygon from shapely.geometry import box as shply_box from shapely.ops import cascaded_union import shapely.affinity as affinity +from shapely.wkt import loads as sloads +from shapely.wkt import dumps as sdumps +from shapely.geometry.base import BaseGeometry # Used for solid polygons in Matplotlib from descartes.patch import PolygonPatch +import simplejson as json class Geometry: def __init__(self): @@ -20,6 +24,9 @@ class Geometry: # Final geometry: MultiPolygon self.solid_geometry = None + + # Attributes to be included in serialization + self.ser_attrs = ['units', 'solid_geometry'] def isolation_geometry(self, offset): """ @@ -113,7 +120,25 @@ class Geometry: self.units = units self.scale(factor) return factor - + + def to_dict(self): + """ + Returns a respresentation of the object as a dictionary. + ``self.solid_geometry`` has been converted using the ``to_dict`` + function. Attributes to include are listed in + ``self.ser_attrs``. + + :return: A dictionary-encoded copy of the object. + :rtype: dict + """ + d = {} + for attr in self.ser_attrs: + d[attr] = getattr(self, attr) + return d + + def from_dict(self, d): + return + class Gerber (Geometry): """ @@ -207,6 +232,13 @@ class Gerber (Geometry): # Geometry from flashes self.flash_geometry = [] + # Attributes to be included in serialization + # Always append to it because it carries contents + # from Geometry. + self.ser_attrs += ['digits', 'fraction', 'apertures', 'paths', + 'buffered_paths', 'regions', 'flashes', + 'flash_geometry'] + def scale(self, factor): """ Scales the objects' geometry on the XY plane by a given factor. @@ -458,6 +490,11 @@ class Excellon(Geometry): # Trailing "T" or leading "L" self.zeros = "" + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from Geometry. + self.ser_attrs += ['tools', 'drills', 'zeros'] def parse_file(self, filename): efile = open(filename, 'r') @@ -592,6 +629,13 @@ class CNCjob(Geometry): self.input_geometry_bounds = None self.gcode_parsed = None self.steps_per_circ = 20 # Used when parsing G-code arcs + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from Geometry. + self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia', + 'gcode', 'input_geometry_bounds', 'gcode_parsed', + 'steps_per_circ'] def generate_from_excellon(self, exobj): """ @@ -949,6 +993,7 @@ class CNCjob(Geometry): return gcode def point2gcode(self, point): + # TODO: This is not doing anything. gcode = "" t = "G0%d X%.4fY%.4f\n" path = list(point.coords) @@ -1067,6 +1112,22 @@ def find_polygon(poly_set, point): return poly return None +def to_dict(geo): + output = '' + if isinstance(geo, BaseGeometry): + return { + "__class__": "Shply", + "__inst__": sdumps(geo) + } + return geo + +def dict2obj(d): + if '__class__' in d and '__inst__' in d: + # For now assume all classes are Shapely geometry. + return sloads(d['__inst__']) + else: + return d + ############### cam.py #################### def coord(gstr, digits, fraction): """ diff --git a/cirkuix.py b/cirkuix.py index c6914116..d5722cb4 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -39,9 +39,11 @@ class CirkuixObj: 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 + + :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" @@ -63,19 +65,21 @@ class CirkuixObj: self.axes.patch.set_visible(False) # No background self.axes.set_aspect(1) - def set_options(self, options): - for name in options: - self.options[name] = options[name] - return - def to_form(self): + """ + 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 - @rtype : None + Reads form into ``self.options``. + + :return: None + :rtype : None """ for option in self.options: self.read_form_item(option) @@ -83,8 +87,9 @@ class CirkuixObj: def build_ui(self): """ Sets up the UI/form for this object. - @return: None - @rtype : None + + :return: None + :rtype : None """ # Where the UI for this object is drawn @@ -110,6 +115,13 @@ class CirkuixObj: sw.show() def set_form_item(self, option): + """ + Copies the specified options 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 @@ -217,6 +229,11 @@ class CirkuixGerber(CirkuixObj, Gerber): 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'] + def convert_units(self, units): factor = Gerber.convert_units(self, units) @@ -290,8 +307,14 @@ class CirkuixExcellon(CirkuixObj, Excellon): "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'] + def convert_units(self, units): factor = Excellon.convert_units(self, units) @@ -339,10 +362,6 @@ class CirkuixExcellon(CirkuixObj, Excellon): button.connect("activate", on_accept) button.connect("clicked", on_accept) - # def parse_lines(self, elines): - # Excellon.parse_lines(self, elines) - # self.options["units"] = self.units - class CirkuixCNCjob(CirkuixObj, CNCjob): """ @@ -371,6 +390,11 @@ class CirkuixCNCjob(CirkuixObj, CNCjob): "tooldia": "entry_eval" }) + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options'] + def plot(self, figure): CirkuixObj.plot(self, figure) #self.setup_axes(figure) @@ -417,6 +441,11 @@ class CirkuixGeometry(CirkuixObj, Geometry): "paintmargin": "entry_eval" }) + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options'] + def scale(self, factor): if type(self.solid_geometry) == list: self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0)) @@ -510,7 +539,8 @@ class App: self.units_label = self.builder.get_object("label_units") # 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)) + 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") @@ -1100,9 +1130,51 @@ class App: 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 + """ + + d = {"objs": [self.stuff[o].to_dict() for o in self.stuff], + "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() + + ######################################## ## EVENT HANDLERS ## ######################################## + def on_file_saveproject(self, param): + return + + def on_file_saveprojectas(self, param): + def on_success(app_obj, filename): + assert isinstance(app_obj, App) + app_obj.save_project(filename) + app_obj.info("Project saved to: " + filename) + + self.file_chooser_save_action(on_success) + return + + def on_file_saveprojectcopy(self, param): + return + def on_options_app2project(self, param): """ Callback for Options->Transfer Options->App=>Project. Copies options diff --git a/cirkuix.ui b/cirkuix.ui index cd2651a2..b11a3920 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -26,6 +26,21 @@ False gtk-jump-to + + True + False + gtk-save + + + True + False + gtk-save-as + + + True + False + gtk-save-as + False @@ -2063,6 +2078,42 @@ False + + + Save Project + True + False + image6 + False + + + + + + Save Project As ... + True + False + image7 + False + + + + + + Save a Project copy ... + True + False + image8 + False + + + + + + True + False + + gtk-quit diff --git a/defaults.json b/defaults.json index bcc117a8..b6811a04 100644 --- a/defaults.json +++ b/defaults.json @@ -1 +1 @@ -{"cncjob_multicolored": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "cncjob_solid": false, "gerber_isotooldia": 0.16, "gerber_plot": true, "gerber_mergepolys": true, "gerber_cutoutgapsize": 0.15, "geometry_feedrate": 5.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": false, "excellon_plot": true, "excellon_feedrate": 5.0, "cncjob_tooldia": 0.16, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "gerber_noncoppermargin": 0.0, "gerber_gaps": "4", "excellon_multicolored": false, "gerber_bboxmargin": 0.0, "cncjob_plot": true, "excellon_drillz": -0.1, "gerber_bboxrounded": false, "geometry_multicolored": false, "geometry_cnctooldia": 0.16, "geometry_solid": false, "geometry_painttooldia": 0.0625} \ No newline at end of file +{"cncjob_multicolored": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "cncjob_solid": false, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": true, "excellon_drillz": -0.1, "geometry_feedrate": 5.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": false, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.16, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.16, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "excellon_multicolored": false, "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false} \ No newline at end of file