cirkuix.py 48 KB

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