cirkuix.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  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. "painttooldia": 0.0625,
  227. "paintoverlap": 0.15
  228. })
  229. self.form_kinds.update({
  230. "plot": "cb",
  231. "solid": "cb",
  232. "multicolored": "cb",
  233. "cutz": "entry_eval",
  234. "travelz": "entry_eval",
  235. "feedrate": "entry_eval",
  236. "painttooldia": "entry_eval",
  237. "paintoverlap": "entry_eval"
  238. })
  239. def plot(self, figure):
  240. self.setup_axes(figure)
  241. for geo in self.solid_geometry:
  242. if type(geo) == Polygon:
  243. x, y = geo.exterior.coords.xy
  244. self.axes.plot(x, y, 'r-')
  245. for ints in geo.interiors:
  246. x, y = ints.coords.xy
  247. self.axes.plot(x, y, 'r-')
  248. continue
  249. if type(geo) == LineString or type(geo) == LinearRing:
  250. x, y = geo.coords.xy
  251. self.axes.plot(x, y, 'r-')
  252. continue
  253. ########################################
  254. ## App ##
  255. ########################################
  256. class App:
  257. """
  258. The main application class. The constructor starts the GUI.
  259. """
  260. def __init__(self):
  261. """
  262. Starts the application and the Gtk.main().
  263. @return: app
  264. """
  265. # Needed to interact with the GUI from other threads.
  266. #GLib.threads_init()
  267. GObject.threads_init()
  268. #Gdk.threads_init()
  269. ########################################
  270. ## GUI ##
  271. ########################################
  272. self.gladefile = "cirkuix.ui"
  273. self.builder = Gtk.Builder()
  274. self.builder.add_from_file(self.gladefile)
  275. self.window = self.builder.get_object("window1")
  276. self.window.set_title("Cirkuix")
  277. self.position_label = self.builder.get_object("label3")
  278. self.grid = self.builder.get_object("grid1")
  279. self.notebook = self.builder.get_object("notebook1")
  280. self.info_label = self.builder.get_object("label_status")
  281. self.progress_bar = self.builder.get_object("progressbar")
  282. self.progress_bar.set_show_text(True)
  283. ## Event handling ##
  284. self.builder.connect_signals(self)
  285. ## Make plot area ##
  286. self.figure = None
  287. self.axes = None
  288. self.canvas = None
  289. self.setup_plot()
  290. self.setup_component_viewer()
  291. self.setup_component_editor()
  292. ########################################
  293. ## DATA ##
  294. ########################################
  295. self.setup_obj_classes()
  296. self.stuff = {} # CirkuixObj's by name
  297. self.mouse = None # Mouse coordinates over plot
  298. # What is selected by the user. It is
  299. # a key if self.stuff
  300. self.selected_item_name = None
  301. self.plot_click_subscribers = {}
  302. # For debugging only
  303. def someThreadFunc(self):
  304. print "Hello World!"
  305. t = threading.Thread(target=someThreadFunc, args=(self,))
  306. t.start()
  307. ########################################
  308. ## START ##
  309. ########################################
  310. self.window.show_all()
  311. #Gtk.main()
  312. def setup_plot(self):
  313. """
  314. Sets up the main plotting area by creating a matplotlib
  315. figure in self.canvas, adding axes and configuring them.
  316. These axes should not be ploted on and are just there to
  317. display the axes ticks and grid.
  318. @return: None
  319. """
  320. self.figure = Figure(dpi=50)
  321. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  322. self.axes.set_aspect(1)
  323. #t = arange(0.0,5.0,0.01)
  324. #s = sin(2*pi*t)
  325. #self.axes.plot(t,s)
  326. self.axes.grid()
  327. self.figure.patch.set_visible(False)
  328. self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
  329. self.canvas.set_hexpand(1)
  330. self.canvas.set_vexpand(1)
  331. # Events
  332. self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
  333. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  334. self.canvas.set_can_focus(True) # For key press
  335. self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
  336. #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
  337. self.grid.attach(self.canvas, 0, 0, 600, 400)
  338. def setup_obj_classes(self):
  339. CirkuixObj.app = self
  340. def setup_component_viewer(self):
  341. """
  342. List or Tree where whatever has been loaded or created is
  343. displayed.
  344. @return: None
  345. """
  346. self.store = Gtk.ListStore(str)
  347. self.tree = Gtk.TreeView(self.store)
  348. #self.list = Gtk.ListBox()
  349. self.tree_select = self.tree.get_selection()
  350. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  351. renderer = Gtk.CellRendererText()
  352. column = Gtk.TreeViewColumn("Title", renderer, text=0)
  353. self.tree.append_column(column)
  354. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  355. def setup_component_editor(self):
  356. """
  357. Initial configuration of the component editor. Creates
  358. a page titled "Selection" on the notebook on the left
  359. side of the main window.
  360. @return: None
  361. """
  362. box_selected = self.builder.get_object("box_selected")
  363. # Remove anything else in the box
  364. box_children = box_selected.get_children()
  365. for child in box_children:
  366. box_selected.remove(child)
  367. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  368. label1 = Gtk.Label("Choose an item from Project")
  369. box1.pack_start(label1, False, False, 1)
  370. box_selected.pack_start(box1, True, True, 0)
  371. def info(self, text):
  372. """
  373. Show text on the status bar.
  374. @return: None
  375. """
  376. self.info_label.set_text(text)
  377. def zoom(self, factor, center=None):
  378. """
  379. Zooms the plot by factor around a given
  380. center point. Takes care of re-drawing.
  381. @return: None
  382. """
  383. xmin, xmax = self.axes.get_xlim()
  384. ymin, ymax = self.axes.get_ylim()
  385. width = xmax-xmin
  386. height = ymax-ymin
  387. if center is None:
  388. center = [(xmin+xmax)/2.0, (ymin+ymax)/2.0]
  389. # For keeping the point at the pointer location
  390. relx = (xmax-center[0])/width
  391. rely = (ymax-center[1])/height
  392. new_width = width/factor
  393. new_height = height/factor
  394. xmin = center[0]-new_width*(1-relx)
  395. xmax = center[0]+new_width*relx
  396. ymin = center[1]-new_height*(1-rely)
  397. ymax = center[1]+new_height*rely
  398. for name in self.stuff:
  399. self.stuff[name].axes.set_xlim((xmin, xmax))
  400. self.stuff[name].axes.set_ylim((ymin, ymax))
  401. self.axes.set_xlim((xmin, xmax))
  402. self.axes.set_ylim((ymin, ymax))
  403. self.canvas.queue_draw()
  404. def build_list(self):
  405. """
  406. Clears and re-populates the list of objects in tcurrently
  407. in the project.
  408. @return: None
  409. """
  410. self.store.clear()
  411. for key in self.stuff:
  412. self.store.append([key])
  413. def get_radio_value(self, radio_set):
  414. """
  415. Returns the radio_set[key] if the radiobutton
  416. whose name is key is active.
  417. @return: radio_set[key]
  418. """
  419. for name in radio_set:
  420. if self.builder.get_object(name).get_active():
  421. return radio_set[name]
  422. def plot_all(self):
  423. """
  424. Re-generates all plots from all objects.
  425. @return: None
  426. """
  427. self.clear_plots()
  428. for i in self.stuff:
  429. self.stuff[i].plot(self.figure)
  430. self.on_zoom_fit(None)
  431. self.axes.grid()
  432. self.canvas.queue_draw()
  433. def clear_plots(self):
  434. """
  435. Clears self.axes and self.figure.
  436. @return: None
  437. """
  438. self.axes.cla()
  439. self.figure.clf()
  440. self.figure.add_axes(self.axes)
  441. self.canvas.queue_draw()
  442. def get_eval(self, widget_name):
  443. """
  444. Runs eval() on the on the text entry of name 'widget_name'
  445. and returns the results.
  446. @param widget_name: Name of Gtk.Entry
  447. @return: Depends on contents of the entry text.
  448. """
  449. value = self.builder.get_object(widget_name).get_text()
  450. return eval(value)
  451. def set_list_selection(self, name):
  452. """
  453. Marks a given object as selected in the list ob objects
  454. in the GUI. This selection will in turn trigger
  455. self.on_tree_selection_changed().
  456. @param name: Name of the object.
  457. @return: None
  458. """
  459. iter = self.store.get_iter_first()
  460. while iter is not None and self.store[iter][0] != name:
  461. iter = self.store.iter_next(iter)
  462. self.tree_select.unselect_all()
  463. self.tree_select.select_iter(iter)
  464. # Need to return False such that GLib.idle_add
  465. # or .timeout_add do not repear.
  466. return False
  467. def new_object(self, kind, name, initialize):
  468. """
  469. Creates a new specalized CirkuixObj and attaches it to the application,
  470. this is, updates the GUI accordingly, any other records and plots it.
  471. @param kind: Knd of object to create.
  472. @param name: Name for the object.
  473. @param initilize: Function to run after the
  474. object has been created but before attacing it
  475. to the application. Takes the new object and the
  476. app as parameters.
  477. @return: The object requested
  478. @rtype : CirkuixObj extended
  479. """
  480. # Check for existing name
  481. if name in self.stuff:
  482. return None
  483. # Create object
  484. classdict = {
  485. "gerber": CirkuixGerber,
  486. "excellon": CirkuixExcellon,
  487. "cncjob": CirkuixCNCjob,
  488. "geometry": CirkuixGeometry
  489. }
  490. obj = classdict[kind](name)
  491. # Initialize as per user request
  492. # User must take care to implement initialize
  493. # in a thread-safe way as is is likely that we
  494. # have been invoked in a separate thread.
  495. initialize(obj, self)
  496. # Add to our records
  497. self.stuff[name] = obj
  498. # Update GUI list and select it (Thread-safe?)
  499. self.store.append([name])
  500. #self.build_list()
  501. GLib.idle_add(lambda: self.set_list_selection(name))
  502. # TODO: Gtk.notebook.set_current_page is not known to
  503. # TODO: return False. Fix this??
  504. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  505. # Plot
  506. # TODO: (Thread-safe?)
  507. obj.plot(self.figure)
  508. obj.axes.set_alpha(0.0)
  509. self.on_zoom_fit(None)
  510. return obj
  511. def set_progress_bar(self, percentage, text=""):
  512. self.progress_bar.set_text(text)
  513. self.progress_bar.set_fraction(percentage)
  514. return False
  515. ########################################
  516. ## EVENT HANDLERS ##
  517. ########################################
  518. def on_gerber_generate_noncopper(self, widget):
  519. name = self.selected_item_name + "_noncopper"
  520. def geo_init(geo_obj, app_obj):
  521. assert isinstance(geo_obj, CirkuixGeometry)
  522. gerber = app_obj.stuff[self.selected_item_name]
  523. assert isinstance(gerber, CirkuixGerber)
  524. gerber.read_form()
  525. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
  526. non_copper = bounding_box.difference(gerber.solid_geometry)
  527. geo_obj.solid_geometry = non_copper
  528. # TODO: Check for None
  529. self.new_object("geometry", name, geo_init)
  530. def on_gerber_generate_cutout(self, widget):
  531. name = self.selected_item_name + "_cutout"
  532. def geo_init(geo_obj, app_obj):
  533. # TODO: get from object
  534. margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
  535. gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
  536. gerber = app_obj.stuff[app_obj.selected_item_name]
  537. minx, miny, maxx, maxy = gerber.bounds()
  538. minx -= margin
  539. maxx += margin
  540. miny -= margin
  541. maxy += margin
  542. midx = 0.5 * (minx + maxx)
  543. midy = 0.5 * (miny + maxy)
  544. hgap = 0.5 * gap_size
  545. pts = [[midx-hgap, maxy],
  546. [minx, maxy],
  547. [minx, midy+hgap],
  548. [minx, midy-hgap],
  549. [minx, miny],
  550. [midx-hgap, miny],
  551. [midx+hgap, miny],
  552. [maxx, miny],
  553. [maxx, midy-hgap],
  554. [maxx, midy+hgap],
  555. [maxx, maxy],
  556. [midx+hgap, maxy]]
  557. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  558. [pts[6], pts[7], pts[10], pts[11]]],
  559. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  560. [pts[3], pts[4], pts[7], pts[8]]],
  561. "4": [[pts[0], pts[1], pts[2]],
  562. [pts[3], pts[4], pts[5]],
  563. [pts[6], pts[7], pts[8]],
  564. [pts[9], pts[10], pts[11]]]}
  565. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  566. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  567. # TODO: Check for None
  568. self.new_object("geometry", name, geo_init)
  569. def on_eval_update(self, widget):
  570. """
  571. Modifies the content of a Gtk.Entry by running
  572. eval() on its contents and puting it back as a
  573. string.
  574. """
  575. # TODO: error handling here
  576. widget.set_text(str(eval(widget.get_text())))
  577. def on_generate_isolation(self, widget):
  578. print "Generating Isolation Geometry:"
  579. iso_name = self.selected_item_name + "_iso"
  580. def iso_init(geo_obj, app_obj):
  581. # TODO: Object must be updated on form change and the options
  582. # TODO: read from the object.
  583. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
  584. geo_obj.solid_geometry = self.stuff[self.selected_item_name].isolation_geometry(tooldia/2.0)
  585. # TODO: Do something if this is None. Offer changing name?
  586. self.new_object("geometry", iso_name, iso_init)
  587. def on_generate_cncjob(self, widget):
  588. print "Generating CNC job"
  589. job_name = self.selected_item_name + "_cnc"
  590. def job_init(job_obj, app_obj):
  591. # TODO: Object must be updated on form change and the options
  592. # TODO: read from the object.
  593. z_cut = app_obj.get_eval("entry_eval_geometry_cutz")
  594. z_move = app_obj.get_eval("entry_eval_geometry_travelz")
  595. feedrate = app_obj.get_eval("entry_eval_geometry_feedrate")
  596. geometry = app_obj.stuff[app_obj.selected_item_name]
  597. assert isinstance(job_obj, CirkuixCNCjob)
  598. job_obj.z_cut = z_cut
  599. job_obj.z_move = z_move
  600. job_obj.feedrate = feedrate
  601. job_obj.generate_from_geometry(geometry.solid_geometry)
  602. job_obj.gcode_parse()
  603. job_obj.create_geometry()
  604. self.new_object("cncjob", job_name, job_init)
  605. def on_generate_paintarea(self, widget):
  606. self.info("Click inside the desired polygon.")
  607. geo = self.stuff[self.selected_item_name]
  608. geo.read_form()
  609. tooldia = geo.options["painttooldia"]
  610. overlap = geo.options["paintoverlap"]
  611. def doit(event):
  612. self.plot_click_subscribers.pop("generate_paintarea")
  613. self.info("")
  614. point = [event.xdata, event.ydata]
  615. poly = find_polygon(geo.solid_geometry, point)
  616. def gen_paintarea(geo_obj, app_obj):
  617. assert isinstance(geo_obj, CirkuixGeometry)
  618. assert isinstance(app_obj, App)
  619. cp = clear_poly(poly, tooldia, overlap)
  620. geo_obj.solid_geometry = cp
  621. name = self.selected_item_name + "_paint"
  622. self.new_object("geometry", name, gen_paintarea)
  623. self.plot_click_subscribers["generate_paintarea"] = doit
  624. def on_cncjob_tooldia_activate(self, widget):
  625. job = self.stuff[self.selected_item_name]
  626. tooldia = self.get_eval("entry_eval_cncjob_tooldia")
  627. job.tooldia = tooldia
  628. print "Changing tool diameter to:", tooldia
  629. def on_cncjob_exportgcode(self, widget):
  630. def on_success(self, filename):
  631. cncjob = self.stuff[self.selected_item_name]
  632. f = open(filename, 'w')
  633. f.write(cncjob.gcode)
  634. f.close()
  635. print "Saved to:", filename
  636. self.file_chooser_save_action(on_success)
  637. def on_delete(self, widget):
  638. self.stuff.pop(self.selected_item_name)
  639. #self.tree.get_selection().disconnect(self.signal_id)
  640. self.build_list() # Update the items list
  641. #self.signal_id = self.tree.get_selection().connect(
  642. # "changed", self.on_tree_selection_changed)
  643. self.plot_all()
  644. #self.notebook.set_current_page(1)
  645. def on_replot(self, widget):
  646. self.plot_all()
  647. def on_clear_plots(self, widget):
  648. self.clear_plots()
  649. def on_activate_name(self, entry):
  650. """
  651. Hitting 'Enter' after changing the name of an item
  652. updates the item dictionary and re-builds the item list.
  653. """
  654. # Disconnect event listener
  655. self.tree.get_selection().disconnect(self.signal_id)
  656. new_name = entry.get_text() # Get from form
  657. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  658. self.stuff[new_name].options["name"] = new_name # update object
  659. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  660. self.selected_item_name = new_name # Update selection name
  661. self.build_list() # Update the items list
  662. # Reconnect event listener
  663. self.signal_id = self.tree.get_selection().connect(
  664. "changed", self.on_tree_selection_changed)
  665. def on_tree_selection_changed(self, selection):
  666. model, treeiter = selection.get_selected()
  667. if treeiter is not None:
  668. print "You selected", model[treeiter][0]
  669. self.selected_item_name = model[treeiter][0]
  670. #self.stuff[self.selected_item_name].build_ui()
  671. GLib.timeout_add(100, lambda: self.stuff[self.selected_item_name].build_ui())
  672. else:
  673. print "Nothing selected"
  674. self.selected_item_name = None
  675. self.setup_component_editor()
  676. def on_file_new(self, param):
  677. print "File->New not implemented yet."
  678. def on_filequit(self, param):
  679. print "quit from menu"
  680. self.window.destroy()
  681. Gtk.main_quit()
  682. def on_closewindow(self, param):
  683. print "quit from X"
  684. self.window.destroy()
  685. Gtk.main_quit()
  686. def file_chooser_action(self, on_success):
  687. '''
  688. Opens the file chooser and runs on_success on a separate thread
  689. upon completion of valid file choice.
  690. '''
  691. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  692. Gtk.FileChooserAction.OPEN,
  693. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  694. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  695. response = dialog.run()
  696. if response == Gtk.ResponseType.OK:
  697. filename = dialog.get_filename()
  698. dialog.destroy()
  699. t = threading.Thread(target=on_success, args=(self, filename))
  700. t.daemon = True
  701. t.start()
  702. #on_success(self, filename)
  703. elif response == Gtk.ResponseType.CANCEL:
  704. print("Cancel clicked")
  705. dialog.destroy()
  706. def file_chooser_save_action(self, on_success):
  707. """
  708. Opens the file chooser and runs on_success
  709. upon completion of valid file choice.
  710. """
  711. dialog = Gtk.FileChooserDialog("Save file", self.window,
  712. Gtk.FileChooserAction.SAVE,
  713. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  714. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  715. dialog.set_current_name("Untitled")
  716. response = dialog.run()
  717. if response == Gtk.ResponseType.OK:
  718. filename = dialog.get_filename()
  719. dialog.destroy()
  720. on_success(self, filename)
  721. elif response == Gtk.ResponseType.CANCEL:
  722. print("Cancel clicked")
  723. dialog.destroy()
  724. def on_fileopengerber(self, param):
  725. # IMPORTANT: on_success will run on a separate thread. Use
  726. # GLib.idle_add(function, **kwargs) to launch actions that will
  727. # updata the GUI.
  728. def on_success(app_obj, filename):
  729. assert isinstance(app_obj, App)
  730. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  731. def obj_init(gerber_obj, app_obj):
  732. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  733. gerber_obj.parse_file(filename)
  734. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  735. name = filename.split('/')[-1].split('\\')[-1]
  736. app_obj.new_object("gerber", name, obj_init)
  737. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  738. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  739. # on_success gets run on a separate thread
  740. self.file_chooser_action(on_success)
  741. def on_fileopenexcellon(self, param):
  742. # IMPORTANT: on_success will run on a separate thread. Use
  743. # GLib.idle_add(function, **kwargs) to launch actions that will
  744. # updata the GUI.
  745. def on_success(app_obj, filename):
  746. assert isinstance(app_obj, App)
  747. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  748. def obj_init(excellon_obj, app_obj):
  749. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  750. excellon_obj.parse_file(filename)
  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("excellon", 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_fileopengcode(self, param):
  759. # IMPORTANT: on_success will run on a separate thread. Use
  760. # GLib.idle_add(function, **kwargs) to launch actions that will
  761. # updata the GUI.
  762. def on_success(app_obj, filename):
  763. assert isinstance(app_obj, App)
  764. def obj_init(job_obj, app_obj):
  765. assert isinstance(app_obj, App)
  766. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening G-Code ..."))
  767. f = open(filename)
  768. gcode = f.read()
  769. f.close()
  770. job_obj.gcode = gcode
  771. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  772. job_obj.gcode_parse()
  773. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating geometry ..."))
  774. job_obj.create_geometry()
  775. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  776. name = filename.split('/')[-1].split('\\')[-1]
  777. app_obj.new_object("cncjob", name, obj_init)
  778. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  779. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  780. # on_success gets run on a separate thread
  781. self.file_chooser_action(on_success)
  782. def on_mouse_move_over_plot(self, event):
  783. try: # May fail in case mouse not within axes
  784. self.position_label.set_label("X: %.4f Y: %.4f"%(
  785. event.xdata, event.ydata))
  786. self.mouse = [event.xdata, event.ydata]
  787. except:
  788. self.position_label.set_label("")
  789. self.mouse = None
  790. def on_click_over_plot(self, event):
  791. # For key presses
  792. self.canvas.grab_focus()
  793. try:
  794. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(
  795. event.button, event.x, event.y, event.xdata, event.ydata)
  796. for subscriber in self.plot_click_subscribers:
  797. self.plot_click_subscribers[subscriber](event)
  798. except:
  799. print "Outside plot!"
  800. def on_zoom_in(self, event):
  801. self.zoom(1.5)
  802. return
  803. def on_zoom_out(self, event):
  804. self.zoom(1/1.5)
  805. def on_zoom_fit(self, event):
  806. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  807. width = xmax-xmin
  808. height = ymax-ymin
  809. r = width/height
  810. Fw, Fh = self.canvas.get_width_height()
  811. Fr = float(Fw)/Fh
  812. print "Window aspect ratio:", Fr
  813. print "Data aspect ratio:", r
  814. #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
  815. #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
  816. if r > Fr:
  817. #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width))
  818. xmin -= 0.05*width
  819. xmax += 0.05*width
  820. ycenter = (ymin+ymax)/2.0
  821. newheight = height*r/Fr
  822. ymin = ycenter-newheight/2.0
  823. ymax = ycenter+newheight/2.0
  824. #self.axes.set_ylim((ycenter-newheight/2.0, ycenter+newheight/2.0))
  825. else:
  826. #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height))
  827. ymin -= 0.05*height
  828. ymax += 0.05*height
  829. xcenter = (xmax+ymin)/2.0
  830. newwidth = width*Fr/r
  831. xmin = xcenter-newwidth/2.0
  832. xmax = xcenter+newwidth/2.0
  833. #self.axes.set_xlim((xcenter-newwidth/2.0, xcenter+newwidth/2.0))
  834. for name in self.stuff:
  835. self.stuff[name].axes.set_xlim((xmin, xmax))
  836. self.stuff[name].axes.set_ylim((ymin, ymax))
  837. self.axes.set_xlim((xmin, xmax))
  838. self.axes.set_ylim((ymin, ymax))
  839. self.canvas.queue_draw()
  840. return
  841. # def on_scroll_over_plot(self, event):
  842. # print "Scroll"
  843. # center = [event.xdata, event.ydata]
  844. # if sign(event.step):
  845. # self.zoom(1.5, center=center)
  846. # else:
  847. # self.zoom(1/1.5, center=center)
  848. #
  849. # def on_window_scroll(self, event):
  850. # print "Scroll"
  851. def on_key_over_plot(self, event):
  852. print 'you pressed', event.key, event.xdata, event.ydata
  853. if event.key == '1': # 1
  854. self.on_zoom_fit(None)
  855. return
  856. if event.key == '2': # 2
  857. self.zoom(1/1.5, self.mouse)
  858. return
  859. if event.key == '3': # 3
  860. self.zoom(1.5, self.mouse)
  861. return
  862. app = App()
  863. Gtk.main()