FlatCAMObj.py 35 KB

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