Several minor fixes and features. Plotting, object list, form save, new, delete, etc.

This commit is contained in:
Juan Pablo Caram
2014-01-22 23:55:46 -05:00
parent 3459c55da4
commit 80cb2a8de3
3 changed files with 337 additions and 104 deletions

View File

@@ -1,6 +1,6 @@
import threading import threading
from gi.repository import Gtk, Gdk, GLib, GObject from gi.repository import Gtk, Gdk, GLib, GObject
import simplejson as json
from matplotlib.figure import Figure from matplotlib.figure import Figure
from numpy import arange, sin, pi from numpy import arange, sin, pi
@@ -54,10 +54,13 @@ class CirkuixObj:
print "Clearing Axes" print "Clearing Axes"
self.axes.cla() self.axes.cla()
self.axes.set_frame_on(False)
self.axes.set_xticks([])
self.axes.set_yticks([])
self.axes.patch.set_visible(False) # No background self.axes.patch.set_visible(False) # No background
self.axes.set_aspect(1) self.axes.set_aspect(1)
return self.axes #return self.axes
def set_options(self, options): def set_options(self, options):
for name in options: for name in options:
@@ -150,6 +153,24 @@ class CirkuixObj:
# self.axes.cla() # self.axes.cla()
# return # return
def serialize(self):
"""
Returns a representation of the object as a dictionary so
it can be later exported as JSON. Override this method.
@return: Dictionary representing the object
@rtype: dict
"""
return
def deserialize(self, obj_dict):
"""
Re-builds an object from its serialized version.
@param obj_dict: Dictionary representing a CirkuixObj
@type obj_dict: dict
@return None
"""
return
class CirkuixGerber(CirkuixObj, Gerber): class CirkuixGerber(CirkuixObj, Gerber):
""" """
@@ -221,6 +242,12 @@ class CirkuixGerber(CirkuixObj, Gerber):
x, y = ints.coords.xy x, y = ints.coords.xy
self.axes.plot(x, y, linespec) self.axes.plot(x, y, linespec)
def serialize(self):
return {
"options": self.options,
"kind": self.kind
}
class CirkuixExcellon(CirkuixObj, Excellon): class CirkuixExcellon(CirkuixObj, Excellon):
""" """
@@ -408,12 +435,11 @@ class App:
""" """
Starts the application and the Gtk.main(). Starts the application and the Gtk.main().
@return: app @return: app
@rtype: App
""" """
# Needed to interact with the GUI from other threads. # Needed to interact with the GUI from other threads.
#GLib.threads_init()
GObject.threads_init() GObject.threads_init()
#Gdk.threads_init()
## GUI ## ## GUI ##
self.gladefile = "cirkuix.ui" self.gladefile = "cirkuix.ui"
@@ -427,6 +453,7 @@ class App:
self.info_label = self.builder.get_object("label_status") self.info_label = self.builder.get_object("label_status")
self.progress_bar = self.builder.get_object("progressbar") self.progress_bar = self.builder.get_object("progressbar")
self.progress_bar.set_show_text(True) self.progress_bar.set_show_text(True)
self.units_label = self.builder.get_object("label_units")
## Event handling ## ## Event handling ##
self.builder.connect_signals(self) self.builder.connect_signals(self)
@@ -449,8 +476,18 @@ class App:
# a key if self.stuff # a key if self.stuff
self.selected_item_name = None self.selected_item_name = None
self.defaults = {
"units": "in"
} # Application defaults
self.options = {} # Project options
self.plot_click_subscribers = {} self.plot_click_subscribers = {}
# Initialization
self.load_defaults()
self.options.update(self.defaults)
self.units_label.set_text("[" + self.options["units"] + "]")
# For debugging only # For debugging only
def someThreadFunc(self): def someThreadFunc(self):
print "Hello World!" print "Hello World!"
@@ -460,6 +497,7 @@ class App:
######################################## ########################################
## START ## ## START ##
######################################## ########################################
self.window.set_default_size(900, 600)
self.window.show_all() self.window.show_all()
#Gtk.main() #Gtk.main()
@@ -478,7 +516,7 @@ class App:
#t = arange(0.0,5.0,0.01) #t = arange(0.0,5.0,0.01)
#s = sin(2*pi*t) #s = sin(2*pi*t)
#self.axes.plot(t,s) #self.axes.plot(t,s)
self.axes.grid() self.axes.grid(True)
self.figure.patch.set_visible(False) self.figure.patch.set_visible(False)
self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
@@ -491,6 +529,7 @@ class App:
self.canvas.set_can_focus(True) # For key press self.canvas.set_can_focus(True) # For key press
self.canvas.mpl_connect('key_press_event', self.on_key_over_plot) self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
#self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot) #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
self.canvas.connect("configure-event", self.on_canvas_configure)
self.grid.attach(self.canvas, 0, 0, 600, 400) self.grid.attach(self.canvas, 0, 0, 600, 400)
@@ -499,7 +538,7 @@ class App:
def setup_component_viewer(self): def setup_component_viewer(self):
""" """
List or Tree where whatever has been loaded or created is Sets up list or Tree where whatever has been loaded or created is
displayed. displayed.
@return: None @return: None
""" """
@@ -507,6 +546,7 @@ class App:
self.store = Gtk.ListStore(str) self.store = Gtk.ListStore(str)
self.tree = Gtk.TreeView(self.store) self.tree = Gtk.TreeView(self.store)
#self.list = Gtk.ListBox() #self.list = Gtk.ListBox()
self.tree.connect("row_activated", self.on_row_activated)
self.tree_select = self.tree.get_selection() self.tree_select = self.tree.get_selection()
self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed) self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
renderer = Gtk.CellRendererText() renderer = Gtk.CellRendererText()
@@ -531,8 +571,11 @@ class App:
box1 = Gtk.Box(Gtk.Orientation.VERTICAL) box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
label1 = Gtk.Label("Choose an item from Project") label1 = Gtk.Label("Choose an item from Project")
box1.pack_start(label1, False, False, 1) box1.pack_start(label1, True, False, 1)
box_selected.pack_start(box1, True, True, 0) box_selected.pack_start(box1, True, True, 0)
#box_selected.show()
box1.show()
label1.show()
def info(self, text): def info(self, text):
""" """
@@ -578,13 +621,18 @@ class App:
def build_list(self): def build_list(self):
""" """
Clears and re-populates the list of objects in tcurrently Clears and re-populates the list of objects in currently
in the project. in the project.
@return: None @return: None
""" """
print "build_list(): clearing"
self.tree_select.unselect_all()
self.store.clear() self.store.clear()
print "repopulating...",
for key in self.stuff: for key in self.stuff:
print key,
self.store.append([key]) self.store.append([key])
print
def get_radio_value(self, radio_set): def get_radio_value(self, radio_set):
""" """
@@ -602,15 +650,29 @@ class App:
Re-generates all plots from all objects. Re-generates all plots from all objects.
@return: None @return: None
""" """
self.clear_plots() self.clear_plots()
self.set_progress_bar(0.1, "Re-plotting...")
for i in self.stuff:
self.stuff[i].plot(self.figure) def thread_func(app_obj):
percentage = 0.1
self.on_zoom_fit(None) try:
self.axes.grid() delta = 0.9/len(self.stuff)
self.canvas.queue_draw() except ZeroDivisionError:
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
return
for i in self.stuff:
self.stuff[i].plot(self.figure)
percentage += delta
GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
self.on_zoom_fit(None)
self.axes.grid(True)
self.canvas.queue_draw()
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
t = threading.Thread(target=thread_func, args=(self,))
t.daemon = True
t.start()
def clear_plots(self): def clear_plots(self):
""" """
@@ -618,9 +680,12 @@ class App:
@return: None @return: None
""" """
# TODO: Create a setup_axes method that gets called here and in setup_plot?
self.axes.cla() self.axes.cla()
self.figure.clf() self.figure.clf()
self.figure.add_axes(self.axes) self.figure.add_axes(self.axes)
self.axes.set_aspect(1)
self.axes.grid(True)
self.canvas.queue_draw() self.canvas.queue_draw()
def get_eval(self, widget_name): def get_eval(self, widget_name):
@@ -710,11 +775,88 @@ class App:
self.progress_bar.set_fraction(percentage) self.progress_bar.set_fraction(percentage)
return False return False
def save_project(self):
return
def get_current(self):
"""
Returns the currently selected CirkuixObj in the application.
@return: Currently selected CirkuixObj in the application.
@rtype: CirkuixObj
"""
try:
return self.stuff[self.selected_item_name]
except:
return None
def adjust_axes(self, xmin, ymin, xmax, ymax):
m_x = 15 # pixels
m_y = 25 # pixels
width = xmax-xmin
height = ymax-ymin
r = width/height
Fw, Fh = self.canvas.get_width_height()
Fr = float(Fw)/Fh
x_ratio = float(m_x)/Fw
y_ratio = float(m_y)/Fh
if r > Fr:
ycenter = (ymin+ymax)/2.0
newheight = height*r/Fr
ymin = ycenter-newheight/2.0
ymax = ycenter+newheight/2.0
else:
xcenter = (xmax+ymin)/2.0
newwidth = width*Fr/r
xmin = xcenter-newwidth/2.0
xmax = xcenter+newwidth/2.0
for name in self.stuff:
if self.stuff[name].axes is None:
continue
self.stuff[name].axes.set_xlim((xmin, xmax))
self.stuff[name].axes.set_ylim((ymin, ymax))
self.stuff[name].axes.set_position([x_ratio, y_ratio,
1-2*x_ratio, 1-2*y_ratio])
self.axes.set_xlim((xmin, xmax))
self.axes.set_ylim((ymin, ymax))
self.axes.set_position([x_ratio, y_ratio,
1-2*x_ratio, 1-2*y_ratio])
self.canvas.queue_draw()
def load_defaults(self):
try:
f = open("defaults.json")
options = f.read()
f.close()
except:
self.info("ERROR: Could not load defaults file.")
return
try:
defaults = json.loads(options)
except:
self.info("ERROR: Failed to parse defaults file.")
return
self.defaults.update(defaults)
######################################## ########################################
## EVENT HANDLERS ## ## EVENT HANDLERS ##
######################################## ########################################
def on_canvas_configure(self, widget, event):
print "on_canvas_configure()"
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
def on_row_activated(self, widget, path, col):
self.notebook.set_current_page(1)
def on_generate_gerber_bounding_box(self, widget): def on_generate_gerber_bounding_box(self, widget):
gerber = self.stuff[self.selected_item_name] gerber = self.get_current()
gerber.read_form() gerber.read_form()
name = self.selected_item_name + "_bbox" name = self.selected_item_name + "_bbox"
@@ -734,8 +876,20 @@ class App:
@param widget: The widget from which this was called. @param widget: The widget from which this was called.
@return: None @return: None
""" """
self.stuff[self.selected_item_name].read_form() print "Re-plotting"
self.stuff[self.selected_item_name].plot(self.figure)
self.get_current().read_form()
self.set_progress_bar(0.5, "Plotting...")
#GLib.idle_add(lambda: self.set_progress_bar(0.5, "Plotting..."))
def thread_func(app_obj):
#GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
t = threading.Thread(target=thread_func, args=(self,))
t.daemon = True
t.start()
def on_generate_excellon_cncjob(self, widget): def on_generate_excellon_cncjob(self, widget):
""" """
@@ -746,13 +900,13 @@ class App:
""" """
job_name = self.selected_item_name + "_cnc" job_name = self.selected_item_name + "_cnc"
excellon = self.stuff[self.selected_item_name] excellon = self.get_current()
assert isinstance(excellon, CirkuixExcellon) assert isinstance(excellon, CirkuixExcellon)
excellon.read_form() excellon.read_form()
# Object initialization function for app.new_object() # Object initialization function for app.new_object()
def job_init(job_obj, app_obj): def job_init(job_obj, app_obj):
excellon_ = self.stuff[self.selected_item_name] excellon_ = self.get_current()
assert isinstance(excellon_, CirkuixExcellon) assert isinstance(excellon_, CirkuixExcellon)
assert isinstance(job_obj, CirkuixCNCjob) assert isinstance(job_obj, CirkuixCNCjob)
@@ -791,13 +945,13 @@ class App:
@param widget: The widget from which this was called. @param widget: The widget from which this was called.
@return: None @return: None
""" """
excellon = self.stuff[self.selected_item_name] excellon = self.get_current()
assert isinstance(excellon, CirkuixExcellon) assert isinstance(excellon, CirkuixExcellon)
excellon.show_tool_chooser() excellon.show_tool_chooser()
def on_entry_eval_activate(self, widget): def on_entry_eval_activate(self, widget):
self.on_eval_update(widget) self.on_eval_update(widget)
obj = self.stuff[self.selected_item_name] obj = self.get_current()
assert isinstance(obj, CirkuixObj) assert isinstance(obj, CirkuixObj)
obj.read_form() obj.read_form()
@@ -895,7 +1049,7 @@ class App:
# TODO: Object must be updated on form change and the options # TODO: Object must be updated on form change and the options
# TODO: read from the object. # TODO: read from the object.
tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia") tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
geo_obj.solid_geometry = self.stuff[self.selected_item_name].isolation_geometry(tooldia/2.0) geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia/2.0)
# TODO: Do something if this is None. Offer changing name? # TODO: Do something if this is None. Offer changing name?
self.new_object("geometry", iso_name, iso_init) self.new_object("geometry", iso_name, iso_init)
@@ -953,7 +1107,7 @@ class App:
in a new CirkuixGeometry object. in a new CirkuixGeometry object.
""" """
self.info("Click inside the desired polygon.") self.info("Click inside the desired polygon.")
geo = self.stuff[self.selected_item_name] geo = self.get_current()
geo.read_form() geo.read_form()
tooldia = geo.options["painttooldia"] tooldia = geo.options["painttooldia"]
overlap = geo.options["paintoverlap"] overlap = geo.options["paintoverlap"]
@@ -979,7 +1133,7 @@ class App:
def on_cncjob_exportgcode(self, widget): def on_cncjob_exportgcode(self, widget):
def on_success(self, filename): def on_success(self, filename):
cncjob = self.stuff[self.selected_item_name] cncjob = self.get_current()
f = open(filename, 'w') f = open(filename, 'w')
f.write(cncjob.gcode) f.write(cncjob.gcode)
f.close() f.close()
@@ -987,15 +1141,22 @@ class App:
self.file_chooser_save_action(on_success) self.file_chooser_save_action(on_success)
def on_delete(self, widget): def on_delete(self, widget):
"""
Delete the currently selected CirkuixObj.
@param widget: The widget from which this was called.
@return:
"""
print "on_delete():", self.selected_item_name
# Remove plot
self.figure.delaxes(self.get_current().axes)
self.canvas.queue_draw()
# Remove from dictionary
self.stuff.pop(self.selected_item_name) self.stuff.pop(self.selected_item_name)
#self.tree.get_selection().disconnect(self.signal_id) # Update UI
self.build_list() # Update the items list self.build_list() # Update the items list
#self.signal_id = self.tree.get_selection().connect(
# "changed", self.on_tree_selection_changed)
self.plot_all()
#self.notebook.set_current_page(1)
def on_replot(self, widget): def on_replot(self, widget):
self.plot_all() self.plot_all()
@@ -1023,22 +1184,48 @@ class App:
# Reconnect event listener # Reconnect event listener
self.signal_id = self.tree.get_selection().connect( self.signal_id = self.tree.get_selection().connect(
"changed", self.on_tree_selection_changed) "changed", self.on_tree_selection_changed)
def on_tree_selection_changed(self, selection): def on_tree_selection_changed(self, selection):
"""
Callback for selection change in the project list. This changes
the currently selected CirkuixObj.
@param selection: Selection associated to the project tree or list
@type selection: Gtk.TreeSelection
@return: None
"""
print "on_tree_selection_change(): ",
model, treeiter = selection.get_selected() model, treeiter = selection.get_selected()
if treeiter is not None: if treeiter is not None:
# Save data for previous selection
obj = self.get_current()
if obj is not None:
obj.read_form()
print "You selected", model[treeiter][0] print "You selected", model[treeiter][0]
self.selected_item_name = model[treeiter][0] self.selected_item_name = model[treeiter][0]
#self.stuff[self.selected_item_name].build_ui() GLib.idle_add(lambda: self.get_current().build_ui())
GLib.timeout_add(100, lambda: self.stuff[self.selected_item_name].build_ui())
else: else:
print "Nothing selected" print "Nothing selected"
self.selected_item_name = None self.selected_item_name = None
self.setup_component_editor() self.setup_component_editor()
def on_file_new(self, param): def on_file_new(self, param):
print "File->New not implemented yet." # Remove everythong from memory
# Clear plot
self.clear_plots()
# Clear object editor
#self.setup_component_editor()
# Clear data
self.stuff = {}
# Clear list
#self.tree_select.unselect_all()
self.build_list()
#print "File->New not implemented yet."
def on_filequit(self, param): def on_filequit(self, param):
print "quit from menu" print "quit from menu"
@@ -1204,44 +1391,12 @@ class App:
xmin, ymin, xmax, ymax = get_bounds(self.stuff) xmin, ymin, xmax, ymax = get_bounds(self.stuff)
width = xmax-xmin width = xmax-xmin
height = ymax-ymin height = ymax-ymin
r = width/height xmin -= 0.05*width
xmax += 0.05*width
Fw, Fh = self.canvas.get_width_height() ymin -= 0.05*height
Fr = float(Fw)/Fh ymax += 0.05*height
print "Window aspect ratio:", Fr self.adjust_axes(xmin, ymin, xmax, ymax)
print "Data aspect ratio:", r
#self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
#self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
if r > Fr:
#self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
xmin -= 0.05*width
xmax += 0.05*width
ycenter = (ymin+ymax)/2.0
newheight = height*r/Fr
ymin = ycenter-newheight/2.0
ymax = ycenter+newheight/2.0
#self.axes.set_ylim((ycenter-newheight/2.0, ycenter+newheight/2.0))
else:
#self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
ymin -= 0.05*height
ymax += 0.05*height
xcenter = (xmax+ymin)/2.0
newwidth = width*Fr/r
xmin = xcenter-newwidth/2.0
xmax = xcenter+newwidth/2.0
#self.axes.set_xlim((xcenter-newwidth/2.0, xcenter+newwidth/2.0))
for name in self.stuff:
self.stuff[name].axes.set_xlim((xmin, xmax))
self.stuff[name].axes.set_ylim((ymin, ymax))
self.axes.set_xlim((xmin, xmax))
self.axes.set_ylim((ymin, ymax))
self.canvas.queue_draw()
return
# def on_scroll_over_plot(self, event): # def on_scroll_over_plot(self, event):
# print "Scroll" # print "Scroll"
# center = [event.xdata, event.ydata] # center = [event.xdata, event.ydata]

