FlatCAM.py 85 KB

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