FlatCAM.py 85 KB

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