FlatCAMObj.py 38 KB

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