FlatCAMObj.py 20 KB

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