FlatCAM.py 88 KB

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