cirkuix.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. import threading
  2. from gi.repository import Gtk, Gdk, GLib, GObject
  3. from matplotlib.figure import Figure
  4. from numpy import arange, sin, pi
  5. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  6. #from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
  7. #from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
  8. from camlib import *
  9. ########################################
  10. ## CirkuixObj ##
  11. ########################################
  12. class CirkuixObj:
  13. """
  14. Base type of objects handled in Cirkuix. These become interactive
  15. in the GUI, can be plotted, and their options can be modified
  16. by the user in their respective forms.
  17. """
  18. form_getters = {}
  19. form_setters = {}
  20. # Instance of the application to which these are related.
  21. # The app should set this value.
  22. # TODO: Move the definitions of form_getters and form_setters
  23. # TODO: to their respective classes and use app to reference
  24. # TODO: the builder.
  25. app = None
  26. def __init__(self, name):
  27. self.options = {"name": name}
  28. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  29. self.radios = {} # Name value pairs for radio sets
  30. self.radios_inv = {} # Inverse of self.radios
  31. self.axes = None # Matplotlib axes
  32. self.kind = None # Override with proper name
  33. def setup_axes(self, figure):
  34. if self.axes is None:
  35. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  36. label=self.options["name"])
  37. elif self.axes not in figure.axes:
  38. figure.add_axes(self.axes)
  39. self.axes.patch.set_visible(False) # No background
  40. self.axes.set_aspect(1)
  41. return self.axes
  42. def set_options(self, options):
  43. for name in options:
  44. self.options[name] = options[name]
  45. return
  46. def to_form(self):
  47. for option in self.options:
  48. self.set_form_item(option)
  49. def read_form(self):
  50. for option in self.form_getters:
  51. self.read_form_item(option)
  52. def build_ui(self):
  53. """
  54. Sets up the UI/form for this object.
  55. @return: None
  56. @rtype : None
  57. """
  58. # Where the UI for this object is drawn
  59. box_selected = self.app.builder.get_object("box_selected")
  60. # Remove anything else in the box
  61. box_children = box_selected.get_children()
  62. for child in box_children:
  63. box_selected.remove(child)
  64. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  65. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  66. osw.remove(sw) # TODO: Is this needed ?
  67. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  68. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  69. # Put in the UI
  70. box_selected.pack_start(sw, True, True, 0)
  71. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  72. entry_name.connect("activate", self.app.on_activate_name)
  73. self.to_form()
  74. sw.show()
  75. def set_form_item(self, option):
  76. fkind = self.form_kinds[option]
  77. fname = fkind + "_" + self.kind + "_" + option
  78. if fkind == 'entry_eval' or fkind == 'entry_text':
  79. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  80. return
  81. if fkind == 'cb':
  82. self.app.builder.get_object(fname).set_active(self.options[option])
  83. return
  84. if fkind == 'radio':
  85. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  86. return
  87. print "Unknown kind of form item:", fkind
  88. def read_form_item(self, option):
  89. fkind = self.form_kinds[option]
  90. fname = fkind + "_" + self.kind + "_" + option
  91. if fkind == 'entry_text':
  92. self.options[option] = self.app.builder(fname).get_text()
  93. return
  94. if fkind == 'entry_eval':
  95. self.options[option] = self.app.get_eval(fname)
  96. return
  97. if fkind == 'cb':
  98. self.options[option] = self.app.builder.get_object(fname).get_active()
  99. return
  100. if fkind == 'radio':
  101. self.options[option] = self.app.get_radio_value(self.radios[option])
  102. return
  103. print "Unknown kind of form item:", fkind
  104. class CirkuixGerber(CirkuixObj, Gerber):
  105. """
  106. Represents Gerber code.
  107. """
  108. def __init__(self, name):
  109. Gerber.__init__(self)
  110. CirkuixObj.__init__(self, name)
  111. self.kind = "gerber"
  112. # The 'name' is already in self.options
  113. self.options.update({
  114. "plot": True,
  115. "mergepolys": True,
  116. "multicolored": False,
  117. "solid": False,
  118. "isotooldia": 0.4/25.4,
  119. "cutoutmargin": 0.2,
  120. "cutoutgapsize": 0.15,
  121. "gaps": "tb",
  122. "noncoppermargin": 0.0
  123. })
  124. self.form_kinds.update({
  125. "plot": "cb",
  126. "mergepolys": "cb",
  127. "multicolored": "cb",
  128. "solid": "cb",
  129. "isotooldia": "entry_eval",
  130. "cutoutmargin": "entry_eval",
  131. "cutoutgapsize": "entry_eval",
  132. "gaps": "radio",
  133. "noncoppermargin": "entry_eval"
  134. })
  135. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  136. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  137. def plot(self, figure):
  138. self.setup_axes(figure)
  139. self.create_geometry()
  140. geometry = None # TODO: Test if needed
  141. if self.options["mergepolys"]:
  142. geometry = self.solid_geometry
  143. else:
  144. geometry = self.buffered_paths + \
  145. [poly['polygon'] for poly in self.regions] + \
  146. self.flash_geometry
  147. linespec = None # TODO: Test if needed
  148. if self.options["multicolored"]:
  149. linespec = '-'
  150. else:
  151. linespec = 'k-'
  152. for poly in geometry:
  153. x, y = poly.exterior.xy
  154. self.axes.plot(x, y, linespec)
  155. for ints in poly.interiors:
  156. x, y = ints.coords.xy
  157. self.axes.plot(x, y, linespec)
  158. class CirkuixExcellon(CirkuixObj, Excellon):
  159. """
  160. Represents Excellon code.
  161. """
  162. def __init__(self, name):
  163. Excellon.__init__(self)
  164. CirkuixObj.__init__(self, name)
  165. self.kind = "excellon"
  166. self.options.update({
  167. "plot": True,
  168. "solid": False,
  169. "multicolored": False
  170. })
  171. self.form_kinds.update({
  172. "plot": "cb",
  173. "solid": "cb",
  174. "multicolored": "cb"
  175. })
  176. def plot(self, figure):
  177. self.setup_axes(figure)
  178. self.create_geometry()
  179. # Plot excellon
  180. for geo in self.solid_geometry:
  181. x, y = geo.exterior.coords.xy
  182. self.axes.plot(x, y, 'r-')
  183. for ints in geo.interiors:
  184. x, y = ints.coords.xy
  185. self.axes.plot(x, y, 'g-')
  186. class CirkuixCNCjob(CirkuixObj, CNCjob):
  187. """
  188. Represents G-Code.
  189. """
  190. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  191. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  192. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  193. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  194. CirkuixObj.__init__(self, name)
  195. self.kind = "cncjob"
  196. self.options.update({
  197. "plot": True,
  198. "solid": False,
  199. "multicolored": "cb",
  200. "tooldia": 0.4/25.4 # 0.4mm in inches
  201. })
  202. self.form_kinds.update({
  203. "plot": "cb",
  204. "solid": "cb",
  205. "multicolored": "cb",
  206. "tooldia": "entry_eval"
  207. })
  208. def plot(self, figure):
  209. self.setup_axes(figure)
  210. self.plot2(self.axes, tooldia=self.options["tooldia"])
  211. class CirkuixGeometry(CirkuixObj, Geometry):
  212. """
  213. Geometric object not associated with a specific
  214. format.
  215. """
  216. def __init__(self, name):
  217. CirkuixObj.__init__(self, name)
  218. self.kind = "geometry"
  219. self.options.update({
  220. "plot": True,
  221. "solid": False,
  222. "multicolored": False,
  223. "cutz": -0.002,
  224. "travelz": 0.1,
  225. "feedrate": 5.0
  226. })
  227. self.form_kinds.update({
  228. "plot": "cb",
  229. "solid": "cb",
  230. "multicolored": "cb",
  231. "cutz": "entry_eval",
  232. "travelz": "entry_eval",
  233. "feedrate": "entry_eval"
  234. })
  235. def plot(self, figure):
  236. self.setup_axes(figure)
  237. for geo in self.solid_geometry:
  238. if type(geo) == Polygon:
  239. x, y = geo.exterior.coords.xy
  240. self.axes.plot(x, y, 'r-')
  241. for ints in geo.interiors:
  242. x, y = ints.coords.xy
  243. self.axes.plot(x, y, 'r-')
  244. continue
  245. if type(geo) == LineString or type(geo) == LinearRing:
  246. x, y = geo.coords.xy
  247. self.axes.plot(x, y, 'r-')
  248. continue
  249. ########################################
  250. ## App ##
  251. ########################################
  252. class App:
  253. """
  254. The main application class. The constructor starts the GUI.
  255. """
  256. def __init__(self):
  257. """
  258. Starts the application and the Gtk.main().
  259. @return: app
  260. """
  261. # Needed to interact with the GUI from other threads.
  262. #GLib.threads_init()
  263. GObject.threads_init()
  264. #Gdk.threads_init()
  265. ########################################
  266. ## GUI ##
  267. ########################################
  268. self.gladefile = "cirkuix.ui"
  269. self.builder = Gtk.Builder()
  270. self.builder.add_from_file(self.gladefile)
  271. self.window = self.builder.get_object("window1")
  272. self.window.set_title("Cirkuix")
  273. self.position_label = self.builder.get_object("label3")
  274. self.grid = self.builder.get_object("grid1")
  275. self.notebook = self.builder.get_object("notebook1")
  276. self.info_label = self.builder.get_object("label_status")
  277. self.progress_bar = self.builder.get_object("progressbar")
  278. self.progress_bar.set_show_text(True)
  279. ## Event handling ##
  280. self.builder.connect_signals(self)
  281. ## Make plot area ##
  282. self.figure = None
  283. self.axes = None
  284. self.canvas = None
  285. self.setup_plot()
  286. self.setup_component_viewer()
  287. self.setup_component_editor()
  288. ########################################
  289. ## DATA ##
  290. ########################################
  291. self.setup_obj_classes()
  292. self.stuff = {} # CirkuixObj's by name
  293. self.mouse = None # Mouse coordinates over plot
  294. # What is selected by the user. It is
  295. # a key if self.stuff
  296. self.selected_item_name = None
  297. # For debugging only
  298. def someThreadFunc(self):
  299. print "Hello World!"
  300. t = threading.Thread(target=someThreadFunc, args=(self,))
  301. t.start()
  302. ########################################
  303. ## START ##
  304. ########################################
  305. self.window.show_all()
  306. #Gtk.main()
  307. def setup_plot(self):
  308. """
  309. Sets up the main plotting area by creating a matplotlib
  310. figure in self.canvas, adding axes and configuring them.
  311. These axes should not be ploted on and are just there to
  312. display the axes ticks and grid.
  313. @return: None
  314. """
  315. self.figure = Figure(dpi=50)
  316. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  317. self.axes.set_aspect(1)
  318. #t = arange(0.0,5.0,0.01)
  319. #s = sin(2*pi*t)
  320. #self.axes.plot(t,s)
  321. self.axes.grid()
  322. self.figure.patch.set_visible(False)
  323. self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
  324. self.canvas.set_hexpand(1)
  325. self.canvas.set_vexpand(1)
  326. # Events
  327. self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
  328. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  329. self.canvas.set_can_focus(True) # For key press
  330. self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
  331. #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
  332. self.grid.attach(self.canvas, 0, 0, 600, 400)
  333. def setup_obj_classes(self):
  334. CirkuixObj.app = self
  335. def setup_component_viewer(self):
  336. """
  337. List or Tree where whatever has been loaded or created is
  338. displayed.
  339. @return: None
  340. """
  341. self.store = Gtk.ListStore(str)
  342. self.tree = Gtk.TreeView(self.store)
  343. #self.list = Gtk.ListBox()
  344. self.tree_select = self.tree.get_selection()
  345. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  346. renderer = Gtk.CellRendererText()
  347. column = Gtk.TreeViewColumn("Title", renderer, text=0)
  348. self.tree.append_column(column)
  349. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  350. def setup_component_editor(self):
  351. """
  352. Initial configuration of the component editor. Creates
  353. a page titled "Selection" on the notebook on the left
  354. side of the main window.
  355. @return: None
  356. """
  357. box_selected = self.builder.get_object("box_selected")
  358. # Remove anything else in the box
  359. box_children = box_selected.get_children()
  360. for child in box_children:
  361. box_selected.remove(child)
  362. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  363. label1 = Gtk.Label("Choose an item from Project")
  364. box1.pack_start(label1, False, False, 1)
  365. box_selected.pack_start(box1, True, True, 0)
  366. def info(self, text):
  367. """
  368. Show text on the status bar.
  369. @return: None
  370. """
  371. self.info_label.set_text(text)
  372. def zoom(self, factor, center=None):
  373. """
  374. Zooms the plot by factor around a given
  375. center point. Takes care of re-drawing.
  376. @return: None
  377. """
  378. xmin, xmax = self.axes.get_xlim()
  379. ymin, ymax = self.axes.get_ylim()
  380. width = xmax-xmin
  381. height = ymax-ymin
  382. if center is None:
  383. center = [(xmin+xmax)/2.0, (ymin+ymax)/2.0]
  384. # For keeping the point at the pointer location
  385. relx = (xmax-center[0])/width
  386. rely = (ymax-center[1])/height
  387. new_width = width/factor
  388. new_height = height/factor
  389. xmin = center[0]-new_width*(1-relx)
  390. xmax = center[0]+new_width*relx
  391. ymin = center[1]-new_height*(1-rely)
  392. ymax = center[1]+new_height*rely
  393. for name in self.stuff:
  394. self.stuff[name].axes.set_xlim((xmin, xmax))
  395. self.stuff[name].axes.set_ylim((ymin, ymax))
  396. self.axes.set_xlim((xmin, xmax))
  397. self.axes.set_ylim((ymin, ymax))
  398. self.canvas.queue_draw()
  399. def build_list(self):
  400. """
  401. Clears and re-populates the list of objects in tcurrently
  402. in the project.
  403. @return: None
  404. """
  405. self.store.clear()
  406. for key in self.stuff:
  407. self.store.append([key])
  408. def get_radio_value(self, radio_set):
  409. """
  410. Returns the radio_set[key] if the radiobutton
  411. whose name is key is active.
  412. @return: radio_set[key]
  413. """
  414. for name in radio_set:
  415. if self.builder.get_object(name).get_active():
  416. return radio_set[name]
  417. def plot_all(self):
  418. """
  419. Re-generates all plots from all objects.
  420. @return: None
  421. """
  422. self.clear_plots()
  423. for i in self.stuff:
  424. self.stuff[i].plot(self.figure)
  425. self.on_zoom_fit(None)
  426. self.axes.grid()
  427. self.canvas.queue_draw()
  428. def clear_plots(self):
  429. """
  430. Clears self.axes and self.figure.
  431. @return: None
  432. """
  433. self.axes.cla()
  434. self.figure.clf()
  435. self.figure.add_axes(self.axes)
  436. self.canvas.queue_draw()
  437. def get_eval(self, widget_name):
  438. """
  439. Runs eval() on the on the text entry of name 'widget_name'
  440. and returns the results.
  441. @param widget_name: Name of Gtk.Entry
  442. @return: Depends on contents of the entry text.
  443. """
  444. value = self.builder.get_object(widget_name).get_text()
  445. return eval(value)
  446. def set_list_selection(self, name):
  447. """
  448. Marks a given object as selected in the list ob objects
  449. in the GUI. This selection will in turn trigger
  450. self.on_tree_selection_changed().
  451. @param name: Name of the object.
  452. @return: None
  453. """
  454. iter = self.store.get_iter_first()
  455. while iter is not None and self.store[iter][0] != name:
  456. iter = self.store.iter_next(iter)
  457. self.tree_select.unselect_all()
  458. self.tree_select.select_iter(iter)
  459. # Need to return False such that GLib.idle_add
  460. # or .timeout_add do not repear.
  461. return False
  462. def new_object(self, kind, name, initialize):
  463. """
  464. Creates a new specalized CirkuixObj and attaches it to the application,
  465. this is, updates the GUI accordingly, any other records and plots it.
  466. @param kind: Knd of object to create.
  467. @param name: Name for the object.
  468. @param initilize: Function to run after the
  469. object has been created but before attacing it
  470. to the application. Takes the new object and the
  471. app as parameters.
  472. @return: The object requested
  473. @rtype : CirkuixObj extended
  474. """
  475. # Check for existing name
  476. if name in self.stuff:
  477. return None
  478. # Create object
  479. classdict = {
  480. "gerber": CirkuixGerber,
  481. "excellon": CirkuixExcellon,
  482. "cncjob": CirkuixCNCjob,
  483. "geometry": CirkuixGeometry
  484. }
  485. obj = classdict[kind](name)
  486. # Initialize as per user request
  487. # User must take care to implement initialize
  488. # in a thread-safe way as is is likely that we
  489. # have been invoked in a separate thread.
  490. initialize(obj, self)
  491. # Add to our records
  492. self.stuff[name] = obj
  493. # Update GUI list and select it (Thread-safe?)
  494. self.store.append([name])
  495. #self.build_list()
  496. GLib.idle_add(lambda: self.set_list_selection(name))
  497. # TODO: Gtk.notebook.set_current_page is not known to
  498. # TODO: return False. Fix this??
  499. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  500. # Plot
  501. # TODO: (Thread-safe?)
  502. obj.plot(self.figure)
  503. obj.axes.set_alpha(0.0)
  504. self.on_zoom_fit(None)
  505. return obj
  506. def set_progress_bar(self, percentage, text=""):
  507. self.progress_bar.set_text(text)
  508. self.progress_bar.set_fraction(percentage)
  509. return False
  510. ########################################
  511. ## EVENT HANDLERS ##
  512. ########################################
  513. def on_gerber_generate_noncopper(self, widget):
  514. name = self.selected_item_name + "_noncopper"
  515. def geo_init(geo_obj, app_obj):
  516. assert isinstance(geo_obj, CirkuixGeometry)
  517. gerber = app_obj.stuff[self.selected_item_name]
  518. assert isinstance(gerber, CirkuixGerber)
  519. gerber.read_form()
  520. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
  521. non_copper = bounding_box.difference(gerber.solid_geometry)
  522. geo_obj.solid_geometry = non_copper
  523. # TODO: Check for None
  524. self.new_object("geometry", name, geo_init)
  525. def on_gerber_generate_cutout(self, widget):
  526. name = self.selected_item_name + "_cutout"
  527. def geo_init(geo_obj, app_obj):
  528. # TODO: get from object
  529. margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
  530. gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
  531. gerber = app_obj.stuff[app_obj.selected_item_name]
  532. minx, miny, maxx, maxy = gerber.bounds()
  533. minx -= margin
  534. maxx += margin
  535. miny -= margin
  536. maxy += margin
  537. midx = 0.5 * (minx + maxx)
  538. midy = 0.5 * (miny + maxy)
  539. hgap = 0.5 * gap_size
  540. pts = [[midx-hgap, maxy],
  541. [minx, maxy],
  542. [minx, midy+hgap],
  543. [minx, midy-hgap],
  544. [minx, miny],
  545. [midx-hgap, miny],
  546. [midx+hgap, miny],
  547. [maxx, miny],
  548. [maxx, midy-hgap],
  549. [maxx, midy+hgap],
  550. [maxx, maxy],
  551. [midx+hgap, maxy]]
  552. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  553. [pts[6], pts[7], pts[10], pts[11]]],
  554. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  555. [pts[3], pts[4], pts[7], pts[8]]],
  556. "4": [[pts[0], pts[1], pts[2]],
  557. [pts[3], pts[4], pts[5]],
  558. [pts[6], pts[7], pts[8]],
  559. [pts[9], pts[10], pts[11]]]}
  560. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  561. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  562. # TODO: Check for None
  563. self.new_object("geometry", name, geo_init)
  564. def on_eval_update(self, widget):
  565. """
  566. Modifies the content of a Gtk.Entry by running
  567. eval() on its contents and puting it back as a
  568. string.
  569. """
  570. # TODO: error handling here
  571. widget.set_text(str(eval(widget.get_text())))
  572. def on_generate_isolation(self, widget):
  573. print "Generating Isolation Geometry:"
  574. iso_name = self.selected_item_name + "_iso"
  575. def iso_init(geo_obj, app_obj):
  576. # TODO: Object must be updated on form change and the options
  577. # TODO: read from the object.
  578. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
  579. geo_obj.solid_geometry = self.stuff[self.selected_item_name].isolation_geometry(tooldia/2.0)
  580. # TODO: Do something if this is None. Offer changing name?
  581. self.new_object("geometry", iso_name, iso_init)
  582. def on_generate_cncjob(self, widget):
  583. print "Generating CNC job"
  584. job_name = self.selected_item_name + "_cnc"
  585. def job_init(job_obj, app_obj):
  586. # TODO: Object must be updated on form change and the options
  587. # TODO: read from the object.
  588. z_cut = app_obj.get_eval("entry_eval_geometry_cutz")
  589. z_move = app_obj.get_eval("entry_eval_geometry_travelz")
  590. feedrate = app_obj.get_eval("entry_eval_geometry_feedrate")
  591. geometry = app_obj.stuff[app_obj.selected_item_name]
  592. assert isinstance(job_obj, CirkuixCNCjob)
  593. job_obj.z_cut = z_cut
  594. job_obj.z_move = z_move
  595. job_obj.feedrate = feedrate
  596. job_obj.generate_from_geometry(geometry.solid_geometry)
  597. job_obj.gcode_parse()
  598. job_obj.create_geometry()
  599. self.new_object("cncjob", job_name, job_init)
  600. def on_cncjob_tooldia_activate(self, widget):
  601. job = self.stuff[self.selected_item_name]
  602. tooldia = self.get_eval("entry_eval_cncjob_tooldia")
  603. job.tooldia = tooldia
  604. print "Changing tool diameter to:", tooldia
  605. def on_cncjob_exportgcode(self, widget):
  606. def on_success(self, filename):
  607. cncjob = self.stuff[self.selected_item_name]
  608. f = open(filename, 'w')
  609. f.write(cncjob.gcode)
  610. f.close()
  611. print "Saved to:", filename
  612. self.file_chooser_save_action(on_success)
  613. def on_delete(self, widget):
  614. self.stuff.pop(self.selected_item_name)
  615. #self.tree.get_selection().disconnect(self.signal_id)
  616. self.build_list() # Update the items list
  617. #self.signal_id = self.tree.get_selection().connect(
  618. # "changed", self.on_tree_selection_changed)
  619. self.plot_all()
  620. #self.notebook.set_current_page(1)
  621. def on_replot(self, widget):
  622. self.plot_all()
  623. def on_clear_plots(self, widget):
  624. self.clear_plots()
  625. def on_activate_name(self, entry):
  626. """
  627. Hitting 'Enter' after changing the name of an item
  628. updates the item dictionary and re-builds the item list.
  629. """
  630. # Disconnect event listener
  631. self.tree.get_selection().disconnect(self.signal_id)
  632. new_name = entry.get_text() # Get from form
  633. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  634. self.stuff[new_name].options["name"] = new_name # update object
  635. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  636. self.selected_item_name = new_name # Update selection name
  637. self.build_list() # Update the items list
  638. # Reconnect event listener
  639. self.signal_id = self.tree.get_selection().connect(
  640. "changed", self.on_tree_selection_changed)
  641. def on_tree_selection_changed(self, selection):
  642. model, treeiter = selection.get_selected()
  643. if treeiter is not None:
  644. print "You selected", model[treeiter][0]
  645. self.selected_item_name = model[treeiter][0]
  646. #self.stuff[self.selected_item_name].build_ui()
  647. GLib.timeout_add(100, lambda: self.stuff[self.selected_item_name].build_ui())
  648. else:
  649. print "Nothing selected"
  650. self.selected_item_name = None
  651. self.setup_component_editor()
  652. def on_file_new(self, param):
  653. print "File->New not implemented yet."
  654. def on_filequit(self, param):
  655. print "quit from menu"
  656. self.window.destroy()
  657. Gtk.main_quit()
  658. def on_closewindow(self, param):
  659. print "quit from X"
  660. self.window.destroy()
  661. Gtk.main_quit()
  662. def file_chooser_action(self, on_success):
  663. '''
  664. Opens the file chooser and runs on_success on a separate thread
  665. upon completion of valid file choice.
  666. '''
  667. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  668. Gtk.FileChooserAction.OPEN,
  669. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  670. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  671. response = dialog.run()
  672. if response == Gtk.ResponseType.OK:
  673. filename = dialog.get_filename()
  674. dialog.destroy()
  675. t = threading.Thread(target=on_success, args=(self, filename))
  676. t.daemon = True
  677. t.start()
  678. #on_success(self, filename)
  679. elif response == Gtk.ResponseType.CANCEL:
  680. print("Cancel clicked")
  681. dialog.destroy()
  682. def file_chooser_save_action(self, on_success):
  683. """
  684. Opens the file chooser and runs on_success
  685. upon completion of valid file choice.
  686. """
  687. dialog = Gtk.FileChooserDialog("Save file", self.window,
  688. Gtk.FileChooserAction.SAVE,
  689. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  690. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  691. dialog.set_current_name("Untitled")
  692. response = dialog.run()
  693. if response == Gtk.ResponseType.OK:
  694. filename = dialog.get_filename()
  695. dialog.destroy()
  696. on_success(self, filename)
  697. elif response == Gtk.ResponseType.CANCEL:
  698. print("Cancel clicked")
  699. dialog.destroy()
  700. def on_fileopengerber(self, param):
  701. # IMPORTANT: on_success will run on a separate thread. Use
  702. # GLib.idle_add(function, **kwargs) to launch actions that will
  703. # updata the GUI.
  704. def on_success(app_obj, filename):
  705. assert isinstance(app_obj, App)
  706. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  707. def obj_init(gerber_obj, app_obj):
  708. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  709. gerber_obj.parse_file(filename)
  710. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  711. name = filename.split('/')[-1].split('\\')[-1]
  712. app_obj.new_object("gerber", name, obj_init)
  713. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  714. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  715. # on_success gets run on a separate thread
  716. self.file_chooser_action(on_success)
  717. def on_fileopenexcellon(self, param):
  718. # IMPORTANT: on_success will run on a separate thread. Use
  719. # GLib.idle_add(function, **kwargs) to launch actions that will
  720. # updata the GUI.
  721. def on_success(app_obj, filename):
  722. assert isinstance(app_obj, App)
  723. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  724. def obj_init(excellon_obj, app_obj):
  725. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  726. excellon_obj.parse_file(filename)
  727. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  728. name = filename.split('/')[-1].split('\\')[-1]
  729. app_obj.new_object("excellon", name, obj_init)
  730. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  731. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  732. # on_success gets run on a separate thread
  733. self.file_chooser_action(on_success)
  734. def on_fileopengcode(self, param):
  735. # IMPORTANT: on_success will run on a separate thread. Use
  736. # GLib.idle_add(function, **kwargs) to launch actions that will
  737. # updata the GUI.
  738. def on_success(app_obj, filename):
  739. assert isinstance(app_obj, App)
  740. def obj_init(job_obj, app_obj):
  741. assert isinstance(app_obj, App)
  742. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening G-Code ..."))
  743. f = open(filename)
  744. gcode = f.read()
  745. f.close()
  746. job_obj.gcode = gcode
  747. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  748. job_obj.gcode_parse()
  749. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating geometry ..."))
  750. job_obj.create_geometry()
  751. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  752. name = filename.split('/')[-1].split('\\')[-1]
  753. app_obj.new_object("cncjob", name, obj_init)
  754. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  755. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  756. # on_success gets run on a separate thread
  757. self.file_chooser_action(on_success)
  758. def on_mouse_move_over_plot(self, event):
  759. try: # May fail in case mouse not within axes
  760. self.position_label.set_label("X: %.4f Y: %.4f"%(
  761. event.xdata, event.ydata))
  762. self.mouse = [event.xdata, event.ydata]
  763. except:
  764. self.position_label.set_label("")
  765. self.mouse = None
  766. def on_click_over_plot(self, event):
  767. # For key presses
  768. self.canvas.grab_focus()
  769. try:
  770. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(
  771. event.button, event.x, event.y, event.xdata, event.ydata)
  772. except:
  773. print "Outside plot!"
  774. def on_zoom_in(self, event):
  775. self.zoom(1.5)
  776. return
  777. def on_zoom_out(self, event):
  778. self.zoom(1/1.5)
  779. def on_zoom_fit(self, event):
  780. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  781. width = xmax-xmin
  782. height = ymax-ymin
  783. r = width/height
  784. Fw, Fh = self.canvas.get_width_height()
  785. Fr = float(Fw)/Fh
  786. print "Window aspect ratio:", Fr
  787. print "Data aspect ratio:", r
  788. #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
  789. #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
  790. if r > Fr:
  791. #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
  792. xmin -= 0.05*width
  793. xmax += 0.05*width
  794. ycenter = (ymin+ymax)/2.0
  795. newheight = height*r/Fr
  796. ymin = ycenter-newheight/2.0
  797. ymax = ycenter+newheight/2.0
  798. #self.axes.set_ylim((ycenter-newheight/2.0, ycenter+newheight/2.0))
  799. else:
  800. #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
  801. ymin -= 0.05*height
  802. ymax += 0.05*height
  803. xcenter = (xmax+ymin)/2.0
  804. newwidth = width*Fr/r
  805. xmin = xcenter-newwidth/2.0
  806. xmax = xcenter+newwidth/2.0
  807. #self.axes.set_xlim((xcenter-newwidth/2.0, xcenter+newwidth/2.0))
  808. for name in self.stuff:
  809. self.stuff[name].axes.set_xlim((xmin, xmax))
  810. self.stuff[name].axes.set_ylim((ymin, ymax))
  811. self.axes.set_xlim((xmin, xmax))
  812. self.axes.set_ylim((ymin, ymax))
  813. self.canvas.queue_draw()
  814. return
  815. # def on_scroll_over_plot(self, event):
  816. # print "Scroll"
  817. # center = [event.xdata, event.ydata]
  818. # if sign(event.step):
  819. # self.zoom(1.5, center=center)
  820. # else:
  821. # self.zoom(1/1.5, center=center)
  822. #
  823. # def on_window_scroll(self, event):
  824. # print "Scroll"
  825. def on_key_over_plot(self, event):
  826. print 'you pressed', event.key, event.xdata, event.ydata
  827. if event.key == '1': # 1
  828. self.on_zoom_fit(None)
  829. return
  830. if event.key == '2': # 2
  831. self.zoom(1/1.5, self.mouse)
  832. return
  833. if event.key == '3': # 3
  834. self.zoom(1.5, self.mouse)
  835. return
  836. app = App()
  837. Gtk.main()