FlatCAMObj.py 62 KB

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