FlatCAMObj.py 53 KB

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