cirkuix.py 43 KB

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