FlatCAMObj.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  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. from gi.repository import Gtk
  9. from gi.repository import Gdk
  10. from gi.repository import GLib
  11. from camlib import *
  12. ########################################
  13. ## FlatCAMObj ##
  14. ########################################
  15. class FlatCAMObj:
  16. """
  17. Base type of objects handled in FlatCAM. These become interactive
  18. in the GUI, can be plotted, and their options can be modified
  19. by the user in their respective forms.
  20. """
  21. # Instance of the application to which these are related.
  22. # The app should set this value.
  23. app = None
  24. def __init__(self, name):
  25. self.options = {"name": name}
  26. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  27. self.radios = {} # Name value pairs for radio sets
  28. self.radios_inv = {} # Inverse of self.radios
  29. self.axes = None # Matplotlib axes
  30. self.kind = None # Override with proper name
  31. def setup_axes(self, figure):
  32. """
  33. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  34. them to figure if not part of the figure. 4) Sets transparent
  35. background. 5) Sets 1:1 scale aspect ratio.
  36. :param figure: A Matplotlib.Figure on which to add/configure axes.
  37. :type figure: matplotlib.figure.Figure
  38. :return: None
  39. :rtype: None
  40. """
  41. if self.axes is None:
  42. print "New axes"
  43. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  44. label=self.options["name"])
  45. elif self.axes not in figure.axes:
  46. print "Clearing and attaching axes"
  47. self.axes.cla()
  48. figure.add_axes(self.axes)
  49. else:
  50. print "Clearing Axes"
  51. self.axes.cla()
  52. # Remove all decoration. The app's axes will have
  53. # the ticks and grid.
  54. self.axes.set_frame_on(False) # No frame
  55. self.axes.set_xticks([]) # No tick
  56. self.axes.set_yticks([]) # No ticks
  57. self.axes.patch.set_visible(False) # No background
  58. self.axes.set_aspect(1)
  59. def to_form(self):
  60. """
  61. Copies options to the UI form.
  62. :return: None
  63. """
  64. for option in self.options:
  65. self.set_form_item(option)
  66. def read_form(self):
  67. """
  68. Reads form into ``self.options``.
  69. :return: None
  70. :rtype: None
  71. """
  72. for option in self.options:
  73. self.read_form_item(option)
  74. def build_ui(self):
  75. """
  76. Sets up the UI/form for this object.
  77. :return: None
  78. :rtype: None
  79. """
  80. # Where the UI for this object is drawn
  81. box_selected = self.app.builder.get_object("box_selected")
  82. # Remove anything else in the box
  83. box_children = box_selected.get_children()
  84. for child in box_children:
  85. box_selected.remove(child)
  86. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  87. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  88. osw.remove(sw) # TODO: Is this needed ?
  89. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  90. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  91. # Put in the UI
  92. box_selected.pack_start(sw, True, True, 0)
  93. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  94. entry_name.connect("activate", self.app.on_activate_name)
  95. self.to_form()
  96. sw.show()
  97. def set_form_item(self, option):
  98. """
  99. Copies the specified option to the UI form.
  100. :param option: Name of the option (Key in ``self.options``).
  101. :type option: str
  102. :return: None
  103. """
  104. fkind = self.form_kinds[option]
  105. fname = fkind + "_" + self.kind + "_" + option
  106. if fkind == 'entry_eval' or fkind == 'entry_text':
  107. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  108. return
  109. if fkind == 'cb':
  110. self.app.builder.get_object(fname).set_active(self.options[option])
  111. return
  112. if fkind == 'radio':
  113. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  114. return
  115. print "Unknown kind of form item:", fkind
  116. def read_form_item(self, option):
  117. """
  118. Reads the specified option from the UI form into ``self.options``.
  119. :param option: Name of the option.
  120. :type option: str
  121. :return: None
  122. """
  123. fkind = self.form_kinds[option]
  124. fname = fkind + "_" + self.kind + "_" + option
  125. if fkind == 'entry_text':
  126. self.options[option] = self.app.builder.get_object(fname).get_text()
  127. return
  128. if fkind == 'entry_eval':
  129. self.options[option] = self.app.get_eval(fname)
  130. return
  131. if fkind == 'cb':
  132. self.options[option] = self.app.builder.get_object(fname).get_active()
  133. return
  134. if fkind == 'radio':
  135. self.options[option] = self.app.get_radio_value(self.radios[option])
  136. return
  137. print "Unknown kind of form item:", fkind
  138. def plot(self):
  139. """
  140. Plot this object (Extend this method to implement the actual plotting).
  141. Axes get created, appended to canvas and cleared before plotting.
  142. Call this in descendants before doing the plotting.
  143. :return: Whether to continue plotting or not depending on the "plot" option.
  144. :rtype: bool
  145. """
  146. # Axes must exist and be attached to canvas.
  147. if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
  148. self.axes = self.app.plotcanvas.new_axes(self.options['name'])
  149. if not self.options["plot"]:
  150. self.axes.cla()
  151. self.app.plotcanvas.auto_adjust_axes()
  152. return False
  153. # Clear axes or we will plot on top of them.
  154. self.axes.cla()
  155. # GLib.idle_add(self.axes.cla)
  156. return True
  157. def serialize(self):
  158. """
  159. Returns a representation of the object as a dictionary so
  160. it can be later exported as JSON. Override this method.
  161. :return: Dictionary representing the object
  162. :rtype: dict
  163. """
  164. return
  165. def deserialize(self, obj_dict):
  166. """
  167. Re-builds an object from its serialized version.
  168. :param obj_dict: Dictionary representing a FlatCAMObj
  169. :type obj_dict: dict
  170. :return: None
  171. """
  172. return
  173. class FlatCAMGerber(FlatCAMObj, Gerber):
  174. """
  175. Represents Gerber code.
  176. """
  177. def __init__(self, name):
  178. Gerber.__init__(self)
  179. FlatCAMObj.__init__(self, name)
  180. self.kind = "gerber"
  181. # The 'name' is already in self.options from FlatCAMObj
  182. self.options.update({
  183. "plot": True,
  184. "mergepolys": True,
  185. "multicolored": False,
  186. "solid": False,
  187. "isotooldia": 0.016,
  188. "isopasses": 1,
  189. "isooverlap": 0.15,
  190. "cutouttooldia": 0.07,
  191. "cutoutmargin": 0.2,
  192. "cutoutgapsize": 0.15,
  193. "gaps": "tb",
  194. "noncoppermargin": 0.0,
  195. "noncopperrounded": False,
  196. "bboxmargin": 0.0,
  197. "bboxrounded": False
  198. })
  199. # The 'name' is already in self.form_kinds from FlatCAMObj
  200. self.form_kinds.update({
  201. "plot": "cb",
  202. "mergepolys": "cb",
  203. "multicolored": "cb",
  204. "solid": "cb",
  205. "isotooldia": "entry_eval",
  206. "isopasses": "entry_eval",
  207. "isooverlap": "entry_eval",
  208. "cutouttooldia": "entry_eval",
  209. "cutoutmargin": "entry_eval",
  210. "cutoutgapsize": "entry_eval",
  211. "gaps": "radio",
  212. "noncoppermargin": "entry_eval",
  213. "noncopperrounded": "cb",
  214. "bboxmargin": "entry_eval",
  215. "bboxrounded": "cb"
  216. })
  217. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  218. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  219. # Attributes to be included in serialization
  220. # Always append to it because it carries contents
  221. # from predecessors.
  222. self.ser_attrs += ['options', 'kind']
  223. def convert_units(self, units):
  224. """
  225. Converts the units of the object by scaling dimensions in all geometry
  226. and options.
  227. :param units: Units to which to convert the object: "IN" or "MM".
  228. :type units: str
  229. :return: None
  230. :rtype: None
  231. """
  232. factor = Gerber.convert_units(self, units)
  233. self.options['isotooldia'] *= factor
  234. self.options['cutoutmargin'] *= factor
  235. self.options['cutoutgapsize'] *= factor
  236. self.options['noncoppermargin'] *= factor
  237. self.options['bboxmargin'] *= factor
  238. def plot(self):
  239. # Does all the required setup and returns False
  240. # if the 'ptint' option is set to False.
  241. if not FlatCAMObj.plot(self):
  242. return
  243. if self.options["mergepolys"]:
  244. geometry = self.solid_geometry
  245. else:
  246. geometry = self.buffered_paths + \
  247. [poly['polygon'] for poly in self.regions] + \
  248. self.flash_geometry
  249. if self.options["multicolored"]:
  250. linespec = '-'
  251. else:
  252. linespec = 'k-'
  253. if self.options["solid"]:
  254. for poly in geometry:
  255. # TODO: Too many things hardcoded.
  256. patch = PolygonPatch(poly,
  257. facecolor="#BBF268",
  258. edgecolor="#006E20",
  259. alpha=0.75,
  260. zorder=2)
  261. self.axes.add_patch(patch)
  262. else:
  263. for poly in geometry:
  264. x, y = poly.exterior.xy
  265. self.axes.plot(x, y, linespec)
  266. for ints in poly.interiors:
  267. x, y = ints.coords.xy
  268. self.axes.plot(x, y, linespec)
  269. # self.app.plotcanvas.auto_adjust_axes()
  270. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  271. def serialize(self):
  272. return {
  273. "options": self.options,
  274. "kind": self.kind
  275. }
  276. class FlatCAMExcellon(FlatCAMObj, Excellon):
  277. """
  278. Represents Excellon/Drill code.
  279. """
  280. def __init__(self, name):
  281. Excellon.__init__(self)
  282. FlatCAMObj.__init__(self, name)
  283. self.kind = "excellon"
  284. self.options.update({
  285. "plot": True,
  286. "solid": False,
  287. "drillz": -0.1,
  288. "travelz": 0.1,
  289. "feedrate": 5.0,
  290. "toolselection": ""
  291. })
  292. self.form_kinds.update({
  293. "plot": "cb",
  294. "solid": "cb",
  295. "drillz": "entry_eval",
  296. "travelz": "entry_eval",
  297. "feedrate": "entry_eval",
  298. "toolselection": "entry_text"
  299. })
  300. # TODO: Document this.
  301. self.tool_cbs = {}
  302. # Attributes to be included in serialization
  303. # Always append to it because it carries contents
  304. # from predecessors.
  305. self.ser_attrs += ['options', 'kind']
  306. def convert_units(self, units):
  307. factor = Excellon.convert_units(self, units)
  308. self.options['drillz'] *= factor
  309. self.options['travelz'] *= factor
  310. self.options['feedrate'] *= factor
  311. def plot(self):
  312. # Does all the required setup and returns False
  313. # if the 'ptint' option is set to False.
  314. if not FlatCAMObj.plot(self):
  315. return
  316. try:
  317. _ = iter(self.solid_geometry)
  318. except TypeError:
  319. self.solid_geometry = [self.solid_geometry]
  320. # Plot excellon (All polygons?)
  321. if self.options["solid"]:
  322. for geo in self.solid_geometry:
  323. patch = PolygonPatch(geo,
  324. facecolor="#C40000",
  325. edgecolor="#750000",
  326. alpha=0.75,
  327. zorder=3)
  328. self.axes.add_patch(patch)
  329. else:
  330. for geo in self.solid_geometry:
  331. x, y = geo.exterior.coords.xy
  332. self.axes.plot(x, y, 'r-')
  333. for ints in geo.interiors:
  334. x, y = ints.coords.xy
  335. self.axes.plot(x, y, 'g-')
  336. #self.app.plotcanvas.auto_adjust_axes()
  337. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  338. def show_tool_chooser(self):
  339. win = Gtk.Window()
  340. box = Gtk.Box(spacing=2)
  341. box.set_orientation(Gtk.Orientation(1))
  342. win.add(box)
  343. for tool in self.tools:
  344. self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
  345. box.pack_start(self.tool_cbs[tool], False, False, 1)
  346. button = Gtk.Button(label="Accept")
  347. box.pack_start(button, False, False, 1)
  348. win.show_all()
  349. def on_accept(widget):
  350. win.destroy()
  351. tool_list = []
  352. for toolx in self.tool_cbs:
  353. if self.tool_cbs[toolx].get_active():
  354. tool_list.append(toolx)
  355. self.options["toolselection"] = ", ".join(tool_list)
  356. self.to_form()
  357. button.connect("activate", on_accept)
  358. button.connect("clicked", on_accept)
  359. class FlatCAMCNCjob(FlatCAMObj, CNCjob):
  360. """
  361. Represents G-Code.
  362. """
  363. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  364. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  365. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  366. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  367. FlatCAMObj.__init__(self, name)
  368. self.kind = "cncjob"
  369. self.options.update({
  370. "plot": True,
  371. "tooldia": 0.4 / 25.4 # 0.4mm in inches
  372. })
  373. self.form_kinds.update({
  374. "plot": "cb",
  375. "tooldia": "entry_eval"
  376. })
  377. # Attributes to be included in serialization
  378. # Always append to it because it carries contents
  379. # from predecessors.
  380. self.ser_attrs += ['options', 'kind']
  381. def plot(self):
  382. # Does all the required setup and returns False
  383. # if the 'ptint' option is set to False.
  384. if not FlatCAMObj.plot(self):
  385. return
  386. self.plot2(self.axes, tooldia=self.options["tooldia"])
  387. #self.app.plotcanvas.auto_adjust_axes()
  388. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  389. def convert_units(self, units):
  390. factor = CNCjob.convert_units(self, units)
  391. print "FlatCAMCNCjob.convert_units()"
  392. self.options["tooldia"] *= factor
  393. class FlatCAMGeometry(FlatCAMObj, Geometry):
  394. """
  395. Geometric object not associated with a specific
  396. format.
  397. """
  398. def __init__(self, name):
  399. FlatCAMObj.__init__(self, name)
  400. Geometry.__init__(self)
  401. self.kind = "geometry"
  402. self.options.update({
  403. "plot": True,
  404. "solid": False,
  405. "multicolored": False,
  406. "cutz": -0.002,
  407. "travelz": 0.1,
  408. "feedrate": 5.0,
  409. "cnctooldia": 0.4 / 25.4,
  410. "painttooldia": 0.0625,
  411. "paintoverlap": 0.15,
  412. "paintmargin": 0.01
  413. })
  414. self.form_kinds.update({
  415. "plot": "cb",
  416. "solid": "cb",
  417. "multicolored": "cb",
  418. "cutz": "entry_eval",
  419. "travelz": "entry_eval",
  420. "feedrate": "entry_eval",
  421. "cnctooldia": "entry_eval",
  422. "painttooldia": "entry_eval",
  423. "paintoverlap": "entry_eval",
  424. "paintmargin": "entry_eval"
  425. })
  426. # Attributes to be included in serialization
  427. # Always append to it because it carries contents
  428. # from predecessors.
  429. self.ser_attrs += ['options', 'kind']
  430. def scale(self, factor):
  431. """
  432. Scales all geometry by a given factor.
  433. :param factor: Factor by which to scale the object's geometry/
  434. :type factor: float
  435. :return: None
  436. :rtype: None
  437. """
  438. if type(self.solid_geometry) == list:
  439. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  440. for g in self.solid_geometry]
  441. else:
  442. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  443. origin=(0, 0))
  444. def offset(self, vect):
  445. """
  446. Offsets all geometry by a given vector/
  447. :param vect: (x, y) vector by which to offset the object's geometry.
  448. :type vect: tuple
  449. :return: None
  450. :rtype: None
  451. """
  452. dx, dy = vect
  453. if type(self.solid_geometry) == list:
  454. self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
  455. for g in self.solid_geometry]
  456. else:
  457. self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
  458. def convert_units(self, units):
  459. factor = Geometry.convert_units(self, units)
  460. self.options['cutz'] *= factor
  461. self.options['travelz'] *= factor
  462. self.options['feedrate'] *= factor
  463. self.options['cnctooldia'] *= factor
  464. self.options['painttooldia'] *= factor
  465. self.options['paintmargin'] *= factor
  466. return factor
  467. def plot(self):
  468. """
  469. Plots the object into its axes. If None, of if the axes
  470. are not part of the app's figure, it fetches new ones.
  471. :return: None
  472. """
  473. # Does all the required setup and returns False
  474. # if the 'ptint' option is set to False.
  475. if not FlatCAMObj.plot(self):
  476. return
  477. # Make sure solid_geometry is iterable.
  478. try:
  479. _ = iter(self.solid_geometry)
  480. except TypeError:
  481. self.solid_geometry = [self.solid_geometry]
  482. for geo in self.solid_geometry:
  483. if type(geo) == Polygon:
  484. x, y = geo.exterior.coords.xy
  485. self.axes.plot(x, y, 'r-')
  486. for ints in geo.interiors:
  487. x, y = ints.coords.xy
  488. self.axes.plot(x, y, 'r-')
  489. continue
  490. if type(geo) == LineString or type(geo) == LinearRing:
  491. x, y = geo.coords.xy
  492. self.axes.plot(x, y, 'r-')
  493. continue
  494. if type(geo) == MultiPolygon:
  495. for poly in geo:
  496. x, y = poly.exterior.coords.xy
  497. self.axes.plot(x, y, 'r-')
  498. for ints in poly.interiors:
  499. x, y = ints.coords.xy
  500. self.axes.plot(x, y, 'r-')
  501. continue
  502. print "WARNING: Did not plot:", str(type(geo))
  503. #self.app.plotcanvas.auto_adjust_axes()
  504. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)