FlatCAM.py 101 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. import threading
  9. # TODO: Bundle together. This is just for debugging.
  10. from gi.repository import Gtk
  11. from gi.repository import Gdk
  12. from gi.repository import GdkPixbuf
  13. from gi.repository import GLib
  14. from gi.repository import GObject
  15. import simplejson as json
  16. from matplotlib.figure import Figure
  17. from numpy import arange, sin, pi
  18. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  19. from mpl_toolkits.axes_grid.anchored_artists import AnchoredText
  20. from camlib import *
  21. import sys
  22. import urllib
  23. import copy
  24. ########################################
  25. ## FlatCAMObj ##
  26. ########################################
  27. class FlatCAMObj:
  28. """
  29. Base type of objects handled in FlatCAM. These become interactive
  30. in the GUI, can be plotted, and their options can be modified
  31. by the user in their respective forms.
  32. """
  33. # Instance of the application to which these are related.
  34. # The app should set this value.
  35. app = None
  36. def __init__(self, name):
  37. self.options = {"name": name}
  38. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  39. self.radios = {} # Name value pairs for radio sets
  40. self.radios_inv = {} # Inverse of self.radios
  41. self.axes = None # Matplotlib axes
  42. self.kind = None # Override with proper name
  43. def setup_axes(self, figure):
  44. """
  45. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  46. them to figure if not part of the figure. 4) Sets transparent
  47. background. 5) Sets 1:1 scale aspect ratio.
  48. :param figure: A Matplotlib.Figure on which to add/configure axes.
  49. :type figure: matplotlib.figure.Figure
  50. :return: None
  51. :rtype: None
  52. """
  53. if self.axes is None:
  54. print "New axes"
  55. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  56. label=self.options["name"])
  57. elif self.axes not in figure.axes:
  58. print "Clearing and attaching axes"
  59. self.axes.cla()
  60. figure.add_axes(self.axes)
  61. else:
  62. print "Clearing Axes"
  63. self.axes.cla()
  64. # Remove all decoration. The app's axes will have
  65. # the ticks and grid.
  66. self.axes.set_frame_on(False) # No frame
  67. self.axes.set_xticks([]) # No tick
  68. self.axes.set_yticks([]) # No ticks
  69. self.axes.patch.set_visible(False) # No background
  70. self.axes.set_aspect(1)
  71. def to_form(self):
  72. """
  73. Copies options to the UI form.
  74. :return: None
  75. """
  76. for option in self.options:
  77. self.set_form_item(option)
  78. def read_form(self):
  79. """
  80. Reads form into ``self.options``.
  81. :return: None
  82. :rtype: None
  83. """
  84. for option in self.options:
  85. self.read_form_item(option)
  86. def build_ui(self):
  87. """
  88. Sets up the UI/form for this object.
  89. :return: None
  90. :rtype: None
  91. """
  92. # Where the UI for this object is drawn
  93. box_selected = self.app.builder.get_object("box_selected")
  94. # Remove anything else in the box
  95. box_children = box_selected.get_children()
  96. for child in box_children:
  97. box_selected.remove(child)
  98. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  99. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  100. osw.remove(sw) # TODO: Is this needed ?
  101. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  102. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  103. # Put in the UI
  104. box_selected.pack_start(sw, True, True, 0)
  105. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  106. entry_name.connect("activate", self.app.on_activate_name)
  107. self.to_form()
  108. sw.show()
  109. def set_form_item(self, option):
  110. """
  111. Copies the specified option to the UI form.
  112. :param option: Name of the option (Key in ``self.options``).
  113. :type option: str
  114. :return: None
  115. """
  116. fkind = self.form_kinds[option]
  117. fname = fkind + "_" + self.kind + "_" + option
  118. if fkind == 'entry_eval' or fkind == 'entry_text':
  119. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  120. return
  121. if fkind == 'cb':
  122. self.app.builder.get_object(fname).set_active(self.options[option])
  123. return
  124. if fkind == 'radio':
  125. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  126. return
  127. print "Unknown kind of form item:", fkind
  128. def read_form_item(self, option):
  129. """
  130. Reads the specified option from the UI form into ``self.options``.
  131. :param option: Name of the option.
  132. :type option: str
  133. :return: None
  134. """
  135. fkind = self.form_kinds[option]
  136. fname = fkind + "_" + self.kind + "_" + option
  137. if fkind == 'entry_text':
  138. self.options[option] = self.app.builder.get_object(fname).get_text()
  139. return
  140. if fkind == 'entry_eval':
  141. self.options[option] = self.app.get_eval(fname)
  142. return
  143. if fkind == 'cb':
  144. self.options[option] = self.app.builder.get_object(fname).get_active()
  145. return
  146. if fkind == 'radio':
  147. self.options[option] = self.app.get_radio_value(self.radios[option])
  148. return
  149. print "Unknown kind of form item:", fkind
  150. # def plot(self, figure):
  151. # """
  152. # Extend this method! Sets up axes if needed and
  153. # clears them. Descendants must do the actual plotting.
  154. # """
  155. # # Creates the axes if necessary and sets them up.
  156. # self.setup_axes(figure)
  157. def plot(self):
  158. if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
  159. self.axes = self.app.plotcanvas.new_axes(self.options['name'])
  160. if not self.options["plot"]:
  161. self.axes.cla()
  162. self.app.plotcanvas.auto_adjust_axes()
  163. return False
  164. return True
  165. def serialize(self):
  166. """
  167. Returns a representation of the object as a dictionary so
  168. it can be later exported as JSON. Override this method.
  169. :return: Dictionary representing the object
  170. :rtype: dict
  171. """
  172. return
  173. def deserialize(self, obj_dict):
  174. """
  175. Re-builds an object from its serialized version.
  176. :param obj_dict: Dictionary representing a FlatCAMObj
  177. :type obj_dict: dict
  178. :return None
  179. """
  180. return
  181. class FlatCAMGerber(FlatCAMObj, Gerber):
  182. """
  183. Represents Gerber code.
  184. """
  185. def __init__(self, name):
  186. Gerber.__init__(self)
  187. FlatCAMObj.__init__(self, name)
  188. self.kind = "gerber"
  189. # The 'name' is already in self.options from FlatCAMObj
  190. self.options.update({
  191. "plot": True,
  192. "mergepolys": True,
  193. "multicolored": False,
  194. "solid": False,
  195. "isotooldia": 0.016,
  196. "isopasses": 1,
  197. "isooverlap": 0.15,
  198. "cutoutmargin": 0.2,
  199. "cutoutgapsize": 0.15,
  200. "gaps": "tb",
  201. "noncoppermargin": 0.0,
  202. "bboxmargin": 0.0,
  203. "bboxrounded": False
  204. })
  205. # The 'name' is already in self.form_kinds from FlatCAMObj
  206. self.form_kinds.update({
  207. "plot": "cb",
  208. "mergepolys": "cb",
  209. "multicolored": "cb",
  210. "solid": "cb",
  211. "isotooldia": "entry_eval",
  212. "isopasses": "entry_eval",
  213. "isooverlap": "entry_eval",
  214. "cutoutmargin": "entry_eval",
  215. "cutoutgapsize": "entry_eval",
  216. "gaps": "radio",
  217. "noncoppermargin": "entry_eval",
  218. "bboxmargin": "entry_eval",
  219. "bboxrounded": "cb"
  220. })
  221. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  222. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  223. # Attributes to be included in serialization
  224. # Always append to it because it carries contents
  225. # from predecessors.
  226. self.ser_attrs += ['options', 'kind']
  227. def convert_units(self, units):
  228. """
  229. Converts the units of the object by scaling dimensions in all geometry
  230. and options.
  231. :param units: Units to which to convert the object: "IN" or "MM".
  232. :type units: str
  233. :return: None
  234. :rtype: None
  235. """
  236. factor = Gerber.convert_units(self, units)
  237. self.options['isotooldia'] *= factor
  238. self.options['cutoutmargin'] *= factor
  239. self.options['cutoutgapsize'] *= factor
  240. self.options['noncoppermargin'] *= factor
  241. self.options['bboxmargin'] *= factor
  242. def plot(self):
  243. # Does all the required setup and returns False
  244. # if the 'ptint' option is set to False.
  245. if not FlatCAMObj.plot(self):
  246. return
  247. if self.options["mergepolys"]:
  248. geometry = self.solid_geometry
  249. else:
  250. geometry = self.buffered_paths + \
  251. [poly['polygon'] for poly in self.regions] + \
  252. self.flash_geometry
  253. if self.options["multicolored"]:
  254. linespec = '-'
  255. else:
  256. linespec = 'k-'
  257. if self.options["solid"]:
  258. for poly in geometry:
  259. # TODO: Too many things hardcoded.
  260. patch = PolygonPatch(poly,
  261. facecolor="#BBF268",
  262. edgecolor="#006E20",
  263. alpha=0.75,
  264. zorder=2)
  265. self.axes.add_patch(patch)
  266. else:
  267. for poly in geometry:
  268. x, y = poly.exterior.xy
  269. self.axes.plot(x, y, linespec)
  270. for ints in poly.interiors:
  271. x, y = ints.coords.xy
  272. self.axes.plot(x, y, linespec)
  273. self.app.plotcanvas.auto_adjust_axes()
  274. def serialize(self):
  275. return {
  276. "options": self.options,
  277. "kind": self.kind
  278. }
  279. class FlatCAMExcellon(FlatCAMObj, Excellon):
  280. """
  281. Represents Excellon/Drill code.
  282. """
  283. def __init__(self, name):
  284. Excellon.__init__(self)
  285. FlatCAMObj.__init__(self, name)
  286. self.kind = "excellon"
  287. self.options.update({
  288. "plot": True,
  289. "solid": False,
  290. "multicolored": False,
  291. "drillz": -0.1,
  292. "travelz": 0.1,
  293. "feedrate": 5.0,
  294. "toolselection": ""
  295. })
  296. self.form_kinds.update({
  297. "plot": "cb",
  298. "solid": "cb",
  299. "multicolored": "cb",
  300. "drillz": "entry_eval",
  301. "travelz": "entry_eval",
  302. "feedrate": "entry_eval",
  303. "toolselection": "entry_text"
  304. })
  305. # TODO: Document this.
  306. self.tool_cbs = {}
  307. # Attributes to be included in serialization
  308. # Always append to it because it carries contents
  309. # from predecessors.
  310. self.ser_attrs += ['options', 'kind']
  311. def convert_units(self, units):
  312. factor = Excellon.convert_units(self, units)
  313. self.options['drillz'] *= factor
  314. self.options['travelz'] *= factor
  315. self.options['feedrate'] *= factor
  316. def plot(self):
  317. # Does all the required setup and returns False
  318. # if the 'ptint' option is set to False.
  319. if not FlatCAMObj.plot(self):
  320. return
  321. try:
  322. _ = iter(self.solid_geometry)
  323. except TypeError:
  324. self.solid_geometry = [self.solid_geometry]
  325. # Plot excellon (All polygons?)
  326. for geo in self.solid_geometry:
  327. x, y = geo.exterior.coords.xy
  328. self.axes.plot(x, y, 'r-')
  329. for ints in geo.interiors:
  330. x, y = ints.coords.xy
  331. self.axes.plot(x, y, 'g-')
  332. self.app.plotcanvas.auto_adjust_axes()
  333. def show_tool_chooser(self):
  334. win = Gtk.Window()
  335. box = Gtk.Box(spacing=2)
  336. box.set_orientation(Gtk.Orientation(1))
  337. win.add(box)
  338. for tool in self.tools:
  339. self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
  340. box.pack_start(self.tool_cbs[tool], False, False, 1)
  341. button = Gtk.Button(label="Accept")
  342. box.pack_start(button, False, False, 1)
  343. win.show_all()
  344. def on_accept(widget):
  345. win.destroy()
  346. tool_list = []
  347. for tool in self.tool_cbs:
  348. if self.tool_cbs[tool].get_active():
  349. tool_list.append(tool)
  350. self.options["toolselection"] = ", ".join(tool_list)
  351. self.to_form()
  352. button.connect("activate", on_accept)
  353. button.connect("clicked", on_accept)
  354. class FlatCAMCNCjob(FlatCAMObj, CNCjob):
  355. """
  356. Represents G-Code.
  357. """
  358. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  359. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  360. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  361. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  362. FlatCAMObj.__init__(self, name)
  363. self.kind = "cncjob"
  364. self.options.update({
  365. "plot": True,
  366. "tooldia": 0.4 / 25.4 # 0.4mm in inches
  367. })
  368. self.form_kinds.update({
  369. "plot": "cb",
  370. "tooldia": "entry_eval"
  371. })
  372. # Attributes to be included in serialization
  373. # Always append to it because it carries contents
  374. # from predecessors.
  375. self.ser_attrs += ['options', 'kind']
  376. def plot(self):
  377. # Does all the required setup and returns False
  378. # if the 'ptint' option is set to False.
  379. if not FlatCAMObj.plot(self):
  380. return
  381. self.plot2(self.axes, tooldia=self.options["tooldia"])
  382. self.app.plotcanvas.auto_adjust_axes()
  383. def convert_units(self, units):
  384. factor = CNCjob.convert_units(self, units)
  385. print "FlatCAMCNCjob.convert_units()"
  386. self.options["tooldia"] *= factor
  387. class FlatCAMGeometry(FlatCAMObj, Geometry):
  388. """
  389. Geometric object not associated with a specific
  390. format.
  391. """
  392. def __init__(self, name):
  393. FlatCAMObj.__init__(self, name)
  394. Geometry.__init__(self)
  395. self.kind = "geometry"
  396. self.options.update({
  397. "plot": True,
  398. "solid": False,
  399. "multicolored": False,
  400. "cutz": -0.002,
  401. "travelz": 0.1,
  402. "feedrate": 5.0,
  403. "cnctooldia": 0.4 / 25.4,
  404. "painttooldia": 0.0625,
  405. "paintoverlap": 0.15,
  406. "paintmargin": 0.01
  407. })
  408. self.form_kinds.update({
  409. "plot": "cb",
  410. "solid": "cb",
  411. "multicolored": "cb",
  412. "cutz": "entry_eval",
  413. "travelz": "entry_eval",
  414. "feedrate": "entry_eval",
  415. "cnctooldia": "entry_eval",
  416. "painttooldia": "entry_eval",
  417. "paintoverlap": "entry_eval",
  418. "paintmargin": "entry_eval"
  419. })
  420. # Attributes to be included in serialization
  421. # Always append to it because it carries contents
  422. # from predecessors.
  423. self.ser_attrs += ['options', 'kind']
  424. def scale(self, factor):
  425. """
  426. Scales all geometry by a given factor.
  427. :param factor: Factor by which to scale the object's geometry/
  428. :type factor: float
  429. :return: None
  430. :rtype: None
  431. """
  432. if type(self.solid_geometry) == list:
  433. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  434. for g in self.solid_geometry]
  435. else:
  436. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  437. origin=(0, 0))
  438. def offset(self, vect):
  439. """
  440. Offsets all geometry by a given vector/
  441. :param vect: (x, y) vector by which to offset the object's geometry.
  442. :type vect: tuple
  443. :return: None
  444. :rtype: None
  445. """
  446. dx, dy = vect
  447. if type(self.solid_geometry) == list:
  448. self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
  449. for g in self.solid_geometry]
  450. else:
  451. self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
  452. def convert_units(self, units):
  453. factor = Geometry.convert_units(self, units)
  454. self.options['cutz'] *= factor
  455. self.options['travelz'] *= factor
  456. self.options['feedrate'] *= factor
  457. self.options['cnctooldia'] *= factor
  458. self.options['painttooldia'] *= factor
  459. self.options['paintmargin'] *= factor
  460. return factor
  461. def plot(self):
  462. """
  463. Plots the object into its axes. If None, of if the axes
  464. are not part of the app's figure, it fetches new ones.
  465. :return: None
  466. """
  467. # Does all the required setup and returns False
  468. # if the 'ptint' option is set to False.
  469. if not FlatCAMObj.plot(self):
  470. return
  471. # Make sure solid_geometry is iterable.
  472. try:
  473. _ = iter(self.solid_geometry)
  474. except TypeError:
  475. self.solid_geometry = [self.solid_geometry]
  476. for geo in self.solid_geometry:
  477. if type(geo) == Polygon:
  478. x, y = geo.exterior.coords.xy
  479. self.axes.plot(x, y, 'r-')
  480. for ints in geo.interiors:
  481. x, y = ints.coords.xy
  482. self.axes.plot(x, y, 'r-')
  483. continue
  484. if type(geo) == LineString or type(geo) == LinearRing:
  485. x, y = geo.coords.xy
  486. self.axes.plot(x, y, 'r-')
  487. continue
  488. if type(geo) == MultiPolygon:
  489. for poly in geo:
  490. x, y = poly.exterior.coords.xy
  491. self.axes.plot(x, y, 'r-')
  492. for ints in poly.interiors:
  493. x, y = ints.coords.xy
  494. self.axes.plot(x, y, 'r-')
  495. continue
  496. print "WARNING: Did not plot:", str(type(geo))
  497. self.app.plotcanvas.auto_adjust_axes()
  498. ########################################
  499. ## App ##
  500. ########################################
  501. class App:
  502. """
  503. The main application class. The constructor starts the GUI.
  504. """
  505. def __init__(self):
  506. """
  507. Starts the application. Takes no parameters.
  508. :return: app
  509. :rtype: App
  510. """
  511. # Needed to interact with the GUI from other threads.
  512. GObject.threads_init()
  513. # GLib.log_set_handler()
  514. #### GUI ####
  515. self.gladefile = "FlatCAM.ui"
  516. self.builder = Gtk.Builder()
  517. self.builder.add_from_file(self.gladefile)
  518. self.window = self.builder.get_object("window1")
  519. self.position_label = self.builder.get_object("label3")
  520. self.grid = self.builder.get_object("grid1")
  521. self.notebook = self.builder.get_object("notebook1")
  522. self.info_label = self.builder.get_object("label_status")
  523. self.progress_bar = self.builder.get_object("progressbar")
  524. self.progress_bar.set_show_text(True)
  525. self.units_label = self.builder.get_object("label_units")
  526. self.toolbar = self.builder.get_object("toolbar_main")
  527. # White (transparent) background on the "Options" tab.
  528. self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
  529. Gdk.RGBA(1, 1, 1, 1))
  530. # Combo box to choose between project and application options.
  531. self.combo_options = self.builder.get_object("combo_options")
  532. self.combo_options.set_active(1)
  533. self.setup_project_list() # The "Project" tab
  534. self.setup_component_editor() # The "Selected" tab
  535. self.setup_toolbar()
  536. #### Event handling ####
  537. self.builder.connect_signals(self)
  538. #### Make plot area ####
  539. self.plotcanvas = PlotCanvas(self.grid)
  540. self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
  541. self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  542. self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
  543. self.setup_tooltips()
  544. #### DATA ####
  545. self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  546. self.setup_obj_classes()
  547. self.stuff = {} # FlatCAMObj's by name
  548. self.mouse = None # Mouse coordinates over plot
  549. # What is selected by the user. It is
  550. # a key if self.stuff
  551. self.selected_item_name = None
  552. # Used to inhibit the on_options_update callback when
  553. # the options are being changed by the program and not the user.
  554. self.options_update_ignore = False
  555. self.toggle_units_ignore = False
  556. self.defaults = {
  557. "units": "in"
  558. } # Application defaults
  559. ## Current Project ##
  560. self.options = {} # Project options
  561. self.project_filename = None
  562. self.form_kinds = {
  563. "units": "radio"
  564. }
  565. self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
  566. "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
  567. self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
  568. "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
  569. # Options for each kind of FlatCAMObj.
  570. # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
  571. for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
  572. obj = FlatCAMClass("no_name")
  573. for option in obj.form_kinds:
  574. self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
  575. # if obj.form_kinds[option] == "radio":
  576. # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
  577. # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
  578. ## Event subscriptions ##
  579. self.plot_click_subscribers = {}
  580. self.plot_mousemove_subscribers = {}
  581. ## Tools ##
  582. self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas.axes,
  583. self.plot_click_subscribers, self.plot_mousemove_subscribers)
  584. # Toolbar icon
  585. # TODO: Where should I put this? Tool should have a method to add to toolbar?
  586. meas_ico = Gtk.Image.new_from_file('share/measure32.png')
  587. measure = Gtk.ToolButton.new(meas_ico, "")
  588. measure.connect("clicked", self.measure.toggle_active)
  589. measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
  590. "Click on point to set reference.\n" +
  591. "(Click on plot and hit <b>m</b>)")
  592. self.toolbar.insert(measure, -1)
  593. #### Initialization ####
  594. self.load_defaults()
  595. self.options.update(self.defaults) # Copy app defaults to project options
  596. self.options2form() # Populate the app defaults form
  597. self.units_label.set_text("[" + self.options["units"] + "]")
  598. #### Check for updates ####
  599. self.version = 2
  600. t1 = threading.Thread(target=self.versionCheck)
  601. t1.daemon = True
  602. t1.start()
  603. #### For debugging only ###
  604. def someThreadFunc(app_obj):
  605. print "Hello World!"
  606. t = threading.Thread(target=someThreadFunc, args=(self,))
  607. t.daemon = True
  608. t.start()
  609. ########################################
  610. ## START ##
  611. ########################################
  612. self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
  613. self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
  614. self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
  615. Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
  616. self.window.set_title("FlatCAM - Alpha 3 UNSTABLE - Check for updates!")
  617. self.window.set_default_size(900, 600)
  618. self.window.show_all()
  619. def setup_toolbar(self):
  620. # Zoom fit
  621. zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
  622. zoom_fit = Gtk.ToolButton.new(zf_ico, "")
  623. zoom_fit.connect("clicked", self.on_zoom_fit)
  624. zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
  625. self.toolbar.insert(zoom_fit, -1)
  626. # Zoom out
  627. zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
  628. zoom_out = Gtk.ToolButton.new(zo_ico, "")
  629. zoom_out.connect("clicked", self.on_zoom_out)
  630. zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
  631. self.toolbar.insert(zoom_out, -1)
  632. # Zoom in
  633. zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
  634. zoom_in = Gtk.ToolButton.new(zi_ico, "")
  635. zoom_in.connect("clicked", self.on_zoom_in)
  636. zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
  637. self.toolbar.insert(zoom_in, -1)
  638. # Clear plot
  639. cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
  640. clear_plot = Gtk.ToolButton.new(cp_ico, "")
  641. clear_plot.connect("clicked", self.on_clear_plots)
  642. clear_plot.set_tooltip_markup("Clear Plot")
  643. self.toolbar.insert(clear_plot, -1)
  644. # Replot
  645. rp_ico = Gtk.Image.new_from_file('share/replot32.png')
  646. replot = Gtk.ToolButton.new(rp_ico, "")
  647. replot.connect("clicked", self.on_toolbar_replot)
  648. replot.set_tooltip_markup("Re-plot all")
  649. self.toolbar.insert(replot, -1)
  650. # Delete item
  651. del_ico = Gtk.Image.new_from_file('share/delete32.png')
  652. delete = Gtk.ToolButton.new(del_ico, "")
  653. delete.connect("clicked", self.on_delete)
  654. delete.set_tooltip_markup("Delete selected\nobject.")
  655. self.toolbar.insert(delete, -1)
  656. def setup_obj_classes(self):
  657. """
  658. Sets up application specifics on the FlatCAMObj class.
  659. :return: None
  660. """
  661. FlatCAMObj.app = self
  662. def setup_project_list(self):
  663. """
  664. Sets up list or Tree where whatever has been loaded or created is
  665. displayed.
  666. :return: None
  667. """
  668. self.store = Gtk.ListStore(str)
  669. self.tree = Gtk.TreeView(self.store)
  670. #self.list = Gtk.ListBox()
  671. self.tree.connect("row_activated", self.on_row_activated)
  672. self.tree_select = self.tree.get_selection()
  673. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  674. renderer = Gtk.CellRendererText()
  675. column = Gtk.TreeViewColumn("Objects", renderer, text=0)
  676. self.tree.append_column(column)
  677. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  678. def setup_component_editor(self):
  679. """
  680. Initial configuration of the component editor. Creates
  681. a page titled "Selection" on the notebook on the left
  682. side of the main window.
  683. :return: None
  684. """
  685. box_selected = self.builder.get_object("box_selected")
  686. # Remove anything else in the box
  687. box_children = box_selected.get_children()
  688. for child in box_children:
  689. box_selected.remove(child)
  690. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  691. label1 = Gtk.Label("Choose an item from Project")
  692. box1.pack_start(label1, True, False, 1)
  693. box_selected.pack_start(box1, True, True, 0)
  694. #box_selected.show()
  695. box1.show()
  696. label1.show()
  697. def info(self, text):
  698. """
  699. Show text on the status bar.
  700. :param text: Text to display.
  701. :type text: str
  702. :return: None
  703. """
  704. self.info_label.set_text(text)
  705. def build_list(self):
  706. """
  707. Clears and re-populates the list of objects in currently
  708. in the project.
  709. :return: None
  710. """
  711. print "build_list(): clearing"
  712. self.tree_select.unselect_all()
  713. self.store.clear()
  714. print "repopulating...",
  715. for key in self.stuff:
  716. print key,
  717. self.store.append([key])
  718. print
  719. def get_radio_value(self, radio_set):
  720. """
  721. Returns the radio_set[key] of the radiobutton
  722. whose name is key is active.
  723. :param radio_set: A dictionary containing widget_name: value pairs.
  724. :type radio_set: dict
  725. :return: radio_set[key]
  726. """
  727. for name in radio_set:
  728. if self.builder.get_object(name).get_active():
  729. return radio_set[name]
  730. def plot_all(self):
  731. """
  732. Re-generates all plots from all objects.
  733. :return: None
  734. """
  735. self.plotcanvas.clear()
  736. self.set_progress_bar(0.1, "Re-plotting...")
  737. def thread_func(app_obj):
  738. percentage = 0.1
  739. try:
  740. delta = 0.9 / len(self.stuff)
  741. except ZeroDivisionError:
  742. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  743. return
  744. for i in self.stuff:
  745. self.stuff[i].plot()
  746. percentage += delta
  747. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  748. app_obj.plotcanvas.auto_adjust_axes()
  749. self.on_zoom_fit(None)
  750. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  751. t = threading.Thread(target=thread_func, args=(self,))
  752. t.daemon = True
  753. t.start()
  754. def get_eval(self, widget_name):
  755. """
  756. Runs eval() on the on the text entry of name 'widget_name'
  757. and returns the results.
  758. :param widget_name: Name of Gtk.Entry
  759. :type widget_name: str
  760. :return: Depends on contents of the entry text.
  761. """
  762. value = self.builder.get_object(widget_name).get_text()
  763. if value == "":
  764. value = "None"
  765. try:
  766. evald = eval(value)
  767. return evald
  768. except:
  769. self.info("Could not evaluate: " + value)
  770. return None
  771. def set_list_selection(self, name):
  772. """
  773. Marks a given object as selected in the list ob objects
  774. in the GUI. This selection will in turn trigger
  775. ``self.on_tree_selection_changed()``.
  776. :param name: Name of the object.
  777. :type name: str
  778. :return: None
  779. """
  780. iter = self.store.get_iter_first()
  781. while iter is not None and self.store[iter][0] != name:
  782. iter = self.store.iter_next(iter)
  783. self.tree_select.unselect_all()
  784. self.tree_select.select_iter(iter)
  785. # Need to return False such that GLib.idle_add
  786. # or .timeout_add do not repeat.
  787. return False
  788. def new_object(self, kind, name, initialize):
  789. """
  790. Creates a new specalized FlatCAMObj and attaches it to the application,
  791. this is, updates the GUI accordingly, any other records and plots it.
  792. :param kind: The kind of object to create. One of 'gerber',
  793. 'excellon', 'cncjob' and 'geometry'.
  794. :type kind: str
  795. :param name: Name for the object.
  796. :type name: str
  797. :param initialize: Function to run after creation of the object
  798. but before it is attached to the application. The function is
  799. called with 2 parameters: the new object and the App instance.
  800. :type initialize: function
  801. :return: None
  802. :rtype: None
  803. """
  804. # Check for existing name
  805. if name in self.stuff:
  806. self.info("Rename " + name + " in project first.")
  807. return None
  808. # Create object
  809. classdict = {
  810. "gerber": FlatCAMGerber,
  811. "excellon": FlatCAMExcellon,
  812. "cncjob": FlatCAMCNCjob,
  813. "geometry": FlatCAMGeometry
  814. }
  815. obj = classdict[kind](name)
  816. obj.units = self.options["units"] # TODO: The constructor should look at defaults.
  817. # Initialize as per user request
  818. # User must take care to implement initialize
  819. # in a thread-safe way as is is likely that we
  820. # have been invoked in a separate thread.
  821. #initialize(obj, self)
  822. # Set default options from self.options
  823. for option in self.options:
  824. if option.find(kind + "_") == 0:
  825. oname = option[len(kind)+1:]
  826. obj.options[oname] = self.options[option]
  827. # Initialize as per user request
  828. # User must take care to implement initialize
  829. # in a thread-safe way as is is likely that we
  830. # have been invoked in a separate thread.
  831. initialize(obj, self)
  832. # Check units and convert if necessary
  833. if self.options["units"].upper() != obj.units.upper():
  834. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  835. obj.convert_units(self.options["units"])
  836. # Add to our records
  837. self.stuff[name] = obj
  838. # Update GUI list and select it (Thread-safe?)
  839. self.store.append([name])
  840. #self.build_list()
  841. GLib.idle_add(lambda: self.set_list_selection(name))
  842. # TODO: Gtk.notebook.set_current_page is not known to
  843. # TODO: return False. Fix this??
  844. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  845. # Plot
  846. # TODO: (Thread-safe?)
  847. obj.plot()
  848. self.on_zoom_fit(None)
  849. return obj
  850. def set_progress_bar(self, percentage, text=""):
  851. """
  852. Sets the application's progress bar to a given frac_digits and text.
  853. :param percentage: The frac_digits (0.0-1.0) of the progress.
  854. :type percentage: float
  855. :param text: Text to display on the progress bar.
  856. :type text: str
  857. :return: None
  858. """
  859. self.progress_bar.set_text(text)
  860. self.progress_bar.set_fraction(percentage)
  861. return False
  862. def get_current(self):
  863. """
  864. Returns the currently selected FlatCAMObj in the application.
  865. :return: Currently selected FlatCAMObj in the application.
  866. :rtype: FlatCAMObj or None
  867. """
  868. # TODO: Could possibly read the form into the object here.
  869. # But there are some cases when the form for the object
  870. # is not up yet. See on_tree_selection_changed.
  871. try:
  872. return self.stuff[self.selected_item_name]
  873. except:
  874. return None
  875. def load_defaults(self):
  876. """
  877. Loads the aplication's default settings from defaults.json into
  878. ``self.defaults``.
  879. :return: None
  880. """
  881. try:
  882. f = open("defaults.json")
  883. options = f.read()
  884. f.close()
  885. except:
  886. self.info("ERROR: Could not load defaults file.")
  887. return
  888. try:
  889. defaults = json.loads(options)
  890. except:
  891. e = sys.exc_info()[0]
  892. print e
  893. self.info("ERROR: Failed to parse defaults file.")
  894. return
  895. self.defaults.update(defaults)
  896. def read_form(self):
  897. """
  898. Reads the options form into self.defaults/self.options.
  899. :return: None
  900. :rtype: None
  901. """
  902. combo_sel = self.combo_options.get_active()
  903. options_set = [self.options, self.defaults][combo_sel]
  904. for option in options_set:
  905. self.read_form_item(option, options_set)
  906. def read_form_item(self, name, dest):
  907. """
  908. Reads the value of a form item in the defaults/options form and
  909. saves it to the corresponding dictionary.
  910. :param name: Name of the form item. A key in ``self.defaults`` or
  911. ``self.options``.
  912. :type name: str
  913. :param dest: Dictionary to which to save the value.
  914. :type dest: dict
  915. :return: None
  916. """
  917. fkind = self.form_kinds[name]
  918. fname = fkind + "_" + "app" + "_" + name
  919. if fkind == 'entry_text':
  920. dest[name] = self.builder.get_object(fname).get_text()
  921. return
  922. if fkind == 'entry_eval':
  923. dest[name] = self.get_eval(fname)
  924. return
  925. if fkind == 'cb':
  926. dest[name] = self.builder.get_object(fname).get_active()
  927. return
  928. if fkind == 'radio':
  929. dest[name] = self.get_radio_value(self.radios[name])
  930. return
  931. print "Unknown kind of form item:", fkind
  932. def options2form(self):
  933. """
  934. Sets the 'Project Options' or 'Application Defaults' form with values from
  935. ``self.options`` or ``self.defaults``.
  936. :return: None
  937. :rtype: None
  938. """
  939. # Set the on-change callback to do nothing while we do the changes.
  940. self.options_update_ignore = True
  941. self.toggle_units_ignore = True
  942. combo_sel = self.combo_options.get_active()
  943. options_set = [self.options, self.defaults][combo_sel]
  944. for option in options_set:
  945. self.set_form_item(option, options_set[option])
  946. self.options_update_ignore = False
  947. self.toggle_units_ignore = False
  948. def set_form_item(self, name, value):
  949. """
  950. Sets a form item 'name' in the GUI with the given 'value'. The syntax of
  951. form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
  952. cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
  953. whatever name it's been given. For self.defaults, name is a key in the dictionary.
  954. :param name: Name of the form field.
  955. :type name: str
  956. :param value: The value to set the form field to.
  957. :type value: Depends on field kind.
  958. :return: None
  959. """
  960. if name not in self.form_kinds:
  961. print "WARNING: Tried to set unknown option/form item:", name
  962. return
  963. fkind = self.form_kinds[name]
  964. fname = fkind + "_" + "app" + "_" + name
  965. if fkind == 'entry_eval' or fkind == 'entry_text':
  966. try:
  967. self.builder.get_object(fname).set_text(str(value))
  968. except:
  969. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  970. return
  971. if fkind == 'cb':
  972. try:
  973. self.builder.get_object(fname).set_active(value)
  974. except:
  975. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  976. return
  977. if fkind == 'radio':
  978. try:
  979. self.builder.get_object(self.radios_inv[name][value]).set_active(True)
  980. except:
  981. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  982. return
  983. print "Unknown kind of form item:", fkind
  984. def save_project(self, filename):
  985. """
  986. Saves the current project to the specified file.
  987. :param filename: Name of the file in which to save.
  988. :type filename: str
  989. :return: None
  990. """
  991. # Capture the latest changes
  992. try:
  993. self.get_current().read_form()
  994. except:
  995. pass
  996. d = {"objs": [self.stuff[o].to_dict() for o in self.stuff],
  997. "options": self.options}
  998. try:
  999. f = open(filename, 'w')
  1000. except:
  1001. print "ERROR: Failed to open file for saving:", filename
  1002. return
  1003. try:
  1004. json.dump(d, f, default=to_dict)
  1005. except:
  1006. print "ERROR: File open but failed to write:", filename
  1007. f.close()
  1008. return
  1009. f.close()
  1010. def open_project(self, filename):
  1011. """
  1012. Loads a project from the specified file.
  1013. :param filename: Name of the file from which to load.
  1014. :type filename: str
  1015. :return: None
  1016. """
  1017. try:
  1018. f = open(filename, 'r')
  1019. except:
  1020. print "WARNING: Failed to open project file:", filename
  1021. return
  1022. try:
  1023. d = json.load(f, object_hook=dict2obj)
  1024. except:
  1025. print "WARNING: Failed to parse project file:", filename
  1026. f.close()
  1027. return
  1028. # Clear the current project
  1029. self.on_file_new(None)
  1030. # Project options
  1031. self.options.update(d['options'])
  1032. self.project_filename = filename
  1033. self.units_label.set_text(self.options["units"])
  1034. # Re create objects
  1035. for obj in d['objs']:
  1036. def obj_init(obj_inst, app_inst):
  1037. obj_inst.from_dict(obj)
  1038. self.new_object(obj['kind'], obj['options']['name'], obj_init)
  1039. self.info("Project loaded from: " + filename)
  1040. def populate_objects_combo(self, combo):
  1041. """
  1042. Populates a Gtk.Comboboxtext with the list of the object in the project.
  1043. :param combo: Name or instance of the comboboxtext.
  1044. :type combo: str or Gtk.ComboBoxText
  1045. :return: None
  1046. """
  1047. print "Populating combo!"
  1048. if type(combo) == str:
  1049. combo = self.builder.get_object(combo)
  1050. combo.remove_all()
  1051. for obj in self.stuff:
  1052. combo.append_text(obj)
  1053. def versionCheck(self, *args):
  1054. """
  1055. Checks for the latest version of the program. Alerts the
  1056. user if theirs is outdated. This method is meant to be run
  1057. in a saeparate thread.
  1058. :return: None
  1059. """
  1060. try:
  1061. f = urllib.urlopen("http://caram.cl/flatcam/VERSION") # TODO: Hardcoded.
  1062. except:
  1063. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  1064. return
  1065. try:
  1066. data = json.load(f)
  1067. except:
  1068. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  1069. f.close()
  1070. return
  1071. f.close()
  1072. if self.version >= data["version"]:
  1073. GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
  1074. return
  1075. label = Gtk.Label("There is a newer version of FlatCAM\n" +
  1076. "available for download:\n\n" +
  1077. data["name"] + "\n\n" + data["message"])
  1078. dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
  1079. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1080. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  1081. dialog.set_default_size(150, 100)
  1082. dialog.set_modal(True)
  1083. box = dialog.get_content_area()
  1084. box.set_border_width(10)
  1085. box.add(label)
  1086. def do_dialog():
  1087. dialog.show_all()
  1088. response = dialog.run()
  1089. dialog.destroy()
  1090. GLib.idle_add(lambda: do_dialog())
  1091. return
  1092. def setup_tooltips(self):
  1093. tooltips = {
  1094. "cb_gerber_plot": "Plot this object on the main window.",
  1095. "cb_gerber_mergepolys": "Show overlapping polygons as single.",
  1096. "cb_gerber_solid": "Paint inside polygons.",
  1097. "cb_gerber_multicolored": "Draw polygons with different polygons."
  1098. }
  1099. for widget in tooltips:
  1100. self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
  1101. def do_nothing(self, param):
  1102. return
  1103. ########################################
  1104. ## EVENT HANDLERS ##
  1105. ########################################
  1106. def on_offset_object(self, widget):
  1107. """
  1108. Offsets the object's geometry by the vector specified
  1109. in the form. Re-plots.
  1110. :param widget: Ignored
  1111. :return: None
  1112. """
  1113. obj = self.get_current()
  1114. obj.read_form()
  1115. assert isinstance(obj, FlatCAMObj)
  1116. try:
  1117. vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
  1118. except:
  1119. self.info("ERROR: Vector is not in (x, y) format.")
  1120. return
  1121. assert isinstance(obj, Geometry)
  1122. obj.offset(vect)
  1123. obj.plot()
  1124. return
  1125. def on_cb_plot_toggled(self, widget):
  1126. """
  1127. Callback for toggling the "Plot" checkbox. Re-plots.
  1128. :param widget: Ignored.
  1129. :return: None
  1130. """
  1131. self.get_current().read_form()
  1132. self.get_current().plot()
  1133. def on_about(self, widget):
  1134. """
  1135. Opens the 'About' dialog box.
  1136. :param widget: Ignored.
  1137. :return: None
  1138. """
  1139. about = self.builder.get_object("aboutdialog")
  1140. response = about.run()
  1141. #about.destroy()
  1142. about.hide()
  1143. def on_create_mirror(self, widget):
  1144. """
  1145. Creates a mirror image of a Gerber object to be used as a bottom
  1146. copper layer.
  1147. :param widget: Ignored.
  1148. :return: None
  1149. """
  1150. # Layer to mirror
  1151. gerb_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
  1152. gerb = self.stuff[gerb_name]
  1153. # For now, lets limit to Gerbers.
  1154. assert isinstance(gerb, FlatCAMGerber)
  1155. # Mirror axis "X" or "Y
  1156. axis = self.get_radio_value({"rb_mirror_x": "X",
  1157. "rb_mirror_y": "Y"})
  1158. mode = self.get_radio_value({"rb_mirror_box": "box",
  1159. "rb_mirror_point": "point"})
  1160. if mode == "point": # A single point defines the mirror axis
  1161. # TODO: Error handling
  1162. px, py = eval(self.point_entry.get_text())
  1163. else: # The axis is the line dividing the box in the middle
  1164. name = self.box_combo.get_active_text()
  1165. bb_obj = self.stuff[name]
  1166. xmin, ymin, xmax, ymax = bb_obj.bounds()
  1167. px = 0.5*(xmin+xmax)
  1168. py = 0.5*(ymin+ymax)
  1169. # Do the mirroring
  1170. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1171. mirrored = affinity.scale(gerb.solid_geometry, xscale, yscale, origin=(px, py))
  1172. def obj_init(obj_inst, app_inst):
  1173. obj_inst.solid_geometry = mirrored
  1174. self.new_object("gerber", gerb.options["name"] + "_mirror", obj_init)
  1175. def on_create_aligndrill(self, widget):
  1176. """
  1177. Creates alignment holes Excellon object. Creates mirror duplicates
  1178. of the specified holes around the specified axis.
  1179. :param widget: Ignored.
  1180. :return: None
  1181. """
  1182. # Mirror axis. Same as in on_create_mirror.
  1183. axis = self.get_radio_value({"rb_mirror_x": "X",
  1184. "rb_mirror_y": "Y"})
  1185. # TODO: Error handling
  1186. mode = self.get_radio_value({"rb_mirror_box": "box",
  1187. "rb_mirror_point": "point"})
  1188. if mode == "point":
  1189. px, py = eval(self.point_entry.get_text())
  1190. else:
  1191. name = self.box_combo.get_active_text()
  1192. bb_obj = self.stuff[name]
  1193. xmin, ymin, xmax, ymax = bb_obj.bounds()
  1194. px = 0.5*(xmin+xmax)
  1195. py = 0.5*(ymin+ymax)
  1196. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1197. # Tools
  1198. tools = {"1": self.get_eval("entry_dblsided_alignholediam")}
  1199. # Parse hole list
  1200. # TODO: Better parsing
  1201. holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
  1202. holes = eval("[" + holes + "]")
  1203. drills = []
  1204. for hole in holes:
  1205. point = Point(hole)
  1206. point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
  1207. drills.append({"point": point, "tool": "1"})
  1208. drills.append({"point": point_mirror, "tool": "1"})
  1209. def obj_init(obj_inst, app_inst):
  1210. obj_inst.tools = tools
  1211. obj_inst.drills = drills
  1212. obj_inst.create_geometry()
  1213. self.new_object("excellon", "Alignment Drills", obj_init)
  1214. def on_toggle_pointbox(self, widget):
  1215. """
  1216. Callback for radio selection change between point and box in the
  1217. Double-sided PCB tool. Updates the UI accordingly.
  1218. :param widget: Ignored.
  1219. :return: None
  1220. """
  1221. # Where the entry or combo go
  1222. box = self.builder.get_object("box_pointbox")
  1223. # Clear contents
  1224. children = box.get_children()
  1225. for child in children:
  1226. box.remove(child)
  1227. choice = self.get_radio_value({"rb_mirror_point": "point",
  1228. "rb_mirror_box": "box"})
  1229. if choice == "point":
  1230. self.point_entry = Gtk.Entry()
  1231. self.builder.get_object("box_pointbox").pack_start(self.point_entry,
  1232. False, False, 1)
  1233. self.point_entry.show()
  1234. else:
  1235. self.box_combo = Gtk.ComboBoxText()
  1236. self.builder.get_object("box_pointbox").pack_start(self.box_combo,
  1237. False, False, 1)
  1238. self.populate_objects_combo(self.box_combo)
  1239. self.box_combo.show()
  1240. def on_tools_doublesided(self, param):
  1241. """
  1242. Callback for menu item Tools->Double Sided PCB Tool. Launches the
  1243. tool placing its UI in the "Tool" tab in the notebook.
  1244. :param param: Ignored.
  1245. :return: None
  1246. """
  1247. # Were are we drawing the UI
  1248. box_tool = self.builder.get_object("box_tool")
  1249. # Remove anything else in the box
  1250. box_children = box_tool.get_children()
  1251. for child in box_children:
  1252. box_tool.remove(child)
  1253. # Get the UI
  1254. osw = self.builder.get_object("offscreenwindow_dblsided")
  1255. sw = self.builder.get_object("sw_dblsided")
  1256. osw.remove(sw)
  1257. vp = self.builder.get_object("vp_dblsided")
  1258. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  1259. # Put in the UI
  1260. box_tool.pack_start(sw, True, True, 0)
  1261. # INITIALIZATION
  1262. # Populate combo box
  1263. self.populate_objects_combo("comboboxtext_bottomlayer")
  1264. # Point entry
  1265. self.point_entry = Gtk.Entry()
  1266. box = self.builder.get_object("box_pointbox")
  1267. for child in box.get_children():
  1268. box.remove(child)
  1269. box.pack_start(self.point_entry, False, False, 1)
  1270. # Show the "Tool" tab
  1271. self.notebook.set_current_page(3)
  1272. sw.show_all()
  1273. def on_toggle_units(self, widget):
  1274. """
  1275. Callback for the Units radio-button change in the Options tab.
  1276. Changes the application's default units or the current project's units.
  1277. If changing the project's units, the change propagates to all of
  1278. the objects in the project.
  1279. :param widget: Ignored.
  1280. :return: None
  1281. """
  1282. if self.toggle_units_ignore:
  1283. return
  1284. combo_sel = self.combo_options.get_active()
  1285. options_set = [self.options, self.defaults][combo_sel]
  1286. # Options to scale
  1287. dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
  1288. 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
  1289. 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
  1290. 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
  1291. 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
  1292. 'geometry_paintmargin']
  1293. def scale_options(factor):
  1294. for dim in dimensions:
  1295. options_set[dim] *= factor
  1296. # The scaling factor depending on choice of units.
  1297. factor = 1/25.4
  1298. if self.builder.get_object('rb_mm').get_active():
  1299. factor = 25.4
  1300. # App units. Convert without warning.
  1301. if combo_sel == 1:
  1302. self.read_form()
  1303. scale_options(factor)
  1304. self.options2form()
  1305. return
  1306. # Changing project units. Warn user.
  1307. label = Gtk.Label("Changing the units of the project causes all geometrical \n" + \
  1308. "properties of all objects to be scaled accordingly. Continue?")
  1309. dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
  1310. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1311. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  1312. dialog.set_default_size(150, 100)
  1313. dialog.set_modal(True)
  1314. box = dialog.get_content_area()
  1315. box.set_border_width(10)
  1316. box.add(label)
  1317. dialog.show_all()
  1318. response = dialog.run()
  1319. dialog.destroy()
  1320. if response == Gtk.ResponseType.OK:
  1321. #print "Converting units..."
  1322. #print "Converting options..."
  1323. self.read_form()
  1324. scale_options(factor)
  1325. self.options2form()
  1326. for obj in self.stuff:
  1327. units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
  1328. #print "Converting ", obj, " to ", units
  1329. self.stuff[obj].convert_units(units)
  1330. current = self.get_current()
  1331. if current is not None:
  1332. current.to_form()
  1333. self.plot_all()
  1334. else:
  1335. # Undo toggling
  1336. self.toggle_units_ignore = True
  1337. if self.builder.get_object('rb_mm').get_active():
  1338. self.builder.get_object('rb_inch').set_active(True)
  1339. else:
  1340. self.builder.get_object('rb_mm').set_active(True)
  1341. self.toggle_units_ignore = False
  1342. self.read_form()
  1343. self.info("Converted units to %s" % self.options["units"])
  1344. self.units_label.set_text("[" + self.options["units"] + "]")
  1345. def on_file_openproject(self, param):
  1346. """
  1347. Callback for menu item File->Open Project. Opens a file chooser and calls
  1348. ``self.open_project()`` after successful selection of a filename.
  1349. :param param: Ignored.
  1350. :return: None
  1351. """
  1352. def on_success(app_obj, filename):
  1353. app_obj.open_project(filename)
  1354. self.file_chooser_action(on_success)
  1355. def on_file_saveproject(self, param):
  1356. """
  1357. Callback for menu item File->Save Project. Saves the project to
  1358. ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
  1359. if set to None. The project is saved by calling ``self.save_project()``.
  1360. :param param: Ignored.
  1361. :return: None
  1362. """
  1363. if self.project_filename is None:
  1364. self.on_file_saveprojectas(None)
  1365. else:
  1366. self.save_project(self.project_filename)
  1367. self.info("Project saved to: " + self.project_filename)
  1368. def on_file_saveprojectas(self, param):
  1369. """
  1370. Callback for menu item File->Save Project As... Opens a file
  1371. chooser and saves the project to the given file via
  1372. ``self.save_project()``.
  1373. :param param: Ignored.
  1374. :return: None
  1375. """
  1376. def on_success(app_obj, filename):
  1377. assert isinstance(app_obj, App)
  1378. app_obj.save_project(filename)
  1379. self.project_filename = filename
  1380. app_obj.info("Project saved to: " + filename)
  1381. self.file_chooser_save_action(on_success)
  1382. def on_file_saveprojectcopy(self, param):
  1383. """
  1384. Callback for menu item File->Save Project Copy... Opens a file
  1385. chooser and saves the project to the given file via
  1386. ``self.save_project``. It does not update ``self.project_filename`` so
  1387. subsequent save requests are done on the previous known filename.
  1388. :param param: Ignore.
  1389. :return: None
  1390. """
  1391. def on_success(app_obj, filename):
  1392. assert isinstance(app_obj, App)
  1393. app_obj.save_project(filename)
  1394. app_obj.info("Project copy saved to: " + filename)
  1395. self.file_chooser_save_action(on_success)
  1396. def on_options_app2project(self, param):
  1397. """
  1398. Callback for Options->Transfer Options->App=>Project. Copies options
  1399. from application defaults to project defaults.
  1400. :param param: Ignored.
  1401. :return: None
  1402. """
  1403. self.options.update(self.defaults)
  1404. self.options2form() # Update UI
  1405. def on_options_project2app(self, param):
  1406. """
  1407. Callback for Options->Transfer Options->Project=>App. Copies options
  1408. from project defaults to application defaults.
  1409. :param param: Ignored.
  1410. :return: None
  1411. """
  1412. self.defaults.update(self.options)
  1413. self.options2form() # Update UI
  1414. def on_options_project2object(self, param):
  1415. """
  1416. Callback for Options->Transfer Options->Project=>Object. Copies options
  1417. from project defaults to the currently selected object.
  1418. :param param: Ignored.
  1419. :return: None
  1420. """
  1421. obj = self.get_current()
  1422. if obj is None:
  1423. self.info("WARNING: No object selected.")
  1424. return
  1425. for option in self.options:
  1426. if option.find(obj.kind + "_") == 0:
  1427. oname = option[len(obj.kind)+1:]
  1428. obj.options[oname] = self.options[option]
  1429. obj.to_form() # Update UI
  1430. def on_options_object2project(self, param):
  1431. """
  1432. Callback for Options->Transfer Options->Object=>Project. Copies options
  1433. from the currently selected object to project defaults.
  1434. :param param: Ignored.
  1435. :return: None
  1436. """
  1437. obj = self.get_current()
  1438. if obj is None:
  1439. self.info("WARNING: No object selected.")
  1440. return
  1441. obj.read_form()
  1442. for option in obj.options:
  1443. if option in ['name']: # TODO: Handle this better...
  1444. continue
  1445. self.options[obj.kind + "_" + option] = obj.options[option]
  1446. self.options2form() # Update UI
  1447. def on_options_object2app(self, param):
  1448. """
  1449. Callback for Options->Transfer Options->Object=>App. Copies options
  1450. from the currently selected object to application defaults.
  1451. :param param: Ignored.
  1452. :return: None
  1453. """
  1454. obj = self.get_current()
  1455. if obj is None:
  1456. self.info("WARNING: No object selected.")
  1457. return
  1458. obj.read_form()
  1459. for option in obj.options:
  1460. if option in ['name']: # TODO: Handle this better...
  1461. continue
  1462. self.defaults[obj.kind + "_" + option] = obj.options[option]
  1463. self.options2form() # Update UI
  1464. def on_options_app2object(self, param):
  1465. """
  1466. Callback for Options->Transfer Options->App=>Object. Copies options
  1467. from application defaults to the currently selected object.
  1468. :param param: Ignored.
  1469. :return: None
  1470. """
  1471. obj = self.get_current()
  1472. if obj is None:
  1473. self.info("WARNING: No object selected.")
  1474. return
  1475. for option in self.defaults:
  1476. if option.find(obj.kind + "_") == 0:
  1477. oname = option[len(obj.kind)+1:]
  1478. obj.options[oname] = self.defaults[option]
  1479. obj.to_form() # Update UI
  1480. def on_file_savedefaults(self, param):
  1481. """
  1482. Callback for menu item File->Save Defaults. Saves application default options
  1483. ``self.defaults`` to defaults.json.
  1484. :param param: Ignored.
  1485. :return: None
  1486. """
  1487. # Read options from file
  1488. try:
  1489. f = open("defaults.json")
  1490. options = f.read()
  1491. f.close()
  1492. except:
  1493. self.info("ERROR: Could not load defaults file.")
  1494. return
  1495. try:
  1496. defaults = json.loads(options)
  1497. except:
  1498. e = sys.exc_info()[0]
  1499. print e
  1500. self.info("ERROR: Failed to parse defaults file.")
  1501. return
  1502. # Update options
  1503. assert isinstance(defaults, dict)
  1504. defaults.update(self.defaults)
  1505. # Save update options
  1506. try:
  1507. f = open("defaults.json", "w")
  1508. json.dump(defaults, f)
  1509. f.close()
  1510. except:
  1511. self.info("ERROR: Failed to write defaults to file.")
  1512. return
  1513. self.info("Defaults saved.")
  1514. def on_options_combo_change(self, widget):
  1515. """
  1516. Called when the combo box to choose between application defaults and
  1517. project option changes value. The corresponding variables are
  1518. copied to the UI.
  1519. :param widget: The widget from which this was called. Ignore.
  1520. :return: None
  1521. """
  1522. #combo_sel = self.combo_options.get_active()
  1523. #print "Options --> ", combo_sel
  1524. self.options2form()
  1525. def on_options_update(self, widget):
  1526. """
  1527. Called whenever a value in the options/defaults form changes.
  1528. All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
  1529. which may be necessary when updating the UI from code and not by the user.
  1530. :param widget: The widget from which this was called. Ignore.
  1531. :return: None
  1532. """
  1533. if self.options_update_ignore:
  1534. return
  1535. self.read_form()
  1536. def on_scale_object(self, widget):
  1537. """
  1538. Callback for request to change an objects geometry scale. The object
  1539. is re-scaled and replotted.
  1540. :param widget: Ignored.
  1541. :return: None
  1542. """
  1543. obj = self.get_current()
  1544. factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
  1545. obj.scale(factor)
  1546. obj.to_form()
  1547. self.on_update_plot(None)
  1548. def on_canvas_configure(self, widget, event):
  1549. """
  1550. Called whenever the canvas changes size. The axes are updated such
  1551. as to use the whole canvas.
  1552. :param widget: Ignored.
  1553. :param event: Ignored.
  1554. :return: None
  1555. """
  1556. self.plotcanvas.auto_adjust_axes()
  1557. def on_row_activated(self, widget, path, col):
  1558. """
  1559. Callback for selection activation (Enter or double-click) on the Project list.
  1560. Switches the notebook page to the object properties form. Calls
  1561. ``self.notebook.set_current_page(1)``.
  1562. :param widget: Ignored.
  1563. :param path: Ignored.
  1564. :param col: Ignored.
  1565. :return: None
  1566. """
  1567. self.notebook.set_current_page(1)
  1568. def on_generate_gerber_bounding_box(self, widget):
  1569. """
  1570. Callback for request from the Gerber form to generate a bounding box for the
  1571. geometry in the object. Creates a FlatCAMGeometry with the bounding box.
  1572. The box can have rounded corners if specified in the form.
  1573. :param widget: Ignored.
  1574. :return: None
  1575. """
  1576. # TODO: Use Gerber.get_bounding_box(...)
  1577. gerber = self.get_current()
  1578. gerber.read_form()
  1579. name = gerber.options["name"] + "_bbox"
  1580. def geo_init(geo_obj, app_obj):
  1581. assert isinstance(geo_obj, FlatCAMGeometry)
  1582. # Bounding box with rounded corners
  1583. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
  1584. if not gerber.options["bboxrounded"]: # Remove rounded corners
  1585. bounding_box = bounding_box.envelope
  1586. geo_obj.solid_geometry = bounding_box
  1587. self.new_object("geometry", name, geo_init)
  1588. def on_update_plot(self, widget):
  1589. """
  1590. Callback for button on form for all kinds of objects.
  1591. Re-plots the current object only.
  1592. :param widget: The widget from which this was called.
  1593. :return: None
  1594. """
  1595. obj = self.get_current()
  1596. obj.read_form()
  1597. self.set_progress_bar(0.5, "Plotting...")
  1598. def thread_func(app_obj):
  1599. assert isinstance(app_obj, App)
  1600. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
  1601. #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
  1602. obj.plot()
  1603. GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
  1604. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  1605. t = threading.Thread(target=thread_func, args=(self,))
  1606. t.daemon = True
  1607. t.start()
  1608. def on_generate_excellon_cncjob(self, widget):
  1609. """
  1610. Callback for button active/click on Excellon form to
  1611. create a CNC Job for the Excellon file.
  1612. :param widget: Ignored
  1613. :return: None
  1614. """
  1615. excellon = self.get_current()
  1616. excellon.read_form()
  1617. job_name = excellon.options["name"] + "_cnc"
  1618. # Object initialization function for app.new_object()
  1619. def job_init(job_obj, app_obj):
  1620. # excellon_ = self.get_current()
  1621. # assert isinstance(excellon_, FlatCAMExcellon)
  1622. assert isinstance(job_obj, FlatCAMCNCjob)
  1623. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1624. job_obj.z_cut = excellon.options["drillz"]
  1625. job_obj.z_move = excellon.options["travelz"]
  1626. job_obj.feedrate = excellon.options["feedrate"]
  1627. # There could be more than one drill size...
  1628. # job_obj.tooldia = # TODO: duplicate variable!
  1629. # job_obj.options["tooldia"] =
  1630. job_obj.generate_from_excellon_by_tool(excellon, excellon.options["toolselection"])
  1631. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1632. job_obj.gcode_parse()
  1633. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1634. job_obj.create_geometry()
  1635. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1636. # To be run in separate thread
  1637. def job_thread(app_obj):
  1638. app_obj.new_object("cncjob", job_name, job_init)
  1639. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1640. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1641. # Start the thread
  1642. t = threading.Thread(target=job_thread, args=(self,))
  1643. t.daemon = True
  1644. t.start()
  1645. def on_excellon_tool_choose(self, widget):
  1646. """
  1647. Callback for button on Excellon form to open up a window for
  1648. selecting tools.
  1649. :param widget: The widget from which this was called.
  1650. :return: None
  1651. """
  1652. excellon = self.get_current()
  1653. assert isinstance(excellon, FlatCAMExcellon)
  1654. excellon.show_tool_chooser()
  1655. def on_entry_eval_activate(self, widget):
  1656. """
  1657. Called when an entry is activated (eg. by hitting enter) if
  1658. set to do so. Its text is eval()'d and set to the returned value.
  1659. The current object is updated.
  1660. :param widget:
  1661. :return:
  1662. """
  1663. self.on_eval_update(widget)
  1664. obj = self.get_current()
  1665. assert isinstance(obj, FlatCAMObj)
  1666. obj.read_form()
  1667. def on_gerber_generate_noncopper(self, widget):
  1668. """
  1669. Callback for button on Gerber form to create a geometry object
  1670. with polygons covering the area without copper or negative of the
  1671. Gerber.
  1672. :param widget: The widget from which this was called.
  1673. :return: None
  1674. """
  1675. gerb = self.get_current()
  1676. gerb.read_form()
  1677. name = gerb.options["name"] + "_noncopper"
  1678. def geo_init(geo_obj, app_obj):
  1679. assert isinstance(geo_obj, FlatCAMGeometry)
  1680. bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
  1681. non_copper = bounding_box.difference(gerb.solid_geometry)
  1682. geo_obj.solid_geometry = non_copper
  1683. # TODO: Check for None
  1684. self.new_object("geometry", name, geo_init)
  1685. def on_gerber_generate_cutout(self, widget):
  1686. """
  1687. Callback for button on Gerber form to create geometry with lines
  1688. for cutting off the board.
  1689. :param widget: The widget from which this was called.
  1690. :return: None
  1691. """
  1692. gerb = self.get_current()
  1693. gerb.read_form()
  1694. name = gerb.options["name"] + "_cutout"
  1695. def geo_init(geo_obj, app_obj):
  1696. margin = gerb.options["cutoutmargin"]
  1697. gap_size = gerb.options["cutoutgapsize"]
  1698. minx, miny, maxx, maxy = gerb.bounds()
  1699. minx -= margin
  1700. maxx += margin
  1701. miny -= margin
  1702. maxy += margin
  1703. midx = 0.5 * (minx + maxx)
  1704. midy = 0.5 * (miny + maxy)
  1705. hgap = 0.5 * gap_size
  1706. pts = [[midx - hgap, maxy],
  1707. [minx, maxy],
  1708. [minx, midy + hgap],
  1709. [minx, midy - hgap],
  1710. [minx, miny],
  1711. [midx - hgap, miny],
  1712. [midx + hgap, miny],
  1713. [maxx, miny],
  1714. [maxx, midy - hgap],
  1715. [maxx, midy + hgap],
  1716. [maxx, maxy],
  1717. [midx + hgap, maxy]]
  1718. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  1719. [pts[6], pts[7], pts[10], pts[11]]],
  1720. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  1721. [pts[3], pts[4], pts[7], pts[8]]],
  1722. "4": [[pts[0], pts[1], pts[2]],
  1723. [pts[3], pts[4], pts[5]],
  1724. [pts[6], pts[7], pts[8]],
  1725. [pts[9], pts[10], pts[11]]]}
  1726. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  1727. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  1728. # TODO: Check for None
  1729. self.new_object("geometry", name, geo_init)
  1730. def on_eval_update(self, widget):
  1731. """
  1732. Modifies the content of a Gtk.Entry by running
  1733. eval() on its contents and puting it back as a
  1734. string.
  1735. :param widget: The widget from which this was called.
  1736. :return: None
  1737. """
  1738. # TODO: error handling here
  1739. widget.set_text(str(eval(widget.get_text())))
  1740. def on_generate_isolation(self, widget):
  1741. """
  1742. Callback for button on Gerber form to create isolation routing geometry.
  1743. :param widget: The widget from which this was called.
  1744. :return: None
  1745. """
  1746. gerb = self.get_current()
  1747. gerb.read_form()
  1748. dia = gerb.options["isotooldia"]
  1749. passes = int(gerb.options["isopasses"])
  1750. overlap = gerb.options["isooverlap"] * dia
  1751. for i in range(passes):
  1752. offset = (2*i + 1)/2.0 * dia - i*overlap
  1753. iso_name = gerb.options["name"] + "_iso%d" % (i+1)
  1754. # TODO: This is ugly. Create way to pass data into init function.
  1755. def iso_init(geo_obj, app_obj):
  1756. # Propagate options
  1757. geo_obj.options["cnctooldia"] = gerb.options["isotooldia"]
  1758. geo_obj.solid_geometry = gerb.isolation_geometry(offset)
  1759. app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
  1760. # TODO: Do something if this is None. Offer changing name?
  1761. self.new_object("geometry", iso_name, iso_init)
  1762. def on_generate_cncjob(self, widget):
  1763. """
  1764. Callback for button on geometry form to generate CNC job.
  1765. :param widget: The widget from which this was called.
  1766. :return: None
  1767. """
  1768. source_geo = self.get_current()
  1769. source_geo.read_form()
  1770. job_name = source_geo.options["name"] + "_cnc"
  1771. # Object initialization function for app.new_object()
  1772. # RUNNING ON SEPARATE THREAD!
  1773. def job_init(job_obj, app_obj):
  1774. assert isinstance(job_obj, FlatCAMCNCjob)
  1775. # Propagate options
  1776. job_obj.options["tooldia"] = source_geo.options["cnctooldia"]
  1777. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1778. job_obj.z_cut = source_geo.options["cutz"]
  1779. job_obj.z_move = source_geo.options["travelz"]
  1780. job_obj.feedrate = source_geo.options["feedrate"]
  1781. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  1782. # TODO: The tolerance should not be hard coded. Just for testing.
  1783. job_obj.generate_from_geometry(source_geo, tolerance=0.0005)
  1784. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1785. job_obj.gcode_parse()
  1786. # TODO: job_obj.create_geometry creates stuff that is not used.
  1787. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1788. #job_obj.create_geometry()
  1789. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1790. # To be run in separate thread
  1791. def job_thread(app_obj):
  1792. app_obj.new_object("cncjob", job_name, job_init)
  1793. GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
  1794. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1795. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1796. # Start the thread
  1797. t = threading.Thread(target=job_thread, args=(self,))
  1798. t.daemon = True
  1799. t.start()
  1800. def on_generate_paintarea(self, widget):
  1801. """
  1802. Callback for button on geometry form.
  1803. Subscribes to the "Click on plot" event and continues
  1804. after the click. Finds the polygon containing
  1805. the clicked point and runs clear_poly() on it, resulting
  1806. in a new FlatCAMGeometry object.
  1807. :param widget: The widget from which this was called.
  1808. :return: None
  1809. """
  1810. self.info("Click inside the desired polygon.")
  1811. geo = self.get_current()
  1812. geo.read_form()
  1813. assert isinstance(geo, FlatCAMGeometry)
  1814. tooldia = geo.options["painttooldia"]
  1815. overlap = geo.options["paintoverlap"]
  1816. # To be called after clicking on the plot.
  1817. def doit(event):
  1818. self.plot_click_subscribers.pop("generate_paintarea")
  1819. self.info("")
  1820. point = [event.xdata, event.ydata]
  1821. poly = find_polygon(geo.solid_geometry, point)
  1822. # Initializes the new geometry object
  1823. def gen_paintarea(geo_obj, app_obj):
  1824. assert isinstance(geo_obj, FlatCAMGeometry)
  1825. assert isinstance(app_obj, App)
  1826. cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
  1827. geo_obj.solid_geometry = cp
  1828. geo_obj.options["cnctooldia"] = tooldia
  1829. name = self.selected_item_name + "_paint"
  1830. self.new_object("geometry", name, gen_paintarea)
  1831. self.plot_click_subscribers["generate_paintarea"] = doit
  1832. def on_cncjob_exportgcode(self, widget):
  1833. """
  1834. Called from button on CNCjob form to save the G-Code from the object.
  1835. :param widget: The widget from which this was called.
  1836. :return: None
  1837. """
  1838. def on_success(app_obj, filename):
  1839. cncjob = app_obj.get_current()
  1840. f = open(filename, 'w')
  1841. f.write(cncjob.gcode)
  1842. f.close()
  1843. app_obj.info("Saved to: " + filename)
  1844. self.file_chooser_save_action(on_success)
  1845. def on_delete(self, widget):
  1846. """
  1847. Delete the currently selected FlatCAMObj.
  1848. :param widget: The widget from which this was called.
  1849. :return: None
  1850. """
  1851. # Keep this for later
  1852. name = copy.copy(self.selected_item_name)
  1853. # Remove plot
  1854. self.plotcanvas.figure.delaxes(self.get_current().axes)
  1855. self.plotcanvas.auto_adjust_axes()
  1856. # Remove from dictionary
  1857. self.stuff.pop(self.selected_item_name)
  1858. # Update UI
  1859. self.build_list() # Update the items list
  1860. self.info("Object deleted: %s" % name)
  1861. def on_toolbar_replot(self, widget):
  1862. """
  1863. Callback for toolbar button. Re-plots all objects.
  1864. :param widget: The widget from which this was called.
  1865. :return: None
  1866. """
  1867. self.get_current().read_form()
  1868. self.plot_all()
  1869. def on_clear_plots(self, widget):
  1870. """
  1871. Callback for toolbar button. Clears all plots.
  1872. :param widget: The widget from which this was called.
  1873. :return: None
  1874. """
  1875. self.plotcanvas.clear()
  1876. def on_activate_name(self, entry):
  1877. """
  1878. Hitting 'Enter' after changing the name of an item
  1879. updates the item dictionary and re-builds the item list.
  1880. :param entry: The widget from which this was called.
  1881. :return: None
  1882. """
  1883. # Disconnect event listener
  1884. self.tree.get_selection().disconnect(self.signal_id)
  1885. new_name = entry.get_text() # Get from form
  1886. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  1887. self.stuff[new_name].options["name"] = new_name # update object
  1888. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  1889. self.selected_item_name = new_name # Update selection name
  1890. self.build_list() # Update the items list
  1891. # Reconnect event listener
  1892. self.signal_id = self.tree.get_selection().connect(
  1893. "changed", self.on_tree_selection_changed)
  1894. def on_tree_selection_changed(self, selection):
  1895. """
  1896. Callback for selection change in the project list. This changes
  1897. the currently selected FlatCAMObj.
  1898. :param selection: Selection associated to the project tree or list
  1899. :type selection: Gtk.TreeSelection
  1900. :return: None
  1901. """
  1902. print "DEBUG: on_tree_selection_change(): ",
  1903. model, treeiter = selection.get_selected()
  1904. if treeiter is not None:
  1905. # Save data for previous selection
  1906. obj = self.get_current()
  1907. if obj is not None:
  1908. obj.read_form()
  1909. print "DEBUG: You selected", model[treeiter][0]
  1910. self.selected_item_name = model[treeiter][0]
  1911. obj_new = self.get_current()
  1912. if obj_new is not None:
  1913. GLib.idle_add(lambda: obj_new.build_ui())
  1914. else:
  1915. print "DEBUG: Nothing selected"
  1916. self.selected_item_name = None
  1917. self.setup_component_editor()
  1918. def on_file_new(self, param):
  1919. """
  1920. Callback for menu item File->New. Returns the application to its
  1921. startup state.
  1922. :param param: Whatever is passed by the event. Ignore.
  1923. :return: None
  1924. """
  1925. # Remove everythong from memory
  1926. # Clear plot
  1927. self.plotcanvas.clear()
  1928. # Clear object editor
  1929. #self.setup_component_editor()
  1930. # Clear data
  1931. self.stuff = {}
  1932. # Clear list
  1933. #self.tree_select.unselect_all()
  1934. self.build_list()
  1935. # Clear project filename
  1936. self.project_filename = None
  1937. # Re-fresh project options
  1938. self.on_options_app2project(None)
  1939. def on_filequit(self, param):
  1940. """
  1941. Callback for menu item File->Quit. Closes the application.
  1942. :param param: Whatever is passed by the event. Ignore.
  1943. :return: None
  1944. """
  1945. self.window.destroy()
  1946. Gtk.main_quit()
  1947. def on_closewindow(self, param):
  1948. """
  1949. Callback for closing the main window.
  1950. :param param: Whatever is passed by the event. Ignore.
  1951. :return: None
  1952. """
  1953. self.window.destroy()
  1954. Gtk.main_quit()
  1955. def file_chooser_action(self, on_success):
  1956. """
  1957. Opens the file chooser and runs on_success on a separate thread
  1958. upon completion of valid file choice.
  1959. :param on_success: A function to run upon completion of a valid file
  1960. selection. Takes 2 parameters: The app instance and the filename.
  1961. Note that it is run on a separate thread, therefore it must take the
  1962. appropriate precautions when accessing shared resources.
  1963. :type on_success: func
  1964. :return: None
  1965. """
  1966. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1967. Gtk.FileChooserAction.OPEN,
  1968. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1969. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1970. response = dialog.run()
  1971. if response == Gtk.ResponseType.OK:
  1972. filename = dialog.get_filename()
  1973. dialog.destroy()
  1974. t = threading.Thread(target=on_success, args=(self, filename))
  1975. t.daemon = True
  1976. t.start()
  1977. #on_success(self, filename)
  1978. elif response == Gtk.ResponseType.CANCEL:
  1979. self.info("Open cancelled.") # print("Cancel clicked")
  1980. dialog.destroy()
  1981. def file_chooser_save_action(self, on_success):
  1982. """
  1983. Opens the file chooser and runs on_success upon completion of valid file choice.
  1984. :param on_success: A function to run upon selection of a filename. Takes 2
  1985. parameters: The instance of the application (App) and the chosen filename. This
  1986. gets run immediately in the same thread.
  1987. :return: None
  1988. """
  1989. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1990. Gtk.FileChooserAction.SAVE,
  1991. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1992. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1993. dialog.set_current_name("Untitled")
  1994. response = dialog.run()
  1995. if response == Gtk.ResponseType.OK:
  1996. filename = dialog.get_filename()
  1997. dialog.destroy()
  1998. on_success(self, filename)
  1999. elif response == Gtk.ResponseType.CANCEL:
  2000. self.info("Save cancelled.") # print("Cancel clicked")
  2001. dialog.destroy()
  2002. def on_fileopengerber(self, param):
  2003. """
  2004. Callback for menu item File->Open Gerber. Defines a function that is then passed
  2005. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
  2006. and updates the progress bar throughout the process.
  2007. :param param: Ignore
  2008. :return: None
  2009. """
  2010. # IMPORTANT: on_success will run on a separate thread. Use
  2011. # GLib.idle_add(function, **kwargs) to launch actions that will
  2012. # updata the GUI.
  2013. def on_success(app_obj, filename):
  2014. assert isinstance(app_obj, App)
  2015. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  2016. def obj_init(gerber_obj, app_obj):
  2017. assert isinstance(gerber_obj, FlatCAMGerber)
  2018. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  2019. gerber_obj.parse_file(filename)
  2020. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
  2021. gerber_obj.create_geometry()
  2022. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  2023. name = filename.split('/')[-1].split('\\')[-1]
  2024. app_obj.new_object("gerber", name, obj_init)
  2025. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  2026. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  2027. # on_success gets run on a separate thread
  2028. self.file_chooser_action(on_success)
  2029. def on_fileopenexcellon(self, param):
  2030. """
  2031. Callback for menu item File->Open Excellon. Defines a function that is then passed
  2032. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
  2033. and updates the progress bar throughout the process.
  2034. :param param: Ignore
  2035. :return: None
  2036. """
  2037. # IMPORTANT: on_success will run on a separate thread. Use
  2038. # GLib.idle_add(function, **kwargs) to launch actions that will
  2039. # updata the GUI.
  2040. def on_success(app_obj, filename):
  2041. assert isinstance(app_obj, App)
  2042. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  2043. def obj_init(excellon_obj, app_obj):
  2044. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  2045. excellon_obj.parse_file(filename)
  2046. excellon_obj.create_geometry()
  2047. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  2048. name = filename.split('/')[-1].split('\\')[-1]
  2049. app_obj.new_object("excellon", name, obj_init)
  2050. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  2051. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  2052. # on_success gets run on a separate thread
  2053. self.file_chooser_action(on_success)
  2054. def on_fileopengcode(self, param):
  2055. """
  2056. Callback for menu item File->Open G-Code. Defines a function that is then passed
  2057. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
  2058. and updates the progress bar throughout the process.
  2059. :param param: Ignore
  2060. :return: None
  2061. """
  2062. # IMPORTANT: on_success will run on a separate thread. Use
  2063. # GLib.idle_add(function, **kwargs) to launch actions that will
  2064. # updata the GUI.
  2065. def on_success(app_obj, filename):
  2066. assert isinstance(app_obj, App)
  2067. def obj_init(job_obj, app_obj_):
  2068. """
  2069. :type app_obj_: App
  2070. """
  2071. assert isinstance(app_obj_, App)
  2072. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
  2073. f = open(filename)
  2074. gcode = f.read()
  2075. f.close()
  2076. job_obj.gcode = gcode
  2077. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
  2078. job_obj.gcode_parse()
  2079. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
  2080. job_obj.create_geometry()
  2081. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
  2082. name = filename.split('/')[-1].split('\\')[-1]
  2083. app_obj.new_object("cncjob", name, obj_init)
  2084. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  2085. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  2086. # on_success gets run on a separate thread
  2087. self.file_chooser_action(on_success)
  2088. def on_mouse_move_over_plot(self, event):
  2089. """
  2090. Callback for the mouse motion event over the plot. This event is generated
  2091. by the Matplotlib backend and has been registered in ``self.__init__()``.
  2092. For details, see: http://matplotlib.org/users/event_handling.html
  2093. :param event: Contains information about the event.
  2094. :return: None
  2095. """
  2096. try: # May fail in case mouse not within axes
  2097. self.position_label.set_label("X: %.4f Y: %.4f" % (
  2098. event.xdata, event.ydata))
  2099. self.mouse = [event.xdata, event.ydata]
  2100. for subscriber in self.plot_mousemove_subscribers:
  2101. self.plot_mousemove_subscribers[subscriber](event)
  2102. except:
  2103. self.position_label.set_label("")
  2104. self.mouse = None
  2105. def on_click_over_plot(self, event):
  2106. """
  2107. Callback for the mouse click event over the plot. This event is generated
  2108. by the Matplotlib backend and has been registered in ``self.__init__()``.
  2109. For details, see: http://matplotlib.org/users/event_handling.html
  2110. Default actions are:
  2111. * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
  2112. :param event: Contains information about the event, like which button
  2113. was clicked, the pixel coordinates and the axes coordinates.
  2114. :return: None
  2115. """
  2116. # For key presses
  2117. self.plotcanvas.canvas.grab_focus()
  2118. try:
  2119. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
  2120. event.button, event.x, event.y, event.xdata, event.ydata)
  2121. # TODO: This custom subscription mechanism is probably not necessary.
  2122. for subscriber in self.plot_click_subscribers:
  2123. self.plot_click_subscribers[subscriber](event)
  2124. self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
  2125. except Exception, e:
  2126. print "Outside plot!"
  2127. def on_zoom_in(self, event):
  2128. """
  2129. Callback for zoom-in request. This can be either from the corresponding
  2130. toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
  2131. :param event: Ignored.
  2132. :return: None
  2133. """
  2134. self.plotcanvas.zoom(1.5)
  2135. return
  2136. def on_zoom_out(self, event):
  2137. """
  2138. Callback for zoom-out request. This can be either from the corresponding
  2139. toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
  2140. :param event: Ignored.
  2141. :return: None
  2142. """
  2143. self.plotcanvas.zoom(1 / 1.5)
  2144. def on_zoom_fit(self, event):
  2145. """
  2146. Callback for zoom-out request. This can be either from the corresponding
  2147. toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
  2148. with axes limits from the geometry bounds of all objects.
  2149. :param event: Ignored.
  2150. :return: None
  2151. """
  2152. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  2153. width = xmax - xmin
  2154. height = ymax - ymin
  2155. xmin -= 0.05 * width
  2156. xmax += 0.05 * width
  2157. ymin -= 0.05 * height
  2158. ymax += 0.05 * height
  2159. self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
  2160. def on_key_over_plot(self, event):
  2161. """
  2162. Callback for the key pressed event when the canvas is focused. Keyboard
  2163. shortcuts are handled here. So far, these are the shortcuts:
  2164. ========== ============================================
  2165. Key Action
  2166. ========== ============================================
  2167. '1' Zoom-fit. Fits the axes limits to the data.
  2168. '2' Zoom-out.
  2169. '3' Zoom-in.
  2170. ========== ============================================
  2171. :param event: Ignored.
  2172. :return: None
  2173. """
  2174. #print 'you pressed', event.key, event.xdata, event.ydata
  2175. if event.key == '1': # 1
  2176. self.on_zoom_fit(None)
  2177. return
  2178. if event.key == '2': # 2
  2179. self.plotcanvas.zoom(1 / 1.5, self.mouse)
  2180. return
  2181. if event.key == '3': # 3
  2182. self.plotcanvas.zoom(1.5, self.mouse)
  2183. return
  2184. if event.key == 'm':
  2185. if self.measure.toggle_active():
  2186. self.info("Measuring tool ON")
  2187. else:
  2188. self.info("Measuring tool OFF")
  2189. return
  2190. class Measurement:
  2191. def __init__(self, container, axes, click_subscibers, move_subscribers, update=None):
  2192. self.update = update
  2193. self.container = container
  2194. self.frame = None
  2195. self.label = None
  2196. self.axes = axes
  2197. self.click_subscribers = click_subscibers
  2198. self.move_subscribers = move_subscribers
  2199. self.point1 = None
  2200. self.point2 = None
  2201. self.active = False
  2202. # self.at = None # AnchoredText object on plot
  2203. def toggle_active(self, *args):
  2204. if self.active: # Deactivate
  2205. self.active = False
  2206. self.move_subscribers.pop("meas")
  2207. self.click_subscribers.pop("meas")
  2208. # self.at.remove()
  2209. self.container.remove(self.frame)
  2210. if self.update is not None:
  2211. self.update()
  2212. return False
  2213. else: # Activate
  2214. print "DEBUG: Activating Measurement Tool..."
  2215. self.active = True
  2216. self.click_subscribers["meas"] = self.on_click
  2217. self.move_subscribers["meas"] = self.on_move
  2218. self.frame = Gtk.Frame()
  2219. self.frame.set_margin_right(5)
  2220. self.frame.set_margin_top(3)
  2221. align = Gtk.Alignment()
  2222. align.set(0, 0.5, 0, 0)
  2223. align.set_padding(4, 4, 4, 4)
  2224. self.label = Gtk.Label()
  2225. self.label.set_label("Measuring tool...")
  2226. align.add(self.label)
  2227. self.frame.add(align)
  2228. self.frame.set_size_request(200, 30)
  2229. self.frame.set_hexpand(True)
  2230. self.container.pack_end(self.frame, False, True, 1)
  2231. self.frame.show_all()
  2232. align.show_all()
  2233. self.label.show_all()
  2234. self.container.queue_draw()
  2235. return True
  2236. def on_move(self, event):
  2237. # try:
  2238. # self.at.remove()
  2239. # except:
  2240. # pass
  2241. if self.point1 is None:
  2242. # self.at = AnchoredText("Click on a reference point...")
  2243. self.label.set_label("Click on a reference point...")
  2244. else:
  2245. dx = event.xdata - self.point1[0]
  2246. dy = event.ydata - self.point1[1]
  2247. d = sqrt(dx**2 + dy**2)
  2248. # self.at = AnchoredText("D = %.4f\nD(x) = %.4f\nD(y) = %.4f" % (d, dx, dy),
  2249. # loc=2, prop={'size': 14}, frameon=False)
  2250. self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
  2251. # self.axes.add_artist(self.at)
  2252. if self.update is not None:
  2253. self.update()
  2254. def on_click(self, event):
  2255. if self.point1 is None:
  2256. self.point1 = (event.xdata, event.ydata)
  2257. return
  2258. else:
  2259. self.point2 = copy.copy(self.point1)
  2260. self.point1 = (event.xdata, event.ydata)
  2261. self.on_move(event)
  2262. class PlotCanvas:
  2263. """
  2264. Class handling the plotting area in the application.
  2265. """
  2266. def __init__(self, container):
  2267. """
  2268. The constructor configures the Matplotlib figure that
  2269. will contain all plots, creates the base axes and connects
  2270. events to the plotting area.
  2271. :param container: The parent container in which to draw plots.
  2272. :rtype: PlotCanvas
  2273. """
  2274. # Options
  2275. self.x_margin = 15 # pixels
  2276. self.y_margin = 25 # Pixels
  2277. # Parent container
  2278. self.container = container
  2279. # Plots go onto a single matplotlib.figure
  2280. self.figure = Figure(dpi=50) # TODO: dpi needed?
  2281. self.figure.patch.set_visible(False)
  2282. # These axes show the ticks and grid. No plotting done here.
  2283. # New axes must have a label, otherwise mpl returns an existing one.
  2284. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  2285. self.axes.set_aspect(1)
  2286. self.axes.grid(True)
  2287. # The canvas is the top level container (Gtk.DrawingArea)
  2288. self.canvas = FigureCanvas(self.figure)
  2289. self.canvas.set_hexpand(1)
  2290. self.canvas.set_vexpand(1)
  2291. self.canvas.set_can_focus(True) # For key press
  2292. # Attach to parent
  2293. self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  2294. # Events
  2295. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
  2296. self.canvas.connect('configure-event', self.auto_adjust_axes)
  2297. self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  2298. self.canvas.connect("scroll-event", self.on_scroll)
  2299. self.mouse = [0,0]
  2300. def mpl_connect(self, event_name, callback):
  2301. """
  2302. Attach an event handler to the canvas through the Matplotlib interface.
  2303. :param event_name: Name of the event
  2304. :type event_name: str
  2305. :param callback: Function to call
  2306. :type callback: func
  2307. :return: Nothing
  2308. """
  2309. self.canvas.mpl_connect(event_name, callback)
  2310. def connect(self, event_name, callback):
  2311. """
  2312. Attach an event handler to the canvas through the native GTK interface.
  2313. :param event_name: Name of the event
  2314. :type event_name: str
  2315. :param callback: Function to call
  2316. :type callback: function
  2317. :return: Nothing
  2318. """
  2319. self.canvas.connect(event_name, callback)
  2320. def clear(self):
  2321. """
  2322. Clears axes and figure.
  2323. :return: None
  2324. """
  2325. # Clear
  2326. self.axes.cla()
  2327. self.figure.clf()
  2328. # Re-build
  2329. self.figure.add_axes(self.axes)
  2330. self.axes.set_aspect(1)
  2331. self.axes.grid(True)
  2332. # Re-draw
  2333. self.canvas.queue_draw()
  2334. def adjust_axes(self, xmin, ymin, xmax, ymax):
  2335. """
  2336. Adjusts all axes while maintaining the use of the whole canvas
  2337. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  2338. request that will be modified to fit these restrictions.
  2339. :param xmin: Requested minimum value for the X axis.
  2340. :type xmin: float
  2341. :param ymin: Requested minimum value for the Y axis.
  2342. :type ymin: float
  2343. :param xmax: Requested maximum value for the X axis.
  2344. :type xmax: float
  2345. :param ymax: Requested maximum value for the Y axis.
  2346. :type ymax: float
  2347. :return: None
  2348. """
  2349. width = xmax - xmin
  2350. height = ymax - ymin
  2351. try:
  2352. r = width / height
  2353. except:
  2354. print "ERROR: Height is", height
  2355. return
  2356. canvas_w, canvas_h = self.canvas.get_width_height()
  2357. canvas_r = float(canvas_w) / canvas_h
  2358. x_ratio = float(self.x_margin) / canvas_w
  2359. y_ratio = float(self.y_margin) / canvas_h
  2360. if r > canvas_r:
  2361. ycenter = (ymin + ymax) / 2.0
  2362. newheight = height * r / canvas_r
  2363. ymin = ycenter - newheight / 2.0
  2364. ymax = ycenter + newheight / 2.0
  2365. else:
  2366. xcenter = (xmax + ymin) / 2.0
  2367. newwidth = width * canvas_r / r
  2368. xmin = xcenter - newwidth / 2.0
  2369. xmax = xcenter + newwidth / 2.0
  2370. # Adjust axes
  2371. for ax in self.figure.get_axes():
  2372. if ax._label != 'base':
  2373. ax.set_frame_on(False) # No frame
  2374. ax.set_xticks([]) # No tick
  2375. ax.set_yticks([]) # No ticks
  2376. ax.patch.set_visible(False) # No background
  2377. ax.set_aspect(1)
  2378. ax.set_xlim((xmin, xmax))
  2379. ax.set_ylim((ymin, ymax))
  2380. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  2381. # Re-draw
  2382. self.canvas.queue_draw()
  2383. def auto_adjust_axes(self, *args):
  2384. """
  2385. Calls ``adjust_axes()`` using the extents of the base axes.
  2386. :rtype : None
  2387. :return: None
  2388. """
  2389. xmin, xmax = self.axes.get_xlim()
  2390. ymin, ymax = self.axes.get_ylim()
  2391. self.adjust_axes(xmin, ymin, xmax, ymax)
  2392. def zoom(self, factor, center=None):
  2393. """
  2394. Zooms the plot by factor around a given
  2395. center point. Takes care of re-drawing.
  2396. :param factor: Number by which to scale the plot.
  2397. :type factor: float
  2398. :param center: Coordinates [x, y] of the point around which to scale the plot.
  2399. :type center: list
  2400. :return: None
  2401. """
  2402. xmin, xmax = self.axes.get_xlim()
  2403. ymin, ymax = self.axes.get_ylim()
  2404. width = xmax - xmin
  2405. height = ymax - ymin
  2406. if center is None:
  2407. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  2408. # For keeping the point at the pointer location
  2409. relx = (xmax - center[0]) / width
  2410. rely = (ymax - center[1]) / height
  2411. new_width = width / factor
  2412. new_height = height / factor
  2413. xmin = center[0] - new_width * (1 - relx)
  2414. xmax = center[0] + new_width * relx
  2415. ymin = center[1] - new_height * (1 - rely)
  2416. ymax = center[1] + new_height * rely
  2417. # Adjust axes
  2418. for ax in self.figure.get_axes():
  2419. ax.set_xlim((xmin, xmax))
  2420. ax.set_ylim((ymin, ymax))
  2421. # Re-draw
  2422. self.canvas.queue_draw()
  2423. def new_axes(self, name):
  2424. """
  2425. Creates and returns an Axes object attached to this object's Figure.
  2426. :param name: Unique label for the axes.
  2427. :return: Axes attached to the figure.
  2428. :rtype: Axes
  2429. """
  2430. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  2431. # def plot_axes(self, axes):
  2432. #
  2433. # if axes not in self.figure.axes:
  2434. # self.figure.add_axes(axes)
  2435. #
  2436. # # Basic configuration
  2437. # axes.set_frame_on(False) # No frame
  2438. # axes.set_xticks([]) # No tick
  2439. # axes.set_yticks([]) # No ticks
  2440. # axes.patch.set_visible(False) # No background
  2441. # axes.set_aspect(1)
  2442. #
  2443. # # Adjust limits
  2444. # self.auto_adjust_axes()
  2445. def on_scroll(self, canvas, event):
  2446. """
  2447. Scroll event handler.
  2448. :param canvas: The widget generating the event. Ignored.
  2449. :param event: Event object containing the event information.
  2450. :return: None
  2451. """
  2452. z, direction = event.get_scroll_direction()
  2453. if direction is Gdk.ScrollDirection.UP:
  2454. self.zoom(1.5, self.mouse)
  2455. else:
  2456. self.zoom(1/1.5, self.mouse)
  2457. def on_mouse_move(self, event):
  2458. """
  2459. Mouse movement event hadler.
  2460. :param event: Contains information about the event.
  2461. :return: None
  2462. """
  2463. self.mouse = [event.xdata, event.ydata]
  2464. app = App()
  2465. Gtk.main()