cirkuix.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500
  1. import threading
  2. from gi.repository import Gtk, Gdk, GLib, GObject
  3. import simplejson as json
  4. from matplotlib.figure import Figure
  5. from numpy import arange, sin, pi
  6. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  7. #from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
  8. #from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
  9. from camlib import *
  10. ########################################
  11. ## CirkuixObj ##
  12. ########################################
  13. class CirkuixObj:
  14. """
  15. Base type of objects handled in Cirkuix. These become interactive
  16. in the GUI, can be plotted, and their options can be modified
  17. by the user in their respective forms.
  18. """
  19. # Instance of the application to which these are related.
  20. # The app should set this value.
  21. app = None
  22. def __init__(self, name):
  23. self.options = {"name": name}
  24. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  25. self.radios = {} # Name value pairs for radio sets
  26. self.radios_inv = {} # Inverse of self.radios
  27. self.axes = None # Matplotlib axes
  28. self.kind = None # Override with proper name
  29. def setup_axes(self, figure):
  30. """
  31. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  32. them to figure if not part of the figure. 4) Sets transparent
  33. background. 5) Sets 1:1 scale aspect ratio.
  34. @param figure: A Matplotlib.Figure on which to add/configure axes.
  35. @type figure: matplotlib.figure.Figure
  36. @return: None
  37. """
  38. if self.axes is None:
  39. print "New axes"
  40. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  41. label=self.options["name"])
  42. elif self.axes not in figure.axes:
  43. print "Clearing and attaching axes"
  44. self.axes.cla()
  45. figure.add_axes(self.axes)
  46. else:
  47. print "Clearing Axes"
  48. self.axes.cla()
  49. # Remove all decoration. The app's axes will have
  50. # the ticks and grid.
  51. self.axes.set_frame_on(False) # No frame
  52. self.axes.set_xticks([]) # No tick
  53. self.axes.set_yticks([]) # No ticks
  54. self.axes.patch.set_visible(False) # No background
  55. self.axes.set_aspect(1)
  56. def set_options(self, options):
  57. for name in options:
  58. self.options[name] = options[name]
  59. return
  60. def to_form(self):
  61. for option in self.options:
  62. self.set_form_item(option)
  63. def read_form(self):
  64. """
  65. Reads form into self.options
  66. @rtype : None
  67. """
  68. for option in self.options:
  69. self.read_form_item(option)
  70. def build_ui(self):
  71. """
  72. Sets up the UI/form for this object.
  73. @return: None
  74. @rtype : None
  75. """
  76. # Where the UI for this object is drawn
  77. box_selected = self.app.builder.get_object("box_selected")
  78. # Remove anything else in the box
  79. box_children = box_selected.get_children()
  80. for child in box_children:
  81. box_selected.remove(child)
  82. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  83. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  84. osw.remove(sw) # TODO: Is this needed ?
  85. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  86. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  87. # Put in the UI
  88. box_selected.pack_start(sw, True, True, 0)
  89. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  90. entry_name.connect("activate", self.app.on_activate_name)
  91. self.to_form()
  92. sw.show()
  93. def set_form_item(self, option):
  94. fkind = self.form_kinds[option]
  95. fname = fkind + "_" + self.kind + "_" + option
  96. if fkind == 'entry_eval' or fkind == 'entry_text':
  97. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  98. return
  99. if fkind == 'cb':
  100. self.app.builder.get_object(fname).set_active(self.options[option])
  101. return
  102. if fkind == 'radio':
  103. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  104. return
  105. print "Unknown kind of form item:", fkind
  106. def read_form_item(self, option):
  107. fkind = self.form_kinds[option]
  108. fname = fkind + "_" + self.kind + "_" + option
  109. if fkind == 'entry_text':
  110. self.options[option] = self.app.builder.get_object(fname).get_text()
  111. return
  112. if fkind == 'entry_eval':
  113. self.options[option] = self.app.get_eval(fname)
  114. return
  115. if fkind == 'cb':
  116. self.options[option] = self.app.builder.get_object(fname).get_active()
  117. return
  118. if fkind == 'radio':
  119. self.options[option] = self.app.get_radio_value(self.radios[option])
  120. return
  121. print "Unknown kind of form item:", fkind
  122. def plot(self, figure):
  123. """
  124. Extend this method! Sets up axes if needed and
  125. clears them. Descendants must do the actual plotting.
  126. """
  127. # Creates the axes if necessary and sets them up.
  128. self.setup_axes(figure)
  129. # Clear axes.
  130. # self.axes.cla()
  131. # return
  132. def serialize(self):
  133. """
  134. Returns a representation of the object as a dictionary so
  135. it can be later exported as JSON. Override this method.
  136. @return: Dictionary representing the object
  137. @rtype: dict
  138. """
  139. return
  140. def deserialize(self, obj_dict):
  141. """
  142. Re-builds an object from its serialized version.
  143. @param obj_dict: Dictionary representing a CirkuixObj
  144. @type obj_dict: dict
  145. @return None
  146. """
  147. return
  148. class CirkuixGerber(CirkuixObj, Gerber):
  149. """
  150. Represents Gerber code.
  151. """
  152. def __init__(self, name):
  153. Gerber.__init__(self)
  154. CirkuixObj.__init__(self, name)
  155. self.kind = "gerber"
  156. # The 'name' is already in self.options from CirkuixObj
  157. self.options.update({
  158. "plot": True,
  159. "mergepolys": True,
  160. "multicolored": False,
  161. "solid": False,
  162. "isotooldia": 0.4/25.4,
  163. "cutoutmargin": 0.2,
  164. "cutoutgapsize": 0.15,
  165. "gaps": "tb",
  166. "noncoppermargin": 0.0,
  167. "bboxmargin": 0.0,
  168. "bboxrounded": False
  169. })
  170. # The 'name' is already in self.form_kinds from CirkuixObj
  171. self.form_kinds.update({
  172. "plot": "cb",
  173. "mergepolys": "cb",
  174. "multicolored": "cb",
  175. "solid": "cb",
  176. "isotooldia": "entry_eval",
  177. "cutoutmargin": "entry_eval",
  178. "cutoutgapsize": "entry_eval",
  179. "gaps": "radio",
  180. "noncoppermargin": "entry_eval",
  181. "bboxmargin": "entry_eval",
  182. "bboxrounded": "cb"
  183. })
  184. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  185. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  186. def convert_units(self, units):
  187. factor = Gerber.convert_units(self, units)
  188. self.options['isotooldia'] *= factor
  189. self.options['cutoutmargin'] *= factor
  190. self.options['cutoutgapsize'] *= factor
  191. self.options['noncoppermargin'] *= factor
  192. self.options['bboxmargin'] *= factor
  193. def plot(self, figure):
  194. CirkuixObj.plot(self, figure)
  195. self.create_geometry()
  196. if self.options["mergepolys"]:
  197. geometry = self.solid_geometry
  198. else:
  199. geometry = self.buffered_paths + \
  200. [poly['polygon'] for poly in self.regions] + \
  201. self.flash_geometry
  202. if self.options["multicolored"]:
  203. linespec = '-'
  204. else:
  205. linespec = 'k-'
  206. for poly in geometry:
  207. x, y = poly.exterior.xy
  208. self.axes.plot(x, y, linespec)
  209. for ints in poly.interiors:
  210. x, y = ints.coords.xy
  211. self.axes.plot(x, y, linespec)
  212. self.app.canvas.queue_draw()
  213. def serialize(self):
  214. return {
  215. "options": self.options,
  216. "kind": self.kind
  217. }
  218. class CirkuixExcellon(CirkuixObj, Excellon):
  219. """
  220. Represents Excellon code.
  221. """
  222. def __init__(self, name):
  223. Excellon.__init__(self)
  224. CirkuixObj.__init__(self, name)
  225. self.kind = "excellon"
  226. self.options.update({
  227. "plot": True,
  228. "solid": False,
  229. "multicolored": False,
  230. "drillz": -0.1,
  231. "travelz": 0.1,
  232. "feedrate": 5.0,
  233. "toolselection": ""
  234. })
  235. self.form_kinds.update({
  236. "plot": "cb",
  237. "solid": "cb",
  238. "multicolored": "cb",
  239. "drillz": "entry_eval",
  240. "travelz": "entry_eval",
  241. "feedrate": "entry_eval",
  242. "toolselection": "entry_text"
  243. })
  244. self.tool_cbs = {}
  245. def convert_units(self, units):
  246. factor = Excellon.convert_units(self, units)
  247. self.options['drillz'] *= factor
  248. self.options['travelz'] *= factor
  249. self.options['feedrate'] *= factor
  250. def plot(self, figure):
  251. CirkuixObj.plot(self, figure)
  252. #self.setup_axes(figure)
  253. self.create_geometry()
  254. # Plot excellon
  255. for geo in self.solid_geometry:
  256. x, y = geo.exterior.coords.xy
  257. self.axes.plot(x, y, 'r-')
  258. for ints in geo.interiors:
  259. x, y = ints.coords.xy
  260. self.axes.plot(x, y, 'g-')
  261. self.app.on_zoom_fit(None)
  262. self.app.canvas.queue_draw()
  263. def show_tool_chooser(self):
  264. win = Gtk.Window()
  265. box = Gtk.Box(spacing=2)
  266. box.set_orientation(Gtk.Orientation(1))
  267. win.add(box)
  268. for tool in self.tools:
  269. self.tool_cbs[tool] = Gtk.CheckButton(label=tool+": "+self.tools[tool])
  270. box.pack_start(self.tool_cbs[tool], False, False, 1)
  271. button = Gtk.Button(label="Accept")
  272. box.pack_start(button, False, False, 1)
  273. win.show_all()
  274. def on_accept(widget):
  275. win.destroy()
  276. tool_list = []
  277. for tool in self.tool_cbs:
  278. if self.tool_cbs[tool].get_active():
  279. tool_list.append(tool)
  280. self.options["toolselection"] = ", ".join(tool_list)
  281. self.to_form()
  282. button.connect("activate", on_accept)
  283. button.connect("clicked", on_accept)
  284. # def parse_lines(self, elines):
  285. # Excellon.parse_lines(self, elines)
  286. # self.options["units"] = self.units
  287. class CirkuixCNCjob(CirkuixObj, CNCjob):
  288. """
  289. Represents G-Code.
  290. """
  291. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  292. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  293. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  294. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  295. CirkuixObj.__init__(self, name)
  296. self.kind = "cncjob"
  297. self.options.update({
  298. "plot": True,
  299. "solid": False,
  300. "multicolored": False,
  301. "tooldia": 0.4/25.4 # 0.4mm in inches
  302. })
  303. self.form_kinds.update({
  304. "plot": "cb",
  305. "solid": "cb",
  306. "multicolored": "cb",
  307. "tooldia": "entry_eval"
  308. })
  309. def plot(self, figure):
  310. CirkuixObj.plot(self, figure)
  311. #self.setup_axes(figure)
  312. self.plot2(self.axes, tooldia=self.options["tooldia"])
  313. self.app.on_zoom_fit(None)
  314. self.app.canvas.queue_draw()
  315. class CirkuixGeometry(CirkuixObj, Geometry):
  316. """
  317. Geometric object not associated with a specific
  318. format.
  319. """
  320. def __init__(self, name):
  321. CirkuixObj.__init__(self, name)
  322. Geometry.__init__(self)
  323. self.kind = "geometry"
  324. self.options.update({
  325. "plot": True,
  326. "solid": False,
  327. "multicolored": False,
  328. "cutz": -0.002,
  329. "travelz": 0.1,
  330. "feedrate": 5.0,
  331. "cnctooldia": 0.4/25.4,
  332. "painttooldia": 0.0625,
  333. "paintoverlap": 0.15,
  334. "paintmargin": 0.01
  335. })
  336. self.form_kinds.update({
  337. "plot": "cb",
  338. "solid": "cb",
  339. "multicolored": "cb",
  340. "cutz": "entry_eval",
  341. "travelz": "entry_eval",
  342. "feedrate": "entry_eval",
  343. "cnctooldia": "entry_eval",
  344. "painttooldia": "entry_eval",
  345. "paintoverlap": "entry_eval",
  346. "paintmargin": "entry_eval"
  347. })
  348. # def convert_units(self, units):
  349. # factor = Geometry.convert_units(self, units)
  350. def scale(self, factor):
  351. if type(self.solid_geometry) == list:
  352. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  353. for g in self.solid_geometry]
  354. else:
  355. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  356. origin=(0, 0))
  357. def convert_units(self, units):
  358. factor = Geometry.convert_units(self, units)
  359. self.options['cutz'] *= factor
  360. self.options['travelz'] *= factor
  361. self.options['feedrate'] *= factor
  362. self.options['cnctooldia'] *= factor
  363. self.options['painttooldia'] *= factor
  364. self.options['paintmargin'] *= factor
  365. return factor
  366. def plot(self, figure):
  367. CirkuixObj.plot(self, figure)
  368. #self.setup_axes(figure)
  369. try:
  370. _ = iter(self.solid_geometry)
  371. except TypeError:
  372. self.solid_geometry = [self.solid_geometry]
  373. for geo in self.solid_geometry:
  374. if type(geo) == Polygon:
  375. x, y = geo.exterior.coords.xy
  376. self.axes.plot(x, y, 'r-')
  377. for ints in geo.interiors:
  378. x, y = ints.coords.xy
  379. self.axes.plot(x, y, 'r-')
  380. continue
  381. if type(geo) == LineString or type(geo) == LinearRing:
  382. x, y = geo.coords.xy
  383. self.axes.plot(x, y, 'r-')
  384. continue
  385. if type(geo) == MultiPolygon:
  386. for poly in geo:
  387. x, y = poly.exterior.coords.xy
  388. self.axes.plot(x, y, 'r-')
  389. for ints in poly.interiors:
  390. x, y = ints.coords.xy
  391. self.axes.plot(x, y, 'r-')
  392. continue
  393. print "WARNING: Did not plot:", str(type(geo))
  394. self.app.on_zoom_fit(None)
  395. self.app.canvas.queue_draw()
  396. ########################################
  397. ## App ##
  398. ########################################
  399. class App:
  400. """
  401. The main application class. The constructor starts the GUI.
  402. """
  403. def __init__(self):
  404. """
  405. Starts the application and the Gtk.main().
  406. @return: app
  407. @rtype: App
  408. """
  409. # Needed to interact with the GUI from other threads.
  410. GObject.threads_init()
  411. ## GUI ##
  412. self.gladefile = "cirkuix.ui"
  413. self.builder = Gtk.Builder()
  414. self.builder.add_from_file(self.gladefile)
  415. self.window = self.builder.get_object("window1")
  416. self.window.set_title("Cirkuix")
  417. self.position_label = self.builder.get_object("label3")
  418. self.grid = self.builder.get_object("grid1")
  419. self.notebook = self.builder.get_object("notebook1")
  420. self.info_label = self.builder.get_object("label_status")
  421. self.progress_bar = self.builder.get_object("progressbar")
  422. self.progress_bar.set_show_text(True)
  423. self.units_label = self.builder.get_object("label_units")
  424. ## Event handling ##
  425. self.builder.connect_signals(self)
  426. ## Make plot area ##
  427. self.figure = None
  428. self.axes = None
  429. self.canvas = None
  430. self.setup_plot()
  431. self.setup_component_viewer()
  432. self.setup_component_editor()
  433. ## DATA ##
  434. self.setup_obj_classes()
  435. self.stuff = {} # CirkuixObj's by name
  436. self.mouse = None # Mouse coordinates over plot
  437. # What is selected by the user. It is
  438. # a key if self.stuff
  439. self.selected_item_name = None
  440. self.defaults = {
  441. "units": "in"
  442. } # Application defaults
  443. self.options = {} # Project options
  444. self.plot_click_subscribers = {}
  445. # Initialization
  446. self.load_defaults()
  447. self.options.update(self.defaults)
  448. self.units_label.set_text("[" + self.options["units"] + "]")
  449. # For debugging only
  450. def someThreadFunc(self):
  451. print "Hello World!"
  452. t = threading.Thread(target=someThreadFunc, args=(self,))
  453. t.start()
  454. ########################################
  455. ## START ##
  456. ########################################
  457. self.window.set_default_size(900, 600)
  458. self.window.show_all()
  459. def setup_plot(self):
  460. """
  461. Sets up the main plotting area by creating a matplotlib
  462. figure in self.canvas, adding axes and configuring them.
  463. These axes should not be ploted on and are just there to
  464. display the axes ticks and grid.
  465. @return: None
  466. """
  467. self.figure = Figure(dpi=50)
  468. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  469. self.axes.set_aspect(1)
  470. #t = arange(0.0,5.0,0.01)
  471. #s = sin(2*pi*t)
  472. #self.axes.plot(t,s)
  473. self.axes.grid(True)
  474. self.figure.patch.set_visible(False)
  475. self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
  476. self.canvas.set_hexpand(1)
  477. self.canvas.set_vexpand(1)
  478. # Events
  479. self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
  480. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  481. self.canvas.set_can_focus(True) # For key press
  482. self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
  483. #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
  484. self.canvas.connect("configure-event", self.on_canvas_configure)
  485. self.grid.attach(self.canvas, 0, 0, 600, 400)
  486. def setup_obj_classes(self):
  487. CirkuixObj.app = self
  488. def setup_component_viewer(self):
  489. """
  490. Sets up list or Tree where whatever has been loaded or created is
  491. displayed.
  492. @return: None
  493. """
  494. self.store = Gtk.ListStore(str)
  495. self.tree = Gtk.TreeView(self.store)
  496. #self.list = Gtk.ListBox()
  497. self.tree.connect("row_activated", self.on_row_activated)
  498. self.tree_select = self.tree.get_selection()
  499. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  500. renderer = Gtk.CellRendererText()
  501. column = Gtk.TreeViewColumn("Title", renderer, text=0)
  502. self.tree.append_column(column)
  503. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  504. def setup_component_editor(self):
  505. """
  506. Initial configuration of the component editor. Creates
  507. a page titled "Selection" on the notebook on the left
  508. side of the main window.
  509. @return: None
  510. """
  511. box_selected = self.builder.get_object("box_selected")
  512. # Remove anything else in the box
  513. box_children = box_selected.get_children()
  514. for child in box_children:
  515. box_selected.remove(child)
  516. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  517. label1 = Gtk.Label("Choose an item from Project")
  518. box1.pack_start(label1, True, False, 1)
  519. box_selected.pack_start(box1, True, True, 0)
  520. #box_selected.show()
  521. box1.show()
  522. label1.show()
  523. def info(self, text):
  524. """
  525. Show text on the status bar.
  526. @return: None
  527. """
  528. self.info_label.set_text(text)
  529. def zoom(self, factor, center=None):
  530. """
  531. Zooms the plot by factor around a given
  532. center point. Takes care of re-drawing.
  533. @return: None
  534. """
  535. xmin, xmax = self.axes.get_xlim()
  536. ymin, ymax = self.axes.get_ylim()
  537. width = xmax-xmin
  538. height = ymax-ymin
  539. if center is None:
  540. center = [(xmin+xmax)/2.0, (ymin+ymax)/2.0]
  541. # For keeping the point at the pointer location
  542. relx = (xmax-center[0])/width
  543. rely = (ymax-center[1])/height
  544. new_width = width/factor
  545. new_height = height/factor
  546. xmin = center[0]-new_width*(1-relx)
  547. xmax = center[0]+new_width*relx
  548. ymin = center[1]-new_height*(1-rely)
  549. ymax = center[1]+new_height*rely
  550. for name in self.stuff:
  551. self.stuff[name].axes.set_xlim((xmin, xmax))
  552. self.stuff[name].axes.set_ylim((ymin, ymax))
  553. self.axes.set_xlim((xmin, xmax))
  554. self.axes.set_ylim((ymin, ymax))
  555. self.canvas.queue_draw()
  556. def build_list(self):
  557. """
  558. Clears and re-populates the list of objects in currently
  559. in the project.
  560. @return: None
  561. """
  562. print "build_list(): clearing"
  563. self.tree_select.unselect_all()
  564. self.store.clear()
  565. print "repopulating...",
  566. for key in self.stuff:
  567. print key,
  568. self.store.append([key])
  569. print
  570. def get_radio_value(self, radio_set):
  571. """
  572. Returns the radio_set[key] if the radiobutton
  573. whose name is key is active.
  574. @return: radio_set[key]
  575. """
  576. for name in radio_set:
  577. if self.builder.get_object(name).get_active():
  578. return radio_set[name]
  579. def plot_all(self):
  580. """
  581. Re-generates all plots from all objects.
  582. @return: None
  583. """
  584. self.clear_plots()
  585. self.set_progress_bar(0.1, "Re-plotting...")
  586. def thread_func(app_obj):
  587. percentage = 0.1
  588. try:
  589. delta = 0.9/len(self.stuff)
  590. except ZeroDivisionError:
  591. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  592. return
  593. for i in self.stuff:
  594. self.stuff[i].plot(self.figure)
  595. percentage += delta
  596. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  597. self.on_zoom_fit(None)
  598. self.axes.grid(True)
  599. self.canvas.queue_draw()
  600. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  601. t = threading.Thread(target=thread_func, args=(self,))
  602. t.daemon = True
  603. t.start()
  604. def clear_plots(self):
  605. """
  606. Clears self.axes and self.figure.
  607. @return: None
  608. """
  609. # TODO: Create a setup_axes method that gets called here and in setup_plot?
  610. self.axes.cla()
  611. self.figure.clf()
  612. self.figure.add_axes(self.axes)
  613. self.axes.set_aspect(1)
  614. self.axes.grid(True)
  615. self.canvas.queue_draw()
  616. def get_eval(self, widget_name):
  617. """
  618. Runs eval() on the on the text entry of name 'widget_name'
  619. and returns the results.
  620. @param widget_name: Name of Gtk.Entry
  621. @return: Depends on contents of the entry text.
  622. """
  623. value = self.builder.get_object(widget_name).get_text()
  624. return eval(value)
  625. def set_list_selection(self, name):
  626. """
  627. Marks a given object as selected in the list ob objects
  628. in the GUI. This selection will in turn trigger
  629. self.on_tree_selection_changed().
  630. @param name: Name of the object.
  631. @return: None
  632. """
  633. iter = self.store.get_iter_first()
  634. while iter is not None and self.store[iter][0] != name:
  635. iter = self.store.iter_next(iter)
  636. self.tree_select.unselect_all()
  637. self.tree_select.select_iter(iter)
  638. # Need to return False such that GLib.idle_add
  639. # or .timeout_add do not repear.
  640. return False
  641. def new_object(self, kind, name, initialize):
  642. """
  643. Creates a new specalized CirkuixObj and attaches it to the application,
  644. this is, updates the GUI accordingly, any other records and plots it.
  645. :param kind: The kind of object to create. One of 'gerber',
  646. 'excellon', 'cncjob' and 'geometry'.
  647. :type kind: str
  648. :param name: Name for the object.
  649. :type name: str
  650. :param initialize: Function to run after creation of the object
  651. but before it is attached to the application. The function is
  652. called with 2 parameters: the new object and the App instance.
  653. :type initialize: function
  654. :return: None
  655. :rtype: None
  656. """
  657. # Check for existing name
  658. if name in self.stuff:
  659. self.info("Rename " + name + " in project first.")
  660. return None
  661. # Create object
  662. classdict = {
  663. "gerber": CirkuixGerber,
  664. "excellon": CirkuixExcellon,
  665. "cncjob": CirkuixCNCjob,
  666. "geometry": CirkuixGeometry
  667. }
  668. obj = classdict[kind](name)
  669. # Initialize as per user request
  670. # User must take care to implement initialize
  671. # in a thread-safe way as is is likely that we
  672. # have been invoked in a separate thread.
  673. initialize(obj, self)
  674. # Check units and convert if necessary
  675. if self.options["units"].upper() != obj.units.upper():
  676. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  677. obj.convert_units(self.options["units"])
  678. # Add to our records
  679. self.stuff[name] = obj
  680. # Update GUI list and select it (Thread-safe?)
  681. self.store.append([name])
  682. #self.build_list()
  683. GLib.idle_add(lambda: self.set_list_selection(name))
  684. # TODO: Gtk.notebook.set_current_page is not known to
  685. # TODO: return False. Fix this??
  686. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  687. # Plot
  688. # TODO: (Thread-safe?)
  689. obj.plot(self.figure)
  690. obj.axes.set_alpha(0.0)
  691. self.on_zoom_fit(None)
  692. return obj
  693. def set_progress_bar(self, percentage, text=""):
  694. self.progress_bar.set_text(text)
  695. self.progress_bar.set_fraction(percentage)
  696. return False
  697. def save_project(self):
  698. return
  699. def get_current(self):
  700. """
  701. Returns the currently selected CirkuixObj in the application.
  702. @return: Currently selected CirkuixObj in the application.
  703. @rtype: CirkuixObj
  704. """
  705. try:
  706. return self.stuff[self.selected_item_name]
  707. except:
  708. return None
  709. def adjust_axes(self, xmin, ymin, xmax, ymax):
  710. m_x = 15 # pixels
  711. m_y = 25 # pixels
  712. width = xmax-xmin
  713. height = ymax-ymin
  714. r = width/height
  715. Fw, Fh = self.canvas.get_width_height()
  716. Fr = float(Fw)/Fh
  717. x_ratio = float(m_x)/Fw
  718. y_ratio = float(m_y)/Fh
  719. if r > Fr:
  720. ycenter = (ymin+ymax)/2.0
  721. newheight = height*r/Fr
  722. ymin = ycenter-newheight/2.0
  723. ymax = ycenter+newheight/2.0
  724. else:
  725. xcenter = (xmax+ymin)/2.0
  726. newwidth = width*Fr/r
  727. xmin = xcenter-newwidth/2.0
  728. xmax = xcenter+newwidth/2.0
  729. for name in self.stuff:
  730. if self.stuff[name].axes is None:
  731. continue
  732. self.stuff[name].axes.set_xlim((xmin, xmax))
  733. self.stuff[name].axes.set_ylim((ymin, ymax))
  734. self.stuff[name].axes.set_position([x_ratio, y_ratio,
  735. 1-2*x_ratio, 1-2*y_ratio])
  736. self.axes.set_xlim((xmin, xmax))
  737. self.axes.set_ylim((ymin, ymax))
  738. self.axes.set_position([x_ratio, y_ratio,
  739. 1-2*x_ratio, 1-2*y_ratio])
  740. self.canvas.queue_draw()
  741. def load_defaults(self):
  742. try:
  743. f = open("defaults.json")
  744. options = f.read()
  745. f.close()
  746. except:
  747. self.info("ERROR: Could not load defaults file.")
  748. return
  749. try:
  750. defaults = json.loads(options)
  751. except:
  752. self.info("ERROR: Failed to parse defaults file.")
  753. return
  754. self.defaults.update(defaults)
  755. ########################################
  756. ## EVENT HANDLERS ##
  757. ########################################
  758. def on_scale_object(self, widget):
  759. obj = self.get_current()
  760. factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
  761. obj.scale(factor)
  762. obj.to_form()
  763. self.on_update_plot(None)
  764. def on_canvas_configure(self, widget, event):
  765. print "on_canvas_configure()"
  766. xmin, xmax = self.axes.get_xlim()
  767. ymin, ymax = self.axes.get_ylim()
  768. self.adjust_axes(xmin, ymin, xmax, ymax)
  769. def on_row_activated(self, widget, path, col):
  770. self.notebook.set_current_page(1)
  771. def on_generate_gerber_bounding_box(self, widget):
  772. gerber = self.get_current()
  773. gerber.read_form()
  774. name = self.selected_item_name + "_bbox"
  775. def geo_init(geo_obj, app_obj):
  776. assert isinstance(geo_obj, CirkuixGeometry)
  777. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
  778. if not gerber.options["bboxrounded"]:
  779. bounding_box = bounding_box.envelope
  780. geo_obj.solid_geometry = bounding_box
  781. self.new_object("geometry", name, geo_init)
  782. def on_update_plot(self, widget):
  783. """
  784. Callback for button on form for all kinds of objects.
  785. Re-plot the current object only.
  786. @param widget: The widget from which this was called.
  787. @return: None
  788. """
  789. print "Re-plotting"
  790. self.get_current().read_form()
  791. self.set_progress_bar(0.5, "Plotting...")
  792. #GLib.idle_add(lambda: self.set_progress_bar(0.5, "Plotting..."))
  793. def thread_func(app_obj):
  794. assert isinstance(app_obj, App)
  795. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
  796. #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
  797. app_obj.get_current().plot(app_obj.figure)
  798. GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
  799. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  800. t = threading.Thread(target=thread_func, args=(self,))
  801. t.daemon = True
  802. t.start()
  803. def on_generate_excellon_cncjob(self, widget):
  804. """
  805. Callback for button active/click on Excellon form to
  806. create a CNC Job for the Excellon file.
  807. @param widget: The widget from which this was called.
  808. @return: None
  809. """
  810. job_name = self.selected_item_name + "_cnc"
  811. excellon = self.get_current()
  812. assert isinstance(excellon, CirkuixExcellon)
  813. excellon.read_form()
  814. # Object initialization function for app.new_object()
  815. def job_init(job_obj, app_obj):
  816. excellon_ = self.get_current()
  817. assert isinstance(excellon_, CirkuixExcellon)
  818. assert isinstance(job_obj, CirkuixCNCjob)
  819. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  820. job_obj.z_cut = excellon_.options["drillz"]
  821. job_obj.z_move = excellon_.options["travelz"]
  822. job_obj.feedrate = excellon_.options["feedrate"]
  823. # There could be more than one drill size...
  824. # job_obj.tooldia = # TODO: duplicate variable!
  825. # job_obj.options["tooldia"] =
  826. job_obj.generate_from_excellon_by_tool(excellon_, excellon_.options["toolselection"])
  827. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  828. job_obj.gcode_parse()
  829. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  830. job_obj.create_geometry()
  831. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  832. # To be run in separate thread
  833. def job_thread(app_obj):
  834. app_obj.new_object("cncjob", job_name, job_init)
  835. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  836. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  837. # Start the thread
  838. t = threading.Thread(target=job_thread, args=(self,))
  839. t.daemon = True
  840. t.start()
  841. def on_excellon_tool_choose(self, widget):
  842. """
  843. Callback for button on Excellon form to open up a window for
  844. selecting tools.
  845. @param widget: The widget from which this was called.
  846. @return: None
  847. """
  848. excellon = self.get_current()
  849. assert isinstance(excellon, CirkuixExcellon)
  850. excellon.show_tool_chooser()
  851. def on_entry_eval_activate(self, widget):
  852. self.on_eval_update(widget)
  853. obj = self.get_current()
  854. assert isinstance(obj, CirkuixObj)
  855. obj.read_form()
  856. def on_gerber_generate_noncopper(self, widget):
  857. """
  858. Callback for button on Gerber form to create a geometry object
  859. with polygons covering the area without copper or negative of the
  860. Gerber.
  861. @param widget: The widget from which this was called.
  862. @return: None
  863. """
  864. name = self.selected_item_name + "_noncopper"
  865. def geo_init(geo_obj, app_obj):
  866. assert isinstance(geo_obj, CirkuixGeometry)
  867. gerber = app_obj.stuff[app_obj.selected_item_name]
  868. assert isinstance(gerber, CirkuixGerber)
  869. gerber.read_form()
  870. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
  871. non_copper = bounding_box.difference(gerber.solid_geometry)
  872. geo_obj.solid_geometry = non_copper
  873. # TODO: Check for None
  874. self.new_object("geometry", name, geo_init)
  875. def on_gerber_generate_cutout(self, widget):
  876. """
  877. Callback for button on Gerber form to create geometry with lines
  878. for cutting off the board.
  879. @param widget: The widget from which this was called.
  880. @return: None
  881. """
  882. name = self.selected_item_name + "_cutout"
  883. def geo_init(geo_obj, app_obj):
  884. # TODO: get from object
  885. margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
  886. gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
  887. gerber = app_obj.stuff[app_obj.selected_item_name]
  888. minx, miny, maxx, maxy = gerber.bounds()
  889. minx -= margin
  890. maxx += margin
  891. miny -= margin
  892. maxy += margin
  893. midx = 0.5 * (minx + maxx)
  894. midy = 0.5 * (miny + maxy)
  895. hgap = 0.5 * gap_size
  896. pts = [[midx-hgap, maxy],
  897. [minx, maxy],
  898. [minx, midy+hgap],
  899. [minx, midy-hgap],
  900. [minx, miny],
  901. [midx-hgap, miny],
  902. [midx+hgap, miny],
  903. [maxx, miny],
  904. [maxx, midy-hgap],
  905. [maxx, midy+hgap],
  906. [maxx, maxy],
  907. [midx+hgap, maxy]]
  908. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  909. [pts[6], pts[7], pts[10], pts[11]]],
  910. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  911. [pts[3], pts[4], pts[7], pts[8]]],
  912. "4": [[pts[0], pts[1], pts[2]],
  913. [pts[3], pts[4], pts[5]],
  914. [pts[6], pts[7], pts[8]],
  915. [pts[9], pts[10], pts[11]]]}
  916. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  917. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  918. # TODO: Check for None
  919. self.new_object("geometry", name, geo_init)
  920. def on_eval_update(self, widget):
  921. """
  922. Modifies the content of a Gtk.Entry by running
  923. eval() on its contents and puting it back as a
  924. string.
  925. @param widget: The widget from which this was called.
  926. @return: None
  927. """
  928. # TODO: error handling here
  929. widget.set_text(str(eval(widget.get_text())))
  930. def on_generate_isolation(self, widget):
  931. """
  932. Callback for button on Gerber form to create isolation routing geometry.
  933. @param widget: The widget from which this was called.
  934. @return: None
  935. """
  936. print "Generating Isolation Geometry:"
  937. iso_name = self.selected_item_name + "_iso"
  938. def iso_init(geo_obj, app_obj):
  939. # TODO: Object must be updated on form change and the options
  940. # TODO: read from the object.
  941. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
  942. geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia/2.0)
  943. # TODO: Do something if this is None. Offer changing name?
  944. self.new_object("geometry", iso_name, iso_init)
  945. def on_generate_cncjob(self, widget):
  946. """
  947. Callback for button on geometry form to generate CNC job.
  948. @param widget: The widget from which this was called.
  949. @return: None
  950. """
  951. print "Generating CNC job"
  952. job_name = self.selected_item_name + "_cnc"
  953. # Object initialization function for app.new_object()
  954. def job_init(job_obj, app_obj):
  955. assert isinstance(job_obj, CirkuixCNCjob)
  956. geometry = app_obj.stuff[app_obj.selected_item_name]
  957. assert isinstance(geometry, CirkuixGeometry)
  958. geometry.read_form()
  959. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  960. job_obj.z_cut = geometry.options["cutz"]
  961. job_obj.z_move = geometry.options["travelz"]
  962. job_obj.feedrate = geometry.options["feedrate"]
  963. job_obj.options["tooldia"] = geometry.options["cnctooldia"]
  964. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  965. job_obj.generate_from_geometry(geometry)
  966. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  967. job_obj.gcode_parse()
  968. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  969. job_obj.create_geometry()
  970. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  971. # To be run in separate thread
  972. def job_thread(app_obj):
  973. app_obj.new_object("cncjob", job_name, job_init)
  974. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  975. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  976. # Start the thread
  977. t = threading.Thread(target=job_thread, args=(self,))
  978. t.daemon = True
  979. t.start()
  980. def on_generate_paintarea(self, widget):
  981. """
  982. Callback for button on geometry form.
  983. Subscribes to the "Click on plot" event and continues
  984. after the click. Finds the polygon containing
  985. the clicked point and runs clear_poly() on it, resulting
  986. in a new CirkuixGeometry object.
  987. """
  988. self.info("Click inside the desired polygon.")
  989. geo = self.get_current()
  990. geo.read_form()
  991. tooldia = geo.options["painttooldia"]
  992. overlap = geo.options["paintoverlap"]
  993. # To be called after clicking on the plot.
  994. def doit(event):
  995. self.plot_click_subscribers.pop("generate_paintarea")
  996. self.info("")
  997. point = [event.xdata, event.ydata]
  998. poly = find_polygon(geo.solid_geometry, point)
  999. # Initializes the new geometry object
  1000. def gen_paintarea(geo_obj, app_obj):
  1001. assert isinstance(geo_obj, CirkuixGeometry)
  1002. assert isinstance(app_obj, App)
  1003. cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
  1004. geo_obj.solid_geometry = cp
  1005. name = self.selected_item_name + "_paint"
  1006. self.new_object("geometry", name, gen_paintarea)
  1007. self.plot_click_subscribers["generate_paintarea"] = doit
  1008. def on_cncjob_exportgcode(self, widget):
  1009. def on_success(self, filename):
  1010. cncjob = self.get_current()
  1011. f = open(filename, 'w')
  1012. f.write(cncjob.gcode)
  1013. f.close()
  1014. print "Saved to:", filename
  1015. self.file_chooser_save_action(on_success)
  1016. def on_delete(self, widget):
  1017. """
  1018. Delete the currently selected CirkuixObj.
  1019. @param widget: The widget from which this was called.
  1020. @return:
  1021. """
  1022. print "on_delete():", self.selected_item_name
  1023. # Remove plot
  1024. self.figure.delaxes(self.get_current().axes)
  1025. self.canvas.queue_draw()
  1026. # Remove from dictionary
  1027. self.stuff.pop(self.selected_item_name)
  1028. # Update UI
  1029. self.build_list() # Update the items list
  1030. def on_replot(self, widget):
  1031. self.plot_all()
  1032. def on_clear_plots(self, widget):
  1033. self.clear_plots()
  1034. def on_activate_name(self, entry):
  1035. """
  1036. Hitting 'Enter' after changing the name of an item
  1037. updates the item dictionary and re-builds the item list.
  1038. """
  1039. # Disconnect event listener
  1040. self.tree.get_selection().disconnect(self.signal_id)
  1041. new_name = entry.get_text() # Get from form
  1042. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  1043. self.stuff[new_name].options["name"] = new_name # update object
  1044. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  1045. self.selected_item_name = new_name # Update selection name
  1046. self.build_list() # Update the items list
  1047. # Reconnect event listener
  1048. self.signal_id = self.tree.get_selection().connect(
  1049. "changed", self.on_tree_selection_changed)
  1050. def on_tree_selection_changed(self, selection):
  1051. """
  1052. Callback for selection change in the project list. This changes
  1053. the currently selected CirkuixObj.
  1054. @param selection: Selection associated to the project tree or list
  1055. @type selection: Gtk.TreeSelection
  1056. @return: None
  1057. """
  1058. print "on_tree_selection_change(): ",
  1059. model, treeiter = selection.get_selected()
  1060. if treeiter is not None:
  1061. # Save data for previous selection
  1062. obj = self.get_current()
  1063. if obj is not None:
  1064. obj.read_form()
  1065. print "You selected", model[treeiter][0]
  1066. self.selected_item_name = model[treeiter][0]
  1067. GLib.idle_add(lambda: self.get_current().build_ui())
  1068. else:
  1069. print "Nothing selected"
  1070. self.selected_item_name = None
  1071. self.setup_component_editor()
  1072. def on_file_new(self, param):
  1073. # Remove everythong from memory
  1074. # Clear plot
  1075. self.clear_plots()
  1076. # Clear object editor
  1077. #self.setup_component_editor()
  1078. # Clear data
  1079. self.stuff = {}
  1080. # Clear list
  1081. #self.tree_select.unselect_all()
  1082. self.build_list()
  1083. #print "File->New not implemented yet."
  1084. def on_filequit(self, param):
  1085. print "quit from menu"
  1086. self.window.destroy()
  1087. Gtk.main_quit()
  1088. def on_closewindow(self, param):
  1089. print "quit from X"
  1090. self.window.destroy()
  1091. Gtk.main_quit()
  1092. def file_chooser_action(self, on_success):
  1093. """
  1094. Opens the file chooser and runs on_success on a separate thread
  1095. upon completion of valid file choice.
  1096. """
  1097. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1098. Gtk.FileChooserAction.OPEN,
  1099. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1100. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1101. response = dialog.run()
  1102. if response == Gtk.ResponseType.OK:
  1103. filename = dialog.get_filename()
  1104. dialog.destroy()
  1105. t = threading.Thread(target=on_success, args=(self, filename))
  1106. t.daemon = True
  1107. t.start()
  1108. #on_success(self, filename)
  1109. elif response == Gtk.ResponseType.CANCEL:
  1110. print("Cancel clicked")
  1111. dialog.destroy()
  1112. def file_chooser_save_action(self, on_success):
  1113. """
  1114. Opens the file chooser and runs on_success
  1115. upon completion of valid file choice.
  1116. """
  1117. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1118. Gtk.FileChooserAction.SAVE,
  1119. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1120. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1121. dialog.set_current_name("Untitled")
  1122. response = dialog.run()
  1123. if response == Gtk.ResponseType.OK:
  1124. filename = dialog.get_filename()
  1125. dialog.destroy()
  1126. on_success(self, filename)
  1127. elif response == Gtk.ResponseType.CANCEL:
  1128. print("Cancel clicked")
  1129. dialog.destroy()
  1130. def on_fileopengerber(self, param):
  1131. # IMPORTANT: on_success will run on a separate thread. Use
  1132. # GLib.idle_add(function, **kwargs) to launch actions that will
  1133. # updata the GUI.
  1134. def on_success(app_obj, filename):
  1135. assert isinstance(app_obj, App)
  1136. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  1137. def obj_init(gerber_obj, app_obj):
  1138. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1139. gerber_obj.parse_file(filename)
  1140. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1141. name = filename.split('/')[-1].split('\\')[-1]
  1142. app_obj.new_object("gerber", name, obj_init)
  1143. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1144. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1145. # on_success gets run on a separate thread
  1146. self.file_chooser_action(on_success)
  1147. def on_fileopenexcellon(self, param):
  1148. # IMPORTANT: on_success will run on a separate thread. Use
  1149. # GLib.idle_add(function, **kwargs) to launch actions that will
  1150. # updata the GUI.
  1151. def on_success(app_obj, filename):
  1152. assert isinstance(app_obj, App)
  1153. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  1154. def obj_init(excellon_obj, app_obj):
  1155. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1156. excellon_obj.parse_file(filename)
  1157. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1158. name = filename.split('/')[-1].split('\\')[-1]
  1159. app_obj.new_object("excellon", name, obj_init)
  1160. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1161. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1162. # on_success gets run on a separate thread
  1163. self.file_chooser_action(on_success)
  1164. def on_fileopengcode(self, param):
  1165. # IMPORTANT: on_success will run on a separate thread. Use
  1166. # GLib.idle_add(function, **kwargs) to launch actions that will
  1167. # updata the GUI.
  1168. def on_success(app_obj, filename):
  1169. assert isinstance(app_obj, App)
  1170. def obj_init(job_obj, app_obj):
  1171. assert isinstance(app_obj, App)
  1172. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening G-Code ..."))
  1173. f = open(filename)
  1174. gcode = f.read()
  1175. f.close()
  1176. job_obj.gcode = gcode
  1177. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1178. job_obj.gcode_parse()
  1179. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating geometry ..."))
  1180. job_obj.create_geometry()
  1181. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1182. name = filename.split('/')[-1].split('\\')[-1]
  1183. app_obj.new_object("cncjob", name, obj_init)
  1184. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1185. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1186. # on_success gets run on a separate thread
  1187. self.file_chooser_action(on_success)
  1188. def on_mouse_move_over_plot(self, event):
  1189. try: # May fail in case mouse not within axes
  1190. self.position_label.set_label("X: %.4f Y: %.4f"%(
  1191. event.xdata, event.ydata))
  1192. self.mouse = [event.xdata, event.ydata]
  1193. except:
  1194. self.position_label.set_label("")
  1195. self.mouse = None
  1196. def on_click_over_plot(self, event):
  1197. # For key presses
  1198. self.canvas.grab_focus()
  1199. try:
  1200. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(
  1201. event.button, event.x, event.y, event.xdata, event.ydata)
  1202. for subscriber in self.plot_click_subscribers:
  1203. self.plot_click_subscribers[subscriber](event)
  1204. except Exception, e:
  1205. print "Outside plot!"
  1206. def on_zoom_in(self, event):
  1207. self.zoom(1.5)
  1208. return
  1209. def on_zoom_out(self, event):
  1210. self.zoom(1/1.5)
  1211. def on_zoom_fit(self, event):
  1212. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  1213. width = xmax-xmin
  1214. height = ymax-ymin
  1215. xmin -= 0.05*width
  1216. xmax += 0.05*width
  1217. ymin -= 0.05*height
  1218. ymax += 0.05*height
  1219. self.adjust_axes(xmin, ymin, xmax, ymax)
  1220. # def on_scroll_over_plot(self, event):
  1221. # print "Scroll"
  1222. # center = [event.xdata, event.ydata]
  1223. # if sign(event.step):
  1224. # self.zoom(1.5, center=center)
  1225. # else:
  1226. # self.zoom(1/1.5, center=center)
  1227. #
  1228. # def on_window_scroll(self, event):
  1229. # print "Scroll"
  1230. def on_key_over_plot(self, event):
  1231. print 'you pressed', event.key, event.xdata, event.ydata
  1232. if event.key == '1': # 1
  1233. self.on_zoom_fit(None)
  1234. return
  1235. if event.key == '2': # 2
  1236. self.zoom(1/1.5, self.mouse)
  1237. return
  1238. if event.key == '3': # 3
  1239. self.zoom(1.5, self.mouse)
  1240. return
  1241. app = App()
  1242. Gtk.main()