FlatCAMObj.py 35 KB

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