FlatCAMObj.py 35 KB

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