FlatCAM.py 86 KB

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