FlatCAM.py 91 KB

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