cirkuix.py 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213
  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. import sys
  11. ########################################
  12. ## CirkuixObj ##
  13. ########################################
  14. class CirkuixObj:
  15. """
  16. Base type of objects handled in Cirkuix. These become interactive
  17. in the GUI, can be plotted, and their options can be modified
  18. by the user in their respective forms.
  19. """
  20. # Instance of the application to which these are related.
  21. # The app should set this value.
  22. app = None
  23. def __init__(self, name):
  24. self.options = {"name": name}
  25. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  26. self.radios = {} # Name value pairs for radio sets
  27. self.radios_inv = {} # Inverse of self.radios
  28. self.axes = None # Matplotlib axes
  29. self.kind = None # Override with proper name
  30. def setup_axes(self, figure):
  31. """
  32. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  33. them to figure if not part of the figure. 4) Sets transparent
  34. background. 5) Sets 1:1 scale aspect ratio.
  35. :param figure: A Matplotlib.Figure on which to add/configure axes.
  36. :type figure: matplotlib.figure.Figure
  37. :return: None
  38. :rtype: None
  39. """
  40. if self.axes is None:
  41. print "New axes"
  42. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  43. label=self.options["name"])
  44. elif self.axes not in figure.axes:
  45. print "Clearing and attaching axes"
  46. self.axes.cla()
  47. figure.add_axes(self.axes)
  48. else:
  49. print "Clearing Axes"
  50. self.axes.cla()
  51. # Remove all decoration. The app's axes will have
  52. # the ticks and grid.
  53. self.axes.set_frame_on(False) # No frame
  54. self.axes.set_xticks([]) # No tick
  55. self.axes.set_yticks([]) # No ticks
  56. self.axes.patch.set_visible(False) # No background
  57. self.axes.set_aspect(1)
  58. def to_form(self):
  59. """
  60. Copies options to the UI form.
  61. :return: None
  62. """
  63. for option in self.options:
  64. self.set_form_item(option)
  65. def read_form(self):
  66. """
  67. Reads form into ``self.options``.
  68. :return: None
  69. :rtype: None
  70. """
  71. for option in self.options:
  72. self.read_form_item(option)
  73. def build_ui(self):
  74. """
  75. Sets up the UI/form for this object.
  76. :return: None
  77. :rtype: None
  78. """
  79. # Where the UI for this object is drawn
  80. box_selected = self.app.builder.get_object("box_selected")
  81. # Remove anything else in the box
  82. box_children = box_selected.get_children()
  83. for child in box_children:
  84. box_selected.remove(child)
  85. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  86. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  87. osw.remove(sw) # TODO: Is this needed ?
  88. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  89. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  90. # Put in the UI
  91. box_selected.pack_start(sw, True, True, 0)
  92. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  93. entry_name.connect("activate", self.app.on_activate_name)
  94. self.to_form()
  95. sw.show()
  96. def set_form_item(self, option):
  97. """
  98. Copies the specified options to the UI form.
  99. :param option: Name of the option (Key in ``self.options``).
  100. :type option: str
  101. :return: None
  102. """
  103. fkind = self.form_kinds[option]
  104. fname = fkind + "_" + self.kind + "_" + option
  105. if fkind == 'entry_eval' or fkind == 'entry_text':
  106. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  107. return
  108. if fkind == 'cb':
  109. self.app.builder.get_object(fname).set_active(self.options[option])
  110. return
  111. if fkind == 'radio':
  112. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  113. return
  114. print "Unknown kind of form item:", fkind
  115. def read_form_item(self, option):
  116. fkind = self.form_kinds[option]
  117. fname = fkind + "_" + self.kind + "_" + option
  118. if fkind == 'entry_text':
  119. self.options[option] = self.app.builder.get_object(fname).get_text()
  120. return
  121. if fkind == 'entry_eval':
  122. self.options[option] = self.app.get_eval(fname)
  123. return
  124. if fkind == 'cb':
  125. self.options[option] = self.app.builder.get_object(fname).get_active()
  126. return
  127. if fkind == 'radio':
  128. self.options[option] = self.app.get_radio_value(self.radios[option])
  129. return
  130. print "Unknown kind of form item:", fkind
  131. def plot(self, figure):
  132. """
  133. Extend this method! Sets up axes if needed and
  134. clears them. Descendants must do the actual plotting.
  135. """
  136. # Creates the axes if necessary and sets them up.
  137. self.setup_axes(figure)
  138. # Clear axes.
  139. # self.axes.cla()
  140. # return
  141. def serialize(self):
  142. """
  143. Returns a representation of the object as a dictionary so
  144. it can be later exported as JSON. Override this method.
  145. @return: Dictionary representing the object
  146. @rtype: dict
  147. """
  148. return
  149. def deserialize(self, obj_dict):
  150. """
  151. Re-builds an object from its serialized version.
  152. @param obj_dict: Dictionary representing a CirkuixObj
  153. @type obj_dict: dict
  154. @return None
  155. """
  156. return
  157. class CirkuixGerber(CirkuixObj, Gerber):
  158. """
  159. Represents Gerber code.
  160. """
  161. def __init__(self, name):
  162. Gerber.__init__(self)
  163. CirkuixObj.__init__(self, name)
  164. self.kind = "gerber"
  165. # The 'name' is already in self.options from CirkuixObj
  166. self.options.update({
  167. "plot": True,
  168. "mergepolys": True,
  169. "multicolored": False,
  170. "solid": False,
  171. "isotooldia": 0.4 / 25.4,
  172. "cutoutmargin": 0.2,
  173. "cutoutgapsize": 0.15,
  174. "gaps": "tb",
  175. "noncoppermargin": 0.0,
  176. "bboxmargin": 0.0,
  177. "bboxrounded": False
  178. })
  179. # The 'name' is already in self.form_kinds from CirkuixObj
  180. self.form_kinds.update({
  181. "plot": "cb",
  182. "mergepolys": "cb",
  183. "multicolored": "cb",
  184. "solid": "cb",
  185. "isotooldia": "entry_eval",
  186. "cutoutmargin": "entry_eval",
  187. "cutoutgapsize": "entry_eval",
  188. "gaps": "radio",
  189. "noncoppermargin": "entry_eval",
  190. "bboxmargin": "entry_eval",
  191. "bboxrounded": "cb"
  192. })
  193. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  194. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  195. # Attributes to be included in serialization
  196. # Always append to it because it carries contents
  197. # from predecessors.
  198. self.ser_attrs += ['options', 'kind']
  199. def convert_units(self, units):
  200. factor = Gerber.convert_units(self, units)
  201. self.options['isotooldia'] *= factor
  202. self.options['cutoutmargin'] *= factor
  203. self.options['cutoutgapsize'] *= factor
  204. self.options['noncoppermargin'] *= factor
  205. self.options['bboxmargin'] *= factor
  206. def plot(self, figure):
  207. CirkuixObj.plot(self, figure)
  208. self.create_geometry()
  209. if self.options["mergepolys"]:
  210. geometry = self.solid_geometry
  211. else:
  212. geometry = self.buffered_paths + \
  213. [poly['polygon'] for poly in self.regions] + \
  214. self.flash_geometry
  215. if self.options["multicolored"]:
  216. linespec = '-'
  217. else:
  218. linespec = 'k-'
  219. for poly in geometry:
  220. x, y = poly.exterior.xy
  221. self.axes.plot(x, y, linespec)
  222. for ints in poly.interiors:
  223. x, y = ints.coords.xy
  224. self.axes.plot(x, y, linespec)
  225. self.app.canvas.queue_draw()
  226. def serialize(self):
  227. return {
  228. "options": self.options,
  229. "kind": self.kind
  230. }
  231. class CirkuixExcellon(CirkuixObj, Excellon):
  232. """
  233. Represents Excellon code.
  234. """
  235. def __init__(self, name):
  236. Excellon.__init__(self)
  237. CirkuixObj.__init__(self, name)
  238. self.kind = "excellon"
  239. self.options.update({
  240. "plot": True,
  241. "solid": False,
  242. "multicolored": False,
  243. "drillz": -0.1,
  244. "travelz": 0.1,
  245. "feedrate": 5.0,
  246. "toolselection": ""
  247. })
  248. self.form_kinds.update({
  249. "plot": "cb",
  250. "solid": "cb",
  251. "multicolored": "cb",
  252. "drillz": "entry_eval",
  253. "travelz": "entry_eval",
  254. "feedrate": "entry_eval",
  255. "toolselection": "entry_text"
  256. })
  257. # TODO: Document this.
  258. self.tool_cbs = {}
  259. # Attributes to be included in serialization
  260. # Always append to it because it carries contents
  261. # from predecessors.
  262. self.ser_attrs += ['options', 'kind']
  263. def convert_units(self, units):
  264. factor = Excellon.convert_units(self, units)
  265. self.options['drillz'] *= factor
  266. self.options['travelz'] *= factor
  267. self.options['feedrate'] *= factor
  268. def plot(self, figure):
  269. CirkuixObj.plot(self, figure)
  270. #self.setup_axes(figure)
  271. self.create_geometry()
  272. # Plot excellon
  273. for geo in self.solid_geometry:
  274. x, y = geo.exterior.coords.xy
  275. self.axes.plot(x, y, 'r-')
  276. for ints in geo.interiors:
  277. x, y = ints.coords.xy
  278. self.axes.plot(x, y, 'g-')
  279. self.app.on_zoom_fit(None)
  280. self.app.canvas.queue_draw()
  281. def show_tool_chooser(self):
  282. win = Gtk.Window()
  283. box = Gtk.Box(spacing=2)
  284. box.set_orientation(Gtk.Orientation(1))
  285. win.add(box)
  286. for tool in self.tools:
  287. self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + self.tools[tool])
  288. box.pack_start(self.tool_cbs[tool], False, False, 1)
  289. button = Gtk.Button(label="Accept")
  290. box.pack_start(button, False, False, 1)
  291. win.show_all()
  292. def on_accept(widget):
  293. win.destroy()
  294. tool_list = []
  295. for tool in self.tool_cbs:
  296. if self.tool_cbs[tool].get_active():
  297. tool_list.append(tool)
  298. self.options["toolselection"] = ", ".join(tool_list)
  299. self.to_form()
  300. button.connect("activate", on_accept)
  301. button.connect("clicked", on_accept)
  302. class CirkuixCNCjob(CirkuixObj, CNCjob):
  303. """
  304. Represents G-Code.
  305. """
  306. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  307. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  308. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  309. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  310. CirkuixObj.__init__(self, name)
  311. self.kind = "cncjob"
  312. self.options.update({
  313. "plot": True,
  314. "solid": False,
  315. "multicolored": False,
  316. "tooldia": 0.4 / 25.4 # 0.4mm in inches
  317. })
  318. self.form_kinds.update({
  319. "plot": "cb",
  320. "solid": "cb",
  321. "multicolored": "cb",
  322. "tooldia": "entry_eval"
  323. })
  324. # Attributes to be included in serialization
  325. # Always append to it because it carries contents
  326. # from predecessors.
  327. self.ser_attrs += ['options', 'kind']
  328. def plot(self, figure):
  329. CirkuixObj.plot(self, figure)
  330. #self.setup_axes(figure)
  331. self.plot2(self.axes, tooldia=self.options["tooldia"])
  332. self.app.on_zoom_fit(None)
  333. self.app.canvas.queue_draw()
  334. class CirkuixGeometry(CirkuixObj, Geometry):
  335. """
  336. Geometric object not associated with a specific
  337. format.
  338. """
  339. def __init__(self, name):
  340. CirkuixObj.__init__(self, name)
  341. Geometry.__init__(self)
  342. self.kind = "geometry"
  343. self.options.update({
  344. "plot": True,
  345. "solid": False,
  346. "multicolored": False,
  347. "cutz": -0.002,
  348. "travelz": 0.1,
  349. "feedrate": 5.0,
  350. "cnctooldia": 0.4 / 25.4,
  351. "painttooldia": 0.0625,
  352. "paintoverlap": 0.15,
  353. "paintmargin": 0.01
  354. })
  355. self.form_kinds.update({
  356. "plot": "cb",
  357. "solid": "cb",
  358. "multicolored": "cb",
  359. "cutz": "entry_eval",
  360. "travelz": "entry_eval",
  361. "feedrate": "entry_eval",
  362. "cnctooldia": "entry_eval",
  363. "painttooldia": "entry_eval",
  364. "paintoverlap": "entry_eval",
  365. "paintmargin": "entry_eval"
  366. })
  367. # Attributes to be included in serialization
  368. # Always append to it because it carries contents
  369. # from predecessors.
  370. self.ser_attrs += ['options', 'kind']
  371. def scale(self, factor):
  372. if type(self.solid_geometry) == list:
  373. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  374. for g in self.solid_geometry]
  375. else:
  376. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  377. origin=(0, 0))
  378. def convert_units(self, units):
  379. factor = Geometry.convert_units(self, units)
  380. self.options['cutz'] *= factor
  381. self.options['travelz'] *= factor
  382. self.options['feedrate'] *= factor
  383. self.options['cnctooldia'] *= factor
  384. self.options['painttooldia'] *= factor
  385. self.options['paintmargin'] *= factor
  386. return factor
  387. def plot(self, figure):
  388. CirkuixObj.plot(self, figure)
  389. #self.setup_axes(figure)
  390. try:
  391. _ = iter(self.solid_geometry)
  392. except TypeError:
  393. self.solid_geometry = [self.solid_geometry]
  394. for geo in self.solid_geometry:
  395. if type(geo) == Polygon:
  396. x, y = geo.exterior.coords.xy
  397. self.axes.plot(x, y, 'r-')
  398. for ints in geo.interiors:
  399. x, y = ints.coords.xy
  400. self.axes.plot(x, y, 'r-')
  401. continue
  402. if type(geo) == LineString or type(geo) == LinearRing:
  403. x, y = geo.coords.xy
  404. self.axes.plot(x, y, 'r-')
  405. continue
  406. if type(geo) == MultiPolygon:
  407. for poly in geo:
  408. x, y = poly.exterior.coords.xy
  409. self.axes.plot(x, y, 'r-')
  410. for ints in poly.interiors:
  411. x, y = ints.coords.xy
  412. self.axes.plot(x, y, 'r-')
  413. continue
  414. print "WARNING: Did not plot:", str(type(geo))
  415. self.app.on_zoom_fit(None)
  416. self.app.canvas.queue_draw()
  417. ########################################
  418. ## App ##
  419. ########################################
  420. class App:
  421. """
  422. The main application class. The constructor starts the GUI.
  423. """
  424. def __init__(self):
  425. """
  426. Starts the application.
  427. :return: app
  428. :rtype: App
  429. """
  430. # Needed to interact with the GUI from other threads.
  431. GObject.threads_init()
  432. ## GUI ##
  433. self.gladefile = "cirkuix.ui"
  434. self.builder = Gtk.Builder()
  435. self.builder.add_from_file(self.gladefile)
  436. self.window = self.builder.get_object("window1")
  437. self.window.set_title("Cirkuix")
  438. self.position_label = self.builder.get_object("label3")
  439. self.grid = self.builder.get_object("grid1")
  440. self.notebook = self.builder.get_object("notebook1")
  441. self.info_label = self.builder.get_object("label_status")
  442. self.progress_bar = self.builder.get_object("progressbar")
  443. self.progress_bar.set_show_text(True)
  444. self.units_label = self.builder.get_object("label_units")
  445. # White (transparent) background on the "Options" tab.
  446. self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
  447. Gdk.RGBA(1, 1, 1, 1))
  448. # Combo box to choose between project and application options.
  449. self.combo_options = self.builder.get_object("combo_options")
  450. self.combo_options.set_active(1)
  451. ## Event handling ##
  452. self.builder.connect_signals(self)
  453. ## Make plot area ##
  454. self.figure = None
  455. self.axes = None
  456. self.canvas = None
  457. self.setup_plot()
  458. self.setup_project_list() # The "Project" tab
  459. self.setup_component_editor() # The "Selected" tab
  460. #### DATA ####
  461. self.setup_obj_classes()
  462. self.stuff = {} # CirkuixObj's by name
  463. self.mouse = None # Mouse coordinates over plot
  464. # What is selected by the user. It is
  465. # a key if self.stuff
  466. self.selected_item_name = None
  467. # Used to inhibit the on_options_update callback when
  468. # the options are being changed by the program and not the user.
  469. self.options_update_ignore = False
  470. self.defaults = {
  471. "units": "in"
  472. } # Application defaults
  473. ## Current Project ##
  474. self.options = {} # Project options
  475. self.project_filename = None
  476. self.form_kinds = {
  477. "units": "radio"
  478. }
  479. self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
  480. "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
  481. self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
  482. "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
  483. # self.combos = []
  484. # Options for each kind of CirkuixObj.
  485. # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
  486. for CirkuixClass in [CirkuixExcellon, CirkuixGeometry, CirkuixGerber, CirkuixCNCjob]:
  487. obj = CirkuixClass("no_name")
  488. for option in obj.form_kinds:
  489. self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
  490. # if obj.form_kinds[option] == "radio":
  491. # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
  492. # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
  493. self.plot_click_subscribers = {}
  494. # Initialization
  495. self.load_defaults()
  496. self.options.update(self.defaults) # Copy app defaults to project options
  497. self.options2form() # Populate the app defaults form
  498. self.units_label.set_text("[" + self.options["units"] + "]")
  499. # For debugging only
  500. def someThreadFunc(self):
  501. print "Hello World!"
  502. t = threading.Thread(target=someThreadFunc, args=(self,))
  503. t.start()
  504. ########################################
  505. ## START ##
  506. ########################################
  507. self.window.set_default_size(900, 600)
  508. self.window.show_all()
  509. def setup_plot(self):
  510. """
  511. Sets up the main plotting area by creating a Matplotlib
  512. figure in self.canvas, adding axes and configuring them.
  513. These axes should not be ploted on and are just there to
  514. display the axes ticks and grid.
  515. :return: None
  516. :rtype: None
  517. """
  518. self.figure = Figure(dpi=50)
  519. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  520. self.axes.set_aspect(1)
  521. #t = arange(0.0,5.0,0.01)
  522. #s = sin(2*pi*t)
  523. #self.axes.plot(t,s)
  524. self.axes.grid(True)
  525. self.figure.patch.set_visible(False)
  526. self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
  527. self.canvas.set_hexpand(1)
  528. self.canvas.set_vexpand(1)
  529. # Events
  530. self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
  531. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  532. self.canvas.set_can_focus(True) # For key press
  533. self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
  534. #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
  535. self.canvas.connect("configure-event", self.on_canvas_configure)
  536. self.grid.attach(self.canvas, 0, 0, 600, 400)
  537. def setup_obj_classes(self):
  538. """
  539. Sets up application specifics on the CirkuixObj class.
  540. :return: None
  541. """
  542. CirkuixObj.app = self
  543. def setup_project_list(self):
  544. """
  545. Sets up list or Tree where whatever has been loaded or created is
  546. displayed.
  547. :return: None
  548. """
  549. self.store = Gtk.ListStore(str)
  550. self.tree = Gtk.TreeView(self.store)
  551. #self.list = Gtk.ListBox()
  552. self.tree.connect("row_activated", self.on_row_activated)
  553. self.tree_select = self.tree.get_selection()
  554. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  555. renderer = Gtk.CellRendererText()
  556. column = Gtk.TreeViewColumn("Title", renderer, text=0)
  557. self.tree.append_column(column)
  558. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  559. def setup_component_editor(self):
  560. """
  561. Initial configuration of the component editor. Creates
  562. a page titled "Selection" on the notebook on the left
  563. side of the main window.
  564. :return: None
  565. """
  566. box_selected = self.builder.get_object("box_selected")
  567. # Remove anything else in the box
  568. box_children = box_selected.get_children()
  569. for child in box_children:
  570. box_selected.remove(child)
  571. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  572. label1 = Gtk.Label("Choose an item from Project")
  573. box1.pack_start(label1, True, False, 1)
  574. box_selected.pack_start(box1, True, True, 0)
  575. #box_selected.show()
  576. box1.show()
  577. label1.show()
  578. def info(self, text):
  579. """
  580. Show text on the status bar.
  581. :param text: Text to display.
  582. :type text: str
  583. :return: None
  584. """
  585. self.info_label.set_text(text)
  586. def zoom(self, factor, center=None):
  587. """
  588. Zooms the plot by factor around a given
  589. center point. Takes care of re-drawing.
  590. :param factor: Number by which to scale the plot.
  591. :type factor: float
  592. :param center: Coordinates [x, y] of the point around which to scale the plot.
  593. :type center: list
  594. :return: None
  595. """
  596. xmin, xmax = self.axes.get_xlim()
  597. ymin, ymax = self.axes.get_ylim()
  598. width = xmax - xmin
  599. height = ymax - ymin
  600. if center is None:
  601. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  602. # For keeping the point at the pointer location
  603. relx = (xmax - center[0]) / width
  604. rely = (ymax - center[1]) / height
  605. new_width = width / factor
  606. new_height = height / factor
  607. xmin = center[0] - new_width * (1 - relx)
  608. xmax = center[0] + new_width * relx
  609. ymin = center[1] - new_height * (1 - rely)
  610. ymax = center[1] + new_height * rely
  611. for name in self.stuff:
  612. self.stuff[name].axes.set_xlim((xmin, xmax))
  613. self.stuff[name].axes.set_ylim((ymin, ymax))
  614. self.axes.set_xlim((xmin, xmax))
  615. self.axes.set_ylim((ymin, ymax))
  616. self.canvas.queue_draw()
  617. def build_list(self):
  618. """
  619. Clears and re-populates the list of objects in currently
  620. in the project.
  621. :return: None
  622. """
  623. print "build_list(): clearing"
  624. self.tree_select.unselect_all()
  625. self.store.clear()
  626. print "repopulating...",
  627. for key in self.stuff:
  628. print key,
  629. self.store.append([key])
  630. print
  631. def get_radio_value(self, radio_set):
  632. """
  633. Returns the radio_set[key] of the radiobutton
  634. whose name is key is active.
  635. :param radio_set: A dictionary containing widget_name: value pairs.
  636. :type radio_set: dict
  637. :return: radio_set[key]
  638. """
  639. for name in radio_set:
  640. if self.builder.get_object(name).get_active():
  641. return radio_set[name]
  642. def plot_all(self):
  643. """
  644. Re-generates all plots from all objects.
  645. :return: None
  646. """
  647. self.clear_plots()
  648. self.set_progress_bar(0.1, "Re-plotting...")
  649. def thread_func(app_obj):
  650. percentage = 0.1
  651. try:
  652. delta = 0.9 / len(self.stuff)
  653. except ZeroDivisionError:
  654. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  655. return
  656. for i in self.stuff:
  657. self.stuff[i].plot(self.figure)
  658. percentage += delta
  659. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  660. self.on_zoom_fit(None)
  661. self.axes.grid(True)
  662. self.canvas.queue_draw()
  663. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  664. t = threading.Thread(target=thread_func, args=(self,))
  665. t.daemon = True
  666. t.start()
  667. def clear_plots(self):
  668. """
  669. Clears self.axes and self.figure.
  670. :return: None
  671. """
  672. # TODO: Create a setup_axes method that gets called here and in setup_plot?
  673. self.axes.cla()
  674. self.figure.clf()
  675. self.figure.add_axes(self.axes)
  676. self.axes.set_aspect(1)
  677. self.axes.grid(True)
  678. self.canvas.queue_draw()
  679. def get_eval(self, widget_name):
  680. """
  681. Runs eval() on the on the text entry of name 'widget_name'
  682. and returns the results.
  683. :param widget_name: Name of Gtk.Entry
  684. :type widget_name: str
  685. :return: Depends on contents of the entry text.
  686. """
  687. value = self.builder.get_object(widget_name).get_text()
  688. if value == "":
  689. value = "None"
  690. try:
  691. evald = eval(value)
  692. return evald
  693. except:
  694. self.info("Could not evaluate: " + value)
  695. return None
  696. def set_list_selection(self, name):
  697. """
  698. Marks a given object as selected in the list ob objects
  699. in the GUI. This selection will in turn trigger
  700. ``self.on_tree_selection_changed()``.
  701. :param name: Name of the object.
  702. :type name: str
  703. :return: None
  704. """
  705. iter = self.store.get_iter_first()
  706. while iter is not None and self.store[iter][0] != name:
  707. iter = self.store.iter_next(iter)
  708. self.tree_select.unselect_all()
  709. self.tree_select.select_iter(iter)
  710. # Need to return False such that GLib.idle_add
  711. # or .timeout_add do not repear.
  712. return False
  713. def new_object(self, kind, name, initialize):
  714. """
  715. Creates a new specalized CirkuixObj and attaches it to the application,
  716. this is, updates the GUI accordingly, any other records and plots it.
  717. :param kind: The kind of object to create. One of 'gerber',
  718. 'excellon', 'cncjob' and 'geometry'.
  719. :type kind: str
  720. :param name: Name for the object.
  721. :type name: str
  722. :param initialize: Function to run after creation of the object
  723. but before it is attached to the application. The function is
  724. called with 2 parameters: the new object and the App instance.
  725. :type initialize: function
  726. :return: None
  727. :rtype: None
  728. """
  729. # Check for existing name
  730. if name in self.stuff:
  731. self.info("Rename " + name + " in project first.")
  732. return None
  733. # Create object
  734. classdict = {
  735. "gerber": CirkuixGerber,
  736. "excellon": CirkuixExcellon,
  737. "cncjob": CirkuixCNCjob,
  738. "geometry": CirkuixGeometry
  739. }
  740. obj = classdict[kind](name)
  741. # Initialize as per user request
  742. # User must take care to implement initialize
  743. # in a thread-safe way as is is likely that we
  744. # have been invoked in a separate thread.
  745. initialize(obj, self)
  746. # Check units and convert if necessary
  747. if self.options["units"].upper() != obj.units.upper():
  748. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  749. obj.convert_units(self.options["units"])
  750. # Set default options from self.options
  751. for option in self.options:
  752. if option.find(kind + "_") == 0:
  753. oname = option[len(kind)+1:]
  754. obj.options[oname] = self.options[option]
  755. # Add to our records
  756. self.stuff[name] = obj
  757. # Update GUI list and select it (Thread-safe?)
  758. self.store.append([name])
  759. #self.build_list()
  760. GLib.idle_add(lambda: self.set_list_selection(name))
  761. # TODO: Gtk.notebook.set_current_page is not known to
  762. # TODO: return False. Fix this??
  763. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  764. # Plot
  765. # TODO: (Thread-safe?)
  766. obj.plot(self.figure)
  767. obj.axes.set_alpha(0.0)
  768. self.on_zoom_fit(None)
  769. return obj
  770. def set_progress_bar(self, percentage, text=""):
  771. """
  772. Sets the application's progress bar to a given fraction and text.
  773. :param percentage: The fraction (0.0-1.0) of the progress.
  774. :type percentage: float
  775. :param text: Text to display on the progress bar.
  776. :type text: str
  777. :return:
  778. """
  779. self.progress_bar.set_text(text)
  780. self.progress_bar.set_fraction(percentage)
  781. return False
  782. def save_project(self):
  783. return
  784. def get_current(self):
  785. """
  786. Returns the currently selected CirkuixObj in the application.
  787. :return: Currently selected CirkuixObj in the application.
  788. :rtype: CirkuixObj or None
  789. """
  790. try:
  791. return self.stuff[self.selected_item_name]
  792. except:
  793. return None
  794. def adjust_axes(self, xmin, ymin, xmax, ymax):
  795. """
  796. Adjusts axes of all plots while maintaining the use of the whole canvas
  797. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  798. request that will be modified to fit these restrictions.
  799. :param xmin: Requested minimum value for the X axis.
  800. :type xmin: float
  801. :param ymin: Requested minimum value for the Y axis.
  802. :type ymin: float
  803. :param xmax: Requested maximum value for the X axis.
  804. :type xmax: float
  805. :param ymax: Requested maximum value for the Y axis.
  806. :type ymax: float
  807. :return: None
  808. """
  809. m_x = 15 # pixels
  810. m_y = 25 # pixels
  811. width = xmax - xmin
  812. height = ymax - ymin
  813. r = width / height
  814. Fw, Fh = self.canvas.get_width_height()
  815. Fr = float(Fw) / Fh
  816. x_ratio = float(m_x) / Fw
  817. y_ratio = float(m_y) / Fh
  818. if r > Fr:
  819. ycenter = (ymin + ymax) / 2.0
  820. newheight = height * r / Fr
  821. ymin = ycenter - newheight / 2.0
  822. ymax = ycenter + newheight / 2.0
  823. else:
  824. xcenter = (xmax + ymin) / 2.0
  825. newwidth = width * Fr / r
  826. xmin = xcenter - newwidth / 2.0
  827. xmax = xcenter + newwidth / 2.0
  828. for name in self.stuff:
  829. if self.stuff[name].axes is None:
  830. continue
  831. self.stuff[name].axes.set_xlim((xmin, xmax))
  832. self.stuff[name].axes.set_ylim((ymin, ymax))
  833. self.stuff[name].axes.set_position([x_ratio, y_ratio,
  834. 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  835. self.axes.set_xlim((xmin, xmax))
  836. self.axes.set_ylim((ymin, ymax))
  837. self.axes.set_position([x_ratio, y_ratio,
  838. 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  839. self.canvas.queue_draw()
  840. def load_defaults(self):
  841. """
  842. Loads the aplication's default settings from defaults.json into
  843. ``self.defaults``.
  844. :return: None
  845. """
  846. try:
  847. f = open("defaults.json")
  848. options = f.read()
  849. f.close()
  850. except:
  851. self.info("ERROR: Could not load defaults file.")
  852. return
  853. try:
  854. defaults = json.loads(options)
  855. except:
  856. e = sys.exc_info()[0]
  857. print e
  858. self.info("ERROR: Failed to parse defaults file.")
  859. return
  860. self.defaults.update(defaults)
  861. def read_form(self):
  862. """
  863. Reads the options form into self.defaults/self.options.
  864. :return: None
  865. :rtype: None
  866. """
  867. combo_sel = self.combo_options.get_active()
  868. options_set = [self.options, self.defaults][combo_sel]
  869. for option in options_set:
  870. self.read_form_item(option, options_set)
  871. def read_form_item(self, name, dest):
  872. """
  873. Reads the value of a form item in the defaults/options form and
  874. saves it to the corresponding dictionary.
  875. :param name: Name of the form item. A key in ``self.defaults`` or
  876. ``self.options``.
  877. :type name: str
  878. :param dest: Dictionary to which to save the value.
  879. :type dest: dict
  880. :return: None
  881. """
  882. fkind = self.form_kinds[name]
  883. fname = fkind + "_" + "app" + "_" + name
  884. if fkind == 'entry_text':
  885. dest[name] = self.builder.get_object(fname).get_text()
  886. return
  887. if fkind == 'entry_eval':
  888. dest[name] = self.get_eval(fname)
  889. return
  890. if fkind == 'cb':
  891. dest[name] = self.builder.get_object(fname).get_active()
  892. return
  893. if fkind == 'radio':
  894. dest[name] = self.get_radio_value(self.radios[name])
  895. return
  896. print "Unknown kind of form item:", fkind
  897. def options2form(self):
  898. """
  899. Sets the 'Project Options' or 'Application Defaults' form with values from
  900. ``self.options`` or ``self.defaults``.
  901. :return: None
  902. :rtype: None
  903. """
  904. # Set the on-change callback to do nothing while we do the changes.
  905. self.options_update_ignore = True
  906. combo_sel = self.combo_options.get_active()
  907. options_set = [self.options, self.defaults][combo_sel]
  908. for option in options_set:
  909. self.set_form_item(option, options_set[option])
  910. self.options_update_ignore = False
  911. def set_form_item(self, name, value):
  912. """
  913. Sets a form item 'name' in the GUI with the given 'value'. The syntax of
  914. form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
  915. cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
  916. whatever name it's been given. For self.defaults, name is a key in the dictionary.
  917. :param name: Name of the form field.
  918. :type name: str
  919. :param value: The value to set the form field to.
  920. :type value: Depends on field kind.
  921. :return: None
  922. """
  923. if name not in self.form_kinds:
  924. print "WARNING: Tried to set unknown option/form item:", name
  925. return
  926. fkind = self.form_kinds[name]
  927. fname = fkind + "_" + "app" + "_" + name
  928. if fkind == 'entry_eval' or fkind == 'entry_text':
  929. try:
  930. self.builder.get_object(fname).set_text(str(value))
  931. except:
  932. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  933. return
  934. if fkind == 'cb':
  935. try:
  936. self.builder.get_object(fname).set_active(value)
  937. except:
  938. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  939. return
  940. if fkind == 'radio':
  941. try:
  942. self.builder.get_object(self.radios_inv[name][value]).set_active(True)
  943. except:
  944. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  945. return
  946. print "Unknown kind of form item:", fkind
  947. def save_project(self, filename):
  948. """
  949. Saves the current project to the specified file.
  950. :param filename: Name of the file in which to save.
  951. :type filename: str
  952. :return: None
  953. """
  954. # Captura the latest changes
  955. try:
  956. self.get_current().read_form()
  957. except:
  958. pass
  959. d = {"objs": [self.stuff[o].to_dict() for o in self.stuff],
  960. "options": self.options}
  961. try:
  962. f = open(filename, 'w')
  963. except:
  964. print "ERROR: Failed to open file for saving:", filename
  965. return
  966. try:
  967. json.dump(d, f, default=to_dict)
  968. except:
  969. print "ERROR: File open but failed to write:", filename
  970. f.close()
  971. return
  972. f.close()
  973. def open_project(self, filename):
  974. """
  975. Loads a project from the specified file.
  976. :param filename: Name of the file from which to load.
  977. :type filename: str
  978. :return: None
  979. """
  980. try:
  981. f = open(filename, 'r')
  982. except:
  983. print "WARNING: Failed to open project file:", filename
  984. return
  985. try:
  986. d = json.load(f, object_hook=dict2obj)
  987. except:
  988. print "WARNING: Failed to parse project file:", filename
  989. f.close()
  990. # Clear the current project
  991. self.on_file_new(None)
  992. # Project options
  993. self.options.update(d['options'])
  994. self.project_filename = filename
  995. # Re create objects
  996. for obj in d['objs']:
  997. def obj_init(obj_inst, app_inst):
  998. obj_inst.from_dict(obj)
  999. self.new_object(obj['kind'], obj['options']['name'], obj_init)
  1000. self.info("Project loaded from: " + filename)
  1001. ########################################
  1002. ## EVENT HANDLERS ##
  1003. ########################################
  1004. def on_file_openproject(self, param):
  1005. """
  1006. Callback for menu item File->Open Project. Opens a file chooser and calls
  1007. ``self.open_project()`` after successful selection of a filename.
  1008. :param param: Ignored.
  1009. :return: None
  1010. """
  1011. def on_success(app_obj, filename):
  1012. app_obj.open_project(filename)
  1013. self.file_chooser_action(on_success)
  1014. def on_file_saveproject(self, param):
  1015. """
  1016. Callback for menu item File->Save Project. Saves the project to
  1017. ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
  1018. if set to None. The project is saved by calling ``self.save_project()``.
  1019. :param param: Ignored.
  1020. :return: None
  1021. """
  1022. if self.project_filename is None:
  1023. self.on_file_saveprojectas(None)
  1024. else:
  1025. self.save_project(self.project_filename)
  1026. self.info("Project saved to: " + self.project_filename)
  1027. def on_file_saveprojectas(self, param):
  1028. """
  1029. Callback for menu item File->Save Project As... Opens a file
  1030. chooser and saves the project to the given file via
  1031. ``self.save_project()``.
  1032. :param param: Ignored.
  1033. :return: None
  1034. """
  1035. def on_success(app_obj, filename):
  1036. assert isinstance(app_obj, App)
  1037. app_obj.save_project(filename)
  1038. self.project_filename = filename
  1039. app_obj.info("Project saved to: " + filename)
  1040. self.file_chooser_save_action(on_success)
  1041. def on_file_saveprojectcopy(self, param):
  1042. """
  1043. Callback for menu item File->Save Project Copy... Opens a file
  1044. chooser and saves the project to the given file via
  1045. ``self.save_project``. It does not update ``self.project_filename`` so
  1046. subsequent save requests are done on the previous known filename.
  1047. :param param: Ignore.
  1048. :return: None
  1049. """
  1050. def on_success(app_obj, filename):
  1051. assert isinstance(app_obj, App)
  1052. app_obj.save_project(filename)
  1053. app_obj.info("Project copy saved to: " + filename)
  1054. self.file_chooser_save_action(on_success)
  1055. def on_options_app2project(self, param):
  1056. """
  1057. Callback for Options->Transfer Options->App=>Project. Copies options
  1058. from application defaults to project defaults.
  1059. :param param: Ignored.
  1060. :return: None
  1061. """
  1062. self.options.update(self.defaults)
  1063. self.options2form() # Update UI
  1064. def on_options_project2app(self, param):
  1065. """
  1066. Callback for Options->Transfer Options->Project=>App. Copies options
  1067. from project defaults to application defaults.
  1068. :param param: Ignored.
  1069. :return: None
  1070. """
  1071. self.defaults.update(self.options)
  1072. self.options2form() # Update UI
  1073. def on_options_project2object(self, param):
  1074. """
  1075. Callback for Options->Transfer Options->Project=>Object. Copies options
  1076. from project defaults to the currently selected object.
  1077. :param param: Ignored.
  1078. :return: None
  1079. """
  1080. obj = self.get_current()
  1081. if obj is None:
  1082. print "WARNING: No object selected."
  1083. return
  1084. for option in self.options:
  1085. if option.find(obj.kind + "_") == 0:
  1086. oname = option[len(obj.kind)+1:]
  1087. obj.options[oname] = self.options[option]
  1088. obj.to_form() # Update UI
  1089. def on_options_object2project(self, param):
  1090. """
  1091. Callback for Options->Transfer Options->Object=>Project. Copies options
  1092. from the currently selected object to project defaults.
  1093. :param param: Ignored.
  1094. :return: None
  1095. """
  1096. obj = self.get_current()
  1097. if obj is None:
  1098. print "WARNING: No object selected."
  1099. return
  1100. obj.read_form()
  1101. for option in obj.options:
  1102. if option in ['name']: # TODO: Handle this better...
  1103. continue
  1104. self.options[obj.kind + "_" + option] = obj.options[option]
  1105. self.options2form() # Update UI
  1106. def on_options_object2app(self, param):
  1107. """
  1108. Callback for Options->Transfer Options->Object=>App. Copies options
  1109. from the currently selected object to application defaults.
  1110. :param param: Ignored.
  1111. :return: None
  1112. """
  1113. obj = self.get_current()
  1114. if obj is None:
  1115. print "WARNING: No object selected."
  1116. return
  1117. obj.read_form()
  1118. for option in obj.options:
  1119. if option in ['name']: # TODO: Handle this better...
  1120. continue
  1121. self.defaults[obj.kind + "_" + option] = obj.options[option]
  1122. self.options2form() # Update UI
  1123. def on_options_app2object(self, param):
  1124. """
  1125. Callback for Options->Transfer Options->App=>Object. Copies options
  1126. from application defaults to the currently selected object.
  1127. :param param: Ignored.
  1128. :return: None
  1129. """
  1130. obj = self.get_current()
  1131. if obj is None:
  1132. print "WARNING: No object selected."
  1133. return
  1134. for option in self.defaults:
  1135. if option.find(obj.kind + "_") == 0:
  1136. oname = option[len(obj.kind)+1:]
  1137. obj.options[oname] = self.defaults[option]
  1138. obj.to_form() # Update UI
  1139. def on_file_savedefaults(self, param):
  1140. """
  1141. Callback for menu item File->Save Defaults. Saves application default options
  1142. ``self.defaults`` to defaults.json.
  1143. :param param: Ignored.
  1144. :return: None
  1145. """
  1146. try:
  1147. f = open("defaults.json")
  1148. options = f.read()
  1149. f.close()
  1150. except:
  1151. self.info("ERROR: Could not load defaults file.")
  1152. return
  1153. try:
  1154. defaults = json.loads(options)
  1155. except:
  1156. e = sys.exc_info()[0]
  1157. print e
  1158. self.info("ERROR: Failed to parse defaults file.")
  1159. return
  1160. assert isinstance(defaults, dict)
  1161. defaults.update(self.defaults)
  1162. try:
  1163. f = open("defaults.json", "w")
  1164. json.dump(defaults, f)
  1165. f.close()
  1166. except:
  1167. self.info("ERROR: Failed to write defaults to file.")
  1168. return
  1169. self.info("Defaults saved.")
  1170. def on_options_combo_change(self, widget):
  1171. """
  1172. Called when the combo box to choose between application defaults and
  1173. project option changes value. The corresponding variables are
  1174. copied to the UI.
  1175. :param widget: The widget from which this was called. Ignore.
  1176. :return: None
  1177. """
  1178. combo_sel = self.combo_options.get_active()
  1179. print "Options --> ", combo_sel
  1180. self.options2form()
  1181. def on_options_update(self, widget):
  1182. """
  1183. Called whenever a value in the options/defaults form changes.
  1184. All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
  1185. which may be necessary when updating the UI from code and not by the user.
  1186. :param widget: The widget from which this was called. Ignore.
  1187. :return: None
  1188. """
  1189. if self.options_update_ignore:
  1190. return
  1191. self.read_form()
  1192. def on_scale_object(self, widget):
  1193. """
  1194. Callback for request to change an objects geometry scale. The object
  1195. is re-scaled and replotted.
  1196. :param widget: Ignored.
  1197. :return: None
  1198. """
  1199. obj = self.get_current()
  1200. assert isinstance(obj, CirkuixObj)
  1201. factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
  1202. obj.scale(factor)
  1203. obj.to_form()
  1204. self.on_update_plot(None)
  1205. def on_canvas_configure(self, widget, event):
  1206. """
  1207. Called whenever the canvas changes size. The axes are updated such
  1208. as to use the whole canvas.
  1209. :param widget: Ignored.
  1210. :param event: Ignored.
  1211. :return: None
  1212. """
  1213. print "on_canvas_configure()"
  1214. xmin, xmax = self.axes.get_xlim()
  1215. ymin, ymax = self.axes.get_ylim()
  1216. self.adjust_axes(xmin, ymin, xmax, ymax)
  1217. def on_row_activated(self, widget, path, col):
  1218. """
  1219. Callback for selection activation (Enter or double-click) on the Project list.
  1220. Switches the notebook page to the object properties form. Calls
  1221. ``self.notebook.set_current_page(1)``.
  1222. :param widget: Ignored.
  1223. :param path: Ignored.
  1224. :param col: Ignored.
  1225. :return: None
  1226. """
  1227. self.notebook.set_current_page(1)
  1228. def on_generate_gerber_bounding_box(self, widget):
  1229. """
  1230. Callback for request from the Gerber form to generate a bounding box for the
  1231. geometry in the object. Creates a CirkuixGeometry with the bounding box.
  1232. :param widget: Ignored.
  1233. :return: None
  1234. """
  1235. gerber = self.get_current()
  1236. gerber.read_form()
  1237. name = self.selected_item_name + "_bbox"
  1238. def geo_init(geo_obj, app_obj):
  1239. assert isinstance(geo_obj, CirkuixGeometry)
  1240. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
  1241. if not gerber.options["bboxrounded"]:
  1242. bounding_box = bounding_box.envelope
  1243. geo_obj.solid_geometry = bounding_box
  1244. self.new_object("geometry", name, geo_init)
  1245. def on_update_plot(self, widget):
  1246. """
  1247. Callback for button on form for all kinds of objects.
  1248. Re-plots the current object only.
  1249. :param widget: The widget from which this was called.
  1250. :return: None
  1251. """
  1252. print "Re-plotting"
  1253. self.get_current().read_form()
  1254. self.set_progress_bar(0.5, "Plotting...")
  1255. #GLib.idle_add(lambda: self.set_progress_bar(0.5, "Plotting..."))
  1256. def thread_func(app_obj):
  1257. assert isinstance(app_obj, App)
  1258. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
  1259. #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
  1260. app_obj.get_current().plot(app_obj.figure)
  1261. GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
  1262. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  1263. t = threading.Thread(target=thread_func, args=(self,))
  1264. t.daemon = True
  1265. t.start()
  1266. def on_generate_excellon_cncjob(self, widget):
  1267. """
  1268. Callback for button active/click on Excellon form to
  1269. create a CNC Job for the Excellon file.
  1270. :param widget: The widget from which this was called.
  1271. :return: None
  1272. """
  1273. job_name = self.selected_item_name + "_cnc"
  1274. excellon = self.get_current()
  1275. assert isinstance(excellon, CirkuixExcellon)
  1276. excellon.read_form()
  1277. # Object initialization function for app.new_object()
  1278. def job_init(job_obj, app_obj):
  1279. excellon_ = self.get_current()
  1280. assert isinstance(excellon_, CirkuixExcellon)
  1281. assert isinstance(job_obj, CirkuixCNCjob)
  1282. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1283. job_obj.z_cut = excellon_.options["drillz"]
  1284. job_obj.z_move = excellon_.options["travelz"]
  1285. job_obj.feedrate = excellon_.options["feedrate"]
  1286. # There could be more than one drill size...
  1287. # job_obj.tooldia = # TODO: duplicate variable!
  1288. # job_obj.options["tooldia"] =
  1289. job_obj.generate_from_excellon_by_tool(excellon_, excellon_.options["toolselection"])
  1290. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1291. job_obj.gcode_parse()
  1292. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1293. job_obj.create_geometry()
  1294. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1295. # To be run in separate thread
  1296. def job_thread(app_obj):
  1297. app_obj.new_object("cncjob", job_name, job_init)
  1298. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1299. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1300. # Start the thread
  1301. t = threading.Thread(target=job_thread, args=(self,))
  1302. t.daemon = True
  1303. t.start()
  1304. def on_excellon_tool_choose(self, widget):
  1305. """
  1306. Callback for button on Excellon form to open up a window for
  1307. selecting tools.
  1308. :param widget: The widget from which this was called.
  1309. :return: None
  1310. """
  1311. excellon = self.get_current()
  1312. assert isinstance(excellon, CirkuixExcellon)
  1313. excellon.show_tool_chooser()
  1314. def on_entry_eval_activate(self, widget):
  1315. """
  1316. Called when an entry is activated (eg. by hitting enter) if
  1317. set to do so. Its text is eval()'d and set to the returned value.
  1318. The current object is updated.
  1319. :param widget:
  1320. :return:
  1321. """
  1322. self.on_eval_update(widget)
  1323. obj = self.get_current()
  1324. assert isinstance(obj, CirkuixObj)
  1325. obj.read_form()
  1326. def on_gerber_generate_noncopper(self, widget):
  1327. """
  1328. Callback for button on Gerber form to create a geometry object
  1329. with polygons covering the area without copper or negative of the
  1330. Gerber.
  1331. :param widget: The widget from which this was called.
  1332. :return: None
  1333. """
  1334. name = self.selected_item_name + "_noncopper"
  1335. def geo_init(geo_obj, app_obj):
  1336. assert isinstance(geo_obj, CirkuixGeometry)
  1337. gerber = app_obj.stuff[app_obj.selected_item_name]
  1338. assert isinstance(gerber, CirkuixGerber)
  1339. gerber.read_form()
  1340. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
  1341. non_copper = bounding_box.difference(gerber.solid_geometry)
  1342. geo_obj.solid_geometry = non_copper
  1343. # TODO: Check for None
  1344. self.new_object("geometry", name, geo_init)
  1345. def on_gerber_generate_cutout(self, widget):
  1346. """
  1347. Callback for button on Gerber form to create geometry with lines
  1348. for cutting off the board.
  1349. :param widget: The widget from which this was called.
  1350. :return: None
  1351. """
  1352. name = self.selected_item_name + "_cutout"
  1353. def geo_init(geo_obj, app_obj):
  1354. # TODO: get from object
  1355. margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
  1356. gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
  1357. gerber = app_obj.stuff[app_obj.selected_item_name]
  1358. minx, miny, maxx, maxy = gerber.bounds()
  1359. minx -= margin
  1360. maxx += margin
  1361. miny -= margin
  1362. maxy += margin
  1363. midx = 0.5 * (minx + maxx)
  1364. midy = 0.5 * (miny + maxy)
  1365. hgap = 0.5 * gap_size
  1366. pts = [[midx - hgap, maxy],
  1367. [minx, maxy],
  1368. [minx, midy + hgap],
  1369. [minx, midy - hgap],
  1370. [minx, miny],
  1371. [midx - hgap, miny],
  1372. [midx + hgap, miny],
  1373. [maxx, miny],
  1374. [maxx, midy - hgap],
  1375. [maxx, midy + hgap],
  1376. [maxx, maxy],
  1377. [midx + hgap, maxy]]
  1378. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  1379. [pts[6], pts[7], pts[10], pts[11]]],
  1380. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  1381. [pts[3], pts[4], pts[7], pts[8]]],
  1382. "4": [[pts[0], pts[1], pts[2]],
  1383. [pts[3], pts[4], pts[5]],
  1384. [pts[6], pts[7], pts[8]],
  1385. [pts[9], pts[10], pts[11]]]}
  1386. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  1387. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  1388. # TODO: Check for None
  1389. self.new_object("geometry", name, geo_init)
  1390. def on_eval_update(self, widget):
  1391. """
  1392. Modifies the content of a Gtk.Entry by running
  1393. eval() on its contents and puting it back as a
  1394. string.
  1395. :param widget: The widget from which this was called.
  1396. :return: None
  1397. """
  1398. # TODO: error handling here
  1399. widget.set_text(str(eval(widget.get_text())))
  1400. def on_generate_isolation(self, widget):
  1401. """
  1402. Callback for button on Gerber form to create isolation routing geometry.
  1403. :param widget: The widget from which this was called.
  1404. :return: None
  1405. """
  1406. print "Generating Isolation Geometry:"
  1407. iso_name = self.selected_item_name + "_iso"
  1408. def iso_init(geo_obj, app_obj):
  1409. # TODO: Object must be updated on form change and the options
  1410. # TODO: read from the object.
  1411. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
  1412. geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia / 2.0)
  1413. # TODO: Do something if this is None. Offer changing name?
  1414. self.new_object("geometry", iso_name, iso_init)
  1415. def on_generate_cncjob(self, widget):
  1416. """
  1417. Callback for button on geometry form to generate CNC job.
  1418. :param widget: The widget from which this was called.
  1419. :return: None
  1420. """
  1421. print "Generating CNC job"
  1422. job_name = self.selected_item_name + "_cnc"
  1423. # Object initialization function for app.new_object()
  1424. def job_init(job_obj, app_obj):
  1425. assert isinstance(job_obj, CirkuixCNCjob)
  1426. geometry = app_obj.stuff[app_obj.selected_item_name]
  1427. assert isinstance(geometry, CirkuixGeometry)
  1428. geometry.read_form()
  1429. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1430. job_obj.z_cut = geometry.options["cutz"]
  1431. job_obj.z_move = geometry.options["travelz"]
  1432. job_obj.feedrate = geometry.options["feedrate"]
  1433. job_obj.options["tooldia"] = geometry.options["cnctooldia"]
  1434. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  1435. job_obj.generate_from_geometry(geometry)
  1436. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1437. job_obj.gcode_parse()
  1438. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1439. job_obj.create_geometry()
  1440. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1441. # To be run in separate thread
  1442. def job_thread(app_obj):
  1443. app_obj.new_object("cncjob", job_name, job_init)
  1444. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1445. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1446. # Start the thread
  1447. t = threading.Thread(target=job_thread, args=(self,))
  1448. t.daemon = True
  1449. t.start()
  1450. def on_generate_paintarea(self, widget):
  1451. """
  1452. Callback for button on geometry form.
  1453. Subscribes to the "Click on plot" event and continues
  1454. after the click. Finds the polygon containing
  1455. the clicked point and runs clear_poly() on it, resulting
  1456. in a new CirkuixGeometry object.
  1457. :param widget: The widget from which this was called.
  1458. :return: None
  1459. """
  1460. self.info("Click inside the desired polygon.")
  1461. geo = self.get_current()
  1462. geo.read_form()
  1463. tooldia = geo.options["painttooldia"]
  1464. overlap = geo.options["paintoverlap"]
  1465. # To be called after clicking on the plot.
  1466. def doit(event):
  1467. self.plot_click_subscribers.pop("generate_paintarea")
  1468. self.info("")
  1469. point = [event.xdata, event.ydata]
  1470. poly = find_polygon(geo.solid_geometry, point)
  1471. # Initializes the new geometry object
  1472. def gen_paintarea(geo_obj, app_obj):
  1473. assert isinstance(geo_obj, CirkuixGeometry)
  1474. assert isinstance(app_obj, App)
  1475. cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
  1476. geo_obj.solid_geometry = cp
  1477. name = self.selected_item_name + "_paint"
  1478. self.new_object("geometry", name, gen_paintarea)
  1479. self.plot_click_subscribers["generate_paintarea"] = doit
  1480. def on_cncjob_exportgcode(self, widget):
  1481. """
  1482. Called from button on CNCjob form to save the G-Code from the object.
  1483. :param widget: The widget from which this was called.
  1484. :return: None
  1485. """
  1486. def on_success(self, filename):
  1487. cncjob = self.get_current()
  1488. f = open(filename, 'w')
  1489. f.write(cncjob.gcode)
  1490. f.close()
  1491. print "Saved to:", filename
  1492. self.file_chooser_save_action(on_success)
  1493. def on_delete(self, widget):
  1494. """
  1495. Delete the currently selected CirkuixObj.
  1496. :param widget: The widget from which this was called.
  1497. :return: None
  1498. """
  1499. print "on_delete():", self.selected_item_name
  1500. # Remove plot
  1501. self.figure.delaxes(self.get_current().axes)
  1502. self.canvas.queue_draw()
  1503. # Remove from dictionary
  1504. self.stuff.pop(self.selected_item_name)
  1505. # Update UI
  1506. self.build_list() # Update the items list
  1507. def on_replot(self, widget):
  1508. """
  1509. Callback for toolbar button. Re-plots all objects.
  1510. :param widget: The widget from which this was called.
  1511. :return: None
  1512. """
  1513. self.plot_all()
  1514. def on_clear_plots(self, widget):
  1515. """
  1516. Callback for toolbar button. Clears all plots.
  1517. :param widget: The widget from which this was called.
  1518. :return: None
  1519. """
  1520. self.clear_plots()
  1521. def on_activate_name(self, entry):
  1522. """
  1523. Hitting 'Enter' after changing the name of an item
  1524. updates the item dictionary and re-builds the item list.
  1525. :param entry: The widget from which this was called.
  1526. :return: None
  1527. """
  1528. # Disconnect event listener
  1529. self.tree.get_selection().disconnect(self.signal_id)
  1530. new_name = entry.get_text() # Get from form
  1531. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  1532. self.stuff[new_name].options["name"] = new_name # update object
  1533. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  1534. self.selected_item_name = new_name # Update selection name
  1535. self.build_list() # Update the items list
  1536. # Reconnect event listener
  1537. self.signal_id = self.tree.get_selection().connect(
  1538. "changed", self.on_tree_selection_changed)
  1539. def on_tree_selection_changed(self, selection):
  1540. """
  1541. Callback for selection change in the project list. This changes
  1542. the currently selected CirkuixObj.
  1543. :param selection: Selection associated to the project tree or list
  1544. :type selection: Gtk.TreeSelection
  1545. :return: None
  1546. """
  1547. print "on_tree_selection_change(): ",
  1548. model, treeiter = selection.get_selected()
  1549. if treeiter is not None:
  1550. # Save data for previous selection
  1551. obj = self.get_current()
  1552. if obj is not None:
  1553. obj.read_form()
  1554. print "You selected", model[treeiter][0]
  1555. self.selected_item_name = model[treeiter][0]
  1556. obj_new = self.get_current()
  1557. if obj_new is not None:
  1558. GLib.idle_add(lambda: obj_new.build_ui())
  1559. else:
  1560. print "Nothing selected"
  1561. self.selected_item_name = None
  1562. self.setup_component_editor()
  1563. def on_file_new(self, param):
  1564. """
  1565. Callback for menu item File->New. Returns the application to its
  1566. startup state.
  1567. :param param: Whatever is passed by the event. Ignore.
  1568. :return: None
  1569. """
  1570. # Remove everythong from memory
  1571. # Clear plot
  1572. self.clear_plots()
  1573. # Clear object editor
  1574. #self.setup_component_editor()
  1575. # Clear data
  1576. self.stuff = {}
  1577. # Clear list
  1578. #self.tree_select.unselect_all()
  1579. self.build_list()
  1580. # Clear project filename
  1581. self.project_filename = None
  1582. # Re-fresh project options
  1583. self.on_options_app2project(None)
  1584. def on_filequit(self, param):
  1585. """
  1586. Callback for menu item File->Quit. Closes the application.
  1587. :param param: Whatever is passed by the event. Ignore.
  1588. :return: None
  1589. """
  1590. print "quit from menu"
  1591. self.window.destroy()
  1592. Gtk.main_quit()
  1593. def on_closewindow(self, param):
  1594. """
  1595. Callback for closing the main window.
  1596. :param param: Whatever is passed by the event. Ignore.
  1597. :return: None
  1598. """
  1599. print "quit from X"
  1600. self.window.destroy()
  1601. Gtk.main_quit()
  1602. def file_chooser_action(self, on_success):
  1603. """
  1604. Opens the file chooser and runs on_success on a separate thread
  1605. upon completion of valid file choice.
  1606. :param on_success: A function to run upon completion of a valid file
  1607. selection. Takes 2 parameters: The app instance and the filename.
  1608. Note that it is run on a separate thread, therefore it must take the
  1609. appropriate precautions when accessing shared resources.
  1610. :type on_success: func
  1611. :return: None
  1612. """
  1613. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1614. Gtk.FileChooserAction.OPEN,
  1615. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1616. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1617. response = dialog.run()
  1618. if response == Gtk.ResponseType.OK:
  1619. filename = dialog.get_filename()
  1620. dialog.destroy()
  1621. t = threading.Thread(target=on_success, args=(self, filename))
  1622. t.daemon = True
  1623. t.start()
  1624. #on_success(self, filename)
  1625. elif response == Gtk.ResponseType.CANCEL:
  1626. print("Cancel clicked")
  1627. dialog.destroy()
  1628. def file_chooser_save_action(self, on_success):
  1629. """
  1630. Opens the file chooser and runs on_success upon completion of valid file choice.
  1631. :param on_success: A function to run upon selection of a filename. Takes 2
  1632. parameters: The instance of the application (App) and the chosen filename. This
  1633. gets run immediately in the same thread.
  1634. :return: None
  1635. """
  1636. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1637. Gtk.FileChooserAction.SAVE,
  1638. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1639. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1640. dialog.set_current_name("Untitled")
  1641. response = dialog.run()
  1642. if response == Gtk.ResponseType.OK:
  1643. filename = dialog.get_filename()
  1644. dialog.destroy()
  1645. on_success(self, filename)
  1646. elif response == Gtk.ResponseType.CANCEL:
  1647. print("Cancel clicked")
  1648. dialog.destroy()
  1649. def on_fileopengerber(self, param):
  1650. """
  1651. Callback for menu item File->Open Gerber. Defines a function that is then passed
  1652. to ``self.file_chooser_action()``. It requests the creation of a CirkuixGerber object
  1653. and updates the progress bar throughout the process.
  1654. :param param: Ignore
  1655. :return: None
  1656. """
  1657. # IMPORTANT: on_success will run on a separate thread. Use
  1658. # GLib.idle_add(function, **kwargs) to launch actions that will
  1659. # updata the GUI.
  1660. def on_success(app_obj, filename):
  1661. assert isinstance(app_obj, App)
  1662. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  1663. def obj_init(gerber_obj, app_obj):
  1664. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1665. gerber_obj.parse_file(filename)
  1666. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1667. name = filename.split('/')[-1].split('\\')[-1]
  1668. app_obj.new_object("gerber", name, obj_init)
  1669. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1670. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1671. # on_success gets run on a separate thread
  1672. self.file_chooser_action(on_success)
  1673. def on_fileopenexcellon(self, param):
  1674. """
  1675. Callback for menu item File->Open Excellon. Defines a function that is then passed
  1676. to ``self.file_chooser_action()``. It requests the creation of a CirkuixExcellon object
  1677. and updates the progress bar throughout the process.
  1678. :param param: Ignore
  1679. :return: None
  1680. """
  1681. # IMPORTANT: on_success will run on a separate thread. Use
  1682. # GLib.idle_add(function, **kwargs) to launch actions that will
  1683. # updata the GUI.
  1684. def on_success(app_obj, filename):
  1685. assert isinstance(app_obj, App)
  1686. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  1687. def obj_init(excellon_obj, app_obj):
  1688. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1689. excellon_obj.parse_file(filename)
  1690. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1691. name = filename.split('/')[-1].split('\\')[-1]
  1692. app_obj.new_object("excellon", name, obj_init)
  1693. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1694. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1695. # on_success gets run on a separate thread
  1696. self.file_chooser_action(on_success)
  1697. def on_fileopengcode(self, param):
  1698. """
  1699. Callback for menu item File->Open G-Code. Defines a function that is then passed
  1700. to ``self.file_chooser_action()``. It requests the creation of a CirkuixCNCjob object
  1701. and updates the progress bar throughout the process.
  1702. :param param: Ignore
  1703. :return: None
  1704. """
  1705. # IMPORTANT: on_success will run on a separate thread. Use
  1706. # GLib.idle_add(function, **kwargs) to launch actions that will
  1707. # updata the GUI.
  1708. def on_success(app_obj, filename):
  1709. assert isinstance(app_obj, App)
  1710. def obj_init(job_obj, app_obj):
  1711. assert isinstance(app_obj, App)
  1712. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening G-Code ..."))
  1713. f = open(filename)
  1714. gcode = f.read()
  1715. f.close()
  1716. job_obj.gcode = gcode
  1717. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1718. job_obj.gcode_parse()
  1719. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating geometry ..."))
  1720. job_obj.create_geometry()
  1721. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1722. name = filename.split('/')[-1].split('\\')[-1]
  1723. app_obj.new_object("cncjob", name, obj_init)
  1724. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1725. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1726. # on_success gets run on a separate thread
  1727. self.file_chooser_action(on_success)
  1728. def on_mouse_move_over_plot(self, event):
  1729. """
  1730. Callback for the mouse motion event over the plot. This event is generated
  1731. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1732. For details, see: http://matplotlib.org/users/event_handling.html
  1733. :param event: Contains information about the event.
  1734. :return: None
  1735. """
  1736. try: # May fail in case mouse not within axes
  1737. self.position_label.set_label("X: %.4f Y: %.4f" % (
  1738. event.xdata, event.ydata))
  1739. self.mouse = [event.xdata, event.ydata]
  1740. except:
  1741. self.position_label.set_label("")
  1742. self.mouse = None
  1743. def on_click_over_plot(self, event):
  1744. """
  1745. Callback for the mouse click event over the plot. This event is generated
  1746. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1747. For details, see: http://matplotlib.org/users/event_handling.html
  1748. :param event:
  1749. :return:
  1750. """
  1751. # For key presses
  1752. self.canvas.grab_focus()
  1753. try:
  1754. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
  1755. event.button, event.x, event.y, event.xdata, event.ydata)
  1756. for subscriber in self.plot_click_subscribers:
  1757. self.plot_click_subscribers[subscriber](event)
  1758. except Exception, e:
  1759. print "Outside plot!"
  1760. def on_zoom_in(self, event):
  1761. """
  1762. Callback for zoom-in request. This can be either from the corresponding
  1763. toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
  1764. :param event: Ignored.
  1765. :return: None
  1766. """
  1767. self.zoom(1.5)
  1768. return
  1769. def on_zoom_out(self, event):
  1770. """
  1771. Callback for zoom-out request. This can be either from the corresponding
  1772. toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
  1773. :param event: Ignored.
  1774. :return: None
  1775. """
  1776. self.zoom(1 / 1.5)
  1777. def on_zoom_fit(self, event):
  1778. """
  1779. Callback for zoom-out request. This can be either from the corresponding
  1780. toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
  1781. with axes limits from the geometry bounds of all objects.
  1782. :param event: Ignored.
  1783. :return: None
  1784. """
  1785. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  1786. width = xmax - xmin
  1787. height = ymax - ymin
  1788. xmin -= 0.05 * width
  1789. xmax += 0.05 * width
  1790. ymin -= 0.05 * height
  1791. ymax += 0.05 * height
  1792. self.adjust_axes(xmin, ymin, xmax, ymax)
  1793. # def on_scroll_over_plot(self, event):
  1794. # print "Scroll"
  1795. # center = [event.xdata, event.ydata]
  1796. # if sign(event.step):
  1797. # self.zoom(1.5, center=center)
  1798. # else:
  1799. # self.zoom(1/1.5, center=center)
  1800. #
  1801. # def on_window_scroll(self, event):
  1802. # print "Scroll"
  1803. def on_key_over_plot(self, event):
  1804. """
  1805. Callback for the key pressed event when the canvas is focused. Keyboard
  1806. shortcuts are handled here. So far, these are the shortcuts:
  1807. ========== ============================================
  1808. Key Action
  1809. ========== ============================================
  1810. '1' Zoom-fit. Fits the axes limits to the data.
  1811. '2' Zoom-out.
  1812. '3' Zoom-in.
  1813. ========== ============================================
  1814. :param event: Ignored.
  1815. :return: None
  1816. """
  1817. print 'you pressed', event.key, event.xdata, event.ydata
  1818. if event.key == '1': # 1
  1819. self.on_zoom_fit(None)
  1820. return
  1821. if event.key == '2': # 2
  1822. self.zoom(1 / 1.5, self.mouse)
  1823. return
  1824. if event.key == '3': # 3
  1825. self.zoom(1.5, self.mouse)
  1826. return
  1827. app = App()
  1828. Gtk.main()