FlatCAM.py 103 KB

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