FlatCAMObj.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  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. import inspect # TODO: Remove
  13. from FlatCAMApp import *
  14. from camlib import *
  15. from ObjectUI import *
  16. class LoudDict(dict):
  17. def __init__(self, *args, **kwargs):
  18. super(LoudDict, self).__init__(*args, **kwargs)
  19. self.callback = lambda x: None
  20. self.silence = False
  21. def set_change_callback(self, callback):
  22. if self.silence:
  23. return
  24. self.callback = callback
  25. def __setitem__(self, key, value):
  26. super(LoudDict, self).__setitem__(key, value)
  27. try:
  28. if self.__getitem__(key) == value:
  29. return
  30. except KeyError:
  31. pass
  32. self.callback(key)
  33. ########################################
  34. ## FlatCAMObj ##
  35. ########################################
  36. class FlatCAMObj(GObject.GObject, object):
  37. """
  38. Base type of objects handled in FlatCAM. These become interactive
  39. in the GUI, can be plotted, and their options can be modified
  40. by the user in their respective forms.
  41. """
  42. # Instance of the application to which these are related.
  43. # The app should set this value.
  44. app = None
  45. # name = GObject.property(type=str)
  46. def __init__(self, name, ui):
  47. """
  48. :param name: Name of the object given by the user.
  49. :param ui: User interface to interact with the object.
  50. :type ui: ObjectUI
  51. :return: FlatCAMObj
  52. """
  53. GObject.GObject.__init__(self)
  54. # View
  55. self.ui = ui
  56. self.options = LoudDict(name=name)
  57. self.options.set_change_callback(self.on_options_change)
  58. self.form_fields = {"name": self.ui.name_entry}
  59. self.radios = {} # Name value pairs for radio sets
  60. self.radios_inv = {} # Inverse of self.radios
  61. self.axes = None # Matplotlib axes
  62. self.kind = None # Override with proper name
  63. self.muted_ui = False
  64. self.ui.name_entry.connect('activate', self.on_name_activate)
  65. self.ui.offset_button.connect('clicked', self.on_offset_button_click)
  66. self.ui.offset_button.connect('activate', self.on_offset_button_click)
  67. self.ui.scale_button.connect('clicked', self.on_scale_button_click)
  68. self.ui.scale_button.connect('activate', self.on_scale_button_click)
  69. def __str__(self):
  70. return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
  71. def on_name_activate(self, *args):
  72. old_name = copy(self.options["name"])
  73. new_name = self.ui.name_entry.get_text()
  74. self.options["name"] = self.ui.name_entry.get_text()
  75. self.app.info("Name changed from %s to %s" % (old_name, new_name))
  76. def on_offset_button_click(self, *args):
  77. self.read_form()
  78. vect = self.ui.offsetvector_entry.get_value()
  79. self.offset(vect)
  80. self.plot()
  81. def on_scale_button_click(self, *args):
  82. self.read_form()
  83. factor = self.ui.scale_entry.get_value()
  84. self.scale(factor)
  85. self.plot()
  86. def on_options_change(self, key):
  87. self.form_fields[key].set_value(self.options[key])
  88. return
  89. def setup_axes(self, figure):
  90. """
  91. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  92. them to figure if not part of the figure. 4) Sets transparent
  93. background. 5) Sets 1:1 scale aspect ratio.
  94. :param figure: A Matplotlib.Figure on which to add/configure axes.
  95. :type figure: matplotlib.figure.Figure
  96. :return: None
  97. :rtype: None
  98. """
  99. if self.axes is None:
  100. print "New axes"
  101. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  102. label=self.options["name"])
  103. elif self.axes not in figure.axes:
  104. print "Clearing and attaching axes"
  105. self.axes.cla()
  106. figure.add_axes(self.axes)
  107. else:
  108. print "Clearing Axes"
  109. self.axes.cla()
  110. # Remove all decoration. The app's axes will have
  111. # the ticks and grid.
  112. self.axes.set_frame_on(False) # No frame
  113. self.axes.set_xticks([]) # No tick
  114. self.axes.set_yticks([]) # No ticks
  115. self.axes.patch.set_visible(False) # No background
  116. self.axes.set_aspect(1)
  117. def to_form(self):
  118. """
  119. Copies options to the UI form.
  120. :return: None
  121. """
  122. for option in self.options:
  123. self.set_form_item(option)
  124. def read_form(self):
  125. """
  126. Reads form into ``self.options``.
  127. :return: None
  128. :rtype: None
  129. """
  130. print inspect.stack()[1][3], "--> FlatCAMObj.read_form()"
  131. for option in self.options:
  132. self.read_form_item(option)
  133. def build_ui(self):
  134. """
  135. Sets up the UI/form for this object.
  136. :return: None
  137. :rtype: None
  138. """
  139. self.muted_ui = True
  140. print inspect.stack()[1][3], "--> FlatCAMObj.build_ui()"
  141. # Where the UI for this object is drawn
  142. # box_selected = self.app.builder.get_object("box_selected")
  143. box_selected = self.app.builder.get_object("vp_selected")
  144. # Remove anything else in the box
  145. box_children = box_selected.get_children()
  146. for child in box_children:
  147. box_selected.remove(child)
  148. # Put in the UI
  149. # box_selected.pack_start(sw, True, True, 0)
  150. box_selected.add(self.ui)
  151. self.to_form()
  152. box_selected.show_all()
  153. self.ui.show()
  154. self.muted_ui = False
  155. def set_form_item(self, option):
  156. """
  157. Copies the specified option to the UI form.
  158. :param option: Name of the option (Key in ``self.options``).
  159. :type option: str
  160. :return: None
  161. """
  162. try:
  163. self.form_fields[option].set_value(self.options[option])
  164. except KeyError:
  165. App.log.warn("Tried to set an option or field that does not exist: %s" % option)
  166. def read_form_item(self, option):
  167. """
  168. Reads the specified option from the UI form into ``self.options``.
  169. :param option: Name of the option.
  170. :type option: str
  171. :return: None
  172. """
  173. try:
  174. self.options[option] = self.form_fields[option].get_value()
  175. except KeyError:
  176. App.log.warning("Failed to read option from field: %s" % option)
  177. def plot(self):
  178. """
  179. Plot this object (Extend this method to implement the actual plotting).
  180. Axes get created, appended to canvas and cleared before plotting.
  181. Call this in descendants before doing the plotting.
  182. :return: Whether to continue plotting or not depending on the "plot" option.
  183. :rtype: bool
  184. """
  185. # Axes must exist and be attached to canvas.
  186. if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
  187. self.axes = self.app.plotcanvas.new_axes(self.options['name'])
  188. if not self.options["plot"]:
  189. self.axes.cla()
  190. self.app.plotcanvas.auto_adjust_axes()
  191. return False
  192. # Clear axes or we will plot on top of them.
  193. self.axes.cla()
  194. # GLib.idle_add(self.axes.cla)
  195. return True
  196. def serialize(self):
  197. """
  198. Returns a representation of the object as a dictionary so
  199. it can be later exported as JSON. Override this method.
  200. :return: Dictionary representing the object
  201. :rtype: dict
  202. """
  203. return
  204. def deserialize(self, obj_dict):
  205. """
  206. Re-builds an object from its serialized version.
  207. :param obj_dict: Dictionary representing a FlatCAMObj
  208. :type obj_dict: dict
  209. :return: None
  210. """
  211. return
  212. class FlatCAMGerber(FlatCAMObj, Gerber):
  213. """
  214. Represents Gerber code.
  215. """
  216. def __init__(self, name):
  217. Gerber.__init__(self)
  218. FlatCAMObj.__init__(self, name, GerberObjectUI())
  219. self.kind = "gerber"
  220. self.form_fields.update({
  221. "plot": self.ui.plot_cb,
  222. "multicolored": self.ui.multicolored_cb,
  223. "solid": self.ui.solid_cb,
  224. "isotooldia": self.ui.iso_tool_dia_entry,
  225. "isopasses": self.ui.iso_width_entry,
  226. "isooverlap": self.ui.iso_overlap_entry,
  227. "cutouttooldia": self.ui.cutout_tooldia_entry,
  228. "cutoutmargin": self.ui.cutout_margin_entry,
  229. "cutoutgapsize": self.ui.cutout_gap_entry,
  230. "gaps": self.ui.gaps_radio,
  231. "noncoppermargin": self.ui.noncopper_margin_entry,
  232. "noncopperrounded": self.ui.noncopper_rounded_cb,
  233. "bboxmargin": self.ui.bbmargin_entry,
  234. "bboxrounded": self.ui.bbrounded_cb
  235. })
  236. # The 'name' is already in self.options from FlatCAMObj
  237. # Automatically updates the UI
  238. self.options.update({
  239. "plot": True,
  240. "multicolored": False,
  241. "solid": False,
  242. "isotooldia": 0.016,
  243. "isopasses": 1,
  244. "isooverlap": 0.15,
  245. "cutouttooldia": 0.07,
  246. "cutoutmargin": 0.2,
  247. "cutoutgapsize": 0.15,
  248. "gaps": "tb",
  249. "noncoppermargin": 0.0,
  250. "noncopperrounded": False,
  251. "bboxmargin": 0.0,
  252. "bboxrounded": False
  253. })
  254. # Attributes to be included in serialization
  255. # Always append to it because it carries contents
  256. # from predecessors.
  257. self.ser_attrs += ['options', 'kind']
  258. assert isinstance(self.ui, GerberObjectUI)
  259. self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
  260. self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
  261. self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
  262. self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
  263. self.ui.multicolored_cb.connect('clicked', self.on_multicolored_cb_click)
  264. self.ui.multicolored_cb.connect('activate', self.on_multicolored_cb_click)
  265. self.ui.generate_iso_button.connect('clicked', self.on_iso_button_click)
  266. self.ui.generate_iso_button.connect('activate', self.on_iso_button_click)
  267. self.ui.generate_cutout_button.connect('clicked', self.on_generatecutout_button_click)
  268. self.ui.generate_cutout_button.connect('activate', self.on_generatecutout_button_click)
  269. self.ui.generate_bb_button.connect('clicked', self.on_generatebb_button_click)
  270. self.ui.generate_bb_button.connect('activate', self.on_generatebb_button_click)
  271. self.ui.generate_noncopper_button.connect('clicked', self.on_generatenoncopper_button_click)
  272. self.ui.generate_noncopper_button.connect('activate', self.on_generatenoncopper_button_click)
  273. def on_generatenoncopper_button_click(self, *args):
  274. self.read_form()
  275. name = self.options["name"] + "_noncopper"
  276. def geo_init(geo_obj, app_obj):
  277. assert isinstance(geo_obj, FlatCAMGeometry)
  278. bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
  279. if not self.options["noncopperrounded"]:
  280. bounding_box = bounding_box.envelope
  281. non_copper = bounding_box.difference(self.solid_geometry)
  282. geo_obj.solid_geometry = non_copper
  283. # TODO: Check for None
  284. self.app.new_object("geometry", name, geo_init)
  285. def on_generatebb_button_click(self, *args):
  286. self.read_form()
  287. name = self.options["name"] + "_bbox"
  288. def geo_init(geo_obj, app_obj):
  289. assert isinstance(geo_obj, FlatCAMGeometry)
  290. # Bounding box with rounded corners
  291. bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
  292. if not self.options["bboxrounded"]: # Remove rounded corners
  293. bounding_box = bounding_box.envelope
  294. geo_obj.solid_geometry = bounding_box
  295. self.app.new_object("geometry", name, geo_init)
  296. def on_generatecutout_button_click(self, *args):
  297. self.read_form()
  298. name = self.options["name"] + "_cutout"
  299. def geo_init(geo_obj, app_obj):
  300. margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
  301. gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
  302. minx, miny, maxx, maxy = self.bounds()
  303. minx -= margin
  304. maxx += margin
  305. miny -= margin
  306. maxy += margin
  307. midx = 0.5 * (minx + maxx)
  308. midy = 0.5 * (miny + maxy)
  309. hgap = 0.5 * gap_size
  310. pts = [[midx - hgap, maxy],
  311. [minx, maxy],
  312. [minx, midy + hgap],
  313. [minx, midy - hgap],
  314. [minx, miny],
  315. [midx - hgap, miny],
  316. [midx + hgap, miny],
  317. [maxx, miny],
  318. [maxx, midy - hgap],
  319. [maxx, midy + hgap],
  320. [maxx, maxy],
  321. [midx + hgap, maxy]]
  322. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  323. [pts[6], pts[7], pts[10], pts[11]]],
  324. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  325. [pts[3], pts[4], pts[7], pts[8]]],
  326. "4": [[pts[0], pts[1], pts[2]],
  327. [pts[3], pts[4], pts[5]],
  328. [pts[6], pts[7], pts[8]],
  329. [pts[9], pts[10], pts[11]]]}
  330. cuts = cases[self.options['gaps']]
  331. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  332. # TODO: Check for None
  333. self.app.new_object("geometry", name, geo_init)
  334. def on_iso_button_click(self, *args):
  335. self.read_form()
  336. dia = self.options["isotooldia"]
  337. passes = int(self.options["isopasses"])
  338. overlap = self.options["isooverlap"] * dia
  339. for i in range(passes):
  340. offset = (2*i + 1)/2.0 * dia - i*overlap
  341. iso_name = self.options["name"] + "_iso%d" % (i+1)
  342. # TODO: This is ugly. Create way to pass data into init function.
  343. def iso_init(geo_obj, app_obj):
  344. # Propagate options
  345. geo_obj.options["cnctooldia"] = self.options["isotooldia"]
  346. geo_obj.solid_geometry = self.isolation_geometry(offset)
  347. app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
  348. # TODO: Do something if this is None. Offer changing name?
  349. self.app.new_object("geometry", iso_name, iso_init)
  350. def on_plot_cb_click(self, *args):
  351. if self.muted_ui:
  352. return
  353. self.read_form_item('plot')
  354. self.plot()
  355. def on_solid_cb_click(self, *args):
  356. if self.muted_ui:
  357. return
  358. self.read_form_item('solid')
  359. self.plot()
  360. def on_multicolored_cb_click(self, *args):
  361. if self.muted_ui:
  362. return
  363. self.read_form_item('multicolored')
  364. self.plot()
  365. def convert_units(self, units):
  366. """
  367. Converts the units of the object by scaling dimensions in all geometry
  368. and options.
  369. :param units: Units to which to convert the object: "IN" or "MM".
  370. :type units: str
  371. :return: None
  372. :rtype: None
  373. """
  374. factor = Gerber.convert_units(self, units)
  375. self.options['isotooldia'] *= factor
  376. self.options['cutoutmargin'] *= factor
  377. self.options['cutoutgapsize'] *= factor
  378. self.options['noncoppermargin'] *= factor
  379. self.options['bboxmargin'] *= factor
  380. def plot(self):
  381. # Does all the required setup and returns False
  382. # if the 'ptint' option is set to False.
  383. if not FlatCAMObj.plot(self):
  384. return
  385. # if self.options["mergepolys"]:
  386. # geometry = self.solid_geometry
  387. # else:
  388. # geometry = self.buffered_paths + \
  389. # [poly['polygon'] for poly in self.regions] + \
  390. # self.flash_geometry
  391. geometry = self.solid_geometry
  392. # Make sure geometry is iterable.
  393. try:
  394. _ = iter(geometry)
  395. except TypeError:
  396. geometry = [geometry]
  397. if self.options["multicolored"]:
  398. linespec = '-'
  399. else:
  400. linespec = 'k-'
  401. if self.options["solid"]:
  402. for poly in geometry:
  403. # TODO: Too many things hardcoded.
  404. try:
  405. patch = PolygonPatch(poly,
  406. facecolor="#BBF268",
  407. edgecolor="#006E20",
  408. alpha=0.75,
  409. zorder=2)
  410. self.axes.add_patch(patch)
  411. except AssertionError:
  412. print "WARNING: A geometry component was not a polygon:"
  413. print poly
  414. else:
  415. for poly in geometry:
  416. x, y = poly.exterior.xy
  417. self.axes.plot(x, y, linespec)
  418. for ints in poly.interiors:
  419. x, y = ints.coords.xy
  420. self.axes.plot(x, y, linespec)
  421. # self.app.plotcanvas.auto_adjust_axes()
  422. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  423. def serialize(self):
  424. return {
  425. "options": self.options,
  426. "kind": self.kind
  427. }
  428. class FlatCAMExcellon(FlatCAMObj, Excellon):
  429. """
  430. Represents Excellon/Drill code.
  431. """
  432. def __init__(self, name):
  433. Excellon.__init__(self)
  434. FlatCAMObj.__init__(self, name, ExcellonObjectUI())
  435. self.kind = "excellon"
  436. self.form_fields.update({
  437. "name": self.ui.name_entry,
  438. "plot": self.ui.plot_cb,
  439. "solid": self.ui.solid_cb,
  440. "drillz": self.ui.cutz_entry,
  441. "travelz": self.ui.travelz_entry,
  442. "feedrate": self.ui.feedrate_entry,
  443. "toolselection": self.ui.tools_entry
  444. })
  445. self.options.update({
  446. "plot": True,
  447. "solid": False,
  448. "drillz": -0.1,
  449. "travelz": 0.1,
  450. "feedrate": 5.0,
  451. "toolselection": ""
  452. })
  453. # self.form_kinds.update({
  454. # "plot": "cb",
  455. # "solid": "cb",
  456. # "drillz": "entry_eval",
  457. # "travelz": "entry_eval",
  458. # "feedrate": "entry_eval",
  459. # "toolselection": "entry_text"
  460. # })
  461. # TODO: Document this.
  462. self.tool_cbs = {}
  463. # Attributes to be included in serialization
  464. # Always append to it because it carries contents
  465. # from predecessors.
  466. self.ser_attrs += ['options', 'kind']
  467. self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
  468. self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
  469. self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
  470. self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
  471. def on_plot_cb_click(self, *args):
  472. if self.muted_ui:
  473. return
  474. self.read_form_item('plot')
  475. self.plot()
  476. def on_solid_cb_click(self, *args):
  477. if self.muted_ui:
  478. return
  479. self.read_form_item('solid')
  480. self.plot()
  481. def convert_units(self, units):
  482. factor = Excellon.convert_units(self, units)
  483. self.options['drillz'] *= factor
  484. self.options['travelz'] *= factor
  485. self.options['feedrate'] *= factor
  486. def plot(self):
  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. try:
  492. _ = iter(self.solid_geometry)
  493. except TypeError:
  494. self.solid_geometry = [self.solid_geometry]
  495. # Plot excellon (All polygons?)
  496. if self.options["solid"]:
  497. for geo in self.solid_geometry:
  498. patch = PolygonPatch(geo,
  499. facecolor="#C40000",
  500. edgecolor="#750000",
  501. alpha=0.75,
  502. zorder=3)
  503. self.axes.add_patch(patch)
  504. else:
  505. for geo in self.solid_geometry:
  506. x, y = geo.exterior.coords.xy
  507. self.axes.plot(x, y, 'r-')
  508. for ints in geo.interiors:
  509. x, y = ints.coords.xy
  510. self.axes.plot(x, y, 'g-')
  511. #self.app.plotcanvas.auto_adjust_axes()
  512. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  513. def show_tool_chooser(self):
  514. win = Gtk.Window()
  515. box = Gtk.Box(spacing=2)
  516. box.set_orientation(Gtk.Orientation(1))
  517. win.add(box)
  518. for tool in self.tools:
  519. self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
  520. box.pack_start(self.tool_cbs[tool], False, False, 1)
  521. button = Gtk.Button(label="Accept")
  522. box.pack_start(button, False, False, 1)
  523. win.show_all()
  524. def on_accept(widget):
  525. win.destroy()
  526. tool_list = []
  527. for toolx in self.tool_cbs:
  528. if self.tool_cbs[toolx].get_active():
  529. tool_list.append(toolx)
  530. self.options["toolselection"] = ", ".join(tool_list)
  531. self.to_form()
  532. button.connect("activate", on_accept)
  533. button.connect("clicked", on_accept)
  534. class FlatCAMCNCjob(FlatCAMObj, CNCjob):
  535. """
  536. Represents G-Code.
  537. """
  538. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  539. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  540. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  541. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  542. FlatCAMObj.__init__(self, name, CNCObjectUI())
  543. self.kind = "cncjob"
  544. self.options.update({
  545. "plot": True,
  546. "tooldia": 0.4 / 25.4 # 0.4mm in inches
  547. })
  548. self.form_fields.update({
  549. "name": self.ui.name_entry,
  550. "plot": self.ui.plot_cb,
  551. "tooldia": self.ui.tooldia_entry
  552. })
  553. # self.form_kinds.update({
  554. # "plot": "cb",
  555. # "tooldia": "entry_eval"
  556. # })
  557. # Attributes to be included in serialization
  558. # Always append to it because it carries contents
  559. # from predecessors.
  560. self.ser_attrs += ['options', 'kind']
  561. self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
  562. self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
  563. def on_plot_cb_click(self, *args):
  564. if self.muted_ui:
  565. return
  566. self.read_form_item('plot')
  567. self.plot()
  568. def plot(self):
  569. # Does all the required setup and returns False
  570. # if the 'ptint' option is set to False.
  571. if not FlatCAMObj.plot(self):
  572. return
  573. self.plot2(self.axes, tooldia=self.options["tooldia"])
  574. #self.app.plotcanvas.auto_adjust_axes()
  575. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
  576. def convert_units(self, units):
  577. factor = CNCjob.convert_units(self, units)
  578. print "FlatCAMCNCjob.convert_units()"
  579. self.options["tooldia"] *= factor
  580. class FlatCAMGeometry(FlatCAMObj, Geometry):
  581. """
  582. Geometric object not associated with a specific
  583. format.
  584. """
  585. def __init__(self, name):
  586. FlatCAMObj.__init__(self, name, GeometryObjectUI())
  587. Geometry.__init__(self)
  588. self.kind = "geometry"
  589. self.form_fields.update({
  590. "name": self.ui.name_entry,
  591. "plot": self.ui.plot_cb,
  592. # "solid": self.ui.sol,
  593. # "multicolored": self.ui.,
  594. "cutz": self.ui.cutz_entry,
  595. "travelz": self.ui.travelz_entry,
  596. "feedrate": self.ui.cncfeedrate_entry,
  597. "cnctooldia": self.ui.cnctooldia_entry,
  598. "painttooldia": self.ui.painttooldia_entry,
  599. "paintoverlap": self.ui.paintoverlap_entry,
  600. "paintmargin": self.ui.paintmargin_entry
  601. })
  602. self.options.update({
  603. "plot": True,
  604. # "solid": False,
  605. # "multicolored": False,
  606. "cutz": -0.002,
  607. "travelz": 0.1,
  608. "feedrate": 5.0,
  609. "cnctooldia": 0.4 / 25.4,
  610. "painttooldia": 0.0625,
  611. "paintoverlap": 0.15,
  612. "paintmargin": 0.01
  613. })
  614. # self.form_kinds.update({
  615. # "plot": "cb",
  616. # "solid": "cb",
  617. # "multicolored": "cb",
  618. # "cutz": "entry_eval",
  619. # "travelz": "entry_eval",
  620. # "feedrate": "entry_eval",
  621. # "cnctooldia": "entry_eval",
  622. # "painttooldia": "entry_eval",
  623. # "paintoverlap": "entry_eval",
  624. # "paintmargin": "entry_eval"
  625. # })
  626. # Attributes to be included in serialization
  627. # Always append to it because it carries contents
  628. # from predecessors.
  629. self.ser_attrs += ['options', 'kind']
  630. assert isinstance(self.ui, GeometryObjectUI)
  631. self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
  632. self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
  633. self.ui.generate_cnc_button.connect('clicked', self.on_generatecnc_button_click)
  634. self.ui.generate_cnc_button.connect('activate', self.on_generatecnc_button_click)
  635. self.ui.generate_paint_button.connect('clicked', self.on_paint_button_click)
  636. self.ui.generate_paint_button.connect('activate', self.on_paint_button_click)
  637. def on_paint_button_click(self, *args):
  638. self.app.info("Click inside the desired polygon.")
  639. self.read_form()
  640. tooldia = self.options["painttooldia"]
  641. overlap = self.options["paintoverlap"]
  642. # Connection ID for the click event
  643. subscription = None
  644. # To be called after clicking on the plot.
  645. def doit(event):
  646. self.app.plotcanvas.mpl_disconnect(subscription)
  647. point = [event.xdata, event.ydata]
  648. poly = find_polygon(self.solid_geometry, point)
  649. # Initializes the new geometry object
  650. def gen_paintarea(geo_obj, app_obj):
  651. assert isinstance(geo_obj, FlatCAMGeometry)
  652. #assert isinstance(app_obj, App)
  653. cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap)
  654. geo_obj.solid_geometry = cp
  655. geo_obj.options["cnctooldia"] = tooldia
  656. name = self.options["name"] + "_paint"
  657. self.new_object("geometry", name, gen_paintarea)
  658. subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
  659. def on_generatecnc_button_click(self, *args):
  660. self.read_form()
  661. job_name = self.options["name"] + "_cnc"
  662. # Object initialization function for app.new_object()
  663. # RUNNING ON SEPARATE THREAD!
  664. def job_init(job_obj, app_obj):
  665. assert isinstance(job_obj, FlatCAMCNCjob)
  666. # Propagate options
  667. job_obj.options["tooldia"] = self.options["cnctooldia"]
  668. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  669. job_obj.z_cut = self.options["cutz"]
  670. job_obj.z_move = self.options["travelz"]
  671. job_obj.feedrate = self.options["feedrate"]
  672. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  673. # TODO: The tolerance should not be hard coded. Just for testing.
  674. job_obj.generate_from_geometry(self, tolerance=0.0005)
  675. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  676. job_obj.gcode_parse()
  677. # TODO: job_obj.create_geometry creates stuff that is not used.
  678. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  679. #job_obj.create_geometry()
  680. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  681. # To be run in separate thread
  682. def job_thread(app_obj):
  683. app_obj.new_object("cncjob", job_name, job_init)
  684. GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
  685. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  686. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  687. # Send to worker
  688. self.app.worker.add_task(job_thread, [self.app])
  689. def on_plot_cb_click(self, *args):
  690. if self.muted_ui:
  691. return
  692. self.read_form_item('plot')
  693. self.plot()
  694. def scale(self, factor):
  695. """
  696. Scales all geometry by a given factor.
  697. :param factor: Factor by which to scale the object's geometry/
  698. :type factor: float
  699. :return: None
  700. :rtype: None
  701. """
  702. if type(self.solid_geometry) == list:
  703. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  704. for g in self.solid_geometry]
  705. else:
  706. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  707. origin=(0, 0))
  708. def offset(self, vect):
  709. """
  710. Offsets all geometry by a given vector/
  711. :param vect: (x, y) vector by which to offset the object's geometry.
  712. :type vect: tuple
  713. :return: None
  714. :rtype: None
  715. """
  716. dx, dy = vect
  717. if type(self.solid_geometry) == list:
  718. self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
  719. for g in self.solid_geometry]
  720. else:
  721. self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
  722. def convert_units(self, units):
  723. factor = Geometry.convert_units(self, units)
  724. self.options['cutz'] *= factor
  725. self.options['travelz'] *= factor
  726. self.options['feedrate'] *= factor
  727. self.options['cnctooldia'] *= factor
  728. self.options['painttooldia'] *= factor
  729. self.options['paintmargin'] *= factor
  730. return factor
  731. def plot(self):
  732. """
  733. Plots the object into its axes. If None, of if the axes
  734. are not part of the app's figure, it fetches new ones.
  735. :return: None
  736. """
  737. # Does all the required setup and returns False
  738. # if the 'ptint' option is set to False.
  739. if not FlatCAMObj.plot(self):
  740. return
  741. # Make sure solid_geometry is iterable.
  742. try:
  743. _ = iter(self.solid_geometry)
  744. except TypeError:
  745. self.solid_geometry = [self.solid_geometry]
  746. for geo in self.solid_geometry:
  747. if type(geo) == Polygon:
  748. x, y = geo.exterior.coords.xy
  749. self.axes.plot(x, y, 'r-')
  750. for ints in geo.interiors:
  751. x, y = ints.coords.xy
  752. self.axes.plot(x, y, 'r-')
  753. continue
  754. if type(geo) == LineString or type(geo) == LinearRing:
  755. x, y = geo.coords.xy
  756. self.axes.plot(x, y, 'r-')
  757. continue
  758. if type(geo) == MultiPolygon:
  759. for poly in geo:
  760. x, y = poly.exterior.coords.xy
  761. self.axes.plot(x, y, 'r-')
  762. for ints in poly.interiors:
  763. x, y = ints.coords.xy
  764. self.axes.plot(x, y, 'r-')
  765. continue
  766. print "WARNING: Did not plot:", str(type(geo))
  767. #self.app.plotcanvas.auto_adjust_axes()
  768. GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)