FlatCAM.py 85 KB

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