FlatCAMObj.py 43 KB

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