View File

@@ -1752,33 +1752,6 @@
<object class="GtkMenu" id="menu2"> <object class="GtkMenu" id="menu2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="imagemenuitem6">
<property name="label">gtk-cut</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="imagemenuitem7">
<property name="label">gtk-copy</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="imagemenuitem8">
<property name="label">gtk-paste</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child> <child>
<object class="GtkImageMenuItem" id="imagemenuitem9"> <object class="GtkImageMenuItem" id="imagemenuitem9">
<property name="label">gtk-delete</property> <property name="label">gtk-delete</property>
@@ -1999,7 +1972,95 @@
<property name="vexpand">True</property> <property name="vexpand">True</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<placeholder/> <object class="GtkBox" id="box12">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label43">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="ypad">3</property>
<property name="label" translatable="yes">APLICATION DEFAULTS</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="grid7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
@@ -2119,7 +2180,7 @@
</child> </child>
<child> <child>
<object class="GtkLabel" id="label3"> <object class="GtkLabel" id="label3">
<property name="width_request">120</property> <property name="width_request">140</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">5</property>
@@ -2132,6 +2193,20 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="label_units">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="label" translatable="yes">[in]</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child> <child>
<object class="GtkProgressBar" id="progressbar"> <object class="GtkProgressBar" id="progressbar">
<property name="width_request">50</property> <property name="width_request">50</property>
@@ -2147,7 +2222,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">2</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
</object> </object>

3
defaults.json Normal file
View File

@@ -0,0 +1,3 @@
{
"units": "in"
}