FlatCAM.py 99 KB

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