FlatCAM.py 103 KB

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