FlatCAM.py 113 KB

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