FlatCAMObj.py 19 KB

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