FlatCAMObj.py 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432
  1. from PyQt4 import QtCore
  2. from copy import copy
  3. from ObjectUI import *
  4. import FlatCAMApp
  5. import inspect # TODO: For debugging only.
  6. from camlib import *
  7. from FlatCAMCommon import LoudDict
  8. from FlatCAMDraw import FlatCAMDraw
  9. ########################################
  10. ## FlatCAMObj ##
  11. ########################################
  12. class FlatCAMObj(QtCore.QObject):
  13. """
  14. Base type of objects handled in FlatCAM. These become interactive
  15. in the GUI, can be plotted, and their options can be modified
  16. by the user in their respective forms.
  17. """
  18. # Instance of the application to which these are related.
  19. # The app should set this value.
  20. app = None
  21. def __init__(self, name):
  22. """
  23. :param name: Name of the object given by the user.
  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.app.report_usage("obj_on_offset_button")
  57. self.read_form()
  58. vect = self.ui.offsetvector_entry.get_value()
  59. self.offset(vect)
  60. self.plot()
  61. def on_scale_button_click(self):
  62. self.app.report_usage("obj_on_scale_button")
  63. self.read_form()
  64. factor = self.ui.scale_entry.get_value()
  65. self.scale(factor)
  66. self.plot()
  67. def setup_axes(self, figure):
  68. """
  69. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  70. them to figure if not part of the figure. 4) Sets transparent
  71. background. 5) Sets 1:1 scale aspect ratio.
  72. :param figure: A Matplotlib.Figure on which to add/configure axes.
  73. :type figure: matplotlib.figure.Figure
  74. :return: None
  75. :rtype: None
  76. """
  77. if self.axes is None:
  78. FlatCAMApp.App.log.debug("setup_axes(): New axes")
  79. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  80. label=self.options["name"])
  81. elif self.axes not in figure.axes:
  82. FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
  83. self.axes.cla()
  84. figure.add_axes(self.axes)
  85. else:
  86. FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes")
  87. self.axes.cla()
  88. # Remove all decoration. The app's axes will have
  89. # the ticks and grid.
  90. self.axes.set_frame_on(False) # No frame
  91. self.axes.set_xticks([]) # No tick
  92. self.axes.set_yticks([]) # No ticks
  93. self.axes.patch.set_visible(False) # No background
  94. self.axes.set_aspect(1)
  95. def to_form(self):
  96. """
  97. Copies options to the UI form.
  98. :return: None
  99. """
  100. for option in self.options:
  101. self.set_form_item(option)
  102. def read_form(self):
  103. """
  104. Reads form into ``self.options``.
  105. :return: None
  106. :rtype: None
  107. """
  108. FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
  109. for option in self.options:
  110. self.read_form_item(option)
  111. def build_ui(self):
  112. """
  113. Sets up the UI/form for this object. Show the UI
  114. in the App.
  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. return True
  178. def serialize(self):
  179. """
  180. Returns a representation of the object as a dictionary so
  181. it can be later exported as JSON. Override this method.
  182. :return: Dictionary representing the object
  183. :rtype: dict
  184. """
  185. return
  186. def deserialize(self, obj_dict):
  187. """
  188. Re-builds an object from its serialized version.
  189. :param obj_dict: Dictionary representing a FlatCAMObj
  190. :type obj_dict: dict
  191. :return: None
  192. """
  193. return
  194. class FlatCAMGerber(FlatCAMObj, Gerber):
  195. """
  196. Represents Gerber code.
  197. """
  198. ui_type = GerberObjectUI
  199. def __init__(self, name):
  200. Gerber.__init__(self)
  201. FlatCAMObj.__init__(self, name)
  202. self.kind = "gerber"
  203. # The 'name' is already in self.options from FlatCAMObj
  204. # Automatically updates the UI
  205. self.options.update({
  206. "plot": True,
  207. "multicolored": False,
  208. "solid": False,
  209. "isotooldia": 0.016,
  210. "isopasses": 1,
  211. "isooverlap": 0.15,
  212. "combine_passes": True,
  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. """
  236. Maps options with GUI inputs.
  237. Connects GUI events to methods.
  238. :param ui: GUI object.
  239. :type ui: GerberObjectUI
  240. :return: None
  241. """
  242. FlatCAMObj.set_ui(self, ui)
  243. FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
  244. self.form_fields.update({
  245. "plot": self.ui.plot_cb,
  246. "multicolored": self.ui.multicolored_cb,
  247. "solid": self.ui.solid_cb,
  248. "isotooldia": self.ui.iso_tool_dia_entry,
  249. "isopasses": self.ui.iso_width_entry,
  250. "isooverlap": self.ui.iso_overlap_entry,
  251. "combine_passes":self.ui.combine_passes_cb,
  252. "cutouttooldia": self.ui.cutout_tooldia_entry,
  253. "cutoutmargin": self.ui.cutout_margin_entry,
  254. "cutoutgapsize": self.ui.cutout_gap_entry,
  255. "gaps": self.ui.gaps_radio,
  256. "noncoppermargin": self.ui.noncopper_margin_entry,
  257. "noncopperrounded": self.ui.noncopper_rounded_cb,
  258. "bboxmargin": self.ui.bbmargin_entry,
  259. "bboxrounded": self.ui.bbrounded_cb
  260. })
  261. assert isinstance(self.ui, GerberObjectUI)
  262. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  263. self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
  264. self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
  265. self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
  266. self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
  267. self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
  268. self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
  269. def on_generatenoncopper_button_click(self, *args):
  270. self.app.report_usage("gerber_on_generatenoncopper_button")
  271. self.read_form()
  272. name = self.options["name"] + "_noncopper"
  273. def geo_init(geo_obj, app_obj):
  274. assert isinstance(geo_obj, FlatCAMGeometry)
  275. bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
  276. if not self.options["noncopperrounded"]:
  277. bounding_box = bounding_box.envelope
  278. non_copper = bounding_box.difference(self.solid_geometry)
  279. geo_obj.solid_geometry = non_copper
  280. # TODO: Check for None
  281. self.app.new_object("geometry", name, geo_init)
  282. def on_generatebb_button_click(self, *args):
  283. self.app.report_usage("gerber_on_generatebb_button")
  284. self.read_form()
  285. name = self.options["name"] + "_bbox"
  286. def geo_init(geo_obj, app_obj):
  287. assert isinstance(geo_obj, FlatCAMGeometry)
  288. # Bounding box with rounded corners
  289. bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
  290. if not self.options["bboxrounded"]: # Remove rounded corners
  291. bounding_box = bounding_box.envelope
  292. geo_obj.solid_geometry = bounding_box
  293. self.app.new_object("geometry", name, geo_init)
  294. def on_generatecutout_button_click(self, *args):
  295. self.app.report_usage("gerber_on_generatecutout_button")
  296. self.read_form()
  297. name = self.options["name"] + "_cutout"
  298. def geo_init(geo_obj, app_obj):
  299. margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
  300. gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
  301. minx, miny, maxx, maxy = self.bounds()
  302. minx -= margin
  303. maxx += margin
  304. miny -= margin
  305. maxy += margin
  306. midx = 0.5 * (minx + maxx)
  307. midy = 0.5 * (miny + maxy)
  308. hgap = 0.5 * gap_size
  309. pts = [[midx - hgap, maxy],
  310. [minx, maxy],
  311. [minx, midy + hgap],
  312. [minx, midy - hgap],
  313. [minx, miny],
  314. [midx - hgap, miny],
  315. [midx + hgap, miny],
  316. [maxx, miny],
  317. [maxx, midy - hgap],
  318. [maxx, midy + hgap],
  319. [maxx, maxy],
  320. [midx + hgap, maxy]]
  321. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  322. [pts[6], pts[7], pts[10], pts[11]]],
  323. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  324. [pts[3], pts[4], pts[7], pts[8]]],
  325. "4": [[pts[0], pts[1], pts[2]],
  326. [pts[3], pts[4], pts[5]],
  327. [pts[6], pts[7], pts[8]],
  328. [pts[9], pts[10], pts[11]]]}
  329. cuts = cases[self.options['gaps']]
  330. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  331. # TODO: Check for None
  332. self.app.new_object("geometry", name, geo_init)
  333. def on_iso_button_click(self, *args):
  334. self.app.report_usage("gerber_on_iso_button")
  335. self.read_form()
  336. self.isolate()
  337. def follow(self, outname=None):
  338. """
  339. Creates a geometry object "following" the gerber paths.
  340. :return: None
  341. """
  342. default_name = self.options["name"] + "_follow"
  343. follow_name = outname or default_name
  344. def follow_init(follow_obj, app_obj):
  345. # Propagate options
  346. follow_obj.options["cnctooldia"] = self.options["isotooldia"]
  347. follow_obj.solid_geometry = self.solid_geometry
  348. app_obj.info("Follow geometry created: %s" % follow_obj.options["name"])
  349. # TODO: Do something if this is None. Offer changing name?
  350. self.app.new_object("geometry", follow_name, follow_init)
  351. def isolate(self, dia=None, passes=None, overlap=None, outname=None, combine=None):
  352. """
  353. Creates an isolation routing geometry object in the project.
  354. :param dia: Tool diameter
  355. :param passes: Number of tool widths to cut
  356. :param overlap: Overlap between passes in fraction of tool diameter
  357. :param outname: Base name of the output object
  358. :return: None
  359. """
  360. if dia is None:
  361. dia = self.options["isotooldia"]
  362. if passes is None:
  363. passes = int(self.options["isopasses"])
  364. if overlap is None:
  365. overlap = self.options["isooverlap"]
  366. if combine is None:
  367. combine = self.options["combine_passes"]
  368. else:
  369. combine = bool(combine)
  370. base_name = self.options["name"] + "_iso"
  371. base_name = outname or base_name
  372. def generate_envelope(offset, invert):
  373. # isolation_geometry produces an envelope that is going on the left of the geometry
  374. # (the copper features). To leave the least amount of burrs on the features
  375. # the tool needs to travel on the right side of the features (this is called conventional milling)
  376. # the first pass is the one cutting all of the features, so it needs to be reversed
  377. # the other passes overlap preceding ones and cut the left over copper. It is better for them
  378. # to cut on the right side of the left over copper i.e on the left side of the features.
  379. geom = self.isolation_geometry(offset)
  380. if invert:
  381. if type(geom) is MultiPolygon:
  382. pl = []
  383. for p in geom:
  384. pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
  385. geom = MultiPolygon(pl)
  386. elif type(geom) is Polygon:
  387. geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
  388. else:
  389. raise "Unexpected Geometry"
  390. return geom
  391. if combine:
  392. iso_name = base_name
  393. # TODO: This is ugly. Create way to pass data into init function.
  394. def iso_init(geo_obj, app_obj):
  395. # Propagate options
  396. geo_obj.options["cnctooldia"] = self.options["isotooldia"]
  397. geo_obj.solid_geometry = []
  398. for i in range(passes):
  399. offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
  400. geom = generate_envelope (offset, i == 0)
  401. geo_obj.solid_geometry.append(geom)
  402. app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
  403. # TODO: Do something if this is None. Offer changing name?
  404. self.app.new_object("geometry", iso_name, iso_init)
  405. else:
  406. for i in range(passes):
  407. offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
  408. if passes > 1:
  409. iso_name = base_name + str(i + 1)
  410. else:
  411. iso_name = base_name
  412. # TODO: This is ugly. Create way to pass data into init function.
  413. def iso_init(geo_obj, app_obj):
  414. # Propagate options
  415. geo_obj.options["cnctooldia"] = self.options["isotooldia"]
  416. geo_obj.solid_geometry = generate_envelope (offset, i == 0)
  417. app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
  418. # TODO: Do something if this is None. Offer changing name?
  419. self.app.new_object("geometry", iso_name, iso_init)
  420. def on_plot_cb_click(self, *args):
  421. if self.muted_ui:
  422. return
  423. self.read_form_item('plot')
  424. self.plot()
  425. def on_solid_cb_click(self, *args):
  426. if self.muted_ui:
  427. return
  428. self.read_form_item('solid')
  429. self.plot()
  430. def on_multicolored_cb_click(self, *args):
  431. if self.muted_ui:
  432. return
  433. self.read_form_item('multicolored')
  434. self.plot()
  435. def convert_units(self, units):
  436. """
  437. Converts the units of the object by scaling dimensions in all geometry
  438. and options.
  439. :param units: Units to which to convert the object: "IN" or "MM".
  440. :type units: str
  441. :return: None
  442. :rtype: None
  443. """
  444. factor = Gerber.convert_units(self, units)
  445. self.options['isotooldia'] *= factor
  446. self.options['cutoutmargin'] *= factor
  447. self.options['cutoutgapsize'] *= factor
  448. self.options['noncoppermargin'] *= factor
  449. self.options['bboxmargin'] *= factor
  450. def plot(self):
  451. FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
  452. # Does all the required setup and returns False
  453. # if the 'ptint' option is set to False.
  454. if not FlatCAMObj.plot(self):
  455. return
  456. geometry = self.solid_geometry
  457. # Make sure geometry is iterable.
  458. try:
  459. _ = iter(geometry)
  460. except TypeError:
  461. geometry = [geometry]
  462. if self.options["multicolored"]:
  463. linespec = '-'
  464. else:
  465. linespec = 'k-'
  466. if self.options["solid"]:
  467. for poly in geometry:
  468. # TODO: Too many things hardcoded.
  469. try:
  470. patch = PolygonPatch(poly,
  471. facecolor="#BBF268",
  472. edgecolor="#006E20",
  473. alpha=0.75,
  474. zorder=2)
  475. self.axes.add_patch(patch)
  476. except AssertionError:
  477. FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
  478. FlatCAMApp.App.log.warning(str(poly))
  479. else:
  480. for poly in geometry:
  481. x, y = poly.exterior.xy
  482. self.axes.plot(x, y, linespec)
  483. for ints in poly.interiors:
  484. x, y = ints.coords.xy
  485. self.axes.plot(x, y, linespec)
  486. self.app.plotcanvas.auto_adjust_axes()
  487. def serialize(self):
  488. return {
  489. "options": self.options,
  490. "kind": self.kind
  491. }
  492. class FlatCAMExcellon(FlatCAMObj, Excellon):
  493. """
  494. Represents Excellon/Drill code.
  495. """
  496. ui_type = ExcellonObjectUI
  497. def __init__(self, name):
  498. Excellon.__init__(self)
  499. FlatCAMObj.__init__(self, name)
  500. self.kind = "excellon"
  501. self.options.update({
  502. "plot": True,
  503. "solid": False,
  504. "drillz": -0.1,
  505. "travelz": 0.1,
  506. "feedrate": 5.0,
  507. # "toolselection": ""
  508. "tooldia": 0.1,
  509. "toolchange": False,
  510. "toolchangez": 1.0,
  511. "spindlespeed": None
  512. })
  513. # TODO: Document this.
  514. self.tool_cbs = {}
  515. # Attributes to be included in serialization
  516. # Always append to it because it carries contents
  517. # from predecessors.
  518. self.ser_attrs += ['options', 'kind']
  519. @staticmethod
  520. def merge(exc_list, exc_final):
  521. FlatCAMExcellon.merge(exc_list,exc_final,False)
  522. @staticmethod
  523. def merge(exc_list, exc_final, copy_options):
  524. """
  525. Merges(copy if used on one) the excellon of objects in exc_list into
  526. options have same like exc_final
  527. the geometry of geo_final.
  528. :param exc_list: List of FlatCAMExcellon Objects to join.
  529. :param exc_final: Destination FlatCAMExcellon object.
  530. :return: None
  531. """
  532. if type(exc_list) is not list:
  533. exc_list_real= list()
  534. exc_list_real.append(exc_list)
  535. else:
  536. exc_list_real=exc_list
  537. for exc in exc_list_real:
  538. # Expand lists
  539. if type(exc) is list:
  540. FlatCAMExcellon.merge(exc, exc_final, copy_options)
  541. # If not list, just append
  542. else:
  543. if copy_options is True:
  544. exc_final.options["plot"]=exc.options["plot"]
  545. exc_final.options["solid"]=exc.options["solid"]
  546. exc_final.options["drillz"]=exc.options["drillz"]
  547. exc_final.options["travelz"]=exc.options["travelz"]
  548. exc_final.options["feedrate"]=exc.options["feedrate"]
  549. exc_final.options["tooldia"]=exc.options["tooldia"]
  550. exc_final.options["toolchange"]=exc.options["toolchange"]
  551. exc_final.options["toolchangez"]=exc.options["toolchangez"]
  552. exc_final.options["spindlespeed"]=exc.options["spindlespeed"]
  553. for drill in exc.drills:
  554. point = Point(drill['point'].x,drill['point'].y)
  555. exc_final.drills.append({"point": point, "tool": drill['tool']})
  556. toolsrework=dict()
  557. max_numeric_tool=0
  558. for toolname in exc.tools.iterkeys():
  559. numeric_tool=int(toolname)
  560. if numeric_tool>max_numeric_tool:
  561. max_numeric_tool=numeric_tool
  562. toolsrework[exc.tools[toolname]['C']]=toolname
  563. #final as last becouse names from final tools will be used
  564. for toolname in exc_final.tools.iterkeys():
  565. numeric_tool=int(toolname)
  566. if numeric_tool>max_numeric_tool:
  567. max_numeric_tool=numeric_tool
  568. toolsrework[exc_final.tools[toolname]['C']]=toolname
  569. for toolvalues in toolsrework.iterkeys():
  570. if toolsrework[toolvalues] in exc_final.tools:
  571. if exc_final.tools[toolsrework[toolvalues]]!={"C": toolvalues}:
  572. exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues}
  573. else:
  574. exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues}
  575. exc_final.create_geometry()
  576. def build_ui(self):
  577. FlatCAMObj.build_ui(self)
  578. # Populate tool list
  579. n = len(self.tools)
  580. self.ui.tools_table.setColumnCount(2)
  581. self.ui.tools_table.setHorizontalHeaderLabels(['#', 'Diameter'])
  582. self.ui.tools_table.setRowCount(n)
  583. self.ui.tools_table.setSortingEnabled(False)
  584. i = 0
  585. for tool in self.tools:
  586. id = QtGui.QTableWidgetItem(tool)
  587. id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  588. self.ui.tools_table.setItem(i, 0, id) # Tool name/id
  589. dia = QtGui.QTableWidgetItem(str(self.tools[tool]['C']))
  590. dia.setFlags(QtCore.Qt.ItemIsEnabled)
  591. self.ui.tools_table.setItem(i, 1, dia) # Diameter
  592. i += 1
  593. # sort the tool diameter column
  594. self.ui.tools_table.sortItems(1)
  595. # all the tools are selected by default
  596. self.ui.tools_table.selectColumn(0)
  597. self.ui.tools_table.resizeColumnsToContents()
  598. self.ui.tools_table.resizeRowsToContents()
  599. self.ui.tools_table.horizontalHeader().setStretchLastSection(True)
  600. self.ui.tools_table.verticalHeader().hide()
  601. self.ui.tools_table.setSortingEnabled(True)
  602. def set_ui(self, ui):
  603. """
  604. Configures the user interface for this object.
  605. Connects options to form fields.
  606. :param ui: User interface object.
  607. :type ui: ExcellonObjectUI
  608. :return: None
  609. """
  610. FlatCAMObj.set_ui(self, ui)
  611. FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
  612. self.form_fields.update({
  613. "plot": self.ui.plot_cb,
  614. "solid": self.ui.solid_cb,
  615. "drillz": self.ui.cutz_entry,
  616. "travelz": self.ui.travelz_entry,
  617. "feedrate": self.ui.feedrate_entry,
  618. "tooldia": self.ui.tooldia_entry,
  619. "toolchange": self.ui.toolchange_cb,
  620. "toolchangez": self.ui.toolchangez_entry,
  621. "spindlespeed": self.ui.spindlespeed_entry
  622. })
  623. assert isinstance(self.ui, ExcellonObjectUI), \
  624. "Expected a ExcellonObjectUI, got %s" % type(self.ui)
  625. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  626. self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
  627. self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
  628. self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
  629. def get_selected_tools_list(self):
  630. """
  631. Returns the keys to the self.tools dictionary corresponding
  632. to the selections on the tool list in the GUI.
  633. :return: List of tools.
  634. :rtype: list
  635. """
  636. return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
  637. def generate_milling(self, tools=None, outname=None, tooldia=None):
  638. """
  639. Note: This method is a good template for generic operations as
  640. it takes it's options from parameters or otherwise from the
  641. object's options and returns a success, msg tuple as feedback
  642. for shell operations.
  643. :return: Success/failure condition tuple (bool, str).
  644. :rtype: tuple
  645. """
  646. # Get the tools from the list. These are keys
  647. # to self.tools
  648. if tools is None:
  649. tools = self.get_selected_tools_list()
  650. if outname is None:
  651. outname = self.options["name"] + "_mill"
  652. if tooldia is None:
  653. tooldia = self.options["tooldia"]
  654. if len(tools) == 0:
  655. self.app.inform.emit("Please select one or more tools from the list and try again.")
  656. return False, "Error: No tools."
  657. for tool in tools:
  658. if self.tools[tool]["C"] < tooldia:
  659. self.app.inform.emit("[warning] Milling tool is larger than hole size. Cancelled.")
  660. return False, "Error: Milling tool is larger than hole."
  661. def geo_init(geo_obj, app_obj):
  662. assert isinstance(geo_obj, FlatCAMGeometry), \
  663. "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
  664. app_obj.progress.emit(20)
  665. geo_obj.solid_geometry = []
  666. for hole in self.drills:
  667. if hole['tool'] in tools:
  668. geo_obj.solid_geometry.append(
  669. Point(hole['point']).buffer(self.tools[hole['tool']]["C"] / 2 -
  670. tooldia / 2).exterior
  671. )
  672. def geo_thread(app_obj):
  673. app_obj.new_object("geometry", outname, geo_init)
  674. app_obj.progress.emit(100)
  675. # Create a promise with the new name
  676. self.app.collection.promise(outname)
  677. # Send to worker
  678. self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
  679. return True, ""
  680. def on_generate_milling_button_click(self, *args):
  681. self.app.report_usage("excellon_on_create_milling_button")
  682. self.read_form()
  683. self.generate_milling()
  684. def on_create_cncjob_button_click(self, *args):
  685. self.app.report_usage("excellon_on_create_cncjob_button")
  686. self.read_form()
  687. # Get the tools from the list
  688. tools = self.get_selected_tools_list()
  689. if len(tools) == 0:
  690. self.app.inform.emit("Please select one or more tools from the list and try again.")
  691. return
  692. job_name = self.options["name"] + "_cnc"
  693. # Object initialization function for app.new_object()
  694. def job_init(job_obj, app_obj):
  695. assert isinstance(job_obj, FlatCAMCNCjob), \
  696. "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
  697. app_obj.progress.emit(20)
  698. job_obj.z_cut = self.options["drillz"]
  699. job_obj.z_move = self.options["travelz"]
  700. job_obj.feedrate = self.options["feedrate"]
  701. job_obj.spindlespeed = self.options["spindlespeed"]
  702. # There could be more than one drill size...
  703. # job_obj.tooldia = # TODO: duplicate variable!
  704. # job_obj.options["tooldia"] =
  705. tools_csv = ','.join(tools)
  706. job_obj.generate_from_excellon_by_tool(self, tools_csv,
  707. toolchange=self.options["toolchange"],
  708. toolchangez=self.options["toolchangez"])
  709. app_obj.progress.emit(50)
  710. job_obj.gcode_parse()
  711. app_obj.progress.emit(60)
  712. job_obj.create_geometry()
  713. app_obj.progress.emit(80)
  714. # To be run in separate thread
  715. def job_thread(app_obj):
  716. app_obj.new_object("cncjob", job_name, job_init)
  717. app_obj.progress.emit(100)
  718. # Create promise for the new name.
  719. self.app.collection.promise(job_name)
  720. # Send to worker
  721. # self.app.worker.add_task(job_thread, [self.app])
  722. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  723. def on_plot_cb_click(self, *args):
  724. if self.muted_ui:
  725. return
  726. self.read_form_item('plot')
  727. self.plot()
  728. def on_solid_cb_click(self, *args):
  729. if self.muted_ui:
  730. return
  731. self.read_form_item('solid')
  732. self.plot()
  733. def convert_units(self, units):
  734. factor = Excellon.convert_units(self, units)
  735. self.options['drillz'] *= factor
  736. self.options['travelz'] *= factor
  737. self.options['feedrate'] *= factor
  738. def plot(self):
  739. # Does all the required setup and returns False
  740. # if the 'ptint' option is set to False.
  741. if not FlatCAMObj.plot(self):
  742. return
  743. try:
  744. _ = iter(self.solid_geometry)
  745. except TypeError:
  746. self.solid_geometry = [self.solid_geometry]
  747. # Plot excellon (All polygons?)
  748. if self.options["solid"]:
  749. for geo in self.solid_geometry:
  750. patch = PolygonPatch(geo,
  751. facecolor="#C40000",
  752. edgecolor="#750000",
  753. alpha=0.75,
  754. zorder=3)
  755. self.axes.add_patch(patch)
  756. else:
  757. for geo in self.solid_geometry:
  758. x, y = geo.exterior.coords.xy
  759. self.axes.plot(x, y, 'r-')
  760. for ints in geo.interiors:
  761. x, y = ints.coords.xy
  762. self.axes.plot(x, y, 'g-')
  763. self.app.plotcanvas.auto_adjust_axes()
  764. class FlatCAMCNCjob(FlatCAMObj, CNCjob):
  765. """
  766. Represents G-Code.
  767. """
  768. ui_type = CNCObjectUI
  769. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  770. feedrate=3.0, z_cut=-0.002, tooldia=0.0,
  771. spindlespeed=None):
  772. FlatCAMApp.App.log.debug("Creating CNCJob object...")
  773. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  774. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia,
  775. spindlespeed=spindlespeed)
  776. FlatCAMObj.__init__(self, name)
  777. self.kind = "cncjob"
  778. self.options.update({
  779. "plot": True,
  780. "tooldia": 0.4 / 25.4, # 0.4mm in inches
  781. "append": "",
  782. "prepend": ""
  783. })
  784. # Attributes to be included in serialization
  785. # Always append to it because it carries contents
  786. # from predecessors.
  787. self.ser_attrs += ['options', 'kind']
  788. def set_ui(self, ui):
  789. FlatCAMObj.set_ui(self, ui)
  790. FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
  791. assert isinstance(self.ui, CNCObjectUI), \
  792. "Expected a CNCObjectUI, got %s" % type(self.ui)
  793. self.form_fields.update({
  794. "plot": self.ui.plot_cb,
  795. "tooldia": self.ui.tooldia_entry,
  796. "append": self.ui.append_text,
  797. "prepend": self.ui.prepend_text
  798. })
  799. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  800. self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
  801. self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
  802. def on_updateplot_button_click(self, *args):
  803. """
  804. Callback for the "Updata Plot" button. Reads the form for updates
  805. and plots the object.
  806. """
  807. self.read_form()
  808. self.plot()
  809. def on_exportgcode_button_click(self, *args):
  810. self.app.report_usage("cncjob_on_exportgcode_button")
  811. try:
  812. filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...",
  813. directory=self.app.defaults["last_folder"])
  814. except TypeError:
  815. filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...")
  816. preamble = str(self.ui.prepend_text.get_value())
  817. postamble = str(self.ui.append_text.get_value())
  818. self.export_gcode(filename, preamble=preamble, postamble=postamble)
  819. def export_gcode(self, filename, preamble='', postamble=''):
  820. f = open(filename, 'w')
  821. f.write(preamble + '\n' + self.gcode + "\n" + postamble)
  822. f.close()
  823. # Just for adding it to the recent files list.
  824. self.app.file_opened.emit("cncjob", filename)
  825. self.app.inform.emit("Saved to: " + filename)
  826. def on_plot_cb_click(self, *args):
  827. if self.muted_ui:
  828. return
  829. self.read_form_item('plot')
  830. self.plot()
  831. def plot(self):
  832. # Does all the required setup and returns False
  833. # if the 'ptint' option is set to False.
  834. if not FlatCAMObj.plot(self):
  835. return
  836. self.plot2(self.axes, tooldia=self.options["tooldia"])
  837. self.app.plotcanvas.auto_adjust_axes()
  838. def convert_units(self, units):
  839. factor = CNCjob.convert_units(self, units)
  840. FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
  841. self.options["tooldia"] *= factor
  842. class FlatCAMGeometry(FlatCAMObj, Geometry):
  843. """
  844. Geometric object not associated with a specific
  845. format.
  846. """
  847. ui_type = GeometryObjectUI
  848. @staticmethod
  849. def merge(geo_list, geo_final):
  850. """
  851. Merges the geometry of objects in geo_list into
  852. the geometry of geo_final.
  853. :param geo_list: List of FlatCAMGeometry Objects to join.
  854. :param geo_final: Destination FlatCAMGeometry object.
  855. :return: None
  856. """
  857. if geo_final.solid_geometry is None:
  858. geo_final.solid_geometry = []
  859. if type(geo_final.solid_geometry) is not list:
  860. geo_final.solid_geometry = [geo_final.solid_geometry]
  861. for geo in geo_list:
  862. # Expand lists
  863. if type(geo) is list:
  864. FlatCAMGeometry.merge(geo, geo_final)
  865. # If not list, just append
  866. else:
  867. geo_final.solid_geometry.append(geo.solid_geometry)
  868. # try: # Iterable
  869. # for shape in geo.solid_geometry:
  870. # geo_final.solid_geometry.append(shape)
  871. #
  872. # except TypeError: # Non-iterable
  873. # geo_final.solid_geometry.append(geo.solid_geometry)
  874. def __init__(self, name):
  875. FlatCAMObj.__init__(self, name)
  876. Geometry.__init__(self)
  877. self.kind = "geometry"
  878. self.options.update({
  879. "plot": True,
  880. "cutz": -0.002,
  881. "travelz": 0.1,
  882. "feedrate": 5.0,
  883. "spindlespeed": None,
  884. "cnctooldia": 0.4 / 25.4,
  885. "painttooldia": 0.0625,
  886. "paintoverlap": 0.15,
  887. "paintmargin": 0.01,
  888. "paintmethod": "standard",
  889. "multidepth": False,
  890. "depthperpass": 0.002
  891. })
  892. # Attributes to be included in serialization
  893. # Always append to it because it carries contents
  894. # from predecessors.
  895. self.ser_attrs += ['options', 'kind']
  896. def build_ui(self):
  897. FlatCAMObj.build_ui(self)
  898. def set_ui(self, ui):
  899. FlatCAMObj.set_ui(self, ui)
  900. FlatCAMApp.App.log.debug("FlatCAMGeometry.set_ui()")
  901. assert isinstance(self.ui, GeometryObjectUI), \
  902. "Expected a GeometryObjectUI, got %s" % type(self.ui)
  903. self.form_fields.update({
  904. "plot": self.ui.plot_cb,
  905. "cutz": self.ui.cutz_entry,
  906. "travelz": self.ui.travelz_entry,
  907. "feedrate": self.ui.cncfeedrate_entry,
  908. "spindlespeed": self.ui.cncspindlespeed_entry,
  909. "cnctooldia": self.ui.cnctooldia_entry,
  910. "painttooldia": self.ui.painttooldia_entry,
  911. "paintoverlap": self.ui.paintoverlap_entry,
  912. "paintmargin": self.ui.paintmargin_entry,
  913. "paintmethod": self.ui.paintmethod_combo,
  914. "multidepth": self.ui.mpass_cb,
  915. "depthperpass": self.ui.maxdepth_entry
  916. })
  917. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  918. self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
  919. self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
  920. def on_paint_button_click(self, *args):
  921. self.app.report_usage("geometry_on_paint_button")
  922. self.app.info("Click inside the desired polygon.")
  923. self.read_form()
  924. tooldia = self.options["painttooldia"]
  925. overlap = self.options["paintoverlap"]
  926. # Connection ID for the click event
  927. subscription = None
  928. # To be called after clicking on the plot.
  929. def doit(event):
  930. self.app.info("Painting polygon...")
  931. self.app.plotcanvas.mpl_disconnect(subscription)
  932. point = [event.xdata, event.ydata]
  933. self.paint_poly(point, tooldia, overlap)
  934. subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
  935. def paint_poly(self, inside_pt, tooldia, overlap):
  936. # Which polygon.
  937. #poly = find_polygon(self.solid_geometry, inside_pt)
  938. poly = self.find_polygon(inside_pt)
  939. # No polygon?
  940. if poly is None:
  941. self.app.log.warning('No polygon found.')
  942. self.app.inform.emit('[warning] No polygon found.')
  943. return
  944. proc = self.app.proc_container.new("Painting polygon.")
  945. name = self.options["name"] + "_paint"
  946. # Initializes the new geometry object
  947. def gen_paintarea(geo_obj, app_obj):
  948. assert isinstance(geo_obj, FlatCAMGeometry), \
  949. "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
  950. #assert isinstance(app_obj, App)
  951. if self.options["paintmethod"] == "seed":
  952. cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]),
  953. tooldia, overlap=overlap)
  954. else:
  955. cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]),
  956. tooldia, overlap=overlap)
  957. geo_obj.solid_geometry = list(cp.get_objects())
  958. geo_obj.options["cnctooldia"] = tooldia
  959. self.app.inform.emit("Done.")
  960. def job_thread(app_obj):
  961. try:
  962. app_obj.new_object("geometry", name, gen_paintarea)
  963. except Exception as e:
  964. proc.done()
  965. raise e
  966. proc.done()
  967. self.app.inform.emit("Polygon Paint started ...")
  968. # Promise object with the new name
  969. self.app.collection.promise(name)
  970. # Background
  971. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  972. def on_generatecnc_button_click(self, *args):
  973. self.app.report_usage("geometry_on_generatecnc_button")
  974. self.read_form()
  975. self.generatecncjob()
  976. def generatecncjob(self,
  977. z_cut=None,
  978. z_move=None,
  979. feedrate=None,
  980. tooldia=None,
  981. outname=None,
  982. spindlespeed=None,
  983. multidepth=None,
  984. depthperpass=None):
  985. """
  986. Creates a CNCJob out of this Geometry object. The actual
  987. work is done by the target FlatCAMCNCjob object's
  988. `generate_from_geometry_2()` method.
  989. :param z_cut: Cut depth (negative)
  990. :param z_move: Hight of the tool when travelling (not cutting)
  991. :param feedrate: Feed rate while cutting
  992. :param tooldia: Tool diameter
  993. :param outname: Name of the new object
  994. :param spindlespeed: Spindle speed (RPM)
  995. :return: None
  996. """
  997. outname = outname if outname is not None else self.options["name"] + "_cnc"
  998. z_cut = z_cut if z_cut is not None else self.options["cutz"]
  999. z_move = z_move if z_move is not None else self.options["travelz"]
  1000. feedrate = feedrate if feedrate is not None else self.options["feedrate"]
  1001. tooldia = tooldia if tooldia is not None else self.options["cnctooldia"]
  1002. multidepth = multidepth if multidepth is not None else self.options["multidepth"]
  1003. depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"]
  1004. # To allow default value to be "" (optional in gui) and translate to None
  1005. # if not isinstance(spindlespeed, int):
  1006. # if isinstance(self.options["spindlespeed"], int) or \
  1007. # isinstance(self.options["spindlespeed"], float):
  1008. # spindlespeed = int(self.options["spindlespeed"])
  1009. # else:
  1010. # spindlespeed = None
  1011. if spindlespeed is None:
  1012. # int or None.
  1013. spindlespeed = self.options['spindlespeed']
  1014. # Object initialization function for app.new_object()
  1015. # RUNNING ON SEPARATE THREAD!
  1016. def job_init(job_obj, app_obj):
  1017. assert isinstance(job_obj, FlatCAMCNCjob), \
  1018. "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
  1019. # Propagate options
  1020. job_obj.options["tooldia"] = tooldia
  1021. app_obj.progress.emit(20)
  1022. job_obj.z_cut = z_cut
  1023. job_obj.z_move = z_move
  1024. job_obj.feedrate = feedrate
  1025. job_obj.spindlespeed = spindlespeed
  1026. app_obj.progress.emit(40)
  1027. # TODO: The tolerance should not be hard coded. Just for testing.
  1028. job_obj.generate_from_geometry_2(self,
  1029. multidepth=multidepth,
  1030. depthpercut=depthperpass,
  1031. tolerance=0.0005)
  1032. app_obj.progress.emit(50)
  1033. job_obj.gcode_parse()
  1034. app_obj.progress.emit(80)
  1035. # To be run in separate thread
  1036. def job_thread(app_obj):
  1037. with self.app.proc_container.new("Generating CNC Job."):
  1038. app_obj.new_object("cncjob", outname, job_init)
  1039. app_obj.inform.emit("CNCjob created: %s" % outname)
  1040. app_obj.progress.emit(100)
  1041. # Create a promise with the name
  1042. self.app.collection.promise(outname)
  1043. # Send to worker
  1044. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1045. def on_plot_cb_click(self, *args): # TODO: args not needed
  1046. if self.muted_ui:
  1047. return
  1048. self.read_form_item('plot')
  1049. self.plot()
  1050. def scale(self, factor):
  1051. """
  1052. Scales all geometry by a given factor.
  1053. :param factor: Factor by which to scale the object's geometry/
  1054. :type factor: float
  1055. :return: None
  1056. :rtype: None
  1057. """
  1058. if type(self.solid_geometry) == list:
  1059. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  1060. for g in self.solid_geometry]
  1061. else:
  1062. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  1063. origin=(0, 0))
  1064. def offset(self, vect):
  1065. """
  1066. Offsets all geometry by a given vector/
  1067. :param vect: (x, y) vector by which to offset the object's geometry.
  1068. :type vect: tuple
  1069. :return: None
  1070. :rtype: None
  1071. """
  1072. dx, dy = vect
  1073. def translate_recursion(geom):
  1074. if type(geom) == list:
  1075. geoms=list()
  1076. for local_geom in geom:
  1077. geoms.append(translate_recursion(local_geom))
  1078. return geoms
  1079. else:
  1080. return affinity.translate(geom, xoff=dx, yoff=dy)
  1081. self.solid_geometry=translate_recursion(self.solid_geometry)
  1082. def convert_units(self, units):
  1083. factor = Geometry.convert_units(self, units)
  1084. self.options['cutz'] *= factor
  1085. self.options['travelz'] *= factor
  1086. self.options['feedrate'] *= factor
  1087. self.options['cnctooldia'] *= factor
  1088. self.options['painttooldia'] *= factor
  1089. self.options['paintmargin'] *= factor
  1090. return factor
  1091. def plot_element(self, element):
  1092. try:
  1093. for sub_el in element:
  1094. self.plot_element(sub_el)
  1095. except TypeError: # Element is not iterable...
  1096. if type(element) == Polygon:
  1097. x, y = element.exterior.coords.xy
  1098. self.axes.plot(x, y, 'r-')
  1099. for ints in element.interiors:
  1100. x, y = ints.coords.xy
  1101. self.axes.plot(x, y, 'r-')
  1102. return
  1103. if type(element) == LineString or type(element) == LinearRing:
  1104. x, y = element.coords.xy
  1105. self.axes.plot(x, y, 'r-')
  1106. return
  1107. FlatCAMApp.App.log.warning("Did not plot:" + str(type(element)))
  1108. def plot(self):
  1109. """
  1110. Plots the object into its axes. If None, of if the axes
  1111. are not part of the app's figure, it fetches new ones.
  1112. :return: None
  1113. """
  1114. # Does all the required setup and returns False
  1115. # if the 'ptint' option is set to False.
  1116. if not FlatCAMObj.plot(self):
  1117. return
  1118. # Make sure solid_geometry is iterable.
  1119. # TODO: This method should not modify the object !!!
  1120. # try:
  1121. # _ = iter(self.solid_geometry)
  1122. # except TypeError:
  1123. # if self.solid_geometry is None:
  1124. # self.solid_geometry = []
  1125. # else:
  1126. # self.solid_geometry = [self.solid_geometry]
  1127. #
  1128. # for geo in self.solid_geometry:
  1129. #
  1130. # if type(geo) == Polygon:
  1131. # x, y = geo.exterior.coords.xy
  1132. # self.axes.plot(x, y, 'r-')
  1133. # for ints in geo.interiors:
  1134. # x, y = ints.coords.xy
  1135. # self.axes.plot(x, y, 'r-')
  1136. # continue
  1137. #
  1138. # if type(geo) == LineString or type(geo) == LinearRing:
  1139. # x, y = geo.coords.xy
  1140. # self.axes.plot(x, y, 'r-')
  1141. # continue
  1142. #
  1143. # if type(geo) == MultiPolygon:
  1144. # for poly in geo:
  1145. # x, y = poly.exterior.coords.xy
  1146. # self.axes.plot(x, y, 'r-')
  1147. # for ints in poly.interiors:
  1148. # x, y = ints.coords.xy
  1149. # self.axes.plot(x, y, 'r-')
  1150. # continue
  1151. #
  1152. # FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
  1153. self.plot_element(self.solid_geometry)
  1154. self.app.plotcanvas.auto_adjust_axes()