cirkuix.py 36 KB

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