FlatCAM.py 77 KB

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